@vibeframe/cli 0.71.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 +1289 -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];
@@ -30047,20 +30082,24 @@ Examples:
30047
30082
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
30048
30083
  Run 'vibe schema scene.<command>' for structured parameter info.`);
30049
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();
30050
30086
  const aspect = validateAspect(options.ratio);
30051
30087
  const duration = validateDuration(options.duration);
30052
30088
  const name = options.name ?? basename4(dir.replace(/\/+$/, ""));
30053
30089
  const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
30054
30090
  if (options.dryRun) {
30055
- outputResult({
30056
- dryRun: true,
30091
+ outputSuccess({
30057
30092
  command: "scene init",
30058
- params: {
30059
- dir,
30060
- name,
30061
- aspect,
30062
- duration,
30063
- 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
+ }
30064
30103
  }
30065
30104
  });
30066
30105
  return;
@@ -30076,19 +30115,21 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30076
30115
  hosts: skillHosts
30077
30116
  });
30078
30117
  if (isJsonMode()) {
30079
- outputResult({
30080
- success: true,
30118
+ outputSuccess({
30081
30119
  command: "scene init",
30082
- dir,
30083
- name,
30084
- aspect,
30085
- duration,
30086
- visualStyle: visualStyle?.name ?? null,
30087
- created: result.created,
30088
- merged: result.merged,
30089
- skipped: result.skipped,
30090
- skillFiles: skillResult.files,
30091
- 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
+ }
30092
30133
  });
30093
30134
  return;
30094
30135
  }
@@ -30129,6 +30170,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30129
30170
  });
30130
30171
  VALID_INSTALL_SKILL_HOSTS = ["claude-code", "cursor", "auto", "all"];
30131
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();
30132
30174
  const hostFlag = options.host ?? "auto";
30133
30175
  if (!VALID_INSTALL_SKILL_HOSTS.includes(hostFlag)) {
30134
30176
  exitWithError(usageError(`Invalid --host: ${hostFlag}`, `Valid: ${VALID_INSTALL_SKILL_HOSTS.join(", ")}`));
@@ -30148,15 +30190,17 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30148
30190
  dryRun: options.dryRun ?? false
30149
30191
  });
30150
30192
  if (isJsonMode()) {
30151
- outputResult({
30152
- success: true,
30193
+ outputSuccess({
30153
30194
  command: "scene install-skill",
30154
- projectDir,
30155
- host: hostFlag,
30156
- resolvedHosts: hosts,
30157
- bundleVersion: result.bundleVersion,
30158
- files: result.files,
30159
- 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
+ }
30160
30204
  });
30161
30205
  return;
30162
30206
  }
@@ -30178,6 +30222,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30178
30222
  }
30179
30223
  });
30180
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();
30181
30226
  const projectDir = resolve14(projectDirArg);
30182
30227
  const result = await getComposePrompts({
30183
30228
  projectDir,
@@ -30185,18 +30230,21 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30185
30230
  });
30186
30231
  if (!result.success) {
30187
30232
  if (isJsonMode()) {
30188
- outputResult({
30233
+ outputSuccess({
30189
30234
  command: "scene compose-prompts",
30190
- ...result
30235
+ startedAt,
30236
+ data: { ...result }
30191
30237
  });
30192
- process.exit(1);
30238
+ process.exitCode = 1;
30239
+ return;
30193
30240
  }
30194
30241
  exitWithError(generalError(result.error ?? "compose-prompts failed"));
30195
30242
  }
30196
30243
  if (isJsonMode()) {
30197
- outputResult({
30244
+ outputSuccess({
30198
30245
  command: "scene compose-prompts",
30199
- ...result
30246
+ startedAt,
30247
+ data: { ...result }
30200
30248
  });
30201
30249
  return;
30202
30250
  }
@@ -30230,20 +30278,23 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30230
30278
  console.log(source_default.dim("Re-run with --json to get the full per-beat userPrompt + cues for direct consumption."));
30231
30279
  });
30232
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();
30233
30282
  if (!name) {
30234
30283
  const all = listVisualStyles();
30235
30284
  if (isJsonMode()) {
30236
- outputResult({
30237
- success: true,
30285
+ outputSuccess({
30238
30286
  command: "scene styles",
30239
- count: all.length,
30240
- styles: all.map((s) => ({
30241
- name: s.name,
30242
- slug: s.slug,
30243
- designer: s.designer,
30244
- mood: s.mood,
30245
- bestFor: s.bestFor
30246
- }))
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
+ }
30247
30298
  });
30248
30299
  return;
30249
30300
  }
@@ -30271,7 +30322,11 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30271
30322
  return;
30272
30323
  }
30273
30324
  if (isJsonMode()) {
30274
- outputResult({ success: true, command: "scene styles", style });
30325
+ outputSuccess({
30326
+ command: "scene styles",
30327
+ startedAt,
30328
+ data: { style }
30329
+ });
30275
30330
  return;
30276
30331
  }
30277
30332
  console.log();
@@ -30293,6 +30348,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30293
30348
  console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
30294
30349
  });
30295
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();
30296
30352
  if (options.style) options.style = validatePreset(options.style);
30297
30353
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
30298
30354
  let tts;
@@ -30303,25 +30359,28 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30303
30359
  }
30304
30360
  if (options.dryRun) {
30305
30361
  const id = slugifySceneName(name);
30306
- outputResult({
30307
- dryRun: true,
30362
+ outputSuccess({
30308
30363
  command: "scene add",
30309
- params: {
30310
- name,
30311
- id,
30312
- preset: options.style,
30313
- narration: !!options.narration,
30314
- visuals: !!options.visuals,
30315
- duration: options.duration,
30316
- headline: options.headline,
30317
- kicker: options.kicker,
30318
- project: options.project,
30319
- insertInto: options.insertInto,
30320
- imageProvider: options.imageProvider,
30321
- tts,
30322
- audio: options.audio,
30323
- // commander sets `audio: false` when --no-audio is passed
30324
- 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
+ }
30325
30384
  }
30326
30385
  });
30327
30386
  return;
@@ -30356,9 +30415,10 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30356
30415
  exitWithError(generalError(result.error ?? "Scene add failed"));
30357
30416
  }
30358
30417
  if (isJsonMode()) {
30359
- outputResult({
30418
+ outputSuccess({
30360
30419
  command: "scene add",
30361
- ...result
30420
+ startedAt,
30421
+ data: { ...result }
30362
30422
  });
30363
30423
  return;
30364
30424
  }
@@ -30390,6 +30450,7 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30390
30450
  }
30391
30451
  });
30392
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();
30393
30454
  const projectDir = resolve14(options.project);
30394
30455
  if (!await rootExists(projectDir, root)) {
30395
30456
  exitWithError(generalError(
@@ -30407,11 +30468,12 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30407
30468
  exitWithError(generalError(`Lint failed: ${msg}`));
30408
30469
  }
30409
30470
  if (isJsonMode()) {
30410
- outputResult({
30471
+ outputSuccess({
30411
30472
  command: "scene lint",
30412
- ...result
30473
+ startedAt,
30474
+ data: { ...result }
30413
30475
  });
30414
- if (!result.ok) process.exit(1);
30476
+ if (!result.ok) process.exitCode = 1;
30415
30477
  return;
30416
30478
  }
30417
30479
  if (result.ok && result.warningCount === 0 && result.infoCount === 0) {
@@ -30445,29 +30507,33 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30445
30507
  console.log(` ${source_default.green("\u2714")} ${fx.file} ${source_default.dim(fx.codes.join(", "))}`);
30446
30508
  }
30447
30509
  }
30448
- if (!result.ok) process.exit(1);
30510
+ if (!result.ok) process.exitCode = 1;
30449
30511
  });
30450
30512
  VALID_FPS = [24, 30, 60];
30451
30513
  VALID_QUALITIES = ["draft", "standard", "high"];
30452
30514
  VALID_FORMATS = ["mp4", "webm", "mov"];
30453
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();
30454
30517
  const fps = validateFps(options.fps);
30455
30518
  const quality = validateQuality(options.quality);
30456
30519
  const format = validateFormat(options.format);
30457
30520
  const workers = validateWorkers(options.workers);
30458
30521
  const projectDir = resolve14(options.project);
30459
30522
  if (options.dryRun) {
30460
- outputResult({
30461
- dryRun: true,
30523
+ outputSuccess({
30462
30524
  command: "scene render",
30463
- params: {
30464
- projectDir,
30465
- root,
30466
- output: options.out,
30467
- fps,
30468
- quality,
30469
- format,
30470
- 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
+ }
30471
30537
  }
30472
30538
  });
30473
30539
  return;
@@ -30488,13 +30554,22 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30488
30554
  if (!result.success) {
30489
30555
  spinner2?.fail("Render failed");
30490
30556
  if (isJsonMode()) {
30491
- outputResult({ command: "scene render", ...result });
30492
- process.exit(1);
30557
+ outputSuccess({
30558
+ command: "scene render",
30559
+ startedAt,
30560
+ data: { ...result }
30561
+ });
30562
+ process.exitCode = 1;
30563
+ return;
30493
30564
  }
30494
30565
  exitWithError(generalError(result.error ?? "Render failed"));
30495
30566
  }
30496
30567
  if (isJsonMode()) {
30497
- outputResult({ command: "scene render", ...result });
30568
+ outputSuccess({
30569
+ command: "scene render",
30570
+ startedAt,
30571
+ data: { ...result }
30572
+ });
30498
30573
  return;
30499
30574
  }
30500
30575
  spinner2?.succeed(source_default.green(`Render complete: ${result.outputPath}`));
@@ -30514,25 +30589,29 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30514
30589
  }
30515
30590
  });
30516
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();
30517
30593
  const projectDir = resolve14(projectDirArg);
30518
30594
  if (options.dryRun) {
30519
- outputResult({
30520
- dryRun: true,
30595
+ outputSuccess({
30521
30596
  command: "scene build",
30522
- params: {
30523
- projectDir,
30524
- mode: options.mode,
30525
- effort: options.effort,
30526
- composer: options.composer,
30527
- skipNarration: options.skipNarration ?? false,
30528
- skipBackdrop: options.skipBackdrop ?? false,
30529
- skipRender: options.skipRender ?? false,
30530
- ttsProvider: options.tts,
30531
- voice: options.voice,
30532
- imageProvider: options.imageProvider,
30533
- imageQuality: options.quality,
30534
- imageSize: options.imageSize,
30535
- 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
+ }
30536
30615
  }
30537
30616
  });
30538
30617
  return;
@@ -30586,13 +30665,22 @@ Run 'vibe schema scene.<command>' for structured parameter info.`);
30586
30665
  if (!result.success) {
30587
30666
  spinner2?.fail(`Build failed: ${result.error}`);
30588
30667
  if (isJsonMode()) {
30589
- outputResult({ command: "scene build", ...result });
30590
- process.exit(1);
30668
+ outputSuccess({
30669
+ command: "scene build",
30670
+ startedAt,
30671
+ data: { ...result }
30672
+ });
30673
+ process.exitCode = 1;
30674
+ return;
30591
30675
  }
30592
30676
  exitWithError(generalError(result.error ?? "Build failed"));
30593
30677
  }
30594
30678
  if (isJsonMode()) {
30595
- outputResult({ command: "scene build", ...result });
30679
+ outputSuccess({
30680
+ command: "scene build",
30681
+ startedAt,
30682
+ data: { ...result }
30683
+ });
30596
30684
  return;
30597
30685
  }
