@mevdragon/vidfarm-devcli 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.director.md CHANGED
@@ -236,7 +236,7 @@ Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/
236
236
  ```
237
237
 
238
238
  - **Poll** `GET /clips/scan/:scanId` until the source `status` is `complete` (or `failed` — crashed pipelines are reconciled to `failed`, pollers never spin).
239
- - **Results**: `GET /clips/feed?source=<source_video_id>` (or the whole library), hybrid search via `POST /clips/search` `{ query: "someone looks confused" }`, per-clip download at `GET /clips/:clipId/download`. Clips carry taxonomy tags, a description, a transcript, an `aspect` field when cropped, and (with a gemini/openai key) a semantic embedding.
239
+ - **Results**: `GET /clips/feed?source=<source_video_id>` (or the whole library), hybrid search via `POST /clips/search` `{ query: "someone looks confused" }`, per-clip download at `GET /clips/:clipId/download`. Clips carry taxonomy tags, a description, a transcript, an `aspect` field when cropped, the hunt's `tracer`, and (with a gemini/openai key) a semantic embedding.
240
240
  - **Billing** — **AWS compute only** (`clip_scan_lambda` GB-seconds + Step Functions transitions, a fraction of a cent for typical hunts; the 202 response includes a `compute_estimate`). The AI tagging/refine runs on **your saved provider key (BYOK)** and is never wallet-billed. Submission is wallet-gated (402 when empty).
241
241
  - **The original stays temporary** — URL-ingested and uploaded sources live ONLY in your temp folder (`users/…/temporary/clip-sources/…`) with a hard **30-day TTL** (auto-deleted from S3 + the temp-file list). The hunted clips themselves are durable.
242
242
  - **Captions rule** — "no captions / no on-screen text" hunts are handled by scene SELECTION (the refine pass drops text-heavy scenes). **Never run GhostCut caption removal on a long-form source** (it is hard-capped at ~15 minutes); to actually erase burned-in text, apply `POST /api/v1/primitives/videos/remove-captions` to individual FINISHED clips afterwards.
@@ -266,7 +266,7 @@ Both surfaces can generate a NEW AI video/image (optionally conditioned on refer
266
266
 
267
267
  **Cost gate (see the Cost spectrum section):** AI **video** generation is the expensive end of the spectrum ($1–$10+/video) — **ask the director's permission before generating video**, and prefer reusing existing footage (My Files / their computer / a web search) or filming when it would do. **Image** generation is cheap — use it freely without asking. When a director wants specificity that only AI video can deliver and their budget allows, this is the right tool; just confirm first.
268
268
 
269
- - **Editor web copilot**: use the `generate_layer` editor_action — it does the whole flow (submit job → placeholder clip → auto-swap on completion) and reads `pending_generations` from `editor_context`. Set `media_type`, `prompt`, `aspect_ratio` (match the canvas), and `intent` (`fill_gap` with `start`/`duration`, `replace_layer` with `replace_layer_key`, or `add`). For character consistency pass a cast `reference_url` in `input_references` (video) / `prompt_attachments` (image).
269
+ - **Editor web copilot**: use the `generate_layer` editor_action — it does the whole flow (submit job → placeholder clip → auto-swap on completion) and reads `pending_generations` from `editor_context`. Set `media_type`, `prompt`, `aspect_ratio` (match the canvas), and `intent` (`fill_gap` with `start`/`duration`, `replace_layer` with `replace_layer_key`, or `add`). For character consistency pass a cast `reference_url` — or the character's `character_sprite_card.png` viewUrl from My Files (see "Character consistency") — in `input_references` (video) / `prompt_attachments` (image).
270
270
 
271
271
  - **Desktop agents (Claude Code / Codex)** — two devcli commands:
272
272
  ```bash
@@ -311,11 +311,14 @@ Compositions support **first-class animated captions**: caption layers whose wor
311
311
 
312
312
  **Word timings**: real word-level timestamps come from OpenAI STT (whisper-1); Gemini/OpenRouter transcripts get char-weighted estimated word windows, which still read well. Hand-typed text is auto-paged and estimated.
313
313
 
