@vibeframe/cli 0.70.0 → 0.72.0

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.
Files changed (2) hide show
  1. package/dist/index.js +1745 -755
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -6557,6 +6557,7 @@ __export(output_exports, {
6557
6557
  notFoundError: () => notFoundError,
6558
6558
  outputError: () => outputError,
6559
6559
  outputResult: () => outputResult,
6560
+ outputSuccess: () => outputSuccess,
6560
6561
  spinner: () => spinner,
6561
6562
  suggestNext: () => suggestNext,
6562
6563
  usageError: () => usageError
@@ -6615,6 +6616,40 @@ function formatCost(min, max, unit) {
6615
6616
  if (min === max) return `~$${min.toFixed(2)} ${unit}`;
6616
6617
  return `~$${min.toFixed(2)}-$${max.toFixed(2)} ${unit}`;
6617
6618
  }
6619
+ function lookupCostEstimateUpperBound(command) {
6620
+ return COST_ESTIMATES[command]?.max ?? 0;
6621
+ }
6622
+ function outputSuccess(opts) {
6623
+ const elapsedMs = Math.max(0, Date.now() - opts.startedAt);
6624
+ const costUsd = opts.costUsd ?? (opts.dryRun ? lookupCostEstimateUpperBound(opts.command) : 0);
6625
+ const envelope = {
6626
+ command: opts.command,
6627
+ ...opts.dryRun ? { dryRun: true } : {},
6628
+ elapsedMs,
6629
+ costUsd,
6630
+ warnings: opts.warnings ?? [],
6631
+ data: opts.data
6632
+ };
6633
+ if (isJsonMode()) {
6634
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
6635
+ if (fields) {
6636
+ const keys = fields.split(",").map((k) => k.trim());
6637
+ const data = opts.data;
6638
+ const filteredData = {};
6639
+ for (const key of keys) {
6640
+ if (key in data) filteredData[key] = data[key];
6641
+ }
6642
+ envelope.data = filteredData;
6643
+ }
6644
+ console.log(JSON.stringify(envelope, null, 2));
6645
+ return;
6646
+ }
6647
+ if (isQuietMode()) {
6648
+ const data = opts.data;
6649
+ const primary = data.outputPath ?? data.output ?? data.path ?? data.url ?? data.id;
6650
+ if (primary !== void 0) console.log(String(primary));
6651
+ }
6652
+ }
6618
6653
  function outputResult(result) {
6619
6654
  if (result.dryRun && result.command && typeof result.command === "string") {
6620
6655
  const cost = COST_ESTIMATES[result.command];
@@ -29108,6 +29143,26 @@ function detectAgentHosts(env2 = process.env) {
29108
29143
  detected: false,
29109
29144
  signals: [],
29110
29145
  projectFiles: ["AGENTS.md", ".aider.conf.yml"]
29146
+ },
29147
+ {
29148
+ id: "gemini-cli",
29149
+ label: "Gemini CLI",
29150
+ detected: false,
29151
+ signals: [],
29152
+ // Per https://geminicli.com/docs/cli/gemini-md/ Gemini CLI's primary
29153
+ // context file is GEMINI.md; AGENTS.md is the cross-tool fallback
29154
+ // VibeFrame writes by default. Both are honoured.
29155
+ projectFiles: ["GEMINI.md", "AGENTS.md", ".gemini/"]
29156
+ },
29157
+ {
29158
+ id: "opencode",
29159
+ label: "OpenCode",
29160
+ detected: false,
29161
+ signals: [],
29162
+ // sst/opencode officially supports the agents.md spec — AGENTS.md at
29163
+ // project root is the standard place. Local config also under
29164
+ // `.opencode/` per https://opencode.ai/docs/config/.
29165
+ projectFiles: ["AGENTS.md", ".opencode/"]
29111
29166
  }
29112
29167
  ].map((host) => {
29113
29168
  const signals2 = [];
@@ -29165,14 +29220,21 @@ var init_agent_host_detect = __esm({
29165
29220
  "claude-code": "claude",
29166
29221
  codex: "codex",
29167
29222
  cursor: "cursor",
29168
- aider: "aider"
29223
+ aider: "aider",
29224
+ "gemini-cli": "gemini",
29225
+ opencode: "opencode"
29169
29226
  };
29170
29227
  HOST_CONFIG_DIRS = {
29171
29228
  "claude-code": ".claude",
29172
29229
  codex: ".codex",
29173
29230
  cursor: ".cursor",
29174
29231
  // some installs; macOS app stores prefs elsewhere
29175
- aider: null
29232
+ aider: null,
29233
+ "gemini-cli": ".gemini",
29234
+ // sst/opencode uses XDG-style `~/.config/opencode/` per
29235
+ // https://opencode.ai/docs/config/. The path is relative to $HOME so
29236
+ // the join in detectAgentHosts() resolves correctly.
29237
+ opencode: ".config/opencode"
29176
29238
  };
29177
29239
  }
29178
29240
  });
@@ -30020,20 +30082,24 @@ Examples:
30020
30082
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
30021
30083
  Run 'vibe schema scene.<command>' for structured parameter info.`);
30022
30084
  sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--visual-style <name>", `Seed DESIGN.md from a named style (browse via \`vibe scene styles\`). E.g. "Swiss Pulse"`).option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
30085
+ const startedAt = Date.now();
30023
30086
  const aspect = validateAspect(options.ratio);
30024
30087
  const duration = validateDuration(options.duration);
30025
30088
  const name = options.name ?? basename4(dir.replace(/\/+$/, ""));
30026
30089
  const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
30027
30090
  if (options.dryRun) {
30028
- outputResult({
30029
- dryRun: true,
30091
+ outputSuccess({
30030
30092
  command: "scene init",
30031
- params: {
30032
- dir,
30033
- name,
30034
- aspect,
30035
- duration,
30036
- visualStyle: visualStyle?.name ?? null
30093
+ startedAt,
30094
+ dryRun: true,
30095
+ data: {
30096
+ params: {
30097
+ dir,
30098
+ name,
30099
+ aspect,
30100
+ duration,
30101
+ visualStyle: visualStyle?.name ?? null
30102
+ }
30037
30103
  }
30038
30104
  });
30039
30105
  return;
@@ -30049,19 +30115,21 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30049
30115
  hosts: skillHosts
30050
30116
  });
30051
30117
  if (isJsonMode()) {
30052
- outputResult({
30053
- success: true,
30118
+ outputSuccess({
30054
30119
  command: "scene init",
30055
- dir,
30056
- name,
30057
- aspect,
30058
- duration,
30059
- visualStyle: visualStyle?.name ?? null,
30060
- created: result.created,
30061
- merged: result.merged,
30062
- skipped: result.skipped,
30063
- skillFiles: skillResult.files,
30064
- skillBundleVersion: skillResult.bundleVersion
30120
+ startedAt,
30121
+ data: {
30122
+ dir,
30123
+ name,
30124
+ aspect,
30125
+ duration,
30126
+ visualStyle: visualStyle?.name ?? null,
30127
+ created: result.created,
30128
+ merged: result.merged,
30129
+ skipped: result.skipped,
30130
+ skillFiles: skillResult.files,
30131
+ skillBundleVersion: skillResult.bundleVersion
30132
+ }
30065
30133
  });
30066
30134
  return;
30067
30135
  }
@@ -30102,6 +30170,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30102
30170
  });
30103
30171
  VALID_INSTALL_SKILL_HOSTS = ["claude-code", "cursor", "auto", "all"];
30104
30172
  sceneCommand.command("install-skill").description("Install the Hyperframes skill into a scene project so the host agent can read it (Phase H1)").argument("[project-dir]", "Project directory containing STORYBOARD.md / DESIGN.md", ".").option("--host <id>", `Host layout target: ${VALID_INSTALL_SKILL_HOSTS.join(" | ")}`, "auto").option("--force", "Overwrite existing skill files (default: skip-on-exist)").option("--dry-run", "Preview which files would be written without changing anything").action(async (projectDirArg, options) => {
30173
+ const startedAt = Date.now();
30105
30174
  const hostFlag = options.host ?? "auto";
30106
30175
  if (!VALID_INSTALL_SKILL_HOSTS.includes(hostFlag)) {
30107
30176
  exitWithError(usageError(`Invalid --host: ${hostFlag}`, `Valid: ${VALID_INSTALL_SKILL_HOSTS.join(", ")}`));
@@ -30121,15 +30190,17 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30121
30190
  dryRun: options.dryRun ?? false
30122
30191
  });
30123
30192
  if (isJsonMode()) {
30124
- outputResult({
30125
- success: true,
30193
+ outputSuccess({
30126
30194
  command: "scene install-skill",
30127
- projectDir,
30128
- host: hostFlag,
30129
- resolvedHosts: hosts,
30130
- bundleVersion: result.bundleVersion,
30131
- files: result.files,
30132
- dryRun: options.dryRun ?? false
30195
+ startedAt,
30196
+ dryRun: options.dryRun ?? false,
30197
+ data: {
30198
+ projectDir,
30199
+ host: hostFlag,
30200
+ resolvedHosts: hosts,
30201
+ bundleVersion: result.bundleVersion,
30202
+ files: result.files
30203
+ }
30133
30204
  });
30134
30205
  return;
30135
30206
  }
@@ -30151,6 +30222,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30151
30222
  }
30152
30223
  });
30153
30224
  sceneCommand.command("compose-prompts").description("Emit the per-beat compose plan for the host agent to author HTML itself (Phase H2 \u2014 no LLM call)").argument("[project-dir]", "Project directory containing STORYBOARD.md / DESIGN.md", ".").option("--beat <id>", "Restrict the plan to a single beat by id (e.g. 'hook', '1')").action(async (projectDirArg, options) => {
30225
+ const startedAt = Date.now();
30154
30226
  const projectDir = resolve14(projectDirArg);
30155
30227
  const result = await getComposePrompts({
30156
30228
  projectDir,
@@ -30158,18 +30230,21 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30158
30230
  });
30159
30231
  if (!result.success) {
30160
30232
  if (isJsonMode()) {
30161
- outputResult({
30233
+ outputSuccess({
30162
30234
  command: "scene compose-prompts",
30163
- ...result
30235
+ startedAt,
30236
+ data: { ...result }
30164
30237
  });
30165
- process.exit(1);
30238
+ process.exitCode = 1;
30239
+ return;
30166
30240
  }
30167
30241
  exitWithError(generalError(result.error ?? "compose-prompts failed"));
30168
30242
  }
30169
30243
  if (isJsonMode()) {
30170
- outputResult({
30244
+ outputSuccess({
30171
30245
  command: "scene compose-prompts",
30172
- ...result
30246
+ startedAt,
30247
+ data: { ...result }
30173
30248
  });
30174
30249
  return;
30175
30250
  }
@@ -30203,20 +30278,23 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30203
30278
  console.log(source_default.dim("Re-run with --json to get the full per-beat userPrompt + cues for direct consumption."));
30204
30279
  });
30205
30280
  sceneCommand.command("styles").description("List vendored visual styles (or show one) for DESIGN.md seeding").argument("[name]", "Style name to inspect (omit to list all)").action((name) => {
30281
+ const startedAt = Date.now();
30206
30282
  if (!name) {
30207
30283
  const all = listVisualStyles();
30208
30284
  if (isJsonMode()) {
30209
- outputResult({
30210
- success: true,
30285
+ outputSuccess({
30211
30286
  command: "scene styles",
30212
- count: all.length,
30213
- styles: all.map((s) => ({
30214
- name: s.name,
30215
- slug: s.slug,
30216
- designer: s.designer,
30217
- mood: s.mood,
30218
- bestFor: s.bestFor
30219
- }))
30287
+ startedAt,
30288
+ data: {
30289
+ count: all.length,
30290
+ styles: all.map((s) => ({
30291
+ name: s.name,
30292
+ slug: s.slug,
30293
+ designer: s.designer,
30294
+ mood: s.mood,
30295
+ bestFor: s.bestFor
30296
+ }))
30297
+ }
30220
30298
  });
30221
30299
  return;
30222
30300
  }
@@ -30244,7 +30322,11 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30244
30322
  return;
30245
30323
  }
30246
30324
  if (isJsonMode()) {
30247
- outputResult({ success: true, command: "scene styles", style });
30325
+ outputSuccess({
30326
+ command: "scene styles",
30327
+ startedAt,
30328
+ data: { style }
30329
+ });
30248
30330
  return;
30249
30331
  }
30250
30332
  console.log();
@@ -30266,6 +30348,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30266
30348
  console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
30267
30349
  });
30268
30350
  sceneCommand.command("add").description("Add a new scene to a project: AI narration + image + per-scene HTML").argument("<name>", "Scene name (slugified into the composition id)").option("--style <preset>", `Style preset: ${SCENE_PRESETS.join(", ")}`, "simple").option("--narration <text>", "Narration text (or path to a .txt file). Drives TTS + scene duration.").option("--narration-file <path>", "Existing narration audio file (.wav/.mp3). Skips TTS \u2014 useful with hyperframes tts, Mac say, or other external tools.").option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option("--visuals <prompt>", "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider").option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option("--tts <provider>", "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)", "auto").option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option("--no-audio", "Skip TTS even when --narration is provided (useful for tests/agent dry runs)").option("--no-image", "Skip image generation even when --visuals is provided").option("--no-transcribe", "Skip Whisper word-level transcribe step (no transcript-<id>.json emitted)").option("--transcribe-language <code>", "BCP-47 language code passed to Whisper (e.g. en, ko)").option("--force", "Overwrite an existing compositions/scene-<id>.html").option("--dry-run", "Preview parameters without writing files or calling APIs").action(async (name, options) => {
30351
+ const startedAt = Date.now();
30269
30352
  if (options.style) options.style = validatePreset(options.style);
30270
30353
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
30271
30354
  let tts;
@@ -30276,25 +30359,28 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30276
30359
  }
30277
30360
  if (options.dryRun) {
30278
30361
  const id = slugifySceneName(name);
30279
- outputResult({
30280
- dryRun: true,
30362
+ outputSuccess({
30281
30363
  command: "scene add",
30282
- params: {
30283
- name,
30284
- id,
30285
- preset: options.style,
30286
- narration: !!options.narration,
30287
- visuals: !!options.visuals,
30288
- duration: options.duration,
30289
- headline: options.headline,
30290
- kicker: options.kicker,
30291
- project: options.project,
30292
- insertInto: options.insertInto,
30293
- imageProvider: options.imageProvider,
30294
- tts,
30295
- audio: options.audio,
30296
- // commander sets `audio: false` when --no-audio is passed
30297
- image: options.image
30364
+ startedAt,
30365
+ dryRun: true,
30366
+ data: {
30367
+ params: {
30368
+ name,
30369
+ id,
30370
+ preset: options.style,
30371
+ narration: !!options.narration,
30372
+ visuals: !!options.visuals,
30373
+ duration: options.duration,
30374
+ headline: options.headline,
30375
+ kicker: options.kicker,
30376
+ project: options.project,
30377
+ insertInto: options.insertInto,
30378
+ imageProvider: options.imageProvider,
30379
+ tts,
30380
+ audio: options.audio,
30381
+ // commander sets `audio: false` when --no-audio is passed
30382
+ image: options.image
30383
+ }
30298
30384
  }
30299
30385
  });
30300
30386
  return;
@@ -30329,9 +30415,10 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30329
30415
  exitWithError(generalError(result.error ?? "Scene add failed"));
30330
30416
  }
30331
30417
  if (isJsonMode()) {
30332
- outputResult({
30418
+ outputSuccess({
30333
30419
  command: "scene add",
30334
- ...result
30420
+ startedAt,
30421
+ data: { ...result }
30335
30422
  });
30336
30423
  return;
30337
30424
  }
@@ -30363,6 +30450,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30363
30450
  }
30364
30451
  });
30365
30452
  sceneCommand.command("lint").description("Validate scene HTML against Hyperframes rules (in-process, no Chrome required)").argument("[root]", "Root composition file relative to --project", "index.html").option("--project <dir>", "Project directory", ".").option("--fix", 'Apply mechanical auto-fixes (currently: missing class="clip")').action(async (root, options) => {
30453
+ const startedAt = Date.now();
30366
30454
  const projectDir = resolve14(options.project);
30367
30455
  if (!await rootExists(projectDir, root)) {
30368
30456
  exitWithError(generalError(
@@ -30380,11 +30468,12 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30380
30468
  exitWithError(generalError(`Lint failed: ${msg}`));
30381
30469
  }
30382
30470
  if (isJsonMode()) {
30383
- outputResult({
30471
+ outputSuccess({
30384
30472
  command: "scene lint",
30385
- ...result
30473
+ startedAt,
30474
+ data: { ...result }
30386
30475
  });
30387
- if (!result.ok) process.exit(1);
30476
+ if (!result.ok) process.exitCode = 1;
30388
30477
  return;
30389
30478
  }
30390
30479
  if (result.ok && result.warningCount === 0 && result.infoCount === 0) {
@@ -30418,29 +30507,33 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30418
30507
  console.log(` ${source_default.green("\u2714")} ${fx.file} ${source_default.dim(fx.codes.join(", "))}`);
30419
30508
  }
30420
30509
  }
30421
- if (!result.ok) process.exit(1);
30510
+ if (!result.ok) process.exitCode = 1;
30422
30511
  });
30423
30512
  VALID_FPS = [24, 30, 60];
30424
30513
  VALID_QUALITIES = ["draft", "standard", "high"];
30425
30514
  VALID_FORMATS = ["mp4", "webm", "mov"];
30426
30515
  sceneCommand.command("render").description("Render a scene project to MP4/WebM/MOV via the Hyperframes producer (requires Chrome)").argument("[root]", "Root composition file relative to --project", "index.html").option("--project <dir>", "Project directory", ".").option("-o, --out <path>", "Output file (default: renders/<name>-<timestamp>.<format>)").option("--fps <n>", `Frames per second: ${VALID_FPS.join("|")}`, "30").option("--quality <q>", `Quality preset: ${VALID_QUALITIES.join("|")}`, "standard").option("--format <f>", `Output container: ${VALID_FORMATS.join("|")}`, "mp4").option("--workers <n>", "Capture workers (1-16, default 1)", "1").option("--dry-run", "Preview parameters without rendering").action(async (root, options) => {
30516
+ const startedAt = Date.now();
30427
30517
  const fps = validateFps(options.fps);
30428
30518
  const quality = validateQuality(options.quality);
30429
30519
  const format = validateFormat(options.format);
30430
30520
  const workers = validateWorkers(options.workers);
30431
30521
  const projectDir = resolve14(options.project);
30432
30522
  if (options.dryRun) {
30433
- outputResult({
30434
- dryRun: true,
30523
+ outputSuccess({
30435
30524
  command: "scene render",
30436
- params: {
30437
- projectDir,
30438
- root,
30439
- output: options.out,
30440
- fps,
30441
- quality,
30442
- format,
30443
- workers
30525
+ startedAt,
30526
+ dryRun: true,
30527
+ data: {
30528
+ params: {
30529
+ projectDir,
30530
+ root,
30531
+ output: options.out,
30532
+ fps,
30533
+ quality,
30534
+ format,
30535
+ workers
30536
+ }
30444
30537
  }
30445
30538
  });
30446
30539
  return;
@@ -30461,13 +30554,22 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30461
30554
  if (!result.success) {
30462
30555
  spinner2?.fail("Render failed");
30463
30556
  if (isJsonMode()) {
30464
- outputResult({ command: "scene render", ...result });
30465
- process.exit(1);
30557
+ outputSuccess({
30558
+ command: "scene render",
30559
+ startedAt,
30560
+ data: { ...result }
30561
+ });
30562
+ process.exitCode = 1;
30563
+ return;
30466
30564
  }
30467
30565
  exitWithError(generalError(result.error ?? "Render failed"));
30468
30566
  }
30469
30567
  if (isJsonMode()) {
30470
- outputResult({ command: "scene render", ...result });
30568
+ outputSuccess({
30569
+ command: "scene render",
30570
+ startedAt,
30571
+ data: { ...result }
30572
+ });
30471
30573
  return;
30472
30574
  }
30473
30575
  spinner2?.succeed(source_default.green(`Render complete: ${result.outputPath}`));
@@ -30487,25 +30589,29 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30487
30589
  }
30488
30590
  });
30489
30591
  sceneCommand.command("build").description("One-shot: read STORYBOARD.md cues, dispatch TTS + image-gen per beat, compose, render to MP4 (v0.60)").argument("[project-dir]", "Project directory containing STORYBOARD.md", ".").option("--mode <mode>", "Build mode: agent (host-agent authors HTML) | batch (CLI's internal LLM authors HTML) | auto (agent if any host detected) [Plan H \u2014 Phase 3]", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "LLM that composes scene HTML in batch mode: claude|openai|gemini (default: auto-resolve from available API keys, claude > gemini > openai)").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro (overrides frontmatter)").option("--voice <id>", "Voice id (provider-specific \u2014 overrides frontmatter)").option("--image-provider <name>", "Image provider: openai (only one supported in v0.60)").option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").action(async (projectDirArg, options) => {
30592
+ const startedAt = Date.now();
30490
30593
  const projectDir = resolve14(projectDirArg);
30491
30594
  if (options.dryRun) {
30492
- outputResult({
30493
- dryRun: true,
30595
+ outputSuccess({
30494
30596
  command: "scene build",
30495
- params: {
30496
- projectDir,
30497
- mode: options.mode,
30498
- effort: options.effort,
30499
- composer: options.composer,
30500
- skipNarration: options.skipNarration ?? false,
30501
- skipBackdrop: options.skipBackdrop ?? false,
30502
- skipRender: options.skipRender ?? false,
30503
- ttsProvider: options.tts,
30504
- voice: options.voice,
30505
- imageProvider: options.imageProvider,
30506
- imageQuality: options.quality,
30507
- imageSize: options.imageSize,
30508
- force: options.force ?? false
30597
+ startedAt,
30598
+ dryRun: true,
30599
+ data: {
30600
+ params: {
30601
+ projectDir,
30602
+ mode: options.mode,
30603
+ effort: options.effort,
30604
+ composer: options.composer,
30605
+ skipNarration: options.skipNarration ?? false,
30606
+ skipBackdrop: options.skipBackdrop ?? false,
30607
+ skipRender: options.skipRender ?? false,
30608
+ ttsProvider: options.tts,
30609
+ voice: options.voice,
30610
+ imageProvider: options.imageProvider,
30611
+ imageQuality: options.quality,
30612
+ imageSize: options.imageSize,
30613
+ force: options.force ?? false
30614
+ }
30509
30615
  }
30510
30616
  });
30511
30617
  return;
@@ -30559,13 +30665,22 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30559
30665
  if (!result.success) {
30560
30666
  spinner2?.fail(`Build failed: ${result.error}`);
30561
30667
  if (isJsonMode()) {
30562
- outputResult({ command: "scene build", ...result });
30563
- process.exit(1);
30668
+ outputSuccess({
30669
+ command: "scene build",
30670
+ startedAt,
30671
+ data: { ...result }
30672
+ });
30673
+ process.exitCode = 1;
30674
+ return;
30564
30675
  }
30565
30676
  exitWithError(generalError(result.error ?? "Build failed"));
30566
30677
  }
30567
30678
  if (isJsonMode()) {
30568
- outputResult({ command: "scene build", ...result });
30679
+ outputSuccess({
30680
+ command: "scene build",
30681
+ startedAt,
30682
+ data: { ...result }
30683
+ });
30569
30684
  return;
30570
30685
  }
30571
30686
  if (result.phase === "needs-author") {
@@ -31579,27 +31694,31 @@ Use this image analysis to inform the color palette, typography placement, and o
31579
31694
  }
31580
31695
  function registerMotionCommand(aiCommand) {
31581
31696
  aiCommand.command("motion").description("Generate motion graphics using Claude + Remotion (render & composite)").argument("<description>", "Natural language description of the motion graphic").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output file path", "motion.tsx").option("-d, --duration <sec>", "Duration in seconds", "5").option("-w, --width <px>", "Width in pixels", "1920").option("-h, --height <px>", "Height in pixels", "1080").option("--fps <fps>", "Frame rate", "30").option("-s, --style <style>", "Style preset: minimal, corporate, playful, cinematic").option("--render", "Render the generated code with Remotion (output .webm)").option("--video <path>", "Base video to composite the motion graphic onto").option("--image <path>", "Image to analyze with Gemini \u2014 color/mood fed into Claude prompt").option("--from-tsx <path>", "Refine an existing TSX file instead of generating from scratch").option("-m, --model <alias>", "LLM model: sonnet (default), opus, gemini, gemini-3.1-pro", "sonnet").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
31697
+ const startedAt = Date.now();
31582
31698
  try {
31583
31699
  if (options.output) {
31584
31700
  validateOutputPath(options.output);
31585
31701
  }
31586
31702
  if (options.dryRun) {
31587
- outputResult({
31703
+ outputSuccess({
31704
+ command: "generate motion",
31705
+ startedAt,
31588
31706
  dryRun: true,
31589
- command: "ai motion",
31590
- params: {
31591
- description: description.slice(0, 200),
31592
- duration: options.duration,
31593
- width: options.width,
31594
- height: options.height,
31595
- fps: options.fps,
31596
- style: options.style,
31597
- render: options.render ?? false,
31598
- video: options.video,
31599
- image: options.image,
31600
- fromTsx: options.fromTsx,
31601
- model: options.model,
31602
- output: options.output
31707
+ data: {
31708
+ params: {
31709
+ description: description.slice(0, 200),
31710
+ duration: options.duration,
31711
+ width: options.width,
31712
+ height: options.height,
31713
+ fps: options.fps,
31714
+ style: options.style,
31715
+ render: options.render ?? false,
31716
+ video: options.video,
31717
+ image: options.image,
31718
+ fromTsx: options.fromTsx,
31719
+ model: options.model,
31720
+ output: options.output
31721
+ }
31603
31722
  }
31604
31723
  });
31605
31724
  return;
@@ -31713,20 +31832,24 @@ async function executeSoundEffect(options) {
31713
31832
  }
31714
31833
  function registerSoundEffectCommand(parent) {
31715
31834
  parent.command("sound-effect").description("Generate sound effect using ElevenLabs").argument("<prompt>", "Description of the sound effect").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "sound-effect.mp3").option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)").option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
31835
+ const startedAt = Date.now();
31716
31836
  try {
31717
31837
  rejectControlChars(prompt3);
31718
31838
  if (options.output) {
31719
31839
  validateOutputPath(options.output);
31720
31840
  }
31721
31841
  if (options.dryRun) {
31722
- outputResult({
31723
- dryRun: true,
31842
+ outputSuccess({
31724
31843
  command: "generate sound-effect",
31725
- params: {
31726
- prompt: prompt3,
31727
- duration: options.duration,
31728
- promptInfluence: options.promptInfluence,
31729
- output: options.output
31844
+ startedAt,
31845
+ dryRun: true,
31846
+ data: {
31847
+ params: {
31848
+ prompt: prompt3,
31849
+ duration: options.duration,
31850
+ promptInfluence: options.promptInfluence,
31851
+ output: options.output
31852
+ }
31730
31853
  }
31731
31854
  });
31732
31855
  return;
@@ -31753,7 +31876,11 @@ function registerSoundEffectCommand(parent) {
31753
31876
  await writeFile13(outputPath, result.audioBuffer);
31754
31877
  spinner2.succeed(source_default.green("Sound effect generated"));
31755
31878
  if (isJsonMode()) {
31756
- outputResult({ success: true, outputPath });
31879
+ outputSuccess({
31880
+ command: "generate sound-effect",
31881
+ startedAt,
31882
+ data: { outputPath }
31883
+ });
31757
31884
  return;
31758
31885
  }
31759
31886
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -31803,6 +31930,7 @@ async function executeMusicStatus(options) {
31803
31930
  }
31804
31931
  function registerMusicStatusCommand(parent) {
31805
31932
  parent.command("music-status", { hidden: true }).description("Check music generation status").argument("<task-id>", "Task ID from music generation").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").action(async (taskId, options) => {
31933
+ const startedAt = Date.now();
31806
31934
  try {
31807
31935
  const apiKey = await requireApiKey(
31808
31936
  "REPLICATE_API_TOKEN",
@@ -31814,12 +31942,15 @@ function registerMusicStatusCommand(parent) {
31814
31942
  const result = await replicate.getMusicStatus(taskId);
31815
31943
  if (isJsonMode()) {
31816
31944
  const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
31817
- outputResult({
31818
- success: true,
31819
- taskId,
31820
- status,
31821
- audioUrl: result.audioUrl,
31822
- error: result.error
31945
+ outputSuccess({
31946
+ command: "generate music-status",
31947
+ startedAt,
31948
+ data: {
31949
+ taskId,
31950
+ status,
31951
+ audioUrl: result.audioUrl,
31952
+ error: result.error
31953
+ }
31823
31954
  });
31824
31955
  return;
31825
31956
  }
@@ -31857,6 +31988,7 @@ var init_music_status = __esm({
31857
31988
  // src/commands/generate/video-cancel.ts
31858
31989
  function registerVideoCancelCommand(parent) {
31859
31990
  parent.command("video-cancel", { hidden: true }).description("Cancel video generation (Grok or Runway)").argument("<task-id>", "Task ID to cancel").option("-p, --provider <provider>", "Provider: grok, runway", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET env)").action(async (taskId, options) => {
31991
+ const startedAt = Date.now();
31860
31992
  try {
31861
31993
  const provider = (options.provider || "grok").toLowerCase();
31862
31994
  let success = false;
@@ -31869,7 +32001,11 @@ function registerVideoCancelCommand(parent) {
31869
32001
  if (success) {
31870
32002
  spinner2.succeed(source_default.green("Generation cancelled"));
31871
32003
  if (isJsonMode()) {
31872
- outputResult({ success: true, taskId, provider: "grok", cancelled: true });
32004
+ outputSuccess({
32005
+ command: "generate video-cancel",
32006
+ startedAt,
32007
+ data: { taskId, provider: "grok", cancelled: true }
32008
+ });
31873
32009
  return;
31874
32010
  }
31875
32011
  } else {
@@ -31889,7 +32025,11 @@ function registerVideoCancelCommand(parent) {
31889
32025
  if (success) {
31890
32026
  spinner2.succeed(source_default.green("Generation cancelled"));
31891
32027
  if (isJsonMode()) {
31892
- outputResult({ success: true, taskId, provider: "runway", cancelled: true });
32028
+ outputSuccess({
32029
+ command: "generate video-cancel",
32030
+ startedAt,
32031
+ data: { taskId, provider: "runway", cancelled: true }
32032
+ });
31893
32033
  return;
31894
32034
  }
31895
32035
  } else {
@@ -31962,16 +32102,18 @@ async function executeBackground(options) {
31962
32102
  }
31963
32103
  function registerBackgroundCommand(parent) {
31964
32104
  parent.command("background").description("Generate video background using DALL-E").argument("<description>", "Background description").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("-o, --output <path>", "Output file path (downloads image)").option("-a, --aspect <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
32105
+ const startedAt = Date.now();
31965
32106
  try {
31966
32107
  rejectControlChars(description);
31967
32108
  if (options.output) {
31968
32109
  validateOutputPath(options.output);
31969
32110
  }
31970
32111
  if (options.dryRun) {
31971
- outputResult({
31972
- dryRun: true,
32112
+ outputSuccess({
31973
32113
  command: "generate background",
31974
- params: { description, aspect: options.aspect, output: options.output }
32114
+ startedAt,
32115
+ dryRun: true,
32116
+ data: { params: { description, aspect: options.aspect, output: options.output } }
31975
32117
  });
31976
32118
  return;
31977
32119
  }
@@ -32006,7 +32148,11 @@ function registerBackgroundCommand(parent) {
32006
32148
  await mkdir9(dirname7(outputPath), { recursive: true });
32007
32149
  await writeFile14(outputPath, buffer);
32008
32150
  }
32009
- outputResult({ success: true, imageUrl: img.url, outputPath });
32151
+ outputSuccess({
32152
+ command: "generate background",
32153
+ startedAt,
32154
+ data: { imageUrl: img.url, outputPath }
32155
+ });
32010
32156
  return;
32011
32157
  }
32012
32158
  console.log();
@@ -32152,6 +32298,7 @@ async function executeStoryboard(options) {
32152
32298
  }
32153
32299
  function registerStoryboardCommand(parent) {
32154
32300
  parent.command("storyboard").description("Generate video storyboard from content using Claude").argument("<content>", "Content to analyze (text or file path)").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output JSON file path").option("-d, --duration <sec>", "Target total duration in seconds").option("-f, --file", "Treat content argument as file path").option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low").option("--dry-run", "Preview parameters without executing").action(async (content, options) => {
32301
+ const startedAt = Date.now();
32155
32302
  try {
32156
32303
  rejectControlChars(content);
32157
32304
  if (options.output) {
@@ -32167,13 +32314,16 @@ function registerStoryboardCommand(parent) {
32167
32314
  textContent = await readFile14(filePath, "utf-8");
32168
32315
  }
32169
32316
  if (options.dryRun) {
32170
- outputResult({
32171
- dryRun: true,
32317
+ outputSuccess({
32172
32318
  command: "generate storyboard",
32173
- params: {
32174
- content: textContent.substring(0, 200),
32175
- duration: options.duration,
32176
- creativity
32319
+ startedAt,
32320
+ dryRun: true,
32321
+ data: {
32322
+ params: {
32323
+ content: textContent.substring(0, 200),
32324
+ duration: options.duration,
32325
+ creativity
32326
+ }
32177
32327
  }
32178
32328
  });
32179
32329
  return;
@@ -32205,16 +32355,23 @@ function registerStoryboardCommand(parent) {
32205
32355
  const outputPath = resolve19(process.cwd(), options.output);
32206
32356
  await writeFile15(outputPath, JSON.stringify(segments, null, 2), "utf-8");
32207
32357
  if (isJsonMode()) {
32208
- outputResult({
32209
- success: true,
32210
- segmentCount: segments.length,
32211
- segments,
32212
- outputPath
32358
+ outputSuccess({
32359
+ command: "generate storyboard",
32360
+ startedAt,
32361
+ data: {
32362
+ segmentCount: segments.length,
32363
+ segments,
32364
+ outputPath
32365
+ }
32213
32366
  });
32214
32367
  return;
32215
32368
  }
32216
32369
  } else if (isJsonMode()) {
32217
- outputResult({ success: true, segmentCount: segments.length, segments });
32370
+ outputSuccess({
32371
+ command: "generate storyboard",
32372
+ startedAt,
32373
+ data: { segmentCount: segments.length, segments }
32374
+ });
32218
32375
  return;
32219
32376
  }
32220
32377
  console.log();
@@ -32459,6 +32616,7 @@ async function executeSpeech(options) {
32459
32616
  }
32460
32617
  function registerSpeechCommand(parent) {
32461
32618
  parent.command("speech").alias("tts").description("Generate speech from text using ElevenLabs").argument("[text]", "Text to convert to speech (interactive if omitted)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "output.mp3").option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM").option("--list-voices", "List available voices").option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat).option("--dry-run", "Preview parameters without executing").action(async (text, options) => {
32619
+ const startedAt = Date.now();
32462
32620
  try {
32463
32621
  if (!text) {
32464
32622
  if (hasTTY()) {
@@ -32480,10 +32638,11 @@ function registerSpeechCommand(parent) {
32480
32638
  validateOutputPath(options.output);
32481
32639
  }
32482
32640
  if (options.dryRun) {
32483
- outputResult({
32484
- dryRun: true,
32641
+ outputSuccess({
32485
32642
  command: "generate speech",
32486
- params: { text, voice: options.voice, output: options.output }
32643
+ startedAt,
32644
+ dryRun: true,
32645
+ data: { params: { text, voice: options.voice, output: options.output } }
32487
32646
  });
32488
32647
  return;
32489
32648
  }
@@ -32572,10 +32731,13 @@ function registerSpeechCommand(parent) {
32572
32731
  }
32573
32732
  }
32574
32733
  if (isJsonMode()) {
32575
- outputResult({
32576
- success: true,
32577
- characterCount: result.characterCount,
32578
- outputPath
32734
+ outputSuccess({
32735
+ command: "generate speech",
32736
+ startedAt,
32737
+ data: {
32738
+ characterCount: result.characterCount,
32739
+ outputPath
32740
+ }
32579
32741
  });
32580
32742
  return;
32581
32743
  }
@@ -32664,6 +32826,7 @@ async function executeMusic(options) {
32664
32826
  }
32665
32827
  function registerMusicCommand(parent) {
32666
32828
  parent.command("music").description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)").argument("<prompt>", "Description of the music to generate").option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs").option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)").option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8").option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)").option("-m, --melody <file>", "Reference melody audio file for conditioning (Replicate only)").option("--model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large").option("-o, --output <path>", "Output audio file path", "music.mp3").option("--no-wait", "Don't wait for generation to complete (Replicate async mode)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
32829
+ const startedAt = Date.now();
32667
32830
  try {
32668
32831
  rejectControlChars(prompt3);
32669
32832
  if (options.output) {
@@ -32671,16 +32834,19 @@ function registerMusicCommand(parent) {
32671
32834
  }
32672
32835
  const provider = (options.provider || "elevenlabs").toLowerCase();
32673
32836
  if (options.dryRun) {
32674
- outputResult({
32675
- dryRun: true,
32837
+ outputSuccess({
32676
32838
  command: "generate music",
32677
- params: {
32678
- prompt: prompt3,
32679
- provider,
32680
- duration: options.duration,
32681
- model: options.model,
32682
- output: options.output,
32683
- instrumental: options.instrumental
32839
+ startedAt,
32840
+ dryRun: true,
32841
+ data: {
32842
+ params: {
32843
+ prompt: prompt3,
32844
+ provider,
32845
+ duration: options.duration,
32846
+ model: options.model,
32847
+ output: options.output,
32848
+ instrumental: options.instrumental
32849
+ }
32684
32850
  }
32685
32851
  });
32686
32852
  return;
@@ -32707,11 +32873,14 @@ function registerMusicCommand(parent) {
32707
32873
  await writeFile17(outputPath, result.audioBuffer);
32708
32874
  spinner2.succeed(source_default.green("Music generated successfully"));
32709
32875
  if (isJsonMode()) {
32710
- outputResult({
32711
- success: true,
32712
- provider: "elevenlabs",
32713
- outputPath,
32714
- duration
32876
+ outputSuccess({
32877
+ command: "generate music",
32878
+ startedAt,
32879
+ data: {
32880
+ provider: "elevenlabs",
32881
+ outputPath,
32882
+ duration
32883
+ }
32715
32884
  });
32716
32885
  return;
32717
32886
  }
@@ -32779,12 +32948,15 @@ function registerMusicCommand(parent) {
32779
32948
  await writeFile17(outputPath, audioBuffer);
32780
32949
  spinner2.succeed(source_default.green("Music generated successfully"));
32781
32950
  if (isJsonMode()) {
32782
- outputResult({
32783
- success: true,
32784
- provider: "replicate",
32785
- taskId: result.taskId,
32786
- audioUrl: finalResult.audioUrl,
32787
- outputPath
32951
+ outputSuccess({
32952
+ command: "generate music",
32953
+ startedAt,
32954
+ data: {
32955
+ provider: "replicate",
32956
+ taskId: result.taskId,
32957
+ audioUrl: finalResult.audioUrl,
32958
+ outputPath
32959
+ }
32788
32960
  });
32789
32961
  return;
32790
32962
  }
@@ -33084,6 +33256,7 @@ import { existsSync as existsSync15 } from "node:fs";
33084
33256
  import { writeFile as writeFile19, mkdir as mkdir11 } from "node:fs/promises";
33085
33257
  function registerThumbnailCommand(parent) {
33086
33258
  parent.command("thumbnail").description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)").argument("[description]", "Thumbnail description (for DALL-E generation)").option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)").option("-o, --output <path>", "Output file path").option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter").option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI").option("--prompt <prompt>", "Custom prompt for best-frame analysis").option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash").action(async (description, options) => {
33259
+ const startedAt = Date.now();
33087
33260
  try {
33088
33261
  if (description) rejectControlChars(description);
33089
33262
  if (options.output) {
@@ -33119,11 +33292,14 @@ function registerThumbnailCommand(parent) {
33119
33292
  }
33120
33293
  spinner3.succeed(source_default.green("Best frame extracted"));
33121
33294
  if (isJsonMode()) {
33122
- outputResult({
33123
- success: true,
33124
- timestamp: result2.timestamp,
33125
- reason: result2.reason,
33126
- outputPath: result2.outputPath
33295
+ outputSuccess({
33296
+ command: "generate thumbnail",
33297
+ startedAt,
33298
+ data: {
33299
+ timestamp: result2.timestamp,
33300
+ reason: result2.reason,
33301
+ outputPath: result2.outputPath
33302
+ }
33127
33303
  });
33128
33304
  return;
33129
33305
  }
@@ -33171,7 +33347,11 @@ function registerThumbnailCommand(parent) {
33171
33347
  await mkdir11(dirname9(outputPath), { recursive: true });
33172
33348
  await writeFile19(outputPath, buffer);
33173
33349
  }
33174
- outputResult({ success: true, imageUrl: img.url, outputPath });
33350
+ outputSuccess({
33351
+ command: "generate thumbnail",
33352
+ startedAt,
33353
+ data: { imageUrl: img.url, outputPath }
33354
+ });
33175
33355
  return;
33176
33356
  }
33177
33357
  console.log();
@@ -33242,6 +33422,7 @@ function getStatusColor(status) {
33242
33422
  }
33243
33423
  function registerVideoStatusCommand(parent) {
33244
33424
  parent.command("video-status", { hidden: true }).description("Check video generation status (Grok, Runway, or Kling)").argument("<task-id>", "Task ID from video generation").option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)").option("-t, --type <type>", "Task type: text2video or image2video (Kling only)", "text2video").option("-w, --wait", "Wait for completion").option("-o, --output <path>", "Download video when complete").action(async (taskId, options) => {
33425
+ const startedAt = Date.now();
33245
33426
  try {
33246
33427
  const provider = (options.provider || "grok").toLowerCase();
33247
33428
  if (provider === "grok") {
@@ -33264,14 +33445,17 @@ function registerVideoStatusCommand(parent) {
33264
33445
  outputPath = resolve24(process.cwd(), options.output);
33265
33446
  await writeFile20(outputPath, buffer);
33266
33447
  }
33267
- outputResult({
33268
- success: true,
33269
- taskId,
33270
- provider: "grok",
33271
- status: result.status,
33272
- videoUrl: result.videoUrl,
33273
- error: result.error,
33274
- outputPath
33448
+ outputSuccess({
33449
+ command: "generate video-status",
33450
+ startedAt,
33451
+ data: {
33452
+ taskId,
33453
+ provider: "grok",
33454
+ status: result.status,
33455
+ videoUrl: result.videoUrl,
33456
+ error: result.error,
33457
+ outputPath
33458
+ }
33275
33459
  });
33276
33460
  return;
33277
33461
  }
@@ -33329,15 +33513,18 @@ function registerVideoStatusCommand(parent) {
33329
33513
  outputPath = resolve24(process.cwd(), options.output);
33330
33514
  await writeFile20(outputPath, buffer);
33331
33515
  }
33332
- outputResult({
33333
- success: true,
33334
- taskId,
33335
- provider: "runway",
33336
- status: result.status,
33337
- videoUrl: result.videoUrl,
33338
- progress: result.progress,
33339
- error: result.error,
33340
- outputPath
33516
+ outputSuccess({
33517
+ command: "generate video-status",
33518
+ startedAt,
33519
+ data: {
33520
+ taskId,
33521
+ provider: "runway",
33522
+ status: result.status,
33523
+ videoUrl: result.videoUrl,
33524
+ progress: result.progress,
33525
+ error: result.error,
33526
+ outputPath
33527
+ }
33341
33528
  });
33342
33529
  return;
33343
33530
  }
@@ -33393,15 +33580,18 @@ function registerVideoStatusCommand(parent) {
33393
33580
  outputPath = resolve24(process.cwd(), options.output);
33394
33581
  await writeFile20(outputPath, buffer);
33395
33582
  }
33396
- outputResult({
33397
- success: true,
33398
- taskId,
33399
- provider: "kling",
33400
- status: result.status,
33401
- videoUrl: result.videoUrl,
33402
- duration: result.duration,
33403
- error: result.error,
33404
- outputPath
33583
+ outputSuccess({
33584
+ command: "generate video-status",
33585
+ startedAt,
33586
+ data: {
33587
+ taskId,
33588
+ provider: "kling",
33589
+ status: result.status,
33590
+ videoUrl: result.videoUrl,
33591
+ duration: result.duration,
33592
+ error: result.error,
33593
+ outputPath
33594
+ }
33405
33595
  });
33406
33596
  return;
33407
33597
  }
@@ -33465,22 +33655,26 @@ import { resolve as resolve25 } from "node:path";
33465
33655
  import { writeFile as writeFile21 } from "node:fs/promises";
33466
33656
  function registerVideoExtendCommand(parent) {
33467
33657
  parent.command("video-extend", { hidden: true }).description("Extend video duration (Kling by video ID, Veo by operation name)").argument("<id>", "Kling video ID or Veo operation name").option("-p, --provider <provider>", "Provider: kling, veo", "kling").option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path").option("--prompt <text>", "Continuation prompt").option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5").option("-n, --negative <prompt>", "Negative prompt (what to avoid, Kling only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1").option("--no-wait", "Start extension and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (id, options) => {
33658
+ const startedAt = Date.now();
33468
33659
  try {
33469
33660
  const provider = (options.provider || "kling").toLowerCase();
33470
33661
  if (options.output) {
33471
33662
  validateOutputPath(options.output);
33472
33663
  }
33473
33664
  if (options.dryRun) {
33474
- outputResult({
33475
- dryRun: true,
33665
+ outputSuccess({
33476
33666
  command: "generate video-extend",
33477
- params: {
33478
- id,
33479
- provider,
33480
- prompt: options.prompt,
33481
- duration: options.duration,
33482
- negative: options.negative,
33483
- veoModel: options.veoModel
33667
+ startedAt,
33668
+ dryRun: true,
33669
+ data: {
33670
+ params: {
33671
+ id,
33672
+ provider,
33673
+ prompt: options.prompt,
33674
+ duration: options.duration,
33675
+ negative: options.negative,
33676
+ veoModel: options.veoModel
33677
+ }
33484
33678
  }
33485
33679
  });
33486
33680
  return;
@@ -33537,13 +33731,16 @@ function registerVideoExtendCommand(parent) {
33537
33731
  outputPath = resolve25(process.cwd(), options.output);
33538
33732
  await writeFile21(outputPath, buffer);
33539
33733
  }
33540
- outputResult({
33541
- success: true,
33542
- provider: "kling",
33543
- taskId: result.id,
33544
- videoUrl: finalResult.videoUrl,
33545
- duration: finalResult.duration,
33546
- outputPath
33734
+ outputSuccess({
33735
+ command: "generate video-extend",
33736
+ startedAt,
33737
+ data: {
33738
+ provider: "kling",
33739
+ taskId: result.id,
33740
+ videoUrl: finalResult.videoUrl,
33741
+ duration: finalResult.duration,
33742
+ outputPath
33743
+ }
33547
33744
  });
33548
33745
  return;
33549
33746
  }
@@ -33623,13 +33820,16 @@ function registerVideoExtendCommand(parent) {
33623
33820
  outputPath = resolve25(process.cwd(), options.output);
33624
33821
  await writeFile21(outputPath, buffer);
33625
33822
  }
33626
- outputResult({
33627
- success: true,
33628
- provider: "veo",
33629
- taskId: result.id,
33630
- videoUrl: finalResult.videoUrl,
33631
- duration: finalResult.duration,
33632
- outputPath
33823
+ outputSuccess({
33824
+ command: "generate video-extend",
33825
+ startedAt,
33826
+ data: {
33827
+ provider: "veo",
33828
+ taskId: result.id,
33829
+ videoUrl: finalResult.videoUrl,
33830
+ duration: finalResult.duration,
33831
+ outputPath
33832
+ }
33633
33833
  });
33634
33834
  return;
33635
33835
  }
@@ -33744,6 +33944,7 @@ Examples:
33744
33944
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
33745
33945
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
33746
33946
  $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
33947
+ const startedAt = Date.now();
33747
33948
  try {
33748
33949
  if (!prompt3) {
33749
33950
  if (hasTTY()) {
@@ -33802,18 +34003,21 @@ Examples:
33802
34003
  provider = resolved?.name ?? "gemini";
33803
34004
  }
33804
34005
  if (options.dryRun) {
33805
- outputResult({
33806
- dryRun: true,
34006
+ outputSuccess({
33807
34007
  command: "generate image",
33808
- params: {
33809
- prompt: prompt3,
33810
- provider,
33811
- model: options.model,
33812
- ratio: options.ratio,
33813
- size: options.size,
33814
- quality: options.quality,
33815
- count: options.count,
33816
- output: options.output
34008
+ startedAt,
34009
+ dryRun: true,
34010
+ data: {
34011
+ params: {
34012
+ prompt: prompt3,
34013
+ provider,
34014
+ model: options.model,
34015
+ ratio: options.ratio,
34016
+ size: options.size,
34017
+ quality: options.quality,
34018
+ count: options.count,
34019
+ output: options.output
34020
+ }
33817
34021
  }
33818
34022
  });
33819
34023
  return;
@@ -33851,14 +34055,17 @@ Examples:
33851
34055
  await mkdir12(dirname10(outputPath), { recursive: true });
33852
34056
  await writeFile22(outputPath, buffer);
33853
34057
  }
33854
- outputResult({
33855
- success: true,
33856
- provider: "openai",
33857
- images: result.images.map((img) => ({
33858
- url: img.url,
33859
- revisedPrompt: img.revisedPrompt
33860
- })),
33861
- outputPath
34058
+ outputSuccess({
34059
+ command: "generate image",
34060
+ startedAt,
34061
+ data: {
34062
+ provider: "openai",
34063
+ images: result.images.map((img) => ({
34064
+ url: img.url,
34065
+ revisedPrompt: img.revisedPrompt
34066
+ })),
34067
+ outputPath
34068
+ }
33862
34069
  });
33863
34070
  return;
33864
34071
  }
@@ -33970,13 +34177,18 @@ Examples:
33970
34177
  await mkdir12(dirname10(outputPath), { recursive: true });
33971
34178
  await writeFile22(outputPath, buffer);
33972
34179
  }
33973
- outputResult({
33974
- success: true,
33975
- provider: "gemini",
33976
- images: result.images.map((img) => ({
33977
- mimeType: img.mimeType
33978
- })),
33979
- outputPath
34180
+ outputSuccess({
34181
+ command: "generate image",
34182
+ startedAt,
34183
+ warnings: usedLabel.includes("fallback") ? [`Model "${options.model}" failed; fell back to flash`] : [],
34184
+ data: {
34185
+ provider: "gemini",
34186
+ model: usedLabel,
34187
+ images: result.images.map((img) => ({
34188
+ mimeType: img.mimeType
34189
+ })),
34190
+ outputPath
34191
+ }
33980
34192
  });
33981
34193
  return;
33982
34194
  }
@@ -34060,11 +34272,14 @@ Examples:
34060
34272
  await mkdir12(dirname10(outputPath), { recursive: true });
34061
34273
  await writeFile22(outputPath, buffer);
34062
34274
  }
34063
- outputResult({
34064
- success: true,
34065
- provider: "grok",
34066
- images: result.images.map((img) => ({ url: img.url })),
34067
- outputPath
34275
+ outputSuccess({
34276
+ command: "generate image",
34277
+ startedAt,
34278
+ data: {
34279
+ provider: "grok",
34280
+ images: result.images.map((img) => ({ url: img.url })),
34281
+ outputPath
34282
+ }
34068
34283
  });
34069
34284
  return;
34070
34285
  }
@@ -34135,11 +34350,14 @@ Examples:
34135
34350
  proc.on("close", (code) => {
34136
34351
  if (code === 0) {
34137
34352
  if (isJsonMode()) {
34138
- outputResult({
34139
- success: true,
34140
- provider: "runway",
34141
- images: [{ format: "file" }],
34142
- outputPath
34353
+ outputSuccess({
34354
+ command: "generate image",
34355
+ startedAt,
34356
+ data: {
34357
+ provider: "runway",
34358
+ images: [{ format: "file" }],
34359
+ outputPath
34360
+ }
34143
34361
  });
34144
34362
  } else {
34145
34363
  spinner2.succeed(source_default.green("Generated image with Runway"));
@@ -35865,6 +36083,7 @@ Examples:
35865
36083
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
35866
36084
  $ vibe gen vid "ocean waves" -o waves.mp4 -p veo --resolution 1080p # Veo
35867
36085
  $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`).action(async (prompt3, options) => {
36086
+ const startedAt = Date.now();
35868
36087
  try {
35869
36088
  if (!prompt3) {
35870
36089
  if (hasTTY()) {
@@ -35956,19 +36175,22 @@ Examples:
35956
36175
  options.ratio = "16:9";
35957
36176
  }
35958
36177
  if (options.dryRun) {
35959
- outputResult({
35960
- dryRun: true,
36178
+ outputSuccess({
35961
36179
  command: "generate video",
35962
- params: {
35963
- prompt: prompt3,
35964
- provider,
35965
- duration: options.duration,
35966
- ratio: options.ratio,
35967
- image: options.image,
35968
- mode: options.mode,
35969
- negative: options.negative,
35970
- resolution: options.resolution,
35971
- veoModel: options.veoModel
36180
+ startedAt,
36181
+ dryRun: true,
36182
+ data: {
36183
+ params: {
36184
+ prompt: prompt3,
36185
+ provider,
36186
+ duration: options.duration,
36187
+ ratio: options.ratio,
36188
+ image: options.image,
36189
+ mode: options.mode,
36190
+ negative: options.negative,
36191
+ resolution: options.resolution,
36192
+ veoModel: options.veoModel
36193
+ }
35972
36194
  }
35973
36195
  });
35974
36196
  return;
@@ -36248,13 +36470,16 @@ Examples:
36248
36470
  outputPath = resolve29(process.cwd(), options.output);
36249
36471
  await writeFile25(outputPath, buffer);
36250
36472
  }
36251
- outputResult({
36252
- success: true,
36253
- provider,
36254
- taskId: result?.id,
36255
- videoUrl: finalResult.videoUrl,
36256
- duration: finalResult.duration,
36257
- outputPath
36473
+ outputSuccess({
36474
+ command: "generate video",
36475
+ startedAt,
36476
+ data: {
36477
+ provider,
36478
+ taskId: result?.id,
36479
+ videoUrl: finalResult.videoUrl,
36480
+ duration: finalResult.duration,
36481
+ outputPath
36482
+ }
36258
36483
  });
36259
36484
  return;
36260
36485
  }
@@ -37687,6 +37912,7 @@ Examples:
37687
37912
  $ vibe ed sc video.mp4 --dry-run --json
37688
37913
 
37689
37914
  No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires GOOGLE_API_KEY).`).action(async (videoPath, options) => {
37915
+ const startedAt = Date.now();
37690
37916
  try {
37691
37917
  if (options.output) {
37692
37918
  validateOutputPath(options.output);
@@ -37703,16 +37929,19 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37703
37929
  const outputPath = options.output || `${name}-cut${ext}`;
37704
37930
  const useGemini = options.useGemini || false;
37705
37931
  if (options.dryRun) {
37706
- outputResult({
37707
- dryRun: true,
37932
+ outputSuccess({
37708
37933
  command: "edit silence-cut",
37709
- params: {
37710
- videoPath: absVideoPath,
37711
- noiseThreshold: parseFloat(options.noise),
37712
- minDuration: parseFloat(options.minDuration),
37713
- padding: parseFloat(options.padding),
37714
- useGemini,
37715
- analyzeOnly: options.analyzeOnly || false
37934
+ startedAt,
37935
+ dryRun: true,
37936
+ data: {
37937
+ params: {
37938
+ videoPath: absVideoPath,
37939
+ noiseThreshold: parseFloat(options.noise),
37940
+ minDuration: parseFloat(options.minDuration),
37941
+ padding: parseFloat(options.padding),
37942
+ useGemini,
37943
+ analyzeOnly: options.analyzeOnly || false
37944
+ }
37716
37945
  }
37717
37946
  });
37718
37947
  return;
@@ -37737,13 +37966,16 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37737
37966
  }
37738
37967
  spinner2.succeed(source_default.green("Silence detection complete"));
37739
37968
  if (isJsonMode()) {
37740
- outputResult({
37741
- success: true,
37742
- method: result.method,
37743
- totalDuration: result.totalDuration,
37744
- silentPeriods: result.silentPeriods,
37745
- silentDuration: result.silentDuration,
37746
- outputPath: result.outputPath
37969
+ outputSuccess({
37970
+ command: "edit silence-cut",
37971
+ startedAt,
37972
+ data: {
37973
+ method: result.method,
37974
+ totalDuration: result.totalDuration,
37975
+ silentPeriods: result.silentPeriods,
37976
+ silentDuration: result.silentDuration,
37977
+ outputPath: result.outputPath
37978
+ }
37747
37979
  });
37748
37980
  return;
37749
37981
  }
@@ -37781,6 +38013,7 @@ Examples:
37781
38013
  $ vibe ed cap video.mp4 --dry-run --json
37782
38014
 
37783
38015
  Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoPath, options) => {
38016
+ const startedAt = Date.now();
37784
38017
  try {
37785
38018
  if (options.output) {
37786
38019
  validateOutputPath(options.output);
@@ -37793,16 +38026,19 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37793
38026
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37794
38027
  }
37795
38028
  if (options.dryRun) {
37796
- outputResult({
37797
- dryRun: true,
38029
+ outputSuccess({
37798
38030
  command: "edit caption",
37799
- params: {
37800
- videoPath: absVideoPath,
37801
- style: options.style,
37802
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
37803
- fontColor: options.color,
37804
- language: options.language,
37805
- position: options.position
38031
+ startedAt,
38032
+ dryRun: true,
38033
+ data: {
38034
+ params: {
38035
+ videoPath: absVideoPath,
38036
+ style: options.style,
38037
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
38038
+ fontColor: options.color,
38039
+ language: options.language,
38040
+ position: options.position
38041
+ }
37806
38042
  }
37807
38043
  });
37808
38044
  return;
@@ -37831,12 +38067,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37831
38067
  }
37832
38068
  spinner2.succeed(source_default.green("Captions applied"));
37833
38069
  if (isJsonMode()) {
37834
- outputResult({
37835
- success: true,
37836
- segmentCount: result.segmentCount,
37837
- style: options.style || "bold",
37838
- outputPath: result.outputPath,
37839
- srtPath: result.srtPath
38070
+ outputSuccess({
38071
+ command: "edit caption",
38072
+ startedAt,
38073
+ data: {
38074
+ segmentCount: result.segmentCount,
38075
+ style: options.style || "bold",
38076
+ outputPath: result.outputPath,
38077
+ srtPath: result.srtPath
38078
+ }
37840
38079
  });
37841
38080
  return;
37842
38081
  }
@@ -37855,6 +38094,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37855
38094
  }
37856
38095
  });
37857
38096
  aiCommand.command("noise-reduce").description("Remove background noise from audio/video using FFmpeg (no API key needed)").argument("<input>", "Audio or video file path").option("-o, --output <path>", "Output file path (default: <name>-denoised.<ext>)").option("-s, --strength <level>", "Noise reduction strength: low, medium, high (default: medium)", "medium").option("-n, --noise-floor <dB>", "Custom noise floor in dB (overrides strength preset)").option("--dry-run", "Preview parameters without executing").action(async (inputPath, options) => {
38097
+ const startedAt = Date.now();
37858
38098
  try {
37859
38099
  if (options.output) {
37860
38100
  validateOutputPath(options.output);
@@ -37867,13 +38107,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37867
38107
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37868
38108
  }
37869
38109
  if (options.dryRun) {
37870
- outputResult({
37871
- dryRun: true,
38110
+ outputSuccess({
37872
38111
  command: "edit noise-reduce",
37873
- params: {
37874
- inputPath: absInputPath,
37875
- strength: options.strength,
37876
- noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
38112
+ startedAt,
38113
+ dryRun: true,
38114
+ data: {
38115
+ params: {
38116
+ inputPath: absInputPath,
38117
+ strength: options.strength,
38118
+ noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
38119
+ }
37877
38120
  }
37878
38121
  });
37879
38122
  return;
@@ -37894,11 +38137,14 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37894
38137
  }
37895
38138
  spinner2.succeed(source_default.green("Noise reduction complete"));
37896
38139
  if (isJsonMode()) {
37897
- outputResult({
37898
- success: true,
37899
- inputDuration: result.inputDuration,
37900
- strength: options.strength || "medium",
37901
- outputPath: result.outputPath
38140
+ outputSuccess({
38141
+ command: "edit noise-reduce",
38142
+ startedAt,
38143
+ data: {
38144
+ inputDuration: result.inputDuration,
38145
+ strength: options.strength || "medium",
38146
+ outputPath: result.outputPath
38147
+ }
37902
38148
  });
37903
38149
  return;
37904
38150
  }
@@ -37914,6 +38160,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37914
38160
  }
37915
38161
  });
37916
38162
  aiCommand.command("fade").description("Apply fade in/out effects to video (FFmpeg only, no API key needed)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-faded.<ext>)").option("--fade-in <seconds>", "Fade-in duration in seconds (default: 1)", "1").option("--fade-out <seconds>", "Fade-out duration in seconds (default: 1)", "1").option("--audio-only", "Apply fade to audio only (video stream copied)").option("--video-only", "Apply fade to video only (audio stream copied)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38163
+ const startedAt = Date.now();
37917
38164
  try {
37918
38165
  if (options.output) {
37919
38166
  validateOutputPath(options.output);
@@ -37926,15 +38173,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37926
38173
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37927
38174
  }
37928
38175
  if (options.dryRun) {
37929
- outputResult({
37930
- dryRun: true,
38176
+ outputSuccess({
37931
38177
  command: "edit fade",
37932
- params: {
37933
- videoPath: absVideoPath,
37934
- fadeIn: parseFloat(options.fadeIn),
37935
- fadeOut: parseFloat(options.fadeOut),
37936
- audioOnly: options.audioOnly || false,
37937
- videoOnly: options.videoOnly || false
38178
+ startedAt,
38179
+ dryRun: true,
38180
+ data: {
38181
+ params: {
38182
+ videoPath: absVideoPath,
38183
+ fadeIn: parseFloat(options.fadeIn),
38184
+ fadeOut: parseFloat(options.fadeOut),
38185
+ audioOnly: options.audioOnly || false,
38186
+ videoOnly: options.videoOnly || false
38187
+ }
37938
38188
  }
37939
38189
  });
37940
38190
  return;
@@ -37957,12 +38207,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37957
38207
  }
37958
38208
  spinner2.succeed(source_default.green("Fade effects applied"));
37959
38209
  if (isJsonMode()) {
37960
- outputResult({
37961
- success: true,
37962
- totalDuration: result.totalDuration,
37963
- fadeInApplied: result.fadeInApplied,
37964
- fadeOutApplied: result.fadeOutApplied,
37965
- outputPath: result.outputPath
38210
+ outputSuccess({
38211
+ command: "edit fade",
38212
+ startedAt,
38213
+ data: {
38214
+ totalDuration: result.totalDuration,
38215
+ fadeInApplied: result.fadeInApplied,
38216
+ fadeOutApplied: result.fadeOutApplied,
38217
+ outputPath: result.outputPath
38218
+ }
37966
38219
  });
37967
38220
  return;
37968
38221
  }
@@ -37979,6 +38232,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37979
38232
  }
37980
38233
  });
37981
38234
  aiCommand.command("translate-srt").description("Translate SRT subtitle file to another language (Claude/OpenAI)").argument("<srt>", "SRT file path").option("-t, --target <language>", "Target language (e.g., ko, es, fr, ja, zh)").option("-o, --output <path>", "Output file path (default: <name>-<target>.srt)").option("-p, --provider <provider>", "Translation provider: claude, openai (default: claude)", "claude").option("--source <language>", "Source language (auto-detected if omitted)").option("-k, --api-key <key>", "API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (srtPath, options) => {
38235
+ const startedAt = Date.now();
37982
38236
  try {
37983
38237
  if (options.output) {
37984
38238
  validateOutputPath(options.output);
@@ -37991,14 +38245,17 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37991
38245
  exitWithError(notFoundError(absSrtPath));
37992
38246
  }
37993
38247
  if (options.dryRun) {
37994
- outputResult({
37995
- dryRun: true,
38248
+ outputSuccess({
37996
38249
  command: "edit translate-srt",
37997
- params: {
37998
- srtPath: absSrtPath,
37999
- targetLanguage: options.target,
38000
- provider: options.provider || "claude",
38001
- sourceLanguage: options.source
38250
+ startedAt,
38251
+ dryRun: true,
38252
+ data: {
38253
+ params: {
38254
+ srtPath: absSrtPath,
38255
+ targetLanguage: options.target,
38256
+ provider: options.provider || "claude",
38257
+ sourceLanguage: options.source
38258
+ }
38002
38259
  }
38003
38260
  });
38004
38261
  return;
@@ -38028,12 +38285,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38028
38285
  }
38029
38286
  spinner2.succeed(source_default.green("Translation complete"));
38030
38287
  if (isJsonMode()) {
38031
- outputResult({
38032
- success: true,
38033
- segmentCount: result.segmentCount,
38034
- sourceLanguage: result.sourceLanguage,
38035
- targetLanguage: result.targetLanguage,
38036
- outputPath: result.outputPath
38288
+ outputSuccess({
38289
+ command: "edit translate-srt",
38290
+ startedAt,
38291
+ data: {
38292
+ segmentCount: result.segmentCount,
38293
+ sourceLanguage: result.sourceLanguage,
38294
+ targetLanguage: result.targetLanguage,
38295
+ outputPath: result.outputPath
38296
+ }
38037
38297
  });
38038
38298
  return;
38039
38299
  }
@@ -38050,6 +38310,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38050
38310
  }
38051
38311
  });
38052
38312
  aiCommand.command("jump-cut").description("Remove filler words (um, uh, like, etc.) from video using Whisper word-level timestamps").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-jumpcut.<ext>)").option("--fillers <words>", "Comma-separated filler words to detect").option("--padding <seconds>", "Padding around cuts in seconds (default: 0.05)", "0.05").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--analyze-only", "Only detect fillers, don't cut").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
38313
+ const startedAt = Date.now();
38053
38314
  try {
38054
38315
  if (options.output) {
38055
38316
  validateOutputPath(options.output);
@@ -38064,15 +38325,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38064
38325
  }
38065
38326
  if (options.dryRun) {
38066
38327
  const fillers2 = options.fillers ? options.fillers.split(",").map((f) => f.trim()) : void 0;
38067
- outputResult({
38068
- dryRun: true,
38328
+ outputSuccess({
38069
38329
  command: "edit jump-cut",
38070
- params: {
38071
- videoPath: absVideoPath,
38072
- fillers: fillers2,
38073
- padding: parseFloat(options.padding),
38074
- language: options.language,
38075
- analyzeOnly: options.analyzeOnly || false
38330
+ startedAt,
38331
+ dryRun: true,
38332
+ data: {
38333
+ params: {
38334
+ videoPath: absVideoPath,
38335
+ fillers: fillers2,
38336
+ padding: parseFloat(options.padding),
38337
+ language: options.language,
38338
+ analyzeOnly: options.analyzeOnly || false
38339
+ }
38076
38340
  }
38077
38341
  });
38078
38342
  return;
@@ -38101,13 +38365,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38101
38365
  }
38102
38366
  spinner2.succeed(source_default.green("Filler detection complete"));
38103
38367
  if (isJsonMode()) {
38104
- outputResult({
38105
- success: true,
38106
- totalDuration: result.totalDuration,
38107
- fillerCount: result.fillerCount,
38108
- fillerDuration: result.fillerDuration,
38109
- fillers: result.fillers,
38110
- outputPath: result.outputPath
38368
+ outputSuccess({
38369
+ command: "edit jump-cut",
38370
+ startedAt,
38371
+ data: {
38372
+ totalDuration: result.totalDuration,
38373
+ fillerCount: result.fillerCount,
38374
+ fillerDuration: result.fillerDuration,
38375
+ fillers: result.fillers,
38376
+ outputPath: result.outputPath
38377
+ }
38111
38378
  });
38112
38379
  return;
38113
38380
  }
@@ -38909,6 +39176,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38909
39176
  registerEditCommands(editCommand);
38910
39177
  registerFillGapsCommand(editCommand);
38911
39178
  editCommand.command("grade").description("Apply AI-generated color grading (Claude + FFmpeg)").argument("<video>", "Video file path").option("-s, --style <prompt>", "Style description (e.g., 'cinematic warm')").option("--preset <name>", "Built-in preset: film-noir, vintage, cinematic-warm, cool-tones, high-contrast, pastel, cyberpunk, horror").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show filter without applying").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39179
+ const startedAt = Date.now();
38912
39180
  try {
38913
39181
  if (options.style) rejectControlChars(options.style);
38914
39182
  if (options.output) {
@@ -38924,13 +39192,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38924
39192
  exitWithError(notFoundError("FFmpeg not found. Install with: brew install ffmpeg"));
38925
39193
  }
38926
39194
  if (options.dryRun) {
38927
- outputResult({
38928
- dryRun: true,
39195
+ outputSuccess({
38929
39196
  command: "edit grade",
38930
- params: {
38931
- videoPath: resolve33(process.cwd(), videoPath),
38932
- style: options.style || options.preset,
38933
- analyzeOnly: options.analyzeOnly || false
39197
+ startedAt,
39198
+ dryRun: true,
39199
+ data: {
39200
+ params: {
39201
+ videoPath: resolve33(process.cwd(), videoPath),
39202
+ style: options.style || options.preset,
39203
+ analyzeOnly: options.analyzeOnly || false
39204
+ }
38934
39205
  }
38935
39206
  });
38936
39207
  return;
@@ -38956,12 +39227,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38956
39227
  if (isJsonMode()) {
38957
39228
  const absPath2 = resolve33(process.cwd(), videoPath);
38958
39229
  const gradeOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38959
- outputResult({
38960
- success: true,
38961
- style: options.preset || options.style,
38962
- description: gradeResult.description,
38963
- ffmpegFilter: gradeResult.ffmpegFilter,
38964
- outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
39230
+ outputSuccess({
39231
+ command: "edit grade",
39232
+ startedAt,
39233
+ data: {
39234
+ style: options.preset || options.style,
39235
+ description: gradeResult.description,
39236
+ ffmpegFilter: gradeResult.ffmpegFilter,
39237
+ outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
39238
+ }
38965
39239
  });
38966
39240
  return;
38967
39241
  }
@@ -38990,6 +39264,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38990
39264
  }
38991
39265
  });
38992
39266
  editCommand.command("text-overlay").description("Apply text overlays to video (FFmpeg drawtext)").argument("<video>", "Video file path").option("-t, --text <texts...>", "Text lines to overlay (repeat for multiple)").option("-s, --style <style>", "Overlay style: lower-third, center-bold, subtitle, minimal", "lower-third").option("--font-size <size>", "Font size in pixels (auto-calculated if omitted)").option("--font-color <color>", "Font color (default: white)", "white").option("--fade <seconds>", "Fade in/out duration in seconds", "0.3").option("--start <seconds>", "Start time in seconds", "0").option("--end <seconds>", "End time in seconds (default: video duration)").option("-o, --output <path>", "Output video file path").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39267
+ const startedAt = Date.now();
38993
39268
  try {
38994
39269
  if (!options.text || options.text.length === 0) {
38995
39270
  exitWithError(usageError("At least one --text option is required", 'Example: vibe edit text-overlay video.mp4 -t "NEXUS AI" --style center-bold'));
@@ -39002,18 +39277,21 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39002
39277
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39003
39278
  }
39004
39279
  if (options.dryRun) {
39005
- outputResult({
39006
- dryRun: true,
39280
+ outputSuccess({
39007
39281
  command: "edit text-overlay",
39008
- params: {
39009
- videoPath: resolve33(process.cwd(), videoPath),
39010
- texts: options.text,
39011
- style: options.style,
39012
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
39013
- fontColor: options.fontColor,
39014
- fade: parseFloat(options.fade),
39015
- start: parseFloat(options.start),
39016
- end: options.end ? parseFloat(options.end) : void 0
39282
+ startedAt,
39283
+ dryRun: true,
39284
+ data: {
39285
+ params: {
39286
+ videoPath: resolve33(process.cwd(), videoPath),
39287
+ texts: options.text,
39288
+ style: options.style,
39289
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
39290
+ fontColor: options.fontColor,
39291
+ fade: parseFloat(options.fade),
39292
+ start: parseFloat(options.start),
39293
+ end: options.end ? parseFloat(options.end) : void 0
39294
+ }
39017
39295
  }
39018
39296
  });
39019
39297
  return;
@@ -39038,11 +39316,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39038
39316
  }
39039
39317
  spinner2.succeed(source_default.green("Text overlays applied"));
39040
39318
  if (isJsonMode()) {
39041
- outputResult({
39042
- success: true,
39043
- style: options.style,
39044
- texts: options.text,
39045
- outputPath: result.outputPath
39319
+ outputSuccess({
39320
+ command: "edit text-overlay",
39321
+ startedAt,
39322
+ data: {
39323
+ style: options.style,
39324
+ texts: options.text,
39325
+ outputPath: result.outputPath
39326
+ }
39046
39327
  });
39047
39328
  return;
39048
39329
  }
@@ -39059,6 +39340,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39059
39340
  }
39060
39341
  });
39061
39342
  editCommand.command("speed-ramp").description("Apply content-aware speed ramping (Whisper + Claude + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output video file path").option("-s, --style <style>", "Style: dramatic, smooth, action", "dramatic").option("--min-speed <factor>", "Minimum speed factor", "0.25").option("--max-speed <factor>", "Maximum speed factor", "4.0").option("--analyze-only", "Show keyframes without applying").option("-l, --language <lang>", "Language code for transcription").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39343
+ const startedAt = Date.now();
39062
39344
  try {
39063
39345
  if (options.output) {
39064
39346
  validateOutputPath(options.output);
@@ -39067,15 +39349,18 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39067
39349
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39068
39350
  }
39069
39351
  if (options.dryRun) {
39070
- outputResult({
39071
- dryRun: true,
39352
+ outputSuccess({
39072
39353
  command: "edit speed-ramp",
39073
- params: {
39074
- videoPath: resolve33(process.cwd(), videoPath),
39075
- style: options.style,
39076
- minSpeed: parseFloat(options.minSpeed),
39077
- maxSpeed: parseFloat(options.maxSpeed),
39078
- analyzeOnly: options.analyzeOnly || false
39354
+ startedAt,
39355
+ dryRun: true,
39356
+ data: {
39357
+ params: {
39358
+ videoPath: resolve33(process.cwd(), videoPath),
39359
+ style: options.style,
39360
+ minSpeed: parseFloat(options.minSpeed),
39361
+ maxSpeed: parseFloat(options.maxSpeed),
39362
+ analyzeOnly: options.analyzeOnly || false
39363
+ }
39079
39364
  }
39080
39365
  });
39081
39366
  return;
@@ -39128,11 +39413,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39128
39413
  if (isJsonMode()) {
39129
39414
  const avgSpeed2 = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
39130
39415
  const speedRampOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
39131
- outputResult({
39132
- success: true,
39133
- keyframes: speedResult.keyframes,
39134
- avgSpeed: avgSpeed2,
39135
- outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
39416
+ outputSuccess({
39417
+ command: "edit speed-ramp",
39418
+ startedAt,
39419
+ data: {
39420
+ keyframes: speedResult.keyframes,
39421
+ avgSpeed: avgSpeed2,
39422
+ outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
39423
+ }
39136
39424
  });
39137
39425
  return;
39138
39426
  }
@@ -39172,6 +39460,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39172
39460
  }
39173
39461
  });
39174
39462
  editCommand.command("reframe").description("Auto-reframe video to different aspect ratio (Claude Vision + FFmpeg)").argument("<video>", "Video file path").option("-a, --aspect <ratio>", "Target aspect ratio: 9:16, 1:1, 4:5", "9:16").option("-f, --focus <mode>", "Focus mode: auto, face, center, action", "auto").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show crop regions without applying").option("--keyframes <path>", "Export keyframes to JSON file").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39463
+ const startedAt = Date.now();
39175
39464
  try {
39176
39465
  if (options.output) {
39177
39466
  validateOutputPath(options.output);
@@ -39180,14 +39469,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39180
39469
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39181
39470
  }
39182
39471
  if (options.dryRun) {
39183
- outputResult({
39184
- dryRun: true,
39472
+ outputSuccess({
39185
39473
  command: "edit reframe",
39186
- params: {
39187
- videoPath: resolve33(process.cwd(), videoPath),
39188
- aspect: options.aspect,
39189
- focus: options.focus,
39190
- analyzeOnly: options.analyzeOnly || false
39474
+ startedAt,
39475
+ dryRun: true,
39476
+ data: {
39477
+ params: {
39478
+ videoPath: resolve33(process.cwd(), videoPath),
39479
+ aspect: options.aspect,
39480
+ focus: options.focus,
39481
+ analyzeOnly: options.analyzeOnly || false
39482
+ }
39191
39483
  }
39192
39484
  });
39193
39485
  return;
@@ -39255,13 +39547,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39255
39547
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
39256
39548
  if (isJsonMode()) {
39257
39549
  const reframeOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39258
- outputResult({
39259
- success: true,
39260
- sourceWidth,
39261
- sourceHeight,
39262
- aspect: options.aspect,
39263
- cropKeyframes,
39264
- outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
39550
+ outputSuccess({
39551
+ command: "edit reframe",
39552
+ startedAt,
39553
+ data: {
39554
+ sourceWidth,
39555
+ sourceHeight,
39556
+ aspect: options.aspect,
39557
+ cropKeyframes,
39558
+ outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
39559
+ }
39265
39560
  });
39266
39561
  return;
39267
39562
  }
@@ -39312,6 +39607,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39312
39607
  }
39313
39608
  });
39314
39609
  editCommand.command("image").description("Edit image(s) using AI (Gemini/OpenAI/Grok)").argument("<images...>", "Input image file(s) followed by edit prompt").option("-p, --provider <provider>", "Provider: gemini (default), openai, grok", "gemini").option("-k, --api-key <key>", "API key (or set env variable)").option("-o, --output <path>", "Output file path", "edited.png").option("-m, --model <model>", "Model: flash/3.1-flash/latest/pro (Gemini only)", "flash").option("-r, --ratio <ratio>", "Output aspect ratio").option("-s, --size <resolution>", "Resolution: 1K, 2K, 4K (Gemini Pro only)").option("--dry-run", "Preview parameters without executing").action(async (args, options) => {
39610
+ const startedAt = Date.now();
39315
39611
  try {
39316
39612
  if (args.length < 2) {
39317
39613
  exitWithError(usageError("Need at least one image and a prompt"));
@@ -39327,16 +39623,19 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39327
39623
  exitWithError(usageError("Grok supports only 1 input image for editing.", "Use -p gemini (up to 14 images) or -p openai (up to 16 images) for multi-image editing."));
39328
39624
  }
39329
39625
  if (options.dryRun) {
39330
- outputResult({
39331
- dryRun: true,
39626
+ outputSuccess({
39332
39627
  command: "edit image",
39333
- params: {
39334
- imagePaths: imagePaths.map((p) => resolve33(process.cwd(), p)),
39335
- prompt: prompt3,
39336
- provider,
39337
- model: options.model,
39338
- ratio: options.ratio,
39339
- size: options.size
39628
+ startedAt,
39629
+ dryRun: true,
39630
+ data: {
39631
+ params: {
39632
+ imagePaths: imagePaths.map((p) => resolve33(process.cwd(), p)),
39633
+ prompt: prompt3,
39634
+ provider,
39635
+ model: options.model,
39636
+ ratio: options.ratio,
39637
+ size: options.size
39638
+ }
39340
39639
  }
39341
39640
  });
39342
39641
  return;
@@ -39414,11 +39713,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39414
39713
  };
39415
39714
  const resultModel = result.model;
39416
39715
  if (isJsonMode()) {
39417
- outputResult({
39418
- success: true,
39419
- provider,
39420
- model: resultModel || options.model,
39421
- outputPath
39716
+ outputSuccess({
39717
+ command: "edit image",
39718
+ startedAt,
39719
+ data: {
39720
+ provider,
39721
+ model: resultModel || options.model,
39722
+ outputPath
39723
+ }
39422
39724
  });
39423
39725
  await saveImage();
39424
39726
  return;
@@ -39434,6 +39736,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39434
39736
  }
39435
39737
  });
39436
39738
  editCommand.command("interpolate").description("Create slow motion with frame interpolation (FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-f, --factor <number>", "Slow motion factor: 2, 4, or 8", "2").option("--fps <number>", "Target output FPS").option("--quality <mode>", "Quality: fast or quality", "quality").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39739
+ const startedAt = Date.now();
39437
39740
  try {
39438
39741
  if (options.output) {
39439
39742
  validateOutputPath(options.output);
@@ -39444,14 +39747,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39444
39747
  exitWithError(usageError("Factor must be 2, 4, or 8"));
39445
39748
  }
39446
39749
  if (options.dryRun) {
39447
- outputResult({
39448
- dryRun: true,
39750
+ outputSuccess({
39449
39751
  command: "edit interpolate",
39450
- params: {
39451
- videoPath: absPath,
39452
- factor,
39453
- fps: options.fps ? parseInt(options.fps) : void 0,
39454
- quality: options.quality
39752
+ startedAt,
39753
+ dryRun: true,
39754
+ data: {
39755
+ params: {
39756
+ videoPath: absPath,
39757
+ factor,
39758
+ fps: options.fps ? parseInt(options.fps) : void 0,
39759
+ quality: options.quality
39760
+ }
39455
39761
  }
39456
39762
  });
39457
39763
  return;
@@ -39478,12 +39784,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39478
39784
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
39479
39785
  spinner2.succeed(source_default.green(`Created ${factor}x slow motion`));
39480
39786
  if (isJsonMode()) {
39481
- outputResult({
39482
- success: true,
39483
- originalFps,
39484
- targetFps,
39485
- factor,
39486
- outputPath
39787
+ outputSuccess({
39788
+ command: "edit interpolate",
39789
+ startedAt,
39790
+ data: {
39791
+ originalFps,
39792
+ targetFps,
39793
+ factor,
39794
+ outputPath
39795
+ }
39487
39796
  });
39488
39797
  return;
39489
39798
  }
@@ -39509,6 +39818,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39509
39818
  }
39510
39819
  });
39511
39820
  editCommand.command("upscale-video").description("Upscale video resolution using AI or FFmpeg").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-s, --scale <factor>", "Scale factor: 2 or 4", "2").option("-m, --model <model>", "Model: real-esrgan, topaz", "real-esrgan").option("--ffmpeg", "Use FFmpeg lanczos (free, no API)").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").option("--no-wait", "Start processing and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
39821
+ const startedAt = Date.now();
39512
39822
  try {
39513
39823
  if (options.output) {
39514
39824
  validateOutputPath(options.output);
@@ -39519,14 +39829,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39519
39829
  exitWithError(usageError("Scale must be 2 or 4"));
39520
39830
  }
39521
39831
  if (options.dryRun) {
39522
- outputResult({
39523
- dryRun: true,
39832
+ outputSuccess({
39524
39833
  command: "edit upscale-video",
39525
- params: {
39526
- videoPath: absPath,
39527
- scale,
39528
- model: options.model,
39529
- ffmpeg: options.ffmpeg || false
39834
+ startedAt,
39835
+ dryRun: true,
39836
+ data: {
39837
+ params: {
39838
+ videoPath: absPath,
39839
+ scale,
39840
+ model: options.model,
39841
+ ffmpeg: options.ffmpeg || false
39842
+ }
39530
39843
  }
39531
39844
  });
39532
39845
  return;
@@ -39552,10 +39865,13 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39552
39865
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `scale=${newWidth}:${newHeight}:flags=lanczos`, "-c:a", "copy", outputPath, "-y"]);
39553
39866
  spinner3.succeed(source_default.green(`Upscaled to ${newWidth}x${newHeight}`));
39554
39867
  if (isJsonMode()) {
39555
- outputResult({
39556
- success: true,
39557
- dimensions: `${newWidth}x${newHeight}`,
39558
- outputPath
39868
+ outputSuccess({
39869
+ command: "edit upscale-video",
39870
+ startedAt,
39871
+ data: {
39872
+ dimensions: `${newWidth}x${newHeight}`,
39873
+ outputPath
39874
+ }
39559
39875
  });
39560
39876
  return;
39561
39877
  }
@@ -39995,21 +40311,25 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
39995
40311
  }
39996
40312
  function registerReviewCommand(aiCommand) {
39997
40313
  aiCommand.command("review").description("Review video quality using Gemini AI and optionally auto-fix issues").argument("<video>", "Video file path").option("-s, --storyboard <path>", "Storyboard JSON file for context").option("--auto-apply", "Automatically apply fixable corrections").option("--verify", "Run verification pass after applying fixes").option("-m, --model <model>", "Gemini model: flash (default), flash-2.5, pro", "flash").option("-o, --output <path>", "Output video file path (for auto-apply)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
40314
+ const startedAt = Date.now();
39998
40315
  try {
39999
40316
  if (options.output) {
40000
40317
  validateOutputPath(options.output);
40001
40318
  }
40002
40319
  if (options.dryRun) {
40003
- outputResult({
40004
- dryRun: true,
40320
+ outputSuccess({
40005
40321
  command: "ai review",
40006
- params: {
40007
- videoPath,
40008
- storyboard: options.storyboard,
40009
- autoApply: options.autoApply ?? false,
40010
- verify: options.verify ?? false,
40011
- model: options.model,
40012
- output: options.output
40322
+ startedAt,
40323
+ dryRun: true,
40324
+ data: {
40325
+ params: {
40326
+ videoPath,
40327
+ storyboard: options.storyboard,
40328
+ autoApply: options.autoApply ?? false,
40329
+ verify: options.verify ?? false,
40330
+ model: options.model,
40331
+ output: options.output
40332
+ }
40013
40333
  }
40014
40334
  });
40015
40335
  return;
@@ -40543,6 +40863,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40543
40863
  }
40544
40864
  function registerHighlightsCommands(aiCommand) {
40545
40865
  aiCommand.command("highlights").description("Extract highlights from long-form video/audio content").argument("<media>", "Video or audio file path").option("-o, --output <path>", "Output JSON file with highlights").option("--project <path>", "Create project with highlight clips").option("-d, --duration <seconds>", "Target highlight reel duration", "60").option("-n, --count <number>", "Maximum number of highlights").option("-t, --threshold <value>", "Confidence threshold (0-1)", "0.7").option("--criteria <type>", "Selection criteria: emotional | informative | funny | all", "all").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
40866
+ const startedAt = Date.now();
40546
40867
  try {
40547
40868
  if (options.output) {
40548
40869
  validateOutputPath(options.output);
@@ -40552,20 +40873,23 @@ function registerHighlightsCommands(aiCommand) {
40552
40873
  exitWithError(notFoundError(absPath));
40553
40874
  }
40554
40875
  if (options.dryRun) {
40555
- outputResult({
40556
- dryRun: true,
40876
+ outputSuccess({
40557
40877
  command: "ai highlights",
40558
- params: {
40559
- mediaPath,
40560
- output: options.output,
40561
- project: options.project,
40562
- duration: options.duration,
40563
- count: options.count,
40564
- threshold: options.threshold,
40565
- criteria: options.criteria,
40566
- language: options.language,
40567
- useGemini: options.useGemini ?? false,
40568
- lowRes: options.lowRes ?? false
40878
+ startedAt,
40879
+ dryRun: true,
40880
+ data: {
40881
+ params: {
40882
+ mediaPath,
40883
+ output: options.output,
40884
+ project: options.project,
40885
+ duration: options.duration,
40886
+ count: options.count,
40887
+ threshold: options.threshold,
40888
+ criteria: options.criteria,
40889
+ language: options.language,
40890
+ useGemini: options.useGemini ?? false,
40891
+ lowRes: options.lowRes ?? false
40892
+ }
40569
40893
  }
40570
40894
  });
40571
40895
  return;
@@ -40758,13 +41082,13 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40758
41082
  });
40759
41083
  if (allHighlights.length === 0) {
40760
41084
  analyzeSpinner.warn(source_default.yellow("No highlights detected in the content"));
40761
- process.exit(0);
41085
+ return;
40762
41086
  }
40763
41087
  analyzeSpinner.succeed(source_default.green(`Found ${allHighlights.length} potential highlights`));
40764
41088
  }
40765
41089
  if (allHighlights.length === 0) {
40766
41090
  console.log(source_default.yellow("No highlights detected in the content"));
40767
- process.exit(0);
41091
+ return;
40768
41092
  }
40769
41093
  const filterSpinner = ora("\u{1F4CA} Filtering and ranking...").start();
40770
41094
  const threshold = parseFloat(options.threshold);
@@ -40840,6 +41164,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40840
41164
  }
40841
41165
  });
40842
41166
  aiCommand.command("auto-shorts").alias("shorts").description("Auto-generate shorts from long-form video").argument("<video>", "Video file path").option("-o, --output <path>", "Output file (single) or directory (multiple)").option("-d, --duration <seconds>", "Target duration in seconds (15-60)", "60").option("-n, --count <number>", "Number of shorts to generate", "1").option("-a, --aspect <ratio>", "Aspect ratio: 9:16, 1:1", "9:16").option("--output-dir <dir>", "Output directory for multiple shorts").option("--add-captions", "Add auto-generated captions").option("--caption-style <style>", "Caption style: minimal, bold, animated", "bold").option("--analyze-only", "Show segments without generating").option("-l, --language <lang>", "Language code for transcription").option("--use-gemini", "Use Gemini Video Understanding for enhanced visual+audio analysis").option("--low-res", "Use low resolution mode for longer videos (Gemini only)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
41167
+ const startedAt = Date.now();
40843
41168
  try {
40844
41169
  if (options.output) {
40845
41170
  validateOutputPath(options.output);
@@ -40849,22 +41174,25 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40849
41174
  exitWithError(notFoundError(absPath));
40850
41175
  }
40851
41176
  if (options.dryRun) {
40852
- outputResult({
40853
- dryRun: true,
41177
+ outputSuccess({
40854
41178
  command: "ai auto-shorts",
40855
- params: {
40856
- videoPath,
40857
- output: options.output,
40858
- duration: options.duration,
40859
- count: options.count,
40860
- aspect: options.aspect,
40861
- outputDir: options.outputDir,
40862
- addCaptions: options.addCaptions ?? false,
40863
- captionStyle: options.captionStyle,
40864
- analyzeOnly: options.analyzeOnly ?? false,
40865
- language: options.language,
40866
- useGemini: options.useGemini ?? false,
40867
- lowRes: options.lowRes ?? false
41179
+ startedAt,
41180
+ dryRun: true,
41181
+ data: {
41182
+ params: {
41183
+ videoPath,
41184
+ output: options.output,
41185
+ duration: options.duration,
41186
+ count: options.count,
41187
+ aspect: options.aspect,
41188
+ outputDir: options.outputDir,
41189
+ addCaptions: options.addCaptions ?? false,
41190
+ captionStyle: options.captionStyle,
41191
+ analyzeOnly: options.analyzeOnly ?? false,
41192
+ language: options.language,
41193
+ useGemini: options.useGemini ?? false,
41194
+ lowRes: options.lowRes ?? false
41195
+ }
40868
41196
  }
40869
41197
  });
40870
41198
  return;
@@ -51752,13 +52080,13 @@ function getPresetSettings(preset, aspectRatio) {
51752
52080
  }
51753
52081
  return settings;
51754
52082
  }
51755
- async function runHyperframesExport(projectPath, options, spinner2) {
52083
+ async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
51756
52084
  spinner2.text = "Loading project...";
51757
52085
  const { readFile: readFile42 } = await import("node:fs/promises");
51758
52086
  const { resolve: resolve62, basename: basename22 } = await import("node:path");
51759
52087
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
51760
52088
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
51761
- const { exitWithError: exitWithError2, generalError: generalError2, outputResult: outputResult2 } = await Promise.resolve().then(() => (init_output(), output_exports));
52089
+ const { exitWithError: exitWithError2, generalError: generalError2, outputSuccess: outputSuccess2 } = await Promise.resolve().then(() => (init_output(), output_exports));
51762
52090
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
51763
52091
  const filePath = resolve62(process.cwd(), projectPath);
51764
52092
  const content = await readFile42(filePath, "utf-8");
@@ -51784,10 +52112,10 @@ async function runHyperframesExport(projectPath, options, spinner2) {
51784
52112
  return;
51785
52113
  }
51786
52114
  spinner2.succeed(chalk2.green(`Exported: ${result.outputPath}`));
51787
- outputResult2({
51788
- success: true,
52115
+ outputSuccess2({
51789
52116
  command: "export",
51790
- result: {
52117
+ startedAt,
52118
+ data: {
51791
52119
  outputPath: result.outputPath,
51792
52120
  backend: "hyperframes",
51793
52121
  durationMs: result.durationMs,
@@ -51831,6 +52159,7 @@ Cost: Free (no API keys needed). Requires FFmpeg.
51831
52159
  GIF format: 15fps, no audio, looping. Good for previews and sharing.
51832
52160
  Custom flags (--bitrate, --fps, --resolution, --codec) override preset values.
51833
52161
  Run 'vibe schema export' for structured parameter info.`).action(async (projectPath, options) => {
52162
+ const startedAt = Date.now();
51834
52163
  const spinner2 = ora("Checking FFmpeg...").start();
51835
52164
  try {
51836
52165
  if (options.output) {
@@ -51848,27 +52177,30 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
51848
52177
  exitWithError(usageError(overrideError));
51849
52178
  }
51850
52179
  if (options.dryRun) {
51851
- outputResult({
51852
- dryRun: true,
52180
+ outputSuccess({
51853
52181
  command: "export",
51854
- params: {
51855
- project: projectPath,
51856
- output: options.output || null,
51857
- format: options.format,
51858
- preset: options.preset,
51859
- overwrite: options.overwrite,
51860
- gapFill: options.gapFill,
51861
- backend: options.backend,
51862
- bitrate: options.bitrate ?? null,
51863
- fps: customOverrides.fps ?? null,
51864
- resolution: options.resolution ?? null,
51865
- codec: options.codec ?? null
52182
+ startedAt,
52183
+ dryRun: true,
52184
+ data: {
52185
+ params: {
52186
+ project: projectPath,
52187
+ output: options.output || null,
52188
+ format: options.format,
52189
+ preset: options.preset,
52190
+ overwrite: options.overwrite,
52191
+ gapFill: options.gapFill,
52192
+ backend: options.backend,
52193
+ bitrate: options.bitrate ?? null,
52194
+ fps: customOverrides.fps ?? null,
52195
+ resolution: options.resolution ?? null,
52196
+ codec: options.codec ?? null
52197
+ }
51866
52198
  }
51867
52199
  });
51868
52200
  return;
51869
52201
  }
51870
52202
  if (options.backend === "hyperframes") {
51871
- await runHyperframesExport(projectPath, options, spinner2);
52203
+ await runHyperframesExport(projectPath, options, spinner2, startedAt);
51872
52204
  return;
51873
52205
  }
51874
52206
  const ffmpegPath = await findFFmpeg();
@@ -52118,20 +52450,24 @@ var init_detect = __esm({
52118
52450
  init_validate();
52119
52451
  detectCommand = new Command("detect").description("Auto-detect scenes, beats, and silences in media");
52120
52452
  detectCommand.command("scenes").description("Detect scene changes in video").argument("<video>", "Video file path").option("-t, --threshold <value>", "Scene change threshold (0-1)", "0.3").option("-o, --output <path>", "Output JSON file with timestamps").option("-p, --project <path>", "Add scenes as clips to project").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
52453
+ const startedAt = Date.now();
52121
52454
  const spinner2 = ora("Detecting scenes...").start();
52122
52455
  try {
52123
52456
  if (options.output) {
52124
52457
  validateOutputPath(options.output);
52125
52458
  }
52126
52459
  if (options.dryRun) {
52127
- outputResult({
52128
- dryRun: true,
52460
+ outputSuccess({
52129
52461
  command: "detect scenes",
52130
- params: {
52131
- video: videoPath,
52132
- threshold: options.threshold,
52133
- output: options.output || null,
52134
- project: options.project || null
52462
+ startedAt,
52463
+ dryRun: true,
52464
+ data: {
52465
+ params: {
52466
+ video: videoPath,
52467
+ threshold: options.threshold,
52468
+ output: options.output || null,
52469
+ project: options.project || null
52470
+ }
52135
52471
  }
52136
52472
  });
52137
52473
  return;
@@ -52233,20 +52569,24 @@ var init_detect = __esm({
52233
52569
  }
52234
52570
  });
52235
52571
  detectCommand.command("silence").description("Detect silence in audio/video").argument("<media>", "Media file path").option("-n, --noise <dB>", "Noise threshold in dB", "-30").option("-d, --duration <sec>", "Minimum silence duration", "0.5").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
52572
+ const startedAt = Date.now();
52236
52573
  const spinner2 = ora("Detecting silence...").start();
52237
52574
  try {
52238
52575
  if (options.output) {
52239
52576
  validateOutputPath(options.output);
52240
52577
  }
52241
52578
  if (options.dryRun) {
52242
- outputResult({
52243
- dryRun: true,
52579
+ outputSuccess({
52244
52580
  command: "detect silence",
52245
- params: {
52246
- media: mediaPath,
52247
- noise: options.noise,
52248
- duration: options.duration,
52249
- output: options.output || null
52581
+ startedAt,
52582
+ dryRun: true,
52583
+ data: {
52584
+ params: {
52585
+ media: mediaPath,
52586
+ noise: options.noise,
52587
+ duration: options.duration,
52588
+ output: options.output || null
52589
+ }
52250
52590
  }
52251
52591
  });
52252
52592
  return;
@@ -52315,18 +52655,22 @@ var init_detect = __esm({
52315
52655
  }
52316
52656
  });
52317
52657
  detectCommand.command("beats").description("Detect beats in audio (for music sync)").argument("<audio>", "Audio file path").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (audioPath, options) => {
52658
+ const startedAt = Date.now();
52318
52659
  const spinner2 = ora("Detecting beats...").start();
52319
52660
  try {
52320
52661
  if (options.output) {
52321
52662
  validateOutputPath(options.output);
52322
52663
  }
52323
52664
  if (options.dryRun) {
52324
- outputResult({
52325
- dryRun: true,
52665
+ outputSuccess({
52326
52666
  command: "detect beats",
52327
- params: {
52328
- audio: audioPath,
52329
- output: options.output || null
52667
+ startedAt,
52668
+ dryRun: true,
52669
+ data: {
52670
+ params: {
52671
+ audio: audioPath,
52672
+ output: options.output || null
52673
+ }
52330
52674
  }
52331
52675
  });
52332
52676
  return;
@@ -61356,6 +61700,363 @@ Audio:`);
61356
61700
  }
61357
61701
  });
61358
61702
 
61703
+ // src/commands/_shared/walkthroughs/walkthroughs.ts
61704
+ function loadWalkthrough(topic) {
61705
+ const meta = META[topic];
61706
+ const content = CONTENT[topic];
61707
+ if (!meta || !content) {
61708
+ throw new Error(`Unknown walkthrough topic: ${topic}`);
61709
+ }
61710
+ return {
61711
+ topic,
61712
+ title: meta.title,
61713
+ summary: meta.summary,
61714
+ steps: meta.steps,
61715
+ relatedCommands: meta.relatedCommands,
61716
+ content
61717
+ };
61718
+ }
61719
+ function listWalkthroughs() {
61720
+ return WALKTHROUGH_TOPICS.map((topic) => ({
61721
+ topic,
61722
+ title: META[topic].title,
61723
+ summary: META[topic].summary
61724
+ }));
61725
+ }
61726
+ function isWalkthroughTopic(value) {
61727
+ return typeof value === "string" && WALKTHROUGH_TOPICS.includes(value);
61728
+ }
61729
+ var SCENE_WALKTHROUGH, PIPELINE_WALKTHROUGH, META, CONTENT, WALKTHROUGH_TOPICS;
61730
+ var init_walkthroughs = __esm({
61731
+ "src/commands/_shared/walkthroughs/walkthroughs.ts"() {
61732
+ "use strict";
61733
+ SCENE_WALKTHROUGH = `# Scene authoring with vibe
61734
+
61735
+ A scene project is a directory that is **bilingual**: it works with both
61736
+ \`vibe\` and \`npx hyperframes\`. Each scene is one HTML file with scoped CSS
61737
+ and a paused GSAP timeline. Cheap to edit, cheap to lint, expensive only
61738
+ at render.
61739
+
61740
+ \`vibe scene build\` (v0.60+) is the supported one-shot driver from a
61741
+ written storyboard to an MP4. Plan H (v0.70) added \`--mode agent\` so the
61742
+ host agent itself authors the per-beat HTML \u2014 no internal LLM call.
61743
+
61744
+ ## Three authoring paths
61745
+
61746
+ | Path | Command | When to use |
61747
+ |---|---|---|
61748
+ | **One-shot (default, v0.60+)** | \`vibe scene build [project-dir]\` | STORYBOARD.md has YAML frontmatter + per-beat cues |
61749
+ | **High-craft (manual)** | \`DESIGN.md\` + Hyperframes skill in your agent | Maximum control: hand-author each scene |
61750
+ | **Quick draft** | \`vibe scene add --style <preset>\` | No agent or no API keys; fast iteration |
61751
+
61752
+ Recommend \`vibe scene build\` whenever the user has a STORYBOARD with
61753
+ narration / backdrop intent.
61754
+
61755
+ ## High-craft path
61756
+
61757
+ 1. \`vibe scene init my-promo --visual-style "Swiss Pulse"\` \u2014 seeds
61758
+ \`DESIGN.md\` (palette, typography, motion, transitions) plus the
61759
+ \`vibe.project.yaml\` / \`hyperframes.json\` / \`index.html\` scaffold.
61760
+ In Plan H this **also installs the Hyperframes skill** at the
61761
+ right place for your host (\`.claude/skills/hyperframes/\` for Claude
61762
+ Code, \`.cursor/rules/hyperframes.mdc\` for Cursor, universal
61763
+ \`SKILL.md\` for everyone else).
61764
+ 2. Read \`SKILL.md\` (or the host-specific copy) \u2014 Hyperframes
61765
+ framework rules, motion principles, type system, transition recipes.
61766
+ 3. Read \`DESIGN.md\` \u2014 project-specific palette / typography / motion
61767
+ signature (visual identity hard-gate).
61768
+ 4. Author each scene HTML directly under \`compositions/scene-<id>.html\`
61769
+ using the rules from steps 2 and 3. The skill enforces the visual
61770
+ identity contract \u2014 scenes that contradict DESIGN.md fail lint.
61771
+ 5. \`vibe scene lint --fix\` for mechanical issues, \`vibe scene render\`
61772
+ to MP4.
61773
+
61774
+ ## Quick-draft path
61775
+
61776
+ \`\`\`bash
61777
+ vibe scene init my-promo -r 16:9 -d 30
61778
+ vibe scene add intro --style announcement \\
61779
+ --headline "Ship videos, not clicks"
61780
+ vibe scene lint
61781
+ vibe scene render
61782
+ \`\`\`
61783
+
61784
+ \`vibe scene init\` is **idempotent** \u2014 running it on an existing
61785
+ Hyperframes directory merges \`hyperframes.json\` instead of clobbering it.
61786
+ Safe to invoke on user-provided projects.
61787
+
61788
+ ## Subcommands
61789
+
61790
+ \`\`\`bash
61791
+ vibe scene init <dir> [-r 16:9|9:16|1:1|4:5] [-d <sec>] [--visual-style "<name>"]
61792
+ vibe scene styles [<name>] # list / show vendored visual identities
61793
+ vibe scene install-skill [<dir>] [--host all] # retroactive Hyperframes-skill install
61794
+ vibe scene add <name> --style <preset> [...]
61795
+ vibe scene compose-prompts [<dir>] [--beat <id>] # H2: emit plan, no LLM call
61796
+ vibe scene lint [<root>] [--json] [--fix]
61797
+ vibe scene render [<root>] [--fps 30] [--quality standard] [--format mp4]
61798
+ vibe scene build [<dir>] [--mode agent|batch|auto] # H3 dispatch
61799
+ \`\`\`
61800
+
61801
+ ## Style presets (for \`vibe scene add --style\`)
61802
+
61803
+ - **simple** \u2014 backdrop + bottom caption (default)
61804
+ - **announcement** \u2014 single huge headline, gradient text
61805
+ - **explainer** \u2014 kicker + title + subtitle stack
61806
+ - **kinetic-type** \u2014 words animate in word-by-word
61807
+ - **product-shot** \u2014 corner label + bottom headline + slow zoom
61808
+
61809
+ All presets accept \`--narration <text|file>\`, \`--visuals <prompt>\`,
61810
+ \`--headline\`, \`--kicker\`. With \`--narration\`, scene duration auto-derives
61811
+ from the generated TTS audio.
61812
+
61813
+ ## STORYBOARD-to-MP4 (one command, v0.60+)
61814
+
61815
+ \`\`\`bash
61816
+ vibe scene init my-promo --visual-style "Swiss Pulse" -d 12
61817
+ # (edit STORYBOARD.md with per-beat YAML cues \u2014 narration, backdrop, duration)
61818
+ vibe scene build my-promo
61819
+ \`\`\`
61820
+
61821
+ \`vibe scene build\` reads the STORYBOARD frontmatter + per-beat cues,
61822
+ dispatches TTS + image-gen per beat, then either:
61823
+
61824
+ - **\`--mode agent\`** (default when an agent host is detected) \u2014 emits a
61825
+ \`needs-author\` plan via \`vibe scene compose-prompts\`. The host agent
61826
+ authors each \`compositions/scene-<id>.html\` itself, then re-invoking
61827
+ \`vibe scene build\` proceeds to lint + render.
61828
+ - **\`--mode batch\`** \u2014 VibeFrame runs an internal LLM (Claude / OpenAI /
61829
+ Gemini) to compose the HTML, then renders.
61830
+
61831
+ \`VIBE_BUILD_MODE\` env var overrides the auto-resolve.
61832
+
61833
+ ## Lint feedback loop
61834
+
61835
+ \`\`\`bash
61836
+ vibe scene lint --json --fix
61837
+ \`\`\`
61838
+
61839
+ Returns structured findings. The recommended loop: 1) run lint with
61840
+ \`--fix\` (mechanical fixes applied), 2) if \`errorCount > 0\`, edit the
61841
+ scene HTML, 3) re-lint. Cap retries at 3 \u2014 if errors persist, fall back
61842
+ to a template preset (\`vibe scene add <id> --style simple --force\`)
61843
+ and surface the error to the user.
61844
+
61845
+ ## When to use VibeFrame vs raw Hyperframes
61846
+
61847
+ | Task | Tool |
61848
+ |------|------|
61849
+ | Generate narration + image, then author scene | \`vibe scene add\` |
61850
+ | Generate a full scenes project from a STORYBOARD | \`vibe scene build\` |
61851
+ | Hand-tweak a single scene's animation | edit \`compositions/<file>.html\` directly |
61852
+ | Render the project | \`vibe scene render\` *or* \`npx hyperframes render\` (equivalent) |
61853
+ | Lint | \`vibe scene lint\` *or* \`npx hyperframes lint\` (equivalent) |
61854
+
61855
+ The \`vibe\` CLI adds asset generation, AI orchestration, and pipeline
61856
+ integration on top of Hyperframes' rendering primitives.
61857
+
61858
+ ## Quality checklist before render
61859
+
61860
+ - [ ] \`vibe scene lint\` exits 0 (or only warnings)
61861
+ - [ ] \`vibe doctor\` confirms a usable Chrome (required for render)
61862
+ - [ ] Root \`data-duration\` matches the sum of clip durations
61863
+ - [ ] Aspect ratio in \`vibe.project.yaml\` matches the destination platform
61864
+ `;
61865
+ PIPELINE_WALKTHROUGH = `# YAML pipelines (Video as Code)
61866
+
61867
+ A pipeline is a YAML manifest with steps that reference each other's
61868
+ outputs. \`vibe run pipeline.yaml\` executes them with checkpointing and
61869
+ cost estimation.
61870
+
61871
+ ## Minimal skeleton
61872
+
61873
+ \`\`\`yaml
61874
+ name: promo-video
61875
+ description: 15s product teaser
61876
+ steps:
61877
+ - id: backdrop
61878
+ action: generate-image
61879
+ prompt: "sleek product shot on white background"
61880
+ output: backdrop.png
61881
+ - id: scene
61882
+ action: generate-video
61883
+ image: $backdrop.output # reference previous step output
61884
+ prompt: "slow camera pan"
61885
+ duration: 5
61886
+ output: scene.mp4
61887
+ - id: voice
61888
+ action: generate-tts
61889
+ text: "Meet the new standard."
61890
+ output: voice.mp3
61891
+ - id: final
61892
+ action: compose
61893
+ video: $scene.output
61894
+ audio: $voice.output
61895
+ output: final.mp4
61896
+ \`\`\`
61897
+
61898
+ ## Supported actions
61899
+
61900
+ - \`generate-image\`, \`generate-video\`, \`generate-tts\`, \`generate-music\`,
61901
+ \`generate-sound-effect\`, \`generate-storyboard\`, \`generate-motion\`
61902
+ - \`edit-silence-cut\`, \`edit-jump-cut\`, \`edit-caption\`, \`edit-grade\`,
61903
+ \`edit-reframe\`, \`edit-speed-ramp\`, \`edit-fade\`, \`edit-noise-reduce\`,
61904
+ \`edit-text-overlay\`, \`edit-fill-gaps\`
61905
+ - \`analyze-media\`, \`analyze-video\`, \`analyze-review\`, \`analyze-suggest\`
61906
+ - \`audio-transcribe\`, \`audio-isolate\`, \`audio-voice-clone\`, \`audio-dub\`,
61907
+ \`audio-duck\`
61908
+ - \`detect-scenes\`, \`detect-silence\`, \`detect-beats\`
61909
+ - \`compose\`, \`export\`
61910
+ - \`scene-build\` (Plan H one-shot driver) and \`scene-render\`
61911
+ - \`compose-scenes-with-skills\` (internal-LLM compose pass)
61912
+
61913
+ The full set lives in \`packages/cli/src/pipeline/executor.ts\`.
61914
+
61915
+ ## Variable references
61916
+
61917
+ - \`$<step-id>.output\` \u2014 previous step's output path
61918
+ - \`$<step-id>.result.<field>\` \u2014 structured field from JSON result
61919
+ - \`\${ENV_VAR}\` \u2014 environment variable
61920
+ - Values can be templated: \`"\${SCRIPT_TITLE} - Episode \${EPISODE}"\`
61921
+
61922
+ ## Running
61923
+
61924
+ \`\`\`bash
61925
+ vibe run pipeline.yaml --dry-run # plan + cost estimate, no execution
61926
+ vibe run pipeline.yaml # execute
61927
+ vibe run pipeline.yaml --resume # retry from last successful step
61928
+ vibe run pipeline.yaml --from scene # start at specific step
61929
+ vibe run pipeline.yaml --provider-video kling # override provider
61930
+ \`\`\`
61931
+
61932
+ Checkpoints land next to the YAML: \`pipeline.yaml.checkpoint.json\`.
61933
+
61934
+ ## Authoring tips
61935
+
61936
+ 1. **Start from examples** \u2014 \`examples/demo-pipeline.yaml\` (FFmpeg-only,
61937
+ no keys), \`examples/promo-video.yaml\` (AI providers).
61938
+ 2. **Dry-run first** \u2014 you see estimated cost and resolved variable
61939
+ graph before spending API credits.
61940
+ 3. **Keep step ids short and descriptive** (\`intro\`, \`scene1\`, \`voice\`,
61941
+ \`bgm\`) \u2014 they appear in logs and variable refs.
61942
+ 4. **Name outputs** with extensions matching the action (\`.mp4\`, \`.mp3\`,
61943
+ \`.png\`, \`.json\`).
61944
+ 5. **Declare \`budget:\`** on expensive pipelines:
61945
+ \`\`\`yaml
61946
+ budget:
61947
+ tokens: 500_000
61948
+ max_tool_errors: 3
61949
+ cost_usd: 5.00
61950
+ \`\`\`
61951
+ 6. **Split large pipelines** into smaller YAML files and compose via
61952
+ \`action: run-pipeline\` (nested).
61953
+
61954
+ ## Converting ad-hoc shell sessions to pipelines
61955
+
61956
+ When the user has a working shell sequence, extract steps:
61957
+
61958
+ - Each \`vibe ...\` command becomes one step
61959
+ - File outputs become step outputs; downstream \`-i <file>\` references
61960
+ become \`$<id>.output\`
61961
+ - Shared parameters move to a top-level \`defaults:\` section
61962
+ - Wrap the entire chain in a \`name:\` + \`steps:\` skeleton
61963
+
61964
+ The \`compose\` action is the catch-all assembly step (audio mux, video
61965
+ overlay, etc.) \u2014 useful at the tail of a pipeline.
61966
+ `;
61967
+ META = {
61968
+ scene: {
61969
+ title: "Scene authoring with vibe",
61970
+ summary: "Author per-scene HTML compositions and render to MP4 (BUILD flow)",
61971
+ steps: [
61972
+ 'Run `vibe scene init <dir> --visual-style "<style name>"` to scaffold the project + install the Hyperframes skill (Plan H).',
61973
+ "Edit `STORYBOARD.md` with per-beat YAML cues (narration / backdrop / duration).",
61974
+ "Read `SKILL.md` for the framework rules and `DESIGN.md` for the visual-identity hard-gate.",
61975
+ "Run `vibe scene build <dir>`. With an agent host detected, the CLI emits a `needs-author` plan; the host agent authors each `compositions/scene-<id>.html` and re-invokes to render.",
61976
+ "Run `vibe scene lint --fix` to validate, then `vibe scene render` to produce the MP4."
61977
+ ],
61978
+ relatedCommands: [
61979
+ "vibe scene init",
61980
+ "vibe scene styles",
61981
+ "vibe scene install-skill",
61982
+ "vibe scene compose-prompts",
61983
+ "vibe scene build",
61984
+ "vibe scene lint",
61985
+ "vibe scene render",
61986
+ "vibe scene add"
61987
+ ]
61988
+ },
61989
+ pipeline: {
61990
+ title: "YAML pipelines (Video as Code)",
61991
+ summary: "Author and run reproducible multi-step video workflows",
61992
+ steps: [
61993
+ "Sketch the workflow as YAML \u2014 `name`, `description`, then `steps:` with `id` + `action` + inputs/outputs.",
61994
+ "Reference previous step outputs via `$<step-id>.output` (or `$<step-id>.result.<field>` for structured returns).",
61995
+ "Run `vibe run pipeline.yaml --dry-run` to see the resolved graph + cost estimate before spending API budget.",
61996
+ "Add a `budget:` block (tokens / cost_usd / max_tool_errors) to cap expensive runs.",
61997
+ "Run `vibe run pipeline.yaml` to execute. Failed steps checkpoint to `pipeline.yaml.checkpoint.json`; resume with `--resume`."
61998
+ ],
61999
+ relatedCommands: [
62000
+ "vibe run",
62001
+ "vibe schema --list",
62002
+ "vibe doctor"
62003
+ ]
62004
+ }
62005
+ };
62006
+ CONTENT = {
62007
+ scene: SCENE_WALKTHROUGH,
62008
+ pipeline: PIPELINE_WALKTHROUGH
62009
+ };
62010
+ WALKTHROUGH_TOPICS = ["scene", "pipeline"];
62011
+ }
62012
+ });
62013
+
62014
+ // src/tools/manifest/walkthrough.ts
62015
+ var walkthroughSchema, walkthroughTool, walkthroughTools;
62016
+ var init_walkthrough = __esm({
62017
+ "src/tools/manifest/walkthrough.ts"() {
62018
+ "use strict";
62019
+ init_zod();
62020
+ init_define_tool();
62021
+ init_walkthroughs();
62022
+ walkthroughSchema = external_exports.object({
62023
+ topic: external_exports.enum(WALKTHROUGH_TOPICS).optional().describe(
62024
+ "Walkthrough topic to load. Omit to list every available walkthrough \u2014 useful for discovery on first contact."
62025
+ )
62026
+ });
62027
+ walkthroughTool = defineTool({
62028
+ name: "walkthrough",
62029
+ category: "agent",
62030
+ cost: "free",
62031
+ description: "Load the step-by-step authoring guide for a vibe workflow (BUILD scene authoring, YAML pipeline authoring). Universal CLI-equivalent of Claude Code's /vibe-* slash commands \u2014 any host agent that calls this tool gets the same content the slash menu delivers in Claude Code, with no Claude Code dependency. Without a topic, returns the catalog of walkthroughs for discovery.",
62032
+ schema: walkthroughSchema,
62033
+ async execute(args) {
62034
+ if (!args.topic) {
62035
+ const topics = listWalkthroughs();
62036
+ return {
62037
+ success: true,
62038
+ data: { action: "list", topics },
62039
+ humanLines: [
62040
+ `Available walkthroughs: ${topics.map((t) => t.topic).join(", ")}.`,
62041
+ `Call again with topic to load full content.`
62042
+ ]
62043
+ };
62044
+ }
62045
+ const result = loadWalkthrough(args.topic);
62046
+ return {
62047
+ success: true,
62048
+ data: { action: "show", ...result },
62049
+ humanLines: [
62050
+ `Loaded walkthrough: ${result.title}.`,
62051
+ `${result.steps.length} steps, ${result.relatedCommands.length} related commands, ${result.content.length} chars of guide content.`
62052
+ ]
62053
+ };
62054
+ }
62055
+ });
62056
+ walkthroughTools = [walkthroughTool];
62057
+ }
62058
+ });
62059
+
61359
62060
  // src/tools/manifest/index.ts
61360
62061
  var manifest_exports = {};
61361
62062
  __export(manifest_exports, {
@@ -61370,7 +62071,8 @@ __export(manifest_exports, {
61370
62071
  pipelineTools: () => pipelineTools,
61371
62072
  projectTools: () => projectTools,
61372
62073
  sceneTools: () => sceneTools,
61373
- timelineTools: () => timelineTools
62074
+ timelineTools: () => timelineTools,
62075
+ walkthroughTools: () => walkthroughTools
61374
62076
  });
61375
62077
  var manifest;
61376
62078
  var init_manifest = __esm({
@@ -61387,6 +62089,7 @@ var init_manifest = __esm({
61387
62089
  init_project2();
61388
62090
  init_export2();
61389
62091
  init_agent_only();
62092
+ init_walkthrough();
61390
62093
  manifest = [
61391
62094
  ...sceneTools,
61392
62095
  ...audioTools,
@@ -61398,7 +62101,8 @@ var init_manifest = __esm({
61398
62101
  ...timelineTools,
61399
62102
  ...projectTools,
61400
62103
  ...exportTools,
61401
- ...agentOnlyTools
62104
+ ...agentOnlyTools,
62105
+ ...walkthroughTools
61402
62106
  ];
61403
62107
  }
61404
62108
  });
@@ -61602,7 +62306,7 @@ init_source();
61602
62306
  // package.json
61603
62307
  var package_default = {
61604
62308
  name: "@vibeframe/cli",
61605
- version: "0.70.0",
62309
+ version: "0.72.0",
61606
62310
  description: "VibeFrame CLI - natural language video editing from the terminal",
61607
62311
  private: false,
61608
62312
  license: "MIT",
@@ -61834,20 +62538,24 @@ Examples:
61834
62538
  Cost: Free (no API keys needed). Projects are saved as .vibe.json files.
61835
62539
  Run 'vibe schema project.<command>' for structured parameter info.`);
61836
62540
  projectCommand.command("create").description("Create a new project").argument("<name>", "Project name or path (e.g., 'my-project' or 'output/my-project')").option("-o, --output <path>", "Output file path (overrides name-based path)").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)", "16:9").option("-f, --fps <fps>", "Frame rate", "30").option("--dry-run", "Preview parameters without executing").action(async (name, options) => {
62541
+ const startedAt = Date.now();
61837
62542
  const spinner2 = ora("Creating project...").start();
61838
62543
  try {
61839
62544
  if (options.output) {
61840
62545
  validateOutputPath(options.output);
61841
62546
  }
61842
62547
  if (options.dryRun) {
61843
- outputResult({
61844
- dryRun: true,
62548
+ outputSuccess({
61845
62549
  command: "project create",
61846
- params: {
61847
- name,
61848
- output: options.output || null,
61849
- aspectRatio: options.ratio,
61850
- frameRate: options.fps
62550
+ startedAt,
62551
+ dryRun: true,
62552
+ data: {
62553
+ params: {
62554
+ name,
62555
+ output: options.output || null,
62556
+ aspectRatio: options.ratio,
62557
+ frameRate: options.fps
62558
+ }
61851
62559
  }
61852
62560
  });
61853
62561
  return;
@@ -61912,17 +62620,21 @@ projectCommand.command("info").description("Show project information").argument(
61912
62620
  }
61913
62621
  });
61914
62622
  projectCommand.command("set").description("Update project settings").argument("<file>", "Project file path").option("-n, --name <name>", "Project name").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)").option("-f, --fps <fps>", "Frame rate").option("--dry-run", "Preview parameters without executing").action(async (file, options) => {
62623
+ const startedAt = Date.now();
61915
62624
  const spinner2 = ora("Updating project...").start();
61916
62625
  try {
61917
62626
  if (options.dryRun) {
61918
- outputResult({
61919
- dryRun: true,
62627
+ outputSuccess({
61920
62628
  command: "project set",
61921
- params: {
61922
- file,
61923
- name: options.name || null,
61924
- ratio: options.ratio || null,
61925
- fps: options.fps || null
62629
+ startedAt,
62630
+ dryRun: true,
62631
+ data: {
62632
+ params: {
62633
+ file,
62634
+ name: options.name || null,
62635
+ ratio: options.ratio || null,
62636
+ fps: options.fps || null
62637
+ }
61926
62638
  }
61927
62639
  });
61928
62640
  return;
@@ -61973,18 +62685,22 @@ Typical workflow: create project \u2192 add-source \u2192 add-clip \u2192 trim/s
61973
62685
  Cost: Free (no API keys needed).
61974
62686
  Run 'vibe schema timeline.<command>' for structured parameter info.`);
61975
62687
  timelineCommand.command("add-source").description("Add a media source to the project").argument("<project>", "Project file path").argument("<media>", "Media file path").option("-n, --name <name>", "Source name (defaults to filename)").option("-t, --type <type>", "Media type (video, audio, image, lottie)").option("-d, --duration <seconds>", "Duration in seconds (required for images)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, mediaPath, options) => {
62688
+ const startedAt = Date.now();
61976
62689
  const spinner2 = ora("Adding source...").start();
61977
62690
  try {
61978
62691
  if (options.dryRun) {
61979
- outputResult({
61980
- dryRun: true,
62692
+ outputSuccess({
61981
62693
  command: "timeline add-source",
61982
- params: {
61983
- project: projectPath,
61984
- media: mediaPath,
61985
- name: options.name || null,
61986
- type: options.type || null,
61987
- duration: options.duration || null
62694
+ startedAt,
62695
+ dryRun: true,
62696
+ data: {
62697
+ params: {
62698
+ project: projectPath,
62699
+ media: mediaPath,
62700
+ name: options.name || null,
62701
+ type: options.type || null,
62702
+ duration: options.duration || null
62703
+ }
61988
62704
  }
61989
62705
  });
61990
62706
  return;
@@ -62017,21 +62733,25 @@ timelineCommand.command("add-source").description("Add a media source to the pro
62017
62733
  }
62018
62734
  });
62019
62735
  timelineCommand.command("add-clip").description("Add a clip to the timeline").argument("<project>", "Project file path").argument("<source-id>", "Source ID to use").option("-t, --track <id>", "Track ID (defaults to first matching track)").option("-s, --start <seconds>", "Start time in timeline", "0").option("-d, --duration <seconds>", "Clip duration (defaults to source duration)").option("--offset <seconds>", "Source start offset", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, sourceId, options) => {
62736
+ const startedAt = Date.now();
62020
62737
  const spinner2 = ora("Adding clip...").start();
62021
62738
  try {
62022
62739
  validateResourceId(sourceId);
62023
62740
  if (options.track) validateResourceId(options.track);
62024
62741
  if (options.dryRun) {
62025
- outputResult({
62026
- dryRun: true,
62742
+ outputSuccess({
62027
62743
  command: "timeline add-clip",
62028
- params: {
62029
- project: projectPath,
62030
- sourceId,
62031
- track: options.track || null,
62032
- start: options.start,
62033
- duration: options.duration || null,
62034
- offset: options.offset
62744
+ startedAt,
62745
+ dryRun: true,
62746
+ data: {
62747
+ params: {
62748
+ project: projectPath,
62749
+ sourceId,
62750
+ track: options.track || null,
62751
+ start: options.start,
62752
+ duration: options.duration || null,
62753
+ offset: options.offset
62754
+ }
62035
62755
  }
62036
62756
  });
62037
62757
  return;
@@ -62080,16 +62800,20 @@ timelineCommand.command("add-clip").description("Add a clip to the timeline").ar
62080
62800
  }
62081
62801
  });
62082
62802
  timelineCommand.command("add-track").description("Add a new track").argument("<project>", "Project file path").argument("<type>", "Track type (video, audio)").option("-n, --name <name>", "Track name").option("--dry-run", "Preview parameters without executing").action(async (projectPath, type, options) => {
62803
+ const startedAt = Date.now();
62083
62804
  const spinner2 = ora("Adding track...").start();
62084
62805
  try {
62085
62806
  if (options.dryRun) {
62086
- outputResult({
62087
- dryRun: true,
62807
+ outputSuccess({
62088
62808
  command: "timeline add-track",
62089
- params: {
62090
- project: projectPath,
62091
- type,
62092
- name: options.name || null
62809
+ startedAt,
62810
+ dryRun: true,
62811
+ data: {
62812
+ params: {
62813
+ project: projectPath,
62814
+ type,
62815
+ name: options.name || null
62816
+ }
62093
62817
  }
62094
62818
  });
62095
62819
  return;
@@ -62121,20 +62845,24 @@ timelineCommand.command("add-track").description("Add a new track").argument("<p
62121
62845
  }
62122
62846
  });
62123
62847
  timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)").option("-s, --start <seconds>", "Effect start time (relative to clip)", "0").option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)").option("-p, --params <json>", "Effect parameters as JSON", "{}").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, effectType, options) => {
62848
+ const startedAt = Date.now();
62124
62849
  const spinner2 = ora("Adding effect...").start();
62125
62850
  try {
62126
62851
  validateResourceId(clipId);
62127
62852
  if (options.dryRun) {
62128
- outputResult({
62129
- dryRun: true,
62853
+ outputSuccess({
62130
62854
  command: "timeline add-effect",
62131
- params: {
62132
- project: projectPath,
62133
- clipId,
62134
- effectType,
62135
- start: options.start,
62136
- duration: options.duration || null,
62137
- params: options.params
62855
+ startedAt,
62856
+ dryRun: true,
62857
+ data: {
62858
+ params: {
62859
+ project: projectPath,
62860
+ clipId,
62861
+ effectType,
62862
+ start: options.start,
62863
+ duration: options.duration || null,
62864
+ params: options.params
62865
+ }
62138
62866
  }
62139
62867
  });
62140
62868
  return;
@@ -62174,18 +62902,22 @@ timelineCommand.command("add-effect").description("Add an effect to a clip").arg
62174
62902
  }
62175
62903
  });
62176
62904
  timelineCommand.command("trim").description("Trim a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID").option("--start <seconds>", "New start time").option("--duration <seconds>", "New duration").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
62905
+ const startedAt = Date.now();
62177
62906
  const spinner2 = ora("Trimming clip...").start();
62178
62907
  try {
62179
62908
  validateResourceId(clipId);
62180
62909
  if (options.dryRun) {
62181
- outputResult({
62182
- dryRun: true,
62910
+ outputSuccess({
62183
62911
  command: "timeline trim",
62184
- params: {
62185
- project: projectPath,
62186
- clipId,
62187
- start: options.start || null,
62188
- duration: options.duration || null
62912
+ startedAt,
62913
+ dryRun: true,
62914
+ data: {
62915
+ params: {
62916
+ project: projectPath,
62917
+ clipId,
62918
+ start: options.start || null,
62919
+ duration: options.duration || null
62920
+ }
62189
62921
  }
62190
62922
  });
62191
62923
  return;
@@ -62278,17 +63010,21 @@ timelineCommand.command("list").description("List timeline contents").argument("
62278
63010
  }
62279
63011
  });
62280
63012
  timelineCommand.command("split").description("Split a clip at a specific time").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to split").option("-t, --time <seconds>", "Split time relative to clip start", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63013
+ const startedAt = Date.now();
62281
63014
  const spinner2 = ora("Splitting clip...").start();
62282
63015
  try {
62283
63016
  validateResourceId(clipId);
62284
63017
  if (options.dryRun) {
62285
- outputResult({
62286
- dryRun: true,
63018
+ outputSuccess({
62287
63019
  command: "timeline split",
62288
- params: {
62289
- project: projectPath,
62290
- clipId,
62291
- time: options.time
63020
+ startedAt,
63021
+ dryRun: true,
63022
+ data: {
63023
+ params: {
63024
+ project: projectPath,
63025
+ clipId,
63026
+ time: options.time
63027
+ }
62292
63028
  }
62293
63029
  });
62294
63030
  return;
@@ -62325,17 +63061,21 @@ timelineCommand.command("split").description("Split a clip at a specific time").
62325
63061
  }
62326
63062
  });
62327
63063
  timelineCommand.command("duplicate").description("Duplicate a clip").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to duplicate").option("-t, --time <seconds>", "Start time for duplicate (default: after original)").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63064
+ const startedAt = Date.now();
62328
63065
  const spinner2 = ora("Duplicating clip...").start();
62329
63066
  try {
62330
63067
  validateResourceId(clipId);
62331
63068
  if (options.dryRun) {
62332
- outputResult({
62333
- dryRun: true,
63069
+ outputSuccess({
62334
63070
  command: "timeline duplicate",
62335
- params: {
62336
- project: projectPath,
62337
- clipId,
62338
- time: options.time || null
63071
+ startedAt,
63072
+ dryRun: true,
63073
+ data: {
63074
+ params: {
63075
+ project: projectPath,
63076
+ clipId,
63077
+ time: options.time || null
63078
+ }
62339
63079
  }
62340
63080
  });
62341
63081
  return;
@@ -62367,16 +63107,20 @@ timelineCommand.command("duplicate").description("Duplicate a clip").argument("<
62367
63107
  }
62368
63108
  });
62369
63109
  timelineCommand.command("delete").description("Delete a clip from the timeline").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to delete").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63110
+ const startedAt = Date.now();
62370
63111
  const spinner2 = ora("Deleting clip...").start();
62371
63112
  try {
62372
63113
  validateResourceId(clipId);
62373
63114
  if (options.dryRun) {
62374
- outputResult({
62375
- dryRun: true,
63115
+ outputSuccess({
62376
63116
  command: "timeline delete",
62377
- params: {
62378
- project: projectPath,
62379
- clipId
63117
+ startedAt,
63118
+ dryRun: true,
63119
+ data: {
63120
+ params: {
63121
+ project: projectPath,
63122
+ clipId
63123
+ }
62380
63124
  }
62381
63125
  });
62382
63126
  return;
@@ -62404,19 +63148,23 @@ timelineCommand.command("delete").description("Delete a clip from the timeline")
62404
63148
  }
62405
63149
  });
62406
63150
  timelineCommand.command("move").description("Move a clip to a new position").argument("<project>", "Project file path").argument("<clip-id>", "Clip ID to move").option("-t, --time <seconds>", "New start time").option("--track <track-id>", "Move to different track").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, options) => {
63151
+ const startedAt = Date.now();
62407
63152
  const spinner2 = ora("Moving clip...").start();
62408
63153
  try {
62409
63154
  validateResourceId(clipId);
62410
63155
  if (options.track) validateResourceId(options.track);
62411
63156
  if (options.dryRun) {
62412
- outputResult({
62413
- dryRun: true,
63157
+ outputSuccess({
62414
63158
  command: "timeline move",
62415
- params: {
62416
- project: projectPath,
62417
- clipId,
62418
- time: options.time || null,
62419
- track: options.track || null
63159
+ startedAt,
63160
+ dryRun: true,
63161
+ data: {
63162
+ params: {
63163
+ project: projectPath,
63164
+ clipId,
63165
+ time: options.time || null,
63166
+ track: options.track || null
63167
+ }
62420
63168
  }
62421
63169
  });
62422
63170
  return;
@@ -62499,6 +63247,7 @@ Run 'vibe schema analyze.<command>' for structured parameter info.
62499
63247
  `
62500
63248
  );
62501
63249
  analyzeCommand.command("media").description("Analyze any media: images, videos, or YouTube URLs using Gemini").argument("<source>", "Image/video file path, image URL, or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Describe this image', 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second for video (default: 1)").option("--start <seconds>", "Start offset in seconds (video only)").option("--end <seconds>", "End offset in seconds (video only)").option("--low-res", "Use low resolution mode (fewer tokens)").option("-v, --verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").action(async (source, prompt3, options) => {
63250
+ const startedAt = Date.now();
62502
63251
  try {
62503
63252
  rejectControlChars(prompt3);
62504
63253
  if (options.apiKey) {
@@ -62523,15 +63272,17 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
62523
63272
  spinner2.succeed(source_default.green("Analysis complete"));
62524
63273
  const response = sanitizeLLMResponse(result.response || "");
62525
63274
  if (isJsonMode()) {
62526
- let result_obj = { success: true, response, sourceType: result.sourceType, model: result.model };
63275
+ const data = { response, sourceType: result.sourceType, model: result.model };
62527
63276
  if (result.totalTokens) {
62528
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
62529
- }
62530
- if (options.fields) {
62531
- const fields = options.fields.split(",").map((f) => f.trim());
62532
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
62533
- }
62534
- outputResult(result_obj);
63277
+ data.promptTokens = result.promptTokens;
63278
+ data.responseTokens = result.responseTokens;
63279
+ data.totalTokens = result.totalTokens;
63280
+ }
63281
+ outputSuccess({
63282
+ command: "analyze media",
63283
+ startedAt,
63284
+ data
63285
+ });
62535
63286
  return;
62536
63287
  }
62537
63288
  console.log();
@@ -62554,6 +63305,7 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
62554
63305
  }
62555
63306
  });
62556
63307
  analyzeCommand.command("video").description("Analyze video using Gemini (summarize, Q&A, extract info)").argument("<source>", "Video file path or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second (default: 1, higher for action)").option("--start <seconds>", "Start offset in seconds (for clipping)").option("--end <seconds>", "End offset in seconds (for clipping)").option("--low-res", "Use low resolution mode (fewer tokens, longer videos)").option("-v, --verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").action(async (source, prompt3, options) => {
63308
+ const startedAt = Date.now();
62557
63309
  try {
62558
63310
  rejectControlChars(prompt3);
62559
63311
  if (options.apiKey) {
@@ -62578,15 +63330,17 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
62578
63330
  spinner2.succeed(source_default.green("Video analyzed"));
62579
63331
  const response = sanitizeLLMResponse(result.response || "");
62580
63332
  if (isJsonMode()) {
62581
- let result_obj = { success: true, response, model: result.model };
63333
+ const data = { response, model: result.model };
62582
63334
  if (result.totalTokens) {
62583
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
62584
- }
62585
- if (options.fields) {
62586
- const fields = options.fields.split(",").map((f) => f.trim());
62587
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
62588
- }
62589
- outputResult(result_obj);
63335
+ data.promptTokens = result.promptTokens;
63336
+ data.responseTokens = result.responseTokens;
63337
+ data.totalTokens = result.totalTokens;
63338
+ }
63339
+ outputSuccess({
63340
+ command: "analyze video",
63341
+ startedAt,
63342
+ data
63343
+ });
62590
63344
  return;
62591
63345
  }
62592
63346
  console.log();
@@ -62609,6 +63363,7 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
62609
63363
  });
62610
63364
  registerReviewCommand(analyzeCommand);
62611
63365
  analyzeCommand.command("suggest").description("Get AI edit suggestions using Gemini").argument("<project>", "Project file path").argument("<instruction>", "Natural language instruction").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("--apply", "Apply the first suggestion automatically").action(async (projectPath, instruction, options) => {
63366
+ const startedAt = Date.now();
62612
63367
  try {
62613
63368
  rejectControlChars(instruction);
62614
63369
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
@@ -62624,7 +63379,13 @@ analyzeCommand.command("suggest").description("Get AI edit suggestions using Gem
62624
63379
  const suggestions = await gemini.autoEdit(clips, instruction);
62625
63380
  spinner2.succeed(source_default.green(`Found ${suggestions.length} suggestion(s)`));
62626
63381
  if (isJsonMode()) {
62627
- outputResult({ success: true, suggestions: suggestions.map((s) => ({ type: s.type, description: s.description, confidence: s.confidence, clipIds: s.clipIds, params: s.params })) });
63382
+ outputSuccess({
63383
+ command: "analyze suggest",
63384
+ startedAt,
63385
+ data: {
63386
+ suggestions: suggestions.map((s) => ({ type: s.type, description: s.description, confidence: s.confidence, clipIds: s.clipIds, params: s.params }))
63387
+ }
63388
+ });
62628
63389
  return;
62629
63390
  }
62630
63391
  console.log();
@@ -62693,6 +63454,7 @@ Run 'vibe schema audio.<command>' for structured parameter info.
62693
63454
  `
62694
63455
  );
62695
63456
  audioCommand.command("transcribe").description("Transcribe audio using Whisper").argument("<audio>", "Audio file path").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("-l, --language <lang>", "Language code (e.g., en, ko)").option("-o, --output <path>", "Output file path").option("-f, --format <format>", "Output format: json, srt, vtt (auto-detected from extension)").action(async (audioPath, options) => {
63457
+ const startedAt = Date.now();
62696
63458
  try {
62697
63459
  if (options.output) {
62698
63460
  validateOutputPath(options.output);
@@ -62713,7 +63475,16 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
62713
63475
  }
62714
63476
  spinner2.succeed(source_default.green("Transcription complete"));
62715
63477
  if (isJsonMode()) {
62716
- outputResult({ success: true, fullText: result.fullText, segments: result.segments, language: result.detectedLanguage, outputPath: options.output ? resolve37(process.cwd(), options.output) : void 0 });
63478
+ outputSuccess({
63479
+ command: "audio transcribe",
63480
+ startedAt,
63481
+ data: {
63482
+ fullText: result.fullText,
63483
+ segments: result.segments,
63484
+ language: result.detectedLanguage,
63485
+ outputPath: options.output ? resolve37(process.cwd(), options.output) : void 0
63486
+ }
63487
+ });
62717
63488
  return;
62718
63489
  }
62719
63490
  console.log();
@@ -62743,6 +63514,7 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
62743
63514
  }
62744
63515
  });
62745
63516
  audioCommand.command("voices").description("List available ElevenLabs voices").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").action(async (options) => {
63517
+ const startedAt = Date.now();
62746
63518
  try {
62747
63519
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
62748
63520
  const spinner2 = ora("Fetching voices...").start();
@@ -62751,7 +63523,13 @@ audioCommand.command("voices").description("List available ElevenLabs voices").o
62751
63523
  const voices = await elevenlabs.getVoices();
62752
63524
  spinner2.succeed(source_default.green(`Found ${voices.length} voices`));
62753
63525
  if (isJsonMode()) {
62754
- outputResult({ success: true, voices: voices.map((v) => ({ name: v.name, voiceId: v.voice_id, category: v.category, labels: v.labels })) });
63526
+ outputSuccess({
63527
+ command: "audio voices",
63528
+ startedAt,
63529
+ data: {
63530
+ voices: voices.map((v) => ({ name: v.name, voiceId: v.voice_id, category: v.category, labels: v.labels }))
63531
+ }
63532
+ });
62755
63533
  return;
62756
63534
  }
62757
63535
  console.log();
@@ -62769,9 +63547,15 @@ audioCommand.command("voices").description("List available ElevenLabs voices").o
62769
63547
  }
62770
63548
  });
62771
63549
  audioCommand.command("isolate").description("Isolate vocals from audio using ElevenLabs").argument("<audio>", "Input audio file path").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "vocals.mp3").option("--dry-run", "Preview parameters without executing").action(async (audioPath, options) => {
63550
+ const startedAt = Date.now();
62772
63551
  try {
62773
63552
  if (options.dryRun) {
62774
- outputResult({ dryRun: true, command: "audio isolate", audioPath });
63553
+ outputSuccess({
63554
+ command: "audio isolate",
63555
+ startedAt,
63556
+ dryRun: true,
63557
+ data: { params: { audioPath } }
63558
+ });
62775
63559
  return;
62776
63560
  }
62777
63561
  if (options.output) {
@@ -62793,7 +63577,11 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
62793
63577
  await writeFile32(outputPath, result.audioBuffer);
62794
63578
  spinner2.succeed(source_default.green("Vocals isolated"));
62795
63579
  if (isJsonMode()) {
62796
- outputResult({ success: true, outputPath });
63580
+ outputSuccess({
63581
+ command: "audio isolate",
63582
+ startedAt,
63583
+ data: { outputPath }
63584
+ });
62797
63585
  return;
62798
63586
  }
62799
63587
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -62804,9 +63592,15 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
62804
63592
  }
62805
63593
  });
62806
63594
  audioCommand.command("voice-clone").description("Clone a voice from audio samples using ElevenLabs").argument("[samples...]", "Audio sample files (1-25 files)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-n, --name <name>", "Voice name (required)").option("-d, --description <desc>", "Voice description").option("--labels <json>", `Labels as JSON (e.g., '{"accent": "american"}')`).option("--remove-noise", "Remove background noise from samples").option("--list", "List all available voices").option("--dry-run", "Preview parameters without executing").action(async (samples, options) => {
63595
+ const startedAt = Date.now();
62807
63596
  try {
62808
63597
  if (options.dryRun) {
62809
- outputResult({ dryRun: true, command: "audio voice-clone", samples: samples?.length, name: options.name });
63598
+ outputSuccess({
63599
+ command: "audio voice-clone",
63600
+ startedAt,
63601
+ dryRun: true,
63602
+ data: { params: { samples: samples?.length, name: options.name } }
63603
+ });
62810
63604
  return;
62811
63605
  }
62812
63606
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
@@ -62862,7 +63656,11 @@ audioCommand.command("voice-clone").description("Clone a voice from audio sample
62862
63656
  }
62863
63657
  spinner2.succeed(source_default.green("Voice cloned successfully"));
62864
63658
  if (isJsonMode()) {
62865
- outputResult({ success: true, name: options.name, voiceId: result.voiceId });
63659
+ outputSuccess({
63660
+ command: "audio voice-clone",
63661
+ startedAt,
63662
+ data: { name: options.name, voiceId: result.voiceId }
63663
+ });
62866
63664
  return;
62867
63665
  }
62868
63666
  console.log();
@@ -62880,9 +63678,15 @@ audioCommand.command("voice-clone").description("Clone a voice from audio sample
62880
63678
  }
62881
63679
  });
62882
63680
  audioCommand.command("dub").description("Dub audio/video to another language (transcribe, translate, TTS)").argument("<media>", "Input media file (video or audio)").option("-l, --language <lang>", "Target language code (e.g., es, ko, ja) (required)").option("--source <lang>", "Source language code (default: auto-detect)").option("-v, --voice <id>", "ElevenLabs voice ID for output").option("--analyze-only", "Only analyze and show timing, don't generate audio").option("-o, --output <path>", "Output file path").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
63681
+ const startedAt = Date.now();
62883
63682
  try {
62884
63683
  if (options.dryRun) {
62885
- outputResult({ dryRun: true, command: "audio dub", mediaPath, targetLanguage: options.language, sourceLanguage: options.source, voice: options.voice });
63684
+ outputSuccess({
63685
+ command: "audio dub",
63686
+ startedAt,
63687
+ dryRun: true,
63688
+ data: { params: { mediaPath, targetLanguage: options.language, sourceLanguage: options.source, voice: options.voice } }
63689
+ });
62886
63690
  return;
62887
63691
  }
62888
63692
  if (options.output) {
@@ -63042,7 +63846,16 @@ ${segmentTexts}`,
63042
63846
  await writeFile32(finalOutputPath, combinedBuffer);
63043
63847
  spinner2.succeed(source_default.green("Dubbing complete"));
63044
63848
  if (isJsonMode()) {
63045
- outputResult({ success: true, sourceLanguage: transcriptResult.detectedLanguage || options.source || "auto", targetLanguage: options.language, segmentCount: translatedSegments.length, outputPath: finalOutputPath });
63849
+ outputSuccess({
63850
+ command: "audio dub",
63851
+ startedAt,
63852
+ data: {
63853
+ sourceLanguage: transcriptResult.detectedLanguage || options.source || "auto",
63854
+ targetLanguage: options.language,
63855
+ segmentCount: translatedSegments.length,
63856
+ outputPath: finalOutputPath
63857
+ }
63858
+ });
63046
63859
  return;
63047
63860
  }
63048
63861
  console.log();
@@ -63061,13 +63874,19 @@ ${segmentTexts}`,
63061
63874
  }
63062
63875
  });
63063
63876
  audioCommand.command("duck").description("Auto-duck background music when voice is present (FFmpeg)").argument("<music>", "Background music file path").option("-v, --voice <path>", "Voice/narration track (required)").option("-o, --output <path>", "Output audio file path").option("-t, --threshold <dB>", "Sidechain threshold in dB", "-30").option("-r, --ratio <ratio>", "Compression ratio", "3").option("-a, --attack <ms>", "Attack time in ms", "20").option("-l, --release <ms>", "Release time in ms", "200").option("--dry-run", "Preview parameters without executing").action(async (musicPath, options) => {
63877
+ const startedAt = Date.now();
63064
63878
  try {
63065
63879
  if (options.dryRun) {
63066
63880
  const threshold = parseFloat(options.threshold);
63067
63881
  const ratio2 = parseFloat(options.ratio);
63068
63882
  const attack2 = parseFloat(options.attack);
63069
63883
  const release2 = parseFloat(options.release);
63070
- outputResult({ dryRun: true, command: "audio duck", musicPath, voicePath: options.voice, threshold, ratio: ratio2, attack: attack2, release: release2 });
63884
+ outputSuccess({
63885
+ command: "audio duck",
63886
+ startedAt,
63887
+ dryRun: true,
63888
+ data: { params: { musicPath, voicePath: options.voice, threshold, ratio: ratio2, attack: attack2, release: release2 } }
63889
+ });
63071
63890
  return;
63072
63891
  }
63073
63892
  if (options.output) {
@@ -63092,7 +63911,11 @@ audioCommand.command("duck").description("Auto-duck background music when voice
63092
63911
  await execSafe("ffmpeg", ["-i", absMusic, "-i", absVoice, "-filter_complex", filterComplex, "-map", "[out]", outputPath, "-y"]);
63093
63912
  spinner2.succeed(source_default.green("Audio ducking complete"));
63094
63913
  if (isJsonMode()) {
63095
- outputResult({ success: true, musicPath: absMusic, voicePath: options.voice, threshold: thresholdDb, ratio, outputPath });
63914
+ outputSuccess({
63915
+ command: "audio duck",
63916
+ startedAt,
63917
+ data: { musicPath: absMusic, voicePath: options.voice, threshold: thresholdDb, ratio, outputPath }
63918
+ });
63096
63919
  return;
63097
63920
  }
63098
63921
  console.log();
@@ -63130,6 +63953,7 @@ import { resolve as resolve38 } from "node:path";
63130
63953
  import { existsSync as existsSync29 } from "node:fs";
63131
63954
  function registerScriptPipelineCommands(aiCommand) {
63132
63955
  aiCommand.command("regenerate-scene").description("Regenerate a specific scene in a script-to-video output directory").argument("<project-dir>", "Path to the script-to-video output directory").requiredOption("--scene <numbers>", "Scene number(s) to regenerate (1-based), e.g., 3 or 3,4,5").option("--video-only", "Only regenerate video").option("--narration-only", "Only regenerate narration").option("--image-only", "Only regenerate image").option("-g, --generator <engine>", "Video generator: grok | kling | runway | veo", "grok").option("-i, --image-provider <provider>", "Image provider: gemini | openai | grok", "gemini").option("-v, --voice <id>", "ElevenLabs voice ID for narration").option("-a, --aspect-ratio <ratio>", "Aspect ratio: 16:9 | 9:16 | 1:1", "16:9").option("--retries <count>", "Number of retries for video generation failures", String(DEFAULT_VIDEO_RETRIES)).option("--reference-scene <num>", "Use another scene's image as reference for character consistency").option("--dry-run", "Preview parameters without executing").action(async (projectDir, options) => {
63956
+ const startedAt = Date.now();
63133
63957
  try {
63134
63958
  const outputDir = resolve38(process.cwd(), projectDir);
63135
63959
  const projectPath = resolve38(outputDir, "project.vibe.json");
@@ -63148,20 +63972,23 @@ function registerScriptPipelineCommands(aiCommand) {
63148
63972
  exitWithError(usageError("Scene number must be a positive integer (1-based)", "e.g., --scene 3 or --scene 3,4,5"));
63149
63973
  }
63150
63974
  if (options.dryRun) {
63151
- outputResult({
63152
- dryRun: true,
63975
+ outputSuccess({
63153
63976
  command: "pipeline regenerate-scene",
63154
- params: {
63155
- projectDir,
63156
- scene: sceneNums,
63157
- videoOnly: options.videoOnly ?? false,
63158
- narrationOnly: options.narrationOnly ?? false,
63159
- imageOnly: options.imageOnly ?? false,
63160
- generator: options.generator,
63161
- imageProvider: options.imageProvider,
63162
- aspectRatio: options.aspectRatio,
63163
- retries: options.retries,
63164
- referenceScene: options.referenceScene
63977
+ startedAt,
63978
+ dryRun: true,
63979
+ data: {
63980
+ params: {
63981
+ projectDir,
63982
+ scene: sceneNums,
63983
+ videoOnly: options.videoOnly ?? false,
63984
+ narrationOnly: options.narrationOnly ?? false,
63985
+ imageOnly: options.imageOnly ?? false,
63986
+ generator: options.generator,
63987
+ imageProvider: options.imageProvider,
63988
+ aspectRatio: options.aspectRatio,
63989
+ retries: options.retries,
63990
+ referenceScene: options.referenceScene
63991
+ }
63165
63992
  }
63166
63993
  });
63167
63994
  return;
@@ -63356,6 +64183,7 @@ Styles:
63356
64183
  Required API Key: OPENAI_API_KEY (Whisper transcription)
63357
64184
  `
63358
64185
  ).action(async (videoPath, options) => {
64186
+ const startedAt = Date.now();
63359
64187
  try {
63360
64188
  const absVideoPath = resolve41(process.cwd(), videoPath);
63361
64189
  if (!existsSync32(absVideoPath)) {
@@ -63366,20 +64194,23 @@ Required API Key: OPENAI_API_KEY (Whisper transcription)
63366
64194
  }
63367
64195
  const outputFile = options.output || videoPath.replace(/(\.\w+)$/, "-captioned$1");
63368
64196
  if (options.dryRun) {
63369
- outputResult({
63370
- dryRun: true,
64197
+ outputSuccess({
63371
64198
  command: "pipeline animated-caption",
63372
- params: {
63373
- videoPath: absVideoPath,
63374
- outputPath: outputFile,
63375
- style: options.style,
63376
- highlightColor: options.highlightColor,
63377
- fontSize: options.fontSize ? parseInt(options.fontSize) : "auto",
63378
- position: options.position,
63379
- wordsPerGroup: options.wordsPerGroup ? parseInt(options.wordsPerGroup) : "auto",
63380
- maxChars: options.maxChars ? parseInt(options.maxChars) : "auto",
63381
- language: options.language || "auto",
63382
- fast: !!options.fast
64199
+ startedAt,
64200
+ dryRun: true,
64201
+ data: {
64202
+ params: {
64203
+ videoPath: absVideoPath,
64204
+ outputPath: outputFile,
64205
+ style: options.style,
64206
+ highlightColor: options.highlightColor,
64207
+ fontSize: options.fontSize ? parseInt(options.fontSize) : "auto",
64208
+ position: options.position,
64209
+ wordsPerGroup: options.wordsPerGroup ? parseInt(options.wordsPerGroup) : "auto",
64210
+ maxChars: options.maxChars ? parseInt(options.maxChars) : "auto",
64211
+ language: options.language || "auto",
64212
+ fast: !!options.fast
64213
+ }
63383
64214
  }
63384
64215
  });
63385
64216
  return;
@@ -63410,13 +64241,16 @@ Required API Key: OPENAI_API_KEY (Whisper transcription)
63410
64241
  }
63411
64242
  spinner2.succeed(source_default.green("Animated captions applied successfully"));
63412
64243
  if (isJsonMode()) {
63413
- outputResult({
63414
- success: true,
63415
- outputPath: result.outputPath,
63416
- wordCount: result.wordCount,
63417
- groupCount: result.groupCount,
63418
- style: result.style,
63419
- tier: result.tier
64244
+ outputSuccess({
64245
+ command: "pipeline animated-caption",
64246
+ startedAt,
64247
+ data: {
64248
+ outputPath: result.outputPath,
64249
+ wordCount: result.wordCount,
64250
+ groupCount: result.groupCount,
64251
+ style: result.style,
64252
+ tier: result.tier
64253
+ }
63420
64254
  });
63421
64255
  return;
63422
64256
  }
@@ -67684,18 +68518,22 @@ function isMediaFile(filePath) {
67684
68518
  return mediaExts.includes(ext);
67685
68519
  }
67686
68520
  batchCommand.command("import").description("Import multiple media files from a directory").argument("<project>", "Project file path").argument("<directory>", "Directory containing media files").option("-r, --recursive", "Search subdirectories", false).option("-d, --duration <seconds>", "Default duration for images", "5").option("--filter <pattern>", "Filter files by extension (e.g., '.mp4,.mov')").option("--dry-run", "Preview parameters without executing").action(async (projectPath, directory, options) => {
68521
+ const startedAt = Date.now();
67687
68522
  const spinner2 = ora("Scanning directory...").start();
67688
68523
  try {
67689
68524
  if (options.dryRun) {
67690
- outputResult({
67691
- dryRun: true,
68525
+ outputSuccess({
67692
68526
  command: "batch import",
67693
- params: {
67694
- project: projectPath,
67695
- directory,
67696
- recursive: options.recursive,
67697
- duration: options.duration,
67698
- filter: options.filter || null
68527
+ startedAt,
68528
+ dryRun: true,
68529
+ data: {
68530
+ params: {
68531
+ project: projectPath,
68532
+ directory,
68533
+ recursive: options.recursive,
68534
+ duration: options.duration,
68535
+ filter: options.filter || null
68536
+ }
67699
68537
  }
67700
68538
  });
67701
68539
  return;
@@ -67757,19 +68595,23 @@ batchCommand.command("import").description("Import multiple media files from a d
67757
68595
  }
67758
68596
  });
67759
68597
  batchCommand.command("concat").description("Concatenate multiple sources into sequential clips").argument("<project>", "Project file path").argument("[source-ids...]", "Source IDs to concatenate (or --all)").option("--all", "Concatenate all sources in order", false).option("--track <track-id>", "Track to place clips on").option("--start <seconds>", "Starting time", "0").option("--gap <seconds>", "Gap between clips", "0").option("--dry-run", "Preview parameters without executing").action(async (projectPath, sourceIds, options) => {
68598
+ const startedAt = Date.now();
67760
68599
  const spinner2 = ora("Creating clips...").start();
67761
68600
  try {
67762
68601
  if (options.dryRun) {
67763
- outputResult({
67764
- dryRun: true,
68602
+ outputSuccess({
67765
68603
  command: "batch concat",
67766
- params: {
67767
- project: projectPath,
67768
- sourceIds,
67769
- all: options.all,
67770
- track: options.track || null,
67771
- start: options.start,
67772
- gap: options.gap
68604
+ startedAt,
68605
+ dryRun: true,
68606
+ data: {
68607
+ params: {
68608
+ project: projectPath,
68609
+ sourceIds,
68610
+ all: options.all,
68611
+ track: options.track || null,
68612
+ start: options.start,
68613
+ gap: options.gap
68614
+ }
67773
68615
  }
67774
68616
  });
67775
68617
  return;
@@ -67831,20 +68673,24 @@ batchCommand.command("concat").description("Concatenate multiple sources into se
67831
68673
  });
67832
68674
  batchCommand.command("apply-effect").description("Apply an effect to multiple clips").argument("<project>", "Project file path").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, etc.)").argument("[clip-ids...]", "Clip IDs to apply effect to (or --all)").option("--all", "Apply to all clips", false).option("-d, --duration <seconds>", "Effect duration", "1").option("-s, --start <seconds>", "Effect start time (relative to clip)", "0").option("--intensity <value>", "Effect intensity (0-1)", "1").option("--dry-run", "Preview parameters without executing").action(
67833
68675
  async (projectPath, effectType, clipIds, options) => {
68676
+ const startedAt = Date.now();
67834
68677
  const spinner2 = ora("Applying effects...").start();
67835
68678
  try {
67836
68679
  if (options.dryRun) {
67837
- outputResult({
67838
- dryRun: true,
68680
+ outputSuccess({
67839
68681
  command: "batch apply-effect",
67840
- params: {
67841
- project: projectPath,
67842
- effectType,
67843
- clipIds,
67844
- all: options.all,
67845
- duration: options.duration,
67846
- start: options.start,
67847
- intensity: options.intensity
68682
+ startedAt,
68683
+ dryRun: true,
68684
+ data: {
68685
+ params: {
68686
+ project: projectPath,
68687
+ effectType,
68688
+ clipIds,
68689
+ all: options.all,
68690
+ duration: options.duration,
68691
+ start: options.start,
68692
+ intensity: options.intensity
68693
+ }
67848
68694
  }
67849
68695
  });
67850
68696
  return;
@@ -67898,17 +68744,21 @@ batchCommand.command("apply-effect").description("Apply an effect to multiple cl
67898
68744
  }
67899
68745
  );
67900
68746
  batchCommand.command("remove-clips").description("Remove multiple clips from the timeline").argument("<project>", "Project file path").argument("[clip-ids...]", "Clip IDs to remove").option("--all", "Remove all clips", false).option("--track <track-id>", "Remove clips from specific track only").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipIds, options) => {
68747
+ const startedAt = Date.now();
67901
68748
  const spinner2 = ora("Removing clips...").start();
67902
68749
  try {
67903
68750
  if (options.dryRun) {
67904
- outputResult({
67905
- dryRun: true,
68751
+ outputSuccess({
67906
68752
  command: "batch remove-clips",
67907
- params: {
67908
- project: projectPath,
67909
- clipIds,
67910
- all: options.all,
67911
- track: options.track || null
68753
+ startedAt,
68754
+ dryRun: true,
68755
+ data: {
68756
+ params: {
68757
+ project: projectPath,
68758
+ clipIds,
68759
+ all: options.all,
68760
+ track: options.track || null
68761
+ }
67912
68762
  }
67913
68763
  });
67914
68764
  return;
@@ -68432,6 +69282,27 @@ subcommands for video generation, editing, analysis, and pipeline
68432
69282
  orchestration. Cost tiers below \u2014 **always confirm with the user before
68433
69283
  running High / Very High tier commands**.
68434
69284
 
69285
+ ## Agent host coverage
69286
+
69287
+ VibeFrame's CLI works with any bash-capable AI coding agent. Six host
69288
+ families have first-class auto-detection and scaffolds today:
69289
+
69290
+ - **Claude Code** \u2014 \`CLAUDE.md\` (this file's import wrapper) +
69291
+ \`AGENTS.md\` + \`/vibe-*\` slash commands.
69292
+ - **OpenAI Codex** \u2014 reads \`AGENTS.md\` natively (agents.md spec).
69293
+ - **Cursor** \u2014 reads \`AGENTS.md\` plus \`.cursor/rules/\` for project
69294
+ rules; supports MCP via \`.cursor/mcp.json\`.
69295
+ - **Aider** \u2014 reads \`AGENTS.md\` (binary-only detection by VibeFrame).
69296
+ - **Gemini CLI** \u2014 primary context file is \`GEMINI.md\`; also reads
69297
+ \`AGENTS.md\` if present.
69298
+ - **OpenCode** \u2014 reads \`AGENTS.md\` (officially supported per
69299
+ \`opencode.ai/docs/rules\`); supports MCP via \`.opencode/mcp.json\`.
69300
+
69301
+ Run \`vibe doctor\` to see which host(s) the CLI detects in your
69302
+ environment. Anyone running another bash-capable agent (Continue,
69303
+ Sourcegraph Cody, etc.) still gets this \`AGENTS.md\` as the universal
69304
+ fallback.
69305
+
68435
69306
  ## Self-discovery
68436
69307
 
68437
69308
  Don't memorise the command surface \u2014 read it from the CLI:
@@ -68441,6 +69312,8 @@ vibe --help # command groups overview
68441
69312
  vibe schema --list # full machine-readable catalog (80+)
68442
69313
  vibe schema generate.video # JSON Schema for any single command
68443
69314
  vibe doctor # available providers + system health
69315
+ vibe walkthrough scene # step-by-step authoring guide (scene)
69316
+ vibe walkthrough pipeline # step-by-step authoring guide (pipeline)
68444
69317
  \`\`\`
68445
69318
 
68446
69319
  ## Two flows \u2014 pick by intent
@@ -68570,7 +69443,7 @@ var CLAUDE_MD = `@AGENTS.md
68570
69443
 
68571
69444
  This file imports \`AGENTS.md\` (above) \u2014 that's the canonical source.
68572
69445
  Add anything Claude-Code-specific *below*; everything generic should
68573
- stay in AGENTS.md so other agent hosts (Codex, Cursor, Aider) see it
69446
+ stay in AGENTS.md so other agent hosts (Codex, Cursor, Aider, Gemini CLI, OpenCode) see it
68574
69447
  too.
68575
69448
 
68576
69449
  ## Skills
@@ -68593,6 +69466,20 @@ curl -fsSL https://raw.githubusercontent.com/vericontext/vibeframe/main/scripts/
68593
69466
  <!-- Add Claude-specific notes here: which sub-agents to prefer, custom
68594
69467
  slash commands you've added, conversational style preferences, etc. -->
68595
69468
  `;
69469
+ var GEMINI_MD = `@AGENTS.md
69470
+
69471
+ # Gemini CLI overrides
69472
+
69473
+ This project's canonical agent guidance lives in \`AGENTS.md\` \u2014 read
69474
+ it first. The \`@AGENTS.md\` line above is the import marker; if
69475
+ Gemini CLI doesn't inline imported files in your version, open
69476
+ \`AGENTS.md\` directly. Both files are kept in sync by \`vibe init\`.
69477
+
69478
+ ## Project-specific guidance
69479
+
69480
+ <!-- Add Gemini-CLI-specific notes here: any conventions specific to
69481
+ how you drive vibe from Gemini CLI (preferred models, tone, etc.). -->
69482
+ `;
68596
69483
  function renderEnvExample(opts = {}) {
68597
69484
  const fallback2 = opts.withLocalFallbackHeader === false ? "" : `# \u2500\u2500 Local fallbacks (no key needed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
68598
69485
  # Kokoro TTS (local, ~330MB on first call) \u2192 works without ELEVENLABS_API_KEY
@@ -68651,8 +69538,9 @@ defaults:
68651
69538
 
68652
69539
  // src/commands/init.ts
68653
69540
  init_output();
68654
- var VALID_AGENTS = ["claude-code", "codex", "cursor", "aider", "all", "auto"];
69541
+ var VALID_AGENTS = ["claude-code", "codex", "cursor", "aider", "gemini-cli", "opencode", "all", "auto"];
68655
69542
  var initCommand = new Command("init").description("Scaffold project-scope agent files (AGENTS.md / CLAUDE.md / .env.example / .gitignore)").argument("[project-dir]", "Project directory (defaults to cwd)", ".").option("--agent <id>", `Agent target: ${VALID_AGENTS.join(" | ")}`, "auto").option("--force", "Overwrite existing files instead of skipping").option("--dry-run", "Print the file list without writing anything").action(async (projectDirArg, options) => {
69543
+ const startedAt = Date.now();
68656
69544
  const agent = options.agent;
68657
69545
  if (!VALID_AGENTS.includes(agent)) {
68658
69546
  exitWithError(usageError(`Invalid --agent: ${agent}`, `Must be one of: ${VALID_AGENTS.join(", ")}`));
@@ -68661,6 +69549,7 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
68661
69549
  const projectName = basename19(projectDir);
68662
69550
  const targetHosts = resolveTargets(agent);
68663
69551
  const wantsClaude = targetHosts.includes("claude-code");
69552
+ const wantsGemini = targetHosts.includes("gemini-cli");
68664
69553
  const wantsCrossTool = targetHosts.some((h) => h !== "claude-code");
68665
69554
  const actions = [];
68666
69555
  if (!existsSync34(projectDir)) {
@@ -68686,6 +69575,14 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
68686
69575
  options.dryRun
68687
69576
  ));
68688
69577
  }
69578
+ if (wantsGemini) {
69579
+ actions.push(await writeIfMissing(
69580
+ resolve48(projectDir, "GEMINI.md"),
69581
+ GEMINI_MD,
69582
+ options.force,
69583
+ options.dryRun
69584
+ ));
69585
+ }
68689
69586
  actions.push(await writeIfMissing(
68690
69587
  resolve48(projectDir, ".env.example"),
68691
69588
  renderEnvExample(),
@@ -68704,13 +69601,16 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
68704
69601
  options.dryRun
68705
69602
  ));
68706
69603
  if (isJsonMode()) {
68707
- outputResult({
69604
+ outputSuccess({
68708
69605
  command: "init",
68709
- projectDir,
68710
- agent,
68711
- targetHosts,
68712
- actions,
68713
- dryRun: options.dryRun ?? false
69606
+ startedAt,
69607
+ ...options.dryRun ? { dryRun: true } : {},
69608
+ data: {
69609
+ projectDir,
69610
+ agent,
69611
+ targetHosts,
69612
+ actions
69613
+ }
68714
69614
  });
68715
69615
  return;
68716
69616
  }
@@ -68744,7 +69644,7 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
68744
69644
  });
68745
69645
  function resolveTargets(agent) {
68746
69646
  if (agent === "all") {
68747
- return ["claude-code", "codex", "cursor", "aider"];
69647
+ return ["claude-code", "codex", "cursor", "aider", "gemini-cli", "opencode"];
68748
69648
  }
68749
69649
  if (agent === "auto") {
68750
69650
  const detected = detectedAgentHosts().map((h) => h.id);
@@ -68834,14 +69734,15 @@ Examples:
68834
69734
  $ vibe doctor --json Machine-readable output
68835
69735
  `
68836
69736
  ).action(async (options) => {
69737
+ const startedAt = Date.now();
68837
69738
  if (options.json) process.env.VIBE_JSON_OUTPUT = "1";
68838
69739
  const isJson = process.env.VIBE_JSON_OUTPUT === "1";
68839
69740
  const results = await runDiagnostics();
68840
69741
  if (isJson) {
68841
- outputResult({
68842
- success: true,
69742
+ outputSuccess({
68843
69743
  command: "doctor",
68844
- result: results
69744
+ startedAt,
69745
+ data: { ...results }
68845
69746
  });
68846
69747
  return;
68847
69748
  }
@@ -69213,6 +70114,7 @@ async function generateTestVideo(outputPath) {
69213
70114
  }
69214
70115
  }
69215
70116
  var demoCommand = new Command("demo").description("Run sample edits on a test video (no API keys needed)").option("--keep", "Keep demo output files after completion").option("--json", "Output results as JSON").action(async (options) => {
70117
+ const startedAt = Date.now();
69216
70118
  if (options.json) process.env.VIBE_JSON_OUTPUT = "1";
69217
70119
  const isJson = isJsonMode() || !process.stdin.isTTY;
69218
70120
  if (!commandExists("ffmpeg")) {
@@ -69312,11 +70214,16 @@ var demoCommand = new Command("demo").description("Run sample edits on a test vi
69312
70214
  const passed = results.filter((r) => r.success).length;
69313
70215
  const total = results.length;
69314
70216
  if (isJson) {
69315
- outputResult({
69316
- success: passed === total,
70217
+ outputSuccess({
69317
70218
  command: "demo",
69318
- result: { passed, total, steps: results },
69319
- demoDir: DEMO_DIR
70219
+ startedAt,
70220
+ data: {
70221
+ allPassed: passed === total,
70222
+ passed,
70223
+ total,
70224
+ steps: results,
70225
+ demoDir: DEMO_DIR
70226
+ }
69320
70227
  });
69321
70228
  } else {
69322
70229
  console.log();
@@ -69429,6 +70336,7 @@ Variable references: $step_id.output, $step_id.data.field, \${ENV_VAR}
69429
70336
  Cost: Depends on steps. Use --dry-run to preview before executing.
69430
70337
  Run 'vibe schema run' for structured parameter info.
69431
70338
  `).action(async (pipelinePath, options) => {
70339
+ const startedAt = Date.now();
69432
70340
  loadEnv();
69433
70341
  const isJson = options.json || process.env.VIBE_JSON_OUTPUT === "1";
69434
70342
  const cliBudget = {};
@@ -69461,13 +70369,16 @@ Run 'vibe schema run' for structured parameter info.
69461
70369
  budget: hasBudgetFlag ? cliBudget : void 0
69462
70370
  });
69463
70371
  if (isJson) {
69464
- outputResult({
69465
- dryRun: true,
70372
+ outputSuccess({
69466
70373
  command: "run",
69467
- name: manifest2.name,
69468
- steps: result2.steps.map((s) => ({ id: s.id, action: s.action, estimatedCost: s.data?.estimatedCost })),
69469
- totalSteps: result2.totalSteps,
69470
- budget: result2.budget
70374
+ startedAt,
70375
+ dryRun: true,
70376
+ data: {
70377
+ name: manifest2.name,
70378
+ steps: result2.steps.map((s) => ({ id: s.id, action: s.action, estimatedCost: s.data?.estimatedCost })),
70379
+ totalSteps: result2.totalSteps,
70380
+ budget: result2.budget
70381
+ }
69471
70382
  });
69472
70383
  } else {
69473
70384
  console.log(source_default.bold(" Execution Plan:"));
@@ -69499,24 +70410,27 @@ Run 'vibe schema run' for structured parameter info.
69499
70410
  });
69500
70411
  spinner2?.stop();
69501
70412
  if (isJson) {
69502
- outputResult({
69503
- success: result.success,
69504
- name: result.name,
69505
- error: result.error,
69506
- completedSteps: result.completedSteps,
69507
- totalSteps: result.totalSteps,
69508
- totalDuration: result.totalDuration,
69509
- outputDir: result.outputDir,
69510
- budget: result.budget,
69511
- steps: result.steps.map((s) => ({
69512
- id: s.id,
69513
- action: s.action,
69514
- success: s.success,
69515
- output: s.output,
69516
- duration: s.duration,
69517
- error: s.error,
69518
- data: s.data
69519
- }))
70413
+ outputSuccess({
70414
+ command: "run",
70415
+ startedAt,
70416
+ data: {
70417
+ name: result.name,
70418
+ error: result.error,
70419
+ completedSteps: result.completedSteps,
70420
+ totalSteps: result.totalSteps,
70421
+ totalDuration: result.totalDuration,
70422
+ outputDir: result.outputDir,
70423
+ budget: result.budget,
70424
+ steps: result.steps.map((s) => ({
70425
+ id: s.id,
70426
+ action: s.action,
70427
+ success: s.success,
70428
+ output: s.output,
70429
+ duration: s.duration,
70430
+ error: s.error,
70431
+ data: s.data
70432
+ }))
70433
+ }
69520
70434
  });
69521
70435
  } else {
69522
70436
  console.log();
@@ -70301,6 +71215,81 @@ function getEnvVar(provider) {
70301
71215
  return envVars[provider] || "API_KEY";
70302
71216
  }
70303
71217
 
71218
+ // src/commands/walkthrough.ts
71219
+ init_esm();
71220
+ init_source();
71221
+ init_walkthroughs();
71222
+ init_output();
71223
+ var walkthroughCommand = new Command("walkthrough").description("Step-by-step authoring guide for a vibe workflow (universal /vibe-* slash-command equivalent)").argument("[topic]", `Walkthrough topic: ${WALKTHROUGH_TOPICS.join(" | ")}. Omit to list all.`).option("--list", "List available walkthroughs and exit").action(async (topicArg, options) => {
71224
+ const startedAt = Date.now();
71225
+ if (!topicArg || options.list) {
71226
+ const topics = listWalkthroughs();
71227
+ if (isJsonMode()) {
71228
+ outputSuccess({
71229
+ command: "walkthrough",
71230
+ startedAt,
71231
+ data: {
71232
+ action: "list",
71233
+ topics
71234
+ }
71235
+ });
71236
+ return;
71237
+ }
71238
+ console.log();
71239
+ console.log(source_default.bold.cyan("Available walkthroughs"));
71240
+ console.log(source_default.dim("\u2500".repeat(60)));
71241
+ for (const t of topics) {
71242
+ console.log(` ${source_default.bold(t.topic.padEnd(10))} ${source_default.dim(t.summary)}`);
71243
+ }
71244
+ console.log();
71245
+ console.log(source_default.dim("Run `vibe walkthrough <topic>` for the full guide."));
71246
+ console.log(source_default.dim("Add `--json` to get structured output for an agent host."));
71247
+ console.log();
71248
+ return;
71249
+ }
71250
+ if (!isWalkthroughTopic(topicArg)) {
71251
+ exitWithError(usageError(
71252
+ `Unknown walkthrough topic: ${topicArg}`,
71253
+ `Valid topics: ${WALKTHROUGH_TOPICS.join(", ")}`
71254
+ ));
71255
+ }
71256
+ const topic = topicArg;
71257
+ const result = loadWalkthrough(topic);
71258
+ if (isJsonMode()) {
71259
+ outputSuccess({
71260
+ command: "walkthrough",
71261
+ startedAt,
71262
+ data: {
71263
+ action: "show",
71264
+ ...result
71265
+ }
71266
+ });
71267
+ return;
71268
+ }
71269
+ console.log();
71270
+ console.log(source_default.bold.cyan(result.title));
71271
+ console.log(source_default.dim("\u2500".repeat(60)));
71272
+ console.log(source_default.dim(result.summary));
71273
+ console.log();
71274
+ console.log(source_default.bold("Quick steps"));
71275
+ console.log(source_default.dim("\u2500".repeat(60)));
71276
+ for (let i = 0; i < result.steps.length; i++) {
71277
+ console.log(` ${source_default.cyan(`${i + 1}.`)} ${result.steps[i]}`);
71278
+ }
71279
+ console.log();
71280
+ console.log(source_default.bold("Related commands"));
71281
+ console.log(source_default.dim("\u2500".repeat(60)));
71282
+ for (const cmd of result.relatedCommands) {
71283
+ console.log(` ${source_default.cyan(cmd)}`);
71284
+ }
71285
+ console.log();
71286
+ console.log(source_default.bold("Full guide"));
71287
+ console.log(source_default.dim("\u2500".repeat(60)));
71288
+ console.log();
71289
+ console.log(result.content);
71290
+ console.log();
71291
+ });
71292
+
70304
71293
  // src/index.ts
70305
71294
  init_api_key();
70306
71295
 
@@ -70501,6 +71490,7 @@ program2.addCommand(detectCommand);
70501
71490
  program2.addCommand(batchCommand);
70502
71491
  program2.addCommand(schemaCommand);
70503
71492
  program2.addCommand(contextCommand);
71493
+ program2.addCommand(walkthroughCommand);
70504
71494
  program2.addCommand(mediaCommand, { hidden: true });
70505
71495
  function propagateErrorHandling(cmd) {
70506
71496
  for (const sub of cmd.commands) {