30598
30686
  if (result.phase === "needs-author") {
@@ -31606,27 +31694,31 @@ Use this image analysis to inform the color palette, typography placement, and o
31606
31694
  }
31607
31695
  function registerMotionCommand(aiCommand) {
31608
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();
31609
31698
  try {
31610
31699
  if (options.output) {
31611
31700
  validateOutputPath(options.output);
31612
31701
  }
31613
31702
  if (options.dryRun) {
31614
- outputResult({
31703
+ outputSuccess({
31704
+ command: "generate motion",
31705
+ startedAt,
31615
31706
  dryRun: true,
31616
- command: "ai motion",
31617
- params: {
31618
- description: description.slice(0, 200),
31619
- duration: options.duration,
31620
- width: options.width,
31621
- height: options.height,
31622
- fps: options.fps,
31623
- style: options.style,
31624
- render: options.render ?? false,
31625
- video: options.video,
31626
- image: options.image,
31627
- fromTsx: options.fromTsx,
31628
- model: options.model,
31629
- 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
+ }
31630
31722
  }
31631
31723
  });
31632
31724
  return;
@@ -31740,20 +31832,24 @@ async function executeSoundEffect(options) {
31740
31832
  }
31741
31833
  function registerSoundEffectCommand(parent) {
31742
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();
31743
31836
  try {
31744
31837
  rejectControlChars(prompt3);
31745
31838
  if (options.output) {
31746
31839
  validateOutputPath(options.output);
31747
31840
  }
31748
31841
  if (options.dryRun) {
31749
- outputResult({
31750
- dryRun: true,
31842
+ outputSuccess({
31751
31843
  command: "generate sound-effect",
31752
- params: {
31753
- prompt: prompt3,
31754
- duration: options.duration,
31755
- promptInfluence: options.promptInfluence,
31756
- 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
+ }
31757
31853
  }
31758
31854
  });
31759
31855
  return;
@@ -31780,7 +31876,11 @@ function registerSoundEffectCommand(parent) {
31780
31876
  await writeFile13(outputPath, result.audioBuffer);
31781
31877
  spinner2.succeed(source_default.green("Sound effect generated"));
31782
31878
  if (isJsonMode()) {
31783
- outputResult({ success: true, outputPath });
31879
+ outputSuccess({
31880
+ command: "generate sound-effect",
31881
+ startedAt,
31882
+ data: { outputPath }
31883
+ });
31784
31884
  return;
31785
31885
  }
31786
31886
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -31830,6 +31930,7 @@ async function executeMusicStatus(options) {
31830
31930
  }
31831
31931
  function registerMusicStatusCommand(parent) {
31832
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();
31833
31934
  try {
31834
31935
  const apiKey = await requireApiKey(
31835
31936
  "REPLICATE_API_TOKEN",
@@ -31841,12 +31942,15 @@ function registerMusicStatusCommand(parent) {
31841
31942
  const result = await replicate.getMusicStatus(taskId);
31842
31943
  if (isJsonMode()) {
31843
31944
  const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
31844
- outputResult({
31845
- success: true,
31846
- taskId,
31847
- status,
31848
- audioUrl: result.audioUrl,
31849
- 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
+ }
31850
31954
  });
31851
31955
  return;
31852
31956
  }
@@ -31884,6 +31988,7 @@ var init_music_status = __esm({
31884
31988
  // src/commands/generate/video-cancel.ts
31885
31989
  function registerVideoCancelCommand(parent) {
31886
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();
31887
31992
  try {
31888
31993
  const provider = (options.provider || "grok").toLowerCase();
31889
31994
  let success = false;
@@ -31896,7 +32001,11 @@ function registerVideoCancelCommand(parent) {
31896
32001
  if (success) {
31897
32002
  spinner2.succeed(source_default.green("Generation cancelled"));
31898
32003
  if (isJsonMode()) {
31899
- 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
+ });
31900
32009
  return;
31901
32010
  }
31902
32011
  } else {
@@ -31916,7 +32025,11 @@ function registerVideoCancelCommand(parent) {
31916
32025
  if (success) {
31917
32026
  spinner2.succeed(source_default.green("Generation cancelled"));
31918
32027
  if (isJsonMode()) {
31919
- 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
+ });
31920
32033
  return;
31921
32034
  }
31922
32035
  } else {
@@ -31989,16 +32102,18 @@ async function executeBackground(options) {
31989
32102
  }
31990
32103
  function registerBackgroundCommand(parent) {
31991
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();
31992
32106
  try {
31993
32107
  rejectControlChars(description);
31994
32108
  if (options.output) {
31995
32109
  validateOutputPath(options.output);
31996
32110
  }
31997
32111
  if (options.dryRun) {
31998
- outputResult({
31999
- dryRun: true,
32112
+ outputSuccess({
32000
32113
  command: "generate background",
32001
- params: { description, aspect: options.aspect, output: options.output }
32114
+ startedAt,
32115
+ dryRun: true,
32116
+ data: { params: { description, aspect: options.aspect, output: options.output } }
32002
32117
  });
32003
32118
  return;
32004
32119
  }
@@ -32033,7 +32148,11 @@ function registerBackgroundCommand(parent) {
32033
32148
  await mkdir9(dirname7(outputPath), { recursive: true });
32034
32149
  await writeFile14(outputPath, buffer);
32035
32150
  }
32036
- outputResult({ success: true, imageUrl: img.url, outputPath });
32151
+ outputSuccess({
32152
+ command: "generate background",
32153
+ startedAt,
32154
+ data: { imageUrl: img.url, outputPath }
32155
+ });
32037
32156
  return;
32038
32157
  }
32039
32158
  console.log();
@@ -32179,6 +32298,7 @@ async function executeStoryboard(options) {
32179
32298
  }
32180
32299
  function registerStoryboardCommand(parent) {
32181
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();
32182
32302
  try {
32183
32303
  rejectControlChars(content);
32184
32304
  if (options.output) {
@@ -32194,13 +32314,16 @@ function registerStoryboardCommand(parent) {
32194
32314
  textContent = await readFile14(filePath, "utf-8");
32195
32315
  }
32196
32316
  if (options.dryRun) {
32197
- outputResult({
32198
- dryRun: true,
32317
+ outputSuccess({
32199
32318
  command: "generate storyboard",
32200
- params: {
32201
- content: textContent.substring(0, 200),
32202
- duration: options.duration,
32203
- creativity
32319
+ startedAt,
32320
+ dryRun: true,
32321
+ data: {
32322
+ params: {
32323
+ content: textContent.substring(0, 200),
32324
+ duration: options.duration,
32325
+ creativity
32326
+ }
32204
32327
  }
32205
32328
  });
32206
32329
  return;
@@ -32232,16 +32355,23 @@ function registerStoryboardCommand(parent) {
32232
32355
  const outputPath = resolve19(process.cwd(), options.output);
32233
32356
  await writeFile15(outputPath, JSON.stringify(segments, null, 2), "utf-8");
32234
32357
  if (isJsonMode()) {
32235
- outputResult({
32236
- success: true,
32237
- segmentCount: segments.length,
32238
- segments,
32239
- outputPath
32358
+ outputSuccess({
32359
+ command: "generate storyboard",
32360
+ startedAt,
32361
+ data: {
32362
+ segmentCount: segments.length,
32363
+ segments,
32364
+ outputPath
32365
+ }
32240
32366
  });
32241
32367
  return;
32242
32368
  }
32243
32369
  } else if (isJsonMode()) {
32244
- outputResult({ success: true, segmentCount: segments.length, segments });
32370
+ outputSuccess({
32371
+ command: "generate storyboard",
32372
+ startedAt,
32373
+ data: { segmentCount: segments.length, segments }
32374
+ });
32245
32375
  return;
32246
32376
  }
32247
32377
  console.log();
@@ -32486,6 +32616,7 @@ async function executeSpeech(options) {
32486
32616
  }
32487
32617
  function registerSpeechCommand(parent) {
32488
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();
32489
32620
  try {
32490
32621
  if (!text) {
32491
32622
  if (hasTTY()) {
@@ -32507,10 +32638,11 @@ function registerSpeechCommand(parent) {
32507
32638
  validateOutputPath(options.output);
32508
32639
  }
32509
32640
  if (options.dryRun) {
32510
- outputResult({
32511
- dryRun: true,
32641
+ outputSuccess({
32512
32642
  command: "generate speech",
32513
- params: { text, voice: options.voice, output: options.output }
32643
+ startedAt,
32644
+ dryRun: true,
32645
+ data: { params: { text, voice: options.voice, output: options.output } }
32514
32646
  });
32515
32647
  return;
32516
32648
  }
@@ -32599,10 +32731,13 @@ function registerSpeechCommand(parent) {
32599
32731
  }
32600
32732
  }
32601
32733
  if (isJsonMode()) {
32602
- outputResult({
32603
- success: true,
32604
- characterCount: result.characterCount,
32605
- outputPath
32734
+ outputSuccess({
32735
+ command: "generate speech",
32736
+ startedAt,
32737
+ data: {
32738
+ characterCount: result.characterCount,
32739
+ outputPath
32740
+ }
32606
32741
  });
32607
32742
  return;
32608
32743
  }
@@ -32691,6 +32826,7 @@ async function executeMusic(options) {
32691
32826
  }
32692
32827
  function registerMusicCommand(parent) {
32693
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();
32694
32830
  try {
32695
32831
  rejectControlChars(prompt3);
32696
32832
  if (options.output) {
@@ -32698,16 +32834,19 @@ function registerMusicCommand(parent) {
32698
32834
  }
32699
32835
  const provider = (options.provider || "elevenlabs").toLowerCase();
32700
32836
  if (options.dryRun) {
32701
- outputResult({
32702
- dryRun: true,
32837
+ outputSuccess({
32703
32838
  command: "generate music",
32704
- params: {
32705
- prompt: prompt3,
32706
- provider,
32707
- duration: options.duration,
32708
- model: options.model,
32709
- output: options.output,
32710
- 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
+ }
32711
32850
  }
32712
32851
  });
32713
32852
  return;
@@ -32734,11 +32873,14 @@ function registerMusicCommand(parent) {
32734
32873
  await writeFile17(outputPath, result.audioBuffer);
32735
32874
  spinner2.succeed(source_default.green("Music generated successfully"));
32736
32875
  if (isJsonMode()) {
32737
- outputResult({
32738
- success: true,
32739
- provider: "elevenlabs",
32740
- outputPath,
32741
- duration
32876
+ outputSuccess({
32877
+ command: "generate music",
32878
+ startedAt,
32879
+ data: {
32880
+ provider: "elevenlabs",
32881
+ outputPath,
32882
+ duration
32883
+ }
32742
32884
  });
32743
32885
  return;
32744
32886
  }
@@ -32806,12 +32948,15 @@ function registerMusicCommand(parent) {
32806
32948
  await writeFile17(outputPath, audioBuffer);
32807
32949
  spinner2.succeed(source_default.green("Music generated successfully"));
32808
32950
  if (isJsonMode()) {
32809
- outputResult({
32810
- success: true,
32811
- provider: "replicate",
32812
- taskId: result.taskId,
32813
- audioUrl: finalResult.audioUrl,
32814
- 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
+ }
32815
32960
  });
32816
32961
  return;
32817
32962
  }
@@ -33111,6 +33256,7 @@ import { existsSync as existsSync15 } from "node:fs";
33111
33256
  import { writeFile as writeFile19, mkdir as mkdir11 } from "node:fs/promises";
33112
33257
  function registerThumbnailCommand(parent) {
33113
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();
33114
33260
  try {
33115
33261
  if (description) rejectControlChars(description);
33116
33262
  if (options.output) {
@@ -33146,11 +33292,14 @@ function registerThumbnailCommand(parent) {
33146
33292
  }
33147
33293
  spinner3.succeed(source_default.green("Best frame extracted"));
33148
33294
  if (isJsonMode()) {
33149
- outputResult({
33150
- success: true,
33151
- timestamp: result2.timestamp,
33152
- reason: result2.reason,
33153
- 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
+ }
33154
33303
  });
33155
33304
  return;
33156
33305
  }
@@ -33198,7 +33347,11 @@ function registerThumbnailCommand(parent) {
33198
33347
  await mkdir11(dirname9(outputPath), { recursive: true });
33199
33348
  await writeFile19(outputPath, buffer);
33200
33349
  }
33201
- outputResult({ success: true, imageUrl: img.url, outputPath });
33350
+ outputSuccess({
33351
+ command: "generate thumbnail",
33352
+ startedAt,
33353
+ data: { imageUrl: img.url, outputPath }
33354
+ });
33202
33355
  return;
33203
33356
  }
33204
33357
  console.log();
@@ -33269,6 +33422,7 @@ function getStatusColor(status) {
33269
33422
  }
33270
33423
  function registerVideoStatusCommand(parent) {
33271
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();
33272
33426
  try {
33273
33427
  const provider = (options.provider || "grok").toLowerCase();
33274
33428
  if (provider === "grok") {
@@ -33291,14 +33445,17 @@ function registerVideoStatusCommand(parent) {
33291
33445
  outputPath = resolve24(process.cwd(), options.output);
33292
33446
  await writeFile20(outputPath, buffer);
33293
33447
  }
33294
- outputResult({
33295
- success: true,
33296
- taskId,
33297
- provider: "grok",
33298
- status: result.status,
33299
- videoUrl: result.videoUrl,
33300
- error: result.error,
33301
- 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
+ }
33302
33459
  });
33303
33460
  return;
33304
33461
  }
@@ -33356,15 +33513,18 @@ function registerVideoStatusCommand(parent) {
33356
33513
  outputPath = resolve24(process.cwd(), options.output);
33357
33514
  await writeFile20(outputPath, buffer);
33358
33515
  }
33359
- outputResult({
33360
- success: true,
33361
- taskId,
33362
- provider: "runway",
33363
- status: result.status,
33364
- videoUrl: result.videoUrl,
33365
- progress: result.progress,
33366
- error: result.error,
33367
- 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
+ }
33368
33528
  });
33369
33529
  return;
33370
33530
  }
@@ -33420,15 +33580,18 @@ function registerVideoStatusCommand(parent) {
33420
33580
  outputPath = resolve24(process.cwd(), options.output);
33421
33581
  await writeFile20(outputPath, buffer);
33422
33582
  }
33423
- outputResult({
33424
- success: true,
33425
- taskId,
33426
- provider: "kling",
33427
- status: result.status,
33428
- videoUrl: result.videoUrl,
33429
- duration: result.duration,
33430
- error: result.error,
33431
- 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
+ }
33432
33595
  });
33433
33596
  return;
33434
33597
  }
@@ -33492,22 +33655,26 @@ import { resolve as resolve25 } from "node:path";
33492
33655
  import { writeFile as writeFile21 } from "node:fs/promises";
33493
33656
  function registerVideoExtendCommand(parent) {
33494
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();
33495
33659
  try {
33496
33660
  const provider = (options.provider || "kling").toLowerCase();
33497
33661
  if (options.output) {
33498
33662
  validateOutputPath(options.output);
33499
33663
  }
33500
33664
  if (options.dryRun) {
33501
- outputResult({
33502
- dryRun: true,
33665
+ outputSuccess({
33503
33666
  command: "generate video-extend",
33504
- params: {
33505
- id,
33506
- provider,
33507
- prompt: options.prompt,
33508
- duration: options.duration,
33509
- negative: options.negative,
33510
- 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
+ }
33511
33678
  }
33512
33679
  });
33513
33680
  return;
@@ -33564,13 +33731,16 @@ function registerVideoExtendCommand(parent) {
33564
33731
  outputPath = resolve25(process.cwd(), options.output);
33565
33732
  await writeFile21(outputPath, buffer);
33566
33733
  }
33567
- outputResult({
33568
- success: true,
33569
- provider: "kling",
33570
- taskId: result.id,
33571
- videoUrl: finalResult.videoUrl,
33572
- duration: finalResult.duration,
33573
- 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
+ }
33574
33744
  });
33575
33745
  return;
33576
33746
  }
@@ -33650,13 +33820,16 @@ function registerVideoExtendCommand(parent) {
33650
33820
  outputPath = resolve25(process.cwd(), options.output);
33651
33821
  await writeFile21(outputPath, buffer);
33652
33822
  }
33653
- outputResult({
33654
- success: true,
33655
- provider: "veo",
33656
- taskId: result.id,
33657
- videoUrl: finalResult.videoUrl,
33658
- duration: finalResult.duration,
33659
- 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
+ }
33660
33833
  });
33661
33834
  return;
33662
33835
  }
@@ -33771,6 +33944,7 @@ Examples:
33771
33944
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
33772
33945
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
33773
33946
  $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
33947
+ const startedAt = Date.now();
33774
33948
  try {
33775
33949
  if (!prompt3) {
33776
33950
  if (hasTTY()) {
@@ -33829,18 +34003,21 @@ Examples:
33829
34003
  provider = resolved?.name ?? "gemini";
33830
34004
  }
33831
34005
  if (options.dryRun) {
33832
- outputResult({
33833
- dryRun: true,
34006
+ outputSuccess({
33834
34007
  command: "generate image",
33835
- params: {
33836
- prompt: prompt3,
33837
- provider,
33838
- model: options.model,
33839
- ratio: options.ratio,
33840
- size: options.size,
33841
- quality: options.quality,
33842
- count: options.count,
33843
- 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
+ }
33844
34021
  }
33845
34022
  });
33846
34023
  return;
@@ -33878,14 +34055,17 @@ Examples:
33878
34055
  await mkdir12(dirname10(outputPath), { recursive: true });
33879
34056
  await writeFile22(outputPath, buffer);
33880
34057
  }
33881
- outputResult({
33882
- success: true,
33883
- provider: "openai",
33884
- images: result.images.map((img) => ({
33885
- url: img.url,
33886
- revisedPrompt: img.revisedPrompt
33887
- })),
33888
- 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
+ }
33889
34069
  });
33890
34070
  return;
33891
34071
  }
@@ -33997,13 +34177,18 @@ Examples:
33997
34177
  await mkdir12(dirname10(outputPath), { recursive: true });
33998
34178
  await writeFile22(outputPath, buffer);
33999
34179
  }
34000
- outputResult({
34001
- success: true,
34002
- provider: "gemini",
34003
- images: result.images.map((img) => ({
34004
- mimeType: img.mimeType
34005
- })),
34006
- 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
+ }
34007
34192
  });
34008
34193
  return;
34009
34194
  }
@@ -34087,11 +34272,14 @@ Examples:
34087
34272
  await mkdir12(dirname10(outputPath), { recursive: true });
34088
34273
  await writeFile22(outputPath, buffer);
34089
34274
  }
34090
- outputResult({
34091
- success: true,
34092
- provider: "grok",
34093
- images: result.images.map((img) => ({ url: img.url })),
34094
- 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
+ }
34095
34283
  });
34096
34284
  return;
34097
34285
  }
@@ -34162,11 +34350,14 @@ Examples:
34162
34350
  proc.on("close", (code) => {
34163
34351
  if (code === 0) {
34164
34352
  if (isJsonMode()) {
34165
- outputResult({
34166
- success: true,
34167
- provider: "runway",
34168
- images: [{ format: "file" }],
34169
- outputPath
34353
+ outputSuccess({
34354
+ command: "generate image",
34355
+ startedAt,
34356
+ data: {
34357
+ provider: "runway",
34358
+ images: [{ format: "file" }],
34359
+ outputPath
34360
+ }
34170
34361
  });
34171
34362
  } else {
34172
34363
  spinner2.succeed(source_default.green("Generated image with Runway"));
@@ -35892,6 +36083,7 @@ Examples:
35892
36083
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
35893
36084
  $ vibe gen vid "ocean waves" -o waves.mp4 -p veo --resolution 1080p # Veo
35894
36085
  $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`).action(async (prompt3, options) => {
36086
+ const startedAt = Date.now();
35895
36087
  try {
35896
36088
  if (!prompt3) {
35897
36089
  if (hasTTY()) {
@@ -35983,19 +36175,22 @@ Examples:
35983
36175
  options.ratio = "16:9";
35984
36176
  }
35985
36177
  if (options.dryRun) {
35986
- outputResult({
35987
- dryRun: true,
36178
+ outputSuccess({
35988
36179
  command: "generate video",
35989
- params: {
35990
- prompt: prompt3,
35991
- provider,
35992
- duration: options.duration,
35993
- ratio: options.ratio,
35994
- image: options.image,
35995
- mode: options.mode,
35996
- negative: options.negative,
35997
- resolution: options.resolution,
35998
- 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
+ }
35999
36194
  }
36000
36195
  });
36001
36196
  return;
@@ -36275,13 +36470,16 @@ Examples:
36275
36470
  outputPath = resolve29(process.cwd(), options.output);
36276
36471
  await writeFile25(outputPath, buffer);
36277
36472
  }
36278
- outputResult({
36279
- success: true,
36280
- provider,
36281
- taskId: result?.id,
36282
- videoUrl: finalResult.videoUrl,
36283
- duration: finalResult.duration,
36284
- 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
+ }
36285
36483
  });
36286
36484
  return;
36287
36485
  }
@@ -37714,6 +37912,7 @@ Examples:
37714
37912
  $ vibe ed sc video.mp4 --dry-run --json
37715
37913
 
37716
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();
37717
37916
  try {
37718
37917
  if (options.output) {
37719
37918
  validateOutputPath(options.output);
@@ -37730,16 +37929,19 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37730
37929
  const outputPath = options.output || `${name}-cut${ext}`;
37731
37930
  const useGemini = options.useGemini || false;
37732
37931
  if (options.dryRun) {
37733
- outputResult({
37734
- dryRun: true,
37932
+ outputSuccess({
37735
37933
  command: "edit silence-cut",
37736
- params: {
37737
- videoPath: absVideoPath,
37738
- noiseThreshold: parseFloat(options.noise),
37739
- minDuration: parseFloat(options.minDuration),
37740
- padding: parseFloat(options.padding),
37741
- useGemini,
37742
- 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
+ }
37743
37945
  }
37744
37946
  });
37745
37947
  return;
@@ -37764,13 +37966,16 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
37764
37966
  }
37765
37967
  spinner2.succeed(source_default.green("Silence detection complete"));
37766
37968
  if (isJsonMode()) {
37767
- outputResult({
37768
- success: true,
37769
- method: result.method,
37770
- totalDuration: result.totalDuration,
37771
- silentPeriods: result.silentPeriods,
37772
- silentDuration: result.silentDuration,
37773
- 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
+ }
37774
37979
  });
37775
37980
  return;
37776
37981
  }
@@ -37808,6 +38013,7 @@ Examples:
37808
38013
  $ vibe ed cap video.mp4 --dry-run --json
37809
38014
 
37810
38015
  Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoPath, options) => {
38016
+ const startedAt = Date.now();
37811
38017
  try {
37812
38018
  if (options.output) {
37813
38019
  validateOutputPath(options.output);
@@ -37820,16 +38026,19 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37820
38026
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37821
38027
  }
37822
38028
  if (options.dryRun) {
37823
- outputResult({
37824
- dryRun: true,
38029
+ outputSuccess({
37825
38030
  command: "edit caption",
37826
- params: {
37827
- videoPath: absVideoPath,
37828
- style: options.style,
37829
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
37830
- fontColor: options.color,
37831
- language: options.language,
37832
- 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
+ }
37833
38042
  }
37834
38043
  });
37835
38044
  return;
@@ -37858,12 +38067,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37858
38067
  }
37859
38068
  spinner2.succeed(source_default.green("Captions applied"));
37860
38069
  if (isJsonMode()) {
37861
- outputResult({
37862
- success: true,
37863
- segmentCount: result.segmentCount,
37864
- style: options.style || "bold",
37865
- outputPath: result.outputPath,
37866
- 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
+ }
37867
38079
  });
37868
38080
  return;
37869
38081
  }
@@ -37882,6 +38094,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37882
38094
  }
37883
38095
  });
37884
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();
37885
38098
  try {
37886
38099
  if (options.output) {
37887
38100
  validateOutputPath(options.output);
@@ -37894,13 +38107,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37894
38107
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37895
38108
  }
37896
38109
  if (options.dryRun) {
37897
- outputResult({
37898
- dryRun: true,
38110
+ outputSuccess({
37899
38111
  command: "edit noise-reduce",
37900
- params: {
37901
- inputPath: absInputPath,
37902
- strength: options.strength,
37903
- 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
+ }
37904
38120
  }
37905
38121
  });
37906
38122
  return;
@@ -37921,11 +38137,14 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37921
38137
  }
37922
38138
  spinner2.succeed(source_default.green("Noise reduction complete"));
37923
38139
  if (isJsonMode()) {
37924
- outputResult({
37925
- success: true,
37926
- inputDuration: result.inputDuration,
37927
- strength: options.strength || "medium",
37928
- 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
+ }
37929
38148
  });
37930
38149
  return;
37931
38150
  }
@@ -37941,6 +38160,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37941
38160
  }
37942
38161
  });
37943
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();
37944
38164
  try {
37945
38165
  if (options.output) {
37946
38166
  validateOutputPath(options.output);
@@ -37953,15 +38173,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37953
38173
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
37954
38174
  }
37955
38175
  if (options.dryRun) {
37956
- outputResult({
37957
- dryRun: true,
38176
+ outputSuccess({
37958
38177
  command: "edit fade",
37959
- params: {
37960
- videoPath: absVideoPath,
37961
- fadeIn: parseFloat(options.fadeIn),
37962
- fadeOut: parseFloat(options.fadeOut),
37963
- audioOnly: options.audioOnly || false,
37964
- 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
+ }
37965
38188
  }
37966
38189
  });
37967
38190
  return;
@@ -37984,12 +38207,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
37984
38207
  }
37985
38208
  spinner2.succeed(source_default.green("Fade effects applied"));
37986
38209
  if (isJsonMode()) {
37987
- outputResult({
37988
- success: true,
37989
- totalDuration: result.totalDuration,
37990
- fadeInApplied: result.fadeInApplied,
37991
- fadeOutApplied: result.fadeOutApplied,
37992
- 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
+ }
37993
38219
  });
37994
38220
  return;
37995
38221
  }
@@ -38006,6 +38232,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38006
38232
  }
38007
38233
  });
38008
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();
38009
38236
  try {
38010
38237
  if (options.output) {
38011
38238
  validateOutputPath(options.output);
@@ -38018,14 +38245,17 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38018
38245
  exitWithError(notFoundError(absSrtPath));
38019
38246
  }
38020
38247
  if (options.dryRun) {
38021
- outputResult({
38022
- dryRun: true,
38248
+ outputSuccess({
38023
38249
  command: "edit translate-srt",
38024
- params: {
38025
- srtPath: absSrtPath,
38026
- targetLanguage: options.target,
38027
- provider: options.provider || "claude",
38028
- 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
+ }
38029
38259
  }
38030
38260
  });
38031
38261
  return;
@@ -38055,12 +38285,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38055
38285
  }
38056
38286
  spinner2.succeed(source_default.green("Translation complete"));
38057
38287
  if (isJsonMode()) {
38058
- outputResult({
38059
- success: true,
38060
- segmentCount: result.segmentCount,
38061
- sourceLanguage: result.sourceLanguage,
38062
- targetLanguage: result.targetLanguage,
38063
- 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
+ }
38064
38297
  });
38065
38298
  return;
38066
38299
  }
@@ -38077,6 +38310,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38077
38310
  }
38078
38311
  });
38079
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();
38080
38314
  try {
38081
38315
  if (options.output) {
38082
38316
  validateOutputPath(options.output);
@@ -38091,15 +38325,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38091
38325
  }
38092
38326
  if (options.dryRun) {
38093
38327
  const fillers2 = options.fillers ? options.fillers.split(",").map((f) => f.trim()) : void 0;
38094
- outputResult({
38095
- dryRun: true,
38328
+ outputSuccess({
38096
38329
  command: "edit jump-cut",
38097
- params: {
38098
- videoPath: absVideoPath,
38099
- fillers: fillers2,
38100
- padding: parseFloat(options.padding),
38101
- language: options.language,
38102
- 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
+ }
38103
38340
  }
38104
38341
  });
38105
38342
  return;
@@ -38128,13 +38365,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
38128
38365
  }
38129
38366
  spinner2.succeed(source_default.green("Filler detection complete"));
38130
38367
  if (isJsonMode()) {
38131
- outputResult({
38132
- success: true,
38133
- totalDuration: result.totalDuration,
38134
- fillerCount: result.fillerCount,
38135
- fillerDuration: result.fillerDuration,
38136
- fillers: result.fillers,
38137
- 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
+ }
38138
38378
  });
38139
38379
  return;
38140
38380
  }
@@ -38936,6 +39176,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38936
39176
  registerEditCommands(editCommand);
38937
39177
  registerFillGapsCommand(editCommand);
38938
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();
38939
39180
  try {
38940
39181
  if (options.style) rejectControlChars(options.style);
38941
39182
  if (options.output) {
@@ -38951,13 +39192,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38951
39192
  exitWithError(notFoundError("FFmpeg not found. Install with: brew install ffmpeg"));
38952
39193
  }
38953
39194
  if (options.dryRun) {
38954
- outputResult({
38955
- dryRun: true,
39195
+ outputSuccess({
38956
39196
  command: "edit grade",
38957
- params: {
38958
- videoPath: resolve33(process.cwd(), videoPath),
38959
- style: options.style || options.preset,
38960
- 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
+ }
38961
39205
  }
38962
39206
  });
38963
39207
  return;
@@ -38983,12 +39227,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
38983
39227
  if (isJsonMode()) {
38984
39228
  const absPath2 = resolve33(process.cwd(), videoPath);
38985
39229
  const gradeOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
38986
- outputResult({
38987
- success: true,
38988
- style: options.preset || options.style,
38989
- description: gradeResult.description,
38990
- ffmpegFilter: gradeResult.ffmpegFilter,
38991
- 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
+ }
38992
39239
  });
38993
39240
  return;
38994
39241
  }
@@ -39017,6 +39264,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39017
39264
  }
39018
39265
  });
39019
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();
39020
39268
  try {
39021
39269
  if (!options.text || options.text.length === 0) {
39022
39270
  exitWithError(usageError("At least one --text option is required", 'Example: vibe edit text-overlay video.mp4 -t "NEXUS AI" --style center-bold'));
@@ -39029,18 +39277,21 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39029
39277
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39030
39278
  }
39031
39279
  if (options.dryRun) {
39032
- outputResult({
39033
- dryRun: true,
39280
+ outputSuccess({
39034
39281
  command: "edit text-overlay",
39035
- params: {
39036
- videoPath: resolve33(process.cwd(), videoPath),
39037
- texts: options.text,
39038
- style: options.style,
39039
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
39040
- fontColor: options.fontColor,
39041
- fade: parseFloat(options.fade),
39042
- start: parseFloat(options.start),
39043
- 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
+ }
39044
39295
  }
39045
39296
  });
39046
39297
  return;
@@ -39065,11 +39316,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39065
39316
  }
39066
39317
  spinner2.succeed(source_default.green("Text overlays applied"));
39067
39318
  if (isJsonMode()) {
39068
- outputResult({
39069
- success: true,
39070
- style: options.style,
39071
- texts: options.text,
39072
- 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
+ }
39073
39327
  });
39074
39328
  return;
39075
39329
  }
@@ -39086,6 +39340,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39086
39340
  }
39087
39341
  });
39088
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();
39089
39344
  try {
39090
39345
  if (options.output) {
39091
39346
  validateOutputPath(options.output);
@@ -39094,15 +39349,18 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39094
39349
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39095
39350
  }
39096
39351
  if (options.dryRun) {
39097
- outputResult({
39098
- dryRun: true,
39352
+ outputSuccess({
39099
39353
  command: "edit speed-ramp",
39100
- params: {
39101
- videoPath: resolve33(process.cwd(), videoPath),
39102
- style: options.style,
39103
- minSpeed: parseFloat(options.minSpeed),
39104
- maxSpeed: parseFloat(options.maxSpeed),
39105
- 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
+ }
39106
39364
  }
39107
39365
  });
39108
39366
  return;
@@ -39155,11 +39413,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39155
39413
  if (isJsonMode()) {
39156
39414
  const avgSpeed2 = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
39157
39415
  const speedRampOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
39158
- outputResult({
39159
- success: true,
39160
- keyframes: speedResult.keyframes,
39161
- avgSpeed: avgSpeed2,
39162
- 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
+ }
39163
39424
  });
39164
39425
  return;
39165
39426
  }
@@ -39199,6 +39460,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39199
39460
  }
39200
39461
  });
39201
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();
39202
39464
  try {
39203
39465
  if (options.output) {
39204
39466
  validateOutputPath(options.output);
@@ -39207,14 +39469,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39207
39469
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
39208
39470
  }
39209
39471
  if (options.dryRun) {
39210
- outputResult({
39211
- dryRun: true,
39472
+ outputSuccess({
39212
39473
  command: "edit reframe",
39213
- params: {
39214
- videoPath: resolve33(process.cwd(), videoPath),
39215
- aspect: options.aspect,
39216
- focus: options.focus,
39217
- 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
+ }
39218
39483
  }
39219
39484
  });
39220
39485
  return;
@@ -39282,13 +39547,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39282
39547
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
39283
39548
  if (isJsonMode()) {
39284
39549
  const reframeOutputPath = options.output ? resolve33(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
39285
- outputResult({
39286
- success: true,
39287
- sourceWidth,
39288
- sourceHeight,
39289
- aspect: options.aspect,
39290
- cropKeyframes,
39291
- 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
+ }
39292
39560
  });
39293
39561
  return;
39294
39562
  }
@@ -39339,6 +39607,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39339
39607
  }
39340
39608
  });
39341
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();
39342
39611
  try {
39343
39612
  if (args.length < 2) {
39344
39613
  exitWithError(usageError("Need at least one image and a prompt"));
@@ -39354,16 +39623,19 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39354
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."));
39355
39624
  }
39356
39625
  if (options.dryRun) {
39357
- outputResult({
39358
- dryRun: true,
39626
+ outputSuccess({
39359
39627
  command: "edit image",
39360
- params: {
39361
- imagePaths: imagePaths.map((p) => resolve33(process.cwd(), p)),
39362
- prompt: prompt3,
39363
- provider,
39364
- model: options.model,
39365
- ratio: options.ratio,
39366
- 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
+ }
39367
39639
  }
39368
39640
  });
39369
39641
  return;
@@ -39441,11 +39713,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39441
39713
  };
39442
39714
  const resultModel = result.model;
39443
39715
  if (isJsonMode()) {
39444
- outputResult({
39445
- success: true,
39446
- provider,
39447
- model: resultModel || options.model,
39448
- outputPath
39716
+ outputSuccess({
39717
+ command: "edit image",
39718
+ startedAt,
39719
+ data: {
39720
+ provider,
39721
+ model: resultModel || options.model,
39722
+ outputPath
39723
+ }
39449
39724
  });
39450
39725
  await saveImage();
39451
39726
  return;
@@ -39461,6 +39736,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39461
39736
  }
39462
39737
  });
39463
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();
39464
39740
  try {
39465
39741
  if (options.output) {
39466
39742
  validateOutputPath(options.output);
@@ -39471,14 +39747,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39471
39747
  exitWithError(usageError("Factor must be 2, 4, or 8"));
39472
39748
  }
39473
39749
  if (options.dryRun) {
39474
- outputResult({
39475
- dryRun: true,
39750
+ outputSuccess({
39476
39751
  command: "edit interpolate",
39477
- params: {
39478
- videoPath: absPath,
39479
- factor,
39480
- fps: options.fps ? parseInt(options.fps) : void 0,
39481
- 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
+ }
39482
39761
  }
39483
39762
  });
39484
39763
  return;
@@ -39505,12 +39784,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39505
39784
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
39506
39785
  spinner2.succeed(source_default.green(`Created ${factor}x slow motion`));
39507
39786
  if (isJsonMode()) {
39508
- outputResult({
39509
- success: true,
39510
- originalFps,
39511
- targetFps,
39512
- factor,
39513
- outputPath
39787
+ outputSuccess({
39788
+ command: "edit interpolate",
39789
+ startedAt,
39790
+ data: {
39791
+ originalFps,
39792
+ targetFps,
39793
+ factor,
39794
+ outputPath
39795
+ }
39514
39796
  });
39515
39797
  return;
39516
39798
  }
@@ -39536,6 +39818,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39536
39818
  }
39537
39819
  });
39538
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();
39539
39822
  try {
39540
39823
  if (options.output) {
39541
39824
  validateOutputPath(options.output);
@@ -39546,14 +39829,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39546
39829
  exitWithError(usageError("Scale must be 2 or 4"));
39547
39830
  }
39548
39831
  if (options.dryRun) {
39549
- outputResult({
39550
- dryRun: true,
39832
+ outputSuccess({
39551
39833
  command: "edit upscale-video",
39552
- params: {
39553
- videoPath: absPath,
39554
- scale,
39555
- model: options.model,
39556
- 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
+ }
39557
39843
  }
39558
39844
  });
39559
39845
  return;
@@ -39579,10 +39865,13 @@ Run 'vibe schema edit.<command>' for structured parameter info.
39579
39865
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `scale=${newWidth}:${newHeight}:flags=lanczos`, "-c:a", "copy", outputPath, "-y"]);
39580
39866
  spinner3.succeed(source_default.green(`Upscaled to ${newWidth}x${newHeight}`));
39581
39867
  if (isJsonMode()) {
39582
- outputResult({
39583
- success: true,
39584
- dimensions: `${newWidth}x${newHeight}`,
39585
- outputPath
39868
+ outputSuccess({
39869
+ command: "edit upscale-video",
39870
+ startedAt,
39871
+ data: {
39872
+ dimensions: `${newWidth}x${newHeight}`,
39873
+ outputPath
39874
+ }
39586
39875
  });
39587
39876
  return;
39588
39877
  }
@@ -40022,21 +40311,25 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
40022
40311
  }
40023
40312
  function registerReviewCommand(aiCommand) {
40024
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();
40025
40315
  try {
40026
40316
  if (options.output) {
40027
40317
  validateOutputPath(options.output);
40028
40318
  }
40029
40319
  if (options.dryRun) {
40030
- outputResult({
40031
- dryRun: true,
40320
+ outputSuccess({
40032
40321
  command: "ai review",
40033
- params: {
40034
- videoPath,
40035
- storyboard: options.storyboard,
40036
- autoApply: options.autoApply ?? false,
40037
- verify: options.verify ?? false,
40038
- model: options.model,
40039
- 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
+ }
40040
40333
  }
40041
40334
  });
40042
40335
  return;
@@ -40570,6 +40863,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
40570
40863
  }
40571
40864
  function registerHighlightsCommands(aiCommand) {
40572
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();
40573
40867
  try {
40574
40868
  if (options.output) {
40575
40869
  validateOutputPath(options.output);
@@ -40579,20 +40873,23 @@ function registerHighlightsCommands(aiCommand) {
40579
40873
  exitWithError(notFoundError(absPath));
40580
40874
  }
40581
40875
  if (options.dryRun) {
40582
- outputResult({
40583
- dryRun: true,
40876
+ outputSuccess({
40584
40877
  command: "ai highlights",
40585
- params: {
40586
- mediaPath,
40587
- output: options.output,
40588
- project: options.project,
40589
- duration: options.duration,
40590
- count: options.count,
40591
- threshold: options.threshold,
40592
- criteria: options.criteria,
40593
- language: options.language,
40594
- useGemini: options.useGemini ?? false,
40595
- 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
+ }
40596
40893
  }
40597
40894
  });
40598
40895
  return;
@@ -40785,13 +41082,13 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40785
41082
  });
40786
41083
  if (allHighlights.length === 0) {
40787
41084
  analyzeSpinner.warn(source_default.yellow("No highlights detected in the content"));
40788
- process.exit(0);
41085
+ return;
40789
41086
  }
40790
41087
  analyzeSpinner.succeed(source_default.green(`Found ${allHighlights.length} potential highlights`));
40791
41088
  }
40792
41089
  if (allHighlights.length === 0) {
40793
41090
  console.log(source_default.yellow("No highlights detected in the content"));
40794
- process.exit(0);
41091
+ return;
40795
41092
  }
40796
41093
  const filterSpinner = ora("\u{1F4CA} Filtering and ranking...").start();
40797
41094
  const threshold = parseFloat(options.threshold);
@@ -40867,6 +41164,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40867
41164
  }
40868
41165
  });
40869
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();
40870
41168
  try {
40871
41169
  if (options.output) {
40872
41170
  validateOutputPath(options.output);
@@ -40876,22 +41174,25 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
40876
41174
  exitWithError(notFoundError(absPath));
40877
41175
  }
40878
41176
  if (options.dryRun) {
40879
- outputResult({
40880
- dryRun: true,
41177
+ outputSuccess({
40881
41178
  command: "ai auto-shorts",
40882
- params: {
40883
- videoPath,
40884
- output: options.output,
40885
- duration: options.duration,
40886
- count: options.count,
40887
- aspect: options.aspect,
40888
- outputDir: options.outputDir,
40889
- addCaptions: options.addCaptions ?? false,
40890
- captionStyle: options.captionStyle,
40891
- analyzeOnly: options.analyzeOnly ?? false,
40892
- language: options.language,
40893
- useGemini: options.useGemini ?? false,
40894
- 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
+ }
40895
41196
  }
40896
41197
  });
40897
41198
  return;
@@ -51779,13 +52080,13 @@ function getPresetSettings(preset, aspectRatio) {
51779
52080
  }
51780
52081
  return settings;
51781
52082
  }
51782
- async function runHyperframesExport(projectPath, options, spinner2) {
52083
+ async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
51783
52084
  spinner2.text = "Loading project...";
51784
52085
  const { readFile: readFile42 } = await import("node:fs/promises");
51785
52086
  const { resolve: resolve62, basename: basename22 } = await import("node:path");
51786
52087
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
51787
52088
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
51788
- 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));
51789
52090
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
51790
52091
  const filePath = resolve62(process.cwd(), projectPath);
51791
52092
  const content = await readFile42(filePath, "utf-8");
@@ -51811,10 +52112,10 @@ async function runHyperframesExport(projectPath, options, spinner2) {
51811
52112
  return;
51812
52113
  }
51813
52114
  spinner2.succeed(chalk2.green(`Exported: ${result.outputPath}`));
51814
- outputResult2({
51815
- success: true,
52115
+ outputSuccess2({
51816
52116
  command: "export",
51817
- result: {
52117
+ startedAt,
52118
+ data: {
51818
52119
  outputPath: result.outputPath,
51819
52120
  backend: "hyperframes",
51820
52121
  durationMs: result.durationMs,
@@ -51858,6 +52159,7 @@ Cost: Free (no API keys needed). Requires FFmpeg.
51858
52159
  GIF format: 15fps, no audio, looping. Good for previews and sharing.
51859
52160
  Custom flags (--bitrate, --fps, --resolution, --codec) override preset values.
51860
52161
  Run 'vibe schema export' for structured parameter info.`).action(async (projectPath, options) => {
52162
+ const startedAt = Date.now();
51861
52163
  const spinner2 = ora("Checking FFmpeg...").start();
51862
52164
  try {
51863
52165
  if (options.output) {
@@ -51875,27 +52177,30 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
51875
52177
  exitWithError(usageError(overrideError));
51876
52178
  }
51877
52179
  if (options.dryRun) {
51878
- outputResult({
51879
- dryRun: true,
52180
+ outputSuccess({
51880
52181
  command: "export",
51881
- params: {
51882
- project: projectPath,
51883
- output: options.output || null,
51884
- format: options.format,
51885
- preset: options.preset,
51886
- overwrite: options.overwrite,
51887
- gapFill: options.gapFill,
51888
- backend: options.backend,
51889
- bitrate: options.bitrate ?? null,
51890
- fps: customOverrides.fps ?? null,
51891
- resolution: options.resolution ?? null,
51892
- 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
+ }
51893
52198
  }
51894
52199
  });
51895
52200
  return;
51896
52201
  }
51897
52202
  if (options.backend === "hyperframes") {
51898
- await runHyperframesExport(projectPath, options, spinner2);
52203
+ await runHyperframesExport(projectPath, options, spinner2, startedAt);
51899
52204
  return;
51900
52205
  }
51901
52206
  const ffmpegPath = await findFFmpeg();
@@ -52145,20 +52450,24 @@ var init_detect = __esm({
52145
52450
  init_validate();
52146
52451
  detectCommand = new Command("detect").description("Auto-detect scenes, beats, and silences in media");
52147
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();
52148
52454
  const spinner2 = ora("Detecting scenes...").start();
52149
52455
  try {
52150
52456
  if (options.output) {
52151
52457
  validateOutputPath(options.output);
52152
52458
  }
52153
52459
  if (options.dryRun) {
52154
- outputResult({
52155
- dryRun: true,
52460
+ outputSuccess({
52156
52461
  command: "detect scenes",
52157
- params: {
52158
- video: videoPath,
52159
- threshold: options.threshold,
52160
- output: options.output || null,
52161
- 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
+ }
52162
52471
  }
52163
52472
  });
52164
52473
  return;
@@ -52260,20 +52569,24 @@ var init_detect = __esm({
52260
52569
  }
52261
52570
  });
52262
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();
52263
52573
  const spinner2 = ora("Detecting silence...").start();
52264
52574
  try {
52265
52575
  if (options.output) {
52266
52576
  validateOutputPath(options.output);
52267
52577
  }
52268
52578
  if (options.dryRun) {
52269
- outputResult({
52270
- dryRun: true,
52579
+ outputSuccess({
52271
52580
  command: "detect silence",
52272
- params: {
52273
- media: mediaPath,
52274
- noise: options.noise,
52275
- duration: options.duration,
52276
- 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
+ }
52277
52590
  }
52278
52591
  });
52279
52592
  return;
@@ -52342,18 +52655,22 @@ var init_detect = __esm({
52342
52655
  }
52343
52656
  });
52344
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();
52345
52659
  const spinner2 = ora("Detecting beats...").start();
52346
52660
  try {
52347
52661
  if (options.output) {
52348
52662
  validateOutputPath(options.output);
52349
52663
  }
52350
52664
  if (options.dryRun) {
52351
- outputResult({
52352
- dryRun: true,
52665
+ outputSuccess({
52353
52666
  command: "detect beats",
52354
- params: {
52355
- audio: audioPath,
52356
- output: options.output || null
52667
+ startedAt,
52668
+ dryRun: true,
52669
+ data: {
52670
+ params: {
52671
+ audio: audioPath,
52672
+ output: options.output || null
52673
+ }
52357
52674
  }
52358
52675
  });
52359
52676
  return;
@@ -61989,7 +62306,7 @@ init_source();
61989
62306
  // package.json
61990
62307
  var package_default = {
61991
62308
  name: "@vibeframe/cli",
61992
- version: "0.71.0",
62309
+ version: "0.72.0",
61993
62310
  description: "VibeFrame CLI - natural language video editing from the terminal",
61994
62311
  private: false,
61995
62312
  license: "MIT",
@@ -62221,20 +62538,24 @@ Examples:
62221
62538
  Cost: Free (no API keys needed). Projects are saved as .vibe.json files.
62222
62539
  Run 'vibe schema project.<command>' for structured parameter info.`);
62223
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();
62224
62542
  const spinner2 = ora("Creating project...").start();
62225
62543
  try {
62226
62544
  if (options.output) {
62227
62545
  validateOutputPath(options.output);
62228
62546
  }
62229
62547
  if (options.dryRun) {
62230
- outputResult({
62231
- dryRun: true,
62548
+ outputSuccess({
62232
62549
  command: "project create",
62233
- params: {
62234
- name,
62235
- output: options.output || null,
62236
- aspectRatio: options.ratio,
62237
- 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
+ }
62238
62559
  }
62239
62560
  });
62240
62561
  return;
@@ -62299,17 +62620,21 @@ projectCommand.command("info").description("Show project information").argument(
62299
62620
  }
62300
62621
  });
62301
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();
62302
62624
  const spinner2 = ora("Updating project...").start();
62303
62625
  try {
62304
62626
  if (options.dryRun) {
62305
- outputResult({
62306
- dryRun: true,
62627
+ outputSuccess({
62307
62628
  command: "project set",
62308
- params: {
62309
- file,
62310
- name: options.name || null,
62311
- ratio: options.ratio || null,
62312
- 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
+ }
62313
62638
  }
62314
62639
  });
62315
62640
  return;
@@ -62360,18 +62685,22 @@ Typical workflow: create project \u2192 add-source \u2192 add-clip \u2192 trim/s
62360
62685
  Cost: Free (no API keys needed).
62361
62686
  Run 'vibe schema timeline.<command>' for structured parameter info.`);
62362
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();
62363
62689
  const spinner2 = ora("Adding source...").start();
62364
62690
  try {
62365
62691
  if (options.dryRun) {
62366
- outputResult({
62367
- dryRun: true,
62692
+ outputSuccess({
62368
62693
  command: "timeline add-source",
62369
- params: {
62370
- project: projectPath,
62371
- media: mediaPath,
62372
- name: options.name || null,
62373
- type: options.type || null,
62374
- 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
+ }
62375
62704
  }
62376
62705
  });
62377
62706
  return;
@@ -62404,21 +62733,25 @@ timelineCommand.command("add-source").description("Add a media source to the pro
62404
62733
  }
62405
62734
  });
62406
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();
62407
62737
  const spinner2 = ora("Adding clip...").start();
62408
62738
  try {
62409
62739
  validateResourceId(sourceId);
62410
62740
  if (options.track) validateResourceId(options.track);
62411
62741
  if (options.dryRun) {
62412
- outputResult({
62413
- dryRun: true,
62742
+ outputSuccess({
62414
62743
  command: "timeline add-clip",
62415
- params: {
62416
- project: projectPath,
62417
- sourceId,
62418
- track: options.track || null,
62419
- start: options.start,
62420
- duration: options.duration || null,
62421
- 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
+ }
62422
62755
  }
62423
62756
  });
62424
62757
  return;
@@ -62467,16 +62800,20 @@ timelineCommand.command("add-clip").description("Add a clip to the timeline").ar
62467
62800
  }
62468
62801
  });
62469
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();
62470
62804
  const spinner2 = ora("Adding track...").start();
62471
62805
  try {
62472
62806
  if (options.dryRun) {
62473
- outputResult({
62474
- dryRun: true,
62807
+ outputSuccess({
62475
62808
  command: "timeline add-track",
62476
- params: {
62477
- project: projectPath,
62478
- type,
62479
- name: options.name || null
62809
+ startedAt,
62810
+ dryRun: true,
62811
+ data: {
62812
+ params: {
62813
+ project: projectPath,
62814
+ type,
62815
+ name: options.name || null
62816
+ }
62480
62817
  }
62481
62818
  });
62482
62819
  return;
@@ -62508,20 +62845,24 @@ timelineCommand.command("add-track").description("Add a new track").argument("<p
62508
62845
  }
62509
62846
  });
62510
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();
62511
62849
  const spinner2 = ora("Adding effect...").start();
62512
62850
  try {
62513
62851
  validateResourceId(clipId);
62514
62852
  if (options.dryRun) {
62515
- outputResult({
62516
- dryRun: true,
62853
+ outputSuccess({
62517
62854
  command: "timeline add-effect",
62518
- params: {
62519
- project: projectPath,
62520
- clipId,
62521
- effectType,
62522
- start: options.start,
62523
- duration: options.duration || null,
62524
- 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
+ }
62525
62866
  }
62526
62867
  });
62527
62868
  return;
@@ -62561,18 +62902,22 @@ timelineCommand.command("add-effect").description("Add an effect to a clip").arg
62561
62902
  }
62562
62903
  });
62563
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();
62564
62906
  const spinner2 = ora("Trimming clip...").start();
62565
62907
  try {
62566
62908
  validateResourceId(clipId);
62567
62909
  if (options.dryRun) {
62568
- outputResult({
62569
- dryRun: true,
62910
+ outputSuccess({
62570
62911
  command: "timeline trim",
62571
- params: {
62572
- project: projectPath,
62573
- clipId,
62574
- start: options.start || null,
62575
- 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
+ }
62576
62921
  }
62577
62922
  });
62578
62923
  return;
@@ -62665,17 +63010,21 @@ timelineCommand.command("list").description("List timeline contents").argument("
62665
63010
  }
62666
63011
  });
62667
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();
62668
63014
  const spinner2 = ora("Splitting clip...").start();
62669
63015
  try {
62670
63016
  validateResourceId(clipId);
62671
63017
  if (options.dryRun) {
62672
- outputResult({
62673
- dryRun: true,
63018
+ outputSuccess({
62674
63019
  command: "timeline split",
62675
- params: {
62676
- project: projectPath,
62677
- clipId,
62678
- time: options.time
63020
+ startedAt,
63021
+ dryRun: true,
63022
+ data: {
63023
+ params: {
63024
+ project: projectPath,
63025
+ clipId,
63026
+ time: options.time
63027
+ }
62679
63028
  }
62680
63029
  });
62681
63030
  return;
@@ -62712,17 +63061,21 @@ timelineCommand.command("split").description("Split a clip at a specific time").
62712
63061
  }
62713
63062
  });
62714
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();
62715
63065
  const spinner2 = ora("Duplicating clip...").start();
62716
63066
  try {
62717
63067
  validateResourceId(clipId);
62718
63068
  if (options.dryRun) {
62719
- outputResult({
62720
- dryRun: true,
63069
+ outputSuccess({
62721
63070
  command: "timeline duplicate",
62722
- params: {
62723
- project: projectPath,
62724
- clipId,
62725
- time: options.time || null
63071
+ startedAt,
63072
+ dryRun: true,
63073
+ data: {
63074
+ params: {
63075
+ project: projectPath,
63076
+ clipId,
63077
+ time: options.time || null
63078
+ }
62726
63079
  }
62727
63080
  });
62728
63081
  return;
@@ -62754,16 +63107,20 @@ timelineCommand.command("duplicate").description("Duplicate a clip").argument("<
62754
63107
  }
62755
63108
  });
62756
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();
62757
63111
  const spinner2 = ora("Deleting clip...").start();
62758
63112
  try {
62759
63113
  validateResourceId(clipId);
62760
63114
  if (options.dryRun) {
62761
- outputResult({
62762
- dryRun: true,
63115
+ outputSuccess({
62763
63116
  command: "timeline delete",
62764
- params: {
62765
- project: projectPath,
62766
- clipId
63117
+ startedAt,
63118
+ dryRun: true,
63119
+ data: {
63120
+ params: {
63121
+ project: projectPath,
63122
+ clipId
63123
+ }
62767
63124
  }
62768
63125
  });
62769
63126
  return;
@@ -62791,19 +63148,23 @@ timelineCommand.command("delete").description("Delete a clip from the timeline")
62791
63148
  }
62792
63149
  });
62793
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();
62794
63152
  const spinner2 = ora("Moving clip...").start();
62795
63153
  try {
62796
63154
  validateResourceId(clipId);
62797
63155
  if (options.track) validateResourceId(options.track);
62798
63156
  if (options.dryRun) {
62799
- outputResult({
62800
- dryRun: true,
63157
+ outputSuccess({
62801
63158
  command: "timeline move",
62802
- params: {
62803
- project: projectPath,
62804
- clipId,
62805
- time: options.time || null,
62806
- 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
+ }
62807
63168
  }
62808
63169
  });
62809
63170
  return;
@@ -62886,6 +63247,7 @@ Run 'vibe schema analyze.<command>' for structured parameter info.
62886
63247
  `
62887
63248
  );
62888
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();
62889
63251
  try {
62890
63252
  rejectControlChars(prompt3);
62891
63253
  if (options.apiKey) {
@@ -62910,15 +63272,17 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
62910
63272
  spinner2.succeed(source_default.green("Analysis complete"));
62911
63273
  const response = sanitizeLLMResponse(result.response || "");
62912
63274
  if (isJsonMode()) {
62913
- let result_obj = { success: true, response, sourceType: result.sourceType, model: result.model };
63275
+ const data = { response, sourceType: result.sourceType, model: result.model };
62914
63276
  if (result.totalTokens) {
62915
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
62916
- }
62917
- if (options.fields) {
62918
- const fields = options.fields.split(",").map((f) => f.trim());
62919
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
62920
- }
62921
- 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
+ });
62922
63286
  return;
62923
63287
  }
62924
63288
  console.log();
@@ -62941,6 +63305,7 @@ analyzeCommand.command("media").description("Analyze any media: images, videos,
62941
63305
  }
62942
63306
  });
62943
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();
62944
63309
  try {
62945
63310
  rejectControlChars(prompt3);
62946
63311
  if (options.apiKey) {
@@ -62965,15 +63330,17 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
62965
63330
  spinner2.succeed(source_default.green("Video analyzed"));
62966
63331
  const response = sanitizeLLMResponse(result.response || "");
62967
63332
  if (isJsonMode()) {
62968
- let result_obj = { success: true, response, model: result.model };
63333
+ const data = { response, model: result.model };
62969
63334
  if (result.totalTokens) {
62970
- result_obj = { ...result_obj, promptTokens: result.promptTokens, responseTokens: result.responseTokens, totalTokens: result.totalTokens };
62971
- }
62972
- if (options.fields) {
62973
- const fields = options.fields.split(",").map((f) => f.trim());
62974
- result_obj = Object.fromEntries(Object.entries(result_obj).filter(([k]) => fields.includes(k) || k === "success"));
62975
- }
62976
- 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
+ });
62977
63344
  return;
62978
63345
  }
62979
63346
  console.log();
@@ -62996,6 +63363,7 @@ analyzeCommand.command("video").description("Analyze video using Gemini (summari
62996
63363
  });
62997
63364
  registerReviewCommand(analyzeCommand);
62998
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();
62999
63367
  try {
63000
63368
  rejectControlChars(instruction);
63001
63369
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
@@ -63011,7 +63379,13 @@ analyzeCommand.command("suggest").description("Get AI edit suggestions using Gem
63011
63379
  const suggestions = await gemini.autoEdit(clips, instruction);
63012
63380
  spinner2.succeed(source_default.green(`Found ${suggestions.length} suggestion(s)`));
63013
63381
  if (isJsonMode()) {
63014
- 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
+ });
63015
63389
  return;
63016
63390
  }
63017
63391
  console.log();
@@ -63080,6 +63454,7 @@ Run 'vibe schema audio.<command>' for structured parameter info.
63080
63454
  `
63081
63455
  );
63082
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();
63083
63458
  try {
63084
63459
  if (options.output) {
63085
63460
  validateOutputPath(options.output);
@@ -63100,7 +63475,16 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
63100
63475
  }
63101
63476
  spinner2.succeed(source_default.green("Transcription complete"));
63102
63477
  if (isJsonMode()) {
63103
- 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
+ });
63104
63488
  return;
63105
63489
  }
63106
63490
  console.log();
@@ -63130,6 +63514,7 @@ audioCommand.command("transcribe").description("Transcribe audio using Whisper")
63130
63514
  }
63131
63515
  });
63132
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();
63133
63518
  try {
63134
63519
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
63135
63520
  const spinner2 = ora("Fetching voices...").start();
@@ -63138,7 +63523,13 @@ audioCommand.command("voices").description("List available ElevenLabs voices").o
63138
63523
  const voices = await elevenlabs.getVoices();
63139
63524
  spinner2.succeed(source_default.green(`Found ${voices.length} voices`));
63140
63525
  if (isJsonMode()) {
63141
- 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
+ });
63142
63533
  return;
63143
63534
  }
63144
63535
  console.log();
@@ -63156,9 +63547,15 @@ audioCommand.command("voices").description("List available ElevenLabs voices").o
63156
63547
  }
63157
63548
  });
63158
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();
63159
63551
  try {
63160
63552
  if (options.dryRun) {
63161
- outputResult({ dryRun: true, command: "audio isolate", audioPath });
63553
+ outputSuccess({
63554
+ command: "audio isolate",
63555
+ startedAt,
63556
+ dryRun: true,
63557
+ data: { params: { audioPath } }
63558
+ });
63162
63559
  return;
63163
63560
  }
63164
63561
  if (options.output) {
@@ -63180,7 +63577,11 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
63180
63577
  await writeFile32(outputPath, result.audioBuffer);
63181
63578
  spinner2.succeed(source_default.green("Vocals isolated"));
63182
63579
  if (isJsonMode()) {
63183
- outputResult({ success: true, outputPath });
63580
+ outputSuccess({
63581
+ command: "audio isolate",
63582
+ startedAt,
63583
+ data: { outputPath }
63584
+ });
63184
63585
  return;
63185
63586
  }
63186
63587
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -63191,9 +63592,15 @@ audioCommand.command("isolate").description("Isolate vocals from audio using Ele
63191
63592
  }
63192
63593
  });
63193
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();
63194
63596
  try {
63195
63597
  if (options.dryRun) {
63196
- 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
+ });
63197
63604
  return;
63198
63605
  }
63199
63606
  const apiKey = await requireApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
@@ -63249,7 +63656,11 @@ audioCommand.command("voice-clone").description("Clone a voice from audio sample
63249
63656
  }
63250
63657
  spinner2.succeed(source_default.green("Voice cloned successfully"));
63251
63658
  if (isJsonMode()) {
63252
- 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
+ });
63253
63664
  return;
63254
63665
  }
63255
63666
  console.log();
@@ -63267,9 +63678,15 @@ audioCommand.command("voice-clone").description("Clone a voice from audio sample
63267
63678
  }
63268
63679
  });
63269
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();
63270
63682
  try {
63271
63683
  if (options.dryRun) {
63272
- 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
+ });
63273
63690
  return;
63274
63691
  }
63275
63692
  if (options.output) {
@@ -63429,7 +63846,16 @@ ${segmentTexts}`,
63429
63846
  await writeFile32(finalOutputPath, combinedBuffer);
63430
63847
  spinner2.succeed(source_default.green("Dubbing complete"));
63431
63848
  if (isJsonMode()) {
63432
- 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
+ });
63433
63859
  return;
63434
63860
  }
63435
63861
  console.log();
@@ -63448,13 +63874,19 @@ ${segmentTexts}`,
63448
63874
  }
63449
63875
  });
63450
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();
63451
63878
  try {
63452
63879
  if (options.dryRun) {
63453
63880
  const threshold = parseFloat(options.threshold);
63454
63881
  const ratio2 = parseFloat(options.ratio);
63455
63882
  const attack2 = parseFloat(options.attack);
63456
63883
  const release2 = parseFloat(options.release);
63457
- 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
+ });
63458
63890
  return;
63459
63891
  }
63460
63892
  if (options.output) {
@@ -63479,7 +63911,11 @@ audioCommand.command("duck").description("Auto-duck background music when voice
63479
63911
  await execSafe("ffmpeg", ["-i", absMusic, "-i", absVoice, "-filter_complex", filterComplex, "-map", "[out]", outputPath, "-y"]);
63480
63912
  spinner2.succeed(source_default.green("Audio ducking complete"));
63481
63913
  if (isJsonMode()) {
63482
- 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
+ });
63483
63919
  return;
63484
63920
  }
63485
63921
  console.log();
@@ -63517,6 +63953,7 @@ import { resolve as resolve38 } from "node:path";
63517
63953
  import { existsSync as existsSync29 } from "node:fs";
63518
63954
  function registerScriptPipelineCommands(aiCommand) {
63519
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();
63520
63957
  try {
63521
63958
  const outputDir = resolve38(process.cwd(), projectDir);
63522
63959
  const projectPath = resolve38(outputDir, "project.vibe.json");
@@ -63535,20 +63972,23 @@ function registerScriptPipelineCommands(aiCommand) {
63535
63972
  exitWithError(usageError("Scene number must be a positive integer (1-based)", "e.g., --scene 3 or --scene 3,4,5"));
63536
63973
  }
63537
63974
  if (options.dryRun) {
63538
- outputResult({
63539
- dryRun: true,
63975
+ outputSuccess({
63540
63976
  command: "pipeline regenerate-scene",
63541
- params: {
63542
- projectDir,
63543
- scene: sceneNums,
63544
- videoOnly: options.videoOnly ?? false,
63545
- narrationOnly: options.narrationOnly ?? false,
63546
- imageOnly: options.imageOnly ?? false,
63547
- generator: options.generator,
63548
- imageProvider: options.imageProvider,
63549
- aspectRatio: options.aspectRatio,
63550
- retries: options.retries,
63551
- 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
+ }
63552
63992
  }
63553
63993
  });
63554
63994
  return;
@@ -63743,6 +64183,7 @@ Styles:
63743
64183
  Required API Key: OPENAI_API_KEY (Whisper transcription)
63744
64184
  `
63745
64185
  ).action(async (videoPath, options) => {
64186
+ const startedAt = Date.now();
63746
64187
  try {
63747
64188
  const absVideoPath = resolve41(process.cwd(), videoPath);
63748
64189
  if (!existsSync32(absVideoPath)) {
@@ -63753,20 +64194,23 @@ Required API Key: OPENAI_API_KEY (Whisper transcription)
63753
64194
  }
63754
64195
  const outputFile = options.output || videoPath.replace(/(\.\w+)$/, "-captioned$1");
63755
64196
  if (options.dryRun) {
63756
- outputResult({
63757
- dryRun: true,
64197
+ outputSuccess({
63758
64198
  command: "pipeline animated-caption",
63759
- params: {
63760
- videoPath: absVideoPath,
63761
- outputPath: outputFile,
63762
- style: options.style,
63763
- highlightColor: options.highlightColor,
63764
- fontSize: options.fontSize ? parseInt(options.fontSize) : "auto",
63765
- position: options.position,
63766
- wordsPerGroup: options.wordsPerGroup ? parseInt(options.wordsPerGroup) : "auto",
63767
- maxChars: options.maxChars ? parseInt(options.maxChars) : "auto",
63768
- language: options.language || "auto",
63769
- 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
+ }
63770
64214
  }
63771
64215
  });
63772
64216
  return;
@@ -63797,13 +64241,16 @@ Required API Key: OPENAI_API_KEY (Whisper transcription)
63797
64241
  }
63798
64242
  spinner2.succeed(source_default.green("Animated captions applied successfully"));
63799
64243
  if (isJsonMode()) {
63800
- outputResult({
63801
- success: true,
63802
- outputPath: result.outputPath,
63803
- wordCount: result.wordCount,
63804
- groupCount: result.groupCount,
63805
- style: result.style,
63806
- 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
+ }
63807
64254
  });
63808
64255
  return;
63809
64256
  }
@@ -68071,18 +68518,22 @@ function isMediaFile(filePath) {
68071
68518
  return mediaExts.includes(ext);
68072
68519
  }
68073
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();
68074
68522
  const spinner2 = ora("Scanning directory...").start();
68075
68523
  try {
68076
68524
  if (options.dryRun) {
68077
- outputResult({
68078
- dryRun: true,
68525
+ outputSuccess({
68079
68526
  command: "batch import",
68080
- params: {
68081
- project: projectPath,
68082
- directory,
68083
- recursive: options.recursive,
68084
- duration: options.duration,
68085
- 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
+ }
68086
68537
  }
68087
68538
  });
68088
68539
  return;
@@ -68144,19 +68595,23 @@ batchCommand.command("import").description("Import multiple media files from a d
68144
68595
  }
68145
68596
  });
68146
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();
68147
68599
  const spinner2 = ora("Creating clips...").start();
68148
68600
  try {
68149
68601
  if (options.dryRun) {
68150
- outputResult({
68151
- dryRun: true,
68602
+ outputSuccess({
68152
68603
  command: "batch concat",
68153
- params: {
68154
- project: projectPath,
68155
- sourceIds,
68156
- all: options.all,
68157
- track: options.track || null,
68158
- start: options.start,
68159
- 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
+ }
68160
68615
  }
68161
68616
  });
68162
68617
  return;
@@ -68218,20 +68673,24 @@ batchCommand.command("concat").description("Concatenate multiple sources into se
68218
68673
  });
68219
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(
68220
68675
  async (projectPath, effectType, clipIds, options) => {
68676
+ const startedAt = Date.now();
68221
68677
  const spinner2 = ora("Applying effects...").start();
68222
68678
  try {
68223
68679
  if (options.dryRun) {
68224
- outputResult({
68225
- dryRun: true,
68680
+ outputSuccess({
68226
68681
  command: "batch apply-effect",
68227
- params: {
68228
- project: projectPath,
68229
- effectType,
68230
- clipIds,
68231
- all: options.all,
68232
- duration: options.duration,
68233
- start: options.start,
68234
- 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
+ }
68235
68694
  }
68236
68695
  });
68237
68696
  return;
@@ -68285,17 +68744,21 @@ batchCommand.command("apply-effect").description("Apply an effect to multiple cl
68285
68744
  }
68286
68745
  );
68287
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();
68288
68748
  const spinner2 = ora("Removing clips...").start();
68289
68749
  try {
68290
68750
  if (options.dryRun) {
68291
- outputResult({
68292
- dryRun: true,
68751
+ outputSuccess({
68293
68752
  command: "batch remove-clips",
68294
- params: {
68295
- project: projectPath,
68296
- clipIds,
68297
- all: options.all,
68298
- 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
+ }
68299
68762
  }
68300
68763
  });
68301
68764
  return;
@@ -68819,6 +69282,27 @@ subcommands for video generation, editing, analysis, and pipeline
68819
69282
  orchestration. Cost tiers below \u2014 **always confirm with the user before
68820
69283
  running High / Very High tier commands**.
68821
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
+
68822
69306
  ## Self-discovery
68823
69307
 
68824
69308
  Don't memorise the command surface \u2014 read it from the CLI:
@@ -68828,6 +69312,8 @@ vibe --help # command groups overview
68828
69312
  vibe schema --list # full machine-readable catalog (80+)
68829
69313
  vibe schema generate.video # JSON Schema for any single command
68830
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)
68831
69317
  \`\`\`
68832
69318
 
68833
69319
  ## Two flows \u2014 pick by intent
@@ -68957,7 +69443,7 @@ var CLAUDE_MD = `@AGENTS.md
68957
69443
 
68958
69444
  This file imports \`AGENTS.md\` (above) \u2014 that's the canonical source.
68959
69445
  Add anything Claude-Code-specific *below*; everything generic should
68960
- 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
68961
69447
  too.
68962
69448
 
68963
69449
  ## Skills
@@ -68980,6 +69466,20 @@ curl -fsSL https://raw.githubusercontent.com/vericontext/vibeframe/main/scripts/
68980
69466
  <!-- Add Claude-specific notes here: which sub-agents to prefer, custom
68981
69467
  slash commands you've added, conversational style preferences, etc. -->
68982
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
+ `;
68983
69483
  function renderEnvExample(opts = {}) {
68984
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
68985
69485
  # Kokoro TTS (local, ~330MB on first call) \u2192 works without ELEVENLABS_API_KEY
@@ -69040,6 +69540,7 @@ defaults:
69040
69540
  init_output();
69041
69541
  var VALID_AGENTS = ["claude-code", "codex", "cursor", "aider", "gemini-cli", "opencode", "all", "auto"];
69042
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();
69043
69544
  const agent = options.agent;
69044
69545
  if (!VALID_AGENTS.includes(agent)) {
69045
69546
  exitWithError(usageError(`Invalid --agent: ${agent}`, `Must be one of: ${VALID_AGENTS.join(", ")}`));
@@ -69048,6 +69549,7 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
69048
69549
  const projectName = basename19(projectDir);
69049
69550
  const targetHosts = resolveTargets(agent);
69050
69551
  const wantsClaude = targetHosts.includes("claude-code");
69552
+ const wantsGemini = targetHosts.includes("gemini-cli");
69051
69553
  const wantsCrossTool = targetHosts.some((h) => h !== "claude-code");
69052
69554
  const actions = [];
69053
69555
  if (!existsSync34(projectDir)) {
@@ -69073,6 +69575,14 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
69073
69575
  options.dryRun
69074
69576
  ));
69075
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
+ }
69076
69586
  actions.push(await writeIfMissing(
69077
69587
  resolve48(projectDir, ".env.example"),
69078
69588
  renderEnvExample(),
@@ -69091,13 +69601,16 @@ var initCommand = new Command("init").description("Scaffold project-scope agent
69091
69601
  options.dryRun
69092
69602
  ));
69093
69603
  if (isJsonMode()) {
69094
- outputResult({
69604
+ outputSuccess({
69095
69605
  command: "init",
69096
- projectDir,
69097
- agent,
69098
- targetHosts,
69099
- actions,
69100
- dryRun: options.dryRun ?? false
69606
+ startedAt,
69607
+ ...options.dryRun ? { dryRun: true } : {},
69608
+ data: {
69609
+ projectDir,
69610
+ agent,
69611
+ targetHosts,
69612
+ actions
69613
+ }
69101
69614
  });
69102
69615
  return;
69103
69616
  }
@@ -69221,14 +69734,15 @@ Examples:
69221
69734
  $ vibe doctor --json Machine-readable output
69222
69735
  `
69223
69736
  ).action(async (options) => {
69737
+ const startedAt = Date.now();
69224
69738
  if (options.json) process.env.VIBE_JSON_OUTPUT = "1";
69225
69739
  const isJson = process.env.VIBE_JSON_OUTPUT === "1";
69226
69740
  const results = await runDiagnostics();
69227
69741
  if (isJson) {
69228
- outputResult({
69229
- success: true,
69742
+ outputSuccess({
69230
69743
  command: "doctor",
69231
- result: results
69744
+ startedAt,
69745
+ data: { ...results }
69232
69746
  });
69233
69747
  return;
69234
69748
  }
@@ -69600,6 +70114,7 @@ async function generateTestVideo(outputPath) {
69600
70114
  }
69601
70115
  }
69602
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();
69603
70118
  if (options.json) process.env.VIBE_JSON_OUTPUT = "1";
69604
70119
  const isJson = isJsonMode() || !process.stdin.isTTY;
69605
70120
  if (!commandExists("ffmpeg")) {
@@ -69699,11 +70214,16 @@ var demoCommand = new Command("demo").description("Run sample edits on a test vi
69699
70214
  const passed = results.filter((r) => r.success).length;
69700
70215
  const total = results.length;
69701
70216
  if (isJson) {
69702
- outputResult({
69703
- success: passed === total,
70217
+ outputSuccess({
69704
70218
  command: "demo",
69705
- result: { passed, total, steps: results },
69706
- demoDir: DEMO_DIR
70219
+ startedAt,
70220
+ data: {
70221
+ allPassed: passed === total,
70222
+ passed,
70223
+ total,
70224
+ steps: results,
70225
+ demoDir: DEMO_DIR
70226
+ }
69707
70227
  });
69708
70228
  } else {
69709
70229
  console.log();
@@ -69816,6 +70336,7 @@ Variable references: $step_id.output, $step_id.data.field, \${ENV_VAR}
69816
70336
  Cost: Depends on steps. Use --dry-run to preview before executing.
69817
70337
  Run 'vibe schema run' for structured parameter info.
69818
70338
  `).action(async (pipelinePath, options) => {
70339
+ const startedAt = Date.now();
69819
70340
  loadEnv();
69820
70341
  const isJson = options.json || process.env.VIBE_JSON_OUTPUT === "1";
69821
70342
  const cliBudget = {};
@@ -69848,13 +70369,16 @@ Run 'vibe schema run' for structured parameter info.
69848
70369
  budget: hasBudgetFlag ? cliBudget : void 0
69849
70370
  });
69850
70371
  if (isJson) {
69851
- outputResult({
69852
- dryRun: true,
70372
+ outputSuccess({
69853
70373
  command: "run",
69854
- name: manifest2.name,
69855
- steps: result2.steps.map((s) => ({ id: s.id, action: s.action, estimatedCost: s.data?.estimatedCost })),
69856
- totalSteps: result2.totalSteps,
69857
- 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
+ }
69858
70382
  });
69859
70383
  } else {
69860
70384
  console.log(source_default.bold(" Execution Plan:"));
@@ -69886,24 +70410,27 @@ Run 'vibe schema run' for structured parameter info.
69886
70410
  });
69887
70411
  spinner2?.stop();
69888
70412
  if (isJson) {
69889
- outputResult({
69890
- success: result.success,
69891
- name: result.name,
69892
- error: result.error,
69893
- completedSteps: result.completedSteps,
69894
- totalSteps: result.totalSteps,
69895
- totalDuration: result.totalDuration,
69896
- outputDir: result.outputDir,
69897
- budget: result.budget,
69898
- steps: result.steps.map((s) => ({
69899
- id: s.id,
69900
- action: s.action,
69901
- success: s.success,
69902
- output: s.output,
69903
- duration: s.duration,
69904
- error: s.error,
69905
- data: s.data
69906
- }))
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
+ }
69907
70434
  });
69908
70435
  } else {
69909
70436
  console.log();
@@ -70694,13 +71221,17 @@ init_source();
70694
71221
  init_walkthroughs();
70695
71222
  init_output();
70696
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();
70697
71225
  if (!topicArg || options.list) {
70698
71226
  const topics = listWalkthroughs();
70699
71227
  if (isJsonMode()) {
70700
- outputResult({
71228
+ outputSuccess({
70701
71229
  command: "walkthrough",
70702
- action: "list",
70703
- topics
71230
+ startedAt,
71231
+ data: {
71232
+ action: "list",
71233
+ topics
71234
+ }
70704
71235
  });
70705
71236
  return;
70706
71237
  }
@@ -70725,10 +71256,13 @@ var walkthroughCommand = new Command("walkthrough").description("Step-by-step au
70725
71256
  const topic = topicArg;
70726
71257
  const result = loadWalkthrough(topic);
70727
71258
  if (isJsonMode()) {
70728
- outputResult({
71259
+ outputSuccess({
70729
71260
  command: "walkthrough",
70730
- action: "show",
70731
- ...result
71261
+ startedAt,
71262
+ data: {
71263
+ action: "show",
71264
+ ...result
71265
+ }
70732
71266
  });
70733
71267
  return;
70734
71268
  }