@mevdragon/vidfarm-devcli 0.3.2 → 0.3.4

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
@@ -26,7 +26,7 @@ A Vidfarm composition is an **HTML document + JSON metadata**. Each layer (video
26
26
 
27
27
  Every director workflow goes through three concepts:
28
28
 
29
- 1. **Template** — a published composition anyone can fork. Templates live at `/hyperframes/:slug` and have a source video, a decomposed timeline, and metadata (viral DNA, captions, scenes).
29
+ 1. **Template** — a published composition anyone can fork. Template ids start with `template_`; each template opens in the editor at `/editor/:templateId` and has a source video, a decomposed timeline, and metadata (viral DNA, captions, scenes).
30
30
  2. **Composition fork** — a private editable copy of a template. Every fork has a `forkId`, an owner, a working state, and a version history. The Trackpad Editor operates on a fork.
31
31
  3. **Publish** — render the current fork state to MP4 via a Lambda render job. Each publish snapshots the current working state as an immutable version.
32
32
 
@@ -39,42 +39,46 @@ The current API base URLs are:
39
39
  - staging: `https://staging.vidfarm.cc`
40
40
  - production: `https://vidfarm.cc`
41
41
 
42
- Every browser action is authenticated by the session cookie set at `/login`. Every headless action needs the `vidfarm-api-key` header set to the caller's API key (from Settings → API Keys).
42
+ Every browser action is authenticated by the session cookie set at `/login`. Headless API-key auth uses the `vidfarm-api-key` header set to the caller's API key (from Settings → API Keys).
43
+
44
+ **Auth contract: API-key auth is the `vidfarm-api-key: <key>` request header and nothing else. `Authorization: Bearer <key>` is NOT supported — the server never reads the `authorization` header (see `requireAuth` in `src/app.ts`).**
43
45
 
44
46
  ```
45
- vidfarm-api-key: vf_key_<32 hex>
47
+ vidfarm-api-key: vf_key_<id>
46
48
  ```
47
49
 
50
+ Scope note (verified against `src/app.ts`): the `vidfarm-api-key` header authenticates **every** authenticated surface — the `requireAuth` routes (`/api/v1/user/*`, `/api/v1/agency/*`, `/api/v1/templates/*`, `/api/v1/primitives/*`, `/api/v1/rate-limit-status`, editor-chat threads) **and** the `/api/v1/compositions/*` + `/api/v1/videos/*` + editor page routes, where an explicit API key takes precedence over any browser session cookie. Cookies remain how the web UI authenticates; headless callers never need one. Reads on composition routes also work without any credential because the `forkId` acts as an unguessable view bearer token, and share-link tokens (header `vidfarm-share-token` or `?share=` query) can grant `viewer`/`editor`/`publisher` roles.
51
+
48
52
  ## Login flow
49
53
 
50
54
  Directors log in with OTP:
51
55
 
52
- 1. `POST /api/v1/auth/otp/request { email }` — sends a code to email
53
- 2. `POST /api/v1/auth/otp/verify { email, code }` — sets session cookie, returns `{ user, apiKey }`
56
+ 1. `POST /api/v1/user/request-otp { email }` — sends a code to email; returns `{ ok, delivery }` (plus `dev_code` when delivery is `console` outside production)
57
+ 2. `POST /api/v1/user/verify-otp { email, code, name? }` — returns `{ customer, apiKey }` and mints a fresh API key
54
58
 
55
- Save the `apiKey` in the client and use it for automation.
59
+ Save the `apiKey` in the client and use it for automation (as the `vidfarm-api-key` header). The browser login form uses the session-cookie variants `POST /login/otp/request` and `POST /login/otp/verify` instead.
56
60
 
57
61
  ## Provider keys
58
62
 
59
63
  Vidfarm executes many features (smart auto-decompose, editor chat) with the **caller's own AI provider keys**. Directors save keys once via:
60
64
 
61
65
  - UI: Settings → Provider Keys
62
- - API: `PUT /api/v1/user/me/provider-keys { openai, gemini, openrouter, perplexity }`
66
+ - API: `POST /api/v1/user/me/provider-keys { provider, secret, label? }` — one key per call; `provider` is `"openai" | "gemini" | "openrouter" | "perplexity"`. The key is validated against the provider before it is saved. List saved keys with `GET /api/v1/user/me/provider-keys`.
63
67
 
64
68
  Priority: Gemini → OpenAI → OpenRouter. If a director has no keys, smart decompose falls back to a deterministic time-based split.
65
69
 
66
70
  ## Discover templates
67
71
 
68
- Templates are listed on the Vidfarm homepage under `/hyperframes/`. Each has a slug, a preview MP4, source video metadata, and viral DNA.
72
+ Templates are listed on the Vidfarm homepage and `/discover`. Each has a `template_...` id, a preview MP4, source video metadata, and viral DNA.
69
73
 
70
- - Browser: browse the homepage
71
- - API: `GET /api/v1/hyperframes` — returns template summaries
74
+ - Browser: browse `/discover`
75
+ - API: `GET /discover/feed` — returns `{ templates: [{ templateId, slugId, title, previewUrl, viralDna, durationSeconds, sourceType, ... }], next_cursor }`
72
76
 
73
77
  Each template exposes a public preview:
74
78
 
75
- - `GET /hyperframes/:slug` — HTML preview
76
- - `GET /hyperframes/:slug/composition` — composition HTML
77
- - `GET /hyperframes/:slug/composition.json` — composition metadata (scenes, captions, viral DNA)
79
+ - `GET /editor/:templateId` — opens the Trackpad Editor for the template (redirects to your fork of it, or to `/login`)
80
+ - `GET /editor/:templateId/composition` — raw composition HTML built from the template's source video
81
+ - `GET /discover/skills/:templateId` — the template's SKILL.md content as JSON (registry templates only)
78
82
 
79
83
  Directors don't edit templates directly. They fork.
80
84
 
@@ -84,19 +88,19 @@ Directors don't edit templates directly. They fork.
84
88
  POST /api/v1/compositions
85
89
  Content-Type: application/json
86
90
 
87
- { "source_slug_id": "<template slug>", "title": "Optional title" }
91
+ { "template_id": "template_<...>", "title": "Optional title" }
88
92
  ```
89
93
 
90
- Returns the new fork's metadata:
94
+ `template_id` is required and must start with `template_` (the handler also accepts the same value under the key `source`). Returns the new fork's metadata (201):
91
95
 
92
96
  ```
93
- { "id": "<forkId>", "sourceSlugId": "...", "title": "...", "visibility": "private", "latestVersion": 0, ... }
97
+ { "fork_id": "<forkId>", "template_id": "template_...", "parent_fork_id": "...", "parent_version": 1, "title": "...", "visibility": "private", "latest_version": 0, "composition_url": "...", "composition_data_url": "...", "versions_url": "...", "role": "owner", "capabilities": { ... } }
94
98
  ```
95
99
 
96
- Take the returned `forkId` and open the Trackpad Editor at:
100
+ Take the returned `fork_id` and open the Trackpad Editor at:
97
101
 
98
102
  ```
99
- /editor/?fork=<forkId>
103
+ /editor/<templateId>?fork=<forkId>
100
104
  ```
101
105
 
102
106
  The editor loads `composition.html` and `composition.json` from the fork's working state.
@@ -139,9 +143,11 @@ Content-Type: application/json
139
143
 
140
144
  Two modes:
141
145
 
142
- - **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels and viral notes, extracts on-screen captions with positions, and detects viral DNA (hook, retention, payoff). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
146
+ - **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with positions, **transcribes the audio track verbatim** (timestamped segments; Gemini first, OpenAI Whisper fallback; skipped when the video has no audio), and detects viral DNA (hook, retention, payoff). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
143
147
  - **`time-slice`** — deterministic equal-duration split. Instant. No AI provider needed.
144
148
 
149
+ **Source length cap.** Vidfarm caps auto-decompose (and all inspiration ingest) at **120 seconds** of source video. If the source is longer, the API responds with `400 { ok: false, code: "source_too_long", duration_seconds, max_duration_seconds }` and no scenes are written. Trim the source before forking / ingesting.
150
+
145
151
  Response:
146
152
 
147
153
  ```
@@ -150,24 +156,61 @@ Response:
150
156
 
151
157
  If `ghostcut_pending: true`, poll `POST /api/v1/compositions/:forkId/ghostcut-poll` until status transitions to `done` or `failed`.
152
158
 
159
+ Read-only state (does **not** advance the job or bill): `GET /api/v1/compositions/:forkId/ghostcut`. Returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }`. Use this when you want to know both video URLs without triggering another poll — the AI copilot uses it to pick between the raw upload and the caption-free mirror before feeding a video into a primitive route (image extract, dedupe, trim, ai video edit, etc.). Prefer `mirrored_url` when `status === "done"` and the downstream call should be caption-free; prefer `original_source_url` when the user explicitly wants the raw upload or when `status !== "done"`.
160
+
161
+ ## Video context (transcript + scene descriptions)
162
+
163
+ `GET /api/v1/compositions/:forkId/video-context.json` — read-only, non-billing. Returns everything the smart decompose learned about the source video, so an agent can ground its work in what the video actually **says** and **shows**:
164
+
165
+ ```
166
+ {
167
+ "ok": true,
168
+ "status": "ready" | "none",
169
+ "summary": "...",
170
+ "duration_seconds": 12.4,
171
+ "transcript": {
172
+ "text": "full verbatim transcript",
173
+ "language": "en",
174
+ "provider": "gemini",
175
+ "segments": [{ "start": 0.0, "end": 2.4, "text": "..." }]
176
+ },
177
+ "scenes": [{
178
+ "slug": "hook_reveal",
179
+ "start": 0, "duration": 2.5,
180
+ "label": "Hook",
181
+ "description": "Literal visual description of what's on screen in this scene",
182
+ "viral_note": "...",
183
+ "transcript_excerpt": "what is spoken during this scene"
184
+ }],
185
+ "viral_dna": { ... }
186
+ }
187
+ ```
188
+
189
+ `status: "none"` means the fork was never smart-decomposed — run `POST /auto-decompose` with `mode: "smart"` first. `transcript` is `null` when the source has no audio track or transcription failed.
190
+
191
+ Use it whenever you need to know what the video says or shows: writing/translating captions, matching hooks or dubs to spoken audio, or planning scene-level edits.
192
+
193
+ - **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
194
+ - **Desktop agents (Claude Code / Codex)**: `vidfarm edit <template_id>` syncs the same payload to `video-context.json` on disk next to `composition.html` (use `--refetch` to refresh it after a new decompose), or fetch the route directly.
195
+
153
196
  ## Publish
154
197
 
155
- Publishing renders the fork's current working state to an MP4 using shared Remotion Lambda.
198
+ Publishing renders the fork's current working state to an MP4 using shared Remotion Lambda. The route is `/export`:
156
199
 
157
200
  ```