314
- Three surfaces:
315
- - **Editor web copilot** — `editor_action` with `action_type=set_captions`: pass `captions=[{text,start,duration,words?}]` cues derived from the transcript (`video_context` gives timestamped segments), or just `text` (+ `start`/`duration` window) to auto-page. Pick `caption_style`; the call **replaces** all existing animated caption layers, so it's also the "restyle everything" verb. Per-layer: `set_layer_style` with `caption_*` fields restyles one cue, `set_layer_text` rewrites its words, `caption_animation="none"` flattens back to static text. Never hand-build word-by-word captions from many `add_layer` calls.
314
+ Four surfaces:
315
+ - **Editor web copilot** — for narration that needs TRANSCRIBING (a TTS voiceover layer, replaced audio, word-accurate karaoke), call the **captions primitive**: `POST /api/v1/primitives/audio/captions` `{ tracer, payload: { source_url: <narration URL>, style } }` — one job transcribes with word-level timings and returns `captions` (the cue array) plus `set_captions_action` (fully-formed `editor_action` arguments); apply verbatim with `editor_action action_type=set_captions`. For speech that is the decomposed source video's own audio, skip the job: derive cues from `video_context`'s timestamped segments and call `set_captions` directly with `captions=[{text,start,duration,words?}]`, or just `text` (+ `start`/`duration` window) to auto-page. Pick `caption_style`; the call **replaces** all existing animated caption layers, so it's also the "restyle everything" verb. Per-layer: `set_layer_style` with `caption_*` fields restyles one cue, `set_layer_text` rewrites its words, `caption_animation="none"` flattens back to static text. Never hand-build word-by-word captions from many `add_layer` calls.
316
+ - **REST one-step (headless/automation)** — `POST /api/v1/compositions/:forkId/captions` `{ audio_url?|text?, style?, animation?, active_color?, highlight_color?, uppercase?, max_words_per_cue?, offset_sec?, provider?, language? }` transcribes the fork's own narration (auto-resolves: explicit `audio_url` → audio layer → backing video → source video) and writes the animated caption layers into the working composition **server-side** in one synchronous call — BYOK STT, non-billing. On a local `vidfarm serve` box, open editor tabs live-morph the result. Do **not** use it while a cloud editor session is open (the editor's next auto-save would overwrite it) — in-editor, mutate through `set_captions` instead.
316
317
  - **Editor UI (human)** — right-click → **Insert Captions** drops a starter cue; the Inspector's **Animated captions** picker switches styles on any selected text/caption layer, with active-word/highlight color and uppercase controls next to it.
317
318
  - **Desktop agents (devcli)** — `vidfarm captions generate ./work [--style spotlight]` transcribes the composition's own narration LOCAL-FIRST on your key (prefers OpenAI for real word timestamps) and writes the cue layers into `composition.html` on disk (a running `serve` live-morphs it). Alternates: `--audio <file|url>`, `--srt <file>` (e.g. from `vidfarm stt`), or `--text "<script>"`. Then `vidfarm captions style ./work --style karaoke` (restyle, same text/timing), `captions list`, `captions clear`.
318
319
 
320
+ The plain STT primitive grew the same hook: `POST /api/v1/primitives/audio/transcribe` accepts `word_timestamps: true` to attach word-level timings to each transcript segment (real on an OpenAI key via whisper-1; other providers stay segment-level).
321
+
319
322
  ### Local file paths as media (skip the S3 upload)
320
323
 
321
324
  `vidfarm place --src` (and `approve --video/--media`) accept a **local file path**, not just a URL — so a power user bulk-building compositions from a folder of clips on their own machine never has to upload every asset to Vidfarm storage:
@@ -539,8 +542,10 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
539
542
  | `vidfarm upload <file> [--folder <path>]` | `POST /api/v1/user/me/temporary-files/upload` | upload → durable URL (ephemeral; prefer `--folder temp` for scratch) |
540
543
  | `vidfarm download <url> [dest]` | (streams any URL to disk) | download media |
541
544
  | `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
545
+ | `vidfarm files --search "…" [--folder <path>]` | `POST /api/v1/user/me/attachments/search` | find My Files assets by MEANING (keyword + vector over name/folder/notes) |
542
546
  | `vidfarm get-file <id> [dest] [--print]` | (resolve id → view_url, then stream/print) | read one My Files asset |
543
- | `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin]` | `POST /api/v1/user/me/attachments/upload` | write into My Files (persistent) |
547
+ | `vidfarm put-file <file> [--folder <p>] [--as <name>] [--content/--stdin] [--notes "…"]` | `POST /api/v1/user/me/attachments/upload` | write into My Files (persistent) |
548
+ | `vidfarm annotate-file <id\|name> --notes "…"` | `PATCH /api/v1/user/me/attachments/:id` | set metadata notes on one My Files entry (vector-embedded) |
544
549
  | `vidfarm clips scan <video> [--range MM:SS-MM:SS] [--duration <s>] [--aspect 9:16] [--no-text] [--prompt "…"]` | (LOCAL: local ffmpeg + local claude/codex agent → `~/.vidfarm` SQLite) | hunt long-form video into short clips on this machine |
545
550
  | `vidfarm clips scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /clips/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
