@mevdragon/vidfarm-devcli 0.17.0 → 0.18.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.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: vidfarm-media
3
- description: Media resolution for video workflows — narration TTS, voice-matched speech regeneration ("same speaker, new words"), word-level timings, BGM, SFX, and image/clip sourcing through vidfarm. Use when a video needs audio (voiceover/narration, music, sound effects), rewording of existing narration in the original speaker's voice, caption word timings, or media assets. Ships the shared audio engine scripts/audio.mjs (audio_request.json → audio_meta.json) used by faceless-explainer, product-launch-video, and general-video; documents vidfarm tts/stt BYOK primitives, the keyless local Kokoro + whisper.cpp engines, and My Files / clip-library / generation sourcing. No third-party media accounts — everything is user-owned or generated on the user's keys.
3
+ description: Media resolution for video workflows — narration TTS, voice-matched speech regeneration ("same speaker, new words"), word-level timings, BGM, SFX, and image/clip sourcing through vidfarm. Use when a video needs audio (voiceover/narration, music, sound effects), rewording of existing narration in the original speaker's voice, caption word timings, or media assets. Ships the shared audio engine scripts/audio.mjs (audio_request.json → audio_meta.json) used by faceless-explainer, product-launch-video, and general-video; documents vidfarm tts/stt BYOK primitives, the keyless local Kokoro + whisper.cpp engines, and My Files / raws library / generation sourcing. No third-party media accounts — everything is user-owned or generated on the user's keys.
4
4
  ---
5
5
 
6
6
  # vidfarm-media
7
7
 
8
- One engine and one rule. The engine: `scripts/audio.mjs` turns a neutral `audio_request.json` into narration files, word timings, a music bed, and SFX — written into the project and described by one meta JSON. The rule: **no third-party media accounts, ever** — audio and images are the user's own files, found in the user's My Files / clip library, generated on the user's provider keys, or produced by keyless local models. If a source needs a sign-up, it is not a source.
8
+ One engine and one rule. The engine: `scripts/audio.mjs` turns a neutral `audio_request.json` into narration files, word timings, a music bed, and SFX — written into the project and described by one meta JSON. The rule: **no third-party media accounts, ever** — audio and images are the user's own files, found in the user's My Files / raws library, generated on the user's provider keys, or produced by keyless local models. If a source needs a sign-up, it is not a source.
9
9
 
10
10
  ## Preflight (run once, before the brief)
11
11
 
@@ -81,6 +81,6 @@ Voices, styles, formats, and the exact word-timestamp paths: `references/tts.md`
81
81
  Same ownership rule, different surfaces:
82
82
 
83
83
  - **My Files** — `vidfarm files --search "<meaning>"`; annotate assets with `vidfarm annotate-file <id> --notes "…"` so search finds them later; write with `vidfarm put-file`.
84
- - **Clip library** — `vidfarm clips search "<text>"` over clips hunted from the user's long-form footage (`vidfarm clips scan <video>`).
84
+ - **Raws library** — `vidfarm raws search "<text>"` over raws hunted from the user's long-form footage (`vidfarm raws scan <video>`).
85
85
  - **Generate on the user's keys** — `vidfarm generate image|video --prompt "…"` (BYOK primitives; `--place <dir>` drops the result straight into a composition).
86
86
  - Never hotlink or scrape third-party stock; a capture workflow's own screenshots and the user's brand assets are always fair game.
package/SKILL.director.md CHANGED
@@ -208,11 +208,11 @@ Use it whenever you need to know what the video says or shows: writing/translati
208
208
  - **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
209
209
  - **Desktop agents (Claude Code / Codex)**: fetch the `video-context.json` route directly (e.g. `vidfarm api GET /api/v1/compositions/<forkId>/video-context.json`) to ground edits in what the video says and shows.
210
210
 
211
- ## Clip hunting (long-form → short-form clips)
211
+ ## Raws (long-form → short-form raws)
212
212
 
