@vibeframe/cli 0.84.0 → 0.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +218 -49
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -51503,22 +51503,30 @@ var init_key_live_test = __esm({
51503
51503
  signal
51504
51504
  )
51505
51505
  },
51506
- // The three below still have no cheap authenticated GET as of v0.83:
51507
- // fal lacks a documented validation endpoint, Kling auth is per-
51508
- // request HMAC-signed, ImgBB's only authenticated route is image
51509
- // upload. Surface them as `skipped` so the user knows we didn't
51510
- // blank-check.
51506
+ // The three below still have no cheap authenticated GET as of
51507
+ // 2026-04-30 (re-checked while shipping v0.85). Surface them as
51508
+ // `skipped` so the user knows we didn't blank-check; revisit when a
51509
+ // provider publishes an account/me endpoint.
51511
51510
  fal: {
51511
+ // Probed: /api/account, /v1/account, /v1/me, /queue/health, /v1/users/me,
51512
+ // /api/users/me, /v1/billing — all 404 with the standard `Authorization:
51513
+ // Key <key>` header. The platform docs reference an "Accounts API" but
51514
+ // its endpoint paths are unpublished as of this check.
51512
51515
  name: "fal.ai",
51513
- test: async () => ({ ok: false, skipped: true, message: "no auth-only endpoint published" })
51516
+ test: async () => ({ ok: false, skipped: true, message: "no auth-only endpoint (checked 2026-04-30)" })
51514
51517
  },
51515
51518
  kling: {
51519
+ // Per-request HMAC signing means we'd need to sign every probe with the
51520
+ // key's secret half plus a timestamp — possible, but the cheapest
51521
+ // signed endpoint is also a generation request. Not worth the code.
51516
51522
  name: "Kling",
51517
- test: async () => ({ ok: false, skipped: true, message: "per-request HMAC; can't test cheaply" })
51523
+ test: async () => ({ ok: false, skipped: true, message: "HMAC-signed per request (checked 2026-04-30)" })
51518
51524
  },
51519
51525
  imgbb: {
51526
+ // Only documented route is POST /1/upload. Probing it would consume an
51527
+ // upload quota even if we send a 1×1 PNG, so SKIP is the right call.
51520
51528
  name: "ImgBB",
51521
- test: async () => ({ ok: false, skipped: true, message: "only auth route is image upload" })
51529
+ test: async () => ({ ok: false, skipped: true, message: "only route is upload (checked 2026-04-30)" })
51522
51530
  }
51523
51531
  };
51524
51532
  }
@@ -52669,14 +52677,14 @@ __export(ai_audio_exports, {
52669
52677
  });
52670
52678
  import { resolve as resolve54, dirname as dirname19, basename as basename18, extname as extname12 } from "node:path";
52671
52679
  import { readFile as readFile36, writeFile as writeFile40 } from "node:fs/promises";
