@officexapp/vidfarm-devcli 0.21.58 → 0.21.60

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.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm
3
- skill_version: 0.21.58
3
+ skill_version: 0.21.60
4
4
  description: Entry point for making, editing, and rendering short-form video with Vidfarm. Use when the task is to create a video, ad, explainer, or social clip; to edit or re-theme an existing composition; to source footage or audio; or to run a batch of video variants. Routes to the full director skill and the `vidfarm` CLI.
5
5
  ---
6
6
 
@@ -123,6 +123,8 @@ A capable agent can replicate a template entirely on brush 1 and 2, on the free
123
123
 
124
124
  **There is a fourth source, and for a weak model it is the FIRST one: a template somebody already decomposed.** `/discover` holds thousands of viral videos that have been taken apart into their scene-by-scene DNA — hook, retention, payoff, cut rhythm, typography, per-beat paintbrush assignments — and forking one is free. That decomposition costs $1+ in AI spend to reproduce yourself, so always check for an existing one before paying to re-run it. `vidfarm discover --query "<the client's problem, in their words>" --json`; a row carrying `summary`, `keywords` and `promotions` is decomposed. **Weak models: this is not an optimisation, it is the method** — see `vidfarm.cc/flash-harness.md`.
125
125
 
126
+ **A fork is not a half-finished video. It is somebody else's finished one.** Every element in it was chosen for their product, their customer, their face. So the moment you fork, audit the composition element by element and give each one a verdict — KEEP, REPLACE or CUT — before you plan or source anything. See the standard in §6.
127
+
126
128
  ## 3. Worked example — one video, end to end
127
129
 
