@vibeframe/mcp-server 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 (3) hide show
  1. package/README.md +37 -9
  2. package/dist/index.js +818 -509
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -449232,6 +449232,7 @@ __export(output_exports, {
449232
449232
  notFoundError: () => notFoundError,
449233
449233
  outputError: () => outputError,
449234
449234
  outputResult: () => outputResult,
449235
+ outputSuccess: () => outputSuccess,
449235
449236
  spinner: () => spinner,
449236
449237
  suggestNext: () => suggestNext,
449237
449238
  usageError: () => usageError
@@ -449290,6 +449291,40 @@ function formatCost(min, max, unit) {
449290
449291
  if (min === max) return `~$${min.toFixed(2)} ${unit}`;
449291
449292
  return `~$${min.toFixed(2)}-$${max.toFixed(2)} ${unit}`;
449292
449293
  }
449294
+ function lookupCostEstimateUpperBound(command3) {
449295
+ return COST_ESTIMATES[command3]?.max ?? 0;
449296
+ }
449297
+ function outputSuccess(opts) {
449298
+ const elapsedMs = Math.max(0, Date.now() - opts.startedAt);
449299
+ const costUsd = opts.costUsd ?? (opts.dryRun ? lookupCostEstimateUpperBound(opts.command) : 0);
449300
+ const envelope = {
449301
+ command: opts.command,
449302
+ ...opts.dryRun ? { dryRun: true } : {},
449303
+ elapsedMs,
449304
+ costUsd,
449305
+ warnings: opts.warnings ?? [],
449306
+ data: opts.data
449307
+ };
449308
+ if (isJsonMode()) {
449309
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
449310
+ if (fields) {
449311
+ const keys2 = fields.split(",").map((k) => k.trim());
449312
+ const data = opts.data;
449313
+ const filteredData = {};
449314
+ for (const key2 of keys2) {
449315
+ if (key2 in data) filteredData[key2] = data[key2];
449316
+ }
449317
+ envelope.data = filteredData;
449318
+ }
449319
+ console.log(JSON.stringify(envelope, null, 2));
449320
+ return;
449321
+ }
449322
+ if (isQuietMode()) {
449323
+ const data = opts.data;
449324
+ const primary = data.outputPath ?? data.output ?? data.path ?? data.url ?? data.id;
449325
+ if (primary !== void 0) console.log(String(primary));
449326
+ }
449327
+ }
449293
449328
  function outputResult(result) {
449294
449329
  if (result.dryRun && result.command && typeof result.command === "string") {
449295
449330
  const cost = COST_ESTIMATES[result.command];
@@ -451881,6 +451916,7 @@ Examples:
451881
451916
  $ vibe ed sc video.mp4 --dry-run --json
451882
451917
 
451883
451918
  No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires GOOGLE_API_KEY).`).action(async (videoPath, options) => {
451919
+ const startedAt = Date.now();
451884
451920
  try {
451885
451921
  if (options.output) {
451886
451922
  validateOutputPath(options.output);
@@ -451897,16 +451933,19 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
451897
451933
  const outputPath = options.output || `${name}-cut${ext}`;
451898
451934
  const useGemini = options.useGemini || false;
451899
451935
  if (options.dryRun) {
451900
- outputResult({
451901
- dryRun: true,
451936
+ outputSuccess({
451902
451937
  command: "edit silence-cut",
451903
- params: {
451904
- videoPath: absVideoPath,
451905
- noiseThreshold: parseFloat(options.noise),
451906
- minDuration: parseFloat(options.minDuration),
451907
- padding: parseFloat(options.padding),
451908
- useGemini,
451909
- analyzeOnly: options.analyzeOnly || false
451938
+ startedAt,
451939
+ dryRun: true,
451940
+ data: {
451941
+ params: {
451942
+ videoPath: absVideoPath,
451943
+ noiseThreshold: parseFloat(options.noise),
451944
+ minDuration: parseFloat(options.minDuration),
451945
+ padding: parseFloat(options.padding),
451946
+ useGemini,
451947
+ analyzeOnly: options.analyzeOnly || false
451948
+ }
451910
451949
  }
451911
451950
  });
451912
451951
  return;
@@ -451931,13 +451970,16 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
451931
451970
  }
451932
451971
  spinner2.succeed(source_default.green("Silence detection complete"));
451933
451972
  if (isJsonMode()) {
451934
- outputResult({
451935
- success: true,
451936
- method: result.method,
451937
- totalDuration: result.totalDuration,
451938
- silentPeriods: result.silentPeriods,
451939
- silentDuration: result.silentDuration,
451940
- outputPath: result.outputPath
451973
+ outputSuccess({
451974
+ command: "edit silence-cut",
451975
+ startedAt,
451976
+ data: {
451977
+ method: result.method,
451978
+ totalDuration: result.totalDuration,
451979
+ silentPeriods: result.silentPeriods,
451980
+ silentDuration: result.silentDuration,
451981
+ outputPath: result.outputPath
451982
+ }
451941
451983
  });
451942
451984
  return;
451943
451985
  }
@@ -451975,6 +452017,7 @@ Examples:
451975
452017
  $ vibe ed cap video.mp4 --dry-run --json
451976
452018
 
451977
452019
  Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoPath, options) => {
452020
+ const startedAt = Date.now();
451978
452021
  try {
451979
452022
  if (options.output) {
451980
452023
  validateOutputPath(options.output);
@@ -451987,16 +452030,19 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
451987
452030
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
451988
452031
  }
451989
452032
  if (options.dryRun) {
451990
- outputResult({
451991
- dryRun: true,
452033
+ outputSuccess({
451992
452034
  command: "edit caption",
451993
- params: {
451994
- videoPath: absVideoPath,
451995
- style: options.style,
451996
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
451997
- fontColor: options.color,
451998
- language: options.language,
451999
- position: options.position
452035
+ startedAt,
452036
+ dryRun: true,
452037
+ data: {
452038
+ params: {
452039
+ videoPath: absVideoPath,
452040
+ style: options.style,
452041
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
452042
+ fontColor: options.color,
452043
+ language: options.language,
452044
+ position: options.position
452045
+ }
452000
452046
  }
452001
452047
  });
452002
452048
  return;
@@ -452025,12 +452071,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452025
452071
  }
452026
452072
  spinner2.succeed(source_default.green("Captions applied"));
452027
452073
  if (isJsonMode()) {
452028
- outputResult({
452029
- success: true,
452030
- segmentCount: result.segmentCount,
452031
- style: options.style || "bold",
452032
- outputPath: result.outputPath,
452033
- srtPath: result.srtPath
452074
+ outputSuccess({
452075
+ command: "edit caption",
452076
+ startedAt,
452077
+ data: {
452078
+ segmentCount: result.segmentCount,
452079
+ style: options.style || "bold",
452080
+ outputPath: result.outputPath,
452081
+ srtPath: result.srtPath
452082
+ }
452034
452083
  });
452035
452084
  return;
452036
452085
  }
@@ -452049,6 +452098,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452049
452098
  }
452050
452099
  });
452051
452100
  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) => {
452101
+ const startedAt = Date.now();
452052
452102
  try {
452053
452103
  if (options.output) {
452054
452104
  validateOutputPath(options.output);
@@ -452061,13 +452111,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452061
452111
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
452062
452112
  }
452063
452113
  if (options.dryRun) {
452064
- outputResult({
452065
- dryRun: true,
452114
+ outputSuccess({
452066
452115
  command: "edit noise-reduce",
452067
- params: {
452068
- inputPath: absInputPath,
452069
- strength: options.strength,
452070
- noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
452116
+ startedAt,
452117
+ dryRun: true,
452118
+ data: {
452119
+ params: {
452120
+ inputPath: absInputPath,
452121
+ strength: options.strength,
452122
+ noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
452123
+ }
452071
452124
  }
452072
452125
  });
452073
452126
  return;
@@ -452088,11 +452141,14 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452088
452141
  }
452089
452142
  spinner2.succeed(source_default.green("Noise reduction complete"));
452090
452143
  if (isJsonMode()) {
452091
- outputResult({
452092
- success: true,
452093
- inputDuration: result.inputDuration,
452094
- strength: options.strength || "medium",
452095
- outputPath: result.outputPath
452144
+ outputSuccess({
452145
+ command: "edit noise-reduce",
452146
+ startedAt,
452147
+ data: {
452148
+ inputDuration: result.inputDuration,
452149
+ strength: options.strength || "medium",
452150
+ outputPath: result.outputPath
452151
+ }
452096
452152
  });
452097
452153
  return;
452098
452154
  }
@@ -452108,6 +452164,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452108
452164
  }
452109
452165
  });
452110
452166
  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) => {
452167
+ const startedAt = Date.now();
452111
452168
  try {
452112
452169
  if (options.output) {
452113
452170
  validateOutputPath(options.output);
@@ -452120,15 +452177,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452120
452177
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
452121
452178
  }
452122
452179
  if (options.dryRun) {
452123
- outputResult({
452124
- dryRun: true,
452180
+ outputSuccess({
452125
452181
  command: "edit fade",
452126
- params: {
452127
- videoPath: absVideoPath,
452128
- fadeIn: parseFloat(options.fadeIn),
452129
- fadeOut: parseFloat(options.fadeOut),
452130
- audioOnly: options.audioOnly || false,
452131
- videoOnly: options.videoOnly || false
452182
+ startedAt,
452183
+ dryRun: true,
452184
+ data: {
452185
+ params: {
452186
+ videoPath: absVideoPath,
452187
+ fadeIn: parseFloat(options.fadeIn),
452188
+ fadeOut: parseFloat(options.fadeOut),
452189
+ audioOnly: options.audioOnly || false,
452190
+ videoOnly: options.videoOnly || false
452191
+ }
452132
452192
  }
452133
452193
  });
452134
452194
  return;
@@ -452151,12 +452211,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452151
452211
  }
452152
452212
  spinner2.succeed(source_default.green("Fade effects applied"));
452153
452213
  if (isJsonMode()) {
452154
- outputResult({
452155
- success: true,
452156
- totalDuration: result.totalDuration,
452157
- fadeInApplied: result.fadeInApplied,
452158
- fadeOutApplied: result.fadeOutApplied,
452159
- outputPath: result.outputPath
452214
+ outputSuccess({
452215
+ command: "edit fade",
452216
+ startedAt,
452217
+ data: {
452218
+ totalDuration: result.totalDuration,
452219
+ fadeInApplied: result.fadeInApplied,
452220
+ fadeOutApplied: result.fadeOutApplied,
452221
+ outputPath: result.outputPath
452222
+ }
452160
452223
  });
452161
452224
  return;
452162
452225
  }
@@ -452173,6 +452236,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452173
452236
  }
452174
452237
  });
452175
452238
  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) => {
452239
+ const startedAt = Date.now();
452176
452240
  try {
452177
452241
  if (options.output) {
452178
452242
  validateOutputPath(options.output);
@@ -452185,14 +452249,17 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452185
452249
  exitWithError(notFoundError(absSrtPath));
452186
452250
  }
452187
452251
  if (options.dryRun) {
452188
- outputResult({
452189
- dryRun: true,
452252
+ outputSuccess({
452190
452253
  command: "edit translate-srt",
452191
- params: {
452192
- srtPath: absSrtPath,
452193
- targetLanguage: options.target,
452194
- provider: options.provider || "claude",
452195
- sourceLanguage: options.source
452254
+ startedAt,
452255
+ dryRun: true,
452256
+ data: {
452257
+ params: {
452258
+ srtPath: absSrtPath,
452259
+ targetLanguage: options.target,
452260
+ provider: options.provider || "claude",
452261
+ sourceLanguage: options.source
452262
+ }
452196
452263
  }
452197
452264
  });
452198
452265
  return;
@@ -452222,12 +452289,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452222
452289
  }
452223
452290
  spinner2.succeed(source_default.green("Translation complete"));
452224
452291
  if (isJsonMode()) {
452225
- outputResult({
452226
- success: true,
452227
- segmentCount: result.segmentCount,
452228
- sourceLanguage: result.sourceLanguage,
452229
- targetLanguage: result.targetLanguage,
452230
- outputPath: result.outputPath
452292
+ outputSuccess({
452293
+ command: "edit translate-srt",
452294
+ startedAt,
452295
+ data: {
452296
+ segmentCount: result.segmentCount,
452297
+ sourceLanguage: result.sourceLanguage,
452298
+ targetLanguage: result.targetLanguage,
452299
+ outputPath: result.outputPath
452300
+ }
452231
452301
  });
452232
452302
  return;
452233
452303
  }
@@ -452244,6 +452314,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452244
452314
  }
452245
452315
  });
452246
452316
  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) => {
452317
+ const startedAt = Date.now();
452247
452318
  try {
452248
452319
  if (options.output) {
452249
452320
  validateOutputPath(options.output);
@@ -452258,15 +452329,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452258
452329
  }
452259
452330
  if (options.dryRun) {
452260
452331
  const fillers2 = options.fillers ? options.fillers.split(",").map((f) => f.trim()) : void 0;
452261
- outputResult({
452262
- dryRun: true,
452332
+ outputSuccess({
452263
452333
  command: "edit jump-cut",
452264
- params: {
452265
- videoPath: absVideoPath,
452266
- fillers: fillers2,
452267
- padding: parseFloat(options.padding),
452268
- language: options.language,
452269
- analyzeOnly: options.analyzeOnly || false
452334
+ startedAt,
452335
+ dryRun: true,
452336
+ data: {
452337
+ params: {
452338
+ videoPath: absVideoPath,
452339
+ fillers: fillers2,
452340
+ padding: parseFloat(options.padding),
452341
+ language: options.language,
452342
+ analyzeOnly: options.analyzeOnly || false
452343
+ }
452270
452344
  }
452271
452345
  });
452272
452346
  return;
@@ -452295,13 +452369,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452295
452369
  }
452296
452370
  spinner2.succeed(source_default.green("Filler detection complete"));
452297
452371
  if (isJsonMode()) {
452298
- outputResult({
452299
- success: true,
452300
- totalDuration: result.totalDuration,
452301
- fillerCount: result.fillerCount,
452302
- fillerDuration: result.fillerDuration,
452303
- fillers: result.fillers,
452304
- outputPath: result.outputPath
452372
+ outputSuccess({
452373
+ command: "edit jump-cut",
452374
+ startedAt,
452375
+ data: {
452376
+ totalDuration: result.totalDuration,
452377
+ fillerCount: result.fillerCount,
452378
+ fillerDuration: result.fillerDuration,
452379
+ fillers: result.fillers,
452380
+ outputPath: result.outputPath
452381
+ }
452305
452382
  });
452306
452383
  return;
452307
452384
  }
@@ -453446,6 +453523,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453446
453523
  registerEditCommands(editCommand);
453447
453524
  registerFillGapsCommand(editCommand);
453448
453525
  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) => {
453526
+ const startedAt = Date.now();
453449
453527
  try {
453450
453528
  if (options.style) rejectControlChars(options.style);
453451
453529
  if (options.output) {
@@ -453461,13 +453539,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453461
453539
  exitWithError(notFoundError("FFmpeg not found. Install with: brew install ffmpeg"));
453462
453540
  }
453463
453541
  if (options.dryRun) {
453464
- outputResult({
453465
- dryRun: true,
453542
+ outputSuccess({
453466
453543
  command: "edit grade",
453467
- params: {
453468
- videoPath: resolve29(process.cwd(), videoPath),
453469
- style: options.style || options.preset,
453470
- analyzeOnly: options.analyzeOnly || false
453544
+ startedAt,
453545
+ dryRun: true,
453546
+ data: {
453547
+ params: {
453548
+ videoPath: resolve29(process.cwd(), videoPath),
453549
+ style: options.style || options.preset,
453550
+ analyzeOnly: options.analyzeOnly || false
453551
+ }
453471
453552
  }
453472
453553
  });
453473
453554
  return;
@@ -453493,12 +453574,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453493
453574
  if (isJsonMode()) {
453494
453575
  const absPath2 = resolve29(process.cwd(), videoPath);
453495
453576
  const gradeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
453496
- outputResult({
453497
- success: true,
453498
- style: options.preset || options.style,
453499
- description: gradeResult.description,
453500
- ffmpegFilter: gradeResult.ffmpegFilter,
453501
- outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
453577
+ outputSuccess({
453578
+ command: "edit grade",
453579
+ startedAt,
453580
+ data: {
453581
+ style: options.preset || options.style,
453582
+ description: gradeResult.description,
453583
+ ffmpegFilter: gradeResult.ffmpegFilter,
453584
+ outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
453585
+ }
453502
453586
  });
453503
453587
  return;
453504
453588
  }
@@ -453527,6 +453611,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453527
453611
  }
453528
453612
  });
453529
453613
  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) => {
453614
+ const startedAt = Date.now();
453530
453615
  try {
453531
453616
  if (!options.text || options.text.length === 0) {
453532
453617
  exitWithError(usageError("At least one --text option is required", 'Example: vibe edit text-overlay video.mp4 -t "NEXUS AI" --style center-bold'));
@@ -453539,18 +453624,21 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453539
453624
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453540
453625
  }
453541
453626
  if (options.dryRun) {
453542
- outputResult({
453543
- dryRun: true,
453627
+ outputSuccess({
453544
453628
  command: "edit text-overlay",
453545
- params: {
453546
- videoPath: resolve29(process.cwd(), videoPath),
453547
- texts: options.text,
453548
- style: options.style,
453549
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
453550
- fontColor: options.fontColor,
453551
- fade: parseFloat(options.fade),
453552
- start: parseFloat(options.start),
453553
- end: options.end ? parseFloat(options.end) : void 0
453629
+ startedAt,
453630
+ dryRun: true,
453631
+ data: {
453632
+ params: {
453633
+ videoPath: resolve29(process.cwd(), videoPath),
453634
+ texts: options.text,
453635
+ style: options.style,
453636
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
453637
+ fontColor: options.fontColor,
453638
+ fade: parseFloat(options.fade),
453639
+ start: parseFloat(options.start),
453640
+ end: options.end ? parseFloat(options.end) : void 0
453641
+ }
453554
453642
  }
453555
453643
  });
453556
453644
  return;
@@ -453575,11 +453663,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453575
453663
  }
453576
453664
  spinner2.succeed(source_default.green("Text overlays applied"));
453577
453665
  if (isJsonMode()) {
453578
- outputResult({
453579
- success: true,
453580
- style: options.style,
453581
- texts: options.text,
453582
- outputPath: result.outputPath
453666
+ outputSuccess({
453667
+ command: "edit text-overlay",
453668
+ startedAt,
453669
+ data: {
453670
+ style: options.style,
453671
+ texts: options.text,
453672
+ outputPath: result.outputPath
453673
+ }
453583
453674
  });
453584
453675
  return;
453585
453676
  }
@@ -453596,6 +453687,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453596
453687
  }
453597
453688
  });
453598
453689
  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) => {
453690
+ const startedAt = Date.now();
453599
453691
  try {
453600
453692
  if (options.output) {
453601
453693
  validateOutputPath(options.output);
@@ -453604,15 +453696,18 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453604
453696
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453605
453697
  }
453606
453698
  if (options.dryRun) {
453607
- outputResult({
453608
- dryRun: true,
453699
+ outputSuccess({
453609
453700
  command: "edit speed-ramp",
453610
- params: {
453611
- videoPath: resolve29(process.cwd(), videoPath),
453612
- style: options.style,
453613
- minSpeed: parseFloat(options.minSpeed),
453614
- maxSpeed: parseFloat(options.maxSpeed),
453615
- analyzeOnly: options.analyzeOnly || false
453701
+ startedAt,
453702
+ dryRun: true,
453703
+ data: {
453704
+ params: {
453705
+ videoPath: resolve29(process.cwd(), videoPath),
453706
+ style: options.style,
453707
+ minSpeed: parseFloat(options.minSpeed),
453708
+ maxSpeed: parseFloat(options.maxSpeed),
453709
+ analyzeOnly: options.analyzeOnly || false
453710
+ }
453616
453711
  }
453617
453712
  });
453618
453713
  return;
@@ -453665,11 +453760,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453665
453760
  if (isJsonMode()) {
453666
453761
  const avgSpeed2 = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
453667
453762
  const speedRampOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
453668
- outputResult({
453669
- success: true,
453670
- keyframes: speedResult.keyframes,
453671
- avgSpeed: avgSpeed2,
453672
- outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
453763
+ outputSuccess({
453764
+ command: "edit speed-ramp",
453765
+ startedAt,
453766
+ data: {
453767
+ keyframes: speedResult.keyframes,
453768
+ avgSpeed: avgSpeed2,
453769
+ outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
453770
+ }
453673
453771
  });
453674
453772
  return;
453675
453773
  }
@@ -453709,6 +453807,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453709
453807
  }
453710
453808
  });
453711
453809
  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) => {
453810
+ const startedAt = Date.now();
453712
453811
  try {
453713
453812
  if (options.output) {
453714
453813
  validateOutputPath(options.output);
@@ -453717,14 +453816,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453717
453816
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453718
453817
  }
453719
453818
  if (options.dryRun) {
453720
- outputResult({
453721
- dryRun: true,
453819
+ outputSuccess({
453722
453820
  command: "edit reframe",
453723
- params: {
453724
- videoPath: resolve29(process.cwd(), videoPath),
453725
- aspect: options.aspect,
453726
- focus: options.focus,
453727
- analyzeOnly: options.analyzeOnly || false
453821
+ startedAt,
453822
+ dryRun: true,
453823
+ data: {
453824
+ params: {
453825
+ videoPath: resolve29(process.cwd(), videoPath),
453826
+ aspect: options.aspect,
453827
+ focus: options.focus,
453828
+ analyzeOnly: options.analyzeOnly || false
453829
+ }
453728
453830
  }
453729
453831
  });
453730
453832
  return;
@@ -453792,13 +453894,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453792
453894
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
453793
453895
  if (isJsonMode()) {
453794
453896
  const reframeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
453795
- outputResult({
453796
- success: true,
453797
- sourceWidth,
453798
- sourceHeight,
453799
- aspect: options.aspect,
453800
- cropKeyframes,
453801
- outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
453897
+ outputSuccess({
453898
+ command: "edit reframe",
453899
+ startedAt,
453900
+ data: {
453901
+ sourceWidth,
453902
+ sourceHeight,
453903
+ aspect: options.aspect,
453904
+ cropKeyframes,
453905
+ outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
453906
+ }
453802
453907
  });
453803
453908
  return;
453804
453909
  }
@@ -453849,6 +453954,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453849
453954
  }
453850
453955
  });
453851
453956
  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) => {
453957
+ const startedAt = Date.now();
453852
453958
  try {
453853
453959
  if (args.length < 2) {
453854
453960
  exitWithError(usageError("Need at least one image and a prompt"));
@@ -453864,16 +453970,19 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453864
453970
  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."));
453865
453971
  }
453866
453972
  if (options.dryRun) {
453867
- outputResult({
453868
- dryRun: true,
453973
+ outputSuccess({
453869
453974
  command: "edit image",
453870
- params: {
453871
- imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
453872
- prompt: prompt3,
453873
- provider,
453874
- model: options.model,
453875
- ratio: options.ratio,
453876
- size: options.size
453975
+ startedAt,
453976
+ dryRun: true,
453977
+ data: {
453978
+ params: {
453979
+ imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
453980
+ prompt: prompt3,
453981
+ provider,
453982
+ model: options.model,
453983
+ ratio: options.ratio,
453984
+ size: options.size
453985
+ }
453877
453986
  }
453878
453987
  });
453879
453988
  return;
@@ -453951,11 +454060,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453951
454060
  };
453952
454061
  const resultModel = result.model;
453953
454062
  if (isJsonMode()) {
453954
- outputResult({
453955
- success: true,
453956
- provider,
453957
- model: resultModel || options.model,
453958
- outputPath
454063
+ outputSuccess({
454064
+ command: "edit image",
454065
+ startedAt,
454066
+ data: {
454067
+ provider,
454068
+ model: resultModel || options.model,
454069
+ outputPath
454070
+ }
453959
454071
  });
453960
454072
  await saveImage();
453961
454073
  return;
@@ -453971,6 +454083,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453971
454083
  }
453972
454084
  });
453973
454085
  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) => {
454086
+ const startedAt = Date.now();
453974
454087
  try {
453975
454088
  if (options.output) {
453976
454089
  validateOutputPath(options.output);
@@ -453981,14 +454094,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453981
454094
  exitWithError(usageError("Factor must be 2, 4, or 8"));
453982
454095
  }
453983
454096
  if (options.dryRun) {
453984
- outputResult({
453985
- dryRun: true,
454097
+ outputSuccess({
453986
454098
  command: "edit interpolate",
453987
- params: {
453988
- videoPath: absPath,
453989
- factor,
453990
- fps: options.fps ? parseInt(options.fps) : void 0,
453991
- quality: options.quality
454099
+ startedAt,
454100
+ dryRun: true,
454101
+ data: {
454102
+ params: {
454103
+ videoPath: absPath,
454104
+ factor,
454105
+ fps: options.fps ? parseInt(options.fps) : void 0,
454106
+ quality: options.quality
454107
+ }
453992
454108
  }
453993
454109
  });
453994
454110
  return;
@@ -454015,12 +454131,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454015
454131
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
454016
454132
  spinner2.succeed(source_default.green(`Created ${factor}x slow motion`));
454017
454133
  if (isJsonMode()) {
454018
- outputResult({
454019
- success: true,
454020
- originalFps,
454021
- targetFps,
454022
- factor,
454023
- outputPath
454134
+ outputSuccess({
454135
+ command: "edit interpolate",
454136
+ startedAt,
454137
+ data: {
454138
+ originalFps,
454139
+ targetFps,
454140
+ factor,
454141
+ outputPath
454142
+ }
454024
454143
  });
454025
454144
  return;
454026
454145
  }
@@ -454046,6 +454165,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454046
454165
  }
454047
454166
  });
454048
454167
  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) => {
454168
+ const startedAt = Date.now();
454049
454169
  try {
454050
454170
  if (options.output) {
454051
454171
  validateOutputPath(options.output);
@@ -454056,14 +454176,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454056
454176
  exitWithError(usageError("Scale must be 2 or 4"));
454057
454177
  }
454058
454178
  if (options.dryRun) {
454059
- outputResult({
454060
- dryRun: true,
454179
+ outputSuccess({
454061
454180
  command: "edit upscale-video",
454062
- params: {
454063
- videoPath: absPath,
454064
- scale,
454065
- model: options.model,
454066
- ffmpeg: options.ffmpeg || false
454181
+ startedAt,
454182
+ dryRun: true,
454183
+ data: {
454184
+ params: {
454185
+ videoPath: absPath,
454186
+ scale,
454187
+ model: options.model,
454188
+ ffmpeg: options.ffmpeg || false
454189
+ }
454067
454190
  }
454068
454191
  });
454069
454192
  return;
@@ -454089,10 +454212,13 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454089
454212
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `scale=${newWidth}:${newHeight}:flags=lanczos`, "-c:a", "copy", outputPath, "-y"]);
454090
454213
  spinner3.succeed(source_default.green(`Upscaled to ${newWidth}x${newHeight}`));
454091
454214
  if (isJsonMode()) {
454092
- outputResult({
454093
- success: true,
454094
- dimensions: `${newWidth}x${newHeight}`,
454095
- outputPath
454215
+ outputSuccess({
454216
+ command: "edit upscale-video",
454217
+ startedAt,
454218
+ data: {
454219
+ dimensions: `${newWidth}x${newHeight}`,
454220
+ outputPath
454221
+ }
454096
454222
  });
454097
454223
  return;
454098
454224
  }
@@ -454797,21 +454923,25 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
454797
454923
  }
454798
454924
  function registerReviewCommand(aiCommand) {
454799
454925
  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) => {
454926
+ const startedAt = Date.now();
454800
454927
  try {
454801
454928
  if (options.output) {
454802
454929
  validateOutputPath(options.output);
454803
454930
  }
454804
454931
  if (options.dryRun) {
454805
- outputResult({
454806
- dryRun: true,
454932
+ outputSuccess({
454807
454933
  command: "ai review",
454808
- params: {
454809
- videoPath,
454810
- storyboard: options.storyboard,
454811
- autoApply: options.autoApply ?? false,
454812
- verify: options.verify ?? false,
454813
- model: options.model,
454814
- output: options.output
454934
+ startedAt,
454935
+ dryRun: true,
454936
+ data: {
454937
+ params: {
454938
+ videoPath,
454939
+ storyboard: options.storyboard,
454940
+ autoApply: options.autoApply ?? false,
454941
+ verify: options.verify ?? false,
454942
+ model: options.model,
454943
+ output: options.output
454944
+ }
454815
454945
  }
454816
454946
  });
454817
454947
  return;
@@ -455079,27 +455209,31 @@ Use this image analysis to inform the color palette, typography placement, and o
455079
455209
  }
455080
455210
  function registerMotionCommand(aiCommand) {
455081
455211
  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) => {
455212
+ const startedAt = Date.now();
455082
455213
  try {
455083
455214
  if (options.output) {
455084
455215
  validateOutputPath(options.output);
455085
455216
  }
455086
455217
  if (options.dryRun) {
455087
- outputResult({
455218
+ outputSuccess({
455219
+ command: "generate motion",
455220
+ startedAt,
455088
455221
  dryRun: true,
455089
- command: "ai motion",
455090
- params: {
455091
- description: description.slice(0, 200),
455092
- duration: options.duration,
455093
- width: options.width,
455094
- height: options.height,
455095
- fps: options.fps,
455096
- style: options.style,
455097
- render: options.render ?? false,
455098
- video: options.video,
455099
- image: options.image,
455100
- fromTsx: options.fromTsx,
455101
- model: options.model,
455102
- output: options.output
455222
+ data: {
455223
+ params: {
455224
+ description: description.slice(0, 200),
455225
+ duration: options.duration,
455226
+ width: options.width,
455227
+ height: options.height,
455228
+ fps: options.fps,
455229
+ style: options.style,
455230
+ render: options.render ?? false,
455231
+ video: options.video,
455232
+ image: options.image,
455233
+ fromTsx: options.fromTsx,
455234
+ model: options.model,
455235
+ output: options.output
455236
+ }
455103
455237
  }
455104
455238
  });
455105
455239
  return;
@@ -455213,20 +455347,24 @@ async function executeSoundEffect(options) {
455213
455347
  }
455214
455348
  function registerSoundEffectCommand(parent) {
455215
455349
  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) => {
455350
+ const startedAt = Date.now();
455216
455351
  try {
455217
455352
  rejectControlChars(prompt3);
455218
455353
  if (options.output) {
455219
455354
  validateOutputPath(options.output);
455220
455355
  }
455221
455356
  if (options.dryRun) {
455222
- outputResult({
455223
- dryRun: true,
455357
+ outputSuccess({
455224
455358
  command: "generate sound-effect",
455225
- params: {
455226
- prompt: prompt3,
455227
- duration: options.duration,
455228
- promptInfluence: options.promptInfluence,
455229
- output: options.output
455359
+ startedAt,
455360
+ dryRun: true,
455361
+ data: {
455362
+ params: {
455363
+ prompt: prompt3,
455364
+ duration: options.duration,
455365
+ promptInfluence: options.promptInfluence,
455366
+ output: options.output
455367
+ }
455230
455368
  }
455231
455369
  });
455232
455370
  return;
@@ -455253,7 +455391,11 @@ function registerSoundEffectCommand(parent) {
455253
455391
  await writeFile22(outputPath, result.audioBuffer);
455254
455392
  spinner2.succeed(source_default.green("Sound effect generated"));
455255
455393
  if (isJsonMode()) {
455256
- outputResult({ success: true, outputPath });
455394
+ outputSuccess({
455395
+ command: "generate sound-effect",
455396
+ startedAt,
455397
+ data: { outputPath }
455398
+ });
455257
455399
  return;
455258
455400
  }
455259
455401
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -455303,6 +455445,7 @@ async function executeMusicStatus(options) {
455303
455445
  }
455304
455446
  function registerMusicStatusCommand(parent) {
455305
455447
  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) => {
455448
+ const startedAt = Date.now();
455306
455449
  try {
455307
455450
  const apiKey = await requireApiKey(
455308
455451
  "REPLICATE_API_TOKEN",
@@ -455314,12 +455457,15 @@ function registerMusicStatusCommand(parent) {
455314
455457
  const result = await replicate.getMusicStatus(taskId);
455315
455458
  if (isJsonMode()) {
455316
455459
  const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
455317
- outputResult({
455318
- success: true,
455319
- taskId,
455320
- status,
455321
- audioUrl: result.audioUrl,
455322
- error: result.error
455460
+ outputSuccess({
455461
+ command: "generate music-status",
455462
+ startedAt,
455463
+ data: {
455464
+ taskId,
455465
+ status,
455466
+ audioUrl: result.audioUrl,
455467
+ error: result.error
455468
+ }
455323
455469
  });
455324
455470
  return;
455325
455471
  }
@@ -455357,6 +455503,7 @@ var init_music_status = __esm({
455357
455503
  // ../cli/src/commands/generate/video-cancel.ts
455358
455504
  function registerVideoCancelCommand(parent) {
455359
455505
  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) => {
455506
+ const startedAt = Date.now();
455360
455507
  try {
455361
455508
  const provider = (options.provider || "grok").toLowerCase();
455362
455509
  let success = false;
@@ -455369,7 +455516,11 @@ function registerVideoCancelCommand(parent) {
455369
455516
  if (success) {
455370
455517
  spinner2.succeed(source_default.green("Generation cancelled"));
455371
455518
  if (isJsonMode()) {
455372
- outputResult({ success: true, taskId, provider: "grok", cancelled: true });
455519
+ outputSuccess({
455520
+ command: "generate video-cancel",
455521
+ startedAt,
455522
+ data: { taskId, provider: "grok", cancelled: true }
455523
+ });
455373
455524
  return;
455374
455525
  }
455375
455526
  } else {
@@ -455389,7 +455540,11 @@ function registerVideoCancelCommand(parent) {
455389
455540
  if (success) {
455390
455541
  spinner2.succeed(source_default.green("Generation cancelled"));
455391
455542
  if (isJsonMode()) {
455392
- outputResult({ success: true, taskId, provider: "runway", cancelled: true });
455543
+ outputSuccess({
455544
+ command: "generate video-cancel",
455545
+ startedAt,
455546
+ data: { taskId, provider: "runway", cancelled: true }
455547
+ });
455393
455548
  return;
455394
455549
  }
455395
455550
  } else {
@@ -455462,16 +455617,18 @@ async function executeBackground(options) {
455462
455617
  }
455463
455618
  function registerBackgroundCommand(parent) {
455464
455619
  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) => {
455620
+ const startedAt = Date.now();
455465
455621
  try {
455466
455622
  rejectControlChars(description);
455467
455623
  if (options.output) {
455468
455624
  validateOutputPath(options.output);
455469
455625
  }
455470
455626
  if (options.dryRun) {
455471
- outputResult({
455472
- dryRun: true,
455627
+ outputSuccess({
455473
455628
  command: "generate background",
455474
- params: { description, aspect: options.aspect, output: options.output }
455629
+ startedAt,
455630
+ dryRun: true,
455631
+ data: { params: { description, aspect: options.aspect, output: options.output } }
455475
455632
  });
455476
455633
  return;
455477
455634
  }
@@ -455506,7 +455663,11 @@ function registerBackgroundCommand(parent) {
455506
455663
  await mkdir18(dirname25(outputPath), { recursive: true });
455507
455664
  await writeFile23(outputPath, buffer);
455508
455665
  }
455509
- outputResult({ success: true, imageUrl: img.url, outputPath });
455666
+ outputSuccess({
455667
+ command: "generate background",
455668
+ startedAt,
455669
+ data: { imageUrl: img.url, outputPath }
455670
+ });
455510
455671
  return;
455511
455672
  }
455512
455673
  console.log();
@@ -455603,6 +455764,7 @@ async function executeStoryboard(options) {
455603
455764
  }
455604
455765
  function registerStoryboardCommand(parent) {
455605
455766
  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) => {
455767
+ const startedAt = Date.now();
455606
455768
  try {
455607
455769
  rejectControlChars(content);
455608
455770
  if (options.output) {
@@ -455618,13 +455780,16 @@ function registerStoryboardCommand(parent) {
455618
455780
  textContent2 = await readFile23(filePath, "utf-8");
455619
455781
  }
455620
455782
  if (options.dryRun) {
455621
- outputResult({
455622
- dryRun: true,
455783
+ outputSuccess({
455623
455784
  command: "generate storyboard",
455624
- params: {
455625
- content: textContent2.substring(0, 200),
455626
- duration: options.duration,
455627
- creativity
455785
+ startedAt,
455786
+ dryRun: true,
455787
+ data: {
455788
+ params: {
455789
+ content: textContent2.substring(0, 200),
455790
+ duration: options.duration,
455791
+ creativity
455792
+ }
455628
455793
  }
455629
455794
  });
455630
455795
  return;
@@ -455656,16 +455821,23 @@ function registerStoryboardCommand(parent) {
455656
455821
  const outputPath = resolve39(process.cwd(), options.output);
455657
455822
  await writeFile24(outputPath, JSON.stringify(segments, null, 2), "utf-8");
455658
455823
  if (isJsonMode()) {
455659
- outputResult({
455660
- success: true,
455661
- segmentCount: segments.length,
455662
- segments,
455663
- outputPath
455824
+ outputSuccess({
455825
+ command: "generate storyboard",
455826
+ startedAt,
455827
+ data: {
455828
+ segmentCount: segments.length,
455829
+ segments,
455830
+ outputPath
455831
+ }
455664
455832
  });
455665
455833
  return;
455666
455834
  }
455667
455835
  } else if (isJsonMode()) {
455668
- outputResult({ success: true, segmentCount: segments.length, segments });
455836
+ outputSuccess({
455837
+ command: "generate storyboard",
455838
+ startedAt,
455839
+ data: { segmentCount: segments.length, segments }
455840
+ });
455669
455841
  return;
455670
455842
  }
455671
455843
  console.log();
@@ -455797,6 +455969,7 @@ async function executeSpeech(options) {
455797
455969
  }
455798
455970
  function registerSpeechCommand(parent) {
455799
455971
  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) => {
455972
+ const startedAt = Date.now();
455800
455973
  try {
455801
455974
  if (!text) {
455802
455975
  if (hasTTY()) {
@@ -455818,10 +455991,11 @@ function registerSpeechCommand(parent) {
455818
455991
  validateOutputPath(options.output);
455819
455992
  }
455820
455993
  if (options.dryRun) {
455821
- outputResult({
455822
- dryRun: true,
455994
+ outputSuccess({
455823
455995
  command: "generate speech",
455824
- params: { text, voice: options.voice, output: options.output }
455996
+ startedAt,
455997
+ dryRun: true,
455998
+ data: { params: { text, voice: options.voice, output: options.output } }
455825
455999
  });
455826
456000
  return;
455827
456001
  }
@@ -455910,10 +456084,13 @@ function registerSpeechCommand(parent) {
455910
456084
  }
455911
456085
  }
455912
456086
  if (isJsonMode()) {
455913
- outputResult({
455914
- success: true,
455915
- characterCount: result.characterCount,
455916
- outputPath
456087
+ outputSuccess({
456088
+ command: "generate speech",
456089
+ startedAt,
456090
+ data: {
456091
+ characterCount: result.characterCount,
456092
+ outputPath
456093
+ }
455917
456094
  });
455918
456095
  return;
455919
456096
  }
@@ -456002,6 +456179,7 @@ async function executeMusic(options) {
456002
456179
  }
456003
456180
  function registerMusicCommand(parent) {
456004
456181
  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) => {
456182
+ const startedAt = Date.now();
456005
456183
  try {
456006
456184
  rejectControlChars(prompt3);
456007
456185
  if (options.output) {
@@ -456009,16 +456187,19 @@ function registerMusicCommand(parent) {
456009
456187
  }
456010
456188
  const provider = (options.provider || "elevenlabs").toLowerCase();
456011
456189
  if (options.dryRun) {
456012
- outputResult({
456013
- dryRun: true,
456190
+ outputSuccess({
456014
456191
  command: "generate music",
456015
- params: {
456016
- prompt: prompt3,
456017
- provider,
456018
- duration: options.duration,
456019
- model: options.model,
456020
- output: options.output,
456021
- instrumental: options.instrumental
456192
+ startedAt,
456193
+ dryRun: true,
456194
+ data: {
456195
+ params: {
456196
+ prompt: prompt3,
456197
+ provider,
456198
+ duration: options.duration,
456199
+ model: options.model,
456200
+ output: options.output,
456201
+ instrumental: options.instrumental
456202
+ }
456022
456203
  }
456023
456204
  });
456024
456205
  return;
@@ -456045,11 +456226,14 @@ function registerMusicCommand(parent) {
456045
456226
  await writeFile26(outputPath, result.audioBuffer);
456046
456227
  spinner2.succeed(source_default.green("Music generated successfully"));
456047
456228
  if (isJsonMode()) {
456048
- outputResult({
456049
- success: true,
456050
- provider: "elevenlabs",
456051
- outputPath,
456052
- duration
456229
+ outputSuccess({
456230
+ command: "generate music",
456231
+ startedAt,
456232
+ data: {
456233
+ provider: "elevenlabs",
456234
+ outputPath,
456235
+ duration
456236
+ }
456053
456237
  });
456054
456238
  return;
456055
456239
  }
@@ -456117,12 +456301,15 @@ function registerMusicCommand(parent) {
456117
456301
  await writeFile26(outputPath, audioBuffer);
456118
456302
  spinner2.succeed(source_default.green("Music generated successfully"));
456119
456303
  if (isJsonMode()) {
456120
- outputResult({
456121
- success: true,
456122
- provider: "replicate",
456123
- taskId: result.taskId,
456124
- audioUrl: finalResult.audioUrl,
456125
- outputPath
456304
+ outputSuccess({
456305
+ command: "generate music",
456306
+ startedAt,
456307
+ data: {
456308
+ provider: "replicate",
456309
+ taskId: result.taskId,
456310
+ audioUrl: finalResult.audioUrl,
456311
+ outputPath
456312
+ }
456126
456313
  });
456127
456314
  return;
456128
456315
  }
@@ -456157,6 +456344,7 @@ import { existsSync as existsSync47 } from "node:fs";
456157
456344
  import { writeFile as writeFile27, mkdir as mkdir19 } from "node:fs/promises";
456158
456345
  function registerThumbnailCommand(parent) {
456159
456346
  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) => {
456347
+ const startedAt = Date.now();
456160
456348
  try {
456161
456349
  if (description) rejectControlChars(description);
456162
456350
  if (options.output) {
@@ -456192,11 +456380,14 @@ function registerThumbnailCommand(parent) {
456192
456380
  }
456193
456381
  spinner3.succeed(source_default.green("Best frame extracted"));
456194
456382
  if (isJsonMode()) {
456195
- outputResult({
456196
- success: true,
456197
- timestamp: result2.timestamp,
456198
- reason: result2.reason,
456199
- outputPath: result2.outputPath
456383
+ outputSuccess({
456384
+ command: "generate thumbnail",
456385
+ startedAt,
456386
+ data: {
456387
+ timestamp: result2.timestamp,
456388
+ reason: result2.reason,
456389
+ outputPath: result2.outputPath
456390
+ }
456200
456391
  });
456201
456392
  return;
456202
456393
  }
@@ -456244,7 +456435,11 @@ function registerThumbnailCommand(parent) {
456244
456435
  await mkdir19(dirname26(outputPath), { recursive: true });
456245
456436
  await writeFile27(outputPath, buffer);
456246
456437
  }
456247
- outputResult({ success: true, imageUrl: img.url, outputPath });
456438
+ outputSuccess({
456439
+ command: "generate thumbnail",
456440
+ startedAt,
456441
+ data: { imageUrl: img.url, outputPath }
456442
+ });
456248
456443
  return;
456249
456444
  }
456250
456445
  console.log();
@@ -456315,6 +456510,7 @@ function getStatusColor(status) {
456315
456510
  }
456316
456511
  function registerVideoStatusCommand(parent) {
456317
456512
  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) => {
456513
+ const startedAt = Date.now();
456318
456514
  try {
456319
456515
  const provider = (options.provider || "grok").toLowerCase();
456320
456516
  if (provider === "grok") {
@@ -456337,14 +456533,17 @@ function registerVideoStatusCommand(parent) {
456337
456533
  outputPath = resolve44(process.cwd(), options.output);
456338
456534
  await writeFile28(outputPath, buffer);
456339
456535
  }
456340
- outputResult({
456341
- success: true,
456342
- taskId,
456343
- provider: "grok",
456344
- status: result.status,
456345
- videoUrl: result.videoUrl,
456346
- error: result.error,
456347
- outputPath
456536
+ outputSuccess({
456537
+ command: "generate video-status",
456538
+ startedAt,
456539
+ data: {
456540
+ taskId,
456541
+ provider: "grok",
456542
+ status: result.status,
456543
+ videoUrl: result.videoUrl,
456544
+ error: result.error,
456545
+ outputPath
456546
+ }
456348
456547
  });
456349
456548
  return;
456350
456549
  }
@@ -456402,15 +456601,18 @@ function registerVideoStatusCommand(parent) {
456402
456601
  outputPath = resolve44(process.cwd(), options.output);
456403
456602
  await writeFile28(outputPath, buffer);
456404
456603
  }
456405
- outputResult({
456406
- success: true,
456407
- taskId,
456408
- provider: "runway",
456409
- status: result.status,
456410
- videoUrl: result.videoUrl,
456411
- progress: result.progress,
456412
- error: result.error,
456413
- outputPath
456604
+ outputSuccess({
456605
+ command: "generate video-status",
456606
+ startedAt,
456607
+ data: {
456608
+ taskId,
456609
+ provider: "runway",
456610
+ status: result.status,
456611
+ videoUrl: result.videoUrl,
456612
+ progress: result.progress,
456613
+ error: result.error,
456614
+ outputPath
456615
+ }
456414
456616
  });
456415
456617
  return;
456416
456618
  }
@@ -456466,15 +456668,18 @@ function registerVideoStatusCommand(parent) {
456466
456668
  outputPath = resolve44(process.cwd(), options.output);
456467
456669
  await writeFile28(outputPath, buffer);
456468
456670
  }
456469
- outputResult({
456470
- success: true,
456471
- taskId,
456472
- provider: "kling",
456473
- status: result.status,
456474
- videoUrl: result.videoUrl,
456475
- duration: result.duration,
456476
- error: result.error,
456477
- outputPath
456671
+ outputSuccess({
456672
+ command: "generate video-status",
456673
+ startedAt,
456674
+ data: {
456675
+ taskId,
456676
+ provider: "kling",
456677
+ status: result.status,
456678
+ videoUrl: result.videoUrl,
456679
+ duration: result.duration,
456680
+ error: result.error,
456681
+ outputPath
456682
+ }
456478
456683
  });
456479
456684
  return;
456480
456685
  }
@@ -456538,22 +456743,26 @@ import { resolve as resolve45 } from "node:path";
456538
456743
  import { writeFile as writeFile29 } from "node:fs/promises";
456539
456744
  function registerVideoExtendCommand(parent) {
456540
456745
  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) => {
456746
+ const startedAt = Date.now();
456541
456747
  try {
456542
456748
  const provider = (options.provider || "kling").toLowerCase();
456543
456749
  if (options.output) {
456544
456750
  validateOutputPath(options.output);
456545
456751
  }
456546
456752
  if (options.dryRun) {
456547
- outputResult({
456548
- dryRun: true,
456753
+ outputSuccess({
456549
456754
  command: "generate video-extend",
456550
- params: {
456551
- id,
456552
- provider,
456553
- prompt: options.prompt,
456554
- duration: options.duration,
456555
- negative: options.negative,
456556
- veoModel: options.veoModel
456755
+ startedAt,
456756
+ dryRun: true,
456757
+ data: {
456758
+ params: {
456759
+ id,
456760
+ provider,
456761
+ prompt: options.prompt,
456762
+ duration: options.duration,
456763
+ negative: options.negative,
456764
+ veoModel: options.veoModel
456765
+ }
456557
456766
  }
456558
456767
  });
456559
456768
  return;
@@ -456610,13 +456819,16 @@ function registerVideoExtendCommand(parent) {
456610
456819
  outputPath = resolve45(process.cwd(), options.output);
456611
456820
  await writeFile29(outputPath, buffer);
456612
456821
  }
456613
- outputResult({
456614
- success: true,
456615
- provider: "kling",
456616
- taskId: result.id,
456617
- videoUrl: finalResult.videoUrl,
456618
- duration: finalResult.duration,
456619
- outputPath
456822
+ outputSuccess({
456823
+ command: "generate video-extend",
456824
+ startedAt,
456825
+ data: {
456826
+ provider: "kling",
456827
+ taskId: result.id,
456828
+ videoUrl: finalResult.videoUrl,
456829
+ duration: finalResult.duration,
456830
+ outputPath
456831
+ }
456620
456832
  });
456621
456833
  return;
456622
456834
  }
@@ -456696,13 +456908,16 @@ function registerVideoExtendCommand(parent) {
456696
456908
  outputPath = resolve45(process.cwd(), options.output);
456697
456909
  await writeFile29(outputPath, buffer);
456698
456910
  }
456699
- outputResult({
456700
- success: true,
456701
- provider: "veo",
456702
- taskId: result.id,
456703
- videoUrl: finalResult.videoUrl,
456704
- duration: finalResult.duration,
456705
- outputPath
456911
+ outputSuccess({
456912
+ command: "generate video-extend",
456913
+ startedAt,
456914
+ data: {
456915
+ provider: "veo",
456916
+ taskId: result.id,
456917
+ videoUrl: finalResult.videoUrl,
456918
+ duration: finalResult.duration,
456919
+ outputPath
456920
+ }
456706
456921
  });
456707
456922
  return;
456708
456923
  }
@@ -456817,6 +457032,7 @@ Examples:
456817
457032
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
456818
457033
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
456819
457034
  $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
457035
+ const startedAt = Date.now();
456820
457036
  try {
456821
457037
  if (!prompt3) {
456822
457038
  if (hasTTY()) {
@@ -456875,18 +457091,21 @@ Examples:
456875
457091
  provider = resolved?.name ?? "gemini";
456876
457092
  }
456877
457093
  if (options.dryRun) {
456878
- outputResult({
456879
- dryRun: true,
457094
+ outputSuccess({
456880
457095
  command: "generate image",
456881
- params: {
456882
- prompt: prompt3,
456883
- provider,
456884
- model: options.model,
456885
- ratio: options.ratio,
456886
- size: options.size,
456887
- quality: options.quality,
456888
- count: options.count,
456889
- output: options.output
457096
+ startedAt,
457097
+ dryRun: true,
457098
+ data: {
457099
+ params: {
457100
+ prompt: prompt3,
457101
+ provider,
457102
+ model: options.model,
457103
+ ratio: options.ratio,
457104
+ size: options.size,
457105
+ quality: options.quality,
457106
+ count: options.count,
457107
+ output: options.output
457108
+ }
456890
457109
  }
456891
457110
  });
456892
457111
  return;
@@ -456924,14 +457143,17 @@ Examples:
456924
457143
  await mkdir20(dirname27(outputPath), { recursive: true });
456925
457144
  await writeFile30(outputPath, buffer);
456926
457145
  }
456927
- outputResult({
456928
- success: true,
456929
- provider: "openai",
456930
- images: result.images.map((img) => ({
456931
- url: img.url,
456932
- revisedPrompt: img.revisedPrompt
456933
- })),
456934
- outputPath
457146
+ outputSuccess({
457147
+ command: "generate image",
457148
+ startedAt,
457149
+ data: {
457150
+ provider: "openai",
457151
+ images: result.images.map((img) => ({
457152
+ url: img.url,
457153
+ revisedPrompt: img.revisedPrompt
457154
+ })),
457155
+ outputPath
457156
+ }
456935
457157
  });
456936
457158
  return;
456937
457159
  }
@@ -457043,13 +457265,18 @@ Examples:
457043
457265
  await mkdir20(dirname27(outputPath), { recursive: true });
457044
457266
  await writeFile30(outputPath, buffer);
457045
457267
  }
457046
- outputResult({
457047
- success: true,
457048
- provider: "gemini",
457049
- images: result.images.map((img) => ({
457050
- mimeType: img.mimeType
457051
- })),
457052
- outputPath
457268
+ outputSuccess({
457269
+ command: "generate image",
457270
+ startedAt,
457271
+ warnings: usedLabel.includes("fallback") ? [`Model "${options.model}" failed; fell back to flash`] : [],
457272
+ data: {
457273
+ provider: "gemini",
457274
+ model: usedLabel,
457275
+ images: result.images.map((img) => ({
457276
+ mimeType: img.mimeType
457277
+ })),
457278
+ outputPath
457279
+ }
457053
457280
  });
457054
457281
  return;
457055
457282
  }
@@ -457133,11 +457360,14 @@ Examples:
457133
457360
  await mkdir20(dirname27(outputPath), { recursive: true });
457134
457361
  await writeFile30(outputPath, buffer);
457135
457362
  }
457136
- outputResult({
457137
- success: true,
457138
- provider: "grok",
457139
- images: result.images.map((img) => ({ url: img.url })),
457140
- outputPath
457363
+ outputSuccess({
457364
+ command: "generate image",
457365
+ startedAt,
457366
+ data: {
457367
+ provider: "grok",
457368
+ images: result.images.map((img) => ({ url: img.url })),
457369
+ outputPath
457370
+ }
457141
457371
  });
457142
457372
  return;
457143
457373
  }
@@ -457208,11 +457438,14 @@ Examples:
457208
457438
  proc.on("close", (code) => {
457209
457439
  if (code === 0) {
457210
457440
  if (isJsonMode()) {
457211
- outputResult({
457212
- success: true,
457213
- provider: "runway",
457214
- images: [{ format: "file" }],
457215
- outputPath
457441
+ outputSuccess({
457442
+ command: "generate image",
457443
+ startedAt,
457444
+ data: {
457445
+ provider: "runway",
457446
+ images: [{ format: "file" }],
457447
+ outputPath
457448
+ }
457216
457449
  });
457217
457450
  } else {
457218
457451
  spinner2.succeed(source_default.green("Generated image with Runway"));
@@ -458938,6 +459171,7 @@ Examples:
458938
459171
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
458939
459172
  $ vibe gen vid "ocean waves" -o waves.mp4 -p veo --resolution 1080p # Veo
458940
459173
  $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`).action(async (prompt3, options) => {
459174
+ const startedAt = Date.now();
458941
459175
  try {
458942
459176
  if (!prompt3) {
458943
459177
  if (hasTTY()) {
@@ -459029,19 +459263,22 @@ Examples:
459029
459263
  options.ratio = "16:9";
459030
459264
  }
459031
459265
  if (options.dryRun) {
459032
- outputResult({
459033
- dryRun: true,
459266
+ outputSuccess({
459034
459267
  command: "generate video",
459035
- params: {
459036
- prompt: prompt3,
459037
- provider,
459038
- duration: options.duration,
459039
- ratio: options.ratio,
459040
- image: options.image,
459041
- mode: options.mode,
459042
- negative: options.negative,
459043
- resolution: options.resolution,
459044
- veoModel: options.veoModel
459268
+ startedAt,
459269
+ dryRun: true,
459270
+ data: {
459271
+ params: {
459272
+ prompt: prompt3,
459273
+ provider,
459274
+ duration: options.duration,
459275
+ ratio: options.ratio,
459276
+ image: options.image,
459277
+ mode: options.mode,
459278
+ negative: options.negative,
459279
+ resolution: options.resolution,
459280
+ veoModel: options.veoModel
459281
+ }
459045
459282
  }
459046
459283
  });
459047
459284
  return;
@@ -459321,13 +459558,16 @@ Examples:
459321
459558
  outputPath = resolve49(process.cwd(), options.output);
459322
459559
  await writeFile33(outputPath, buffer);
459323
459560
  }
459324
- outputResult({
459325
- success: true,
459326
- provider,
459327
- taskId: result?.id,
459328
- videoUrl: finalResult.videoUrl,
459329
- duration: finalResult.duration,
459330
- outputPath
459561
+ outputSuccess({
459562
+ command: "generate video",
459563
+ startedAt,
459564
+ data: {
459565
+ provider,
459566
+ taskId: result?.id,
459567
+ videoUrl: finalResult.videoUrl,
459568
+ duration: finalResult.duration,
459569
+ outputPath
459570
+ }
459331
459571
  });
459332
459572
  return;
459333
459573
  }
@@ -459892,20 +460132,24 @@ var init_detect = __esm({
459892
460132
  init_validate();
459893
460133
  detectCommand = new Command("detect").description("Auto-detect scenes, beats, and silences in media");
459894
460134
  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) => {
460135
+ const startedAt = Date.now();
459895
460136
  const spinner2 = ora("Detecting scenes...").start();
459896
460137
  try {
459897
460138
  if (options.output) {
459898
460139
  validateOutputPath(options.output);
459899
460140
  }
459900
460141
  if (options.dryRun) {
459901
- outputResult({
459902
- dryRun: true,
460142
+ outputSuccess({
459903
460143
  command: "detect scenes",
459904
- params: {
459905
- video: videoPath,
459906
- threshold: options.threshold,
459907
- output: options.output || null,
459908
- project: options.project || null
460144
+ startedAt,
460145
+ dryRun: true,
460146
+ data: {
460147
+ params: {
460148
+ video: videoPath,
460149
+ threshold: options.threshold,
460150
+ output: options.output || null,
460151
+ project: options.project || null
460152
+ }
459909
460153
  }
459910
460154
  });
459911
460155
  return;
@@ -460007,20 +460251,24 @@ var init_detect = __esm({
460007
460251
  }
460008
460252
  });
460009
460253
  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) => {
460254
+ const startedAt = Date.now();
460010
460255
  const spinner2 = ora("Detecting silence...").start();
460011
460256
  try {
460012
460257
  if (options.output) {
460013
460258
  validateOutputPath(options.output);
460014
460259
  }
460015
460260
  if (options.dryRun) {
460016
- outputResult({
460017
- dryRun: true,
460261
+ outputSuccess({
460018
460262
  command: "detect silence",
460019
- params: {
460020
- media: mediaPath,
460021
- noise: options.noise,
460022
- duration: options.duration,
460023
- output: options.output || null
460263
+ startedAt,
460264
+ dryRun: true,
460265
+ data: {
460266
+ params: {
460267
+ media: mediaPath,
460268
+ noise: options.noise,
460269
+ duration: options.duration,
460270
+ output: options.output || null
460271
+ }
460024
460272
  }
460025
460273
  });
460026
460274
  return;
@@ -460089,18 +460337,22 @@ var init_detect = __esm({
460089
460337
  }
460090
460338
  });
460091
460339
  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) => {
460340
+ const startedAt = Date.now();
460092
460341
  const spinner2 = ora("Detecting beats...").start();
460093
460342
  try {
460094
460343
  if (options.output) {
460095
460344
  validateOutputPath(options.output);
460096
460345
  }
460097
460346
  if (options.dryRun) {
460098
- outputResult({
460099
- dryRun: true,
460347
+ outputSuccess({
460100
460348
  command: "detect beats",
460101
- params: {
460102
- audio: audioPath,
460103
- output: options.output || null
460349
+ startedAt,
460350
+ dryRun: true,
460351
+ data: {
460352
+ params: {
460353
+ audio: audioPath,
460354
+ output: options.output || null
460355
+ }
460104
460356
  }
460105
460357
  });
460106
460358
  return;
@@ -461574,20 +461826,24 @@ Examples:
461574
461826
  A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
461575
461827
  Run 'vibe schema scene.<command>' for structured parameter info.`);
461576
461828
  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) => {
461829
+ const startedAt = Date.now();
461577
461830
  const aspect = validateAspect(options.ratio);
461578
461831
  const duration = validateDuration(options.duration);
461579
461832
  const name = options.name ?? basename6(dir.replace(/\/+$/, ""));
461580
461833
  const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
461581
461834
  if (options.dryRun) {
461582
- outputResult({
461583
- dryRun: true,
461835
+ outputSuccess({
461584
461836
  command: "scene init",
461585
- params: {
461586
- dir,
461587
- name,
461588
- aspect,
461589
- duration,
461590
- visualStyle: visualStyle?.name ?? null
461837
+ startedAt,
461838
+ dryRun: true,
461839
+ data: {
461840
+ params: {
461841
+ dir,
461842
+ name,
461843
+ aspect,
461844
+ duration,
461845
+ visualStyle: visualStyle?.name ?? null
461846
+ }
461591
461847
  }
461592
461848
  });
461593
461849
  return;
@@ -461603,19 +461859,21 @@ sceneCommand.command("init").description("Scaffold a new scene project (or safel
461603
461859
  hosts: skillHosts
461604
461860
  });
461605
461861
  if (isJsonMode()) {
461606
- outputResult({
461607
- success: true,
461862
+ outputSuccess({
461608
461863
  command: "scene init",
461609
- dir,
461610
- name,
461611
- aspect,
461612
- duration,
461613
- visualStyle: visualStyle?.name ?? null,
461614
- created: result.created,
461615
- merged: result.merged,
461616
- skipped: result.skipped,
461617
- skillFiles: skillResult.files,
461618
- skillBundleVersion: skillResult.bundleVersion
461864
+ startedAt,
461865
+ data: {
461866
+ dir,
461867
+ name,
461868
+ aspect,
461869
+ duration,
461870
+ visualStyle: visualStyle?.name ?? null,
461871
+ created: result.created,
461872
+ merged: result.merged,
461873
+ skipped: result.skipped,
461874
+ skillFiles: skillResult.files,
461875
+ skillBundleVersion: skillResult.bundleVersion
461876
+ }
461619
461877
  });
461620
461878
  return;
461621
461879
  }
@@ -461656,6 +461914,7 @@ sceneCommand.command("init").description("Scaffold a new scene project (or safel
461656
461914
  });
461657
461915
  var VALID_INSTALL_SKILL_HOSTS = ["claude-code", "cursor", "auto", "all"];
461658
461916
  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) => {
461917
+ const startedAt = Date.now();
461659
461918
  const hostFlag = options.host ?? "auto";
461660
461919
  if (!VALID_INSTALL_SKILL_HOSTS.includes(hostFlag)) {
461661
461920
  exitWithError(usageError(`Invalid --host: ${hostFlag}`, `Valid: ${VALID_INSTALL_SKILL_HOSTS.join(", ")}`));
@@ -461675,15 +461934,17 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
461675
461934
  dryRun: options.dryRun ?? false
461676
461935
  });
461677
461936
  if (isJsonMode()) {
461678
- outputResult({
461679
- success: true,
461937
+ outputSuccess({
461680
461938
  command: "scene install-skill",
461681
- projectDir,
461682
- host: hostFlag,
461683
- resolvedHosts: hosts,
461684
- bundleVersion: result.bundleVersion,
461685
- files: result.files,
461686
- dryRun: options.dryRun ?? false
461939
+ startedAt,
461940
+ dryRun: options.dryRun ?? false,
461941
+ data: {
461942
+ projectDir,
461943
+ host: hostFlag,
461944
+ resolvedHosts: hosts,
461945
+ bundleVersion: result.bundleVersion,
461946
+ files: result.files
461947
+ }
461687
461948
  });
461688
461949
  return;
461689
461950
  }
@@ -461705,6 +461966,7 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
461705
461966
  }
461706
461967
  });
461707
461968
  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) => {
461969
+ const startedAt = Date.now();
461708
461970
  const projectDir = resolve21(projectDirArg);
461709
461971
  const result = await getComposePrompts({
461710
461972
  projectDir,
@@ -461712,18 +461974,21 @@ sceneCommand.command("compose-prompts").description("Emit the per-beat compose p
461712
461974
  });
461713
461975
  if (!result.success) {
461714
461976
  if (isJsonMode()) {
461715
- outputResult({
461977
+ outputSuccess({
461716
461978
  command: "scene compose-prompts",
461717
- ...result
461979
+ startedAt,
461980
+ data: { ...result }
461718
461981
  });
461719
- process.exit(1);
461982
+ process.exitCode = 1;
461983
+ return;
461720
461984
  }
461721
461985
  exitWithError(generalError(result.error ?? "compose-prompts failed"));
461722
461986
  }
461723
461987
  if (isJsonMode()) {
461724
- outputResult({
461988
+ outputSuccess({
461725
461989
  command: "scene compose-prompts",
461726
- ...result
461990
+ startedAt,
461991
+ data: { ...result }
461727
461992
  });
461728
461993
  return;
461729
461994
  }
@@ -461757,20 +462022,23 @@ sceneCommand.command("compose-prompts").description("Emit the per-beat compose p
461757
462022
  console.log(source_default.dim("Re-run with --json to get the full per-beat userPrompt + cues for direct consumption."));
461758
462023
  });
461759
462024
  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) => {
462025
+ const startedAt = Date.now();
461760
462026
  if (!name) {
461761
462027
  const all = listVisualStyles();
461762
462028
  if (isJsonMode()) {
461763
- outputResult({
461764
- success: true,
462029
+ outputSuccess({
461765
462030
  command: "scene styles",
461766
- count: all.length,
461767
- styles: all.map((s) => ({
461768
- name: s.name,
461769
- slug: s.slug,
461770
- designer: s.designer,
461771
- mood: s.mood,
461772
- bestFor: s.bestFor
461773
- }))
462031
+ startedAt,
462032
+ data: {
462033
+ count: all.length,
462034
+ styles: all.map((s) => ({
462035
+ name: s.name,
462036
+ slug: s.slug,
462037
+ designer: s.designer,
462038
+ mood: s.mood,
462039
+ bestFor: s.bestFor
462040
+ }))
462041
+ }
461774
462042
  });
461775
462043
  return;
461776
462044
  }
@@ -461798,7 +462066,11 @@ sceneCommand.command("styles").description("List vendored visual styles (or show
461798
462066
  return;
461799
462067
  }
461800
462068
  if (isJsonMode()) {
461801
- outputResult({ success: true, command: "scene styles", style });
462069
+ outputSuccess({
462070
+ command: "scene styles",
462071
+ startedAt,
462072
+ data: { style }
462073
+ });
461802
462074
  return;
461803
462075
  }
461804
462076
  console.log();
@@ -461820,6 +462092,7 @@ sceneCommand.command("styles").description("List vendored visual styles (or show
461820
462092
  console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
461821
462093
  });
461822
462094
  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) => {
462095
+ const startedAt = Date.now();
461823
462096
  if (options.style) options.style = validatePreset(options.style);
461824
462097
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
461825
462098
  let tts;
@@ -461830,25 +462103,28 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
461830
462103
  }
461831
462104
  if (options.dryRun) {
461832
462105
  const id = slugifySceneName(name);
461833
- outputResult({
461834
- dryRun: true,
462106
+ outputSuccess({
461835
462107
  command: "scene add",
461836
- params: {
461837
- name,
461838
- id,
461839
- preset: options.style,
461840
- narration: !!options.narration,
461841
- visuals: !!options.visuals,
461842
- duration: options.duration,
461843
- headline: options.headline,
461844
- kicker: options.kicker,
461845
- project: options.project,
461846
- insertInto: options.insertInto,
461847
- imageProvider: options.imageProvider,
461848
- tts,
461849
- audio: options.audio,
461850
- // commander sets `audio: false` when --no-audio is passed
461851
- image: options.image
462108
+ startedAt,
462109
+ dryRun: true,
462110
+ data: {
462111
+ params: {
462112
+ name,
462113
+ id,
462114
+ preset: options.style,
462115
+ narration: !!options.narration,
462116
+ visuals: !!options.visuals,
462117
+ duration: options.duration,
462118
+ headline: options.headline,
462119
+ kicker: options.kicker,
462120
+ project: options.project,
462121
+ insertInto: options.insertInto,
462122
+ imageProvider: options.imageProvider,
462123
+ tts,
462124
+ audio: options.audio,
462125
+ // commander sets `audio: false` when --no-audio is passed
462126
+ image: options.image
462127
+ }
461852
462128
  }
461853
462129
  });
461854
462130
  return;
@@ -461883,9 +462159,10 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
461883
462159
  exitWithError(generalError(result.error ?? "Scene add failed"));
461884
462160
  }
461885
462161
  if (isJsonMode()) {
461886
- outputResult({
462162
+ outputSuccess({
461887
462163
  command: "scene add",
461888
- ...result
462164
+ startedAt,
462165
+ data: { ...result }
461889
462166
  });
461890
462167
  return;
461891
462168
  }
@@ -462181,6 +462458,7 @@ async function executeSceneAdd(opts) {
462181
462458
  };
462182
462459
  }
462183
462460
  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 (root2, options) => {
462461
+ const startedAt = Date.now();
462184
462462
  const projectDir = resolve21(options.project);
462185
462463
  if (!await rootExists(projectDir, root2)) {
462186
462464
  exitWithError(generalError(
@@ -462198,11 +462476,12 @@ sceneCommand.command("lint").description("Validate scene HTML against Hyperframe
462198
462476
  exitWithError(generalError(`Lint failed: ${msg}`));
462199
462477
  }
462200
462478
  if (isJsonMode()) {
462201
- outputResult({
462479
+ outputSuccess({
462202
462480
  command: "scene lint",
462203
- ...result
462481
+ startedAt,
462482
+ data: { ...result }
462204
462483
  });
462205
- if (!result.ok) process.exit(1);
462484
+ if (!result.ok) process.exitCode = 1;
462206
462485
  return;
462207
462486
  }
462208
462487
  if (result.ok && result.warningCount === 0 && result.infoCount === 0) {
@@ -462236,7 +462515,7 @@ sceneCommand.command("lint").description("Validate scene HTML against Hyperframe
462236
462515
  console.log(` ${source_default.green("\u2714")} ${fx.file} ${source_default.dim(fx.codes.join(", "))}`);
462237
462516
  }
462238
462517
  }
462239
- if (!result.ok) process.exit(1);
462518
+ if (!result.ok) process.exitCode = 1;
462240
462519
  });
462241
462520
  function severityTag(severity) {
462242
462521
  if (severity === "error") return source_default.red("\u2718 error ");
@@ -462273,23 +462552,27 @@ function validateWorkers(value) {
462273
462552
  return n;
462274
462553
  }
462275
462554
  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 (root2, options) => {
462555
+ const startedAt = Date.now();
462276
462556
  const fps = validateFps(options.fps);
462277
462557
  const quality = validateQuality(options.quality);
462278
462558
  const format4 = validateFormat(options.format);
462279
462559
  const workers = validateWorkers(options.workers);
462280
462560
  const projectDir = resolve21(options.project);
462281
462561
  if (options.dryRun) {
462282
- outputResult({
462283
- dryRun: true,
462562
+ outputSuccess({
462284
462563
  command: "scene render",
462285
- params: {
462286
- projectDir,
462287
- root: root2,
462288
- output: options.out,
462289
- fps,
462290
- quality,
462291
- format: format4,
462292
- workers
462564
+ startedAt,
462565
+ dryRun: true,
462566
+ data: {
462567
+ params: {
462568
+ projectDir,
462569
+ root: root2,
462570
+ output: options.out,
462571
+ fps,
462572
+ quality,
462573
+ format: format4,
462574
+ workers
462575
+ }
462293
462576
  }
462294
462577
  });
462295
462578
  return;
@@ -462310,13 +462593,22 @@ sceneCommand.command("render").description("Render a scene project to MP4/WebM/M
462310
462593
  if (!result.success) {
462311
462594
  spinner2?.fail("Render failed");
462312
462595
  if (isJsonMode()) {
462313
- outputResult({ command: "scene render", ...result });
462314
- process.exit(1);
462596
+ outputSuccess({
462597
+ command: "scene render",
462598
+ startedAt,
462599
+ data: { ...result }
462600
+ });
462601
+ process.exitCode = 1;
462602
+ return;
462315
462603
  }
462316
462604
  exitWithError(generalError(result.error ?? "Render failed"));
462317
462605
  }
462318
462606
  if (isJsonMode()) {
462319
- outputResult({ command: "scene render", ...result });
462607
+ outputSuccess({
462608
+ command: "scene render",
462609
+ startedAt,
462610
+ data: { ...result }
462611
+ });
462320
462612
  return;
462321
462613
  }
462322
462614
  spinner2?.succeed(source_default.green(`Render complete: ${result.outputPath}`));
@@ -462336,25 +462628,29 @@ sceneCommand.command("render").description("Render a scene project to MP4/WebM/M
462336
462628
  }
462337
462629
  });
462338
462630
  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) => {
462631
+ const startedAt = Date.now();
462339
462632
  const projectDir = resolve21(projectDirArg);
462340
462633
  if (options.dryRun) {
462341
- outputResult({
462342
- dryRun: true,
462634
+ outputSuccess({
462343
462635
  command: "scene build",
462344
- params: {
462345
- projectDir,
462346
- mode: options.mode,
462347
- effort: options.effort,
462348
- composer: options.composer,
462349
- skipNarration: options.skipNarration ?? false,
462350
- skipBackdrop: options.skipBackdrop ?? false,
462351
- skipRender: options.skipRender ?? false,
462352
- ttsProvider: options.tts,
462353
- voice: options.voice,
462354
- imageProvider: options.imageProvider,
462355
- imageQuality: options.quality,
462356
- imageSize: options.imageSize,
462357
- force: options.force ?? false
462636
+ startedAt,
462637
+ dryRun: true,
462638
+ data: {
462639
+ params: {
462640
+ projectDir,
462641
+ mode: options.mode,
462642
+ effort: options.effort,
462643
+ composer: options.composer,
462644
+ skipNarration: options.skipNarration ?? false,
462645
+ skipBackdrop: options.skipBackdrop ?? false,
462646
+ skipRender: options.skipRender ?? false,
462647
+ ttsProvider: options.tts,
462648
+ voice: options.voice,
462649
+ imageProvider: options.imageProvider,
462650
+ imageQuality: options.quality,
462651
+ imageSize: options.imageSize,
462652
+ force: options.force ?? false
462653
+ }
462358
462654
  }
462359
462655
  });
462360
462656
  return;
@@ -462408,13 +462704,22 @@ sceneCommand.command("build").description("One-shot: read STORYBOARD.md cues, di
462408
462704
  if (!result.success) {
462409
462705
  spinner2?.fail(`Build failed: ${result.error}`);
462410
462706
  if (isJsonMode()) {
462411
- outputResult({ command: "scene build", ...result });
462412
- process.exit(1);
462707
+ outputSuccess({
462708
+ command: "scene build",
462709
+ startedAt,
462710
+ data: { ...result }
462711
+ });
462712
+ process.exitCode = 1;
462713
+ return;
462413
462714
  }
462414
462715
  exitWithError(generalError(result.error ?? "Build failed"));
462415
462716
  }
462416
462717
  if (isJsonMode()) {
462417
- outputResult({ command: "scene build", ...result });
462718
+ outputSuccess({
462719
+ command: "scene build",
462720
+ startedAt,
462721
+ data: { ...result }
462722
+ });
462418
462723
  return;
462419
462724
  }
462420
462725
  if (result.phase === "needs-author") {
@@ -465924,6 +466229,7 @@ Cost: Free (no API keys needed). Requires FFmpeg.
465924
466229
  GIF format: 15fps, no audio, looping. Good for previews and sharing.
465925
466230
  Custom flags (--bitrate, --fps, --resolution, --codec) override preset values.
465926
466231
  Run 'vibe schema export' for structured parameter info.`).action(async (projectPath, options) => {
466232
+ const startedAt = Date.now();
465927
466233
  const spinner2 = ora("Checking FFmpeg...").start();
465928
466234
  try {
465929
466235
  if (options.output) {
@@ -465941,27 +466247,30 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
465941
466247
  exitWithError(usageError(overrideError));
465942
466248
  }
465943
466249
  if (options.dryRun) {
465944
- outputResult({
465945
- dryRun: true,
466250
+ outputSuccess({
465946
466251
  command: "export",
465947
- params: {
465948
- project: projectPath,
465949
- output: options.output || null,
465950
- format: options.format,
465951
- preset: options.preset,
465952
- overwrite: options.overwrite,
465953
- gapFill: options.gapFill,
465954
- backend: options.backend,
465955
- bitrate: options.bitrate ?? null,
465956
- fps: customOverrides.fps ?? null,
465957
- resolution: options.resolution ?? null,
465958
- codec: options.codec ?? null
466252
+ startedAt,
466253
+ dryRun: true,
466254
+ data: {
466255
+ params: {
466256
+ project: projectPath,
466257
+ output: options.output || null,
466258
+ format: options.format,
466259
+ preset: options.preset,
466260
+ overwrite: options.overwrite,
466261
+ gapFill: options.gapFill,
466262
+ backend: options.backend,
466263
+ bitrate: options.bitrate ?? null,
466264
+ fps: customOverrides.fps ?? null,
466265
+ resolution: options.resolution ?? null,
466266
+ codec: options.codec ?? null
466267
+ }
465959
466268
  }
465960
466269
  });
465961
466270
  return;
465962
466271
  }
465963
466272
  if (options.backend === "hyperframes") {
465964
- await runHyperframesExport(projectPath, options, spinner2);
466273
+ await runHyperframesExport(projectPath, options, spinner2, startedAt);
465965
466274
  return;
465966
466275
  }
465967
466276
  const ffmpegPath = await findFFmpeg();
@@ -466548,13 +466857,13 @@ function getPresetSettings(preset, aspectRatio) {
466548
466857
  }
466549
466858
  return settings;
466550
466859
  }
466551
- async function runHyperframesExport(projectPath, options, spinner2) {
466860
+ async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
466552
466861
  spinner2.text = "Loading project...";
466553
466862
  const { readFile: readFile34 } = await import("node:fs/promises");
466554
466863
  const { resolve: resolve64, basename: basename18 } = await import("node:path");
466555
466864
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
466556
466865
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
466557
- const { exitWithError: exitWithError2, generalError: generalError2, outputResult: outputResult2 } = await Promise.resolve().then(() => (init_output(), output_exports));
466866
+ const { exitWithError: exitWithError2, generalError: generalError2, outputSuccess: outputSuccess2 } = await Promise.resolve().then(() => (init_output(), output_exports));
466558
466867
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
466559
466868
  const filePath = resolve64(process.cwd(), projectPath);
466560
466869
  const content = await readFile34(filePath, "utf-8");
@@ -466580,10 +466889,10 @@ async function runHyperframesExport(projectPath, options, spinner2) {
466580
466889
  return;
466581
466890
  }
466582
466891
  spinner2.succeed(chalk2.green(`Exported: ${result.outputPath}`));
466583
- outputResult2({
466584
- success: true,
466892
+ outputSuccess2({
466585
466893
  command: "export",
466586
- result: {
466894
+ startedAt,
466895
+ data: {
466587
466896
  outputPath: result.outputPath,
466588
466897
  backend: "hyperframes",
466589
466898
  durationMs: result.durationMs,