diff --git a/README.md b/README.md index 99e9053..d7582e9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,20 @@ +
+ # screenshot-rename -> A Claude Code skill that turns a folder of timestamp-named screenshots into a folder of human-readable, searchable filenames — using parallel Haiku vision agents. +*A Claude Code skill that turns a folder of timestamp-named screenshots*
+*into a folder of human-readable, searchable filenames —*
+*using parallel Haiku vision agents.* -**Documentation:** [pages.tojo.team/cardinale/screenshot-rename](https://pages.tojo.team/cardinale/screenshot-rename/) — full homepage with the workflow, gotchas, and use-case worked examples. +[**Homepage**](https://pages.tojo.team/cardinale/screenshot-rename/)  ·  [SKILL.md](SKILL.md)  ·  [pipeline.py](pipeline.py)  ·  [MIT](LICENSE) + +
+ +Before / after — five real renames, including U+202F handling and user-keyword preservation + +
+ +
``` CleanShot 2026-04-15 at 09.14.07.png @@ -10,28 +22,41 @@ CleanShot 2026-04-15 at 09.14.07.png CleanShot - Shamel Studio Affiliate Referral Code Modal - 2026-04-15 at 09.14.07.png ``` -Built for [CleanShot](https://cleanshot.com)-style screenshot folders, but works on any directory of `.png` / `.gif` / `.mp4` / `.pdf` files named only by timestamp. +Built for [CleanShot](https://cleanshot.com)-style screenshot folders, but works on any directory of `.png` / `.gif` / `.mp4` / `.pdf` files named only by timestamp. Recognizes both **`CleanShot ...`** and Apple **`Screenshot ...`** filenames in the same pass, preserves any leading user-typed keyword prefix, and is safe to re-run on a folder that's already partially renamed. ## Highlights - **Parallel** — describes ~200 files in 3 minutes using 10 concurrent Haiku subagents. -- **Safe** — pre-builds the full rename plan in memory, validates uniqueness and target collisions, then renames atomically with file-count audit. Designed after losing 4 files to a `mv` overwrite during prototyping. -- **Handles video/PDF** — extracts the first frame so vision agents can describe them. -- **Resizes for the vision tool** — Retina screenshots exceed Read's image cap; pipeline downsamples to 1568px max. +- **Safe** — pre-builds the full rename plan in memory, validates uniqueness and target collisions, then renames atomically with a file-count audit. Designed after losing 4 files to a `mv` overwrite during prototyping. +- **Multi-prefix** — same pipeline handles `CleanShot ...`, Apple `Screenshot ...`, and files with hand-typed leading keywords (e.g. `jojo travel CleanShot ...`). +- **Idempotent** — re-running on a folder skips files already in the renamed `App - Description - timestamp.ext` form. No description-stacking. +- **Handles video / PDF** — extracts the first frame so vision agents can describe them. +- **Resizes for the vision tool** — Retina screenshots exceed Read's image cap; pipeline downsamples to 1568 px max. + +## A session, end to end + +
+ +Claude Code session — user prompt, parallel Haiku fan-out across ten batches, receipt of the run + +
+ +The skill activates when you ask Claude conversationally. Behind the scenes it preps the folder, fans out ten Haiku agents in a single round-trip, validates the resulting plan, then applies the renames in a single Python pass with a file-count audit at the end. ## Installation -This is a Claude Code skill. Drop the `screenshot-rename/` directory into `~/.claude/skills/`: +This is a Claude Code skill. Drop the repo into `~/.claude/skills/`: ```bash -git clone https://gitea.tojo.team/cardinale/screenshot-rename.git ~/.claude/skills/screenshot-rename +git clone https://gitea.tojo.team/cardinale/screenshot-rename.git \ + ~/.claude/skills/screenshot-rename ``` In your next Claude Code session, ask: > rename all the cleanshot files in `~/Documents/Screenshots/` based on their content -The skill will activate automatically. +The skill will activate automatically from its description. ## Usage from the command line @@ -52,19 +77,22 @@ python3 pipeline.py plan --src "/path/to/folder" python3 pipeline.py execute --src "/path/to/folder" ``` -The dispatch step (#2) currently requires a Claude Code session. See [Roadmap](#roadmap). +The dispatch step (#2) currently requires a Claude Code session. -## Documentation +## What the parser accepts -- **Homepage with worked examples:** [docs/index.html](docs/index.html) -- **Full skill spec:** [SKILL.md](SKILL.md) -- **Pipeline source:** [pipeline.py](pipeline.py) +| Form | Recognized | Becomes | +|---|---|---| +| `CleanShot 2026-MM-DD at HH.MM.SS.png` | yes | `CleanShot - - 2026-MM-DD at HH.MM.SS.png` | +| `Screenshot 2026-MM-DD at H.MM.SS PM.png` (with U+202F) | yes — U+202F normalized to ASCII space | `Screenshot - - 2026-MM-DD at H.MM.SS PM.png` | +| ` CleanShot 2026-MM-DD at HH.MM.SS.png` | yes — keywords title-cased and prepended to the AI description | `CleanShot - - 2026-MM-DD at HH.MM.SS.png` | +| `App - - 2026-MM-DD at HH.MM.SS.png` | already renamed → **skipped** | (unchanged) | ## The gotchas this skill encodes This skill exists because every one of these caused real damage during development: -1. The macOS `Read` tool has an image-size cap. Resize first. +1. The `Read` tool has an image-size cap. Resize first. 2. Vision can't read `.mp4` or multi-page `.pdf` directly. Extract a frame. 3. **Bash regex `[[ =~ ]]` does NOT populate `BASH_REMATCH` in zsh.** Targets become empty. Loops collide on the same filename. Files vanish. Use Python for any filename mutation. 4. `mv` silently overwrites. Use `mv -n` or `os.rename` with explicit pre-existence check. @@ -74,9 +102,20 @@ This skill exists because every one of these caused real damage during developme 8. `Bash run_in_background` may exit early on `while read` loops. Run renames foreground via Python. 9. Haiku occasionally returns the resized `.jpg` filename instead of the original `.png`. Validator must try alt extensions. 10. Always preserve the original `.mp4` / `.pdf` extension — describe via the extracted frame, rename the source. +11. **macOS `Screenshot` filenames contain U+202F (NARROW NO-BREAK SPACE)** before AM/PM. Haiku echoes it as ASCII space, so a verbatim filename lookup misses every Screenshot file. Normalize on both sides of the lookup; emit ASCII space in the new name. +12. **Re-running is only safe if the parser skips already-renamed files.** Detect `^App - .+ - timestamp.ext$` and exclude. +13. **Leading user-typed keyword prefix is signal, not noise.** Title-case the keywords and prepend them to the AI description before assembling the new name. The full discussion is in [SKILL.md](SKILL.md#the-critical-gotchas-every-one-of-these-caused-real-pain). +## Real-world impact + +| Run | Files | What happened | +|---|---|---| +| 1 | 196 CleanShot | Lost 4 to the bash-regex-in-zsh gotcha (#3). | +| 2 | 196 CleanShot | Rebuilt with Python and `mv -n` — 189 renamed cleanly, zero loss. | +| 3 | 20 mixed (CleanShot + Apple Screenshot + one user-prefixed) | First plan attempt dropped every Screenshot file with a misleading `NO_DESC` error. Diagnosed the U+202F gotcha (#11) via `repr()` of the live filename. After adding U+202F normalization, multi-prefix support, and keyword preservation — all 20 renamed in one pass. | + ## Roadmap - Direct Anthropic API mode (no Claude Code session required) — needs `ANTHROPIC_API_KEY` diff --git a/assets/before-after.html b/assets/before-after.html new file mode 100644 index 0000000..80ccab9 --- /dev/null +++ b/assets/before-after.html @@ -0,0 +1,332 @@ + + + + +screenshot-rename · before / after + + + + + + +
+ +
Sheet · 01 / 02
+ +
+
screenshot-rename · plate i
+

Before & after

+
A real run, twenty files2026·05·04
+
+ +
+ + Filename · before + Filename · after +
+ +
+ +
+ + CleanShot 2026-04-15 at 09.14.07.png + CleanShot - Shamel Studio Affiliate Referral Code Modal - 2026-04-15 at 09.14.07.png +
+ +
+ + Screenshot 2026-03-12 at 11.42.18PM.png + Screenshot - Synqora Modal Architecture Decisions Diagram - 2026-03-12 at 11.42.18 PM.png + U+202F · Apple's narrow no-break space, normalized in plan + execute +
+ +
+ + jojo travel CleanShot 2026-03-31 at 10.52.34.png + CleanShot - Jojo Travel Flight Australia Melbourne Flightaware Map Route - 2026-03-31 at 10.52.34.png + User keyword · hand-typed prefix is preserved and title-cased into the new name +
+ +
+ + CleanShot 2026-01-26 at 17.38.30.png + CleanShot - Claude Code Hitting Limits Conversation Transcript - 2026-01-26 at 17.38.30.png +
+ +
+ + CleanShot 2026-02-09 at 07.29.35.png + CleanShot - Claude Code Skill Rust Refactor Diff View - 2026-02-09 at 07.29.35.png +
+ +
+ +
+
Files in run20  files
+
Renamed20  ✓
+
Prefix variants3  CleanShot · Screenshot · keyword
+
Idempotent rerunsafe  already-renamed skipped
+
+ +
Set in Fraunces & JetBrains Mono. Plate i of ii.
+ +
+ + diff --git a/assets/before-after.png b/assets/before-after.png new file mode 100644 index 0000000..8ef97a5 Binary files /dev/null and b/assets/before-after.png differ diff --git a/assets/session.html b/assets/session.html new file mode 100644 index 0000000..350f7d1 --- /dev/null +++ b/assets/session.html @@ -0,0 +1,340 @@ + + + + +screenshot-rename · session + + + + + + +
+ +
Sheet · 02 / 02
+ +
+
screenshot-rename · plate ii
+

A session, end to end

+
196 files · 10 agents · ~3 minclaude code · 2026·05·04
+
+ +
+ + you +
rename all the cleanshots in ~/Documents/Screenshots/ based on their content.
+ + claude +
I'll prep the folder, batch into ten files of 19, dispatch parallel Haiku agents, then plan and execute. Resizing Retinas first; extracting frames from .mp4 and .pdf.
+ + bash +
# 1 · prep
+ python3 pipeline.py prep --src ~/Documents/Screenshots/
+ → 196 source files · 9 mp4/pdf frames · resized to 1568 px · 10 batches written +
+ + fan-out +
+
+
haikubatch-0119/19
+
haikubatch-0219/19
+
haikubatch-0319/19
+
haikubatch-0419/19
+
haikubatch-0519/19
+
haikubatch-0619/19
+
haikubatch-0719/19
+
haikubatch-0819/19
+
haikubatch-0919/19
+
haikubatch-1010/10
+
+
+ + bash +
# 2 · plan, then execute
+ python3 pipeline.py plan --src ~/Documents/Screenshots/  → 189 renames · 0 errors · 0 collisions
+ python3 pipeline.py execute --src ~/Documents/Screenshots/  → audit 195 = 195 · 189 ✓ +
+ +
+ +
+
+ Agents in flight + 10parallel +
+
+ Renames committed + 189/ 189 planned +
+
+ File-count audit + 195 = 195 +
+
+ Files lost + 0zero overwrites +
+
+ +
Set in Fraunces & JetBrains Mono. Plate ii of ii.
+ +
+ + diff --git a/assets/session.png b/assets/session.png new file mode 100644 index 0000000..05cb373 Binary files /dev/null and b/assets/session.png differ diff --git a/docs/index.html b/docs/index.html index d7dd12d..0f96404 100644 --- a/docs/index.html +++ b/docs/index.html @@ -657,6 +657,132 @@ footer .colophon { max-width: 50ch; } +/* ───── Plate (image + caption) ───────────────── */ + +.plate { + margin: clamp(56px, 7vw, 96px) 0 0; + text-align: center; +} +.plate figure { + margin: 0; + display: inline-block; + max-width: 100%; + position: relative; +} +.plate figure::before, .plate figure::after { + content: ""; + position: absolute; + width: 56px; height: 1px; + background: var(--accent); + top: 50%; +} +.plate figure::before { left: -68px; } +.plate figure::after { right: -68px; } +@media (max-width: 1100px) { + .plate figure::before, .plate figure::after { display: none; } +} +.plate img { + display: block; + width: 100%; + max-width: 1180px; + height: auto; + border: 1px solid var(--rule); + background: var(--paper); + box-shadow: + 0 28px 56px -34px rgba(28,25,22,.30), + 0 6px 14px -8px rgba(28,25,22,.18); +} +.plate figcaption { + font-family: var(--mono); + font-size: 11px; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ink-mute); + margin-top: 22px; + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + flex-wrap: wrap; +} +.plate figcaption .dot { color: var(--accent); } +.plate figcaption .b { color: var(--ink); } + +/* ───── Additions (what's new this revision) ──── */ + +.additions { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + border: 1px solid var(--rule); + background: var(--paper-2); + margin-bottom: 32px; +} +@media (max-width: 920px) { .additions { grid-template-columns: 1fr; } } +.add { + padding: 32px 28px 28px; + border-right: 1px solid var(--rule); + position: relative; +} +.add:last-child { border-right: none; } +@media (max-width: 920px) { + .add { border-right: none; border-bottom: 1px solid var(--rule); } + .add:last-child { border-bottom: none; } +} +.add .badge { + font-family: var(--mono); + font-size: 10.5px; + letter-spacing: 0.20em; + color: var(--accent); + text-transform: uppercase; + margin-bottom: 14px; + display: flex; + align-items: center; + gap: 10px; +} +.add .badge .num { + display: inline-block; + border: 1px solid var(--accent); + border-radius: 50%; + width: 22px; height: 22px; + text-align: center; line-height: 20px; + font-size: 10px; +} +.add h3 { + font-family: var(--serif); + font-style: italic; + font-weight: 500; + font-variation-settings: "opsz" 60; + font-size: 26px; + line-height: 1.15; + margin: 0 0 12px; + letter-spacing: -0.012em; + color: var(--ink); +} +.add p { + font-size: 15px; + color: var(--ink-soft); + line-height: 1.55; + margin: 0 0 14px; +} +.add code { + font-family: var(--mono); + font-size: 12px; + color: var(--accent-deep); + background: rgba(122,31,61,.08); + padding: 1px 6px; +} +.add .glyph { + position: absolute; + top: 24px; right: 22px; + font-family: var(--serif); + font-style: italic; + font-size: 42px; + color: var(--accent-soft); + line-height: 1; + pointer-events: none; +} + /* ───── Reveal animation ─────────────────────── */ @media (prefers-reduced-motion: no-preference) { @@ -686,8 +812,9 @@ footer .colophon { @@ -837,14 +964,29 @@ footer .colophon {

The original timestamp survives unchanged. Sorting still works. The description sits between, set off by em-dashes.

+ +
+
+ Plate i — five real renames, including U+202F handling and user-keyword preservation +
+ + Plate i + · + Five real renames + · + CleanShot · Apple Screenshot · user-keyword + +
+
+
- +
-
screenshot-rename · run log · 2026-05-04
+
screenshot-rename · run ii · 2026-05-04 · 196 files
source files196
resized to 1568px196
frames extracted (mp4 / pdf)9
@@ -860,10 +1002,74 @@ footer .colophon {
+ +
+
+ +

Three additions, one mixed run.

+

+ A second batch of files surfaced cases the first run never hit: Apple's own + Screenshot filenames, user-typed keyword prefixes, and folders where some + files were already renamed. The pipeline now handles all three in a single pass + — and the parser learned a new gotcha along the way. +

+ +
+
+ a. +
imulti-prefix
+

CleanShot and Screenshot, in one run.

+

+ The parser now accepts both CleanShot 2026-MM-DD at HH.MM.SS.png and + Apple's Screenshot 2026-MM-DD at H.MM.SS PM.png. Mixed folders no + longer need two passes — the manifest builder picks up either prefix. +

+
+ +
+ b. +
iikeyword preservation
+

Hand-typed prefixes survive.

+

+ A file named jojo travel CleanShot 2026-...png carries user knowledge + the AI doesn't have. The parser strips the keyword phrase, title-cases it, and + prepends it to the AI description — so the new filename reads + Jojo Travel Flight Australia Melbourne Flightaware Map Route. +

+
+ +
+ c. +
iiiidempotent re-runs
+

Re-running won't stack descriptions.

+

+ The parser now detects names already in the + App - Description - timestamp.ext form and excludes them from the + manifest. You can re-run the skill on a partially-renamed folder without the + name growing on every pass. +

+
+
+ +
+
screenshot-rename · run iii · 2026-05-04 · mixed prefixes
+
source files20
+
CleanShot14
+
Apple Screenshot (with U+202F)5
+
user-prefixed1
+
already-renamed (skipped)0
+
plan validated20 renames · 0 errors
+
file count before / after20 = 20
+
renames committed20 ✓
+
files lost0 ✓
+
+
+
+
- +

Every rule below was paid for.

During development, four files were destroyed by a one-line bash mistake. @@ -880,6 +1086,9 @@ footer .colophon {

  • Run renames foreground.Bash run_in_background with while read may exit early with no progress. Run via Python in the same shell — os.rename is just a syscall.
  • Validate the filename column.Haiku occasionally returns the resized .jpg name instead of the original .png. The plan-builder must try alternate extensions when the claimed source isn't found.
  • Preserve the original extension.The pipeline reads from a resized JPEG but renames the original .mp4 / .pdf. Write the source extension back into the new name.
  • +
  • Apple Screenshot files use U+202F.The narrow no-break space sits between the seconds and AM/PM. Haiku echoes it as ASCII space, the lookup misses, and every Screenshot file is dropped from the plan with a misleading NO_DESC error. Normalize on both sides; emit ASCII space in the new name.
  • +
  • Re-runs must skip already-renamed files.Without an ^App - .+ - timestamp.ext$ exclusion rule the parser will pile a second AI description into every name on every run. The pipeline detects and excludes them.
  • +
  • User-typed keyword prefix is signal.A name like jojo travel CleanShot ... carries knowledge the AI doesn't have. Strip the keyword phrase, title-case it, and prepend it to the description before assembly. Don't drop it.
  • @@ -887,7 +1096,7 @@ footer .colophon {
    - +

    What this looks like in practice.

    The skill earns its keep when "Spotlight will find it" stops being true. Four scenarios where it has. @@ -929,10 +1138,38 @@ footer .colophon {

    + +
    +
    + +

    What it looks like when you ask.

    +

    + The skill is conversational at the top, mechanical underneath. You ask in plain + English; ten Haiku agents fan out in a single round-trip; Python validates the + plan and applies it under the file-count audit. The whole thing fits on one page. +

    + +
    +
    + Plate ii — Claude Code session showing user prompt, parallel Haiku fan-out across ten batches, and the run receipt +
    + + Plate ii + · + A session, end to end + · + 10 agents · ~3 min · zero loss + +
    +
    +
    +
    +
    +
    - +

    Three commands, one folder.

    The skill installs as a Claude Code skill. Once cloned into ~/.claude/skills/, it