128
130
  ```bash
@@ -215,6 +217,7 @@ Say the phase out loud when you switch. Deciding to source mid-build is how a $0
215
217
 
216
218
  The standards themselves are not optional. `vidfarm qa <dir>` is — it is a free local checker for most of them, feedback rather than a gate. Nothing calls it, skipping it is fine, and it allows one fix round by default. **Watching the render is the review that counts.** Full reasoning: `references/hooks-and-virality.md` and `references/reviewing-renders.md`.
217
219
 
220
+ - **Forked a decomposed template? Audit every element, and KEEP is the verdict that needs a reason.** List the layers (`vidfarm pull <forkId> --json` → `.layers[]`, plus `grep -o 'data-src="[^"]*"' composition.html | sort -u`) and write `./work/ADAPTATION.md`: one row per element, verdict KEEP / REPLACE / CUT, why, and what replaced it. **Structure is DNA and stays** — beat count and order, `start`/`duration`, cut rhythm, transitions, caption identity (font, size, placement, background). **Meaning is content and goes** — voiceover, music, caption words, on-screen titles and stats and prices, actor and talking-head clips, product shots and screenshots, stickers that name a thing, the picture behind a background, and every logo, wordmark, @handle, URL and watermark (those are never a KEEP). Generic b-roll may stay when it is still true in the client's world. Run the audit **before** sourcing — every REPLACE row is a line on the procurement list — and report the counts: kept / replaced / cut. Swapping the captions and nothing else is the most common rejected video there is. Full table: `vidfarm.cc/flash-harness.md` §3½.
218
221
  - **Write the four charges before the timeline.** Hook: first line is a complete clause naming a situation, on screen at `start:0`. Loop: one open question by 0:10, closing inside this video. Payoff: shown, not summarized. Bait: one ask, final beat. Banned openers — throat-clearing, a logo, a title card, a fade from black.
219
222
  - **No HTML slop.** Test: could you have made this element inside TikTok's own editor? If you reached past that — a padded capsule, border, gradient fill, blur panel, card, CTA "button", benefit chips, feature grid — cut it. Nothing in a video is clickable. Emphasize with size, weight, caps, an accent colour, or a drawn mark.
220
223
  - **No layout templates — judge the whole frame.** The stack is the tell, not the box: headline + subheadline + CTA centred in a well is a landing page even with every border, fill and capsule stripped off. Banned at frame level — a modal/dialog floating over a dimmed or blurred backdrop, a full-frame wash used to stage a floating block, nav/hero/feature-row sections, a blurred website screenshot as the background plate, document margins. Unstack it into time: hook at t=0, support line on the next cut, CTA spoken or a bare caption. Are you arranging words relative to each other, or relative to the picture? Two text runs at once is the ceiling.
@@ -271,6 +274,10 @@ Also served at `vidfarm.cc`: `/flash-harness.md` (**the weak-model process harne
271
274
 
272
275
  **The two marketplace harnesses never mix.** A buyer's agent must never load the clipper loop and a clipper must never load the buyer loop. If the user's side is unclear, ask: *"Do you want videos made for you, or do you want to get paid to make videos for other people?"*
273
276
 
274
- **But an ask about EARNING has already answered it — do not ask.** *"help me earn on vidfarm marketplace"*, *"help me make money on vidfarm"*, *"find me video gigs"*, *"any work today?"*, *"check the vending machines"*, *"run in agentic clipper mode"* all mean the **clipper** side: load `/agentic-clipper.md`, then run the four moves — read the feed's recent notifications for who is asking right now, pull the `GIG_…` id out of each notification's join link and check `available_funds` before committing, join the funded ones with a Base L2 `wallet_address` in the same call, then `GET /work/available` and poll a queue for a task. The copy-paste version is in `/marketplace.md` → *"help me earn on vidfarm" — the whole first session*.
277
+ **But an ask about EARNING has already answered it — do not ask.** *"help me earn on vidfarm marketplace"*, *"help me make money on vidfarm"*, *"find me video gigs"*, *"any work today?"*, *"check the vending machines"*, *"run in agentic clipper mode"* all mean the **clipper** side: load `/agentic-clipper.md`, then run the four moves — read the feed's recent notifications for who is asking right now, pull the `GIG_…` id out of each notification's join link and check `available_funds` before committing, join the funded ones with a Base L2 `wallet_address` in the same call, then `GET /work/available` and poll a queue for a task. The copy-paste version is in `/marketplace.md` → *"help me earn on vidfarm" — the whole first session*. **Those four moves are the first session; a loop that runs for weeks is paced.** `GET /work/available` is the MAP (one call, every machine) and `POST /gigs/:id/queue/poll` is the CLAIM (per machine) — so walk only the machines the map lit up, one at a time, until your build slots (`max_subagents`) are full, stop there, and resume the walk from that machine when a slot opens. **Never re-poll a machine inside 12 hours** unless a new bell rang for it, the map says it holds work, a proof of yours there was approved or paid, or the gigworker asks; **sweep the notifications twice a day**, not every cycle, and join the funded machines it turns up. A task you cannot deliver is **declined** (`POST /gigs/:id/queue/:msgId/decline` — free, per-worker, and without it the same task heads your FIFO forever), never held and never substituted; a task that names no format is where the clipper's own specialty goes. Full apparatus: `/agentic-clipper.md` § 3⅞ and § 4 ①–②.
275
278
 
276
279
  **Then ask the gigworker one more question: the RUN MODE.** It decides which stages of every task stop for them — `full-interactive` (shortlist · plan · raws · cut), `quick-interactive` (shortlist · cut), `auto-batch` (cut only — build the batch unattended, review it in one sitting), `auto-submit` (nothing; opt-in, in their own words). The middle two are the common ones. `vidfarm clipper-run mode` explains all four and saves the answer, `vidfarm clipper-run next` says what to do right now, and `vidfarm clipper-run gate <task>` exits non-zero while a human gate is open — so `gate && gigs submit …` cannot send an unreviewed cut. Full section: `/marketplace.md` → *The four run modes*.
280
+
281
+ **Then offer the CLIPPER DASHBOARD — `vidfarm dashboard` — and expect a yes.** Three of the four run modes stop to make a human *watch a video*, and a terminal cannot play one; without the dashboard the gigworker reads a path out of `REVIEW_QUEUE.md`, opens the file by hand, and types a verb, once per task. `vidfarm dashboard` is that queue as one localhost page over the same `CLIPPER/` folder — no key, no network, no account: the cut plays and seeks, the buyer's brief sits beside it, the checks are tickable and the unticked ones pre-fill the rejection note, the shortlist candidates play side by side (`1`/`2`/`3` to pick), and `A`/`C`/`D`/`U` answer the gate. Every button calls the same `clipper-run` transition the CLI calls, so the page and the terminal can never disagree. Ask once, record it in `MISSION.md` as `dashboard: yes | no`, and never depend on it — a gigworker who declines keeps `REVIEW_QUEUE.md`, `clipper-run review` and the `gate` rail exactly as they were. Full section: `/agentic-clipper.md` § 3⅘.
282
+
283
+ **Before ANY proof is submitted, run the sanity check — every time, on every side of the loop.** `vidfarm gigs verify-proof --proof <url> --clean-master ./final-clean.mp4` needs no api key. It loads each proof url the way a stranger's browser would (no key, no cookie) and exits non-zero on a 403 bucket, a 404, an s3 AccessDenied body served as `video/mp4`, an **expiring presigned url** (read access dies before the buyer swipes), a `localhost`/Drive/Dropbox link, or a 0-byte file. It then checks the delivery is split the right way: the **watermarked** cut public in `proofs[]`, the **clean master** sealed in `private_note` (`--clean-master`) — recommended, not required, so putting the unwatermarked final in the proof itself only warns, and `--clean-in-proof` says you meant it. `vidfarm gigs submit` runs the same check and refuses a failing proof. The two gates are different questions: `clipper-run gate` = *did a human watch it*, `gigs verify-proof` = *can the buyer open it*. Full section: `/marketplace.md` → *the sanity check before you send*.
package/clipper.md CHANGED
@@ -144,7 +144,9 @@ Read and report back to the user, in plain language:
144
144
  - **`available_funds` / `contract_address`** — whether the gig can pay on-chain (see Phase 5).
145
145
  - **`default_rate_limit_count` / `default_rate_limit_minutes`** — your ceiling. Never poll past it.
146
146
  - **`task_timeout`** — how long you may hold a task before it expires on you.
147
- - **`review_timeout`** — proofs auto-approve after this (default 48h) if the client doesn't review.
147
+ - **`review_timeout`** — seconds until an unreviewed proof auto-approves. **`-1` means never**:
148
+ the client reviews every proof by hand, so nothing pays until they say so. Every vidfarm
149
+ vending machine is `-1`. Read this field before you count on a deadline.
148
150
 
149
151
  **Then judge honestly, out loud:** can you actually deliver this brief with Vidfarm on this
150
152
  machine? If the gig wants 4K motion-graphics work and cost mode is `minimize`, say so now. A gig
@@ -402,8 +404,10 @@ Check the response:
402
404
  - **`409`** — duplicate `task_identifier`. You already submitted this one; don't resubmit.
403
405
  - **`410`** — the task expired while you were rendering. Ask the client for an extension, or move on.
404
406
 
405
- Proofs auto-approve after `review_timeout` (default 48h) if the client doesn't review. Price is
406
- locked at submission time, so a client lowering the price later can't reduce what they already owe.
407
+ Proofs auto-approve after `review_timeout` if the client doesn't review unless it is `-1`,
408
+ which is manual review and never auto-approves. Every vidfarm vending machine is `-1`, so on
409
+ those your proof waits for a human verdict however long that takes. Price is locked at submission
410
+ time, so a client lowering the price later can't reduce what they already owe.
407
411
 
408
412
  ---
409
413
 
package/dist/src/cli.js CHANGED
@@ -26,10 +26,10 @@ import { dedupeMediaLocal, localDedupeAvailable, defaultDedupeOutPath, describeL
26
26
  import { segmentAlphaComponents, encodeTransparentGif, encodeTransparentAnimatedGif, pickPlateColor, detectPlateColor, keySafeArtInstruction, connectivitySafeArtInstruction, analyzeKeyedArt, measureOpaqueShare, HOLE_WARN_PCT } from "./devcli/sticker-pack.js";
27
27
  import { smartKeyPlate, planZonedSheet, zonedSheetInstruction } from "./devcli/plate-key.js";
28
28
  import { runDoctorCommand } from "./devcli/doctor.js";
29
- import { findFreePort } from "./devcli/port-utils.js";
29
+ import { findFreePort, isPortFree } from "./devcli/port-utils.js";
30
30
  import { scanLocalServers } from "./devcli/process-scan.js";
31
31
  import { runSkillsCommand } from "./devcli/skills.js";
32
- import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, listPackTopics, loadAngleBank, loadAwarenessLadder, loadIdeaBank, readPackDoc, readPackTopic, resolvePackTopic, searchPackDocs } from "./devcli/skill-docs.js";
32
+ import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, listPackTopics, loadAdFormatBank, loadAngleBank, loadAwarenessLadder, loadIdeaBank, readPackDoc, readPackTopic, resolvePackTopic, searchPackDocs } from "./devcli/skill-docs.js";
33
33
  import { initTelemetry, reportCliCrash } from "./devcli/telemetry.js";
34
34
  import { resolveLocalDataDir, localBackendAvailable, LocalModeUnavailableError, localApiRequest } from "./devcli/local-backend.js";
35
35
  import { NativeModuleUnavailableError } from "./devcli/clip-store.js";
@@ -1049,11 +1049,13 @@ Account:
1049
1049
  (iconscout takes BOTH values as one secret: <client_id>:<client_secret>)
1050
1050
 
1051
1051
  Agent skill (the director knowledge — a full copy SHIPS INSIDE this CLI):
1052
- ideas [topic] "What should I post?" — the THREE IDEA BANKS (local — offline, free, no AI)
1052
+ ideas [topic] "What should I post?" — the FOUR IDEA BANKS (local — offline, free, no AI)
1053
1053
  50 frames (the rise of · then vs now · …), 5
1054
1054
  AWARENESS STAGES (what the viewer already knows,
1055
- and what the video may ask for), and 44 PROBLEM
1056
- ANGLES (which side of the problem it comes from).
1055
+ and what the video may ask for), 44 PROBLEM
1056
+ ANGLES (which side of the problem it comes from),
1057
+ and 40 AD FORMATS (what the finished video IS —
1058
+ iphone notes, tier list, warning, greenscreen).
1057
1059
  An idea is one point in that grid; vary TWO axes
1058
1060
  and 30 videos never read as repeats. It hands over
1059
1061
  the banks — you fill them with the director's
@@ -1068,7 +1070,9 @@ Agent skill (the director knowledge — a full copy SHIPS INSIDE this CLI):
1068
1070
  --stage <name|n> One stage (unaware · problem-aware · … · most-aware)
1069
1071
  --angles The 44 problem angles, by family
1070
1072
  --angle <name> One angle family or one named angle
1071
- --grid Stage × angle × frame one row per video
1073
+ --formats The 40 ad formatswhat the video LOOKS like
1074
+ --format <name> One format family or one named format
1075
+ --grid Stage × angle × frame × format — one row per video
1072
1076
  --count <n> Sample N, spread across families (default 20 with --topic)
1073
1077
  --json Machine-readable banks (+ starter lines)
1074
1078
  skill topics The craft this pack knows, by SPOKEN name — (local — offline, no account)
@@ -1195,7 +1199,7 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1195
1199
  gigs <sub> Your vending machines, BOTH sides (client + gigworker)
1196
1200
  machines · tasks · add-task · proofs · approve · reject · ring-bell (client, paid)
1197
1201
  earn · feed · check · join · work · claim · submit · proof · earnings (gigworker, free — bring DOLLARPLATOON_API_KEY)
1198
- upload · mine · join-feed
1202
+ upload · verify-proof · mine · join-feed
1199
1203
  "HELP ME EARN ON VIDFARM" IS ONE COMMAND: vidfarm gigs earn. It joins the
1200
1204
  feed, reads who is asking for videos right now, checks every one of those
1201
1205
  machines for MONEY, and ranks them best-payer-first with the join command.
@@ -1203,7 +1207,13 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1203
1207
  spend an hour, because approval does not create money. 'gigs submit
1204
1208
  --clean-master <file>' seals the unwatermarked cut in the proof's private
1205
1209
  note, which the buyer cannot open until a rollup has actually paid you.
1210
+ 'gigs verify-proof --proof <url>' is the PRE-SUBMIT sanity check and needs no
1211
+ key: it loads every url the way a stranger's browser would and refuses a dead
1212
+ link, a 403 bucket or an expiring presigned url, then checks the watermarked
1213
+ cut and the clean master are not the wrong way round. 'gigs submit' runs it
1214
+ for you and will not send a broken proof.
1206
1215
  e.g. vidfarm gigs earn
1216
+ vidfarm gigs verify-proof --proof https://…/final-watermarked.mp4 --clean-master ./final-clean.mp4
1207
1217
  vidfarm gigs add-task --task "60s explainer for acme.com" --price 0.50 --upload-link <share url>
1208
1218
  vidfarm gigs proofs --status pending · vidfarm gigs approve PRF_01H…
1209
1219
  (see: vidfarm gigs help · the loop: vidfarm.cc/agentic-clipper.md)
@@ -1223,6 +1233,14 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1223
1233
  e.g. vidfarm clipper-run mode quick-interactive
1224
1234
  vidfarm clipper-run next
1225
1235
  (see: vidfarm clipper-run help)
1236
+ dashboard THE CLIPPER DASHBOARD for a mission folder, on localhost
1237
+ One page: what waits on you (with the cut PLAYING, not a path to open by hand),
1238
+ what each agent is mid-way through, what was submitted, and the money owed.
1239
+ Every button calls the same clipper-run verb, so the page and the terminal
1240
+ can never disagree. Loopback only, disk only — no key, no network.
1241
+ OFFER IT to a marketplace clipper before the first task; most want it.
1242
+ e.g. vidfarm dashboard · vidfarm dashboard --dir ./CLIPPER --port 4400
1243
+ (vidfarm panel is the same command)
1226
1244
  shared <sub> <link> USE a link someone shared with you — NO account, NO API key
1227
1245
  info · ls --tree · search · grab · note · mkdir · put · get
1228
1246
  grab is the one to reach for: it finds footage by MEANING and downloads it in one step.
@@ -1700,10 +1718,24 @@ async function main() {
1700
1718
  case "clipper-run":
1701
1719
  case "clipper-mode":
1702
1720
  case "run-mode": {
1721
+ // `clipper-run panel` is the same command as `vidfarm panel` — a
1722
+ // gigworker who lives in `clipper-run` should not have to learn a
1723
+ // second top-level verb to find the screen.
1724
+ if ((rest[0] ?? "").toLowerCase() === "panel" || (rest[0] ?? "").toLowerCase() === "dashboard") {
1725
+ await runClipperPanelCommand(rest.slice(1));
1726
+ return;
1727
+ }
1703
1728
  const { runClipperRunCommand } = await import("./devcli/clipper-run.js");
1704
1729
  await runClipperRunCommand(rest);
1705
1730
  return;
1706
1731
  }
1732
+ // THE CLIPPER DASHBOARD — the visual half of the clipper loop: watch the
1733
+ // cut, answer the gate, see what is owed.
1734
+ case "dashboard":
1735
+ case "panel":
1736
+ case "clipper-panel":
1737
+ await runClipperPanelCommand(rest);
1738
+ return;
1707
1739
  case "get-file":
1708
1740
  await runGetFileCommand(rest);
1709
1741
  return;
@@ -2479,6 +2511,83 @@ function printServeBanner(input) {
2479
2511
  console.log(line);
2480
2512
  console.log("");
2481
2513
  }
2514
+ // `vidfarm dashboard` (aka `vidfarm panel`) — the CLIPPER DASHBOARD over a
2515
+ // mission folder.
2516
+ // Backend-free and disk-only: no key, no network. It exists because the CUT
2517
+ // gate means "watch this video", and a terminal cannot play one.
2518
+ async function runClipperPanelCommand(argv) {
2519
+ const { ClipperUserError: PanelArgError } = await import("./devcli/clipper-run.js");
2520
+ let parsed;
2521
+ try {
2522
+ parsed = parseArgs({
2523
+ args: argv,
2524
+ allowPositionals: true,
2525
+ options: {
2526
+ dir: { type: "string" },
2527
+ port: { type: "string", default: "4400" },
2528
+ open: { type: "boolean", default: true },
2529
+ "no-open": { type: "boolean", default: false }
2530
+ }
2531
+ });
2532
+ }
2533
+ catch (error) {
2534
+ // Node's own parser rejects things like `--port -5` ("ambiguous") before any
2535
+ // of our validation runs, and its message names neither the flag nor the fix.
2536
+ throw new PanelArgError(`${error instanceof Error ? error.message.replace(/^\w+ \[[^\]]+\]: /, "") : String(error)}\n` +
2537
+ `Usage: vidfarm panel [--dir <mission folder>] [--port <1-65535>] [--no-open]`);
2538
+ }
2539
+ const { resolveMissionRoot, listTaskStates, readMissionMode } = await import("./devcli/clipper-run.js");
2540
+ const { startClipperPanel, mintPanelToken } = await import("./devcli/clipper-panel.js");
2541
+ const { ClipperUserError } = await import("./devcli/clipper-run.js");
2542
+ const root = resolveMissionRoot(parsed.values.dir ?? parsed.positionals[0]);
2543
+ if (!existsSync(root)) {
2544
+ throw new ClipperUserError(`No mission folder at ${root}.\nCreate one first: vidfarm clipper-run init${parsed.values.dir ? ` --dir ${parsed.values.dir}` : ""}`);
2545
+ }
2546
+ // `--dir MISSION.md` used to boot a panel that looked fine and showed nothing.
2547
+ if (!statSync(root).isDirectory()) {
2548
+ throw new ClipperUserError(`${root} is a file, not a mission folder. Point --dir at the folder that contains it.`);
2549
+ }
2550
+ const rawPort = String(parsed.values.port);
2551
+ const requestedPort = Number(rawPort);
2552
+ if (!Number.isInteger(requestedPort) || requestedPort < 1 || requestedPort > 65535) {
2553
+ throw new ClipperUserError(`Invalid --port "${rawPort}" — give a whole number between 1 and 65535.`);
2554
+ }
2555
+ // An EXPLICIT port that is busy must fail. Silently binding the next one free
2556
+ // meant a bookmarked tab on the old port showed a DIFFERENT mission, and
2557
+ // approvals there went to the wrong gig. With no --port given, moving on is
2558
+ // fine: nobody has been told a number yet.
2559
+ // `--port=4492` is the same intent as `--port 4492`; an exact-string match
2560
+ // meant the `=` form silently fell back to auto-advancing.
2561
+ const portWasChosen = argv.concat(process.argv)
2562
+ .some((a) => a === "--port" || a.startsWith("--port="));
2563
+ const port = portWasChosen ? requestedPort : await findFreePort(requestedPort);
2564
+ if (portWasChosen && !(await isPortFree(port))) {
2565
+ throw new ClipperUserError(`Port ${port} is already in use — another panel or local job is on it.\n` +
2566
+ `Pass a different --port, or run \`vidfarm doctor --kill-orphans\` to reclaim it.`);
2567
+ }
2568
+ const token = mintPanelToken();
2569
+ const { url } = await startClipperPanel({ root, port, token });
2570
+ const tasks = listTaskStates(root);
2571
+ const waiting = tasks.filter((task) => task.gate).length;
2572
+ const open = tasks.filter((task) => !task.gate && task.stage !== "done" && task.stage !== "discarded").length;
2573
+ const mode = readMissionMode(root);
2574
+ const line = `${DIM}${"─".repeat(74)}${RESET}`;
2575
+ console.log("");
2576
+ console.log(line);
2577
+ console.log(`${BOLD}${GREEN} Clipper dashboard${RESET} ${DIM}(local files only — no key, no network)${RESET}`);
2578
+ console.log(line);
2579
+ console.log(` mission ${root}`);
2580
+ console.log(` run mode ${mode.mode}${mode.isSet ? "" : `${DIM} (not set — assuming this)${RESET}`}`);
2581
+ console.log(` tasks ${waiting ? `${YELLOW}${waiting} waiting on you${RESET}` : `${DIM}none waiting on you${RESET}`}${DIM} · ${open} in progress · ${tasks.length} total${RESET}`);
2582
+ console.log("");
2583
+ console.log(` ${BOLD}Open:${RESET} ${url}`);
2584
+ console.log(` ${DIM}The token in that URL is minted per run. Loopback only; do not share the link.${RESET}`);
2585
+ console.log(line);
2586
+ console.log("");
2587
+ if (parsed.values.open && !parsed.values["no-open"])
2588
+ openInBrowser(url);
2589
+ // The HTTP server keeps the process alive.
2590
+ }
2482
2591
  function openInBrowser(url) {
2483
2592
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2484
2593
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -12997,6 +13106,8 @@ async function runIdeasCommand(argv) {
12997
13106
  angle: { type: "string" },
12998
13107
  stages: { type: "boolean", default: false },
12999
13108
  stage: { type: "string" },
13109
+ formats: { type: "boolean", default: false },
13110
+ format: { type: "string" },
13000
13111
  grid: { type: "boolean", default: false }
13001
13112
  }
13002
13113
  });
