@vibeframe/cli 0.102.0 → 0.103.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/CONTEXT.md +4 -4
  2. package/dist/index.js +271 -100
  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
  }
@@ -34891,6 +34939,7 @@ function isActiveStatus(status) {
34891
34939
  }
34892
34940
  function providerStatusCommand(record) {
34893
34941
  if (record.jobType === "generate-video") {
34942
+ if (record.provider !== "runway" && record.provider !== "kling") return void 0;
34894
34943
  const type = record.provider === "kling" && record.providerTaskType ? ` --type ${record.providerTaskType}` : "";
34895
34944
  return `vibe generate video-status ${record.providerTaskId} -p ${record.provider}${type} --json`;
34896
34945
  }
@@ -58194,9 +58243,22 @@ function backdropCacheDescriptor(opts) {
58194
58243
  provider: opts.provider,
58195
58244
  quality: opts.quality,
58196
58245
  size: opts.size,
58246
+ ratio: opts.ratio,
58197
58247
  ext: "png"
58198
58248
  });
58199
58249
  }
58250
+ function imageRatioForSize(size) {
58251
+ switch (size) {
58252
+ case "1024x1024":
58253
+ return "1:1";
58254
+ case "1024x1536":
58255
+ return "2:3";
58256
+ case "1536x1024":
58257
+ return "3:2";
58258
+ default:
58259
+ return "16:9";
58260
+ }
58261
+ }
58200
58262
  function videoCacheDescriptor(opts) {
58201
58263
  return cacheAssetDescriptor("video", {
58202
58264
  beatId: opts.beatId,
@@ -58274,6 +58336,7 @@ async function createBuildPlan(opts) {
58274
58336
  }
58275
58337
  const storyboardMd = await readFile40(storyboardPath2, "utf-8");
58276
58338
  const validation = validateStoryboardMarkdown(storyboardMd);
58339
+ const validationIssues = [...validation.issues];
58277
58340
  const parsed = parseStoryboard(storyboardMd);
58278
58341
  let sourceBeats = parsed.beats;
58279
58342
  if (opts.beat) {
@@ -58296,6 +58359,7 @@ async function createBuildPlan(opts) {
58296
58359
  const mode = opts.mode ?? config2.config.build.mode;
58297
58360
  const imageQuality = opts.imageQuality ?? config2.config.build.imageQuality ?? "hd";
58298
58361
  const imageSize2 = opts.imageSize ?? config2.config.build.imageSize ?? "1536x1024";
58362
+ const imageRatio = imageRatioForSize(imageSize2);
58299
58363
  const needsComposer = includeCompose && mode !== "agent" && sourceBeats.some((beat) => !existsSync44(join20(projectDir, `compositions/scene-${beat.id}.html`)));
58300
58364
  const resolved = await resolvePlanProviders({
58301
58365
  projectDir,
@@ -58345,7 +58409,8 @@ async function createBuildPlan(opts) {
58345
58409
  cue: backdropPrompt,
58346
58410
  provider: resolved.image.resolved,
58347
58411
  quality: imageQuality,
58348
- size: imageSize2
58412
+ size: imageSize2,
58413
+ ratio: imageRatio
58349
58414
  }) : null;
58350
58415
  const videoCache = videoPrompt && !videoReference ? videoCacheDescriptor({
58351
58416
  beatId: beat.id,
@@ -58439,7 +58504,8 @@ async function createBuildPlan(opts) {
58439
58504
  }
58440
58505
  }
58441
58506
  if (narration?.willGenerate) noteProviderNeed(resolved.narration, "Narration");
58442
- if (backdrop?.willGenerate) noteProviderNeed(resolved.image, "Backdrop generation");
58507
+ if (backdrop?.willGenerate && isSupportedBuildImageProvider(resolved.image.resolved))
58508
+ noteProviderNeed(resolved.image, "Backdrop generation");
58443
58509
  if (video?.willGenerate) noteProviderNeed(resolved.video, "Video generation");
58444
58510
  if (music?.willGenerate) noteProviderNeed(resolved.music, "Music generation");
58445
58511
  if (!compositionExists) missing.add("compositions");
@@ -58461,23 +58527,35 @@ async function createBuildPlan(opts) {
58461
58527
  };
58462
58528
  });
58463
58529
  providerResolution.push(...providerResolutionsForPlan(resolved, beats, opts, includeAssets));
58530
+ if (includeAssets && !opts.skipBackdrop && !isSupportedBuildImageProvider(resolved.image.resolved)) {
58531
+ const unsupportedBackdropBeats = beats.filter(
58532
+ (beat) => isBuildGeneratedBackdropPlan(beat.assets.backdrop)
58533
+ );
58534
+ for (const beat of unsupportedBackdropBeats) {
58535
+ validationIssues.push({
58536
+ severity: "error",
58537
+ code: "UNSUPPORTED_BUILD_IMAGE_PROVIDER",
58538
+ beatId: beat.id,
58539
+ 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.`
58540
+ });
58541
+ }
58542
+ if (unsupportedBackdropBeats.length > 0) {
58543
+ const beatFlag = opts.beat ? ` --beat ${opts.beat}` : "";
58544
+ retryWith.push(
58545
+ `vibe build ${projectDir}${beatFlag} --stage assets --image-provider openai --json`
58546
+ );
58547
+ }
58548
+ }
58549
+ const validationOk = !validationIssues.some((issue) => issue.severity === "error");
58464
58550
  if (!existsSync44(join20(projectDir, config2.config.composition.entry))) {
58465
58551
  missing.add("root-composition");
58466
- if (validation.ok) retryWith.push(`vibe build ${projectDir} --stage sync --json`);
58552
+ if (validationOk) retryWith.push(`vibe build ${projectDir} --stage sync --json`);
58467
58553
  }
58468
58554
  if (config2.legacy) {
58469
58555
  warnings.push(
58470
58556
  `Using legacy ${config2.source}; write ${projectDir}/vibe.config.json to use the TO-BE project contract.`
58471
58557
  );
58472
58558
  }
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
58559
  if (!validation.ok) {
58482
58560
  retryWith.push(
58483
58561
  `vibe storyboard validate ${projectDir} --json`,
@@ -58488,7 +58566,7 @@ async function createBuildPlan(opts) {
58488
58566
  projectDir,
58489
58567
  config: config2,
58490
58568
  stage,
58491
- status: validation.ok ? "ready" : "invalid",
58569
+ status: validationOk ? "ready" : "invalid",
58492
58570
  currentStage: stage,
58493
58571
  mode,
58494
58572
  beat: opts.beat ?? null,
@@ -58500,8 +58578,8 @@ async function createBuildPlan(opts) {
58500
58578
  warnings,
58501
58579
  retryWith,
58502
58580
  validation: {
58503
- ok: validation.ok,
58504
- issues: validation.issues
58581
+ ok: validationOk,
58582
+ issues: validationIssues
58505
58583
  }
58506
58584
  });
58507
58585
  }
@@ -58528,6 +58606,15 @@ function finalizeBuildPlan(plan) {
58528
58606
  retryWith: unique3(plan.retryWith)
58529
58607
  };
58530
58608
  }
58609
+ function isBuildGeneratedBackdropPlan(asset) {
58610
+ if (!asset || asset.kind !== "backdrop") return false;
58611
+ return !["referenced-asset", "invalid-reference", "stage-skipped"].includes(asset.reason);
58612
+ }
58613
+ function isSupportedBuildImageProvider(provider) {
58614
+ return SUPPORTED_BUILD_IMAGE_PROVIDERS.includes(
58615
+ provider
58616
+ );
58617
+ }
58531
58618
  function nextCommandsForPlan(plan) {
58532
58619
  if (plan.status === "invalid") return unique3(plan.retryWith);
58533
58620
  const commands = [];
@@ -58827,7 +58914,7 @@ function loadPlanEnv(projectDir) {
58827
58914
  dir = dirname19(dir);
58828
58915
  }
58829
58916
  }
58830
- var import_dotenv3, BACKDROP_COST_USD, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD;
58917
+ var import_dotenv3, BACKDROP_COST_USD, VIDEO_COST_USD, MUSIC_COST_USD, ELEVENLABS_NARRATION_COST_USD, COMPOSE_COST_USD, SUPPORTED_BUILD_IMAGE_PROVIDERS;
58831
58918
  var init_build_plan = __esm({
58832
58919
  "src/commands/_shared/build-plan.ts"() {
58833
58920
  "use strict";
@@ -58846,6 +58933,7 @@ var init_build_plan = __esm({
58846
58933
  MUSIC_COST_USD = 0.5;
58847
58934
  ELEVENLABS_NARRATION_COST_USD = 0.05;
58848
58935
  COMPOSE_COST_USD = 0.06;
58936
+ SUPPORTED_BUILD_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
58849
58937
  }
58850
58938
  });
58851
58939
 
@@ -60252,19 +60340,18 @@ async function dispatchBackdrop(beat, ctx) {
60252
60340
  if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
60253
60341
  const prompt3 = stringOrUndefined4(beat.cues?.backdrop);
60254
60342
  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
60343
  const rel = `assets/backdrop-${beat.id}.png`;
60261
60344
  const abs = join22(ctx.projectDir, rel);
60345
+ const size = ctx.imageSize ?? "1536x1024";
60346
+ const ratio = imageRatioForSize(size);
60347
+ const metadataOptions = { quality: ctx.imageQuality, size, ratio };
60262
60348
  const cache = backdropCacheDescriptor({
60263
60349
  beatId: beat.id,
60264
60350
  cue: prompt3,
60265
60351
  provider: ctx.imageProvider,
60266
60352
  quality: ctx.imageQuality,
60267
- size: ctx.imageSize ?? "1536x1024"
60353
+ size,
60354
+ ratio
60268
60355
  });
60269
60356
  const metadataPath = assetMetadataPath("backdrop", beat.id);
60270
60357
  if (existsSync46(abs) && !ctx.force) {
@@ -60275,7 +60362,7 @@ async function dispatchBackdrop(beat, ctx) {
60275
60362
  beatId: beat.id,
60276
60363
  cue: prompt3,
60277
60364
  provider: ctx.imageProvider,
60278
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60365
+ options: metadataOptions,
60279
60366
  cacheKey: cache.key
60280
60367
  })) {
60281
60368
  ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
@@ -60300,7 +60387,7 @@ async function dispatchBackdrop(beat, ctx) {
60300
60387
  beatId: beat.id,
60301
60388
  cue: prompt3,
60302
60389
  provider: ctx.imageProvider,
60303
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60390
+ options: metadataOptions,
60304
60391
  cacheKey: cache.key,
60305
60392
  canonicalPath: rel,
60306
60393
  cachePath: cache.path
@@ -60316,27 +60403,14 @@ async function dispatchBackdrop(beat, ctx) {
60316
60403
  freshness: "fresh"
60317
60404
  };
60318
60405
  }
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";
60406
+ const generated = await generateBackdropImage(prompt3, ctx, ratio);
60407
+ if (!generated.success) {
60408
+ const error = generated.error;
60335
60409
  ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
60336
60410
  return { status: "failed", error };
60337
60411
  }
60338
60412
  await mkdir24(dirname22(abs), { recursive: true });
60339
- const buffer = Buffer.from(result.images[0].base64, "base64");
60413
+ const buffer = generated.buffer;
60340
60414
  await writeFile48(abs, buffer);
60341
60415
  await mkdir24(dirname22(cacheAbs), { recursive: true });
60342
60416
  await writeFile48(cacheAbs, buffer);
@@ -60345,8 +60419,8 @@ async function dispatchBackdrop(beat, ctx) {
60345
60419
  kind: "backdrop",
60346
60420
  beatId: beat.id,
60347
60421
  cue: prompt3,
60348
- provider: "openai",
60349
- options: { quality: ctx.imageQuality, size: ctx.imageSize ?? "1536x1024" },
60422
+ provider: ctx.imageProvider,
60423
+ options: metadataOptions,
60350
60424
  cacheKey: cache.key,
60351
60425
  canonicalPath: rel,
60352
60426
  cachePath: cache.path
@@ -60355,18 +60429,75 @@ async function dispatchBackdrop(beat, ctx) {
60355
60429
  type: "backdrop-generated",
60356
60430
  beatId: beat.id,
60357
60431
  path: rel,
60358
- provider: "openai"
60432
+ provider: ctx.imageProvider
60359
60433
  });
60360
60434
  return {
60361
60435
  status: "generated",
60362
60436
  path: rel,
60363
- provider: "openai",
60437
+ provider: ctx.imageProvider,
60364
60438
  cachePath: cache.path,
60365
60439
  cacheKey: cache.key,
60366
60440
  metadataPath,
60367
60441
  freshness: "fresh"
60368
60442
  };
60369
60443
  }
60444
+ async function generateBackdropImage(prompt3, ctx, ratio) {
60445
+ loadSceneBuildEnv(ctx.projectDir);
60446
+ const keyInfo = imageProviderKeyInfo(ctx.imageProvider);
60447
+ const apiKey = await getApiKeyFromConfig(keyInfo.configKey, { cwd: ctx.projectDir }) ?? process.env[keyInfo.envVar] ?? "";
60448
+ if (!apiKey) {
60449
+ return {
60450
+ success: false,
60451
+ error: `${keyInfo.envVar} not set \u2014 cannot dispatch backdrop with ${ctx.imageProvider}`
60452
+ };
60453
+ }
60454
+ if (ctx.imageProvider === "openai") {
60455
+ const provider2 = new OpenAIImageProvider();
60456
+ await provider2.initialize({ apiKey });
60457
+ const result2 = await provider2.generateImage(prompt3, {
60458
+ model: "gpt-image-2",
60459
+ size: ctx.imageSize,
60460
+ quality: ctx.imageQuality
60461
+ });
60462
+ return imageBufferFromResult(result2);
60463
+ }
60464
+ if (ctx.imageProvider === "gemini") {
60465
+ const provider2 = new GeminiProvider();
60466
+ await provider2.initialize({ apiKey });
60467
+ const result2 = await provider2.generateImage(prompt3, {
60468
+ model: "flash",
60469
+ aspectRatio: ratio
60470
+ });
60471
+ return imageBufferFromResult(result2);
60472
+ }
60473
+ const provider = new GrokProvider();
60474
+ await provider.initialize({ apiKey });
60475
+ const result = await provider.generateImage(prompt3, {
60476
+ aspectRatio: ratio,
60477
+ n: 1
60478
+ });
60479
+ return imageBufferFromResult(result);
60480
+ }
60481
+ async function imageBufferFromResult(result) {
60482
+ const image = result.images?.[0];
60483
+ if (!result.success || !image) {
60484
+ return { success: false, error: result.error ?? "no image data returned" };
60485
+ }
60486
+ if (image.base64) return { success: true, buffer: Buffer.from(image.base64, "base64") };
60487
+ if (image.url) {
60488
+ const response = await fetch(image.url);
60489
+ if (!response.ok) {
60490
+ return { success: false, error: `failed to download image: HTTP ${response.status}` };
60491
+ }
60492
+ return { success: true, buffer: Buffer.from(await response.arrayBuffer()) };
60493
+ }
60494
+ return { success: false, error: "no image data returned" };
60495
+ }
60496
+ function imageProviderKeyInfo(provider) {
60497
+ if (provider === "gemini") return { configKey: "google", envVar: "GOOGLE_API_KEY" };
60498
+ if (provider === "grok") return { configKey: "xai", envVar: "XAI_API_KEY" };
60499
+ return { configKey: "openai", envVar: "OPENAI_API_KEY" };
60500
+ }
60370
60501
  async function dispatchVideo(beat, ctx) {
60371
60502
  const reference = assetReferenceForBeat(ctx.projectDir, "video", beat);
60372
60503
  if (reference) return referencePrimitiveOutcome("video", beat, ctx, reference);
@@ -61265,14 +61396,14 @@ __export(ai_audio_exports, {
61265
61396
  });
61266
61397
  import { resolve as resolve69, dirname as dirname24, basename as basename22, extname as extname15 } from "node:path";
61267
61398
  import { readFile as readFile45, writeFile as writeFile49 } from "node:fs/promises";
61268
- import { existsSync as existsSync50 } from "node:fs";
61399
+ import { existsSync as existsSync51 } from "node:fs";
61269
61400
  async function executeTranscribe(options) {
61270
61401
  const { audioPath, language, output, format, apiKey } = options;
61271
61402
  try {
61272
61403
  const key = apiKey || process.env.OPENAI_API_KEY;
61273
61404
  if (!key) return { success: false, error: "OPENAI_API_KEY required" };
61274
61405
  const absPath = resolve69(process.cwd(), audioPath);
61275
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61406
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61276
61407
  const whisper = new WhisperProvider();
61277
61408
  await whisper.initialize({ apiKey: key });
61278
61409
  const audioBuffer = await readFile45(absPath);
@@ -61305,7 +61436,7 @@ async function executeIsolate(options) {
61305
61436
  const key = apiKey || process.env.ELEVENLABS_API_KEY;
61306
61437
  if (!key) return { success: false, error: "ELEVENLABS_API_KEY required" };
61307
61438
  const absPath = resolve69(process.cwd(), audioPath);
61308
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61439
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61309
61440
  const audioBuffer = await readFile45(absPath);
61310
61441
  const elevenlabs = new ElevenLabsProvider();
61311
61442
  await elevenlabs.initialize({ apiKey: key });
@@ -61331,7 +61462,7 @@ async function executeVoiceClone(options) {
61331
61462
  const audioBuffers = [];
61332
61463
  for (const samplePath of samplePaths) {
61333
61464
  const absPath = resolve69(process.cwd(), samplePath);
61334
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${samplePath}` };
61465
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${samplePath}` };
61335
61466
  const buffer = await readFile45(absPath);
61336
61467
  audioBuffers.push(buffer);
61337
61468
  }
@@ -61353,7 +61484,7 @@ async function executeDub(options) {
61353
61484
  const { mediaPath, language, source, voice, analyzeOnly, output } = options;
61354
61485
  try {
61355
61486
  const absPath = resolve69(process.cwd(), mediaPath);
61356
- if (!existsSync50(absPath)) return { success: false, error: `File not found: ${absPath}` };
61487
+ if (!existsSync51(absPath)) return { success: false, error: `File not found: ${absPath}` };
61357
61488
  const openaiKey = process.env.OPENAI_API_KEY;
61358
61489
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
61359
61490
  const elevenlabsKey = process.env.ELEVENLABS_API_KEY;
@@ -61481,8 +61612,8 @@ async function executeDuck(options) {
61481
61612
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
61482
61613
  const absMusicPath = resolve69(process.cwd(), musicPath);
61483
61614
  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}` };
61615
+ if (!existsSync51(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
61616
+ if (!existsSync51(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
61486
61617
  const defaultOutput = resolve69(dirname24(absMusicPath), `${basename22(absMusicPath, extname15(absMusicPath))}-ducked${extname15(absMusicPath)}`);
61487
61618
  const outputPath = resolve69(process.cwd(), output || defaultOutput);
61488
61619
  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 +61627,7 @@ async function executeDuck(options) {
61496
61627
  "-y",
61497
61628
  outputPath
61498
61629
  ], { timeout: 12e4, maxBuffer: 50 * 1024 * 1024 });
61499
- if (!existsSync50(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
61630
+ if (!existsSync51(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
61500
61631
  return { success: true, outputPath };
61501
61632
  } catch (error) {
61502
61633
  return { success: false, error: `Audio ducking failed: ${error instanceof Error ? error.message : String(error)}` };
@@ -61514,7 +61645,7 @@ var init_ai_audio = __esm({
61514
61645
  // src/pipeline/executor.ts
61515
61646
  import { resolve as resolve70 } from "node:path";
61516
61647
  import { readFile as readFile46, writeFile as writeFile50, mkdir as mkdir26 } from "node:fs/promises";
61517
- import { existsSync as existsSync51 } from "node:fs";
61648
+ import { existsSync as existsSync52 } from "node:fs";
61518
61649
  function maxCostFor(action) {
61519
61650
  const cmd = ACTION_METADATA[action]?.command;
61520
61651
  if (!cmd) return 0;
@@ -61829,7 +61960,7 @@ async function ensureActionsRegistered() {
61829
61960
  }
61830
61961
  async function loadPipeline(filePath) {
61831
61962
  const absPath = resolve70(process.cwd(), filePath);
61832
- if (!existsSync51(absPath)) {
61963
+ if (!existsSync52(absPath)) {
61833
61964
  throw new Error(`Pipeline file not found: ${absPath}`);
61834
61965
  }
61835
61966
  const content = await readFile46(absPath, "utf-8");
@@ -61854,7 +61985,7 @@ async function executePipeline(manifest2, options = {}) {
61854
61985
  const startTime = Date.now();
61855
61986
  if (options.resume) {
61856
61987
  const checkpointPath = resolve70(outputDir, CHECKPOINT_FILE);
61857
- if (existsSync51(checkpointPath)) {
61988
+ if (existsSync52(checkpointPath)) {
61858
61989
  const checkpointContent = await readFile46(checkpointPath, "utf-8");
61859
61990
  const checkpoint = (0, import_yaml8.parse)(checkpointContent);
61860
61991
  for (const cs of checkpoint.completedSteps) {
@@ -67398,7 +67529,10 @@ async function executeSuggestEdit(options) {
67398
67529
  }
67399
67530
  return { success: true, suggestions };
67400
67531
  } catch (error) {
67401
- return { success: false, error: `Suggest failed: ${error instanceof Error ? error.message : String(error)}` };
67532
+ return {
67533
+ success: false,
67534
+ error: `Suggest failed: ${error instanceof Error ? error.message : String(error)}`
67535
+ };
67402
67536
  }
67403
67537
  }
67404
67538
  var init_ai_suggest_edit = __esm({
@@ -68078,12 +68212,12 @@ var init_generate2 = __esm({
68078
68212
  });
68079
68213
 
68080
68214
  // src/tools/manifest/storyboard.ts
68081
- import { existsSync as existsSync52 } from "node:fs";
68215
+ import { existsSync as existsSync53 } from "node:fs";
68082
68216
  import { readFile as readFile48, writeFile as writeFile52 } from "node:fs/promises";
68083
68217
  import { resolve as resolve74 } from "node:path";
68084
68218
  async function readStoryboard2(projectDir) {
68085
68219
  const path2 = resolve74(projectDir, "STORYBOARD.md");
68086
- if (!existsSync52(path2)) return null;
68220
+ if (!existsSync53(path2)) return null;
68087
68221
  return { path: path2, markdown: await readFile48(path2, "utf-8") };
68088
68222
  }
68089
68223
  var projectDirSchema, storyboardListTool, storyboardValidateTool, storyboardGetTool, storyboardSetTool, storyboardMoveTool, storyboardReviseTool, planTool, storyboardTools;
@@ -71173,7 +71307,7 @@ init_source();
71173
71307
  // package.json
71174
71308
  var package_default = {
71175
71309
  name: "@vibeframe/cli",
71176
- version: "0.102.0",
71310
+ version: "0.103.0",
71177
71311
  description: "VibeFrame CLI - natural language video editing from the terminal",
71178
71312
  private: false,
71179
71313
  license: "MIT",
@@ -71620,7 +71754,9 @@ init_project_resolver();
71620
71754
  import { readFile as readFile12, writeFile as writeFile12 } from "node:fs/promises";
71621
71755
  import { existsSync as existsSync10 } from "node:fs";
71622
71756
  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", `
71757
+ var timelineCommand = new Command("timeline").description("Low-level timeline JSON commands").addHelpText(
71758
+ "after",
71759
+ `
71624
71760
  Examples:
71625
71761
  $ vibe timeline create my-video # my-video/timeline.json
71626
71762
  $ vibe timeline add-source my-video video.mp4 # Returns source ID
@@ -71632,7 +71768,8 @@ Examples:
71632
71768
 
71633
71769
  Typical workflow: create \u2192 add-source \u2192 add-clip \u2192 trim-clip/split-clip \u2192 export
71634
71770
  Cost: Free (no API keys needed).
71635
- Run 'vibe schema timeline.<command>' for structured parameter info.`);
71771
+ Run 'vibe schema timeline.<command>' for structured parameter info.`
71772
+ );
71636
71773
  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
71774
  await executeTimelineCreate(name, options, "timeline create", Date.now());
71638
71775
  });
@@ -71843,7 +71980,10 @@ timelineCommand.command("add-track").description("Add a new track").argument("<p
71843
71980
  exitWithError(generalError(`Failed to add track: ${msg}`));
71844
71981
  }
71845
71982
  });
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) => {
71983
+ timelineCommand.command("add-effect").description("Add an effect to a clip").argument("<project>", "Timeline file or directory").argument("<clip-id>", "Clip ID").argument(
71984
+ "<effect-type>",
71985
+ "Effect type (fadeIn, fadeOut, blur, brightness, contrast, saturation, speed, volume)"
71986
+ ).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
71987
  const startedAt = Date.now();
71848
71988
  const spinner2 = ora("Adding effect...").start();
71849
71989
  try {
@@ -72067,7 +72207,9 @@ timelineCommand.command("list").description("List timeline contents").argument("
72067
72207
  for (const clip of clips) {
72068
72208
  const source = project.getSource(clip.sourceId);
72069
72209
  console.log(` ${source_default.yellow(clip.id)}`);
72070
- console.log(` ${source?.name || "unknown"} @ ${clip.startTime}s (${clip.duration}s)`);
72210
+ console.log(
72211
+ ` ${source?.name || "unknown"} @ ${clip.startTime}s (${clip.duration}s)`
72212
+ );
72071
72213
  if (clip.effects.length > 0) {
72072
72214
  console.log(` Effects: ${clip.effects.map((e) => e.type).join(", ")}`);
72073
72215
  }
@@ -72327,10 +72469,10 @@ function detectMediaType(path2) {
72327
72469
  return "video";
72328
72470
  }
72329
72471
  applyTiers(timelineCommand, {
72330
- "create": "free",
72331
- "info": "free",
72332
- "set": "free",
72333
- "list": "free",
72472
+ create: "free",
72473
+ info: "free",
72474
+ set: "free",
72475
+ list: "free",
72334
72476
  "add-source": "free",
72335
72477
  "add-clip": "free",
72336
72478
  "add-track": "free",
@@ -79933,7 +80075,7 @@ starting from a media file and wants it transformed, it's REMIX.
79933
80075
  | Add static captions | \`vibe edit caption in.mp4 -o out.mp4\` |
79934
80076
  | Translate audio (transcribe \u2192 TTS) | \`vibe audio dub file.mp4 -t ko\` |
79935
80077
  | Transcribe (Whisper, word-level) | \`vibe audio transcribe file.mp4 --granularity word\` |
79936
- | Inspect a video | \`vibe inspect video file.mp4 "summarise"\` |
80078
+ | Inspect media with Gemini | \`vibe inspect media file.mp4 "summarise" --json\` |
79937
80079
 
79938
80080
  ### Compose pipelines (Video as Code)
79939
80081
 
@@ -79946,10 +80088,10 @@ starting from a media file and wants it transformed, it's REMIX.
79946
80088
 
79947
80089
  | Tier | Examples | Approx. per call |
79948
80090
  |---|---|---|
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+ |
80091
+ | **Free** | \`schema/context/doctor\`, \`detect *\`, \`status *\`, \`plan\`, \`storyboard validate\`, \`inspect project/render --cheap\`, deterministic edits | $0 |
80092
+ | **Low** | \`generate narration/sound-effect/music\`, \`audio transcribe\`, \`inspect media\`, optional AI review | $0.01\u2013$0.10 |
80093
+ | **High** | \`generate image/motion\`, \`edit image/reframe/grade/speed-ramp\` | $1\u2013$5 |
80094
+ | **Very High** | \`generate video\`, \`edit fill-gaps\`, \`remix highlights/auto-shorts\`, \`build\` with generated assets | $5\u2013$50+ |
79953
80095
 
79954
80096
  ## Agent invariants
79955
80097
 
@@ -80122,7 +80264,7 @@ defaults:
80122
80264
  # Optional \u2014 uncomment to set per-primitive provider preferences.
80123
80265
  # providers:
80124
80266
  # tts: elevenlabs # auto | elevenlabs | kokoro
80125
- # image: openai # openai | gemini
80267
+ # image: openai # openai | gemini | grok
80126
80268
  # music: elevenlabs
80127
80269
 
80128
80270
  # Optional \u2014 cap total spend per pipeline run.
@@ -80735,11 +80877,12 @@ import { resolve as resolve58 } from "node:path";
80735
80877
  var VALID_STAGES = ["assets", "compose", "sync", "render", "all"];
80736
80878
  var VALID_MODES = ["agent", "batch", "auto"];
80737
80879
  var VALID_TTS_PROVIDERS = ["auto", "elevenlabs", "kokoro"];
80880
+ var VALID_IMAGE_PROVIDERS = ["openai", "gemini", "grok"];
80738
80881
  var VALID_VIDEO_PROVIDERS = ["seedance", "grok", "kling", "runway", "veo"];
80739
80882
  var VALID_MUSIC_PROVIDERS = ["elevenlabs", "replicate"];
80740
80883
  var VALID_COMPOSERS = ["claude", "openai", "gemini"];
80741
80884
  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) => {
80885
+ 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
80886
  const startedAt = Date.now();
80744
80887
  const stage = String(options.stage ?? "all");
80745
80888
  if (!VALID_STAGES.includes(stage)) {
@@ -80762,6 +80905,11 @@ var planCommand = new Command("plan").description("Read STORYBOARD.md and show b
80762
80905
  VALID_MUSIC_PROVIDERS,
80763
80906
  "music"
80764
80907
  );
80908
+ const imageProvider = parseOptionalProvider(
80909
+ options.imageProvider,
80910
+ VALID_IMAGE_PROVIDERS,
80911
+ "image"
80912
+ );
80765
80913
  const composer = parseOptionalProvider(options.composer, VALID_COMPOSERS, "composer");
80766
80914
  const imageQuality = parseOptionalProvider(options.quality, VALID_IMAGE_QUALITIES, "quality");
80767
80915
  const maxCost = options.maxCost !== void 0 ? Number.parseFloat(String(options.maxCost)) : void 0;
@@ -80785,7 +80933,7 @@ var planCommand = new Command("plan").description("Read STORYBOARD.md and show b
80785
80933
  skipMusic: options.skipMusic,
80786
80934
  ttsProvider,
80787
80935
  voice: options.voice,
80788
- imageProvider: options.imageProvider,
80936
+ imageProvider,
80789
80937
  imageQuality,
80790
80938
  imageSize: options.imageSize,
80791
80939
  videoProvider,
@@ -80922,9 +81070,10 @@ import { resolve as resolve63, join as join23 } from "node:path";
80922
81070
  import { readFileSync as readFileSync3, existsSync as existsSync47 } from "node:fs";
80923
81071
  var VALID_MODES2 = ["agent", "batch", "auto"];
80924
81072
  var VALID_STAGES2 = ["assets", "compose", "sync", "render", "all"];
81073
+ var VALID_IMAGE_PROVIDERS2 = ["openai", "gemini", "grok"];
80925
81074
  var VALID_VIDEO_PROVIDERS2 = ["seedance", "grok", "kling", "runway", "veo"];
80926
81075
  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(
81076
+ 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
81077
  "after",
80929
81078
  `
80930
81079
  Examples:
@@ -80967,6 +81116,11 @@ Advanced equivalent: \`vibe scene build\`.`
80967
81116
  VALID_MUSIC_PROVIDERS2,
80968
81117
  "music"
80969
81118
  );
81119
+ const imageProvider = parseOptionalProvider2(
81120
+ options.imageProvider,
81121
+ VALID_IMAGE_PROVIDERS2,
81122
+ "image"
81123
+ );
80970
81124
  const params = {
80971
81125
  projectDir,
80972
81126
  stage,
@@ -80982,7 +81136,7 @@ Advanced equivalent: \`vibe scene build\`.`
80982
81136
  skipRender: options.skipRender ?? false,
80983
81137
  ttsProvider: options.tts,
80984
81138
  voice: options.voice,
80985
- imageProvider: options.imageProvider,
81139
+ imageProvider,
80986
81140
  videoProvider,
80987
81141
  musicProvider,
80988
81142
  imageQuality: options.quality,
@@ -81001,7 +81155,7 @@ Advanced equivalent: \`vibe scene build\`.`
81001
81155
  skipMusic: options.skipMusic,
81002
81156
  ttsProvider: options.tts,
81003
81157
  voice: options.voice,
81004
- imageProvider: options.imageProvider,
81158
+ imageProvider,
81005
81159
  imageQuality: options.quality,
81006
81160
  imageSize: options.imageSize,
81007
81161
  videoProvider,
@@ -81092,7 +81246,7 @@ Advanced equivalent: \`vibe scene build\`.`
81092
81246
  skipRender: options.skipRender,
81093
81247
  ttsProvider: options.tts,
81094
81248
  voice: options.voice,
81095
- imageProvider: options.imageProvider,
81249
+ imageProvider,
81096
81250
  videoProvider,
81097
81251
  musicProvider,
81098
81252
  imageQuality: options.quality,
@@ -81813,7 +81967,7 @@ async function runDiagnostics() {
81813
81967
  installed = false;
81814
81968
  }
81815
81969
  } else if (toolName === "chrome") {
81816
- const { existsSync: existsSync53 } = await import("node:fs");
81970
+ const { existsSync: existsSync54 } = await import("node:fs");
81817
81971
  const { homedir: homedir7 } = await import("node:os");
81818
81972
  const { join: join26 } = await import("node:path");
81819
81973
  const chromePaths = [
@@ -81826,7 +81980,7 @@ async function runDiagnostics() {
81826
81980
  "/usr/bin/chromium",
81827
81981
  "/usr/bin/chromium-browser"
81828
81982
  ];
81829
- installed = chromePaths.some((p) => p && existsSync53(p));
81983
+ installed = chromePaths.some((p) => p && existsSync54(p));
81830
81984
  } else {
81831
81985
  installed = commandExists(toolName);
81832
81986
  }
@@ -81842,28 +81996,33 @@ async function runDiagnostics() {
81842
81996
  configExists = true;
81843
81997
  } catch {
81844
81998
  }
81999
+ const cwd = process.cwd();
82000
+ const activeConfig = await loadConfig({ cwd });
81845
82001
  const providers = {};
81846
82002
  let readyCount = FREE_COMMANDS.length;
81847
82003
  let totalCount = FREE_COMMANDS.length;
81848
82004
  for (const [envVar, commands] of Object.entries(COMMAND_KEY_MAP)) {
82005
+ const providerName = Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ?? envVar;
82006
+ const configuredInConfig = Boolean(
82007
+ activeConfig?.providers[providerName]
82008
+ );
81849
82009
  const configured = Boolean(
81850
- process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean)
82010
+ process.env[envVar] || PROVIDER_ENV_ALIASES[envVar]?.map((alias) => process.env[alias]).find(Boolean) || configuredInConfig
81851
82011
  );
81852
- const providerName = Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ?? envVar;
81853
82012
  providers[providerName] = { envVar, configured, commands };
81854
82013
  totalCount += commands.length;
81855
82014
  if (configured) {
81856
82015
  readyCount += commands.length;
81857
82016
  }
81858
82017
  }
81859
- const cwd = process.cwd();
81860
82018
  const projectFiles = ["AGENTS.md", "CLAUDE.md", "vibe.project.yaml"].map((rel) => ({
81861
82019
  path: rel,
81862
82020
  exists: existsSync48(resolve66(cwd, rel))
81863
82021
  }));
81864
82022
  const projectInitialized = projectFiles.some((f) => f.exists);
81865
- const projectConfigPath = getProjectConfigPath(cwd);
81866
- const projectConfigExists = existsSync48(projectConfigPath);
82023
+ const nearestProjectConfigPath = await findProjectConfigPath(cwd);
82024
+ const projectConfigPath = nearestProjectConfigPath ?? getProjectConfigPath(cwd);
82025
+ const projectConfigExists = Boolean(nearestProjectConfigPath);
81867
82026
  const activeScope = await getActiveScope(cwd);
81868
82027
  const hosts = detectAgentHosts();
81869
82028
  const detectedNames = hosts.filter((h) => h.detected).map((h) => h.label);
@@ -82136,8 +82295,9 @@ async function runLiveKeyTests(results) {
82136
82295
  return;
82137
82296
  }
82138
82297
  const { testKey: testKey2 } = await Promise.resolve().then(() => (init_key_live_test(), key_live_test_exports));
82298
+ const activeConfig = await loadConfig({ cwd: process.cwd() });
82139
82299
  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);
82300
+ const value = process.env[info.envVar] || PROVIDER_ENV_ALIASES[info.envVar]?.map((alias) => process.env[alias]).find(Boolean) || activeConfig?.providers[name];
82141
82301
  if (!value) continue;
82142
82302
  process.stdout.write(` ${name.padEnd(12)} `);
82143
82303
  const result = await testKey2(name, value);
@@ -82378,11 +82538,21 @@ var demoCommand = new Command("demo").description("Run sample edits on a test vi
82378
82538
 
82379
82539
  // src/commands/context.ts
82380
82540
  init_esm();
82541
+ import { existsSync as existsSync50 } from "node:fs";
82381
82542
  import { readFile as readFile44 } from "node:fs/promises";
82382
82543
  import { resolve as resolve68, dirname as dirname23 } from "node:path";
82383
82544
  import { fileURLToPath as fileURLToPath2 } from "node:url";
82384
82545
  var __filename = fileURLToPath2(import.meta.url);
82385
82546
  var __dirname = dirname23(__filename);
82547
+ function resolveContextPath(baseDir = __dirname) {
82548
+ const candidates = [
82549
+ // Bundled package layout: packages/cli/dist/index.js -> packages/cli/CONTEXT.md
82550
+ resolve68(baseDir, "../CONTEXT.md"),
82551
+ // Source/dev layout: packages/cli/src/commands/context.ts -> packages/cli/CONTEXT.md
82552
+ resolve68(baseDir, "../../CONTEXT.md")
82553
+ ];
82554
+ return candidates.find((candidate) => existsSync50(candidate)) ?? null;
82555
+ }
82386
82556
  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
82557
  const options = {
82388
82558
  json: cmd.parent?.opts()?.json || cmd.opts()?.json || cmd.opts()?.format === "json",
@@ -82468,8 +82638,9 @@ var contextCommand = new Command("context").description("Print CLI context/guide
82468
82638
  console.log(JSON.stringify(contract, null, 2));
82469
82639
  return;
82470
82640
  }
82471
- const contextPath = resolve68(__dirname, "../CONTEXT.md");
82472
82641
  try {
82642
+ const contextPath = resolveContextPath();
82643
+ if (!contextPath) throw new Error("CONTEXT.md not found");
82473
82644
  const content = await readFile44(contextPath, "utf-8");
82474
82645
  console.log(
82475
82646
  content.replace(
@@ -82561,7 +82732,7 @@ Use 'vibe schema <command> --json' to get parameter schemas.
82561
82732
  Use 'vibe doctor --json' to check configured API keys.
82562
82733
  Use '--dry-run --json' before any mutating/costly operation.
82563
82734
 
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)
82735
+ 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
82736
 
82566
82737
  Group \u2192 MCP tool name: '<group>_<leaf>' (snake_case). Bare top-level (init/build/render/run) maps to bare MCP names.
82567
82738
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.102.0",
3
+ "version": "0.103.0",
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.0",
187
+ "@vibeframe/core": "0.103.0"
188
188
  },
189
189
  "engines": {
190
190
  "node": ">=20"