158
- POST /api/v1/compositions/:forkId/publish
201
+ POST /api/v1/compositions/:forkId/export
159
202
  Content-Type: application/json
160
203
 
161
- { "title": "optional", "version": null, "tracer": "optional-idempotency-key" }
204
+ { "title": "optional", "version": null, "tracer": "optional-trace-id", "html": "optional HTML to save to working state before rendering" }
162
205
  ```
163
206
 
164
- Returns `{ render_id, job_id, snapshot_version }`. Poll:
207
+ Returns 202 with `{ ok, renderId, status, progress, outputUrl, cost, title, version, ... }` where `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/ghostcut-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
165
208
 
166
209
  ```
167
210
  GET /api/v1/compositions/:forkId/renders/:renderId
168
211
  ```
169
212
 
170
- Response includes `{ phase: "RUNNING" | "SUCCEEDED" | "FAILED", progress, output_url, cost_usd }`. On success, `output_url` is the durable MP4 URL.
213
+ Response includes `{ ok, renderId, status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, framesRendered, totalFrames, cost, outputUrl, outputS3Uri, errors }` (`progress` is 0..1). On success, `outputUrl` is the durable MP4 URL.
171
214
 
172
215
  Every publish creates an immutable version snapshot at `versions/<N>/composition.html` and `versions/<N>/composition.json`.