213
- Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **short clips**. This is the `/clips` surface — browse it at `https://vidfarm.cc/clips` (the Library page's "Finished / Clips" toggle).
213
+ Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **raws**. This is the `/raws` surface — browse it at `https://vidfarm.cc/library/raws` (the Library page's "Approved / Raws" tabs).
214
214
 
215
- **Start a hunt** — `POST /clips/scan` (async: returns `202 { scan_id }` immediately):
215
+ **Start a hunt** — `POST /raws/scan` (async: returns `202 { scan_id }` immediately):
216
216
 
217
217
  ```jsonc
218
218
  {
@@ -228,34 +228,34 @@ Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/
228
228
  // Hunt controls (all optional; also expressible inline in the prompt):
229
229
  "ranges": ["12:30-15:45", "20:00-22:10"], // ONLY hunt these source windows — big cost saver on long videos
230
230
  "target_duration_sec": 30, // SOFT length band, not a hard cut: 10→5-20s, 30→20-40s, 60→40-80s, 120→80-160s
231
- "aspect": "9:16", // crop every clip: 9:16 | 16:9 | 4:3 | 1:1 (synonyms: vertical/portrait, horizontal/landscape, square)
231
+ "aspect": "9:16", // crop every raw: 9:16 | 16:9 | 4:3 | 1:1 (synonyms: vertical/portrait, horizontal/landscape, square)
232
232
  "crop_focus": "center", // center | top | bottom | left | right (top biases toward faces)
233
233
  "avoid_text": true, // prefer scenes WITHOUT burned-in captions/on-screen text — a scene-SELECTION filter, NEVER GhostCut on the source
234
234
  "tracer": "my-campaign" // rolls up all the hunt's billing/observability events
235
235
  }
236
236
  ```
237
237
 
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, the hunt's `tracer`, and (with a gemini/openai key) a semantic embedding.
238
+ - **Poll** `GET /raws/scan/:scanId` until the source `status` is `complete` (or `failed` — crashed pipelines are reconciled to `failed`, pollers never spin).
239
+ - **Results**: `GET /raws/feed?source=<source_video_id>` (or the whole library), hybrid search via `POST /raws/search` `{ query: "someone looks confused" }`, per-raw download at `GET /raws/:clipId/download`. Raws 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
- - **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
- - **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.
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 raws themselves are durable.
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 raws afterwards.
243
243
 
244
244
  **devcli (local-first — this is the default way to hunt on your own machine):**
245
245
 
246
246
  ```bash
247
247
  # Local machine power: local ffmpeg + your local claude/codex CLI subscription (no API key needed)
248
- vidfarm clips scan ./podcast.mp4 --range "12:30-15:45" --duration 30 --aspect vertical --no-text \
248
+ vidfarm raws scan ./podcast.mp4 --range "12:30-15:45" --duration 30 --aspect vertical --no-text \
249
249
  --prompt "guest reaction moments"
250
250
 
251
251
  # Provider keys are the fallback (--provider gemini|openai|openrouter); the cloud pipeline is the
252
252
  # EXPLICIT backup, never the default:
253
- vidfarm clips scan --cloud ./podcast.mp4 --duration 30 --aspect 9:16 --tracer my-campaign
254
- vidfarm clips scan --cloud --url "https://youtube.com/watch?v=…" --range "0:00-10:00"
253
+ vidfarm raws scan --cloud ./podcast.mp4 --duration 30 --aspect 9:16 --tracer my-campaign
254
+ vidfarm raws scan --cloud --url "https://youtube.com/watch?v=…" --range "0:00-10:00"
255
255
 
256
256
  # Then search/reuse the library
257
- vidfarm clips search "confused reaction after reading a message"
258
- vidfarm clips export <clip-ids…> --to ./picks
257
+ vidfarm raws search "confused reaction after reading a message"
258
+ vidfarm raws export <raw-ids…> --to ./picks
259
259
  ```
260
260
 
261
261
  Local scans persist to a SQLite library under `~/.vidfarm` (free compute, subscription-powered evaluation); `--cloud` uploads to your temp folder (30-day TTL) or passes `--url`, runs the deployed pipeline, and bills AWS compute only.
@@ -546,10 +546,10 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
546
546
  | `vidfarm get-file <id> [dest] [--print]` | (resolve id → view_url, then stream/print) | read one My Files asset |
547
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
548
  | `vidfarm annotate-file <id\|name> --notes "…"` | `PATCH /api/v1/user/me/attachments/:id` | set metadata notes on one My Files entry (vector-embedded) |
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 |
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) |
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 |
552
- | `vidfarm clips preset list\|run\|save` / `clips export <ids…> --to <dir>` | (local library) | saved queries; copy clip MP4s out |
549
+ | `vidfarm raws 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-form raws on this machine |
550
+ | `vidfarm raws scan --cloud <video\|--url <url>> [--tracer <id>]` | temp-file presign/PUT/finalize + `POST /raws/scan` + poll | BACKUP: run the hunt on the deployed pipeline (bills AWS compute only) |
551
+ | `vidfarm raws search "…"` / `raws match "…"` / `raws list` / `raws sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the raws library |
552
+ | `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
553
553
  | `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
554
554
  | `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
555
555
  | `vidfarm doctor` | (local environment triage) | check ffmpeg/node/keys/agent CLI/poisoned env before debugging anything else |
@@ -574,10 +574,10 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
574
574
 
575
575
  ### Metadata notes + vector search (find assets by meaning)
576
576
 
577
- 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:
577
+ 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 raws library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
578
578
 
579
579
  - **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`).