546
551
  | `vidfarm clips search "…"` / `clips match "…"` / `clips list` / `clips sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the clip library |
@@ -559,9 +564,26 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
559
564
  - **Write into My Files** — `POST /api/v1/user/me/attachments/upload` (multipart `file` + optional `folder_path`) persists a file into the library. devcli: `vidfarm put-file <localFile> --folder <path>`, or pipe/inline text without a temp file: `echo "…" | vidfarm put-file --stdin --as About.md --folder acme` / `vidfarm put-file --content "…" --as About.md`. This is how an agent saves Getting Started context (About.md, Interview.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) or drops a logo/product demo into a namescoped folder. In the web editor copilot the same capability is the **`browse_files` write action** (`action=write` with `file_name` + `content` + optional `folder_path`), which accepts text files (md/txt/csv/json/srt/vtt).
560
565
  - **`vidfarm upload` is different** — it targets the **ephemeral** temporary-files store (`POST /api/v1/user/me/temporary-files/upload`) for a throwaway durable URL to drop straight into a composition or approved post. Use `put-file` when you want the asset to persist in the user's My Files library; use `upload` for one-shot media you just need a URL for. Namescope scratch uploads under a **`temp/` folder** (`vidfarm upload clip.mp4 --folder temp`) so they stay in one purgeable place. Better still, if you're on a `vidfarm serve` box, skip the upload entirely and reference the file off disk with `place --src ./clip.mp4` (see "Local file paths as media").
561
566
 
567
+ ### Metadata notes + vector search (find assets by meaning)
568
+
569
+ Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the clip library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
570
+
571
+ - **Annotate** — `vidfarm annotate-file <id|name> --notes "Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara."` (`PATCH /me/attachments/:id`, body `{ notes }`; empty string clears). Or set notes at write time: `vidfarm put-file zara.png --folder acme/characters/zara --notes "…"`. Web copilot: `browse_files action=annotate` (or `notes` on `action=write`).
572
+ - **Search** — `vidfarm files --search "the fox mascot reference sheet"` (`POST /me/attachments/search`, body `{ query, folder_path?, limit? }`) returns ranked hits with `similarity` + `keyword_match`, mirroring `/clips/search`: keyword survivors over name/folder/notes are semantically re-ranked, and when nothing keyword-matches it relaxes to a pure semantic pass. Web copilot: `browse_files action=search`.
573
+ - **Annotate what you'll want back.** Filenames alone don't survive months of accumulation — any asset a future session should find (character refs, logo variants, recurring backgrounds, key briefs) deserves notes at save time. `list`/`read` responses include each file's `notes`, so a scan of a folder doubles as a manifest of what's in it.
574
+
575
+ ### Character consistency (recurring characters, mascots, spokespeople)
576
+
577
+ When a director has a **recurring character** that must look the same across videos, persist the character to My Files under the offer's folder (e.g. `acme-skincare/characters/zara/`) as a **pair of files**:
578
+
579
+ 1. **`about_character.md`** — name, role, personality, physical description (face, build, wardrobe, signature colors), voice/tone, and do/don'ts. This is the text you pull wording from when prompting.
580
+ 2. **`character_sprite_card.png`** — ONE reference-sheet image showing the character consistently: full body front/side/back plus a face close-up on a neutral background. If the director has no sprite card, generate one with `POST /api/v1/primitives/images/generate` (image gen is cheap — pass their best character photos as `prompt_attachments`, ask for a "character reference sheet / sprite card" layout), then persist the finished job's URL: `vidfarm download <url> zara.png && vidfarm put-file zara.png --as character_sprite_card.png --folder acme/characters/zara` (web copilot: `browse_files action=write` with `source_url`).
581
+
582
+ **Annotate both files** with notes naming the character so `files --search "our mascot"` finds them from any phrasing. From then on, character consistency is mechanical: **always pass the sprite card's `viewUrl` as the reference input** on every generation featuring the character — `prompt_attachments` for image generation/edit, `input_references` for `videos/generate` and the editor's `generate_layer` — and lift the description from `about_character.md` into the prompt. Before generating a recurring character, **search My Files for an existing sprite card first**; never re-imagine a character from memory when a reference exists (that's how characters drift off-model between videos).
583
+
562
584
  **Assume My Files is multi-offer.** A user often runs more than one product, offer, brand, or region, and namescopes assets into folders accordingly — by product (`acme-skincare/`, `zensleep/`), by offer/campaign (`summer-sale/`), by region (`us/`, `eu/`), by asset type (`logos/`, `ugc-clips/`), or any arbitrary scheme. There is **no fixed layout** — read the `folders` tree first and reason about how this user organized things. Before pulling assets for a task, infer which folder(s) match the product/offer/region the current composition is about (match folder names to the composition title, video context, and what the user said), then scope reads to that folder so you never mix one brand's logo/product-shot/music into another brand's video. If the target offer is ambiguous, ask which product/offer/region (or which folder) this work is for rather than guessing across offers.
563
585
 
564
- In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=read` / `action=write`), so the copilot follows the identical reasoning: list folders, pick the right offer's folder, then read an asset — or `write` a text doc (About.md, awareness-levels.md, …) into that folder. The devcli equivalents are `vidfarm files` / `get-file` / `put-file`.
586
+ In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=search` / `action=read` / `action=write` / `action=annotate`), so the copilot follows the identical reasoning: search or list to find the right offer's folder, then read an asset — or `write` a text doc (About.md, awareness-levels.md, …) or import a media URL (`source_url`) into that folder, annotating anything worth finding again. The devcli equivalents are `vidfarm files [--search]` / `get-file` / `put-file [--notes]` / `annotate-file`.
565
587
 
566
588
  ## Local dev loop (`vidfarm serve`)
567
589
 
@@ -614,7 +636,7 @@ The point of onboarding is to build **durable, reusable context** in My Files, n
614
636
  2. **Awareness level** (Eugene Schwartz — problem-aware, solution-unaware, …) → `awareness-levels.md`. If it's genuinely unknown after thinking it through, note that ads for **every** level should be made and tested. Use `brainstorm/awareness_stages`.
615
637
  3. **Persuasive angles** → `persuasive-angles.md`, via `brainstorm/angles`.
616
638
  4. **Hooks** → `ad-hooks.md`, via `brainstorm/hooks`.
617
- 5. **Brand assets & demos** — ask if they have logos/mascots/themes (suggest a `/brand-assets/` folder, e.g. `/brand-assets/logo.png`) or product demos / screen recordings (suggest a `/product-demos/` folder). `browse_files list` / `vidfarm files` first to see what they already uploaded; filenames should be descriptive so assets are findable.
639
+ 5. **Brand assets & demos** — ask if they have logos/mascots/themes (suggest a `/brand-assets/` folder, e.g. `/brand-assets/logo.png`) or product demos / screen recordings (suggest a `/product-demos/` folder). `browse_files list` / `vidfarm files` first to see what they already uploaded; filenames should be descriptive and every asset worth finding later should get **notes** (`annotate-file` / `browse_files annotate`) so `files --search` works months from now. If they have a recurring character/mascot, set up its `about_character.md` + `character_sprite_card.png` pair now (see "Character consistency").
618
640
  6. **Budget** — ask roughly what they want to spend per video, and map it to the Cost spectrum (free reuse+local render → pennies for cloud render → ~$1 for some AI scenes → $10+ for heavy AI gen). This sets which approach you default to and whether AI **video** generation is on the table (ask permission before using it; image gen is cheap and fine). Budget can also be revisited per editor project.
619
641
  7. **Recommend & adapt a template** — pair what you now know about the offer against the decomposed template catalog (`GET /discover/feed?q=<offer>`, read each result's `promotions`/`keywords`/`summary`), recommend the best 3-6, then fork and **modify** the winner to fit their offer. Prefer already-decomposed templates so the director skips the ~$0.10 decompose cost.
620
642
 
package/SKILL.platform.md CHANGED
@@ -192,7 +192,7 @@ The same runtime is used inside the Trackpad Editor iframe AND when a published
192
192
  - Bidirectionally syncs timeline drag → HTML `data-start`/`data-duration` mutations, and inspector edits → text/style changes
193
193
  - Persists every commit back to the API as PUT `composition.html` and PATCH `composition.json`
194
194
  - Exposes an **Editor Action API** so the Chat panel can invoke typed actions (`add_layer`, `remove_layer`, `set_layer_start`, `set_layer_duration`, `set_layer_style`, `set_captions` — animated word-by-word caption runs — `group_layers`, `ungroup_layers`, `replace_composition_html`, etc.)
195
- - Runs the composition-runtime script inside the preview iframe to keep timeline scrub in sync with the current time. That runtime also scrubs **Ken Burns** image animations (CSS pan/zoom on `[data-kenburns]` `<img>` layers) and **animated captions** (word-by-word `vf-cap-*` animations on `[data-caption-animation]` caption layers — Ken Burns normalizes to clip progress, captions seek to the absolute offset `time − data-start` because each word's inline delay/duration is its spoken window) to the playhead, so the preview matches the render frame-for-frame. Presets and keyframes live in `src/hyperframes/composition.ts` (`KEN_BURNS_CSS`, `CAPTION_ANIM_CSS` + `CAPTION_STYLE_PRESETS`), mirrored into the editor bundle and the serve-time HTML rewrite; `normalizePublishHtml` re-injects both stylesheets (marker-guarded) into legacy compositions at render time. Caption gotcha: the layer's inline `text-transform`/`overflow` must be set on the element (the stylesheet's `[data-caption-uppercase]` rule loses to inline styles), and cue text edits must rebuild the word spans (`refreshCaptionWordSpans`) rather than setting `textContent`.
195
+ - Runs the composition-runtime script inside the preview iframe to keep timeline scrub in sync with the current time. That runtime also scrubs **Ken Burns** image animations (CSS pan/zoom on `[data-kenburns]` `<img>` layers) and **animated captions** (word-by-word `vf-cap-*` animations on `[data-caption-animation]` caption layers — Ken Burns normalizes to clip progress, captions seek to the absolute offset `time − data-start` because each word's inline delay/duration is its spoken window) to the playhead, so the preview matches the render frame-for-frame. Presets and keyframes live in `src/hyperframes/composition.ts` (`KEN_BURNS_CSS`, `CAPTION_ANIM_CSS` + `CAPTION_STYLE_PRESETS`), mirrored into the editor bundle and the serve-time HTML rewrite; `normalizePublishHtml` re-injects both stylesheets (marker-guarded) into legacy compositions at render time. Caption gotcha: the layer's inline `text-transform`/`overflow` must be set on the element (the stylesheet's `[data-caption-uppercase]` rule loses to inline styles), and cue text edits must rebuild the word spans (`refreshCaptionWordSpans`) rather than setting `textContent`. Speech→captions REST surfaces: `primitive:captions` (`POST /api/v1/primitives/audio/captions`, src/primitive-registry.ts) is the composition-agnostic job — STT with `wordTimestamps` (openai/whisper-1 real, others estimated via `estimateCaptionWords`) paged by `src/services/captions.ts` into cues + a ready `set_captions_action`; `POST /api/v1/compositions/:forkId/captions` (src/app.ts, next to the composition.html PUT) is the one-step variant that resolves the fork's narration, transcribes on raw customer keys (`loadCustomerVisionKeys` + `transcribeSpeechWithKey`), applies via `insertCaptionLayers` (shared with the devcli), and persists through the same sanitize path — intentionally NOT watch-suppressed so serve tabs live-morph; not for open cloud editor sessions (auto-save clobber). Scrub-stability guard (both `syncCaptionClip` copies — `src/composition-runtime.ts` + the editor's `runtimeScript()`): Chrome permanently drops a CSS-paused `fill:none` animation once its `currentTime` is pushed past the effect end, so the scrub parks `fill:none` word animations at 0 when the playhead is past their window (visually identical resting state, keeps them alive for scrub-back) while `fill:forwards` presets overshoot to hold their final keyframe; missing word animations are restarted by cycling `animation-name` through `none`. Live element patches that rebuild word spans must re-scrub via the `__player.syncCaptions()` runtime API (the editor calls it after `patchLiveElement`/`previewLiveElement`) or fresh spans sit paused-at-0 (invisible for word-pop/stack-up) until the next seek.
196
196
 
197
197
  The editor is a single monolithic React component (~9k lines). It builds cleanly to a static bundle deployed to CloudFront under `/editor/`.
198
198
 
@@ -266,7 +266,7 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
266
266
 
267
267
  The chat model uses the **caller's provider keys**. No Vidfarm-owned model calls happen in this path.
268
268
 
269
- Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, and `browse_files`. `browse_files` reads **and writes** the caller's My Files library — `action=list`/`read`/`write`, where `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`. Write is what lets the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) namescoped under a product/offer folder; the system prompt in `src/editor-chat.ts` drives when to save. The devcli mirror is `vidfarm put-file`.
269
+ Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, and `browse_files`. `browse_files` reads **and writes** the caller's My Files library — `action=list`/`search`/`read`/`write`/`annotate`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/clips/search`. Write+annotate are what let the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) and character-consistency pairs (`about_character.md` + `character_sprite_card.png`) namescoped under a product/offer folder; the system prompt in `src/editor-chat.ts` drives when to save and annotate. The devcli mirrors are `vidfarm put-file [--notes]`, `annotate-file`, and `files --search`.
270
270
 