173
216
 
@@ -177,7 +220,7 @@ Every publish creates an immutable version snapshot at `versions/<N>/composition
177
220
  GET /api/v1/compositions/:forkId/versions?limit=100
178
221
  ```
179
222
 
180
- Returns the version list with `{ version, reason, message, created_at }`. Reason is `publish`, `clone`, `manual`, or `migration`.
223
+ Returns `{ versions: [{ version, reason, message, created_by, created_at, render_job_id, composition_url, composition_data_url }], next_cursor }`. Reason is `publish`, `clone`, `manual`, or `migration`. To snapshot the working state manually, `POST /api/v1/compositions/:forkId/versions { "message": "optional" }`.
181
224
 
182
225
  To view a past cut without reverting:
183
226
 
@@ -195,12 +238,12 @@ Every fork has a visibility: `private` (default), `unlisted` (legacy), or `publi
195
238
  PATCH /api/v1/compositions/:forkId/visibility { "visibility": "public" }
196
239
  ```
197
240
 
198
- Public forks are accessible at `/editor/?fork=<forkId>` (view-only for non-owners). Public visibility is the simplest sharing surface.
241
+ Public forks are accessible at `/editor/<templateId>?fork=<forkId>` (view-only for non-owners). Public visibility is the simplest sharing surface.
199
242
 
200
- Fine-grained sharing uses per-user permissions and share links:
243
+ Fine-grained sharing uses per-user permissions and share links (all body fields are snake_case):
201
244
 
202
- - `POST /api/v1/compositions/:forkId/permissions { granteeType, granteeId, role, expiresAt? }` — grant a specific user access. Roles: `viewer`, `editor`, `publisher`.
203
- - `POST /api/v1/compositions/:forkId/share-links { role, expiresAt? }` — create a token URL. Anyone with the token gets the role.
245
+ - `POST /api/v1/compositions/:forkId/permissions { grantee_email | grantee_customer_id, role, expires_at? }` — grant a specific registered user access. Exactly one of `grantee_email` or `grantee_customer_id` is required. Roles: `viewer`, `editor`, `publisher`. Returns `{ permission_id, fork_id, grantee_type, grantee_id, role, granted_by, expires_at, created_at }`.
246
+ - `POST /api/v1/compositions/:forkId/share-links { role, expires_at? }` — create a token URL. Anyone with the token gets the role. Returns `{ token, share_url, role, expires_at, ... }`.
204
247
  - `GET /api/v1/compositions/shared/:token` — access via share link
205
248
  - `DELETE /api/v1/compositions/:forkId/permissions/:permissionId` — revoke a grant
206
249
  - `DELETE /api/v1/compositions/:forkId/share-links/:token` — revoke a link
@@ -210,10 +253,13 @@ Fine-grained sharing uses per-user permissions and share links:
210
253
  Directors can clone another fork (their own or a shared one) as a new starting point:
211
254
 
212
255
  ```
213
- POST /api/v1/compositions { "parent_fork_id": "<forkId>", "parent_version": <N or omit for latest> }
256
+ POST /api/v1/compositions/:forkId/clone
257
+ Content-Type: application/json
258
+
259
+ { "parent_version": "<N or omit for latest>", "title": "optional" }
214
260
  ```
215
261
 
216
- The new fork inherits the parent's HTML/JSON at the specified version and creates a version-1 snapshot with `reason: "clone"`.
262
+ The `:forkId` in the path is the fork being cloned. The new fork inherits the parent's HTML/JSON at the specified version and creates a version-1 snapshot with `reason: "clone"`.
217
263
 