@@ -13005,13 +13116,14 @@ async function runIdeasCommand(argv) {
13005
13116
  const { frames, families } = loadIdeaBank();
13006
13117
  if (!frames.length)
13007
13118
  throw new Error("No bundled content-ideas reference found next to this devcli install. Fetch it with `vidfarm skills add vidfarm`.");
13008
- // The three axes of an idea live in one reference, so they live behind one
13119
+ // Every axis of an idea lives in one reference, so they live behind one
13009
13120
  // command. A director who only ever sees the frames posts 30 videos at one
13010
13121
  // awareness stage from one angle, which is the exact failure this expansion
13011
- // exists to prevent — so the stage and angle banks are flags here, not a
13012
- // separate command nobody discovers.
13122
+ // exists to prevent — so the stage, angle and format banks are flags here,
13123
+ // not a separate command nobody discovers.
13013
13124
  const ladder = loadAwarenessLadder();
13014
13125
  const angleBank = loadAngleBank();
13126
+ const formatBank = loadAdFormatBank();
13015
13127
  if (parsed.values.stages || parsed.values.stage) {
13016
13128
  const wanted = String(parsed.values.stage ?? "").trim().toLowerCase();
13017
13129
  const picked = wanted
@@ -13070,6 +13182,51 @@ async function runIdeasCommand(argv) {
13070
13182
  console.log(`${DIM}Pair angles with frames and stages: vidfarm ideas --grid --topic "<offer>"${RESET}`);
13071
13183
  return;
13072
13184
  }
13185
+ // The format is the LAST field of an idea and the cheapest one to change:
13186
+ // the subject, the angle and the script all survive a format swap, so one
13187
+ // idea that worked becomes four posts that do not read as reposts. Printed
13188
+ // here rather than left in the doc because "shoot another talking head" is
13189
+ // what a director defaults to when nobody hands them the alternatives.
13190
+ if (parsed.values.formats || parsed.values.format) {
13191
+ if (!formatBank.formats.length) {
13192
+ throw new Error("The bundled content-ideas reference has no ad-format bank. Update it with `vidfarm skills add vidfarm`.");
13193
+ }
13194
+ const wanted = String(parsed.values.format ?? "").trim().toLowerCase();
13195
+ const picked = wanted
13196
+ ? formatBank.formats.filter((entry) => entry.family.toLowerCase().includes(wanted) || entry.frame.toLowerCase().includes(wanted))
13197
+ : formatBank.formats;
13198
+ if (wanted && !picked.length) {
13199
+ throw new Error(`No format matches "${wanted}". Format families: ${formatBank.families.join(", ")} (see \`vidfarm ideas --formats\`).`);
13200
+ }
13201
+ if (json) {
13202
+ return printJson({
13203
+ ok: true,
13204
+ offline: true,
13205
+ topic: topic || null,
13206
+ total_formats: formatBank.formats.length,
13207
+ families: formatBank.families,
13208
+ formats: picked,
13209
+ method: "references/content-ideas.md"
13210
+ });
13211
+ }
13212
+ console.log(`${BOLD}Ad formats${RESET} ${DIM}(${picked.length} of ${formatBank.formats.length} · what the finished video IS)${RESET}`);
13213
+ if (topic)
13214
+ console.log(`${DIM}Topic: ${RESET}${topic}`);
13215
+ console.log("");
13216
+ let formatGroup = "";
13217
+ for (const entry of picked) {
13218
+ if (entry.family !== formatGroup) {
13219
+ formatGroup = entry.family;
13220
+ console.log(` ${BOLD}${formatGroup}${RESET}`);
13221
+ }
13222
+ console.log(` ${GREEN}${entry.frame}${RESET}${entry.note ? ` ${DIM}— ${entry.note}${RESET}` : ""}`);
13223
+ }
13224
+ console.log("");
13225
+ console.log(`${DIM}The format is the LAST field, not the first. "Let's do a tier list" is a shoot with no idea in it.${RESET}`);
13226
+ console.log(`${YELLOW}Formats that imitate a record${RESET}${DIM} — reviews, an inbox, a search, a stock count — must not fabricate it.${RESET}`);
13227
+ console.log(`${DIM}Use the real screenshot, or keep every claim inside a staged one true: vidfarm skill show ad-formats${RESET}`);
13228
+ return;
13229
+ }
13073
13230
  if (parsed.values.grid) {
13074
13231
  // The grid is the honest answer to "give me 30 videos": vary TWO axes.
13075
13232
  // Walking frames, angles and stages at different strides keeps every row a
@@ -13113,24 +13270,47 @@ async function runIdeasCommand(argv) {
13113
13270
  frame = usableFrames[(i + round + bump + 1) % usableFrames.length];
13114
13271
  }
13115
13272
  gridSeen.add(`${stage.index}|${frame.frame}|${angle.frame}`);
13273
+ // The format walks the bank on its own stride, so a balanced month is
13274
+ // also a varied SHOOT list instead of 30 rows that all get filmed the
13275
+ // same way. It is advisory: unlike the frame and the angle, the stage
13276
+ // does not name its formats, so a director may swap it freely.
13277
+ //
13278
+ // The one pairing that is actually WRONG: a two-column format (us vs
13279
+ // them, myth vs fact) under a frame that is not itself a contrast gives
13280
+ // the video two comparisons, which is the reference's "do not stack two
13281
+ // frames" defect. Walk past those rather than print a row the doc bans.
13282
+ const twoColumn = (entry) => /two-column/i.test(entry?.family ?? "");
13283
+ const frameIsContrast = /contrast/i.test(frame.family);
13284
+ let format = null;
13285
+ for (let step = 0; step < (formatBank.formats.length || 1); step += 1) {
13286
+ const candidate = formatBank.formats[(i * 7 + round + step) % (formatBank.formats.length || 1)];
13287
+ if (!candidate)
13288
+ break;
13289
+ if (twoColumn(candidate) && !frameIsContrast)
13290
+ continue;
13291
+ format = candidate;
13292
+ break;
13293
+ }
13116
13294
  return {
13117
13295
  stage: `${stage.index} · ${stage.stage}`,
13118
13296
  angle: angle.frame,
13119
13297
  angle_family: angle.family,
13120
13298
  frame: frame.frame,
13121
13299
  frame_family: frame.family,
13300
+ format: format?.frame ?? null,
13301
+ format_family: format?.family ?? null,
13122
13302
  starter: topic ? joinIdeaFrame(frame.frame, topic) : null
13123
13303
  };
13124
13304
  });
13125
13305
  if (json)
13126
13306
  return printJson({ ok: true, offline: true, topic: topic || null, rows: grid, method: "references/content-ideas.md" });
13127
- console.log(`${BOLD}Idea grid${RESET} ${DIM}(${total} combinations · stage × angle × frame · offline, free, no AI call)${RESET}`);
13307
+ console.log(`${BOLD}Idea grid${RESET} ${DIM}(${total} combinations · stage × angle × frame × format · offline, free, no AI call)${RESET}`);
13128
13308
  if (topic)
13129
13309
  console.log(`${DIM}Topic: ${RESET}${topic}`);
13130
13310
  console.log("");
13131
13311
  for (const row of grid) {
13132
13312
  console.log(` ${DIM}${row.stage.padEnd(18)}${RESET}${GREEN}${row.angle}${RESET}`);
13133
- console.log(` ${row.starter ?? row.frame}`);
13313
+ console.log(` ${row.starter ?? row.frame}${row.format ? ` ${DIM}· as: ${row.format}${RESET}` : ""}`);
13134
13314
  }
13135
13315
  console.log("");
13136
13316
  console.log(`${DIM}Each row is a DIFFERENT video, not a rewrite — the stage and the angle both moved.${RESET}`);
@@ -13211,11 +13391,12 @@ async function runIdeasCommand(argv) {
13211
13391
  else {
13212
13392
  console.log(`${DIM}Pour ONE topic into a frame to get a video: 'the rise of' + your niche. Add --topic "<offer>" to see it.${RESET}`);
13213
13393
  }
13214
- console.log(`${DIM}A frame is only ONE axis. The other two are here too, and a set that freezes them repeats:${RESET}`);
13394
+ console.log(`${DIM}A frame is only ONE axis. The others are here too, and a set that freezes them repeats:${RESET}`);
13215
13395
  console.log(`${DIM} vidfarm ideas --stages ${RESET}${ladder.length} awareness stages ${DIM}— what the viewer knows, and what you may ask for${RESET}`);
13216
13396
  console.log(`${DIM} vidfarm ideas --angles ${RESET}${angleBank.angles.length} problem angles ${DIM}— which side of the problem the video comes from${RESET}`);
13217
- console.log(`${DIM} vidfarm ideas --grid --topic "<offer>"${RESET} ${DIM}— stage × angle × frame, one row per video${RESET}`);
13218
- console.log(`${DIM}Method + frame→format notes: vidfarm skill show content-ideas · families: vidfarm ideas --families${RESET}`);
13397
+ console.log(`${DIM} vidfarm ideas --formats ${RESET}${formatBank.formats.length} ad formats ${DIM}— what the finished video IS (iphone notes, tier list, warning)${RESET}`);
13398
+ console.log(`${DIM} vidfarm ideas --grid --topic "<offer>"${RESET} ${DIM}— stage × angle × frame × format, one row per video${RESET}`);
13399
+ console.log(`${DIM}Method + build notes: vidfarm skill show content-ideas · families: vidfarm ideas --families${RESET}`);
13219
13400
  console.log(`${DIM}Never ship a frame as a hook — the four charges still apply: vidfarm skill show hooks${RESET}`);
13220
13401
  console.log(`${DIM}Producing the whole set is a batch: vidfarm skill show bulk (one frame per video).${RESET}`);
13221
13402
  // The natural upsell, in the right order: ideas are the easy win, and the
@@ -13404,7 +13585,11 @@ void main().catch(async (error) => {
13404
13585
  || error instanceof NativeModuleUnavailableError
13405
13586
  // marketplace-gigs is imported lazily, so match its "no key yet" error by
13406
13587
  // name rather than pulling the module into the startup path just for this.
13407
- || (error instanceof Error && error.name === "GigsAuthMissingError")) {
13588
+ || (error instanceof Error && error.name === "GigsAuthMissingError")
13589
+ // clipper-run / panel conditions: a wrong task id, an id already taken, a
13590
+ // gate that has to be answered first. All of these are the CLI talking to a
13591
+ // human, and a stack trace reads as "vidfarm is broken".
13592
+ || (error instanceof Error && error.name === "ClipperUserError")) {
13408
13593
  console.error(error.message);
13409
13594
  process.exit(1);
13410
13595
  }