@vibeframe/cli 0.104.1 → 0.104.3

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 +127 -53
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -26008,21 +26008,8 @@ async function prompt(question, hidden = false) {
26008
26008
  });
26009
26009
  }
26010
26010
  async function getApiKey(envVar, providerName, optionValue) {
26011
- if (optionValue) {
26012
- return optionValue;
26013
- }
26014
- const providerKey = providerKeyForEnvVar(envVar);
26015
- if (providerKey) {
26016
- const configKey = await getApiKeyFromConfig(providerKey);
26017
- if (configKey) {
26018
- return configKey;
26019
- }
26020
- }
26021
- loadEnv();
26022
- const envValue = getEnvValue2(envVar);
26023
- if (envValue) {
26024
- return envValue;
26025
- }
26011
+ const configuredKey = await getConfiguredApiKey(envVar, optionValue);
26012
+ if (configuredKey) return configuredKey;
26026
26013
  if (!process.stdin.isTTY) {
26027
26014
  return null;
26028
26015
  }
@@ -26043,6 +26030,19 @@ async function getApiKey(envVar, providerName, optionValue) {
26043
26030
  }
26044
26031
  return apiKey.trim();
26045
26032
  }
26033
+ async function getConfiguredApiKey(envVar, optionValue, options = {}) {
26034
+ if (optionValue) return optionValue;
26035
+ const providerKey = providerKeyForEnvVar(envVar);
26036
+ if (providerKey) {
26037
+ const configKey = await getApiKeyFromConfig(providerKey, { cwd: options.cwd });
26038
+ if (configKey) return configKey;
26039
+ }
26040
+ loadEnv();
26041
+ return getEnvValue2(envVar);
26042
+ }
26043
+ async function hasConfiguredApiKey(envVar, optionValue, options = {}) {
26044
+ return Boolean(await getConfiguredApiKey(envVar, optionValue, options));
26045
+ }
26046
26046
  function hasApiKey(envVar) {
26047
26047
  loadEnv();
26048
26048
  return !!getEnvValue2(envVar);
@@ -26139,7 +26139,7 @@ var init_api_key = __esm({
26139
26139
 
26140
26140
  // src/commands/_shared/tts-resolve.ts
26141
26141
  async function resolveTtsProvider(preferred = "auto") {
26142
- const choice = preferred === "auto" ? hasApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
26142
+ const choice = preferred === "auto" ? await getConfiguredApiKey("ELEVENLABS_API_KEY") ? "elevenlabs" : "kokoro" : preferred;
26143
26143
  if (choice === "elevenlabs") {
26144
26144
  return buildElevenLabs();
26145
26145
  }
@@ -26183,7 +26183,6 @@ var init_tts_resolve = __esm({
26183
26183
  "use strict";
26184
26184
  init_dist2();
26185
26185
  init_api_key();
26186
- init_api_key();
26187
26186
  TtsKeyMissingError = class extends Error {
26188
26187
  constructor(provider) {
26189
26188
  super(
@@ -26479,7 +26478,7 @@ YAML cues that drive narration, backdrop generation, and timing.
26479
26478
 
26480
26479
  \`\`\`yaml
26481
26480
  narration: "Introduce the promise in one crisp sentence."
26482
- backdrop: "Cinematic abstract technology backdrop, precise light, premium editorial feel"
26481
+ backdrop: "Topic-aligned editorial background plate, abstract visual system, no readable text, no logos, no consumer products, clean negative space for HTML overlays"
26483
26482
  duration: 4
26484
26483
  \`\`\`
26485
26484
 
@@ -26490,7 +26489,7 @@ screen and one spoken breath.
26490
26489
 
26491
26490
  \`\`\`yaml
26492
26491
  narration: "Show the mechanism or proof point that makes the promise believable."
26493
- backdrop: "Layered interface details, subtle motion trails, high-contrast product storytelling"
26492
+ backdrop: "Topic-aligned analytical background plate, abstract dashboard structure, no readable text, no product photos, no shoes, no unrelated objects"
26494
26493
  duration: 4
26495
26494
  \`\`\`
26496
26495
 
@@ -26501,7 +26500,7 @@ before/after.
26501
26500
 
26502
26501
  \`\`\`yaml
26503
26502
  narration: "Close with the action the viewer should remember."
26504
- backdrop: "Resolved hero frame, confident final composition, clean negative space"
26503
+ backdrop: "Resolved editorial background plate, confident final composition, clean negative space, no readable text, no logos, no unrelated products"
26505
26504
  duration: 4
26506
26505
  \`\`\`
26507
26506
 
@@ -26554,6 +26553,25 @@ consult this file \u2014 run the generate command directly.
26554
26553
  Browse named styles: \`vibe scene list-styles\`. Re-seed from one with
26555
26554
  \`vibe scene init . --visual-style "Swiss Pulse"\` (idempotent).
26556
26555
 
26556
+ ## Provider keys and project scope
26557
+
26558
+ Use VibeFrame CLI generation for project assets:
26559
+ \`vibe generate image|video|speech ...\`. This lets VibeFrame use keys
26560
+ from \`vibe setup --scope project\`.
26561
+
26562
+ Project-scope keys may live in a parent directory, for example
26563
+ \`../.vibeframe/config.yaml\` when this scene was created by
26564
+ \`vibe init launch\`. The \`vibe\` CLI searches upward automatically, so do
26565
+ not decide keys are missing just because \`.vibeframe/config.yaml\` is not
26566
+ inside this scene folder.
26567
+
26568
+ To verify scope without exposing secrets, run \`vibe doctor --json\` from
26569
+ this directory and inspect \`data.scope.activeScope\` plus
26570
+ \`data.scope.project.configPath\`. Never print config contents. Do not use
26571
+ a host agent's built-in image/audio generation tool for VibeFrame project
26572
+ assets unless the user explicitly requests an external, non-VibeFrame
26573
+ asset.
26574
+
26557
26575
  ## Skills \u2014 USE THESE FIRST
26558
26576
 
26559
26577
  @SKILL.md
@@ -26615,7 +26633,11 @@ npx hyperframes render
26615
26633
  \`\`\`
26616
26634
  4. Videos use \`muted\` with a separate \`<audio>\` element for the audio track.
26617
26635
  5. Sub-compositions use \`data-composition-src="compositions/file.html"\`.
26618
- 6. Only deterministic logic \u2014 no \`Date.now()\`, \`Math.random()\`, or network fetches.
26636
+ 6. For render-stable text, do not apply continuous \`scale\`, \`x\`, \`y\`, or
26637
+ \`filter\` tweens to \`.scene-content\` or any ancestor containing live text.
26638
+ Animate background/media layers instead; text/cards should enter briefly and
26639
+ then hold still at their final CSS positions.
26640
+ 7. Only deterministic logic \u2014 no \`Date.now()\`, \`Math.random()\`, or network fetches.
26619
26641
 
26620
26642
  ## Linting \u2014 run after changes
26621
26643
 
@@ -29043,13 +29065,18 @@ Requirements (non-negotiable):
29043
29065
  producer's seek lands past the timeline's natural end and visibility state
29044
29066
  goes stale \u2014 the hold phase renders BLACK. Anchor the timeline to the full
29045
29067
  beat duration via either:
29046
- 1. A subtle idle motion spanning 0\u2192duration on a parent element, e.g.
29047
- \`tl.fromTo(".scene-content", { scale: 1.0 }, { scale: 1.015, duration: <beat>, ease: "none" }, 0);\`
29068
+ 1. A subtle idle motion spanning 0\u2192duration on a background/media layer,
29069
+ e.g. \`tl.fromTo(".backdrop", { scale: 1.0 }, { scale: 1.015, duration: <beat>, ease: "none" }, 0);\`
29048
29070
  (Ken-Burns, breathing opacity, gradient drift \u2014 should be barely
29049
29071
  perceptible so it doesn't compete with entry/exit beats).
29050
29072
  2. OR an explicit \`tl.set(target, { ...natural state... }, <beat - 0.001>)\`
29051
29073
  anchor at the end.
29052
29074
  This is the #2 source of "text disappears mid-beat" bugs after \`.clip\` sizing.
29075
+ - Do not apply continuous \`scale\`, \`x\`, \`y\`, \`filter\`, or other transform
29076
+ tweens to \`.scene-content\` or any ancestor that contains live text/cards.
29077
+ Animate the backdrop/media plane instead; let text enter briefly, then hold
29078
+ still at its final CSS position. Continuous transforms on text ancestors can
29079
+ create subpixel shimmer in screenshot-captured renders.
29053
29080
  - Timed children inside the composition have \`class="clip"\` plus
29054
29081
  \`data-start\`, \`data-duration\`, \`data-track-index\`.
29055
29082
  - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
@@ -29119,7 +29146,9 @@ Reference shape (verbatim \u2014 match this skeleton exactly, no DOCTYPE / html
29119
29146
  const tl = gsap.timeline({ paused: true });
29120
29147
  // Idle motion spanning full beat duration \u2014 required to keep timeline
29121
29148
  // length aligned with data-duration (otherwise hold phase goes black).
29122
- tl.fromTo(".scene-content", { scale: 1.0 }, { scale: 1.015, duration: <sec>, ease: "none" }, 0);
29149
+ // Keep continuous motion on the background/media layer so live text does
29150
+ // not shimmer from subpixel resampling.
29151
+ tl.fromTo(".backdrop", { scale: 1.0 }, { scale: 1.015, duration: <sec>, ease: "none" }, 0);
29123
29152
  // entry tweens
29124
29153
  window.__timelines["${compositionId}"] = tl;
29125
29154
  </script>
@@ -32190,7 +32219,7 @@ import { resolve as resolve18 } from "node:path";
32190
32219
  import { writeFile as writeFile15 } from "node:fs/promises";
32191
32220
  async function executeSoundEffect(options) {
32192
32221
  try {
32193
- const apiKey = hasApiKey("ELEVENLABS_API_KEY") ? await getApiKeyFromConfig("elevenlabs") || process.env.ELEVENLABS_API_KEY : null;
32222
+ const apiKey = await getConfiguredApiKey("ELEVENLABS_API_KEY");
32194
32223
  if (!apiKey)
32195
32224
  return {
32196
32225
  success: false,
@@ -32289,7 +32318,6 @@ var init_sound_effect = __esm({
32289
32318
  init_ora();
32290
32319
  init_dist2();
32291
32320
  init_api_key();
32292
- init_config();
32293
32321
  init_output();
32294
32322
  init_validate();
32295
32323
  }
@@ -32298,7 +32326,7 @@ var init_sound_effect = __esm({
32298
32326
  // src/commands/generate/music-status.ts
32299
32327
  async function executeMusicStatus(options) {
32300
32328
  try {
32301
- const apiKey = options.apiKey ?? (hasApiKey("REPLICATE_API_TOKEN") ? await getApiKeyFromConfig("replicate") || process.env.REPLICATE_API_TOKEN : null);
32329
+ const apiKey = await getConfiguredApiKey("REPLICATE_API_TOKEN", options.apiKey);
32302
32330
  if (!apiKey)
32303
32331
  return { success: false, error: "REPLICATE_API_TOKEN required for music status" };
32304
32332
  const replicate = new ReplicateProvider();
@@ -32371,7 +32399,6 @@ var init_music_status = __esm({
32371
32399
  init_source();
32372
32400
  init_dist2();
32373
32401
  init_api_key();
32374
- init_config();
32375
32402
  init_output();
32376
32403
  }
32377
32404
  });
@@ -32452,7 +32479,7 @@ import { resolve as resolve19, dirname as dirname4 } from "node:path";
32452
32479
  import { writeFile as writeFile16, mkdir as mkdir8 } from "node:fs/promises";
32453
32480
  async function executeBackground(options) {
32454
32481
  try {
32455
- const apiKey = options.apiKey ?? (hasApiKey("OPENAI_API_KEY") ? await getApiKeyFromConfig("openai") || process.env.OPENAI_API_KEY : null);
32482
+ const apiKey = await getConfiguredApiKey("OPENAI_API_KEY", options.apiKey);
32456
32483
  if (!apiKey)
32457
32484
  return { success: false, error: "OPENAI_API_KEY required for background generation" };
32458
32485
  const openaiImage = new OpenAIImageProvider();
@@ -32583,7 +32610,6 @@ var init_background = __esm({
32583
32610
  init_ora();
32584
32611
  init_dist2();
32585
32612
  init_api_key();
32586
- init_config();
32587
32613
  init_output();
32588
32614
  init_validate();
32589
32615
  }
@@ -33249,7 +33275,7 @@ import { resolve as resolve21 } from "node:path";
33249
33275
  import { writeFile as writeFile18 } from "node:fs/promises";
33250
33276
  async function executeSpeech(options) {
33251
33277
  try {
33252
- const apiKey = hasApiKey("ELEVENLABS_API_KEY") ? await getApiKeyFromConfig("elevenlabs") || process.env.ELEVENLABS_API_KEY : null;
33278
+ const apiKey = await getConfiguredApiKey("ELEVENLABS_API_KEY");
33253
33279
  if (!apiKey)
33254
33280
  return {
33255
33281
  success: false,
@@ -33464,7 +33490,6 @@ var init_speech = __esm({
33464
33490
  init_dist2();
33465
33491
  init_api_key();
33466
33492
  init_tty();
33467
- init_config();
33468
33493
  init_output();
33469
33494
  init_validate();
33470
33495
  }
@@ -33817,7 +33842,7 @@ async function executeVideoGenerate(options) {
33817
33842
  fal: "FAL_API_KEY"
33818
33843
  };
33819
33844
  const envKey = envKeyMap[provider] || "";
33820
- const key = apiKey || (hasApiKey(envKey) ? process.env[envKey] : void 0);
33845
+ const key = await getConfiguredApiKey(envKey, apiKey);
33821
33846
  if (!key) return { success: false, error: `${envKeyMap[provider]} required for ${provider}` };
33822
33847
  let referenceImage;
33823
33848
  let referenceImageBuffer;
@@ -35073,7 +35098,7 @@ async function executeMusic(options) {
35073
35098
  try {
35074
35099
  const provider = options.provider || "elevenlabs";
35075
35100
  if (provider === "elevenlabs") {
35076
- const apiKey2 = hasApiKey("ELEVENLABS_API_KEY") ? await getApiKeyFromConfig("elevenlabs") || process.env.ELEVENLABS_API_KEY : null;
35101
+ const apiKey2 = await getConfiguredApiKey("ELEVENLABS_API_KEY");
35077
35102
  if (!apiKey2)
35078
35103
  return {
35079
35104
  success: false,
@@ -35093,7 +35118,7 @@ async function executeMusic(options) {
35093
35118
  await writeFile23(outputPath2, result2.audioBuffer);
35094
35119
  return { success: true, outputPath: outputPath2, provider: "elevenlabs", duration: duration2 };
35095
35120
  }
35096
- const apiKey = hasApiKey("REPLICATE_API_TOKEN") ? await getApiKeyFromConfig("replicate") || process.env.REPLICATE_API_TOKEN : null;
35121
+ const apiKey = await getConfiguredApiKey("REPLICATE_API_TOKEN");
35097
35122
  if (!apiKey)
35098
35123
  return {
35099
35124
  success: false,
@@ -35328,7 +35353,6 @@ var init_music = __esm({
35328
35353
  init_ora();
35329
35354
  init_dist2();
35330
35355
  init_api_key();
35331
- init_config();
35332
35356
  init_output();
35333
35357
  init_validate();
35334
35358
  init_status_jobs();
@@ -36395,7 +36419,7 @@ Examples:
36395
36419
  )
36396
36420
  );
36397
36421
  }
36398
- if (providerEnvMap[provider] && !hasApiKey(providerEnvMap[provider]) && !options.apiKey) {
36422
+ if (providerEnvMap[provider] && !await hasConfiguredApiKey(providerEnvMap[provider], options.apiKey)) {
36399
36423
  const resolved = resolveProvider("image");
36400
36424
  if (resolved) {
36401
36425
  log(source_default.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
@@ -37835,7 +37859,7 @@ Examples:
37835
37859
  );
37836
37860
  provider = "seedance";
37837
37861
  }
37838
- if (videoEnvMap[provider] && !hasApiKey(videoEnvMap[provider]) && !options.apiKey) {
37862
+ if (videoEnvMap[provider] && !await hasConfiguredApiKey(videoEnvMap[provider], options.apiKey)) {
37839
37863
  const resolved = resolveProvider("video");
37840
37864
  if (resolved) {
37841
37865
  log(source_default.dim(` ${provider} key not found. Using ${resolved.label} instead.`));
@@ -58357,6 +58381,32 @@ var init_build_cache = __esm({
58357
58381
  }
58358
58382
  });
58359
58383
 
58384
+ // src/commands/_shared/build-backdrop-prompt.ts
58385
+ function augmentBackdropPrompt(cue) {
58386
+ const trimmed = cue.trim();
58387
+ const lower = trimmed.toLowerCase();
58388
+ const requestsTextOrMarks = /\b(text|typography|title|headline|label|caption|logo|logos|wordmark|brand mark|brand marks)\b/.test(lower);
58389
+ const forbidsTextOrMarks = /\b(no|without|avoid)\s+(readable\s+)?(text|typography|titles?|headlines?|labels?|captions?|brand\s+logos?|logos?|wordmarks?|brand\s+marks?)\b/.test(
58390
+ lower
58391
+ );
58392
+ const allowsTextOrMarks = requestsTextOrMarks && !forbidsTextOrMarks;
58393
+ const overlayContract = allowsTextOrMarks ? "The image is a video background or end-card plate; do not add any text, logos, charts, or UI beyond what the scene cue explicitly requests." : "The image is a background only; HTML overlays will provide all final text, charts, logos, and UI labels.";
58394
+ const textRule = allowsTextOrMarks ? "If text, logos, or brand marks are explicitly requested, keep them minimal, legible, and do not invent extras." : "No readable text, labels, UI copy, logos, brand marks, watermarks, or invented typography.";
58395
+ return [
58396
+ "Create a 16:9 video background plate for a HyperFrames scene.",
58397
+ overlayContract,
58398
+ `Scene cue: ${trimmed}`,
58399
+ textRule,
58400
+ "Avoid unrelated consumer product photography, shoes, packaging, food, people, celebrity faces, advertisements, and random objects unless explicitly requested by the scene cue.",
58401
+ "Leave generous negative space for overlay text and cards. Keep the result topic-aligned, editorial, cinematic, and non-distracting."
58402
+ ].join(" ");
58403
+ }
58404
+ var init_build_backdrop_prompt = __esm({
58405
+ "src/commands/_shared/build-backdrop-prompt.ts"() {
58406
+ "use strict";
58407
+ }
58408
+ });
58409
+
58360
58410
  // src/commands/_shared/build-plan.ts
58361
58411
  import { existsSync as existsSync44 } from "node:fs";
58362
58412
  import { readFile as readFile40 } from "node:fs/promises";
@@ -58450,6 +58500,7 @@ async function createBuildPlan(opts) {
58450
58500
  const voice = stringOrUndefined3(cue.voice) ?? resolved.voice;
58451
58501
  const narrationText = stringOrUndefined3(cue.narration);
58452
58502
  const backdropPrompt = stringOrUndefined3(cue.backdrop);
58503
+ const augmentedBackdropPrompt = backdropPrompt ? augmentBackdropPrompt(backdropPrompt) : null;
58453
58504
  const videoPrompt = stringOrUndefined3(cue.video);
58454
58505
  const musicPrompt = stringOrUndefined3(cue.music);
58455
58506
  const genericReference = resolveGenericAssetReference(projectDir, cue.asset);
@@ -58469,9 +58520,9 @@ async function createBuildPlan(opts) {
58469
58520
  voice,
58470
58521
  ext: resolved.narration.resolved === "elevenlabs" ? "mp3" : "wav"
58471
58522
  }) : null;
58472
- const backdropCache = backdropPrompt && !backdropReference ? backdropCacheDescriptor({
58523
+ const backdropCache = augmentedBackdropPrompt && !backdropReference ? backdropCacheDescriptor({
58473
58524
  beatId: beat.id,
58474
- cue: backdropPrompt,
58525
+ cue: augmentedBackdropPrompt,
58475
58526
  provider: resolved.image.resolved,
58476
58527
  quality: imageQuality,
58477
58528
  size: imageSize2,
@@ -59001,6 +59052,7 @@ var init_build_plan = __esm({
59001
59052
  init_build_asset_reference();
59002
59053
  init_build_cache();
59003
59054
  init_build_asset_metadata();
59055
+ init_build_backdrop_prompt();
59004
59056
  init_composer_resolve();
59005
59057
  init_storyboard_parse();
59006
59058
  init_project_config();
@@ -60415,8 +60467,9 @@ async function dispatchNarration(beat, ctx) {
60415
60467
  async function dispatchBackdrop(beat, ctx) {
60416
60468
  const reference = assetReferenceForBeat(ctx.projectDir, "backdrop", beat);
60417
60469
  if (reference) return referencePrimitiveOutcome("backdrop", beat, ctx, reference);
60418
- const prompt3 = stringOrUndefined4(beat.cues?.backdrop);
60419
- if (!prompt3) return { status: "no-cue" };
60470
+ const cue = stringOrUndefined4(beat.cues?.backdrop);
60471
+ if (!cue) return { status: "no-cue" };
60472
+ const prompt3 = augmentBackdropPrompt(cue);
60420
60473
  const rel = `assets/backdrop-${beat.id}.png`;
60421
60474
  const abs = join22(ctx.projectDir, rel);
60422
60475
  const size = ctx.imageSize ?? "1536x1024";
@@ -61394,6 +61447,7 @@ var init_scene_build = __esm({
61394
61447
  init_root_sync();
61395
61448
  init_build_cache();
61396
61449
  init_build_asset_metadata();
61450
+ init_build_backdrop_prompt();
61397
61451
  init_ai_video();
61398
61452
  init_music();
61399
61453
  init_status_jobs();
@@ -71384,7 +71438,7 @@ init_source();
71384
71438
  // package.json
71385
71439
  var package_default = {
71386
71440
  name: "@vibeframe/cli",
71387
- version: "0.104.1",
71441
+ version: "0.104.3",
71388
71442
  description: "VibeFrame CLI - natural language video editing from the terminal",
71389
71443
  private: false,
71390
71444
  license: "MIT",
@@ -79953,7 +80007,7 @@ function fourBeatDraft(opts) {
79953
80007
  title: scenic ? "First light" : "Hook",
79954
80008
  duration: opts.durations[0],
79955
80009
  narration: scenic ? `The first light reaches ${opts.product} before the day begins.` : `${opts.product} starts with one clear promise.`,
79956
- backdrop: scenic ? `misty mountain ridge at sunrise, golden light, fog layers, cinematic ${opts.theme}` : `polished opening frame for ${opts.product}, ${opts.theme}, clear hero subject`,
80010
+ backdrop: scenic ? `misty mountain ridge at sunrise, golden light, fog layers, cinematic ${opts.theme}` : `topic-aligned editorial background plate for ${opts.product}, ${opts.theme}, no readable text, no logos, no unrelated consumer products, clean negative space for HTML overlays`,
79957
80011
  motion: scenic ? "quiet title reveal, soft parallax, slow camera drift" : "large readable headline, restrained camera push, immediate value reveal",
79958
80012
  body: scenic ? "Show the mountain identity immediately. Keep text minimal." : "Make the value obvious in the first beat. Avoid setup that delays the payoff."
79959
80013
  },
@@ -79962,7 +80016,7 @@ function fourBeatDraft(opts) {
79962
80016
  title: scenic ? "The path appears" : "Proof",
79963
80017
  duration: opts.durations[1],
79964
80018
  narration: scenic ? "A clear path appears when every step is visible." : "The proof appears when each step is visible.",
79965
- backdrop: scenic ? "ridge trail emerging through fog, warm sunrise edge light, high contrast" : `concrete proof frame for ${opts.product}, visible workflow, before and after contrast`,
80019
+ backdrop: scenic ? "ridge trail emerging through fog, warm sunrise edge light, high contrast" : `topic-aligned proof background plate for ${opts.product}, abstract workflow structure, no readable text, no product photos, no shoes, no unrelated objects`,
79966
80020
  motion: scenic ? "thin line traces the path, small labels fade in" : "interface panels or proof points slide into place with precise transitions",
79967
80021
  body: "Connect the visual proof to the promise in one focused moment."
79968
80022
  },
@@ -79980,7 +80034,7 @@ function fourBeatDraft(opts) {
79980
80034
  title: scenic ? "Day one" : "Close",
79981
80035
  duration: opts.durations[3],
79982
80036
  narration: scenic ? `${opts.product}. Day one begins here.` : `${opts.product} is ready to show.`,
79983
- backdrop: scenic ? "wide mountain ridge at peak golden hour, valley filled with warm light" : `resolved final frame for ${opts.product}, clean negative space, confident lockup`,
80037
+ backdrop: scenic ? "wide mountain ridge at peak golden hour, valley filled with warm light" : `resolved editorial background plate for ${opts.product}, clean negative space, confident lockup, no readable text, no logos, no unrelated products`,
79984
80038
  motion: "clean final title, slow settling motion, gentle fade to the final frame",
79985
80039
  body: "End with a quiet branded title and no new idea."
79986
80040
  }
@@ -79993,7 +80047,7 @@ function threeBeatDraft(opts) {
79993
80047
  title: "Hook",
79994
80048
  duration: opts.durations[0],
79995
80049
  narration: `${opts.product} starts with one clear promise.`,
79996
- backdrop: `polished opening frame for ${opts.product}, ${opts.theme}, clear hero subject`,
80050
+ backdrop: `topic-aligned editorial background plate for ${opts.product}, ${opts.theme}, no readable text, no logos, no unrelated consumer products, clean negative space for HTML overlays`,
79997
80051
  motion: "large readable headline, restrained camera push, immediate value reveal",
79998
80052
  body: "Make the value obvious in one beat."
79999
80053
  },
@@ -80002,7 +80056,7 @@ function threeBeatDraft(opts) {
80002
80056
  title: "Proof",
80003
80057
  duration: opts.durations[1],
80004
80058
  narration: "The proof appears when each step is visible.",
80005
- backdrop: `concrete proof frame for ${opts.product}, visible workflow, before and after contrast`,
80059
+ backdrop: `topic-aligned proof background plate for ${opts.product}, abstract workflow structure, no readable text, no product photos, no shoes, no unrelated objects`,
80006
80060
  motion: "layered panels, highlighted cause and effect, precise transitions",
80007
80061
  body: "Show the mechanism, metric, or before/after that makes the promise believable."
80008
80062
  },
@@ -80011,7 +80065,7 @@ function threeBeatDraft(opts) {
80011
80065
  title: "Close",
80012
80066
  duration: opts.durations[2],
80013
80067
  narration: `${opts.product} is ready to show.`,
80014
- backdrop: `resolved final frame for ${opts.product}, clean negative space, confident lockup`,
80068
+ backdrop: `resolved editorial background plate for ${opts.product}, clean negative space, confident lockup, no readable text, no logos, no unrelated products`,
80015
80069
  motion: "clean final title, subtle parallax, gentle fade to the final frame",
80016
80070
  body: "End on the product name, offer, command, or memorable final line."
80017
80071
  }
@@ -80212,11 +80266,25 @@ When you run vibe commands programmatically:
80212
80266
  echo '{"video":"long.mp4","count":3,"aspect":"9:16"}' | vibe remix auto-shorts --stdin --json
80213
80267
  \`\`\`
80214
80268
 
80215
- ## Provider keys
80269
+ ## Provider keys and project scope
80216
80270
 
80217
- API keys live in \`.env\` (gitignored). Copy \`.env.example\` to start. Run
80218
- \`vibe doctor\` to see which keys are currently detected and which providers
80219
- they unlock.
80271
+ Prefer \`vibe setup --scope project\` for project-local keys. It writes
80272
+ \`.vibeframe/config.yaml\` in the setup directory; when you work from a
80273
+ nested scene directory, the \`vibe\` CLI automatically searches parent
80274
+ directories and uses the nearest project config. Do not assume the config
80275
+ file must exist in the current subdirectory.
80276
+
80277
+ Run \`vibe doctor --json\` from the directory where you will run commands
80278
+ and check \`data.scope.project.configPath\` / \`data.scope.activeScope\` before
80279
+ deciding keys are missing. Never print or copy config file contents.
80280
+
80281
+ For generated project assets, use VibeFrame commands such as
80282
+ \`vibe generate image|video|speech ...\` so these project-scope keys are
80283
+ used. Do not switch to a host agent's built-in image/audio generation tool
80284
+ unless the user explicitly asks for a non-VibeFrame asset.
80285
+
80286
+ \`.env\` is still supported as a gitignored fallback. Copy \`.env.example\`
80287
+ to start if you prefer environment variables.
80220
80288
 
80221
80289
  ### Composition rules (scene HTML)
80222
80290
 
@@ -80230,6 +80298,12 @@ principles, type system, and visual-identity hard-gate. The same skill
80230
80298
  governs \`vibe scene lint\` so your authored HTML and the linter stay in
80231
80299
  agreement.
80232
80300
 
80301
+ For render-stable text, do not apply continuous \`scale\`, \`x\`, \`y\`,
80302
+ \`filter\`, or other transform tweens to \`.scene-content\` or any ancestor
80303
+ containing live text/cards. Put ambient zoom/parallax on background or media
80304
+ layers only; text should enter briefly and then hold still at its final CSS
80305
+ position.
80306
+
80233
80307
  To retro-install on a project scaffolded before this command existed, run
80234
80308
  \`vibe scene install-skill [--host all]\`.
80235
80309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.104.1",
3
+ "version": "0.104.3",
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.104.1",
187
- "@vibeframe/core": "0.104.1"
186
+ "@vibeframe/core": "0.104.3",
187
+ "@vibeframe/ai-providers": "0.104.3"
188
188
  },
189
189
  "engines": {
190
190
  "node": ">=20"