218
264
  ## Delete a fork
219
265
 
@@ -227,8 +273,8 @@ Soft-delete only. The fork's `deletedAt` is set. Versions and share links are pr
227
273
 
228
274
  Vidfarm charges directly in USD from the caller's wallet. There are no credits.
229
275
 
230
- - **Wallet top-up** — Stripe checkout via `POST /api/v1/user/me/wallet/checkout`
231
- - **Balance** — `GET /api/v1/user/me/wallet`
276
+ - **Wallet top-up** — Stripe checkout via the Settings UI, backed by `POST /settings/wallet/funding-link` (browser session; returns `{ checkout_url, client_reference_id }`)
277
+ - **Balance & history** — shown on the Settings page; billing events via `GET /u/:customerId/settings/wallet/events` (browser session). There is no `/api/v1/user/me/wallet` REST endpoint.
232
278
  - **Cost anchors**:
233
279
  - Publish (Remotion Lambda render): typically $0.01 – $0.10 per MP4 depending on length/resolution
234
280
  - Auto-decompose smart mode: pass-through of caller's AI provider spend
@@ -245,14 +291,14 @@ Framing:
245
291
 
246
292
  For agents that operate Vidfarm headlessly, the typical loop is:
247
293
 
248
- 1. `POST /api/v1/compositions { source_slug_id }` → fork
294
+ 1. `POST /api/v1/compositions { template_id }` → fork
249
295
  2. `PUT /api/v1/compositions/:forkId/composition.html` → apply edits
250
296
  3. `PATCH /api/v1/compositions/:forkId/composition.json` → update metadata
251
- 4. `POST /api/v1/compositions/:forkId/publish { tracer }` → render
297
+ 4. `POST /api/v1/compositions/:forkId/export { tracer }` → render
252
298
  5. Poll `GET /api/v1/compositions/:forkId/renders/:renderId` until `SUCCEEDED`
253
- 6. Use `output_url` in the next stage
299
+ 6. Use `outputUrl` in the next stage
254
300
 
255
- Always send a stable `tracer` on publish to make retries idempotent.
301
+ Send a stable `tracer` on export so retries are traceable and filterable in job history — but note that submission is **not idempotent**: every POST creates a new job (and a new charge) even with the same tracer. Do not blind-retry expensive submissions; check the render status first.
256
302
 
257
303
  ## Local dev loop with `vidfarm-devcli`
258
304
 
