@vibeframe/mcp-server 0.31.1 → 0.33.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 (2) hide show
  1. package/dist/index.js +404 -174
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1464,22 +1464,108 @@ async function handleTimelineToolCall(name, args) {
1464
1464
  // ../cli/src/commands/export.ts
1465
1465
  import { Command } from "commander";
1466
1466
  import { readFile as readFile2, access, stat } from "node:fs/promises";
1467
- import { resolve as resolve3, basename } from "node:path";
1467
+ import { resolve as resolve4, basename } from "node:path";
1468
1468
  import { spawn } from "node:child_process";
1469
+ import chalk2 from "chalk";
1470
+ import ora2 from "ora";
1471
+ init_exec_safe();
1472
+
1473
+ // ../cli/src/commands/output.ts
1469
1474
  import chalk from "chalk";
1470
1475
  import ora from "ora";
1471
- init_exec_safe();
1476
+ function usageError(msg, suggestion) {
1477
+ return { success: false, error: msg, code: "USAGE_ERROR", exitCode: 2 /* USAGE */, suggestion, retryable: false };
1478
+ }
1479
+ function notFoundError(path) {
1480
+ return { success: false, error: `File not found: ${path}`, code: "NOT_FOUND", exitCode: 3 /* NOT_FOUND */, retryable: false };
1481
+ }
1482
+ function generalError(msg, suggestion) {
1483
+ return { success: false, error: msg, code: "ERROR", exitCode: 1 /* GENERAL */, suggestion, retryable: false };
1484
+ }
1485
+ function exitWithError(err) {
1486
+ if (isJsonMode()) {
1487
+ console.error(JSON.stringify(err, null, 2));
1488
+ } else {
1489
+ console.error(chalk.red(`
1490
+ ${err.error}`));
1491
+ if (err.suggestion) {
1492
+ console.error(chalk.dim(` ${err.suggestion}`));
1493
+ }
1494
+ console.error();
1495
+ }
1496
+ process.exit(err.exitCode);
1497
+ }
1498
+ function isJsonMode() {
1499
+ return process.env.VIBE_JSON_OUTPUT === "1";
1500
+ }
1501
+ function isQuietMode() {
1502
+ return process.env.VIBE_QUIET_OUTPUT === "1";
1503
+ }
1504
+ function outputResult(result) {
1505
+ if (isJsonMode()) {
1506
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
1507
+ if (fields) {
1508
+ const keys = fields.split(",").map((k) => k.trim());
1509
+ const filtered = {};
1510
+ for (const key of keys) {
1511
+ if (key in result) filtered[key] = result[key];
1512
+ }
1513
+ if ("success" in result) filtered.success = result.success;
1514
+ console.log(JSON.stringify(filtered, null, 2));
1515
+ } else {
1516
+ console.log(JSON.stringify(result, null, 2));
1517
+ }
1518
+ } else if (isQuietMode()) {
1519
+ const primary = result.output ?? result.path ?? result.url ?? result.id ?? result.result;
1520
+ if (primary !== void 0) console.log(String(primary));
1521
+ }
1522
+ }
1523
+
1524
+ // ../cli/src/commands/validate.ts
1525
+ import { resolve as resolve3, relative, isAbsolute } from "node:path";
1526
+ function validateOutputPath(path, cwd = process.cwd()) {
1527
+ rejectControlChars(path);
1528
+ if (isAbsolute(path)) {
1529
+ return resolve3(path);
1530
+ }
1531
+ const resolved = resolve3(cwd, path);
1532
+ const rel = relative(cwd, resolved);
1533
+ if (rel.startsWith("..")) {
1534
+ throw new Error(
1535
+ `Output path "${path}" escapes the working directory. Use a path within "${cwd}".`
1536
+ );
1537
+ }
1538
+ return resolved;
1539
+ }
1540
+ function rejectControlChars(input) {
1541
+ if (/[\x00-\x1f\x7f-\x9f]/.test(input)) {
1542
+ throw new Error("Input contains invalid control characters.");
1543
+ }
1544
+ return input;
1545
+ }
1546
+
1547
+ // ../cli/src/commands/export.ts
1472
1548
  async function resolveProjectPath(inputPath) {
1473
- const filePath = resolve3(process.cwd(), inputPath);
1549
+ const filePath = resolve4(process.cwd(), inputPath);
1474
1550
  try {
1475
1551
  const stats = await stat(filePath);
1476
1552
  if (stats.isDirectory()) {
1477
- return resolve3(filePath, "project.vibe.json");
1553
+ return resolve4(filePath, "project.vibe.json");
1478
1554
  }
1479
1555
  } catch {
1480
1556
  }
1481
1557
  return filePath;
1482
1558
  }
1559
+ async function getMediaDuration(filePath, mediaType, defaultImageDuration = 5) {
1560
+ if (mediaType === "image") {
1561
+ return defaultImageDuration;
1562
+ }
1563
+ try {
1564
+ return await ffprobeDuration(filePath);
1565
+ } catch {
1566
+ return defaultImageDuration;
1567
+ }
1568
+ }
1483
1569
  async function checkHasAudio(filePath) {
1484
1570
  try {
1485
1571
  const { stdout } = await execSafe("ffprobe", [
@@ -1519,16 +1605,24 @@ async function runExport(projectPath, outputPath, options = {}) {
1519
1605
  message: "Project has no clips to export"
1520
1606
  };
1521
1607
  }
1522
- const finalOutputPath = resolve3(process.cwd(), outputPath);
1608
+ const finalOutputPath = resolve4(process.cwd(), outputPath);
1523
1609
  const presetSettings = getPresetSettings(preset, summary.aspectRatio);
1524
1610
  const clips = project.getClips().sort((a, b) => a.startTime - b.startTime);
1525
1611
  const sources = project.getSources();
1526
1612
  const sourceAudioMap = /* @__PURE__ */ new Map();
1613
+ const sourceActualDurationMap = /* @__PURE__ */ new Map();
1527
1614
  for (const clip of clips) {
1528
1615
  const source = sources.find((s) => s.id === clip.sourceId);
1529
1616
  if (source) {
1530
1617
  try {
1531
1618
  await access(source.url);
1619
+ if (!sourceActualDurationMap.has(source.id)) {
1620
+ try {
1621
+ const dur = await getMediaDuration(source.url, source.type);
1622
+ if (dur > 0) sourceActualDurationMap.set(source.id, dur);
1623
+ } catch {
1624
+ }
1625
+ }
1532
1626
  if (source.type === "video" && !sourceAudioMap.has(source.id)) {
1533
1627
  sourceAudioMap.set(source.id, await checkHasAudio(source.url));
1534
1628
  }
@@ -1540,7 +1634,7 @@ async function runExport(projectPath, outputPath, options = {}) {
1540
1634
  }
1541
1635
  }
1542
1636
  }
1543
- const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap);
1637
+ const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, finalOutputPath, { overwrite, format, gapFill }, sourceAudioMap, sourceActualDurationMap);
1544
1638
  await runFFmpegProcess(ffmpegPath, ffmpegArgs, () => {
1545
1639
  });
1546
1640
  return {
@@ -1560,24 +1654,37 @@ var exportCommand = new Command("export").description("Export project to video f
1560
1654
  "-p, --preset <preset>",
1561
1655
  "Quality preset (draft, standard, high, ultra)",
1562
1656
  "standard"
1563
- ).option("-y, --overwrite", "Overwrite output file if exists", false).option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").addHelpText("after", `
1657
+ ).option("-y, --overwrite", "Overwrite output file if exists", false).option("-g, --gap-fill <strategy>", "Gap filling strategy (black, extend)", "extend").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
1564
1658
  Examples:
1565
1659
  $ vibe export project.vibe.json -o output.mp4
1566
1660
  $ vibe export project.vibe.json -o output.mp4 -p high -y
1567
1661
  $ vibe export project.vibe.json -o output.webm -f webm
1568
1662
 
1569
1663
  No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
1570
- const spinner = ora("Checking FFmpeg...").start();
1664
+ const spinner = ora2("Checking FFmpeg...").start();
1571
1665
  try {
1666
+ if (options.output) {
1667
+ validateOutputPath(options.output);
1668
+ }
1669
+ if (options.dryRun) {
1670
+ outputResult({
1671
+ dryRun: true,
1672
+ command: "export",
1673
+ params: {
1674
+ project: projectPath,
1675
+ output: options.output || null,
1676
+ format: options.format,
1677
+ preset: options.preset,
1678
+ overwrite: options.overwrite,
1679
+ gapFill: options.gapFill
1680
+ }
1681
+ });
1682
+ return;
1683
+ }
1572
1684
  const ffmpegPath = await findFFmpeg();
1573
1685
  if (!ffmpegPath) {
1574
- spinner.fail(chalk.red("FFmpeg not found"));
1575
- console.error();
1576
- console.error(chalk.yellow("Please install FFmpeg:"));
1577
- console.error(chalk.dim(" macOS: brew install ffmpeg"));
1578
- console.error(chalk.dim(" Ubuntu: sudo apt install ffmpeg"));
1579
- console.error(chalk.dim(" Windows: winget install ffmpeg"));
1580
- process.exit(1);
1686
+ spinner.fail("FFmpeg not found");
1687
+ exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS), apt install ffmpeg (Linux), or winget install ffmpeg (Windows)"));
1581
1688
  }
1582
1689
  spinner.text = "Loading project...";
1583
1690
  const filePath = await resolveProjectPath(projectPath);
@@ -1586,10 +1693,10 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
1586
1693
  const project = Project.fromJSON(data);
1587
1694
  const summary = project.getSummary();
1588
1695
  if (summary.clipCount === 0) {
1589
- spinner.fail(chalk.red("Project has no clips to export"));
1590
- process.exit(1);
1696
+ spinner.fail("Project has no clips to export");
1697
+ exitWithError(usageError("Project has no clips to export"));
1591
1698
  }
1592
- const outputPath = options.output ? resolve3(process.cwd(), options.output) : resolve3(
1699
+ const outputPath = options.output ? resolve4(process.cwd(), options.output) : resolve4(
1593
1700
  process.cwd(),
1594
1701
  `${basename(projectPath, ".vibe.json")}.${options.format}`
1595
1702
  );
@@ -1598,23 +1705,31 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
1598
1705
  const sources = project.getSources();
1599
1706
  spinner.text = "Verifying source files...";
1600
1707
  const sourceAudioMap = /* @__PURE__ */ new Map();
1708
+ const sourceActualDurationMap = /* @__PURE__ */ new Map();
1601
1709
  for (const clip of clips) {
1602
1710
  const source = sources.find((s) => s.id === clip.sourceId);
1603
1711
  if (source) {
1604
1712
  try {
1605
1713
  await access(source.url);
1714
+ if (!sourceActualDurationMap.has(source.id)) {
1715
+ try {
1716
+ const dur = await getMediaDuration(source.url, source.type);
1717
+ if (dur > 0) sourceActualDurationMap.set(source.id, dur);
1718
+ } catch {
1719
+ }
1720
+ }
1606
1721
  if (source.type === "video" && !sourceAudioMap.has(source.id)) {
1607
1722
  sourceAudioMap.set(source.id, await checkHasAudio(source.url));
1608
1723
  }
1609
1724
  } catch {
1610
- spinner.fail(chalk.red(`Source file not found: ${source.url}`));
1611
- process.exit(1);
1725
+ spinner.fail(`Source file not found: ${source.url}`);
1726
+ exitWithError(notFoundError(source.url));
1612
1727
  }
1613
1728
  }
1614
1729
  }
1615
1730
  spinner.text = "Building export command...";
1616
1731
  const gapFillStrategy = options.gapFill === "black" ? "black" : "extend";
1617
- const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap);
1732
+ const ffmpegArgs = buildFFmpegArgs(clips, sources, presetSettings, outputPath, { ...options, gapFill: gapFillStrategy }, sourceAudioMap, sourceActualDurationMap);
1618
1733
  if (process.env.DEBUG) {
1619
1734
  console.log("\nFFmpeg command:");
1620
1735
  console.log("ffmpeg", ffmpegArgs.join(" "));
@@ -1624,23 +1739,18 @@ No API keys needed. Requires FFmpeg.`).action(async (projectPath, options) => {
1624
1739
  await runFFmpegProcess(ffmpegPath, ffmpegArgs, (progress) => {
1625
1740
  spinner.text = `Encoding... ${progress}%`;
1626
1741
  });
1627
- spinner.succeed(chalk.green(`Exported: ${outputPath}`));
1742
+ spinner.succeed(chalk2.green(`Exported: ${outputPath}`));
1628
1743
  console.log();
1629
- console.log(chalk.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
1630
- console.log(chalk.dim(" Clips:"), summary.clipCount);
1631
- console.log(chalk.dim(" Format:"), options.format);
1632
- console.log(chalk.dim(" Preset:"), options.preset);
1633
- console.log(chalk.dim(" Resolution:"), presetSettings.resolution);
1744
+ console.log(chalk2.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
1745
+ console.log(chalk2.dim(" Clips:"), summary.clipCount);
1746
+ console.log(chalk2.dim(" Format:"), options.format);
1747
+ console.log(chalk2.dim(" Preset:"), options.preset);
1748
+ console.log(chalk2.dim(" Resolution:"), presetSettings.resolution);
1634
1749
  console.log();
1635
1750
  } catch (error) {
1636
- spinner.fail(chalk.red("Export failed"));
1637
- if (error instanceof Error) {
1638
- console.error(chalk.red(error.message));
1639
- if (process.env.DEBUG) {
1640
- console.error(error.stack);
1641
- }
1642
- }
1643
- process.exit(1);
1751
+ spinner.fail("Export failed");
1752
+ const msg = error instanceof Error ? error.message : String(error);
1753
+ exitWithError(generalError(`Export failed: ${msg}`));
1644
1754
  }
1645
1755
  });
1646
1756
  async function findFFmpeg() {
@@ -1743,7 +1853,7 @@ function createGapFillPlans(gaps, clips, sources) {
1743
1853
  return { gap, fills };
1744
1854
  });
1745
1855
  }
1746
- function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map()) {
1856
+ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, sourceAudioMap = /* @__PURE__ */ new Map(), sourceActualDurationMap = /* @__PURE__ */ new Map()) {
1747
1857
  const args = [];
1748
1858
  if (options.overwrite) {
1749
1859
  args.push("-y");
@@ -1828,6 +1938,12 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
1828
1938
  const trimStart = clip.sourceStartOffset;
1829
1939
  const trimEnd = clip.sourceStartOffset + clip.duration;
1830
1940
  videoFilter = `[${srcIdx}:v]trim=start=${trimStart}:end=${trimEnd},setpts=PTS-STARTPTS`;
1941
+ const sourceDuration = sourceActualDurationMap.get(source.id) || source.duration || 0;
1942
+ const availableDuration = sourceDuration - clip.sourceStartOffset;
1943
+ if (availableDuration > 0 && availableDuration < clip.duration - 0.1) {
1944
+ const padDuration = clip.duration - availableDuration;
1945
+ videoFilter += `,tpad=stop_mode=clone:stop_duration=${padDuration.toFixed(3)}`;
1946
+ }
1831
1947
  }
1832
1948
  videoFilter += `,scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
1833
1949
  for (const effect of clip.effects || []) {
@@ -1862,62 +1978,95 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
1862
1978
  videoStreamIdx++;
1863
1979
  }
1864
1980
  }
1865
- const audioGaps = detectTimelineGaps(audioClips, totalDuration);
1866
- const audioSegments = [];
1981
+ const audioTrackMap = /* @__PURE__ */ new Map();
1867
1982
  for (const clip of audioClips) {
1868
- audioSegments.push({ type: "clip", clip, startTime: clip.startTime });
1869
- }
1870
- for (const gap of audioGaps) {
1871
- audioSegments.push({ type: "gap", gap, startTime: gap.start });
1983
+ const trackId = clip.trackId || "audio-track-1";
1984
+ if (!audioTrackMap.has(trackId)) {
1985
+ audioTrackMap.set(trackId, []);
1986
+ }
1987
+ audioTrackMap.get(trackId).push(clip);
1872
1988
  }
1873
- audioSegments.sort((a, b) => a.startTime - b.startTime);
1874
- const audioStreams = [];
1989
+ const videoDuration = videoClips.length > 0 ? Math.max(...videoClips.map((c) => c.startTime + c.duration)) : 0;
1990
+ const audioDuration = audioClips.length > 0 ? Math.max(...audioClips.map((c) => c.startTime + c.duration)) : 0;
1991
+ const timelineDuration = totalDuration || Math.max(videoDuration, audioDuration);
1992
+ const trackOutputLabels = [];
1875
1993
  let audioStreamIdx = 0;
1876
- for (const segment of audioSegments) {
1877
- if (segment.type === "clip" && segment.clip) {
1878
- const clip = segment.clip;
1879
- const source = sources.find((s) => s.id === clip.sourceId);
1880
- if (!source) continue;
1881
- const srcIdx = sourceMap.get(source.id);
1882
- if (srcIdx === void 0) continue;
1883
- const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
1884
- let audioFilter;
1885
- if (hasAudio) {
1886
- const audioTrimStart = clip.sourceStartOffset;
1887
- const audioTrimEnd = clip.sourceStartOffset + clip.duration;
1888
- const sourceDuration = source.duration || 0;
1889
- const clipDuration = clip.duration;
1890
- if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
1891
- const tempo = sourceDuration / clipDuration;
1892
- if (tempo <= 2) {
1893
- audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
1994
+ for (const [, trackClips] of audioTrackMap) {
1995
+ const sorted = [...trackClips].sort((a, b) => a.startTime - b.startTime);
1996
+ const trackGaps = detectTimelineGaps(sorted, timelineDuration);
1997
+ const segments = [];
1998
+ for (const clip of sorted) {
1999
+ segments.push({ type: "clip", clip, startTime: clip.startTime });
2000
+ }
2001
+ for (const gap of trackGaps) {
2002
+ segments.push({ type: "gap", gap, startTime: gap.start });
2003
+ }
2004
+ segments.sort((a, b) => a.startTime - b.startTime);
2005
+ const segmentLabels = [];
2006
+ for (const segment of segments) {
2007
+ if (segment.type === "clip" && segment.clip) {
2008
+ const clip = segment.clip;
2009
+ const source = sources.find((s) => s.id === clip.sourceId);
2010
+ if (!source) continue;
2011
+ const srcIdx = sourceMap.get(source.id);
2012
+ if (srcIdx === void 0) continue;
2013
+ const hasAudio = source.type === "audio" || sourceAudioMap.get(source.id) === true;
2014
+ let audioFilter;
2015
+ if (hasAudio) {
2016
+ const audioTrimStart = clip.sourceStartOffset;
2017
+ const audioTrimEnd = clip.sourceStartOffset + clip.duration;
2018
+ const sourceDuration = source.duration || 0;
2019
+ const clipDuration = clip.duration;
2020
+ if (source.type === "audio" && sourceDuration > clipDuration && audioTrimStart === 0) {
2021
+ const tempo = sourceDuration / clipDuration;
2022
+ if (tempo <= 2) {
2023
+ audioFilter = `[${srcIdx}:a]atempo=${tempo.toFixed(4)},asetpts=PTS-STARTPTS`;
2024
+ } else {
2025
+ audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
2026
+ }
1894
2027
  } else {
1895
2028
  audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
1896
2029
  }
1897
2030
  } else {
1898
- audioFilter = `[${srcIdx}:a]atrim=start=${audioTrimStart}:end=${audioTrimEnd},asetpts=PTS-STARTPTS`;
2031
+ audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
1899
2032
  }
1900
- } else {
1901
- audioFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${clip.duration},asetpts=PTS-STARTPTS`;
1902
- }
1903
- for (const effect of clip.effects || []) {
1904
- if (effect.type === "fadeIn") {
1905
- audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
1906
- } else if (effect.type === "fadeOut") {
1907
- const fadeStart = clip.duration - effect.duration;
1908
- audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
2033
+ const clipVolume = clip.volume;
2034
+ if (clipVolume !== void 0 && clipVolume !== 1) {
2035
+ audioFilter += `,volume=${clipVolume.toFixed(2)}`;
2036
+ }
2037
+ for (const effect of clip.effects || []) {
2038
+ if (effect.type === "fadeIn") {
2039
+ audioFilter += `,afade=t=in:st=0:d=${effect.duration}`;
2040
+ } else if (effect.type === "fadeOut") {
2041
+ const fadeStart = clip.duration - effect.duration;
2042
+ audioFilter += `,afade=t=out:st=${fadeStart}:d=${effect.duration}`;
2043
+ } else if (effect.type === "volume" && effect.params?.level !== void 0) {
2044
+ audioFilter += `,volume=${effect.params.level}`;
2045
+ }
2046
+ }
2047
+ const label = `a${audioStreamIdx}`;
2048
+ audioFilter += `[${label}]`;
2049
+ filterParts.push(audioFilter);
2050
+ segmentLabels.push(`[${label}]`);
2051
+ audioStreamIdx++;
2052
+ } else if (segment.type === "gap" && segment.gap) {
2053
+ const gapDuration = segment.gap.end - segment.gap.start;
2054
+ if (gapDuration > 1e-3) {
2055
+ const label = `a${audioStreamIdx}`;
2056
+ filterParts.push(`anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration.toFixed(4)},asetpts=PTS-STARTPTS[${label}]`);
2057
+ segmentLabels.push(`[${label}]`);
2058
+ audioStreamIdx++;
1909
2059
  }
1910
2060
  }
1911
- audioFilter += `[a${audioStreamIdx}]`;
1912
- filterParts.push(audioFilter);
1913
- audioStreams.push(`[a${audioStreamIdx}]`);
1914
- audioStreamIdx++;
1915
- } else if (segment.type === "gap" && segment.gap) {
1916
- const gapDuration = segment.gap.end - segment.gap.start;
1917
- const audioGapFilter = `anullsrc=r=48000:cl=stereo,atrim=0:${gapDuration},asetpts=PTS-STARTPTS[a${audioStreamIdx}]`;
1918
- filterParts.push(audioGapFilter);
1919
- audioStreams.push(`[a${audioStreamIdx}]`);
1920
- audioStreamIdx++;
2061
+ }
2062
+ if (segmentLabels.length > 1) {
2063
+ const trackLabel = `atrack${trackOutputLabels.length}`;
2064
+ filterParts.push(`${segmentLabels.join("")}concat=n=${segmentLabels.length}:v=0:a=1[${trackLabel}]`);
2065
+ trackOutputLabels.push(`[${trackLabel}]`);
2066
+ } else if (segmentLabels.length === 1) {
2067
+ const trackLabel = `atrack${trackOutputLabels.length}`;
2068
+ filterParts.push(`${segmentLabels[0]}acopy[${trackLabel}]`);
2069
+ trackOutputLabels.push(`[${trackLabel}]`);
1921
2070
  }
1922
2071
  }
1923
2072
  if (videoStreams.length > 1) {
@@ -1927,16 +2076,16 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
1927
2076
  } else if (videoStreams.length === 1) {
1928
2077
  filterParts.push(`${videoStreams[0]}copy[outv]`);
1929
2078
  }
1930
- if (audioStreams.length > 1) {
2079
+ if (trackOutputLabels.length > 1) {
1931
2080
  filterParts.push(
1932
- `${audioStreams.join("")}concat=n=${audioStreams.length}:v=0:a=1[outa]`
2081
+ `${trackOutputLabels.join("")}amix=inputs=${trackOutputLabels.length}:duration=longest:normalize=0[outa]`
1933
2082
  );
1934
- } else if (audioStreams.length === 1) {
1935
- filterParts.push(`${audioStreams[0]}acopy[outa]`);
2083
+ } else if (trackOutputLabels.length === 1) {
2084
+ filterParts.push(`${trackOutputLabels[0]}acopy[outa]`);
1936
2085
  }
1937
2086
  args.push("-filter_complex", filterParts.join(";"));
1938
2087
  args.push("-map", "[outv]");
1939
- if (audioStreams.length > 0) {
2088
+ if (trackOutputLabels.length > 0) {
1940
2089
  args.push("-map", "[outa]");
1941
2090
  }
1942
2091
  args.push(...presetSettings.ffmpegArgs);
@@ -1944,7 +2093,7 @@ function buildFFmpegArgs(clips, sources, presetSettings, outputPath, options, so
1944
2093
  return args;
1945
2094
  }
1946
2095
  function runFFmpegProcess(ffmpegPath, args, onProgress) {
1947
- return new Promise((resolve12, reject) => {
2096
+ return new Promise((resolve13, reject) => {
1948
2097
  const ffmpeg = spawn(ffmpegPath, args, {
1949
2098
  stdio: ["pipe", "pipe", "pipe"]
1950
2099
  });
@@ -1968,7 +2117,7 @@ function runFFmpegProcess(ffmpegPath, args, onProgress) {
1968
2117
  });
1969
2118
  ffmpeg.on("close", (code) => {
1970
2119
  if (code === 0) {
1971
- resolve12();
2120
+ resolve13();
1972
2121
  } else {
1973
2122
  const errorMatch = stderr.match(/Error.*$/m);
1974
2123
  const errorMsg = errorMatch ? errorMatch[0] : `FFmpeg exited with code ${code}`;
@@ -2108,7 +2257,7 @@ async function handleExportToolCall(name, args) {
2108
2257
  }
2109
2258
 
2110
2259
  // ../cli/src/commands/ai-edit.ts
2111
- import { resolve as resolve6, dirname, basename as basename2, extname, join as join2 } from "node:path";
2260
+ import { resolve as resolve7, dirname, basename as basename2, extname, join as join2 } from "node:path";
2112
2261
  import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir3 } from "node:fs/promises";
2113
2262
  import { existsSync as existsSync2 } from "node:fs";
2114
2263
 
@@ -2913,7 +3062,7 @@ var GeminiProvider = class {
2913
3062
  * Sleep helper
2914
3063
  */
2915
3064
  sleep(ms) {
2916
- return new Promise((resolve12) => setTimeout(resolve12, ms));
3065
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
2917
3066
  }
2918
3067
  /**
2919
3068
  * Extend a previously generated Veo video
@@ -6756,7 +6905,7 @@ var RunwayProvider = class {
6756
6905
  * Sleep helper
6757
6906
  */
6758
6907
  sleep(ms) {
6759
- return new Promise((resolve12) => setTimeout(resolve12, ms));
6908
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
6760
6909
  }
6761
6910
  };
6762
6911
  var runwayProvider = new RunwayProvider();
@@ -7172,7 +7321,7 @@ var KlingProvider = class {
7172
7321
  * Sleep helper
7173
7322
  */
7174
7323
  sleep(ms) {
7175
- return new Promise((resolve12) => setTimeout(resolve12, ms));
7324
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
7176
7325
  }
7177
7326
  };
7178
7327
  var klingProvider = new KlingProvider();
@@ -7467,7 +7616,7 @@ var GrokProvider = class {
7467
7616
  }
7468
7617
  }
7469
7618
  sleep(ms) {
7470
- return new Promise((resolve12) => setTimeout(resolve12, ms));
7619
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
7471
7620
  }
7472
7621
  };
7473
7622
  var grokProvider = new GrokProvider();
@@ -7726,7 +7875,7 @@ var ReplicateProvider = class {
7726
7875
  * Sleep helper
7727
7876
  */
7728
7877
  sleep(ms) {
7729
- return new Promise((resolve12) => setTimeout(resolve12, ms));
7878
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
7730
7879
  }
7731
7880
  /**
7732
7881
  * Generate music from text prompt using MusicGen
@@ -8110,12 +8259,12 @@ var replicateProvider = new ReplicateProvider();
8110
8259
  // ../cli/src/utils/api-key.ts
8111
8260
  import { createInterface } from "node:readline";
8112
8261
  import { readFile as readFile4, writeFile as writeFile3, access as access3 } from "node:fs/promises";
8113
- import { resolve as resolve5 } from "node:path";
8262
+ import { resolve as resolve6 } from "node:path";
8114
8263
  import { config } from "dotenv";
8115
- import chalk2 from "chalk";
8264
+ import chalk3 from "chalk";
8116
8265
 
8117
8266
  // ../cli/src/config/index.ts
8118
- import { resolve as resolve4 } from "node:path";
8267
+ import { resolve as resolve5 } from "node:path";
8119
8268
  import { homedir } from "node:os";
8120
8269
  import { readFile as readFile3, writeFile as writeFile2, mkdir, access as access2 } from "node:fs/promises";
8121
8270
  import { parse, stringify } from "yaml";
@@ -8151,8 +8300,8 @@ function createDefaultConfig() {
8151
8300
  }
8152
8301
 
8153
8302
  // ../cli/src/config/index.ts
8154
- var CONFIG_DIR = resolve4(homedir(), ".vibeframe");
8155
- var CONFIG_PATH = resolve4(CONFIG_DIR, "config.yaml");
8303
+ var CONFIG_DIR = resolve5(homedir(), ".vibeframe");
8304
+ var CONFIG_PATH = resolve5(CONFIG_DIR, "config.yaml");
8156
8305
  async function loadConfig() {
8157
8306
  try {
8158
8307
  await access2(CONFIG_PATH);
@@ -8185,20 +8334,20 @@ async function getApiKeyFromConfig(providerKey) {
8185
8334
 
8186
8335
  // ../cli/src/utils/api-key.ts
8187
8336
  function loadEnv() {
8188
- config({ path: resolve5(process.cwd(), ".env"), debug: false });
8337
+ config({ path: resolve6(process.cwd(), ".env"), debug: false, quiet: true });
8189
8338
  const monorepoRoot = findMonorepoRoot();
8190
8339
  if (monorepoRoot && monorepoRoot !== process.cwd()) {
8191
- config({ path: resolve5(monorepoRoot, ".env"), debug: false });
8340
+ config({ path: resolve6(monorepoRoot, ".env"), debug: false, quiet: true });
8192
8341
  }
8193
8342
  }
8194
8343
  function findMonorepoRoot() {
8195
8344
  let dir = process.cwd();
8196
8345
  while (dir !== "/") {
8197
8346
  try {
8198
- __require.resolve(resolve5(dir, "pnpm-workspace.yaml"));
8347
+ __require.resolve(resolve6(dir, "pnpm-workspace.yaml"));
8199
8348
  return dir;
8200
8349
  } catch {
8201
- dir = resolve5(dir, "..");
8350
+ dir = resolve6(dir, "..");
8202
8351
  }
8203
8352
  }
8204
8353
  return null;
@@ -8208,7 +8357,7 @@ async function prompt(question, hidden = false) {
8208
8357
  input: process.stdin,
8209
8358
  output: process.stdout
8210
8359
  });
8211
- return new Promise((resolve12) => {
8360
+ return new Promise((resolve13) => {
8212
8361
  if (hidden && process.stdin.isTTY) {
8213
8362
  process.stdout.write(question);
8214
8363
  let input = "";
@@ -8222,7 +8371,7 @@ async function prompt(question, hidden = false) {
8222
8371
  process.stdin.removeListener("data", onData);
8223
8372
  process.stdout.write("\n");
8224
8373
  rl.close();
8225
- resolve12(input);
8374
+ resolve13(input);
8226
8375
  } else if (char === "") {
8227
8376
  process.exit(1);
8228
8377
  } else if (char === "\x7F" || char === "\b") {
@@ -8237,7 +8386,7 @@ async function prompt(question, hidden = false) {
8237
8386
  } else {
8238
8387
  rl.question(question, (answer) => {
8239
8388
  rl.close();
8240
- resolve12(answer);
8389
+ resolve13(answer);
8241
8390
  });
8242
8391
  }
8243
8392
  });
@@ -8250,9 +8399,11 @@ async function getApiKey(envVar, providerName, optionValue) {
8250
8399
  ANTHROPIC_API_KEY: "anthropic",
8251
8400
  OPENAI_API_KEY: "openai",
8252
8401
  GOOGLE_API_KEY: "google",
8402
+ XAI_API_KEY: "xai",
8253
8403
  ELEVENLABS_API_KEY: "elevenlabs",
8254
8404
  RUNWAY_API_SECRET: "runway",
8255
8405
  KLING_API_KEY: "kling",
8406
+ OPENROUTER_API_KEY: "openrouter",
8256
8407
  IMGBB_API_KEY: "imgbb",
8257
8408
  REPLICATE_API_TOKEN: "replicate"
8258
8409
  };
@@ -8272,22 +8423,22 @@ async function getApiKey(envVar, providerName, optionValue) {
8272
8423
  return null;
8273
8424
  }
8274
8425
  console.log();
8275
- console.log(chalk2.yellow(`${providerName} API key not found.`));
8276
- console.log(chalk2.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`));
8426
+ console.log(chalk3.yellow(`${providerName} API key not found.`));
8427
+ console.log(chalk3.dim(`Set ${envVar} in .env (current directory), run 'vibe setup', or enter below.`));
8277
8428
  console.log();
8278
- const apiKey = await prompt(chalk2.cyan(`Enter ${providerName} API key: `), true);
8429
+ const apiKey = await prompt(chalk3.cyan(`Enter ${providerName} API key: `), true);
8279
8430
  if (!apiKey || apiKey.trim() === "") {
8280
8431
  return null;
8281
8432
  }
8282
- const save = await prompt(chalk2.cyan("Save to .env for future use? (y/N): "));
8433
+ const save = await prompt(chalk3.cyan("Save to .env for future use? (y/N): "));
8283
8434
  if (save.toLowerCase() === "y" || save.toLowerCase() === "yes") {
8284
8435
  await saveApiKeyToEnv(envVar, apiKey.trim());
8285
- console.log(chalk2.green("API key saved to .env"));
8436
+ console.log(chalk3.green("API key saved to .env"));
8286
8437
  }
8287
8438
  return apiKey.trim();
8288
8439
  }
8289
8440
  async function saveApiKeyToEnv(envVar, apiKey) {
8290
- const envPath = resolve5(process.cwd(), ".env");
8441
+ const envPath = resolve6(process.cwd(), ".env");
8291
8442
  let content = "";
8292
8443
  try {
8293
8444
  await access3(envPath);
@@ -9256,8 +9407,8 @@ async function applyTextOverlays(options) {
9256
9407
  if (!texts || texts.length === 0) {
9257
9408
  return { success: false, error: "No texts provided" };
9258
9409
  }
9259
- const absVideoPath = resolve6(process.cwd(), videoPath);
9260
- const absOutputPath = resolve6(process.cwd(), outputPath);
9410
+ const absVideoPath = resolve7(process.cwd(), videoPath);
9411
+ const absOutputPath = resolve7(process.cwd(), outputPath);
9261
9412
  if (!existsSync2(absVideoPath)) {
9262
9413
  return { success: false, error: `Video not found: ${absVideoPath}` };
9263
9414
  }
@@ -9628,10 +9779,10 @@ async function handleAiEditingToolCall(name, args) {
9628
9779
 
9629
9780
  // ../cli/src/commands/ai-analyze.ts
9630
9781
  import { readFile as readFile6 } from "node:fs/promises";
9631
- import { extname as extname2, resolve as resolve7 } from "node:path";
9782
+ import { extname as extname2, resolve as resolve8 } from "node:path";
9632
9783
  import { existsSync as existsSync3 } from "node:fs";
9633
- import chalk3 from "chalk";
9634
- import ora2 from "ora";
9784
+ import chalk4 from "chalk";
9785
+ import ora3 from "ora";
9635
9786
  async function executeGeminiVideo(options) {
9636
9787
  try {
9637
9788
  const apiKey = await getApiKey("GOOGLE_API_KEY", "Google");
@@ -9649,7 +9800,7 @@ async function executeGeminiVideo(options) {
9649
9800
  if (isYouTube) {
9650
9801
  videoData = options.source;
9651
9802
  } else {
9652
- const absPath = resolve7(process.cwd(), options.source);
9803
+ const absPath = resolve8(process.cwd(), options.source);
9653
9804
  if (!existsSync3(absPath)) {
9654
9805
  return { success: false, error: `File not found: ${absPath}` };
9655
9806
  }
@@ -9722,7 +9873,7 @@ async function executeAnalyze(options) {
9722
9873
  }
9723
9874
  imageBuffer = Buffer.from(await response.arrayBuffer());
9724
9875
  } else {
9725
- const absPath = resolve7(process.cwd(), source);
9876
+ const absPath = resolve8(process.cwd(), source);
9726
9877
  if (!existsSync3(absPath)) {
9727
9878
  return { success: false, error: `File not found: ${absPath}` };
9728
9879
  }
@@ -9757,7 +9908,7 @@ async function executeAnalyze(options) {
9757
9908
  }
9758
9909
  videoData = Buffer.from(await response.arrayBuffer());
9759
9910
  } else {
9760
- const absPath = resolve7(process.cwd(), source);
9911
+ const absPath = resolve8(process.cwd(), source);
9761
9912
  if (!existsSync3(absPath)) {
9762
9913
  return { success: false, error: `File not found: ${absPath}` };
9763
9914
  }
@@ -9792,10 +9943,10 @@ async function executeAnalyze(options) {
9792
9943
 
9793
9944
  // ../cli/src/commands/ai-review.ts
9794
9945
  import { readFile as readFile7, rename } from "node:fs/promises";
9795
- import { resolve as resolve8 } from "node:path";
9946
+ import { resolve as resolve9 } from "node:path";
9796
9947
  import { existsSync as existsSync4 } from "node:fs";
9797
- import chalk4 from "chalk";
9798
- import ora3 from "ora";
9948
+ import chalk5 from "chalk";
9949
+ import ora4 from "ora";
9799
9950
  init_exec_safe();
9800
9951
  function parseReviewFeedback(response) {
9801
9952
  let cleaned = response.trim();
@@ -9820,7 +9971,7 @@ function parseReviewFeedback(response) {
9820
9971
  }
9821
9972
  async function executeReview(options) {
9822
9973
  const { videoPath, storyboardPath, autoApply = false, verify = false, model = "flash" } = options;
9823
- const absVideoPath = resolve8(process.cwd(), videoPath);
9974
+ const absVideoPath = resolve9(process.cwd(), videoPath);
9824
9975
  if (!existsSync4(absVideoPath)) {
9825
9976
  return { success: false, error: `Video not found: ${absVideoPath}` };
9826
9977
  }
@@ -9830,7 +9981,7 @@ async function executeReview(options) {
9830
9981
  }
9831
9982
  let storyboardContext = "";
9832
9983
  if (storyboardPath) {
9833
- const absStoryboardPath = resolve8(process.cwd(), storyboardPath);
9984
+ const absStoryboardPath = resolve9(process.cwd(), storyboardPath);
9834
9985
  if (existsSync4(absStoryboardPath)) {
9835
9986
  const content = await readFile7(absStoryboardPath, "utf-8");
9836
9987
  storyboardContext = `
@@ -9886,7 +10037,7 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
9886
10037
  };
9887
10038
  if (autoApply && feedback.autoFixable.length > 0) {
9888
10039
  let currentInput = absVideoPath;
9889
- const outputBase = options.outputPath ? resolve8(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
10040
+ const outputBase = options.outputPath ? resolve9(process.cwd(), options.outputPath) : absVideoPath.replace(/(\.[^.]+)$/, "-reviewed$1");
9890
10041
  for (const fix of feedback.autoFixable) {
9891
10042
  if (fix.type === "color_grade" && fix.ffmpegFilter) {
9892
10043
  try {
@@ -9935,8 +10086,8 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
9935
10086
  // ../cli/src/commands/ai-image.ts
9936
10087
  import { readFile as readFile8, writeFile as writeFile6, mkdir as mkdir4 } from "node:fs/promises";
9937
10088
  import { existsSync as existsSync5 } from "node:fs";
9938
- import chalk5 from "chalk";
9939
- import ora4 from "ora";
10089
+ import chalk6 from "chalk";
10090
+ import ora5 from "ora";
9940
10091
  init_exec_safe();
9941
10092
  async function executeThumbnailBestFrame(options) {
9942
10093
  const {
@@ -10158,9 +10309,9 @@ async function handleAiAnalysisToolCall(name, args) {
10158
10309
 
10159
10310
  // ../cli/src/commands/ai-script-pipeline.ts
10160
10311
  import { readFile as readFile9, writeFile as writeFile7, mkdir as mkdir5, unlink, rename as rename2 } from "node:fs/promises";
10161
- import { resolve as resolve9, basename as basename3, extname as extname3 } from "node:path";
10312
+ import { resolve as resolve10, basename as basename3, extname as extname3 } from "node:path";
10162
10313
  import { existsSync as existsSync6 } from "node:fs";
10163
- import chalk6 from "chalk";
10314
+ import chalk7 from "chalk";
10164
10315
  init_exec_safe();
10165
10316
 
10166
10317
  // ../cli/src/commands/ai-helpers.ts
@@ -10180,7 +10331,37 @@ async function downloadVideo(url, apiKey) {
10180
10331
  var DEFAULT_VIDEO_RETRIES = 2;
10181
10332
  var RETRY_DELAY_MS = 5e3;
10182
10333
  function sleep(ms) {
10183
- return new Promise((resolve12) => setTimeout(resolve12, ms));
10334
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
10335
+ }
10336
+ async function generateVideoWithRetryGrok(grok, segment, options, maxRetries, onProgress) {
10337
+ const prompt2 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
10338
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
10339
+ try {
10340
+ const result = await grok.generateVideo(prompt2, {
10341
+ prompt: prompt2,
10342
+ duration: options.duration,
10343
+ aspectRatio: options.aspectRatio,
10344
+ referenceImage: options.referenceImage
10345
+ });
10346
+ if (result.status !== "failed" && result.id) {
10347
+ return { requestId: result.id };
10348
+ }
10349
+ if (attempt < maxRetries) {
10350
+ onProgress?.(`\u26A0 Retry ${attempt + 1}/${maxRetries}...`);
10351
+ await sleep(RETRY_DELAY_MS);
10352
+ }
10353
+ } catch (err) {
10354
+ const errMsg = err instanceof Error ? err.message : String(err);
10355
+ if (attempt < maxRetries) {
10356
+ onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
10357
+ await sleep(RETRY_DELAY_MS);
10358
+ } else {
10359
+ console.error(chalk7.dim(`
10360
+ [Grok error: ${errMsg}]`));
10361
+ }
10362
+ }
10363
+ }
10364
+ return null;
10184
10365
  }
10185
10366
  async function generateVideoWithRetryKling(kling, segment, options, maxRetries, onProgress) {
10186
10367
  const prompt2 = segment.visualStyle ? `${segment.visuals}. Style: ${segment.visualStyle}` : segment.visuals;
@@ -10211,7 +10392,7 @@ async function generateVideoWithRetryKling(kling, segment, options, maxRetries,
10211
10392
  onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
10212
10393
  await sleep(RETRY_DELAY_MS);
10213
10394
  } else {
10214
- console.error(chalk6.dim(`
10395
+ console.error(chalk7.dim(`
10215
10396
  [Kling error: ${errMsg}]`));
10216
10397
  }
10217
10398
  }
@@ -10240,7 +10421,7 @@ async function generateVideoWithRetryRunway(runway, segment, referenceImage, opt
10240
10421
  onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
10241
10422
  await sleep(RETRY_DELAY_MS);
10242
10423
  } else {
10243
- console.error(chalk6.dim(`
10424
+ console.error(chalk7.dim(`
10244
10425
  [Runway error: ${errMsg}]`));
10245
10426
  }
10246
10427
  }
@@ -10271,7 +10452,7 @@ async function generateVideoWithRetryVeo(gemini, segment, options, maxRetries, o
10271
10452
  onProgress?.(`\u26A0 Error: ${errMsg.slice(0, 50)}... retry ${attempt + 1}/${maxRetries}`);
10272
10453
  await sleep(RETRY_DELAY_MS);
10273
10454
  } else {
10274
- console.error(chalk6.dim(`
10455
+ console.error(chalk7.dim(`
10275
10456
  [Veo error: ${errMsg}]`));
10276
10457
  }
10277
10458
  }
@@ -10326,24 +10507,23 @@ async function executeScriptToVideo(options) {
10326
10507
  }
10327
10508
  let videoApiKey;
10328
10509
  if (!options.imagesOnly) {
10329
- if (options.generator === "kling") {
10330
- videoApiKey = await getApiKey("KLING_API_KEY", "Kling") ?? void 0;
10331
- if (!videoApiKey) {
10332
- return { success: false, outputDir, scenes: 0, error: "Kling API key required (or use imagesOnly option). Run 'vibe setup' or set KLING_API_KEY in .env" };
10333
- }
10334
- } else if (options.generator === "veo") {
10335
- videoApiKey = await getApiKey("GOOGLE_API_KEY", "Google") ?? void 0;
10336
- if (!videoApiKey) {
10337
- return { success: false, outputDir, scenes: 0, error: "Google API key required for Veo video generation (or use imagesOnly option). Run 'vibe setup' or set GOOGLE_API_KEY in .env" };
10338
- }
10339
- } else {
10340
- videoApiKey = await getApiKey("RUNWAY_API_SECRET", "Runway") ?? void 0;
10341
- if (!videoApiKey) {
10342
- return { success: false, outputDir, scenes: 0, error: "Runway API key required (or use imagesOnly option). Run 'vibe setup' or set RUNWAY_API_SECRET in .env" };
10343
- }
10510
+ const generatorKeyMap = {
10511
+ grok: { envVar: "XAI_API_KEY", name: "xAI (Grok)" },
10512
+ kling: { envVar: "KLING_API_KEY", name: "Kling" },
10513
+ runway: { envVar: "RUNWAY_API_SECRET", name: "Runway" },
10514
+ veo: { envVar: "GOOGLE_API_KEY", name: "Google (Veo)" }
10515
+ };
10516
+ const generator = options.generator || "grok";
10517
+ const generatorInfo = generatorKeyMap[generator];
10518
+ if (!generatorInfo) {
10519
+ return { success: false, outputDir, scenes: 0, error: `Invalid generator: ${options.generator}. Available: ${Object.keys(generatorKeyMap).join(", ")}` };
10520
+ }
10521
+ videoApiKey = await getApiKey(generatorInfo.envVar, generatorInfo.name) ?? void 0;
10522
+ if (!videoApiKey) {
10523
+ return { success: false, outputDir, scenes: 0, error: `${generatorInfo.name} API key required (or use imagesOnly option). Run 'vibe setup' or set ${generatorInfo.envVar} in .env` };
10344
10524
  }
10345
10525
  }
10346
- const absOutputDir = resolve9(process.cwd(), outputDir);
10526
+ const absOutputDir = resolve10(process.cwd(), outputDir);
10347
10527
  if (!existsSync6(absOutputDir)) {
10348
10528
  await mkdir5(absOutputDir, { recursive: true });
10349
10529
  }
@@ -10365,7 +10545,7 @@ async function executeScriptToVideo(options) {
10365
10545
  if (segments.length === 0) {
10366
10546
  return { success: false, outputDir, scenes: 0, error: "Failed to generate storyboard" };
10367
10547
  }
10368
- const storyboardPath = resolve9(absOutputDir, "storyboard.json");
10548
+ const storyboardPath = resolve10(absOutputDir, "storyboard.json");
10369
10549
  await writeFile7(storyboardPath, JSON.stringify(segments, null, 2), "utf-8");
10370
10550
  const result = {
10371
10551
  success: true,
@@ -10399,7 +10579,7 @@ async function executeScriptToVideo(options) {
10399
10579
  voiceId: options.voice
10400
10580
  });
10401
10581
  if (ttsResult.success && ttsResult.audioBuffer) {
10402
- const audioPath = resolve9(absOutputDir, `narration-${i + 1}.mp3`);
10582
+ const audioPath = resolve10(absOutputDir, `narration-${i + 1}.mp3`);
10403
10583
  await writeFile7(audioPath, ttsResult.audioBuffer);
10404
10584
  const actualDuration = await getAudioDuration(audioPath);
10405
10585
  segment.duration = actualDuration;
@@ -10487,7 +10667,7 @@ async function executeScriptToVideo(options) {
10487
10667
  }
10488
10668
  }
10489
10669
  }
10490
- const imagePath = resolve9(absOutputDir, `scene-${i + 1}.png`);
10670
+ const imagePath = resolve10(absOutputDir, `scene-${i + 1}.png`);
10491
10671
  if (imageBuffer) {
10492
10672
  await writeFile7(imagePath, imageBuffer);
10493
10673
  imagePaths.push(imagePath);
@@ -10511,7 +10691,57 @@ async function executeScriptToVideo(options) {
10511
10691
  const videoPaths = [];
10512
10692
  const maxRetries = options.retries ?? DEFAULT_VIDEO_RETRIES;
10513
10693
  if (!options.imagesOnly && videoApiKey) {
10514
- if (options.generator === "kling") {
10694
+ if (options.generator === "grok") {
10695
+ const grok = new GrokProvider();
10696
+ await grok.initialize({ apiKey: videoApiKey });
10697
+ for (let i = 0; i < segments.length; i++) {
10698
+ if (!imagePaths[i]) {
10699
+ videoPaths.push("");
10700
+ continue;
10701
+ }
10702
+ const segment = segments[i];
10703
+ const videoDuration = Math.min(15, Math.max(1, segment.duration));
10704
+ const imageBuffer = await readFile9(imagePaths[i]);
10705
+ const ext = extname3(imagePaths[i]).toLowerCase().slice(1);
10706
+ const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
10707
+ const referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
10708
+ const taskResult = await generateVideoWithRetryGrok(
10709
+ grok,
10710
+ segment,
10711
+ { duration: videoDuration, aspectRatio: options.aspectRatio || "16:9", referenceImage },
10712
+ maxRetries
10713
+ );
10714
+ if (taskResult) {
10715
+ try {
10716
+ const waitResult = await grok.waitForCompletion(taskResult.requestId, void 0, 3e5);
10717
+ if (waitResult.status === "completed" && waitResult.videoUrl) {
10718
+ const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
10719
+ const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
10720
+ await writeFile7(videoPath, buffer);
10721
+ const targetDuration = segment.duration;
10722
+ const actualVideoDuration = await getVideoDuration(videoPath);
10723
+ if (actualVideoDuration < targetDuration - 0.1) {
10724
+ const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
10725
+ await extendVideoNaturally(videoPath, targetDuration, extendedPath);
10726
+ await unlink(videoPath);
10727
+ await rename2(extendedPath, videoPath);
10728
+ }
10729
+ videoPaths.push(videoPath);
10730
+ result.videos.push(videoPath);
10731
+ } else {
10732
+ videoPaths.push("");
10733
+ result.failedScenes.push(i + 1);
10734
+ }
10735
+ } catch {
10736
+ videoPaths.push("");
10737
+ result.failedScenes.push(i + 1);
10738
+ }
10739
+ } else {
10740
+ videoPaths.push("");
10741
+ result.failedScenes.push(i + 1);
10742
+ }
10743
+ }
10744
+ } else if (options.generator === "kling") {
10515
10745
  const kling = new KlingProvider();
10516
10746
  await kling.initialize({ apiKey: videoApiKey });
10517
10747
  if (!kling.isConfigured()) {
@@ -10534,13 +10764,13 @@ async function executeScriptToVideo(options) {
10534
10764
  try {
10535
10765
  const waitResult = await kling.waitForCompletion(taskResult.taskId, taskResult.type, void 0, 6e5);
10536
10766
  if (waitResult.status === "completed" && waitResult.videoUrl) {
10537
- const videoPath = resolve9(absOutputDir, `scene-${i + 1}.mp4`);
10767
+ const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
10538
10768
  const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
10539
10769
  await writeFile7(videoPath, buffer);
10540
10770
  const targetDuration = segment.duration;
10541
10771
  const actualVideoDuration = await getVideoDuration(videoPath);
10542
10772
  if (actualVideoDuration < targetDuration - 0.1) {
10543
- const extendedPath = resolve9(absOutputDir, `scene-${i + 1}-extended.mp4`);
10773
+ const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
10544
10774
  await extendVideoNaturally(videoPath, targetDuration, extendedPath);
10545
10775
  await unlink(videoPath);
10546
10776
  await rename2(extendedPath, videoPath);
@@ -10580,13 +10810,13 @@ async function executeScriptToVideo(options) {
10580
10810
  try {
10581
10811
  const waitResult = await veo.waitForVideoCompletion(taskResult.operationName, void 0, 3e5);
10582
10812
  if (waitResult.status === "completed" && waitResult.videoUrl) {
10583
- const videoPath = resolve9(absOutputDir, `scene-${i + 1}.mp4`);
10813
+ const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
10584
10814
  const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
10585
10815
  await writeFile7(videoPath, buffer);
10586
10816
  const targetDuration = segment.duration;
10587
10817
  const actualVideoDuration = await getVideoDuration(videoPath);
10588
10818
  if (actualVideoDuration < targetDuration - 0.1) {
10589
- const extendedPath = resolve9(absOutputDir, `scene-${i + 1}-extended.mp4`);
10819
+ const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
10590
10820
  await extendVideoNaturally(videoPath, targetDuration, extendedPath);
10591
10821
  await unlink(videoPath);
10592
10822
  await rename2(extendedPath, videoPath);
@@ -10632,13 +10862,13 @@ async function executeScriptToVideo(options) {
10632
10862
  try {
10633
10863
  const waitResult = await runway.waitForCompletion(taskResult.taskId, void 0, 3e5);
10634
10864
  if (waitResult.status === "completed" && waitResult.videoUrl) {
10635
- const videoPath = resolve9(absOutputDir, `scene-${i + 1}.mp4`);
10865
+ const videoPath = resolve10(absOutputDir, `scene-${i + 1}.mp4`);
10636
10866
  const buffer = await downloadVideo(waitResult.videoUrl, videoApiKey);
10637
10867
  await writeFile7(videoPath, buffer);
10638
10868
  const targetDuration = segment.duration;
10639
10869
  const actualVideoDuration = await getVideoDuration(videoPath);
10640
10870
  if (actualVideoDuration < targetDuration - 0.1) {
10641
- const extendedPath = resolve9(absOutputDir, `scene-${i + 1}-extended.mp4`);
10871
+ const extendedPath = resolve10(absOutputDir, `scene-${i + 1}-extended.mp4`);
10642
10872
  await extendVideoNaturally(videoPath, targetDuration, extendedPath);
10643
10873
  await unlink(videoPath);
10644
10874
  await rename2(extendedPath, videoPath);
@@ -10751,13 +10981,13 @@ async function executeScriptToVideo(options) {
10751
10981
  });
10752
10982
  currentTime += actualDuration;
10753
10983
  }
10754
- const projectPath = resolve9(absOutputDir, "project.vibe.json");
10984
+ const projectPath = resolve10(absOutputDir, "project.vibe.json");
10755
10985
  await writeFile7(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
10756
10986
  result.projectPath = projectPath;
10757
10987
  result.totalDuration = currentTime;
10758
10988
  if (options.review) {
10759
10989
  try {
10760
- const storyboardFile = resolve9(absOutputDir, "storyboard.json");
10990
+ const storyboardFile = resolve10(absOutputDir, "storyboard.json");
10761
10991
  const reviewTarget = videoPaths.find((p) => p && p !== "") || imagePaths.find((p) => p && p !== "");
10762
10992
  if (reviewTarget) {
10763
10993
  const reviewResult = await executeReview({
@@ -10788,10 +11018,10 @@ async function executeScriptToVideo(options) {
10788
11018
 
10789
11019
  // ../cli/src/commands/ai-highlights.ts
10790
11020
  import { readFile as readFile10, writeFile as writeFile8, mkdir as mkdir6 } from "node:fs/promises";
10791
- import { resolve as resolve10, dirname as dirname2, basename as basename4, extname as extname4 } from "node:path";
11021
+ import { resolve as resolve11, dirname as dirname2, basename as basename4, extname as extname4 } from "node:path";
10792
11022
  import { existsSync as existsSync7 } from "node:fs";
10793
- import chalk7 from "chalk";
10794
- import ora5 from "ora";
11023
+ import chalk8 from "chalk";
11024
+ import ora6 from "ora";
10795
11025
  init_exec_safe();
10796
11026
  function filterHighlights(highlights, options) {
10797
11027
  let filtered = highlights.filter((h) => h.confidence >= options.threshold);
@@ -10815,7 +11045,7 @@ function filterHighlights(highlights, options) {
10815
11045
  }
10816
11046
  async function executeHighlights(options) {
10817
11047
  try {
10818
- const absPath = resolve10(process.cwd(), options.media);
11048
+ const absPath = resolve11(process.cwd(), options.media);
10819
11049
  if (!existsSync7(absPath)) {
10820
11050
  return { success: false, highlights: [], totalDuration: 0, totalHighlightDuration: 0, error: `File not found: ${absPath}` };
10821
11051
  }
@@ -10963,7 +11193,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
10963
11193
  totalHighlightDuration
10964
11194
  };
10965
11195
  if (options.output) {
10966
- const outputPath = resolve10(process.cwd(), options.output);
11196
+ const outputPath = resolve11(process.cwd(), options.output);
10967
11197
  await writeFile8(outputPath, JSON.stringify({
10968
11198
  sourceFile: absPath,
10969
11199
  totalDuration: sourceDuration,
@@ -10998,7 +11228,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
10998
11228
  currentTime += highlight.duration;
10999
11229
  }
11000
11230
  }
11001
- const projectPath = resolve10(process.cwd(), options.project);
11231
+ const projectPath = resolve11(process.cwd(), options.project);
11002
11232
  await writeFile8(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
11003
11233
  extractResult.projectPath = projectPath;
11004
11234
  }
@@ -11018,7 +11248,7 @@ async function executeAutoShorts(options) {
11018
11248
  if (!commandExists("ffmpeg")) {
11019
11249
  return { success: false, shorts: [], error: "FFmpeg not found" };
11020
11250
  }
11021
- const absPath = resolve10(process.cwd(), options.video);
11251
+ const absPath = resolve11(process.cwd(), options.video);
11022
11252
  if (!existsSync7(absPath)) {
11023
11253
  return { success: false, shorts: [], error: `File not found: ${absPath}` };
11024
11254
  }
@@ -11146,7 +11376,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
11146
11376
  }))
11147
11377
  };
11148
11378
  }
11149
- const outputDir = options.outputDir ? resolve10(process.cwd(), options.outputDir) : dirname2(absPath);
11379
+ const outputDir = options.outputDir ? resolve11(process.cwd(), options.outputDir) : dirname2(absPath);
11150
11380
  if (options.outputDir && !existsSync7(outputDir)) {
11151
11381
  await mkdir6(outputDir, { recursive: true });
11152
11382
  }
@@ -11157,7 +11387,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
11157
11387
  for (let i = 0; i < selectedHighlights.length; i++) {
11158
11388
  const h = selectedHighlights[i];
11159
11389
  const baseName = basename4(absPath, extname4(absPath));
11160
- const outputPath = resolve10(outputDir, `${baseName}-short-${i + 1}.mp4`);
11390
+ const outputPath = resolve11(outputDir, `${baseName}-short-${i + 1}.mp4`);
11161
11391
  const { stdout: probeOut } = await execSafe("ffprobe", [
11162
11392
  "-v",
11163
11393
  "error",
@@ -11449,7 +11679,7 @@ async function handleToolCall(name, args) {
11449
11679
 
11450
11680
  // src/resources/index.ts
11451
11681
  import { readFile as readFile11 } from "node:fs/promises";
11452
- import { resolve as resolve11 } from "node:path";
11682
+ import { resolve as resolve12 } from "node:path";
11453
11683
  var resources = [
11454
11684
  {
11455
11685
  uri: "vibe://project/current",
@@ -11484,7 +11714,7 @@ var resources = [
11484
11714
  ];
11485
11715
  var currentProjectPath = process.env.VIBE_PROJECT_PATH || null;
11486
11716
  async function loadProject2(projectPath) {
11487
- const absPath = resolve11(process.cwd(), projectPath);
11717
+ const absPath = resolve12(process.cwd(), projectPath);
11488
11718
  const content = await readFile11(absPath, "utf-8");
11489
11719
  const data = JSON.parse(content);
11490
11720
  return Project.fromJSON(data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/mcp-server",
3
- "version": "0.31.1",
3
+ "version": "0.33.1",
4
4
  "description": "VibeFrame MCP Server - AI-native video editing via Model Context Protocol",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,8 +57,8 @@
57
57
  "tsx": "^4.21.0",
58
58
  "typescript": "^5.3.3",
59
59
  "vitest": "^1.2.2",
60
- "@vibeframe/cli": "0.31.1",
61
- "@vibeframe/core": "0.31.1"
60
+ "@vibeframe/cli": "0.33.1",
61
+ "@vibeframe/core": "0.33.1"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=20"