@mevdragon/vidfarm-devcli 0.3.4 → 0.3.5

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
@@ -443,3 +443,39 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
443
443
  }
444
444
  }'
445
445
  ```
446
+
447
+ ## Brainstorm primitives
448
+
449
+ The `brainstorm/*` primitives are the strategy toolkit. They are reusable, billable AI reasoning steps — the same family the AI Copilot exposes as chip suggestions. Treat **product placement** as a first-class member of this family, right alongside angles and hooks:
450
+
451
+ - `POST /api/v1/primitives/brainstorm/coldstart` — `{ payload: { user_message } }` → foundational questionnaire for a customer starting from zero.
452
+ - `POST /api/v1/primitives/brainstorm/awareness_stages` — `{ payload: { offer_description } }` → which Eugene-Schwartz awareness stages to target first.
453
+ - `POST /api/v1/primitives/brainstorm/angles` — `{ payload: { offer_description, problem_awareness, solution_awareness } }` → persuasive angles.
454
+ - `POST /api/v1/primitives/brainstorm/hooks` — `{ payload: { offer_description } }` → many TikTok-native hooks.
455
+ - `POST /api/v1/primitives/brainstorm/product_placement` — `{ payload: { source_video_url, offer_description } }` → **watches the video** and returns concrete, timestamped opportunities to natively place the product.
456
+
457
+ ### Primitive: brainstorm_product_placement
458
+
459
+ Analyze a source video and identify where a product can be organically placed so it feels native rather than bolted on. This is a **multimodal** primitive: it feeds the actual video to a vision-capable model, so prefer a saved **Gemini** key (native video understanding). OpenRouter multimodal models also work; plain OpenAI chat keys cannot watch video.
460
+
461
+ - `POST /api/v1/primitives/brainstorm/product_placement`
462
+ - Body: `{ "tracer": "...", "payload": { "source_video_url": "...", "offer_description": "...", "count"?: 8, "provider"?: "gemini" }, "webhook_url"?: "..." }`
463
+ - `source_video_url` (required, URL) — the exact durable/public video to analyze. Aliases accepted: `video_url`, `source_url`, `url`. For a fork's composition, call `GET /api/v1/compositions/:forkId/ghostcut` first and pass the correct raw-upload vs caption-free-mirror URL.
464
+ - `offer_description` (required) — the product/offer to place. Aliases: `offer`, `description`, `details`.
465
+ - `count` (optional, 3–30, default 8) — number of opportunities; only send when the user asks for a specific number.
466
+ - Response: standard primitive job. Poll `GET /api/v1/primitives/jobs/:jobId` to completion, then read `result.json` (also surfaced on the job output as `opportunities[]`), where each entry has `timestamp`, `scene`, `placement_type`, `placement_idea`, and `why_it_works`.
467
+
468
+ You can also satisfy a quick, conversational product-placement question by reasoning over an attached video directly instead of calling this primitive — use the primitive when you want a durable, structured, billable artifact the customer can save and reuse.
469
+
470
+ ```bash
471
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/brainstorm/product_placement" \
472
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
473
+ -H "content-type: application/json" \
474
+ -d '{
475
+ "tracer": "product-placement-01",
476
+ "payload": {
477
+ "source_video_url": "https://cdn.example.com/source.mp4",
478
+ "offer_description": "A $29/mo AI meal-planning app for busy parents."
479
+ }
480
+ }'
481
+ ```
package/dist/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { parseArgs } from "node:util";
5
5
  // vidfarm-devcli — thin CLI that pairs a local composition directory with the
@@ -12,6 +12,7 @@ const HELP = `vidfarm-devcli — local editor bridge for the Vidfarm Trackpad Ed
12
12
 
13
13
  Usage:
14
14
  vidfarm-devcli <template_id> [options] Start local editor session
15
+ vidfarm-devcli publish <template_id> [opts] Push local composition to your fork
15
16
  vidfarm-devcli dev-serve [options] Serve an existing local dir only
16
17
  vidfarm-devcli --help Show this help
17
18
 
@@ -24,6 +25,14 @@ Options (edit mode):
24
25
  --share <token> Share token for a shared fork (public read)
25
26
  --refetch Overwrite local composition files with the latest remote copy
26
27
 
28
+ Options (publish mode):
29
+ --dir <path> Local composition directory (default: .vidfarm/<template_id>)
30
+ --host <url> Vidfarm host to publish to (default: ${DEFAULT_HOST})
31
+ --fork <id> Fork id to publish into (default: auto-resolve for the current user)
32
+ --api-key <key> Vidfarm API key (or set VIDFARM_API_KEY) — required to write
33
+ --message <text> Optional message attached to the published version snapshot
34
+ --no-snapshot Only update the working copy; skip the version snapshot
35
+
27
36
  Options (dev-serve mode):
28
37
  --dir <path> Directory to serve (default: current dir)
29
38
  --port <n> Port to listen on (default: ${DEFAULT_PORT})
@@ -48,6 +57,10 @@ async function main() {
48
57
  await runEditCommand(argv.slice(1));
49
58
  return;
50
59
  }
60
+ if (command === "publish") {
61
+ await runPublishCommand(argv.slice(1));
62
+ return;
63
+ }
51
64
  if (!command.startsWith("-")) {
52
65
  // Positional template id → default `edit` command.
53
66
  await runEditCommand(argv);
@@ -100,6 +113,147 @@ async function runEditCommand(argv) {
100
113
  const { runDevServeCommand } = await import("./dev-serve.js");
101
114
  await runDevServeCommand(["--dir", rootDir, "--port", String(port)]);
102
115
  }
116
+ async function runPublishCommand(argv) {
117
+ const parsed = parseArgs({
118
+ args: argv,
119
+ allowPositionals: true,
120
+ options: {
121
+ dir: { type: "string" },
122
+ host: { type: "string", default: DEFAULT_HOST },
123
+ fork: { type: "string" },
124
+ "api-key": { type: "string" },
125
+ share: { type: "string" },
126
+ message: { type: "string" },
127
+ "no-snapshot": { type: "boolean", default: false }
128
+ }
129
+ });
130
+ const templateId = parsed.positionals[0];
131
+ if (!templateId && !parsed.values.dir) {
132
+ throw new Error(`publish requires a template id (or --dir).\n\n${HELP}`);
133
+ }
134
+ const host = trimTrailingSlash(parsed.values.host);
135
+ const apiKey = parsed.values["api-key"] ?? process.env.VIDFARM_API_KEY;
136
+ const shareToken = parsed.values.share;
137
+ if (!apiKey && !shareToken) {
138
+ throw new Error("publish needs credentials to write. Pass --api-key <key> or set VIDFARM_API_KEY.");
139
+ }
140
+ const rootDir = path.resolve(process.cwd(), parsed.values.dir ?? path.join(".vidfarm", templateId ?? ""));
141
+ const forkId = parsed.values.fork ?? (templateId
142
+ ? await resolveForkId({ host, templateId, apiKey, shareToken })
143
+ : null);
144
+ if (!forkId) {
145
+ throw new Error(`Could not resolve a fork id. Pass --fork <id>${templateId ? "" : " (no template id was given to auto-resolve)"}.`);
146
+ }
147
+ const htmlPath = path.join(rootDir, "composition.html");
148
+ if (!existsSync(htmlPath)) {
149
+ throw new Error(`No composition.html at ${htmlPath}. Run \`vidfarm-devcli ${templateId ?? "<template>"} --fork ${forkId}\` first, or pass --dir.`);
150
+ }
151
+ const html = readFileSync(htmlPath, "utf8");
152
+ if (!html.includes("data-composition-id=")) {
153
+ throw new Error("composition.html is missing data-composition-id — refusing to publish a malformed composition.");
154
+ }
155
+ const jsonPath = path.join(rootDir, "composition.json");
156
+ const json = existsSync(jsonPath) ? readFileSync(jsonPath, "utf8") : null;
157
+ if (json !== null) {
158
+ try {
159
+ JSON.parse(json || "{}");
160
+ }
161
+ catch (error) {
162
+ throw new Error(`composition.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
163
+ }
164
+ }
165
+ const auth = { apiKey, shareToken };
166
+ console.log(`[vidfarm] publishing ${forkId} → ${host}`);
167
+ // 1) Working copy: composition.html (required) then composition.json (if any).
168
+ await putComposition(`${host}/api/v1/compositions/${encodeURIComponent(forkId)}/composition.html`, "PUT", html, "text/html; charset=utf-8", auth);
169
+ console.log(`[vidfarm] pushed composition.html (${Buffer.byteLength(html)} bytes)`);
170
+ if (json !== null) {
171
+ await putComposition(`${host}/api/v1/compositions/${encodeURIComponent(forkId)}/composition.json`, "PATCH", json, "application/json", auth);
172
+ console.log(`[vidfarm] pushed composition.json (${Buffer.byteLength(json)} bytes)`);
173
+ }
174
+ // 2) Optional immutable version snapshot.
175
+ let publishedVersion = null;
176
+ if (!parsed.values["no-snapshot"]) {
177
+ const snapshot = await postJson(`${host}/api/v1/compositions/${encodeURIComponent(forkId)}/versions`, { message: parsed.values.message ?? null }, auth);
178
+ publishedVersion = typeof snapshot?.version === "number" ? snapshot.version : null;
179
+ console.log(`[vidfarm] snapshotted version ${publishedVersion ?? "(unknown)"}`);
180
+ }
181
+ printPublishBanner({ forkId, dir: rootDir, host, version: publishedVersion, snapshotted: !parsed.values["no-snapshot"] });
182
+ }
183
+ // Writes one composition file to the remote fork. Unlike the edit-mode
184
+ // fetchCompositionFiles (which tolerates misses), publish MUST fail loudly — a
185
+ // silent skip would make the user think their work is safely stored when it is
186
+ // not.
187
+ async function putComposition(url, method, body, contentType, auth) {
188
+ const res = await fetch(url, {
189
+ method,
190
+ headers: { ...buildAuthHeaders(auth), "content-type": contentType },
191
+ body
192
+ });
193
+ if (!res.ok) {
194
+ throw new Error(await describeHttpFailure(res, url));
195
+ }
196
+ }
197
+ async function postJson(url, body, auth) {
198
+ const res = await fetch(url, {
199
+ method: "POST",
200
+ headers: { ...buildAuthHeaders(auth), "content-type": "application/json" },
201
+ body: JSON.stringify(body)
202
+ });
203
+ if (!res.ok) {
204
+ throw new Error(await describeHttpFailure(res, url));
205
+ }
206
+ return await res.json().catch(() => ({}));
207
+ }
208
+ async function describeHttpFailure(res, url) {
209
+ let detail = "";
210
+ try {
211
+ const text = await res.text();
212
+ if (text) {
213
+ try {
214
+ const parsed = JSON.parse(text);
215
+ detail = parsed.error ? ` — ${parsed.error}${parsed.type ? ` (${parsed.type})` : ""}` : ` — ${text.slice(0, 300)}`;
216
+ }
217
+ catch {
218
+ detail = ` — ${text.slice(0, 300)}`;
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ // ignore body read failure
224
+ }
225
+ const hint = res.status === 401 || res.status === 403
226
+ ? " Check your --api-key and that you own (or have edit rights on) this fork."
227
+ : res.status === 413
228
+ ? " The composition exceeds the server size limit."
229
+ : res.status === 429
230
+ ? " You are being rate-limited; wait and retry."
231
+ : "";
232
+ return `Publish failed: ${res.status} ${res.statusText}${detail} (${url}).${hint}`;
233
+ }
234
+ function printPublishBanner(input) {
235
+ const bold = "\x1b[1m";
236
+ const green = "\x1b[32m";
237
+ const cyan = "\x1b[36m";
238
+ const dim = "\x1b[2m";
239
+ const reset = "\x1b[0m";
240
+ const line = `${dim}${"─".repeat(74)}${reset}`;
241
+ console.log("");
242
+ console.log(line);
243
+ console.log(`${bold}${green} Published to Vidfarm${reset}`);
244
+ console.log(line);
245
+ console.log(` fork ${input.forkId}`);
246
+ console.log(` local dir ${input.dir}`);
247
+ if (input.snapshotted) {
248
+ console.log(` version ${input.version ?? "(unknown)"}`);
249
+ }
250
+ else {
251
+ console.log(` version ${dim}skipped (--no-snapshot)${reset}`);
252
+ }
253
+ console.log(` live html ${cyan}${input.host}/api/v1/compositions/${input.forkId}/composition.html${reset}`);
254
+ console.log(line);
255
+ console.log("");
256
+ }
103
257
  async function resolveForkId(input) {
104
258
  // /editor/<template_id> issues a 302 → /editor/<template_id>?fork=<id>
105
259
  // whenever the caller (or the template's default fork) has one. Follow the
@@ -11,7 +11,11 @@ import { parseArgs } from "node:util";
11
11
  const CORS_HEADERS = {
12
12
  "access-control-allow-origin": "*",
13
13
  "access-control-allow-methods": "GET,PUT,PATCH,POST,OPTIONS",
14
- "access-control-allow-headers": "content-type,authorization",
14
+ // The editor page (vidfarm.cc) fetches us cross-origin and its Sentry
15
+ // instrumentation attaches distributed-tracing headers (sentry-trace,
16
+ // baggage). List them so the preflight passes; the OPTIONS handler also
17
+ // echoes whatever the browser actually asks for, which is the real guard.
18
+ "access-control-allow-headers": "content-type,authorization,vidfarm-api-key,vidfarm-share-token,sentry-trace,baggage",
15
19
  "access-control-max-age": "600"
16
20
  };
17
21
  // Cap request bodies at 8MB so a runaway or malicious client can't OOM the
@@ -155,7 +159,14 @@ function handleRequest(req, res, ctx) {
155
159
  const url = new URL(req.url ?? "/", `http://localhost`);
156
160
  const pathname = url.pathname;
157
161
  if (method === "OPTIONS") {
158
- res.writeHead(204, CORS_HEADERS);
162
+ // Echo the exact headers the browser requested so a preflight never fails
163
+ // over an unexpected header (Sentry tracing, etc.). This is a local dev
164
+ // tool, so permissive CORS is fine.
165
+ const requested = req.headers["access-control-request-headers"];
166
+ res.writeHead(204, {
167
+ ...CORS_HEADERS,
168
+ "access-control-allow-headers": typeof requested === "string" && requested ? requested : CORS_HEADERS["access-control-allow-headers"]
169
+ });
159
170
  res.end();
160
171
  return;
161
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mevdragon/vidfarm-devcli",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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": {