@officexapp/vidfarm-devcli 0.21.59 → 0.21.61

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/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)
@@ -1229,6 +1233,14 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1229
1233
  e.g. vidfarm clipper-run mode quick-interactive
1230
1234
  vidfarm clipper-run next
1231
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)
1232
1244
  shared <sub> <link> USE a link someone shared with you — NO account, NO API key
1233
1245
  info · ls --tree · search · grab · note · mkdir · put · get
1234
1246
  grab is the one to reach for: it finds footage by MEANING and downloads it in one step.
@@ -1706,10 +1718,24 @@ async function main() {
1706
1718
  case "clipper-run":
1707
1719
  case "clipper-mode":
1708
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
+ }
1709
1728
  const { runClipperRunCommand } = await import("./devcli/clipper-run.js");
1710
1729
  await runClipperRunCommand(rest);
1711
1730
  return;
1712
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;
1713
1739
  case "get-file":
1714
1740
  await runGetFileCommand(rest);
1715
1741
  return;
@@ -2485,6 +2511,83 @@ function printServeBanner(input) {
2485
2511
  console.log(line);
2486
2512
  console.log("");
2487
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
+ }
2488
2591
  function openInBrowser(url) {
2489
2592
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2490
2593
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -13003,6 +13106,8 @@ async function runIdeasCommand(argv) {
13003
13106
  angle: { type: "string" },
13004
13107
  stages: { type: "boolean", default: false },
13005
13108
  stage: { type: "string" },
13109
+ formats: { type: "boolean", default: false },
13110
+ format: { type: "string" },
13006
13111
  grid: { type: "boolean", default: false }
13007
13112
  }
13008
13113
  });
@@ -13011,13 +13116,14 @@ async function runIdeasCommand(argv) {
13011
13116
  const { frames, families } = loadIdeaBank();
13012
13117
  if (!frames.length)
13013
13118
  throw new Error("No bundled content-ideas reference found next to this devcli install. Fetch it with `vidfarm skills add vidfarm`.");
13014
- // 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
13015
13120
  // command. A director who only ever sees the frames posts 30 videos at one
13016
13121
  // awareness stage from one angle, which is the exact failure this expansion
13017
- // exists to prevent — so the stage and angle banks are flags here, not a
13018
- // 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.
13019
13124
  const ladder = loadAwarenessLadder();
13020
13125
  const angleBank = loadAngleBank();
13126
+ const formatBank = loadAdFormatBank();
13021
13127
  if (parsed.values.stages || parsed.values.stage) {
13022
13128
  const wanted = String(parsed.values.stage ?? "").trim().toLowerCase();
13023
13129
  const picked = wanted
@@ -13076,6 +13182,51 @@ async function runIdeasCommand(argv) {
13076
13182
  console.log(`${DIM}Pair angles with frames and stages: vidfarm ideas --grid --topic "<offer>"${RESET}`);
13077
13183
  return;
13078
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
+ }
13079
13230
  if (parsed.values.grid) {
13080
13231
  // The grid is the honest answer to "give me 30 videos": vary TWO axes.
13081
13232
  // Walking frames, angles and stages at different strides keeps every row a
@@ -13119,24 +13270,47 @@ async function runIdeasCommand(argv) {
13119
13270
  frame = usableFrames[(i + round + bump + 1) % usableFrames.length];
13120
13271
  }
13121
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
+ }
13122
13294
  return {
13123
13295
  stage: `${stage.index} · ${stage.stage}`,
13124
13296
  angle: angle.frame,
13125
13297
  angle_family: angle.family,
13126
13298
  frame: frame.frame,
13127
13299
  frame_family: frame.family,
13300
+ format: format?.frame ?? null,
13301
+ format_family: format?.family ?? null,
13128
13302
  starter: topic ? joinIdeaFrame(frame.frame, topic) : null
13129
13303
  };
13130
13304
  });
13131
13305
  if (json)
13132
13306
  return printJson({ ok: true, offline: true, topic: topic || null, rows: grid, method: "references/content-ideas.md" });
13133
- 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}`);
13134
13308
  if (topic)
13135
13309
  console.log(`${DIM}Topic: ${RESET}${topic}`);
13136
13310
  console.log("");
13137
13311
  for (const row of grid) {
13138
13312
  console.log(` ${DIM}${row.stage.padEnd(18)}${RESET}${GREEN}${row.angle}${RESET}`);
13139
- console.log(` ${row.starter ?? row.frame}`);
13313
+ console.log(` ${row.starter ?? row.frame}${row.format ? ` ${DIM}· as: ${row.format}${RESET}` : ""}`);
13140
13314
  }
13141
13315
  console.log("");
13142
13316
  console.log(`${DIM}Each row is a DIFFERENT video, not a rewrite — the stage and the angle both moved.${RESET}`);
@@ -13217,11 +13391,12 @@ async function runIdeasCommand(argv) {
13217
13391
  else {
13218
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}`);
13219
13393
  }
13220
- 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}`);
13221
13395
  console.log(`${DIM} vidfarm ideas --stages ${RESET}${ladder.length} awareness stages ${DIM}— what the viewer knows, and what you may ask for${RESET}`);
13222
13396
  console.log(`${DIM} vidfarm ideas --angles ${RESET}${angleBank.angles.length} problem angles ${DIM}— which side of the problem the video comes from${RESET}`);
13223
- console.log(`${DIM} vidfarm ideas --grid --topic "<offer>"${RESET} ${DIM}— stage × angle × frame, one row per video${RESET}`);
13224
- 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}`);
13225
13400
  console.log(`${DIM}Never ship a frame as a hook — the four charges still apply: vidfarm skill show hooks${RESET}`);
13226
13401
  console.log(`${DIM}Producing the whole set is a batch: vidfarm skill show bulk (one frame per video).${RESET}`);
13227
13402
  // The natural upsell, in the right order: ideas are the easy win, and the
@@ -13410,7 +13585,11 @@ void main().catch(async (error) => {
13410
13585
  || error instanceof NativeModuleUnavailableError
13411
13586
  // marketplace-gigs is imported lazily, so match its "no key yet" error by
13412
13587
  // name rather than pulling the module into the startup path just for this.
13413
- || (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")) {
13414
13593
  console.error(error.message);
13415
13594
  process.exit(1);
13416
13595
  }