@@ -271,11 +317,16 @@ Flags: `--port`, `--dir`, `--host`, `--fork`, `--api-key` (or set `VIDFARM_API_K
271
317
 
272
318
  The dev source is persisted in `localStorage.VIDFARM_DEV_SOURCE`, so you don't need to keep the `?dev=` param in the URL after the first load.
273
319
 
320
+ `dev-serve` (`vidfarm-devcli` 0.3.2+) is realtime-safe:
321
+ - Writes coming through `PUT /composition.html` / `PATCH /composition.json` **do not loop back** as reload events — the server suppresses filesystem notifications for the just-written file for 500ms so the editor's own save never triggers a self-reload.
322
+ - The SSE `hello` event carries an `instance` id + a monotonic `cursor`. If the client reconnects after a server restart the browser detects the new instance and fires a `vidfarm:dev-reload` so the editor resyncs from disk. A 20s heartbeat keeps the SSE connection alive through intermediate proxies.
323
+ - Port collisions surface as a clear `[dev-serve] port <n> is already in use` message instead of a raw stack trace. Request bodies are capped at 8MB and time out after 15s so a stuck client can't hang the server. File paths are canonicalised so `../etc/passwd`-style paths cannot escape `--dir`.
324
+
274
325
  ## What NOT to do
275
326
 
276
327
  - Do **not** manipulate composition HTML by string concatenation. Always parse, edit, and re-serialize the DOM. The editor and runtime rely on `data-start`, `data-duration`, `data-track-index`, `data-hf-id`, `data-composition-id` attributes being well-formed.
277
328
  - Do **not** save AI provider keys in the composition HTML or JSON. They belong in the caller's provider-keys record.
278
- - Do **not** call Remotion directly. Publishing goes through `/publish` so billing, retries, cost caps, and Lambda quotas are enforced.
329
+ - Do **not** call Remotion directly. Publishing goes through `POST /api/v1/compositions/:forkId/export` so billing, retries, cost caps, and Lambda quotas are enforced.
279
330
  - Do **not** re-invent scene manipulation as REST commands (`AddScene`, `RemoveScene`, `UpdateSceneField`). The Trackpad Editor is the surface for that. The pattern is: mutate the composition HTML DOM, PUT it back.
280
331
 
281
332
  ## Alpha posture
@@ -283,7 +334,7 @@ The dev source is persisted in `localStorage.VIDFARM_DEV_SOURCE`, so you don't n
283
334
  Vidfarm is in active development. Endpoints, response shapes, and the editor UI can change. When something behaves unexpectedly:
284
335
 
285
336
  - prefer `GET /api/v1/compositions/:forkId` over cached state
286
- - treat `latestVersion` as the source of truth for what the editor loaded
337
+ - treat `latest_version` as the source of truth for what the editor loaded
287
338
  - if a publish fails, check `renders/:renderId` for the error phase and stderr before retrying
288
339
 
289
340
  ## Default assistance pattern
@@ -291,7 +342,7 @@ Vidfarm is in active development. Endpoints, response shapes, and the editor UI
291
342
  When a director asks "make me a video", the default sequence is:
292
343
 
293
344
  1. Ask what source material and what template style they want
294
- 2. Discover a matching template with `GET /api/v1/hyperframes`
345
+ 2. Discover a matching template with `GET /discover/feed`
295
346
  3. Fork it
296
347
  4. If they want an entirely new source video, guide them to `auto-decompose` first
297
348
  5. Open the Trackpad Editor and let them make edits
@@ -312,7 +363,7 @@ Example:
312
363
 
313
364
  ```bash
314
365
  curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
315
- -H "authorization: Bearer $VIDFARM_API_KEY" \
366
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
316
367
  -H "content-type: application/json" \
317
368
  -d '{"source_image_url": "https://cdn.example.com/photo.jpg"}'
318
369
  ```
@@ -323,6 +374,7 @@ Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback spe
323
374
 
324
375
  - `POST /api/v1/primitives/media/dedupe`
325
376
  - Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
377
+ - Note: webhook delivery is not yet active — `webhook_url` is accepted and persisted on the job but never fired. Poll the job endpoints (`GET /api/v1/primitives/jobs/:jobId`) for completion.
326
378
  - Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
327
379
  - Billing: metered as a Remotion render (via `remotion_lambda` on prod, local fallback in dev)
328
380
 
@@ -353,7 +405,7 @@ Video example (camouflage a reused clip):
353
405
 
354
406
  ```bash
355
407
  curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
356
- -H "authorization: Bearer $VIDFARM_API_KEY" \
408
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
357
409
  -H "content-type: application/json" \
358
410
  -d '{
359
411
  "tracer": "dedupe-2026-07-01-abc",
@@ -379,7 +431,7 @@ Image example:
379
431
 
380
432
  ```bash
381
433
  curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
382
- -H "authorization: Bearer $VIDFARM_API_KEY" \
434
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
383
435
  -H "content-type: application/json" \
384
436
  -d '{
385
437
  "tracer": "dedupe-still-01",
package/SKILL.platform.md CHANGED
@@ -66,7 +66,8 @@ compositions/forks/<forkId>/
66
66
  │ ├── composition.html # current editor state
67
67
  │ ├── composition.json # current metadata (scenes, captions, viral DNA)
68
68
  │ ├── manifest.json # fork identity + genealogy pointer
69
- └── ghostcut.json # GhostCut subtitle-removal task state (optional)
69
+ ├── ghostcut.json # GhostCut subtitle-removal task state (optional)
70
+ │ └── smart-decompose.json # raw smart-decompose snapshot: scenes (with visual descriptions), captions, viral DNA, audio transcript (optional)
70
71
  └── versions/<version>/
71
72
  ├── composition.html
72
73
  ├── composition.json
@@ -161,6 +162,7 @@ All three files (`composition.html`, `composition.json`, `manifest.json`) are tr
161
162
  - **Player adapter** — exposes `window.__player = { play(), pause(), seek(t), setTime(t), duration, playing, playbackRate, previewMuted, refreshClips() }` for the editor to control the iframe
162
163
  - **Media coordination** — respects `data-playback-start`, `data-volume`, and clip-specific mute
163
164
  - **Selection sync** — listens for `postMessage({ source: "hf-parent", ... })` from the parent editor to highlight/deselect clips
165
+ - **Origin-scoped messaging** — the runtime rejects inbound `message` events unless `event.source === window.parent`, so sibling iframes / popups cannot spoof play/pause/seek by forging `data.source`. Outbound `window.parent.postMessage(...)` calls are pinned to the parent's origin (seeded from `document.referrer` and refreshed from the first accepted control message) instead of `"*"`, so preview frame coordinates and layer ids no longer leak to any embedding third-party page.
164
166
 
165
167
  The same runtime is used inside the Trackpad Editor iframe AND when a published fork is embedded elsewhere. It is agnostic to whether it's being edited or viewed.
166
168
 
@@ -182,13 +184,13 @@ The editor is a single monolithic React component (~9k lines). It builds cleanly
182
184
 
183
185
  Publish flow:
184
186
 
185
- 1. Client `POST /api/v1/compositions/:forkId/publish`
187
+ 1. Client `POST /api/v1/compositions/:forkId/export` (the route is named `export`; the concept is "publish")
186
188
  2. API stamps `data-composition-id="<forkId>@v<N>"` into the composition HTML, PUTs the stamped HTML to working, and reads composition.json
187
189
  3. API resolves the caller's provider keys (preflight check for required models)
188
190
  4. API creates a `job` record and invokes the `hyperframes_render` primitive Lambda
189
191
  5. The primitive Lambda uploads the composition as a Remotion site, calls `renderMediaOnLambda` with the shared Remotion render Lambda ARN, waits for the MP4 to land in S3
190
192
  6. On success, the primitive returns `{ primary_file_url: "s3://.../output.mp4" }`, the API creates a `ForkVersionRecord` with `reason: "publish"`, writes the snapshot to `versions/<N>/`, and updates the fork's `latestVersion` and `currentRenderJobId`
191
- 7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ phase, progress, output_url, cost_usd }`
193
+ 7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, outputUrl, cost }` (see `adaptJobToPublishStatus` in `src/app.ts`)
192
194
 
193
195
  The Remotion Lambda is **shared across all templates and forks** — it's an infrastructure primitive, not a per-template deployment. Its ARN is passed to the Vidfarm stack as an env var.
194
196
 
@@ -197,9 +199,19 @@ The Remotion Lambda is **shared across all templates and forks** — it's an inf
197
199
  `POST /api/v1/compositions/:forkId/auto-decompose` splits a raw source video into a Trackpad-editable composition. Two modes:
198
200
 
199
201
  - **`time-slice`** (deterministic) — `src/hyperframes/composition.ts`'s `decomposeUndecomposedComposition()` splits the source duration into N equal-length scenes, wraps each in a `<div data-vf-timeline-proxy="video">` layer with `data-start`/`data-duration`/`data-playback-start`. Instant. No AI.
200
- - **`smart`** (AI) — `src/services/hyperframes.ts`'s `smartDecomposeVideo()` downloads the source, samples frames, calls Gemini/OpenAI/OpenRouter vision (in priority order) using the caller's saved keys, extracts scenes with labels and viral notes, extracts on-screen captions with position, and generates viral DNA. Then it also submits an async **GhostCut** subtitle-removal task if the source has hardcoded subs. Takes 30-60s + async ghostcut poll.
202
+ - **`smart`** (AI) — `src/services/hyperframes.ts`'s `smartDecomposeVideo()` downloads the source, samples frames, calls Gemini/OpenAI/OpenRouter vision (in priority order) using the caller's saved keys, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with position, and generates viral DNA. It also demuxes the audio track to WAV and **transcribes it verbatim** with timestamped segments (`transcribeAudioWithFallback()`: Gemini inline-audio first, OpenAI Whisper fallback; non-fatal — a silent video or transcription failure yields `transcript: null`). Then it also submits an async **GhostCut** subtitle-removal task if the source has hardcoded subs. Takes 30-60s + async ghostcut poll.
201
203
 
202
- Both modes update the composition.html and composition.json in place, and create a fork version snapshot with `reason: "manual"`.
204
+ Both modes update the composition.html and composition.json in place, and create a fork version snapshot with `reason: "manual"`. Smart mode additionally persists the raw result — scenes with descriptions, captions, viral DNA, and the transcript — to `working/smart-decompose.json`.
205
+
206
+ ### Video context (transcript + scene descriptions)
207
+
208
+ `GET /api/v1/compositions/:forkId/video-context.json` assembles a read-only, non-billing view over `smart-decompose.json`: the full verbatim transcript (`{ text, language, segments[] }`), every scene with its literal visual `description` and a computed `transcript_excerpt` (transcript segments overlapping the scene's time window), plus summary and viral DNA. Returns `{ status: "none" }` for forks that were never smart-decomposed. Same surface, three consumers:
209
+
210
+ 1. **Editor chat** — the optional `video_context` tool (registered alongside `http_request` in both `src/app.ts` and `infra/lambda/editor-chat.ts`) fetches it on demand; the shared system prompt in `src/editor-chat.ts` tells the model when to reach for it (what does the video say/show, caption/dub matching, scene-level edit planning).
211
+ 2. **`http_request`** — the route lives under `COMPOSITIONS_PREFIX`, so chat can also GET it directly (e.g. for a different fork).
212
+ 3. **Desktop agents (Claude Code / Codex)** — the devcli (`src/cli.ts`) syncs it to `video-context.json` on disk next to `composition.html` during `vidfarm edit`; `--refetch` refreshes it after a new decompose.
213
+
214
+ **Source-duration guardrail.** `probeMediaAsset()` in `src/services/media-processing.ts` enforces a hard cap of `MAX_PROBE_DURATION_SECONDS` (120s). If the probed source exceeds it, the probe throws `MediaDurationExceededError` and the route responds with `400 { code: "source_too_long", duration_seconds, max_duration_seconds }`. The same cap applies to inspiration ingest (`ensureInspirationReadyThenFork`) — over-long submissions land as `InspirationRecord { status: "failed", errorMessage }` without ever creating a Template or fork. This is baked into the probe (not the route) so every consumer — auto-decompose, ingest, `video_probe` / `audio_probe` primitives — is protected from repeated probes of long videos being used to burn Lambda / ffmpeg / vision-API budget.
203
215
 
204
216
  ## GhostCut integration
205
217
 
@@ -211,6 +223,8 @@ GhostCut is an external AI subtitle-removal service. Vidfarm proxies to it via `
211
223
 
