@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 +36 -0
- package/dist/src/cli.js +155 -1
- package/dist/src/dev-serve.js +13 -2
- package/package.json +1 -1
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
|
package/dist/src/dev-serve.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|