52672
- import { existsSync as existsSync37 } from "node:fs";
52680
+ import { existsSync as existsSync38 } from "node:fs";
52673
52681
  async function executeTranscribe(options) {
52674
52682
  const { audioPath, language, output, format, apiKey } = options;
52675
52683
  try {
52676
52684
  const key = apiKey || process.env.OPENAI_API_KEY;
52677
52685
  if (!key) return { success: false, error: "OPENAI_API_KEY required" };
52678
52686
  const absPath = resolve54(process.cwd(), audioPath);
52679
- if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
52687
+ if (!existsSync38(absPath)) return { success: false, error: `File not found: ${absPath}` };
52680
52688
  const whisper = new WhisperProvider();
52681
52689
  await whisper.initialize({ apiKey: key });
52682
52690
  const audioBuffer = await readFile36(absPath);
@@ -52709,7 +52717,7 @@ async function executeIsolate(options) {
52709
52717
  const key = apiKey || process.env.ELEVENLABS_API_KEY;
52710
52718
  if (!key) return { success: false, error: "ELEVENLABS_API_KEY required" };
52711
52719
  const absPath = resolve54(process.cwd(), audioPath);
52712
- if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
52720
+ if (!existsSync38(absPath)) return { success: false, error: `File not found: ${absPath}` };
52713
52721
  const audioBuffer = await readFile36(absPath);
52714
52722
  const elevenlabs = new ElevenLabsProvider();
52715
52723
  await elevenlabs.initialize({ apiKey: key });
@@ -52735,7 +52743,7 @@ async function executeVoiceClone(options) {
52735
52743
  const audioBuffers = [];
52736
52744
  for (const samplePath of samplePaths) {
52737
52745
  const absPath = resolve54(process.cwd(), samplePath);
52738
- if (!existsSync37(absPath)) return { success: false, error: `File not found: ${samplePath}` };
52746
+ if (!existsSync38(absPath)) return { success: false, error: `File not found: ${samplePath}` };
52739
52747
  const buffer = await readFile36(absPath);
52740
52748
  audioBuffers.push(buffer);
52741
52749
  }
@@ -52757,7 +52765,7 @@ async function executeDub(options) {
52757
52765
  const { mediaPath, language, source, voice, analyzeOnly, output } = options;
52758
52766
  try {
52759
52767
  const absPath = resolve54(process.cwd(), mediaPath);
52760
- if (!existsSync37(absPath)) return { success: false, error: `File not found: ${absPath}` };
52768
+ if (!existsSync38(absPath)) return { success: false, error: `File not found: ${absPath}` };
52761
52769
  const openaiKey = process.env.OPENAI_API_KEY;
52762
52770
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
52763
52771
  const elevenlabsKey = process.env.ELEVENLABS_API_KEY;
@@ -52885,8 +52893,8 @@ async function executeDuck(options) {
52885
52893
  if (!commandExists("ffmpeg")) return { success: false, error: "FFmpeg not found" };
52886
52894
  const absMusicPath = resolve54(process.cwd(), musicPath);
52887
52895
  const absVoicePath = resolve54(process.cwd(), voicePath);
52888
- if (!existsSync37(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
52889
- if (!existsSync37(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
52896
+ if (!existsSync38(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
52897
+ if (!existsSync38(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
52890
52898
  const defaultOutput = resolve54(dirname19(absMusicPath), `${basename18(absMusicPath, extname12(absMusicPath))}-ducked${extname12(absMusicPath)}`);
52891
52899
  const outputPath = resolve54(process.cwd(), output || defaultOutput);
52892
52900
  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`;
@@ -52900,7 +52908,7 @@ async function executeDuck(options) {
52900
52908
  "-y",
52901
52909
  outputPath
52902
52910
  ], { timeout: 12e4, maxBuffer: 50 * 1024 * 1024 });
52903
- if (!existsSync37(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
52911
+ if (!existsSync38(outputPath)) return { success: false, error: "FFmpeg failed to create output" };
52904
52912
  return { success: true, outputPath };
52905
52913
  } catch (error) {
52906
52914
  return { success: false, error: `Audio ducking failed: ${error instanceof Error ? error.message : String(error)}` };
@@ -52918,7 +52926,7 @@ var init_ai_audio = __esm({
52918
52926
  // src/pipeline/executor.ts
52919
52927
  import { resolve as resolve55 } from "node:path";
52920
52928
  import { readFile as readFile37, writeFile as writeFile41, mkdir as mkdir22 } from "node:fs/promises";
52921
- import { existsSync as existsSync38 } from "node:fs";
52929
+ import { existsSync as existsSync39 } from "node:fs";
52922
52930
  function maxCostFor(action) {
52923
52931
  const cmd = ACTION_METADATA[action]?.command;
52924
52932
  if (!cmd) return 0;
@@ -53135,7 +53143,7 @@ async function ensureActionsRegistered() {
53135
53143
  }
53136
53144
  async function loadPipeline(filePath) {
53137
53145
  const absPath = resolve55(process.cwd(), filePath);
53138
- if (!existsSync38(absPath)) {
53146
+ if (!existsSync39(absPath)) {
53139
53147
  throw new Error(`Pipeline file not found: ${absPath}`);
53140
53148
  }
53141
53149
  const content = await readFile37(absPath, "utf-8");
@@ -53160,7 +53168,7 @@ async function executePipeline(manifest2, options = {}) {
53160
53168
  const startTime = Date.now();
53161
53169
  if (options.resume) {
53162
53170
  const checkpointPath = resolve55(outputDir, CHECKPOINT_FILE);
53163
- if (existsSync38(checkpointPath)) {
53171
+ if (existsSync39(checkpointPath)) {
53164
53172
  const checkpointContent = await readFile37(checkpointPath, "utf-8");
53165
53173
  const checkpoint = (0, import_yaml8.parse)(checkpointContent);
53166
53174
  for (const cs of checkpoint.completedSteps) {
@@ -59763,7 +59771,7 @@ var init_generate2 = __esm({
59763
59771
  // src/tools/manifest/remix.ts
59764
59772
  import { writeFile as writeFile43 } from "node:fs/promises";
59765
59773
  import { tmpdir as tmpdir3 } from "node:os";
59766
- import { join as join13 } from "node:path";
59774
+ import { join as join14 } from "node:path";
59767
59775
  var pipelineHighlightsTool, pipelineAutoShortsTool, pipelineRunTool, pipelineRegenerateSceneTool, remixTools;
59768
59776
  var init_remix = __esm({
59769
59777
  "src/tools/manifest/remix.ts"() {
@@ -59871,7 +59879,7 @@ var init_remix = __esm({
59871
59879
  let resolvedPath = args.pipelinePath;
59872
59880
  let tempPath;
59873
59881
  if (args.pipelineYaml) {
59874
- tempPath = join13(tmpdir3(), `vibe-mcp-pipeline-${Date.now()}.yaml`);
59882
+ tempPath = join14(tmpdir3(), `vibe-mcp-pipeline-${Date.now()}.yaml`);
59875
59883
  await writeFile43(tempPath, args.pipelineYaml, "utf-8");
59876
59884
  resolvedPath = tempPath;
59877
59885
  }
@@ -60628,7 +60636,7 @@ __export(project_builder_exports, {
60628
60636
  resolveSourceUrl: () => resolveSourceUrl
60629
60637
  });
60630
60638
  import { mkdtemp, mkdir as mkdir24, copyFile as copyFile3, writeFile as writeFile47, rm as rm4, readdir as readdir3 } from "node:fs/promises";
60631
- import { existsSync as existsSync39 } from "node:fs";
60639
+ import { existsSync as existsSync40 } from "node:fs";
60632
60640
  import { tmpdir as tmpdir4 } from "node:os";
60633
60641
  import { createRequire } from "node:module";
60634
60642
  import * as path from "node:path";
@@ -60640,7 +60648,7 @@ async function buildTempProject(state, projectFileDir) {
60640
60648
  for (const source of state.sources) {
60641
60649
  if (copied.has(source.id)) continue;
60642
60650
  const resolved = resolveSourceUrl(source.url, projectFileDir);
60643
- if (!existsSync39(resolved)) {
60651
+ if (!existsSync40(resolved)) {
60644
60652
  throw new Error(`Source file not found: ${resolved} (from source id=${source.id})`);
60645
60653
  }
60646
60654
  const dest = path.join(assetsDir, path.basename(resolved));
@@ -61669,7 +61677,7 @@ var init_export2 = __esm({
61669
61677
 
61670
61678
  // src/tools/manifest/agent-only.ts
61671
61679
  import { readFile as readFile41, writeFile as writeFile48, readdir as readdir4, stat as stat6, access as access9, unlink as unlink4 } from "node:fs/promises";
61672
- import { resolve as resolve63, join as join15, basename as basename22, extname as extname13 } from "node:path";
61680
+ import { resolve as resolve63, join as join16, basename as basename22, extname as extname13 } from "node:path";
61673
61681
  function matchPattern(filename, pattern) {
61674
61682
  const regex2 = new RegExp(
61675
61683
  "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".") + "$",
@@ -61752,7 +61760,7 @@ var init_agent_only = __esm({
61752
61760
  const results = [];
61753
61761
  for (const entry of entries) {
61754
61762
  if (args.pattern && !matchPattern(entry.name, args.pattern)) continue;
61755
- const fullPath = join15(absPath, entry.name);
61763
+ const fullPath = join16(absPath, entry.name);
61756
61764
  const stats = await stat6(fullPath);
61757
61765
  if (entry.isDirectory()) {
61758
61766
  results.push(`[DIR] ${entry.name}/`);
@@ -61857,7 +61865,7 @@ var init_agent_only = __esm({
61857
61865
  const scanDir = async (dir) => {
61858
61866
  const entries = await readdir4(dir, { withFileTypes: true });
61859
61867
  for (const entry of entries) {
61860
- const entryPath = join15(dir, entry.name);
61868
+ const entryPath = join16(dir, entry.name);
61861
61869
  if (entry.isDirectory() && recursive) {
61862
61870
  await scanDir(entryPath);
61863
61871
  } else if (entry.isFile()) {
@@ -62757,6 +62765,10 @@ function toAgentDefinition(t) {
62757
62765
  return {
62758
62766
  name: t.name,
62759
62767
  description: t.description,
62768
+ // Propagate the manifest's cost tier so the agent's executor can
62769
+ // mandate a confirm prompt for high-spend tools without needing a
62770
+ // parallel SSOT.
62771
+ cost: t.cost,
62760
62772
  parameters: {
62761
62773
  type: "object",
62762
62774
  properties,
@@ -62825,7 +62837,7 @@ init_source();
62825
62837
  // package.json
62826
62838
  var package_default = {
62827
62839
  name: "@vibeframe/cli",
62828
- version: "0.84.0",
62840
+ version: "0.86.0",
62829
62841
  description: "VibeFrame CLI - natural language video editing from the terminal",
62830
62842
  private: false,
62831
62843
  license: "MIT",
@@ -70948,8 +70960,10 @@ init_esm();
70948
70960
  init_source();
70949
70961
  init_ora();
70950
70962
  init_scene_build();
70963
+ init_storyboard_parse();
70951
70964
  init_output();
70952
- import { resolve as resolve48 } from "node:path";
70965
+ import { resolve as resolve48, join as join13 } from "node:path";
70966
+ import { readFileSync as readFileSync2, existsSync as existsSync35 } from "node:fs";
70953
70967
  var VALID_MODES = ["agent", "batch", "auto"];
70954
70968
  var buildCommand = new Command("build").description("Build a VibeFrame video project from STORYBOARD.md").argument("[project-dir]", "Video project directory", ".").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("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro").option("--voice <id>", "Voice id").option("--image-provider <name>", "Image provider: openai").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("after", `
70955
70969
  Examples:
@@ -71043,9 +71057,53 @@ function printBuildDryRun(projectDirArg, params) {
71043
71057
  console.log(` Image: ${source_default.bold(String(params.imageProvider ?? "openai"))} ${source_default.dim(`${params.imageQuality} ${params.imageSize}`)}${params.skipBackdrop ? source_default.dim(" (skipped)") : ""}`);
71044
71058
  console.log(` Render: ${source_default.bold(params.skipRender ? "skip" : "yes")}`);
71045
71059
  console.log(` Regenerate: ${source_default.bold(params.force ? "yes" : "cache when possible")}`);
71060
+ const estimate = estimateBuildCost(params);
71061
+ if (estimate) {
71062
+ console.log();
71063
+ console.log(source_default.bold.cyan("Estimated cost (tier-derived, conservative)"));
71064
+ console.log(source_default.dim("-".repeat(60)));
71065
+ console.log(` Beats: ${source_default.bold(estimate.beats)}`);
71066
+ if (!params.skipNarration) {
71067
+ console.log(` Narration: ${source_default.cyan(`$${estimate.narrationUsd.toFixed(2)}`)} ${source_default.dim(`(${estimate.beats} \xD7 low)`)}`);
71068
+ }
71069
+ if (!params.skipBackdrop) {
71070
+ console.log(` Backdrops: ${source_default.yellow(`$${estimate.backdropUsd.toFixed(2)}`)} ${source_default.dim(`(${estimate.beats} \xD7 high)`)}`);
71071
+ }
71072
+ if (params.mode !== "agent") {
71073
+ console.log(` Compose (LLM): ${source_default.cyan(`$${estimate.composeUsd.toFixed(2)}`)} ${source_default.dim(`(${estimate.beats} \xD7 ~$0.06 batch mode)`)}`);
71074
+ } else {
71075
+ console.log(` Compose: ${source_default.dim(`$0.00 (agent mode \u2014 host LLM does composition)`)}`);
71076
+ }
71077
+ console.log(` ${source_default.bold("Total:")} ${source_default.bold(`$${estimate.totalUsd.toFixed(2)}`)}`);
71078
+ }
71046
71079
  console.log();
71047
71080
  console.log(source_default.dim("No assets, provider calls, or video files were created."));
71048
71081
  }
71082
+ function estimateBuildCost(params) {
71083
+ const storyboardPath = join13(params.projectDir, "STORYBOARD.md");
71084
+ if (!existsSync35(storyboardPath)) return null;
71085
+ let beats = 0;
71086
+ try {
71087
+ const md = readFileSync2(storyboardPath, "utf-8");
71088
+ beats = parseStoryboard(md).beats.length;
71089
+ } catch {
71090
+ return null;
71091
+ }
71092
+ if (beats === 0) return null;
71093
+ const TTS_PER_BEAT = 0.05;
71094
+ const IMAGE_PER_BEAT = 3;
71095
+ const COMPOSE_PER_BEAT_BATCH = 0.06;
71096
+ const narrationUsd = params.skipNarration ? 0 : beats * TTS_PER_BEAT;
71097
+ const backdropUsd = params.skipBackdrop ? 0 : beats * IMAGE_PER_BEAT;
71098
+ const composeUsd = params.mode === "agent" ? 0 : beats * COMPOSE_PER_BEAT_BATCH;
71099
+ return {
71100
+ beats,
71101
+ narrationUsd,
71102
+ backdropUsd,
71103
+ composeUsd,
71104
+ totalUsd: narrationUsd + backdropUsd + composeUsd
71105
+ };
71106
+ }
71049
71107
  function printNeedsAuthor(result) {
71050
71108
  console.log(source_default.cyan("Agent authoring required"));
71051
71109
  console.log();
@@ -71254,10 +71312,11 @@ init_exec_safe();
71254
71312
  init_api_key();
71255
71313
  init_agent_host_detect();
71256
71314
  init_composer_resolve();
71315
+ init_cost_tier();
71257
71316
  init_scene_build();
71258
71317
  init_output();
71259
71318
  import { access as access7 } from "node:fs/promises";
71260
- import { existsSync as existsSync35 } from "node:fs";
71319
+ import { existsSync as existsSync36 } from "node:fs";
71261
71320
  import { resolve as resolve50 } from "node:path";
71262
71321
  var COMMAND_KEY_MAP = getCommandKeyMap();
71263
71322
  var FREE_COMMANDS = [
@@ -71295,7 +71354,10 @@ Examples:
71295
71354
  });
71296
71355
  return;
71297
71356
  }
71298
- printReport(results, { verbose: Boolean(options.verbose) });
71357
+ printReport(results, {
71358
+ verbose: Boolean(options.verbose),
71359
+ program: doctorCommand.parent
71360
+ });
71299
71361
  if (options.testKeys) {
71300
71362
  await runLiveKeyTests(results);
71301
71363
  }
@@ -71376,20 +71438,20 @@ async function runDiagnostics() {
71376
71438
  installed = false;
71377
71439
  }
71378
71440
  } else if (toolName === "chrome") {
71379
- const { existsSync: existsSync40 } = await import("node:fs");
71441
+ const { existsSync: existsSync41 } = await import("node:fs");
71380
71442
  const { homedir: homedir6 } = await import("node:os");
71381
- const { join: join16 } = await import("node:path");
71443
+ const { join: join17 } = await import("node:path");
71382
71444
  const chromePaths = [
71383
71445
  process.env.HYPERFRAMES_CHROME_PATH,
71384
71446
  process.env.CHROME_PATH,
71385
- join16(homedir6(), ".cache", "puppeteer", "chrome-headless-shell", "mac_arm-147.0.7727.56", "chrome-headless-shell-mac_arm", "chrome-headless-shell"),
71447
+ join17(homedir6(), ".cache", "puppeteer", "chrome-headless-shell", "mac_arm-147.0.7727.56", "chrome-headless-shell-mac_arm", "chrome-headless-shell"),
71386
71448
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
71387
71449
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
71388
71450
  "/usr/bin/google-chrome",
71389
71451
  "/usr/bin/chromium",
71390
71452
  "/usr/bin/chromium-browser"
71391
71453
  ];
71392
- installed = chromePaths.some((p) => p && existsSync40(p));
71454
+ installed = chromePaths.some((p) => p && existsSync41(p));
71393
71455
  } else {
71394
71456
  installed = commandExists(toolName);
71395
71457
  }
@@ -71420,7 +71482,7 @@ async function runDiagnostics() {
71420
71482
  const cwd = process.cwd();
71421
71483
  const projectFiles = ["AGENTS.md", "CLAUDE.md", "vibe.project.yaml"].map((rel) => ({
71422
71484
  path: rel,
71423
- exists: existsSync35(resolve50(cwd, rel))
71485
+ exists: existsSync36(resolve50(cwd, rel))
71424
71486
  }));
71425
71487
  const projectInitialized = projectFiles.some((f) => f.exists);
71426
71488
  const hosts = detectAgentHosts();
@@ -71435,8 +71497,8 @@ async function runDiagnostics() {
71435
71497
  } catch (err) {
71436
71498
  if (!(err instanceof ComposerResolveError)) throw err;
71437
71499
  }
71438
- const sceneProjectInCwd = existsSync35(resolve50(cwd, "STORYBOARD.md"));
71439
- const skillInstalled = existsSync35(resolve50(cwd, "SKILL.md"));
71500
+ const sceneProjectInCwd = existsSync36(resolve50(cwd, "STORYBOARD.md"));
71501
+ const skillInstalled = existsSync36(resolve50(cwd, "SKILL.md"));
71440
71502
  return {
71441
71503
  system: {
71442
71504
  node: { version: nodeVersion, ok: true },
@@ -71462,7 +71524,7 @@ async function runDiagnostics() {
71462
71524
  };
71463
71525
  }
71464
71526
  function printReport(results, opts = { verbose: false }) {
71465
- const { verbose } = opts;
71527
+ const { verbose, program: program3 } = opts;
71466
71528
  console.log();
71467
71529
  console.log(source_default.bold(" System"));
71468
71530
  const nodeIcon = results.system.node.ok ? source_default.green("OK") : source_default.red("MISSING");
@@ -71599,12 +71661,40 @@ function printReport(results, opts = { verbose: false }) {
71599
71661
  console.log(
71600
71662
  source_default.dim(` Full command catalog: vibe schema --list`)
71601
71663
  );
71664
+ if (program3) {
71665
+ const tierCounts = countCostTiers(program3);
71666
+ const total = tierCounts.free + tierCounts.low + tierCounts.high + tierCounts["very-high"];
71667
+ if (total > 0) {
71668
+ const parts = [
71669
+ `${TIER_COLOR.free(`${tierCounts.free} free`)}`,
71670
+ `${TIER_COLOR.low(`${tierCounts.low} low`)}`,
71671
+ `${TIER_COLOR.high(`${tierCounts.high} high`)}`,
71672
+ `${TIER_COLOR["very-high"](`${tierCounts["very-high"]} very-high`)}`
71673
+ ];
71674
+ console.log(source_default.dim(` Cost mix:`) + ` ${parts.join(source_default.dim(", "))}`);
71675
+ }
71676
+ }
71602
71677
  const nextStep = pickNextStep(results, missing.length > 0);
71603
71678
  if (nextStep) {
71604
71679
  console.log(source_default.dim(` ${nextStep}`));
71605
71680
  }
71606
71681
  console.log();
71607
71682
  }
71683
+ function countCostTiers(program3) {
71684
+ const counts = {
71685
+ "free": 0,
71686
+ "low": 0,
71687
+ "high": 0,
71688
+ "very-high": 0
71689
+ };
71690
+ const walk = (cmd) => {
71691
+ const tier = getCostTier(cmd);
71692
+ if (tier) counts[tier]++;
71693
+ for (const child of cmd.commands) walk(child);
71694
+ };
71695
+ for (const top of program3.commands) walk(top);
71696
+ return counts;
71697
+ }
71608
71698
  async function runLiveKeyTests(results) {
71609
71699
  console.log();
71610
71700
  console.log(source_default.bold(" Live key tests"));
@@ -71657,7 +71747,7 @@ init_detect();
71657
71747
  init_ai_edit();
71658
71748
  init_output();
71659
71749
  import { resolve as resolve51 } from "node:path";
71660
- import { existsSync as existsSync36 } from "node:fs";
71750
+ import { existsSync as existsSync37 } from "node:fs";
71661
71751
  import { mkdir as mkdir21, rm as rm3 } from "node:fs/promises";
71662
71752
  var DEMO_DIR = resolve51(process.cwd(), ".vibeframe-demo");
71663
71753
  async function generateTestVideo(outputPath) {
@@ -71688,7 +71778,7 @@ async function generateTestVideo(outputPath) {
71688
71778
  "-y",
71689
71779
  outputPath
71690
71780
  ], { timeout: 3e4, maxBuffer: 50 * 1024 * 1024 });
71691
- return existsSync36(outputPath);
71781
+ return existsSync37(outputPath);
71692
71782
  } catch {
71693
71783
  try {
71694
71784
  await execSafe("ffmpeg", [
@@ -71714,7 +71804,7 @@ async function generateTestVideo(outputPath) {
71714
71804
  "-y",
71715
71805
  outputPath
71716
71806
  ], { timeout: 3e4, maxBuffer: 50 * 1024 * 1024 });
71717
- return existsSync36(outputPath);
71807
+ return existsSync37(outputPath);
71718
71808
  } catch {
71719
71809
  return false;
71720
71810
  }
@@ -72448,10 +72538,26 @@ function maskSensitiveArgs(args) {
72448
72538
  }
72449
72539
  return masked;
72450
72540
  }
72541
+ function estimateToolCost(cost) {
72542
+ if (!cost) return 0;
72543
+ switch (cost) {
72544
+ case "free":
72545
+ return 0;
72546
+ case "low":
72547
+ return 0.05;
72548
+ case "medium":
72549
+ return 0.5;
72550
+ case "high":
72551
+ return 3;
72552
+ case "very-high":
72553
+ return 25;
72554
+ }
72555
+ }
72451
72556
  var AgentExecutor = class {
72452
72557
  adapter = null;
72453
72558
  registry;
72454
72559
  memory;
72560
+ cumulativeUsd = 0;
72455
72561
  context;
72456
72562
  config;
72457
72563
  initialized = false;
@@ -72539,12 +72645,39 @@ var AgentExecutor = class {
72539
72645
  }
72540
72646
  /**
72541
72647
  * Execute a single tool
72648
+ *
72649
+ * Confirm-prompt logic:
72650
+ * - `noConfirm: true` → never prompt (automation / CI)
72651
+ * - `confirmAlways: true` → prompt for every tool call
72652
+ * - default → prompt only when the tool's
72653
+ * cost is high/very-high (the
72654
+ * `costGate`)
72655
+ *
72656
+ * The two non-trivial cases require `confirmCallback` to be set; if
72657
+ * it isn't, no prompt fires and the tool runs.
72542
72658
  */
72543
72659
  async executeTool(toolCall) {
72544
- if (this.config.confirmCallback) {
72660
+ const def = this.registry.getDefinition(toolCall.name);
72661
+ const cost = def?.cost;
72662
+ const isExpensive = cost === "high" || cost === "very-high";
72663
+ if (this.config.budgetUsd !== void 0) {
72664
+ const estimate = estimateToolCost(cost);
72665
+ if (this.cumulativeUsd + estimate > this.config.budgetUsd) {
72666
+ const remaining = (this.config.budgetUsd - this.cumulativeUsd).toFixed(2);
72667
+ return {
72668
+ toolCallId: toolCall.id,
72669
+ success: false,
72670
+ output: "",
72671
+ error: `Budget exceeded: ${toolCall.name} estimated at $${estimate.toFixed(2)} but only $${remaining} remains of $${this.config.budgetUsd.toFixed(2)} cap.`
72672
+ };
72673
+ }
72674
+ }
72675
+ const shouldConfirm = !this.config.noConfirm && this.config.confirmCallback && (this.config.confirmAlways || isExpensive);
72676
+ if (shouldConfirm && this.config.confirmCallback) {
72545
72677
  const confirmed = await this.config.confirmCallback(
72546
72678
  toolCall.name,
72547
- toolCall.arguments
72679
+ toolCall.arguments,
72680
+ cost
72548
72681
  );
72549
72682
  if (!confirmed) {
72550
72683
  return {
@@ -72560,8 +72693,18 @@ var AgentExecutor = class {
72560
72693
  this.context
72561
72694
  );
72562
72695
  result.toolCallId = toolCall.id;
72696
+ if (result.success) {
72697
+ this.cumulativeUsd += estimateToolCost(cost);
72698
+ }
72563
72699
  return result;
72564
72700
  }
72701
+ /**
72702
+ * Cumulative tier-estimated USD spent in this session. Updates after
72703
+ * every successful tool call. Useful for status output / `tools` REPL.
72704
+ */
72705
+ getCumulativeUsd() {
72706
+ return this.cumulativeUsd;
72707
+ }
72565
72708
  /**
72566
72709
  * Update context (e.g., when project changes)
72567
72710
  */
@@ -72601,7 +72744,7 @@ init_config();
72601
72744
  init_tty();
72602
72745
  init_api_key();
72603
72746
  init_output();
72604
- async function promptConfirm2(toolName, args) {
72747
+ async function promptConfirm2(toolName, args, cost) {
72605
72748
  const { createInterface: createInterface4 } = await import("node:readline");
72606
72749
  const rl = createInterface4({
72607
72750
  input: process.stdin,
@@ -72611,7 +72754,15 @@ async function promptConfirm2(toolName, args) {
72611
72754
  return new Promise((resolve64) => {
72612
72755
  const argsStr = JSON.stringify(args, null, 2);
72613
72756
  console.log();
72614
- console.log(source_default.yellow(`Execute ${source_default.bold(toolName)}?`));
72757
+ const tierColor = {
72758
+ "free": source_default.green,
72759
+ "low": source_default.cyan,
72760
+ "medium": source_default.yellow,
72761
+ "high": source_default.yellow,
72762
+ "very-high": source_default.red
72763
+ };
72764
+ const costBadge = cost ? ` ${tierColor[cost](`[${cost.toUpperCase()}]`)}` : "";
72765
+ console.log(source_default.yellow(`Execute ${source_default.bold(toolName)}?${costBadge}`));
72615
72766
  console.log(source_default.dim(argsStr));
72616
72767
  rl.question(source_default.cyan("(y/n): "), (answer) => {
72617
72768
  rl.close();
@@ -72621,7 +72772,12 @@ async function promptConfirm2(toolName, args) {
72621
72772
  }
72622
72773
  async function startAgent(options = {}) {
72623
72774
  const isNonInteractive = !!options.input;
72624
- const confirmMode = options.confirm || false;
72775
+ const confirmAlways = options.confirm || false;
72776
+ const noConfirm = options.noConfirm || false;
72777
+ const budgetUsd = options.budgetUsd ? parseFloat(options.budgetUsd) : void 0;
72778
+ if (budgetUsd !== void 0 && (Number.isNaN(budgetUsd) || budgetUsd < 0)) {
72779
+ exitWithError(generalError(`Invalid --budget-usd: ${options.budgetUsd}`, "Must be a non-negative number (e.g., --budget-usd 5)"));
72780
+ }
72625
72781
  if (!isNonInteractive && !hasTTY()) {
72626
72782
  exitWithError(generalError("Agent mode requires a terminal.", "Run 'vibe agent' directly from your terminal, or use --input <query> for non-interactive mode."));
72627
72783
  }
@@ -72658,7 +72814,13 @@ async function startAgent(options = {}) {
72658
72814
  maxTurns,
72659
72815
  verbose,
72660
72816
  projectPath: options.project,
72661
- confirmCallback: confirmMode ? promptConfirm2 : void 0
72817
+ // Always supply the callback so the cost gate (high/very-high
72818
+ // tools) can prompt even without `--confirm`. The executor
72819
+ // decides whether to invoke based on `confirmAlways` / `noConfirm`.
72820
+ confirmCallback: noConfirm ? void 0 : promptConfirm2,
72821
+ confirmAlways,
72822
+ noConfirm,
72823
+ budgetUsd
72662
72824
  });
72663
72825
  await agent.initialize();
72664
72826
  spinner2.succeed(source_default.green("Agent initialized"));
@@ -72695,9 +72857,11 @@ async function startAgent(options = {}) {
72695
72857
  console.log();
72696
72858
  console.log(source_default.dim(` v${version} \xB7 ${provider}${options.model ? ` \xB7 ${options.model}` : ""} \xB7 ${cwd}`));
72697
72859
  console.log();
72860
+ const confirmStatus = noConfirm ? source_default.dim("no-confirm") : confirmAlways ? source_default.yellow("confirm mode") : source_default.dim("cost gate (high/very-high)");
72698
72861
  const statusParts = [
72699
72862
  source_default.green(`${toolCount} tools`),
72700
- confirmMode ? source_default.yellow("confirm mode") : null,
72863
+ confirmStatus,
72864
+ budgetUsd !== void 0 ? source_default.magenta(`budget: $${budgetUsd.toFixed(2)}`) : null,
72701
72865
  options.project ? source_default.blue(`project: ${options.project}`) : null
72702
72866
  ].filter(Boolean);
72703
72867
  console.log(source_default.dim(" ") + statusParts.join(source_default.dim(" \xB7 ")));
@@ -72750,6 +72914,11 @@ async function startAgent(options = {}) {
72750
72914
  }
72751
72915
  console.log();
72752
72916
  console.log(source_default.dim(`Total: ${tools.length} tools`));
72917
+ const spent = agent.getCumulativeUsd();
72918
+ if (spent > 0 || budgetUsd !== void 0) {
72919
+ const budgetStr = budgetUsd !== void 0 ? ` / $${budgetUsd.toFixed(2)} cap` : "";
72920
+ console.log(source_default.dim(`Spent (tier-estimated): $${spent.toFixed(2)}${budgetStr}`));
72921
+ }
72753
72922
  console.log();
72754
72923
  rl.prompt();
72755
72924
  return;
@@ -72811,7 +72980,7 @@ async function startAgent(options = {}) {
72811
72980
  rl.prompt();
72812
72981
  });
72813
72982
  }
72814
- var agentCommand = new Command("agent").description("Start the AI agent with natural language interface").option("-p, --provider <provider>", "LLM provider (openai, claude, gemini, ollama, xai, openrouter)", "openai").option("-m, --model <model>", "Model to use (provider-specific)").option("--project <path>", "Timeline file or directory to load").option("-v, --verbose", "Show verbose output including tool calls").option("--max-turns <n>", "Maximum turns per request", "10").option("-i, --input <query>", "Run a single query and exit (non-interactive)").option("-c, --confirm", "Confirm before each tool execution").action(async (options) => {
72983
+ var agentCommand = new Command("agent").description("Start the AI agent with natural language interface").option("-p, --provider <provider>", "LLM provider (openai, claude, gemini, ollama, xai, openrouter)", "openai").option("-m, --model <model>", "Model to use (provider-specific)").option("--project <path>", "Timeline file or directory to load").option("-v, --verbose", "Show verbose output including tool calls").option("--max-turns <n>", "Maximum turns per request", "10").option("-i, --input <query>", "Run a single query and exit (non-interactive)").option("-c, --confirm", "Confirm before EVERY tool execution (overrides cost gate)").option("--no-confirm", "Disable all confirm prompts including the high/very-high cost gate (CI / automation)").option("--budget-usd <usd>", "Reject tool calls past this cumulative USD ceiling (tier-estimated, conservative)").action(async (options) => {
72815
72984
  await startAgent(options);
72816
72985
  });
72817
72986
  function getEnvVar(provider) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.84.0",
3
+ "version": "0.86.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.84.0",
187
- "@vibeframe/core": "0.84.0"
186
+ "@vibeframe/ai-providers": "0.86.0",
187
+ "@vibeframe/core": "0.86.0"
188
188
  },
189
189
  "engines": {
190
190
  "node": ">=20"