212
224
  State is persisted per fork at `compositions/forks/<forkId>/working/ghostcut.json`. The client polls `POST /api/v1/compositions/:forkId/ghostcut-poll` every few seconds during smart decompose.
213
225
 
226
+ Read-only companion: `GET /api/v1/compositions/:forkId/ghostcut` returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }` without touching GhostCut. Non-billing. Exposed for two reasons: (1) the editor chat harness / AI copilot uses it to look up both video URLs on demand so downstream primitive calls can pick raw-upload vs caption-free-mirror without hardcoding either in chat context; (2) it's `http_request`-allowlisted under `COMPOSITIONS_PREFIX` in `src/app.ts`, which is why the editor chat system prompt (`src/editor-chat.ts`) directs the model to call it before feeding a video into a primitive route. The AI is instructed **not** to call `POST /ghostcut-poll` unless the user explicitly asks to advance subtitle removal.
227
+
214
228
  Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-through cost: `GHOSTCUT_USD_PER_30S` (default 0.10).
215
229
 
216
230
  ## Editor Chat
@@ -253,7 +267,7 @@ The billing Lambda has its own custom domain because Stripe webhooks require a s
253
267
 
254
268
  There are two kinds of things directors can fork:
255
269
 
256
- 1. **Draft templates** (`/hyperframes/:slug`) — pre-decomposed compositions checked into the repo under `templates/vidfarm_template_XXXX/`. Each has a `composition.html`, `composition.json`, `template.config.json`, a `SKILL.md`, and a `src/` if it needs custom Remotion.
270
+ 1. **Draft templates** — pre-decomposed compositions checked into the repo under `templates/vidfarm_template_XXXX/`. Each has a `composition.html`, `composition.json`, `template.config.json`, a `SKILL.md`, and a `src/` if it needs custom Remotion. Registered templates open in the browser at `/editor/:templateId` (there is no `/hyperframes/:slug` route).
257
271
  2. **Submitted videos** — raw uploads from directors, decomposed on demand via auto-decompose.
258
272
 
259
273
  Templates are static. Compositions/forks are the mutable working copies.
@@ -286,10 +300,10 @@ CDK stacks are under `infra/cdk/`:
286
300
  Deploy commands are in `package.json`:
287
301
 
288
302
  - `npm run cdk:synth:staging-serverless`
289
- - `npm run cdk:deploy:staging-serverless`
290
- - `npm run cdk:deploy:prod`
303
+ - `npm run cdk:deploy:staging-serverless` (and `npm run cdk:deploy:staging-serverless:hotswap` for Lambda-code-only iteration)
304
+ - `npm run cdk:deploy:prod-serverless` (and `npm run cdk:deploy:prod-serverless:hotswap`)
291
305
 
292
- Never call bare `cdk deploy`. Always go through the npm scripts so the correct `.env.<stage>` is loaded.
306
+ Never call bare `cdk deploy`. Always go through the npm scripts the `cdk:*` scripts load `.env.<stage>` via `dotenv-cli` (`dotenv -e .env.<stage> -- npx aws-cdk ...`) and pick the right stack app.
293
307
 
294
308
  Deploy order: **staging first**, then production. Never skip staging.
295
309
 
@@ -314,7 +328,7 @@ Top-level:
314
328
  - `templates/` — draft template sources
315
329
  - `auto-create-hyperframe-templates/` — extractor prompts / schemas for new template creation
316
330
  - `scripts/` — one-shot ops scripts and migration scripts
317
- - `data/` — local dev SQLite + storage (gitignored)
331
+ - `data/` — local dev storage (gitignored). Also holds leftover `*.sqlite` / `vidfarm.db` files from the retired SQLite/EC2 era — the runtime no longer reads SQLite anywhere; DynamoDB is the only database.
318
332
  - `.env.staging`, `.env.production` — stage config (gitignored)
319
333
 
320
334
  Legacy but still active:
@@ -332,7 +346,7 @@ Legacy but still active:
332
346
 
333
347
  ## Where to look for things
334
348
 
335
- - Composition endpoints — `src/app.ts` under `COMPOSITIONS_PREFIX` (~line 6970+)
349
+ - Composition endpoints — `src/app.ts`: grep for `COMPOSITIONS_PREFIX` (the block runs from `app.post(COMPOSITIONS_PREFIX` fork creation through the `versions` routes; line numbers drift, always grep the symbol)
336
350
  - Fork CRUD & permissions — `src/services/serverless-records.ts`, `src/services/fork-access.ts`
337
351
  - Storage keys + public-read prefix list — `src/services/storage.ts`
338
352
  - Fork manifest.json helper — `src/services/fork-manifest.ts`
@@ -363,10 +377,13 @@ vidfarm-devcli dev-serve --dir ./my-fork --port 4321
363
377
 
364
378
  Edit flags: `--port`, `--dir`, `--host`, `--fork`, `--api-key` (or `VIDFARM_API_KEY`), `--share`, `--refetch`.
365
379
 
366
- `dev-serve` behavior:
380
+ `dev-serve` behavior (`vidfarm-devcli` 0.3.2+):
367
381
  - Serves `composition.html` / `composition.json` / `manifest.json` (and `/versions/{n}/…`) with CORS `*`.
368
382
  - Accepts `PUT /composition.html` and `PATCH /composition.json`.
369
- - Watches the dir with `fs.watch`, coalesces bursts, fans out reload events via `GET /_events` SSE.
383
+ - Watches the dir with `fs.watch`, coalesces bursts (80ms), fans out reload events via `GET /_events` SSE.
384
+ - Reload loop prevention: any successful `PUT` / `PATCH` marks the target file "self-write suppressed" for 500ms so the fs.watch event fired by the write we just performed does NOT round-trip back to the editor as a reload event.
385
+ - SSE `hello` includes `{ at, instance, cursor }`. `instance` changes on every dev-serve restart so a reconnecting client can detect a restart and force a resync; `cursor` is a monotonic counter that bumps on every broadcast reload. Heartbeat `: heartbeat <ts>` every 20s keeps intermediate proxies from timing out the connection.
386
+ - Safety rails: request bodies capped at 8MB with a 15s body-read timeout; path resolution runs through `path.relative(rootDir, ...)` so `versions/../../etc/passwd`-style paths cannot escape the served directory; `EADDRINUSE` produces a plain-English error instead of a raw stack; broadcast iterates a snapshot of the client set so a mid-broadcast disconnect can't skip healthy clients; `SIGINT` + `SIGTERM` both close cleanly with a 2s hard-exit fallback.
370
387
 
371
- The editor side lives in `demo/src/dev-mode.ts` — a self-installing module imported at the top of `demo/src/main.tsx`. It reads `?dev=<url>` (persisted to `localStorage.VIDFARM_DEV_SOURCE`), monkey-patches `window.fetch` to rewrite `/api/v1/compositions/{forkId}/*` → `${devSource}/*`, injects a persistent orange banner with an Exit button, and dispatches `vidfarm:dev-reload` on SSE reload events. `HyperframesStudioEditor.tsx` listens for that event and re-runs the composition fetch effect with `cache: "reload"`.
388
+ The editor side lives in `demo/src/dev-mode.ts` — a self-installing module imported at the top of `demo/src/main.tsx`. It reads `?dev=<url>` (persisted to `localStorage.VIDFARM_DEV_SOURCE`), monkey-patches `window.fetch` to rewrite `/api/v1/compositions/{forkId}/*` → `${devSource}/*`, injects a persistent orange banner with an Exit button, and dispatches `vidfarm:dev-reload` on SSE reload events. It also caches the `hello.instance` and re-dispatches `vidfarm:dev-reload` with `{ reason: "server-restart" }` when a reconnect sees a different instance. `HyperframesStudioEditor.tsx` listens for that event and re-runs the composition fetch effect with `cache: "reload"`.
372
389
  - Lambdas — `infra/lambda/*.ts`
package/dist/src/cli.js CHANGED
@@ -127,7 +127,10 @@ async function resolveForkId(input) {
127
127
  return null;
128
128
  }
129
129
  async function fetchCompositionFiles(input) {
130
- const files = ["composition.html", "composition.json", "manifest.json"];
130
+ // video-context.json carries the source video's audio transcript and
131
+ // per-scene visual descriptions so desktop agents (Claude Code, Codex)
132
+ // can ground edits in what the video actually says and shows.
133
+ const files = ["composition.html", "composition.json", "manifest.json", "video-context.json"];
131
134
  for (const filename of files) {
132
135
  const target = path.join(input.dir, filename);
133
136
  if (existsSync(target) && !input.refetch) {
@@ -147,10 +150,12 @@ async function fetchCompositionFiles(input) {
147
150
  }
148
151
  function buildAuthHeaders(input) {
149
152
  const headers = { accept: "text/html,application/json" };
153
+ // The server reads exactly these header names (see requireAuth and
154
+ // readShareToken in src/app.ts) — Bearer/authorization is not supported.
150
155
  if (input.apiKey)
151
- headers.authorization = `Bearer ${input.apiKey}`;
156
+ headers["vidfarm-api-key"] = input.apiKey;
152
157
  if (input.shareToken)
153
- headers["x-vidfarm-share-token"] = input.shareToken;
158
+ headers["vidfarm-share-token"] = input.shareToken;
154
159
  return headers;
155
160
  }
156
161
  function printEditorBanner(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. Point it at a template id, edit composition.html on disk (Claude Code, Codex, etc.), preview live in the browser at https://vidfarm.cc/editor/.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -39,11 +39,11 @@
39
39
  "build": "npm run build:frontend && npm run build:hyperframes-editor && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
40
40
  "start": "node --import ./dist/src/instrument.js --enable-source-maps dist/src/index.js",
41
41
  "check": "tsc -p tsconfig.json --noEmit",
42
- "cdk:synth": "npm run build && npx aws-cdk synth --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
43
- "cdk:deploy": "npm run build && npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
44
- "cdk:deploy:prod-serverless": "npm run build && npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
45
- "cdk:synth:staging-serverless": "npm run build && npx aws-cdk synth --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
46
- "cdk:deploy:staging-serverless": "npm run build && npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
42
+ "cdk:deploy:prod-serverless": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
43
+ "cdk:synth:staging-serverless": "npm run build && dotenv -e .env.staging -- npx aws-cdk synth --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
44
+ "cdk:deploy:staging-serverless": "npm run build && dotenv -e .env.staging -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
45
+ "cdk:deploy:staging-serverless:hotswap": "npm run build && dotenv -e .env.staging -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js' --hotswap-fallback --require-approval never",
46
+ "cdk:deploy:prod-serverless:hotswap": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js' --hotswap-fallback --require-approval never",
47
47
  "prepack": "npm run build"
48
48
  },
49
49
  "dependencies": {
@@ -92,6 +92,7 @@
92
92
  "@types/react-dom": "^18.3.7",
93
93
  "aws-cdk-lib": "^2.196.0",
94
94
  "constructs": "^10.4.2",
95
+ "dotenv-cli": "^11.0.0",
95
96
  "tsx": "^4.19.4",
96
97
  "typescript": "^5.8.3",
97
98
  "zustand": "^5.0.13"