271
271
  ## Primitives
272
272
 
@@ -315,7 +315,7 @@ Per-stage config is loaded via `.env.<stage>`. Critical variables:
315
315
  - `AWS_REGION`, AWS credentials
316
316
  - `VIDFARM_<STAGE>_PUBLIC_BASE_URL` — the API base
317
317
  - `VIDFARM_<STAGE>_ENCRYPTION_SECRET` — DynamoDB field encryption
318
- - `VIDFARM_<STAGE>_API_KEY_SALT` — API key hashing
318
+ - `VIDFARM_<STAGE>_API_KEY_SALT` — API key hashing. Rotating it invalidates previously-issued keys on that stage only; the shared staging table can hold keys hashed under several salts (deployed stage vs local dev boxes). `getVisibleApiKey` (src/app.ts) is salt-aware: it returns the newest active key whose `key_hash` validates under THIS server's salt (minting one if none do), so boot pages / Settings never hand the browser a key the serving box would reject — the editor chat and all its server-executed tools authenticate with that embedded key.
319
319
  - `VIDFARM_<STAGE>_BOOTSTRAP_*` — initial admin user
320
320
  - `VIDFARM_<STAGE>_RESEND_*` — email
321
321
  - `VIDFARM_<STAGE>_SENTRY_*` — Sentry DSN
