@vibeframe/cli 0.102.0 → 0.103.1

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/CONTEXT.md +4 -4
  2. package/dist/index.js +293 -104
  3. package/package.json +3 -3
package/CONTEXT.md CHANGED
@@ -40,10 +40,10 @@ vibe guide <topic> --json # Step-by-step guides (motion | scene | p
40
40
 
41
41
  | Tier | Commands | Per-call cost |
42
42
  | --------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------- |
43
- | Free | `detect *`, `edit silence-cut/fade/noise-reduce/text-overlay/interpolate`, `timeline *`, `scene lint/list-styles`, `audio duck` | $0 |
44
- | Low | `inspect *`, `audio transcribe/list-voices`, `generate image` | $0.01–0.10 |
45
- | High | `generate video`, `edit image`, `edit grade/reframe/speed-ramp` | $1–5 |
46
- | Very High | `remix *` (highlights, auto-shorts, regenerate-scene), `vibe build` (full pipeline) | $5–50+ |
43
+ | Free | `schema/context/doctor`, `detect *`, `status *`, `plan`, `storyboard validate`, `inspect project/render --cheap`, deterministic edits | $0 |
44
+ | Low | `generate narration/sound-effect/music`, `audio transcribe/list-voices`, `inspect media`, optional AI review | $0.01–0.10 |
45
+ | High | `generate image/motion`, `edit image/reframe/grade/speed-ramp` | $1–5 |
46
+ | Very High | `generate video`, `edit fill-gaps`, `remix highlights/auto-shorts`, `build` with generated assets | $5–50+ |
47
47
 
48
48
  > Rule: **confirm with the user before any High / Very-High call**.
49
49
 