580
- - **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`.
580
+ - **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 `/raws/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`.
581
581
  - **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.
582
582
 
583
583
  ### Character consistency (recurring characters, mascots, spokespeople)
@@ -719,7 +719,7 @@ Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-
719
719
  - Response: standard primitive job (`202 { job_id }`). The job probes the source, submits to GhostCut, polls to completion server-side (typically a few minutes), and mirrors the result. Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read `primary_file_url` / `video.file_url` (`captionsRemovedVideoUrl` in the output).
720
720
  - Billing: `~$0.10 per 30 seconds` of source video (per-chunk, 1-chunk minimum; `ghostcut_subtitle_removal` cost center), plus the small probe cost.
721
721
  - For the CURRENT fork's decompose flow, don't re-run this primitive — the caption-free mirror already exists via `GET /api/v1/compositions/:forkId/remove-video-captions`.
722
- - **Short clips only — hard cap ~15 minutes** (`GHOSTCUT_MAX_DURATION_SEC`, default 900s). Never point this at a long-form source: for "clips without captions" from a long video, run a clip hunt with `avoid_text: true` (scene selection — see "Clip hunting") and, if needed, apply this primitive to the individual FINISHED clips.
722
+ - **Short clips only — hard cap ~15 minutes** (`GHOSTCUT_MAX_DURATION_SEC`, default 900s). Never point this at a long-form source: for "clips without captions" from a long video, run a raws hunt with `avoid_text: true` (scene selection — see "Raws") and, if needed, apply this primitive to the individual FINISHED raws.
723
723
 
724
724
  Example:
725
725
 
package/SKILL.platform.md CHANGED
@@ -247,13 +247,13 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
247
247
 
248
248
  ## Clip hunting pipeline (long-form → short-form)
249
249
 
250
- `POST /clips/scan` starts a Step Functions STANDARD workflow (`…ClipScanWorkflow`): `probe_and_detect` → `Map(process_scene, maxConcurrency 5)` → `finalize`, all three states dispatching the same `infra/lambda/clip-scan.ts` on an `action` field. Business logic lives in the shared `src/services/clip-curation/` core (also consumed by devcli and the Hono API), with the hunt controls in `hunt.ts`:
250
+ `POST /raws/scan` starts a Step Functions STANDARD workflow (`…ClipScanWorkflow`): `probe_and_detect` → `Map(process_scene, maxConcurrency 5)` → `finalize`, all three states dispatching the same `infra/lambda/clip-scan.ts` on an `action` field. Business logic lives in the shared `src/services/clip-curation/` core (also consumed by devcli and the Hono API), with the hunt controls in `hunt.ts`:
251
251
 
252
252
  - **`hunt_spec`** — windows (time ranges; ffmpeg input-seeks per window so only the requested footage is DECODED — the actual cost saving), a soft `duration_band` (`resolveDurationBand`: 10→5-20s, 30→20-40s via ±target/3), output `aspect` + `crop_focus` (smart-centre ffmpeg crop applied to clips, thumbnails, AND tagging keyframes so the model judges the framed output), `avoid_text` (rides the keep/drop/merge refine pass as a selection filter), and `source_url` (RapidAPI ingest → the user's temp folder). The spec travels as ONE opaque JsonPath object through the state machine, so new hunt fields are Lambda-code-only changes — no state-machine redeploy.
253
253
  - **Sources are temporary** — URL ingests and uploads land at `users/{id}/temporary/clip-sources/…` with a hard 30-day TTL enforced three ways: tag-scoped S3 lifecycle (`vidfarm-temp=1`, tagged server-side at temp-file finalize/upload), native DynamoDB TTL (`expires_at_epoch_seconds`, top-level on the item), and the list-route sweep. Hunted clips (`clips/{owner}/…`) are durable.
254
254
  - **Billing: AWS compute ONLY.** Every Lambda invocation post-hoc bills its GB-seconds to the `clip_scan_lambda` cost center (idempotent on `awsRequestId`); `finalize` adds `step_functions_standard` scaled by scene count (3 + N transitions); URL ingest adds the existing `rapidapi_video_download` pass-through. `jobId = scan_id`, and the caller's `tracer` rides every event (gsi2-tracer rollups). The BYOK tagging/embedding spend (gemini/openai/openrouter keys in the state input) is deliberately never wallet-billed. Submission is wallet-gated (`assertWalletCanQueueJobs` → 402); the 202 response carries a pre-scan `compute_estimate`.
255
- - **Failure reconciliation** — the workflow has no failure hook into records, so `GET /clips/scan/:scanId` lazily `DescribeExecution`s a running scan and flips the scan+source records to `failed` (or `complete`) so pollers converge.
256
- - **devcli is LOCAL-FIRST** — `vidfarm clips scan` runs local ffmpeg and, by default, the user's local claude/codex CLI subscription (`LocalAgentClipClient`, implementing the same `ClipHuntModelClient` interface as the API providers; refine's model call lives on the client as `decideScenes` to make them swappable). Provider keys are the fallback; `--cloud` is the explicit backup that presign-uploads to the temp folder and drives this pipeline.
255
+ - **Failure reconciliation** — the workflow has no failure hook into records, so `GET /raws/scan/:scanId` lazily `DescribeExecution`s a running scan and flips the scan+source records to `failed` (or `complete`) so pollers converge.
256
+ - **devcli is LOCAL-FIRST** — `vidfarm raws scan` runs local ffmpeg and, by default, the user's local claude/codex CLI subscription (`LocalAgentClipClient`, implementing the same `ClipHuntModelClient` interface as the API providers; refine's model call lives on the client as `decideScenes` to make them swappable). Provider keys are the fallback; `--cloud` is the explicit backup that presign-uploads to the temp folder and drives this pipeline.
257
257
  - **Bundling gotcha** — the clip-scan and primitive-media Lambdas bundle `ffmpeg-static`/`ffprobe-static` with `forceDockerBundling: true`: ffmpeg-static downloads the binary for the platform it is installed on, so a mac-local bundle ships a darwin ffmpeg the Lambda cannot exec (`exit 126`). They also need `VIDFARM_DATA_DIR=/tmp/vidfarm` because `config.ts` mkdirs at import.
258
258
 
259
259
  ## Editor Chat
@@ -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`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). `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`.
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`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). `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 `/raws/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