@@ -397,7 +397,7 @@ Legacy but still active:
397
397
 
398
398
  ## Local editor dev loop (`vidfarm serve`)
399
399
 
400
- `@mevdragon/vidfarm-devcli` (`src/cli.ts`) has two roles. First, `serve` boots the **full** Vidfarm backend locally — single origin, disk-backed records + storage — so Claude Code, Codex, or a human can edit `composition.html` on disk and see the browser live-morph the change (detailed below). Second, it wraps the **entire director REST flow** as 1:1 CLI commands — `discover`, `inspiration-add`, `fork`, `decompose`, `snapshot`, `render` (→ `/render`), `approve`, `schedule`, `posts`/`schedules`, `upload`/`download`, `files`/`get-file`/`put-file` (My Files read+write), `login`/`whoami`/`provider-keys`, plus a raw `api <METHOD> <path>` passthrough. Each command prints its prod frontend URL as a first-class output, and `--json` gives pure JSON for agents. The command↔route table lives in `SKILL.director.md`.
400
+ `@mevdragon/vidfarm-devcli` (`src/cli.ts`) has two roles. First, `serve` boots the **full** Vidfarm backend locally — single origin, disk-backed records + storage — so Claude Code, Codex, or a human can edit `composition.html` on disk and see the browser live-morph the change (detailed below). Second, it wraps the **entire director REST flow** as 1:1 CLI commands — `discover`, `inspiration-add`, `fork`, `decompose`, `snapshot`, `render` (→ `/render`), `approve`, `schedule`, `posts`/`schedules`, `upload`/`download`, `files`/`get-file`/`put-file`/`annotate-file` (My Files read+write+notes; `files --search` is the hybrid keyword+vector lookup over names/folders/notes), `login`/`whoami`/`provider-keys`, plus a raw `api <METHOD> <path>` passthrough. Each command prints its prod frontend URL as a first-class output, and `--json` gives pure JSON for agents. The command↔route table lives in `SKILL.director.md`.
401
401
 
402
402
  ```bash
403
403
  # Boot the full editor locally, pull a template's default fork onto disk, open pre-authed.