package/dist/index.js CHANGED
@@ -25732,6 +25732,7 @@ __export(config_exports, {
25732
25732
  USER_CONFIG_DIR: () => USER_CONFIG_DIR,
25733
25733
  USER_CONFIG_PATH: () => USER_CONFIG_PATH,
25734
25734
  createDefaultConfig: () => createDefaultConfig,
25735
+ findProjectConfigPath: () => findProjectConfigPath,
25735
25736
  getActiveScope: () => getActiveScope,
25736
25737
  getApiKeyFromConfig: () => getApiKeyFromConfig,
25737
25738
  getConfigDir: () => getConfigDir,
@@ -26461,9 +26462,14 @@ End on the product name, offer, or command. Avoid adding a new idea in the
26461
26462
  final beat.
26462
26463
  `;
26463
26464
  }
26464
- function buildProjectClaudeMd(name) {
26465
+ function buildProjectAgentsMd(name) {
26465
26466
  return `# ${name} \u2014 Scene Authoring Project
26466
26467
 
26468
+ This is the canonical cross-agent guidance file for this scene project.
26469
+ Claude Code imports it through \`CLAUDE.md\`; Codex, Cursor, Aider,
26470
+ Gemini CLI, OpenCode, and other bash-capable agents should read it
26471
+ directly.
26472
+
26467
26473
  This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
26468
26474
  HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
26469
26475
  directory.
@@ -26503,6 +26509,8 @@ Browse named styles: \`vibe scene list-styles\`. Re-seed from one with
26503
26509
 
26504
26510
  ## Skills \u2014 USE THESE FIRST
26505
26511
 
26512
+ @SKILL.md
26513
+
26506
26514
  **Always invoke the relevant skill before authoring scenes.** Skills encode
26507
26515
  framework-specific patterns (GSAP timeline registration, data-attribute
26508
26516
  semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
@@ -26571,6 +26579,17 @@ vibe scene lint --json # structured output for agent loops
26571
26579
  \`\`\`
26572
26580
  `;
26573
26581
  }
26582
+ function buildProjectClaudeMd(name) {
26583
+ return `@AGENTS.md
26584
+
26585
+ # ${name} \u2014 Claude Code Overrides
26586
+
26587
+ This file imports \`AGENTS.md\`; keep cross-agent VibeFrame and
26588
+ Hyperframes instructions there so Codex, Cursor, Aider, Gemini CLI, and
26589
+ OpenCode see the same project rules. Add Claude-Code-specific notes below
26590
+ only when this project needs them.
26591
+ `;
26592
+ }
26574
26593
  function buildSceneGitignore() {
26575
26594
  return `# VibeFrame \u2014 caches, checkpoints, and project-scope config.yaml (may contain API keys)
26576
26595
  .vibeframe/
@@ -26613,6 +26632,7 @@ function describeSceneScaffold(opts) {
26613
26632
  }
26614
26633
  if (profile === "agent" || profile === "full") {
26615
26634
  groups.agent = [
26635
+ resolve7(dir, "AGENTS.md"),
26616
26636
  resolve7(dir, "SKILL.md"),
26617
26637
  resolve7(dir, "references"),
26618
26638
  resolve7(dir, "CLAUDE.md")
@@ -26692,9 +26712,24 @@ async function scaffoldSceneProject(opts) {
26692
26712
  created.push(vibePath);
26693
26713
  }
26694
26714
  if (profile === "agent" || profile === "full") {
26715
+ const agentsPath = resolve7(dir, "AGENTS.md");
26716
+ if (await pathExists(agentsPath)) {
26717
+ skipped2.push(agentsPath);
26718
+ } else {
26719
+ await writeFile4(agentsPath, buildProjectAgentsMd(name), "utf-8");
26720
+ created.push(agentsPath);
26721
+ }
26695
26722
  const claudePath = resolve7(dir, "CLAUDE.md");
26696
26723
  if (await pathExists(claudePath)) {
26697
- skipped2.push(claudePath);
26724
+ const existing = await readFile5(claudePath, "utf-8");
26725
+ if (existing.includes("@AGENTS.md")) {
26726
+ skipped2.push(claudePath);
26727
+ } else {
26728
+ await writeFile4(claudePath, `@AGENTS.md
26729
+
26730
+ ${existing}`, "utf-8");
26731
+ merged.push(claudePath);
26732
+ }
26698
26733
  } else {
26699
26734
  await writeFile4(claudePath, buildProjectClaudeMd(name), "utf-8");
26700
26735
  created.push(claudePath);
@@ -32543,22 +32578,35 @@ function applySuggestion(project, suggestion) {
32543
32578
  if (clipIds.length === 0) return false;
32544
32579
  const clipId = clipIds[0];
32545
32580
  switch (type) {
32546
- case "trim":
32547
- if (params.newDuration) {
32548
- return project.trimClipEnd(clipId, params.newDuration);
32581
+ case "trim": {
32582
+ const newDuration = params.newDuration;
32583
+ if (typeof newDuration === "number") {
32584
+ return project.trimClipEnd(clipId, newDuration);
32549
32585
  }
32550
32586
  break;
32551
- case "add-effect":
32552
- if (params.effectType) {
32587
+ }
32588
+ case "add-effect": {
32589
+ const effectType = params.effectType;
32590
+ if (typeof effectType === "string") {
32591
+ const startTime = typeof params.startTime === "number" ? params.startTime : 0;
32592
+ const duration = typeof params.duration === "number" ? params.duration : 1;
32593
+ const rawEffectParams = params.effectParams && typeof params.effectParams === "object" ? params.effectParams : {};
32594
+ const effectParams = {};
32595
+ for (const [k, v] of Object.entries(rawEffectParams)) {
32596
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
32597
+ effectParams[k] = v;
32598
+ }
32599
+ }
32553
32600
  const effect = project.addEffect(clipId, {
32554
- type: params.effectType,
32555
- startTime: params.startTime || 0,
32556
- duration: params.duration || 1,
32557
- params: params.effectParams || {}
32601
+ type: effectType,
32602
+ startTime,
32603
+ duration,
32604
+ params: effectParams
32558
32605
  });
32559
32606
  return effect !== null;
32560
32607
  }
32561
32608
  break;
32609
+ }
32562
32610
  case "delete":
32563
32611
  return project.removeClip(clipId);
32564
32612
  }
@@ -32763,6 +32811,19 @@ var init_open_url = __esm({
32763
32811
  // src/utils/tty.ts
32764
32812
  import { createInterface as createInterface2 } from "node:readline";
32765
32813
  import { ReadStream } from "node:tty";
32814
+ function stripAnsi2(value) {
32815
+ return value.replace(ANSI_RE, "");
32816
+ }
32817
+ function terminalColumns() {
32818
+ return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
32819
+ }
32820
+ function fitOptionToLine(value, prefixColumns) {
32821
+ const maxColumns = Math.max(8, terminalColumns() - prefixColumns - 1);
32822
+ const visible = stripAnsi2(value);
32823
+ if (visible.length <= maxColumns) return value;
32824
+ if (maxColumns <= ELLIPSIS.length) return ELLIPSIS.slice(0, maxColumns);
32825
+ return `${visible.slice(0, maxColumns - ELLIPSIS.length)}${ELLIPSIS}`;
32826
+ }
32766
32827
  function splitTTYInput(chunk) {
32767
32828
  const tokens = [];
32768
32829
  for (let i = 0; i < chunk.length; i++) {
@@ -32883,7 +32944,8 @@ async function promptSelect(_question, options, defaultIndex = 0, opts = {}) {
32883
32944
  }
32884
32945
  for (let i = 0; i < options.length; i++) {
32885
32946
  const marker = i === selected ? "\x1B[36m\u276F\x1B[0m" : " ";
32886
- const text = i === selected ? `\x1B[1m${options[i]}\x1B[0m` : options[i];
32947
+ const optionText = fitOptionToLine(options[i], 5);
32948
+ const text = i === selected ? `\x1B[1m${optionText}\x1B[0m` : optionText;
32887
32949
  process.stdout.write(`\x1B[2K ${marker} ${text}
32888
32950
  `);
32889
32951
  }
@@ -32962,7 +33024,8 @@ async function promptConfirm(question, defaultYes = true) {
32962
33024
  }
32963
33025
  for (let i = 0; i < options.length; i++) {
32964
33026
  const marker = i === selected ? "\x1B[36m\u276F\x1B[0m" : " ";
32965
- const text = i === selected ? `\x1B[1m${options[i]}\x1B[0m` : options[i];
33027
+ const optionText = fitOptionToLine(options[i], 5);
33028
+ const text = i === selected ? `\x1B[1m${optionText}\x1B[0m` : optionText;
32966
33029
  process.stdout.write(`\x1B[2K ${marker} ${text}
32967
33030
  `);
32968
33031
  }
@@ -33030,7 +33093,8 @@ async function promptMultiSelect(_question, options, defaultSelected = [], opts
33030
33093
  for (let i = 0; i < options.length; i++) {
33031
33094
  const pointer = i === cursor ? "\x1B[36m\u276F\x1B[0m" : " ";
33032
33095
  const box = selected[i] ? "\x1B[36m[x]\x1B[0m" : "[ ]";
33033
- const text = i === cursor ? `\x1B[1m${options[i]}\x1B[0m` : options[i];
33096
+ const optionText = fitOptionToLine(options[i], 9);
33097
+ const text = i === cursor ? `\x1B[1m${optionText}\x1B[0m` : optionText;
33034
33098
  process.stdout.write(`\x1B[2K ${pointer} ${box} ${text}
33035
33099
  `);
33036
33100
  }
@@ -33122,12 +33186,14 @@ async function promptMultiSelect(_question, options, defaultSelected = [], opts
33122
33186
  }
33123
33187
  return [...picked].sort((a, b) => a - b);
33124
33188
  }
33125
- var ttyStream;
33189
+ var ttyStream, ANSI_RE, ELLIPSIS;
33126
33190
  var init_tty = __esm({
33127
33191
  "src/utils/tty.ts"() {
33128
33192
  "use strict";
33129
33193
  init_open_url();
33130
33194
  ttyStream = null;
33195
+ ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g");
33196
+ ELLIPSIS = "...";
33131
33197
  }
33132
33198
  });
33133
33199
 
@@ -34891,6 +34957,7 @@ function isActiveStatus(status) {
34891
34957
  }
34892
34958
  function providerStatusCommand(record) {
34893
34959
  if (record.jobType === "generate-video") {
34960
+ if (record.provider !== "runway" && record.provider !== "kling") return void 0;
34894
34961
  const type = record.provider === "kling" && record.providerTaskType ? ` --type ${record.providerTaskType}` : "";
34895
34962
  return `vibe generate video-status ${record.providerTaskId} -p ${record.provider}${type} --json`;
34896
34963
  }
@@ -58194,9 +58261,22 @@ function backdropCacheDescriptor(opts) {
58194
58261
  provider: opts.provider,
58195
58262
  quality: opts.quality,
58196
58263
  size: opts.size,
58264
+ ratio: opts.ratio,
58197
58265
  ext: "png"
58198
58266
  });
58199
58267
  }
58268
+ function imageRatioForSize(size) {
58269
+ switch (size) {
58270
+ case "1024x1024":
58271
+ return "1:1";
58272
+ case "1024x1536":
58273
+ return "2:3";
58274
+ case "1536x1024":
58275
+ return "3:2";
58276
+ default:
58277
+ return "16:9";
58278
+ }
58279
+ }
58200
58280
  function videoCacheDescriptor(opts) {
58201
58281
  return cacheAssetDescriptor("video", {
58202
58282
  beatId: opts.beatId,
@@ -58274,6 +58354,7 @@ async function createBuildPlan(opts) {
58274
58354
  }
58275
58355
  const storyboardMd = await readFile40(storyboardPath2, "utf-8");
58276
58356
  const validation = validateStoryboardMarkdown(storyboardMd);
58357
+ const validationIssues = [...validation.issues];
58277
58358
  const parsed = parseStoryboard(storyboardMd);
58278
58359
  let sourceBeats = parsed.beats;
58279
58360
  if (opts.beat) {
@@ -58296,6 +58377,7 @@ async function createBuildPlan(opts) {
58296
58377
  const mode = opts.mode ?? config2.config.build.mode;
58297
58378
  const imageQuality = opts.imageQuality ?? config2.config.build.imageQuality ?? "hd";
58298
58379
  const imageSize2 = opts.imageSize ?? config2.config.build.imageSize ?? "1536x1024";
58380
+ const imageRatio = imageRatioForSize(imageSize2);
58299
58381
  const needsComposer = includeCompose && mode !== "agent" && sourceBeats.some((beat) => !existsSync44(join20(projectDir, `compositions/scene-${beat.id}.html`)));
58300
58382
  const resolved = await resolvePlanProviders({
58301
58383
  projectDir,
@@ -58345,7 +58427,8 @@ async function createBuildPlan(opts) {
58345
58427
  cue: backdropPrompt,
58346
58428
  provider: resolved.image.resolved,
58347
58429
  quality: imageQuality,
58348
- size: imageSize2
58430
+ size: imageSize2,
58431
+ ratio: imageRatio
58349
58432
  }) : null;
58350
58433
  const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
58351
58434
  beatId: beat.id,
@@ -58439,7 +58522,8 @@ async function createBuildPlan(opts) {
58439
58522
  }
58440
58523
  }
58441
58524
  if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
58442
- if (backdrop?.willGenerate) noteProviderNeed(resolved.image, "Backdrop generation");
58525
+ if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
58526
+ noteProviderNeed(resolved.image, "Backdrop generation");
58443
58527
  if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
58444
58528
  if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
58445
58529
  if (!compositionExists) missing.add("compositions");
@@ -58461,23 +58545,35 @@ async function createBuildPlan(opts) {
58461
58545
  };
58462
58546
  });
58463
58547
  providerResolution.push(...providerResolutionsForPlan(resolved, beats, opts, includeAssets));
58548
+ if (includeAssets && !opts.skipBackdrop && !isSupportedBuildImageProvider(resolved.image.resolved)) {
58549
+ const unsupportedBackdropBeats = beats.filter(
58550
+ (beat) => isBuildGeneratedBackdropPlan(beat.assets.backdrop)
58551
+ );
58552
+ for (const beat of unsupportedBackdropBeats) {
58553
+ validationIssues.push({
58554
+ severity: "error",
58555
+ code: "UNSUPPORTED_BUILD_IMAGE_PROVIDER",
58556
+ beatId: beat.id,
58557
+ message: `Build backdrop generation supports openai, gemini, and grok. Beat "${beat.id}" resolves image provider "${resolved.image.resolved}"; rerun with one of those or replace the backdrop cue with a project asset path.`
58558
+ });
58559
+ }
58560
+ if (unsupportedBackdropBeats.length > 0) {
58561
+ const beatFlag = opts.beat ? ` --beat ${opts.beat}` : "";
58562
+ retryWith.push(
58563
+ `vibe build ${projectDir}${beatFlag} --stage assets --image-provider openai --json`
58564
+ );
58565
+ }
58566
+ }
58567
+ const validationOk = !validationIssues.some((issue) => issue.severity === "error");
58464
58568
  if (!existsSync44(join20(projectDir, config2.config.composition.entry))) {
58465
58569
  missing.add("root-composition");
58466
- if (validation.ok) retryWith.push(`vibe build ${projectDir} --stage sync --json`);
58570
+ if (validationOk) retryWith.push(`vibe build ${projectDir} --stage sync --json`);
58467
58571
  }
58468
58572
  if (config2.legacy) {
58469
58573
  warnings.push(
58470
58574
  `Using legacy ${config2.source}; write ${projectDir}/vibe.config.json to use the TO-BE project contract.`
58471
58575
  );
58472
58576
  }
58473
- if (resolved.image.resolved !== "openai" && beats.some((beat) => {
58474
- const backdrop = beat.assets.backdrop;
58475
- return backdrop && !["referenced-asset", "invalid-reference", "stage-skipped"].includes(backdrop.reason);
58476
- }) && !opts.skipBackdrop) {
58477
- warnings.push(
58478
- `Image provider "${resolved.image.resolved}" is not supported by build assets yet; use --image-provider openai.`
58479
- );
58480
- }
58481
58577
  if (!validation.ok) {
58482
58578
  retryWith.push(
58483
58579
  `vibe storyboard validate ${projectDir} --json`,
@@ -58488,7 +58584,7 @@ async function createBuildPlan(opts) {
58488
58584
  projectDir,
58489
58585
  config: config2,
58490
58586
  stage,
58491
- status: validation.ok ? "ready" : "invalid",
58587
+ status: validationOk ? "ready" : "invalid",
58492
58588
  currentStage: stage,
58493
58589
  mode,
58494
58590
  beat: opts.beat ?? null,
@@ -58500,8 +58596,8 @@ async function createBuildPlan(opts) {
58500
58596
  warnings,
58501
58597
  retryWith,
58502
58598
  validation: {
58503
- ok: validation.ok,
58504
- issues: validation.issues
58599
+ ok: validationOk,
58600
+ issues: validationIssues
58505
58601
  }
58506
58602
  });
58507
58603
  }
@@ -58528,6 +58624,15 @@ function finalizeBuildPlan(plan) {
58528
58624
  retryWith: unique3(plan.retryWith)
58529
58625
  };
58530
58626
  }
58627
+ function isBuildGeneratedBackdropPlan(asset) {
58628
+ if (!asset || asset.kind !== "backdrop") return false;
58629
+ return !["referenced-asset", "invalid-reference", "stage-skipped"].includes(asset.reason);
58630
+ }
58631
+ function isSupportedBuildImageProvider(provider) {
58632
+ return SUPPORTED_BUILD_IMAGE_PROVIDERS.includes(
58633
+ provider
58634
+ );
58635
+ }
58531
58636
  function nextCommandsForPlan(plan) {
58532
58637
  if (plan.status === "invalid") return unique3(plan.retryWith);
58533
58638
  const commands = [];
@@ -58827,7 +58932,7 @@ function loadPlanEnv(projectDir) {
58827
58932
  dir = dirname19(dir);
58828
58933
  }
58829
58934
  }
58830
- var import_dotenv3, BACKDROP_COST_USD, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD;
58935
+ var import_dotenv3, BACKDROP_COST_USD, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
58831
58936
  var init_build_plan = __esm({
58832
58937
  "src/commands/_shared/build-plan.ts"() {
58833
58938
  "use strict";
@@ -58846,6 +58951,7 @@ var init_build_plan = __esm({
58846
58951
  MUSIC_COST_USD = 0.5;
58847
58952
  ELEVENLABS_NARRATION_COST_USD = 0.05;
58848
58953
  COMPOSE_COST_USD = 0.06;
58954
+ SUPPORTED_BUILD_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
58849
58955
  }
58850
58956
  });
58851
58957
 
@@ -60252,19 +60358,18 @@ async function dispatchBackdrop(beat, ctx) {
60252
60358
  if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
60253
60359
  const prompt3 = stringOrUndefined4(beat.cues?.backdrop);
60254
60360
  if (!prompt3) return { status: "no-cue" };
60255
- if (ctx.imageProvider !== "openai") {
60256
- const error = `image provider "${ctx.imageProvider}" not yet supported (use openai)`;
60257
- ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
60258
- return { status: "failed", error };
60259
- }
60260
60361
  const rel = `assets/backdrop-${beat.id}.png`;
60261
60362
  const abs = join22(ctx.projectDir, rel);
60363
+ const size = ctx.imageSize ?? "1536x1024";
60364
+ const ratio = imageRatioForSize(size);
60365
+ const metadataOptions = { quality: ctx.imageQuality, size, ratio };
60262
60366
  const cache = backdropCacheDescriptor({
60263
60367
  beatId: beat.id,
60264
60368
  cue: prompt3,
60265
60369
  provider: ctx.imageProvider,
60266
60370
  quality: ctx.imageQuality,
60267
- size: ctx.imageSize ?? "1536x1024"
60371
+ size,
60372
+ ratio
60268
60373
  });
60269
60374
  const metadataPath = assetMetadataPath("backdrop", beat.id);
60270
60375
  if (existsSync46(abs) && !ctx.force) {
@@ -60275,7 +60380,7 @@ async function dispatchBackdrop(beat, ctx) {
60275
60380
  beatId: beat.id,
60276
60381
  cue: prompt3,
60277
60382
  provider: ctx.imageProvider,
60278
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60383
+ options: metadataOptions,
60279
60384
  cacheKey: cache.key
60280
60385
  })) {
60281
60386
  ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
@@ -60300,7 +60405,7 @@ async function dispatchBackdrop(beat, ctx) {
60300
60405
  beatId: beat.id,
60301
60406
  cue: prompt3,
60302
60407
  provider: ctx.imageProvider,
60303
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60408
+ options: metadataOptions,
60304
60409
  cacheKey: cache.key,
60305
60410
  canonicalPath: rel,
60306
60411
  cachePath: cache.path
@@ -60316,27 +60421,14 @@ async function dispatchBackdrop(beat, ctx) {
60316
60421
  freshness: "fresh"
60317
60422
  };
60318
60423
  }
60319
- loadSceneBuildEnv(ctx.projectDir);
60320
- const apiKey = await getApiKeyFromConfig("openai", { cwd: ctx.projectDir }) ?? process.env.OPENAI_API_KEY ?? "";
60321
- if (!apiKey) {
60322
- const error = "OPENAI_API_KEY not set \u2014 cannot dispatch backdrop";
60323
- ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
60324
- return { status: "failed", error };
60325
- }
60326
- const provider = new OpenAIImageProvider();
60327
- await provider.initialize({ apiKey });
60328
- const result = await provider.generateImage(prompt3, {
60329
- model: "gpt-image-2",
60330
- size: ctx.imageSize,
60331
- quality: ctx.imageQuality
60332
- });
60333
- if (!result.success || !result.images?.[0]?.base64) {
60334
- const error = result.error ?? "no image data returned";
60424
+ const generated = await generateBackdropImage(prompt3, ctx, ratio);
60425
+ if (!generated.success) {
60426
+ const error = generated.error;
60335
60427
  ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
60336
60428
  return { status: "failed", error };
60337
60429
  }
60338
60430
  await mkdir24(dirname22(abs), { recursive: true });
60339
- const buffer = Buffer.from(result.images[0].base64, "base64");
60431
+ const buffer = generated.buffer;
60340
60432
  await writeFile48(abs, buffer);
60341
60433
  await mkdir24(dirname22(cacheAbs), { recursive: true });
60342
60434
  await writeFile48(cacheAbs, buffer);
@@ -60345,8 +60437,8 @@ async function dispatchBackdrop(beat, ctx) {
60345
60437
  kind: "backdrop",
60346
60438
  beatId: beat.id,
60347
60439
  cue: prompt3,
60348
- provider: "openai",
60349
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60440
+ provider: ctx.imageProvider,
60441
+ options: metadataOptions,
60350
60442
  cacheKey: cache.key,
60351
60443
  canonicalPath: rel,
60352
60444
  cachePath: cache.path
@@ -60355,18 +60447,75 @@ async function dispatchBackdrop(beat, ctx) {
60355
60447
  type: "backdrop-generated",
60356
60448
  beatId: beat.id,
60357
60449
  path: rel,
60358
- provider: "openai"
60450
+ provider: ctx.imageProvider
60359
60451
  });
60360
60452
  return {
60361
60453
  status: "generated",
60362
60454
  path: rel,
60363
- provider: "openai",
60455
+ provider: ctx.imageProvider,
60364
60456
  cachePath: cache.path,
60365
60457
  cacheKey: cache.key,
60366
60458
  metadataPath,
60367
60459
  freshness: "fresh"
60368
60460
  };
60369
60461
  }
60462
+ async function generateBackdropImage(prompt3, ctx, ratio) {
60463
+ loadSceneBuildEnv(ctx.projectDir);
60464
+ const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
60465
+ const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
60466
+ if (!apiKey) {
60467
+ return {
60468
+ success: false,
60469
+ error: `${keyInfo.envVar} not set \u2014 cannot dispatch backdrop with ${ctx.imageProvider}`
60470
+ };
60471
+ }
60472
+ if (ctx.imageProvider === "openai") {
60473
+ const provider2 = new OpenAIImageProvider();
60474
+ await provider2.initialize({ apiKey });
60475
+ const result2 = await provider2.generateImage(prompt3, {
60476
+ model: "gpt-image-2",
60477
+ size: ctx.imageSize,
60478
+ quality: ctx.imageQuality
60479
+ });
60480
+ return imageBufferFromResult(result2);
60481
+ }
60482
+ if (ctx.imageProvider === "gemini") {
60483
+ const provider2 = new GeminiProvider();
60484
+ await provider2.initialize({ apiKey });
60485
+ const result2 = await provider2.generateImage(prompt3, {
60486
+ model: "flash",
60487
+ aspectRatio: ratio
60488
+ });
60489
+ return imageBufferFromResult(result2);
60490
+ }
60491
+ const provider = new GrokProvider();
60492
+ await provider.initialize({ apiKey });
60493
+ const result = await provider.generateImage(prompt3, {
60494
+ aspectRatio: ratio,
60495
+ n: 1
60496
+ });
60497
+ return imageBufferFromResult(result);
60498
+ }
60499
+ async function imageBufferFromResult(result) {
60500
+ const image = result.images?.[0];
60501
+ if (!result.success || !image) {
60502
+ return { success: false, error: result.error ?? "no image data returned" };
60503
+ }
60504
+ if (image.base64) return { success: true, buffer: Buffer.from(image.base64, "base64") };
60505
+ if (image.url) {
60506
+ const response = await fetch(image.url);
60507
+ if (!response.ok) {
60508
+ return { success: false, error: `failed to download image: HTTP ${response.status}` };
60509
+ }
60510
+ return { success: true, buffer: Buffer.from(await response.arrayBuffer()) };
60511
+ }
60512
+ return { success: false, error: "no image data returned" };
60513
+ }
60514
+ function imageProviderKeyInfo(provider) {
60515
+ if (provider === "gemini") return { configKey: "google", envVar: "GOOGLE_API_KEY" };
60516
+ if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
60517
+ return { configKey: "openai", envVar: "OPENAI_API_KEY" };
60518
+ }
60370
60519
  async function dispatchVideo(beat, ctx) {
60371
60520
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
60372
60521
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
@@ -61265,14 +61414,14 @@ __export(ai_audio_exports, {
61265
61414
  });
61266
61415
  import { resolve as resolve69, dirname as dirname24, basename as basename22, extname as extname15 } from "node:path";
61267
61416
  import { readFile as readFile45, writeFile as writeFile49 } from "node:fs/promises";
61268
- import { existsSync as existsSync50 } from "node:fs";
61417
+ import { existsSync as existsSync51 } from "node:fs";
61269
61418
  async function executeTranscribe(options) {
61270
61419
  const { audioPath, language, output, format, apiKey } = options;
61271
61420
  try {
61272
61421
  const key = apiKey || process.env.OPENAI_API_KEY;
61273
61422
  if (!key) return { success: false, error: "OPENAI_API_KEY required" };
61274
61423
  const absPath = resolve69(process.cwd(), audioPath);
61275
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61424
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61276
61425
  const whisper = new WhisperProvider();
61277
61426
  await whisper.initialize({ apiKey: key });
61278
61427
  const audioBuffer = await readFile45(absPath);
@@ -61305,7 +61454,7 @@ async function executeIsolate(options) {
61305
61454
  const key = apiKey || process.env.ELEVENLABS_API_KEY;
61306
61455
  if (!key) return { success: false, error: "ELEVENLABS_API_KEY required" };
61307
61456
  const absPath = resolve69(process.cwd(), audioPath);
61308
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61457
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61309
61458
  const audioBuffer = await readFile45(absPath);
61310
61459
  const elevenlabs = new ElevenLabsProvider();
61311
61460
  await elevenlabs.initialize({ apiKey: key });
@@ -61331,7 +61480,7 @@ async function executeVoiceClone(options) {
61331
61480
  const audioBuffers = [];
61332
61481
  for (const samplePath of samplePaths) {
61333
61482
  const absPath = resolve69(process.cwd(), samplePath);
61334
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${samplePath}` };
61483
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${samplePath}` };
61335
61484
  const buffer = await readFile45(absPath);
61336
61485
  audioBuffers.push(buffer);
61337
61486
  }
@@ -61353,7 +61502,7 @@ async function executeDub(options) {
61353
61502
  const { mediaPath, language, source, voice, analyzeOnly, output } = options;
61354
61503
  try {
61355
61504
  const absPath = resolve69(process.cwd(), mediaPath);
61356
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61505
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61357
61506
  const openaiKey = process.env.OPENAI_API_KEY;
61358
61507
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
61359
61508
  const elevenlabsKey = process.env.ELEVENLABS_API_KEY;
@@ -61481,8 +61630,8 @@ async function executeDuck(options) {
61481
61630
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
61482
61631
  const absMusicPath = resolve69(process.cwd(), musicPath);
61483
61632
  const absVoicePath = resolve69(process.cwd(), voicePath);
61484
- if (!existsSync50(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
61485
- if (!existsSync50(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
61633
+ if (!existsSync51(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
61634
+ if (!existsSync51(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
61486
61635
  const defaultOutput = resolve69(dirname24(absMusicPath), `${basename22(absMusicPath, extname15(absMusicPath))}-ducked${extname15(absMusicPath)}`);
61487
61636
  const outputPath = resolve69(process.cwd(), output || defaultOutput);
61488
61637
  const filter = `[1:a]asplit=2[sc][mix];[0:a][sc]sidechaincompress=threshold=${threshold}dB:ratio=${ratio}:attack=${attack}:release=${release}[ducked];[ducked][mix]amix=inputs=2:duration=longest`;
@@ -61496,7 +61645,7 @@ async function executeDuck(options) {
61496
61645
  "-y",
61497
61646
  outputPath
61498
61647
  ], { timeout: 12e4, maxBuffer: 50 * 1024 * 1024 });
61499
- if (!existsSync50(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
61648
+ if (!existsSync51(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
61500
61649
  return { success: true, outputPath };
61501
61650
  } catch (error) {
61502
61651
  return { success: false, error: `Audio ducking failed: ${error instanceof Error ? error.message : String(error)}` };
@@ -61514,7 +61663,7 @@ var init_ai_audio = __esm({
61514
61663
  // src/pipeline/executor.ts
61515
61664
  import { resolve as resolve70 } from "node:path";
61516
61665
  import { readFile as readFile46, writeFile as writeFile50, mkdir as mkdir26 } from "node:fs/promises";
61517
- import { existsSync as existsSync51 } from "node:fs";
61666
+ import { existsSync as existsSync52 } from "node:fs";
61518
61667
  function maxCostFor(action) {
61519
61668
  const cmd = ACTION_METADATA[action]?.command;
61520
61669
  if (!cmd) return 0;
@@ -61829,7 +61978,7 @@ async function ensureActionsRegistered() {
61829
61978
  }
61830
61979
  async function loadPipeline(filePath) {
61831
61980
  const absPath = resolve70(process.cwd(), filePath);
61832
- if (!existsSync51(absPath)) {
61981
+ if (!existsSync52(absPath)) {
61833
61982
  throw new Error(`Pipeline file not found: ${absPath}`);
61834
61983
  }
61835
61984
  const content = await readFile46(absPath, "utf-8");
@@ -61854,7 +62003,7 @@ async function executePipeline(manifest2, options = {}) {
61854
62003
  const startTime = Date.now();
61855
62004
  if (options.resume) {
61856
62005
  const checkpointPath = resolve70(outputDir, CHECKPOINT_FILE);
61857
- if (existsSync51(checkpointPath)) {
62006
+ if (existsSync52(checkpointPath)) {
61858
62007
  const checkpointContent = await readFile46(checkpointPath, "utf-8");
61859
62008
  const checkpoint = (0, import_yaml8.parse)(checkpointContent);
61860
62009
  for (const cs of checkpoint.completedSteps) {
@@ -67398,7 +67547,10 @@ async function executeSuggestEdit(options) {
67398
67547
  }
67399
67548
  return { success: true, suggestions };
67400
67549
  } catch (error) {
67401
- return { success: false, error: `Suggest failed: ${error instanceof Error ? error.message : String(error)}` };
67550
+ return {
67551
+ success: false,
67552
+ error: `Suggest failed: ${error instanceof Error ? error.message : String(error)}`
67553
+ };
67402
67554
  }
67403
67555
  }
67404
67556
  var init_ai_suggest_edit = __esm({
@@ -68078,12 +68230,12 @@ var init_generate2 = __esm({
68078
68230
  });
68079
68231
 
68080
68232
  // src/tools/manifest/storyboard.ts
68081
- import { existsSync as existsSync52 } from "node:fs";
68233
+ import { existsSync as existsSync53 } from "node:fs";
68082
68234
  import { readFile as readFile48, writeFile as writeFile52 } from "node:fs/promises";
68083
68235
  import { resolve as resolve74 } from "node:path";
68084
68236
  async function readStoryboard2(projectDir) {
68085
68237
  const path2 = resolve74(projectDir, "STORYBOARD.md");
68086
- if (!existsSync52(path2)) return null;
68238
+ if (!existsSync53(path2)) return null;
68087
68239
  return { path: path2, markdown: await readFile48(path2, "utf-8") };
68088
68240
  }
68089
68241
  var projectDirSchema, storyboardListTool, storyboardValidateTool, storyboardGetTool, storyboardSetTool, storyboardMoveTool, storyboardReviseTool, planTool, storyboardTools;
@@ -71173,7 +71325,7 @@ init_source();
71173
71325
  // package.json
71174
71326
  var package_default = {
71175
71327
  name: "@vibeframe/cli",
71176
- version: "0.102.0",
71328
+ version: "0.103.1",
71177
71329
  description: "VibeFrame CLI - natural language video editing from the terminal",
71178
71330
  private: false,
71179
71331
  license: "MIT",
@@ -71620,7 +71772,9 @@ init_project_resolver();
71620
71772
  import { readFile as readFile12, writeFile as writeFile12 } from "node:fs/promises";
71621
71773
  import { existsSync as existsSync10 } from "node:fs";
71622
71774
  import { resolve as resolve16, basename as basename3, extname } from "node:path";
71623
- var timelineCommand = new Command("timeline").description("Low-level timeline JSON commands").addHelpText("after", `
71775
+ var timelineCommand = new Command("timeline").description("Low-level timeline JSON commands").addHelpText(
71776
+ "after",
71777
+ `
71624
71778
  Examples:
71625
71779
  $ vibe timeline create my-video # my-video/timeline.json
71626
71780
  $ vibe timeline add-source my-video video.mp4 # Returns source ID
@@ -71632,7 +71786,8 @@ Examples:
71632
71786
 
71633
71787
  Typical workflow: create \u2192 add-source \u2192 add-clip \u2192 trim-clip/split-clip \u2192 export
71634
71788
  Cost: Free (no API keys needed).
71635
- Run 'vibe schema timeline.<command>' for structured parameter info.`);
71789
+ Run 'vibe schema timeline.<command>' for structured parameter info.`
71790
+ );
71636
71791
  timelineCommand.command("create").description("Create a low-level timeline JSON file").argument("<name>", "Timeline name or path (e.g., 'my-video' or 'output/my-video')").option("-o, --output <path>", "Output file path (overrides name-based path)").option("-r, --ratio <ratio>", "Aspect ratio (16:9, 9:16, 1:1, 4:5)", "16:9").option("--fps <fps>", "Frame rate", "30").option("--dry-run", "Preview parameters without executing").action(async (name, options) => {
71637
71792
  await executeTimelineCreate(name, options, "timeline create", Date.now());
71638
71793
  });
@@ -71843,7 +71998,10 @@ timelineCommand.command("add-track").description("Add a new track").argument("<p
71843
71998
  exitWithError(generalError(`Failed to add track: ${msg}`));
71844
71999
  }
71845
72000
  });
71846
- timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Timeline file or directory").argument("<clip-id>", "Clip ID").argument("<effect-type>", "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)").option("--start <seconds>", "Effect start time (relative to clip)", "0").option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)").option("--params <json>", "Effect parameters as JSON", "{}").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, effectType, options) => {
72001
+ timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Timeline file or directory").argument("<clip-id>", "Clip ID").argument(
72002
+ "<effect-type>",
72003
+ "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)"
72004
+ ).option("--start <seconds>", "Effect start time (relative to clip)", "0").option("-d, --duration <seconds>", "Effect duration (defaults to clip duration)").option("--params <json>", "Effect parameters as JSON", "{}").option("--dry-run", "Preview parameters without executing").action(async (projectPath, clipId, effectType, options) => {
71847
72005
  const startedAt = Date.now();
71848
72006
  const spinner2 = ora("Adding effect...").start();
71849
72007
  try {
@@ -72067,7 +72225,9 @@ timelineCommand.command("list").description("List timeline contents").argument("
72067
72225
  for (const clip of clips) {
72068
72226
  const source = project.getSource(clip.sourceId);
72069
72227
  console.log(` ${source_default.yellow(clip.id)}`);
72070
- console.log(` ${source?.name || "unknown"} @ ${clip.startTime}s (${clip.duration}s)`);
72228
+ console.log(
72229
+ ` ${source?.name || "unknown"} @ ${clip.startTime}s (${clip.duration}s)`
72230
+ );
72071
72231
  if (clip.effects.length > 0) {
72072
72232
  console.log(` Effects: ${clip.effects.map((e) => e.type).join(", ")}`);
72073
72233
  }
@@ -72327,10 +72487,10 @@ function detectMediaType(path2) {
72327
72487
  return "video";
72328
72488
  }
72329
72489
  applyTiers(timelineCommand, {
72330
- "create": "free",
72331
- "info": "free",
72332
- "set": "free",
72333
- "list": "free",
72490
+ create: "free",
72491
+ info: "free",
72492
+ set: "free",
72493
+ list: "free",
72334
72494
  "add-source": "free",
72335
72495
  "add-clip": "free",
72336
72496
  "add-track": "free",
@@ -79933,7 +80093,7 @@ starting from a media file and wants it transformed, it's REMIX.
79933
80093
  | Add static captions | \`vibe edit caption in.mp4 -o out.mp4\` |
79934
80094
  | Translate audio (transcribe \u2192 TTS) | \`vibe audio dub file.mp4 -t ko\` |
79935
80095
  | Transcribe (Whisper, word-level) | \`vibe audio transcribe file.mp4 --granularity word\` |
79936
- | Inspect a video | \`vibe inspect video file.mp4 "summarise"\` |
80096
+ | Inspect media with Gemini | \`vibe inspect media file.mp4 "summarise" --json\` |
79937
80097
 
79938
80098
  ### Compose pipelines (Video as Code)
79939
80099
 
@@ -79946,10 +80106,10 @@ starting from a media file and wants it transformed, it's REMIX.
79946
80106
 
79947
80107
  | Tier | Examples | Approx. per call |
79948
80108
  |---|---|---|
79949
- | **Free** | \`detect *\`, \`edit silence-cut/fade/noise-reduce\`, \`schema\`, \`project\`, \`timeline\` | $0 |
79950
- | **Low** | \`inspect *\`, \`audio transcribe\`, \`generate image\` | $0.01\u2013$0.10 |
79951
- | **High** | \`generate video\`, \`edit image\` | $1\u2013$5 |
79952
- | **Very High** | \`remix *\` (highlights, auto-shorts, regenerate-scene), \`build\` | $5\u2013$50+ |
80109
+ | **Free** | \`schema/context/doctor\`, \`detect *\`, \`status *\`, \`plan\`, \`storyboard validate\`, \`inspect project/render --cheap\`, deterministic edits | $0 |
80110
+ | **Low** | \`generate narration/sound-effect/music\`, \`audio transcribe\`, \`inspect media\`, optional AI review | $0.01\u2013$0.10 |
80111
+ | **High** | \`generate image/motion\`, \`edit image/reframe/grade/speed-ramp\` | $1\u2013$5 |
80112
+ | **Very High** | \`generate video\`, \`edit fill-gaps\`, \`remix highlights/auto-shorts\`, \`build\` with generated assets | $5\u2013$50+ |
79953
80113
 
79954
80114
  ## Agent invariants
79955
80115
 
@@ -80122,7 +80282,7 @@ defaults:
80122
80282
  # Optional \u2014 uncomment to set per-primitive provider preferences.
80123
80283
  # providers:
80124
80284
  # tts: elevenlabs # auto | elevenlabs | kokoro
80125
- # image: openai # openai | gemini
80285
+ # image: openai # openai | gemini | grok
80126
80286
  # music: elevenlabs
80127
80287
 
80128
80288
  # Optional \u2014 cap total spend per pipeline run.
@@ -80735,11 +80895,12 @@ import { resolve as resolve58 } from "node:path";
80735
80895
  var VALID_STAGES = ["assets", "compose", "sync", "render", "all"];
80736
80896
  var VALID_MODES = ["agent", "batch", "auto"];
80737
80897
  var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "kokoro"];
80898
+ var VALID_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
80738
80899
  var VALID_VIDEO_PROVIDERS = ["seedance", "grok", "kling", "runway", "veo"];
80739
80900
  var VALID_MUSIC_PROVIDERS = ["elevenlabs", "replicate"];
80740
80901
  var VALID_COMPOSERS = ["claude", "openai", "gemini"];
80741
80902
  var VALID_IMAGE_QUALITIES = ["standard", "hd"];
80742
- var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", "Image provider: openai").option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
80903
+ var planCommand = new Command("plan").description("Read STORYBOARD.md and show build plan, costs, missing cues, and provider needs").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Stage to plan: ${VALID_STAGES.join("|")}`, "all").option("--beat <id>", "Restrict the plan to one beat").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--skip-narration", "Don't include narration generation in the plan").option("--skip-backdrop", "Don't include backdrop image generation in the plan").option("--skip-video", "Don't include video generation in the plan").option("--skip-music", "Don't include music generation in the plan").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS.join("|")}`).option("--quality <q>", "Image quality: standard|hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--force", "Plan regeneration even when outputs already exist").option("--max-cost <usd>", "Fail if estimated cost exceeds this USD cap").action(async (projectDirArg, options) => {
80743
80904
  const startedAt = Date.now();
80744
80905
  const stage = String(options.stage ?? "all");
80745
80906
  if (!VALID_STAGES.includes(stage)) {
@@ -80762,6 +80923,11 @@ var planCommand = new Command("plan").description("Read STORYBOARD.md and show b
80762
80923
  VALID_MUSIC_PROVIDERS,
80763
80924
  "music"
80764
80925
  );
80926
+ const imageProvider = parseOptionalProvider(
80927
+ options.imageProvider,
80928
+ VALID_IMAGE_PROVIDERS,
80929
+ "image"
80930
+ );
80765
80931
  const composer = parseOptionalProvider(options.composer, VALID_COMPOSERS, "composer");
80766
80932
  const imageQuality = parseOptionalProvider(options.quality, VALID_IMAGE_QUALITIES, "quality");
80767
80933
  const maxCost = options.maxCost !== void 0 ? Number.parseFloat(String(options.maxCost)) : void 0;
@@ -80785,7 +80951,7 @@ var planCommand = new Command("plan").description("Read STORYBOARD.md and show b
80785
80951
  skipMusic: options.skipMusic,
80786
80952
  ttsProvider,
80787
80953
  voice: options.voice,
80788
- imageProvider: options.imageProvider,
80954
+ imageProvider,
80789
80955
  imageQuality,
80790
80956
  imageSize: options.imageSize,
80791
80957
  videoProvider,
@@ -80922,9 +81088,10 @@ import { resolve as resolve63, join as join23 } from "node:path";
80922
81088
  import { readFileSync as readFileSync3, existsSync as existsSync47 } from "node:fs";
80923
81089
  var VALID_MODES2 = ["agent", "batch", "auto"];
80924
81090
  var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
81091
+ var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
80925
81092
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
80926
81093
  var VALID_MUSIC_PROVIDERS2 = ["elevenlabs", "replicate"];
80927
- var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").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-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", "Image provider: openai").option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).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").addHelpText(
81094
+ var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").option("--stage <stage>", `Build stage: ${VALID_STAGES2.join("|")}`, "all").option("--beat <id>", "Restrict asset/compose work to one beat id").option("--mode <mode>", "Build mode: agent|batch|auto", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "Batch composer: claude|openai|gemini").option("--max-cost <usd>", "Fail before provider spend when estimated cost exceeds this USD cap").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-video", "Don't dispatch video generation even when beats declare video cues").option("--skip-music", "Don't dispatch music generation even when beats declare music cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", `Image provider: ${VALID_IMAGE_PROVIDERS2.join("|")}`).option("--video-provider <name>", `Video provider: ${VALID_VIDEO_PROVIDERS2.join("|")}`).option("--music-provider <name>", `Music provider: ${VALID_MUSIC_PROVIDERS2.join("|")}`).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").addHelpText(
80928
81095
  "after",
80929
81096
  `
80930
81097
  Examples:
@@ -80967,6 +81134,11 @@ Advanced equivalent: \`vibe scene build\`.`
80967
81134
  VALID_MUSIC_PROVIDERS2,
80968
81135
  "music"
80969
81136
  );
81137
+ const imageProvider = parseOptionalProvider2(
81138
+ options.imageProvider,
81139
+ VALID_IMAGE_PROVIDERS2,
81140
+ "image"
81141
+ );
80970
81142
  const params = {
80971
81143
  projectDir,
80972
81144
  stage,
@@ -80982,7 +81154,7 @@ Advanced equivalent: \`vibe scene build\`.`
80982
81154
  skipRender: options.skipRender ?? false,
80983
81155
  ttsProvider: options.tts,
80984
81156
  voice: options.voice,
80985
- imageProvider: options.imageProvider,
81157
+ imageProvider,
80986
81158
  videoProvider,
80987
81159
  musicProvider,
80988
81160
  imageQuality: options.quality,
@@ -81001,7 +81173,7 @@ Advanced equivalent: \`vibe scene build\`.`
81001
81173
  skipMusic: options.skipMusic,
81002
81174
  ttsProvider: options.tts,
81003
81175
  voice: options.voice,
81004
- imageProvider: options.imageProvider,
81176
+ imageProvider,
81005
81177
  imageQuality: options.quality,
81006
81178
  imageSize: options.imageSize,
81007
81179
  videoProvider,
@@ -81092,7 +81264,7 @@ Advanced equivalent: \`vibe scene build\`.`
81092
81264
  skipRender: options.skipRender,
81093
81265
  ttsProvider: options.tts,
81094
81266
  voice: options.voice,
81095
- imageProvider: options.imageProvider,
81267
+ imageProvider,
81096
81268
  videoProvider,
81097
81269
  musicProvider,
81098
81270
  imageQuality: options.quality,
@@ -81813,7 +81985,7 @@ async function runDiagnostics() {
81813
81985
  installed = false;
81814
81986
  }
81815
81987
  } else if (toolName === "chrome") {
81816
- const { existsSync: existsSync53 } = await import("node:fs");
81988
+ const { existsSync: existsSync54 } = await import("node:fs");
81817
81989
  const { homedir: homedir7 } = await import("node:os");
81818
81990
  const { join: join26 } = await import("node:path");
81819
81991
  const chromePaths = [
@@ -81826,7 +81998,7 @@ async function runDiagnostics() {
81826
81998
  "/usr/bin/chromium",
81827
81999
  "/usr/bin/chromium-browser"
81828
82000
  ];
81829
- installed = chromePaths.some((p) => p && existsSync53(p));
82001
+ installed = chromePaths.some((p) => p && existsSync54(p));
81830
82002
  } else {
81831
82003
  installed = commandExists(toolName);
81832
82004
  }
@@ -81842,28 +82014,33 @@ async function runDiagnostics() {
81842
82014
  configExists = true;
81843
82015
  } catch {
81844
82016
  }
82017
+ const cwd = process.cwd();
82018
+ const activeConfig = await loadConfig({ cwd });
81845
82019
  const providers = {};
81846
82020
  let readyCount = FREE_COMMANDS.length;
81847
82021
  let totalCount = FREE_COMMANDS.length;
81848
82022
  for (const [envVar, commands] of Object.entries(COMMAND_KEY_MAP)) {
82023
+ const providerName = Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ?? envVar;
82024
+ const configuredInConfig = Boolean(
82025
+ activeConfig?.providers[providerName]
82026
+ );
81849
82027
  const configured = Boolean(
81850
- process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean)
82028
+ process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean) || configuredInConfig
81851
82029
  );
81852
- const providerName = Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ?? envVar;
81853
82030
  providers[providerName] = { envVar, configured, commands };
81854
82031
  totalCount += commands.length;
81855
82032
  if (configured) {
81856
82033
  readyCount += commands.length;
81857
82034
  }
81858
82035
  }
81859
- const cwd = process.cwd();
81860
82036
  const projectFiles = ["AGENTS.md", "CLAUDE.md", "vibe.project.yaml"].map((rel) => ({
81861
82037
  path: rel,
81862
82038
  exists: existsSync48(resolve66(cwd, rel))
81863
82039
  }));
81864
82040
  const projectInitialized = projectFiles.some((f) => f.exists);
81865
- const projectConfigPath = getProjectConfigPath(cwd);
81866
- const projectConfigExists = existsSync48(projectConfigPath);
82041
+ const nearestProjectConfigPath = await findProjectConfigPath(cwd);
82042
+ const projectConfigPath = nearestProjectConfigPath ?? getProjectConfigPath(cwd);
82043
+ const projectConfigExists = Boolean(nearestProjectConfigPath);
81867
82044
  const activeScope = await getActiveScope(cwd);
81868
82045
  const hosts = detectAgentHosts();
81869
82046
  const detectedNames = hosts.filter((h) => h.detected).map((h) => h.label);
@@ -82136,8 +82313,9 @@ async function runLiveKeyTests(results) {
82136
82313
  return;
82137
82314
  }
82138
82315
  const { testKey: testKey2 } = await Promise.resolve().then(() => (init_key_live_test(), key_live_test_exports));
82316
+ const activeConfig = await loadConfig({ cwd: process.cwd() });
82139
82317
  for (const [name, info] of configured) {
82140
- const value = process.env[info.envVar] || PROVIDER_ENV_ALIASES[info.envVar]?.map((alias) => process.env[alias]).find(Boolean);
82318
+ const value = process.env[info.envVar] || PROVIDER_ENV_ALIASES[info.envVar]?.map((alias) => process.env[alias]).find(Boolean) || activeConfig?.providers[name];
82141
82319
  if (!value) continue;
82142
82320
  process.stdout.write(` ${name.padEnd(12)} `);
82143
82321
  const result = await testKey2(name, value);
@@ -82378,11 +82556,21 @@ var demoCommand = new Command("demo").description("Run sample edits on a test vi
82378
82556
 
82379
82557
  // src/commands/context.ts
82380
82558
  init_esm();
82559
+ import { existsSync as existsSync50 } from "node:fs";
82381
82560
  import { readFile as readFile44 } from "node:fs/promises";
82382
82561
  import { resolve as resolve68, dirname as dirname23 } from "node:path";
82383
82562
  import { fileURLToPath as fileURLToPath2 } from "node:url";
82384
82563
  var __filename = fileURLToPath2(import.meta.url);
82385
82564
  var __dirname = dirname23(__filename);
82565
+ function resolveContextPath(baseDir = __dirname) {
82566
+ const candidates = [
82567
+ // Bundled package layout: packages/cli/dist/index.js -> packages/cli/CONTEXT.md
82568
+ resolve68(baseDir, "../CONTEXT.md"),
82569
+ // Source/dev layout: packages/cli/src/commands/context.ts -> packages/cli/CONTEXT.md
82570
+ resolve68(baseDir, "../../CONTEXT.md")
82571
+ ];
82572
+ return candidates.find((candidate) => existsSync50(candidate)) ?? null;
82573
+ }
82386
82574
  var contextCommand = new Command("context").description("Print CLI context/guidelines for AI agent integration").option("--format <format>", "Output format: markdown | json", "markdown").action(async (_options, cmd) => {
82387
82575
  const options = {
82388
82576
  json: cmd.parent?.opts()?.json || cmd.opts()?.json || cmd.opts()?.format === "json",
@@ -82468,8 +82656,9 @@ var contextCommand = new Command("context").description("Print CLI context/guide
82468
82656
  console.log(JSON.stringify(contract, null, 2));
82469
82657
  return;
82470
82658
  }
82471
- const contextPath = resolve68(__dirname, "../CONTEXT.md");
82472
82659
  try {
82660
+ const contextPath = resolveContextPath();
82661
+ if (!contextPath) throw new Error("CONTEXT.md not found");
82473
82662
  const content = await readFile44(contextPath, "utf-8");
82474
82663
  console.log(
82475
82664
  content.replace(
@@ -82561,7 +82750,7 @@ Use 'vibe schema <command> --json' to get parameter schemas.
82561
82750
  Use 'vibe doctor --json' to check configured API keys.
82562
82751
  Use '--dry-run --json' before any mutating/costly operation.
82563
82752
 
82564
- Cost tiers: Free (detect, edit silence-cut/fade/noise-reduce, project, timeline) | Low (inspect, audio transcribe, generate image) | High (generate video, edit image) | Very High (remix highlights/auto-shorts/regenerate-scene, vibe build)
82753
+ Cost tiers: Free (schema/context/doctor/detect/status/plan/storyboard validate/inspect project/render --cheap, deterministic edits) | Low (generate narration/sound-effect/music, audio transcribe, inspect media, optional AI review) | High (generate image/motion, edit image/reframe/grade/speed-ramp) | Very High (generate video, edit fill-gaps, remix highlights/auto-shorts, build with generated assets)
82565
82754
 
82566
82755
  Group \u2192 MCP tool name: '<group>_<leaf>' (snake_case). Bare top-level (init/build/render/run) maps to bare MCP names.
82567
82756
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.102.0",
3
+ "version": "0.103.1",
4
4
  "description": "VibeFrame CLI - natural language video editing from the terminal",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -183,8 +183,8 @@
183
183
  "tsx": "^4.21.0",
184
184
  "typescript": "^5.3.3",
185
185
  "vitest": "^1.2.2",
186
- "@vibeframe/ai-providers": "0.102.0",
187
- "@vibeframe/core": "0.102.0"
186
+ "@vibeframe/ai-providers": "0.103.1",
187
+ "@vibeframe/core": "0.103.1"
188
188
  },
189
189
  "engines": {
190
190
  "node": ">=20"