@koda-sl/baker-cli 0.68.0 → 0.71.2

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.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
4
+ IMAGE_GENERATE_MODELS,
4
5
  MODEL_REGISTRY,
5
6
  SEEDANCE_DURATIONS,
6
7
  ValidationError,
@@ -8,10 +9,10 @@ import {
8
9
  defaultRegistry,
9
10
  generateCatalog,
10
11
  validateCanvasDeep
11
- } from "./chunk-K6LHXCKD.js";
12
+ } from "./chunk-JIDZ37KG.js";
12
13
 
13
14
  // src/cli.ts
14
- import { defineCommand as defineCommand136, runMain } from "citty";
15
+ import { defineCommand as defineCommand138, runMain } from "citty";
15
16
 
16
17
  // src/commands/actions/index.ts
17
18
  import { defineCommand as defineCommand12 } from "citty";
@@ -146,9 +147,9 @@ async function handleResponse(response) {
146
147
  throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
147
148
  }
148
149
  }
149
- async function apiGet(path6, params) {
150
+ async function apiGet(path7, params) {
150
151
  const env = getEnv();
151
- const url = new URL(path6, env.BAKER_API_URL);
152
+ const url = new URL(path7, env.BAKER_API_URL);
152
153
  if (params) {
153
154
  const clean = sanitizeParams(params);
154
155
  for (const [key, value] of Object.entries(clean)) {
@@ -173,12 +174,12 @@ async function apiGet(path6, params) {
173
174
  }
174
175
  return handleResponse(response);
175
176
  }
176
- async function apiPost(path6, body, opts) {
177
+ async function apiPost(path7, body, opts) {
177
178
  const env = getEnv();
178
179
  const timeoutMs = opts?.timeoutMs ?? 6e4;
179
180
  let response;
180
181
  try {
181
- response = await fetchWithRateLimitRetry(new URL(path6, env.BAKER_API_URL).toString(), {
182
+ response = await fetchWithRateLimitRetry(new URL(path7, env.BAKER_API_URL).toString(), {
182
183
  method: "POST",
183
184
  headers: {
184
185
  Authorization: `Bearer ${env.BAKER_API_KEY}`,
@@ -917,31 +918,31 @@ function cachePath(category, key) {
917
918
  return join(dir, `${hashKey(key)}.json`);
918
919
  }
919
920
  function cacheGet(category, key) {
920
- const path6 = cachePath(category, key);
921
- if (!existsSync(path6)) {
921
+ const path7 = cachePath(category, key);
922
+ if (!existsSync(path7)) {
922
923
  return null;
923
924
  }
924
925
  try {
925
- const raw = readFileSync(path6, "utf-8");
926
+ const raw = readFileSync(path7, "utf-8");
926
927
  const entry = JSON.parse(raw);
927
928
  if (entry.expiresAt < Date.now()) {
928
- rmSync(path6, { force: true });
929
+ rmSync(path7, { force: true });
929
930
  return null;
930
931
  }
931
932
  return entry;
932
933
  } catch {
933
- rmSync(path6, { force: true });
934
+ rmSync(path7, { force: true });
934
935
  return null;
935
936
  }
936
937
  }
937
938
  function cacheSet(category, key, data, ttlMs, fields) {
938
- const path6 = cachePath(category, key);
939
+ const path7 = cachePath(category, key);
939
940
  const entry = {
940
941
  expiresAt: Date.now() + ttlMs,
941
942
  data,
942
943
  fields
943
944
  };
944
- writeFileSync(path6, JSON.stringify(entry), "utf-8");
945
+ writeFileSync(path7, JSON.stringify(entry), "utf-8");
945
946
  }
946
947
  var HOUR = 60 * 60 * 1e3;
947
948
  var MINUTE = 60 * 1e3;
@@ -7713,6 +7714,24 @@ async function probeDuration(filePath) {
7713
7714
  import { readFile as readFile2 } from "fs/promises";
7714
7715
  import path2 from "path";
7715
7716
  import { defineCommand as defineCommand74 } from "citty";
7717
+
7718
+ // src/commands/canvas/placeholders.ts
7719
+ function unsuppliedPlaceholderAssets(canvas) {
7720
+ const nodes = canvas?.nodes;
7721
+ if (!Array.isArray(nodes)) return [];
7722
+ const out = [];
7723
+ for (const n of nodes) {
7724
+ const node = n;
7725
+ if (node?.type !== "ingest") continue;
7726
+ const params = node.params;
7727
+ if (params?.source === "path" && typeof params.path === "string" && params.path.includes("[TODO")) {
7728
+ out.push({ node: String(node.id ?? "?"), placeholder: params.path });
7729
+ }
7730
+ }
7731
+ return out;
7732
+ }
7733
+
7734
+ // src/commands/canvas/run.ts
7716
7735
  var runCommand = defineCommand74({
7717
7736
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
7718
7737
  args: {
@@ -7734,6 +7753,25 @@ var runCommand = defineCommand74({
7734
7753
  `);
7735
7754
  process.exit(2);
7736
7755
  }
7756
+ const pending = unsuppliedPlaceholderAssets(parsed);
7757
+ if (pending.length > 0) {
7758
+ process.stderr.write(
7759
+ `${JSON.stringify(
7760
+ {
7761
+ ok: false,
7762
+ error: {
7763
+ code: "unsupplied_assets",
7764
+ message: "This canvas still has placeholder asset slots \u2014 supply a real image (or video) at each before running. Each `el_*` ingest is a [TODO] you fill with the real logo/subject/product, then re-run.",
7765
+ assets: pending
7766
+ }
7767
+ },
7768
+ null,
7769
+ 2
7770
+ )}
7771
+ `
7772
+ );
7773
+ process.exit(2);
7774
+ }
7737
7775
  const engine = createEngineFromEnv({
7738
7776
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
7739
7777
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
@@ -8191,7 +8229,7 @@ var scaffoldStaticAdCommand = defineCommand75({
8191
8229
 
8192
8230
  // src/commands/canvas/scaffold-video.ts
8193
8231
  import { cp, mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
8194
- import path4 from "path";
8232
+ import path5 from "path";
8195
8233
  import { defineCommand as defineCommand76 } from "citty";
8196
8234
 
8197
8235
  // src/engine/scaffold/video.ts
@@ -8199,7 +8237,19 @@ import { z as z3 } from "zod";
8199
8237
  var FIXED_TTS_MODEL = "elevenlabs/eleven_v3";
8200
8238
  var FIXED_SFX_MODEL = "elevenlabs/eleven_text_to_sound_v2";
8201
8239
  var FIXED_MUSIC_MODEL = "elevenlabs/music-v1";
8240
+ var FIXED_LIPSYNC_MODEL = "fal/veed-lipsync";
8202
8241
  var MUSIC_BED_GAIN_DB = -12;
8242
+ var NARRATOR_SPEAKERS = /* @__PURE__ */ new Set([
8243
+ "voiceover",
8244
+ "voice_over",
8245
+ "narrator",
8246
+ "narration",
8247
+ "vo",
8248
+ "announcer",
8249
+ "off_screen",
8250
+ "offscreen",
8251
+ "off-screen"
8252
+ ]);
8203
8253
  var SHARED_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"]);
8204
8254
  var EDGES = ["start", "end"];
8205
8255
  function snapToSeedance(durationS) {
@@ -8215,11 +8265,39 @@ function snapToSeedance(durationS) {
8215
8265
  }
8216
8266
  return best;
8217
8267
  }
8268
+ function ceilToSeedance(durationS) {
8269
+ const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
8270
+ if (!Number.isFinite(durationS) || durationS <= 0) return SEEDANCE_DURATIONS[0];
8271
+ for (const d of SEEDANCE_DURATIONS) if (d >= durationS) return d;
8272
+ return max;
8273
+ }
8274
+ function sceneDurationS(scene) {
8275
+ const raw = scene.duration_s ?? (scene.end_s != null && scene.start_s != null ? scene.end_s - scene.start_s : 5);
8276
+ const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
8277
+ return Math.min(Math.max(raw, 0.5), max);
8278
+ }
8279
+ function trimArgs(durationS) {
8280
+ return [
8281
+ "-i",
8282
+ "{{in.clip}}",
8283
+ "-t",
8284
+ durationS.toFixed(3),
8285
+ "-an",
8286
+ "-c:v",
8287
+ "libx264",
8288
+ "-pix_fmt",
8289
+ "yuv420p",
8290
+ "{{out.video}}"
8291
+ ];
8292
+ }
8218
8293
  var FrameAsset = z3.object({ url: z3.string().optional() }).loose().optional();
8219
8294
  var DialogueLine = z3.object({
8220
8295
  speaker: z3.string().optional(),
8221
8296
  line: z3.string().optional(),
8297
+ // Absolute seconds on the source timeline (the deconstruct emits both).
8222
8298
  start_s: z3.number().optional(),
8299
+ end_s: z3.number().optional(),
8300
+ delivery: z3.string().optional(),
8223
8301
  voice_description: z3.string().optional()
8224
8302
  }).loose();
8225
8303
  var Sfx = z3.object({
@@ -8259,7 +8337,12 @@ var VideoBlueprint = z3.object({
8259
8337
  identified_track: z3.object({ title: z3.string().optional(), artist: z3.string().optional() }).loose().nullish()
8260
8338
  }).loose().optional(),
8261
8339
  cast: z3.array(z3.object({ id: z3.string().optional(), description: z3.string().optional() }).loose()).optional(),
8262
- voiceover: z3.object({ voice_description: z3.string().optional() }).loose().optional()
8340
+ voiceover: z3.object({
8341
+ // on_camera | mixed → mouths are on screen (lip-sync candidates);
8342
+ // voiceover | none → narration over the picture (no lip-sync).
8343
+ mode: z3.string().optional(),
8344
+ voice_description: z3.string().optional()
8345
+ }).loose().optional()
8263
8346
  }).loose().optional(),
8264
8347
  scenes: z3.array(Scene).min(1)
8265
8348
  }).loose();
@@ -8391,7 +8474,17 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
8391
8474
  ].join("\n");
8392
8475
  const description = framePrompt?.trim() || `the ${edge} frame of scene ${sceneIndex + 1} \u2014 describe the full composition, subjects, setting, action, lighting, and palette here. (Edit this line to change ONLY this frame.)`;
8393
8476
  return [
8394
- `Render the ${EDGE} frame of scene ${sceneIndex + 1} as a single still image. This prompt is self-contained and edit-per-frame: change the FRAME DESCRIPTION below to alter ONLY this frame. EXCLUDE all overlay text, captions, stickers, and watermarks \u2014 they are composited on top later.`,
8477
+ `Render the ${EDGE} frame of scene ${sceneIndex + 1} as a single still image. This prompt is self-contained and edit-per-frame: change the FRAME DESCRIPTION below to alter ONLY this frame.`,
8478
+ "",
8479
+ "CRITICAL \u2014 RENDER A CLEAN PLATE WITH ZERO TEXT OR GRAPHICS:",
8480
+ "This frame is a background plate. ALL words, captions, headlines, lower-third bars,",
8481
+ "news tickers/crawls, chyrons, on-screen logos/wordmarks, station bugs, watermarks,",
8482
+ "subtitles, UI and numbers are added afterwards as a separate HTML layer. Render NONE",
8483
+ "of them \u2014 no legible text anywhere in the image, not even in the background, on the",
8484
+ "news desk, on screens, or as part of a 'broadcast look'. If a reference image (a logo,",
8485
+ "a desk, a studio) contains any text or graphics, DO NOT reproduce that text \u2014 render",
8486
+ "the subject/scene only, with blank surfaces where text would be. Imperfect/garbled",
8487
+ "letterforms are the worst outcome; leave those areas clean.",
8395
8488
  "",
8396
8489
  "REFERENCE IMAGES (in the order provided):",
8397
8490
  legend,
@@ -8399,7 +8492,7 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
8399
8492
  "FRAME DESCRIPTION (this frame's editable prompt):",
8400
8493
  description,
8401
8494
  "",
8402
- "Keep every recurring element identical to its reference image across all frames. Use the GLOBAL STYLE REFERENCE only for shared cast identity, palette, typography mood, and aspect ratio \u2014 do NOT copy another scene's composition from it; this frame's content is the FRAME DESCRIPTION above.",
8495
+ "Keep every recurring element identical to its reference image across all frames. Use the GLOBAL STYLE REFERENCE only for shared cast identity, palette, typography mood, and aspect ratio \u2014 do NOT copy another scene's composition from it; this frame's content is the FRAME DESCRIPTION above. Again: NO rendered text or graphic overlays \u2014 clean plate only.",
8403
8496
  "",
8404
8497
  "GLOBAL STYLE REFERENCE (shared across frames; not this frame's content):",
8405
8498
  "{{target_blueprint}}"
@@ -8450,7 +8543,7 @@ function buildSeedancePrompt(scene, sceneIndex, present) {
8450
8543
  if (transcript) parts.push(`Transcript: ${transcript}`);
8451
8544
  return parts.join("\n");
8452
8545
  }
8453
- function buildSceneVisuals(blueprint, slots, opts, nodes) {
8546
+ function buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns) {
8454
8547
  const ar = aspectRatioParam(blueprint);
8455
8548
  const reuse = opts.frames === "reuse";
8456
8549
  const clipRefs = [];
@@ -8472,10 +8565,11 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
8472
8565
  ctx,
8473
8566
  nodes
8474
8567
  );
8568
+ const dur = sceneDurationS(scene);
8475
8569
  const clipParams = {
8476
8570
  model: opts.videoModel,
8477
8571
  prompt: buildSeedancePrompt(scene, i, slotsForScene(slots, i)),
8478
- duration: snapToSeedance(scene.duration_s ?? 5)
8572
+ duration: ceilToSeedance(dur)
8479
8573
  };
8480
8574
  if (ar) clipParams.aspect_ratio = ar;
8481
8575
  nodes.push({
@@ -8484,7 +8578,29 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
8484
8578
  inputs: { first_frame: firstFrame, last_frame: lastFrame },
8485
8579
  params: clipParams
8486
8580
  });
8487
- clipRefs.push(`$ref:s${i}_clip.video`);
8581
+ let base = `$ref:s${i}_clip.video`;
8582
+ const onCam = (sceneTurns.get(i) ?? []).filter((t) => t.onCamera);
8583
+ const solo = onCam.length === 1 ? onCam[0] : void 0;
8584
+ if (solo) {
8585
+ nodes.push({
8586
+ id: `s${i}_lipsync`,
8587
+ type: "video_lipsync",
8588
+ inputs: { video: base, audio: solo.audioRef },
8589
+ params: { model: FIXED_LIPSYNC_MODEL }
8590
+ });
8591
+ base = `$ref:s${i}_lipsync.video`;
8592
+ }
8593
+ if (ceilToSeedance(dur) === dur) {
8594
+ clipRefs.push(base);
8595
+ } else {
8596
+ nodes.push({
8597
+ id: `s${i}_trim`,
8598
+ type: "ffmpeg",
8599
+ inputs: { clip: base },
8600
+ params: { args: trimArgs(dur), outputs: { video: { kind: "video", ext: "mp4" } } }
8601
+ });
8602
+ clipRefs.push(`$ref:s${i}_trim.video`);
8603
+ }
8488
8604
  });
8489
8605
  return clipRefs;
8490
8606
  }
@@ -8497,8 +8613,21 @@ function musicBedPrompt(blueprint, musicPrompt) {
8497
8613
 
8498
8614
  Reference vibe: the original used "${title}"${by} (identified via AudD). Match its mood, tempo, and energy with ORIGINAL music \u2014 do not reproduce the track.`;
8499
8615
  }
8500
- function buildAudio(blueprint, nodes) {
8616
+ function onCameraDialogue(blueprint) {
8617
+ const mode = blueprint.global?.voiceover?.mode;
8618
+ return mode !== "voiceover" && mode !== "none";
8619
+ }
8620
+ var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
8621
+ function isOnCameraSpeaker(speaker, casts, cameraOn) {
8622
+ if (!cameraOn) return false;
8623
+ if (NARRATOR_SPEAKERS.has(speaker.toLowerCase())) return false;
8624
+ return casts.has(speaker);
8625
+ }
8626
+ function buildDialogue(blueprint, nodes) {
8501
8627
  const tracks = [];
8628
+ const sceneTurns = /* @__PURE__ */ new Map();
8629
+ const casts = castIdSet(blueprint);
8630
+ const cameraOn = onCameraDialogue(blueprint);
8502
8631
  const voiceNodeBySpeaker = /* @__PURE__ */ new Map();
8503
8632
  const speakerDescription = (speaker) => {
8504
8633
  for (const scene of blueprint.scenes) {
@@ -8517,41 +8646,62 @@ function buildAudio(blueprint, nodes) {
8517
8646
  voiceNodeBySpeaker.set(speaker, id);
8518
8647
  return id;
8519
8648
  };
8520
- const scriptBySpeaker = /* @__PURE__ */ new Map();
8521
- const orderedSpeakers = [];
8522
- for (const scene of blueprint.scenes) {
8523
- for (const line of scene.dialogue ?? []) {
8524
- if (!line.line) continue;
8525
- const speaker = line.speaker ?? "voiceover";
8526
- const start = line.start_s ?? scene.start_s ?? 0;
8527
- const existing = scriptBySpeaker.get(speaker);
8528
- if (existing) {
8529
- existing.lines.push(line.line);
8530
- existing.start = Math.min(existing.start, start);
8531
- } else {
8532
- scriptBySpeaker.set(speaker, { lines: [line.line], start });
8533
- orderedSpeakers.push(speaker);
8534
- }
8535
- }
8536
- }
8537
8649
  const usedVoIds = /* @__PURE__ */ new Set();
8538
- orderedSpeakers.forEach((speaker, idx) => {
8539
- const script = scriptBySpeaker.get(speaker);
8540
- if (!script) return;
8541
- const voiceNode = ensureVoiceNode(speaker);
8542
- let id = sanitizeId2(`vo_${speaker}`, `vo_${idx}`);
8543
- while (usedVoIds.has(id)) id = `${id}_${idx}`;
8544
- usedVoIds.add(id);
8545
- nodes.push({
8546
- id,
8547
- type: "tts",
8548
- inputs: { voice_ref: `$ref:${voiceNode}.voice_id` },
8549
- // Join lines with a space: each line keeps its own terminal punctuation, so
8550
- // sentence boundaries (and the pauses they imply) survive into one read.
8551
- params: { model: FIXED_TTS_MODEL, text: script.lines.join(" "), voice: "{{voice_ref}}" }
8650
+ blueprint.scenes.forEach((scene, sceneIndex) => {
8651
+ const lines = (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).slice().sort((a, b) => (a.start_s ?? 0) - (b.start_s ?? 0));
8652
+ if (lines.length === 0) return;
8653
+ const groups = [];
8654
+ for (const line of lines) {
8655
+ const speaker = line.speaker ?? "voiceover";
8656
+ const last = groups[groups.length - 1];
8657
+ if (last && last.speaker === speaker) last.lines.push(line);
8658
+ else groups.push({ speaker, lines: [line] });
8659
+ }
8660
+ const list = [];
8661
+ groups.forEach((group, gi) => {
8662
+ const first = group.lines[0];
8663
+ const last = group.lines[group.lines.length - 1];
8664
+ if (!first || !last) return;
8665
+ const start = first.start_s ?? scene.start_s ?? 0;
8666
+ const end = last.end_s ?? last.start_s ?? scene.end_s ?? start;
8667
+ const voiceNode = ensureVoiceNode(group.speaker);
8668
+ let id = sanitizeId2(`vo_s${sceneIndex}_${group.speaker}`, `vo_${sceneIndex}_${gi}`);
8669
+ if (usedVoIds.has(id)) {
8670
+ let n = 2;
8671
+ while (usedVoIds.has(`${id}_${n}`)) n++;
8672
+ id = `${id}_${n}`;
8673
+ }
8674
+ usedVoIds.add(id);
8675
+ nodes.push({
8676
+ id,
8677
+ type: "tts",
8678
+ inputs: { voice_ref: `$ref:${voiceNode}.voice_id` },
8679
+ // Lines join with a space; each keeps its terminal punctuation so eleven_v3
8680
+ // reads the sentence boundaries (and their pauses) within the one turn.
8681
+ params: {
8682
+ model: FIXED_TTS_MODEL,
8683
+ text: group.lines.map((l) => l.line.trim()).join(" "),
8684
+ voice: "{{voice_ref}}"
8685
+ }
8686
+ });
8687
+ const turn = {
8688
+ sceneIndex,
8689
+ speaker: group.speaker,
8690
+ onCamera: isOnCameraSpeaker(group.speaker, casts, cameraOn),
8691
+ start_s: start,
8692
+ end_s: end,
8693
+ ttsId: id,
8694
+ audioRef: `$ref:${id}.audio`
8695
+ };
8696
+ list.push(turn);
8697
+ tracks.push({ slot: id, ref: turn.audioRef, start_s: start, end_s: end, kind: "vo" });
8552
8698
  });
8553
- tracks.push({ slot: id, ref: `$ref:${id}.audio`, start_s: script.start });
8699
+ sceneTurns.set(sceneIndex, list);
8554
8700
  });
8701
+ return { tracks, sceneTurns };
8702
+ }
8703
+ function buildSfxMusic(blueprint, nodes) {
8704
+ const tracks = [];
8555
8705
  blueprint.scenes.forEach((scene, i) => {
8556
8706
  (scene.sfx ?? []).forEach((sfx, k) => {
8557
8707
  const text = sfx.sound_effect_prompt ?? sfx.description;
@@ -8560,7 +8710,12 @@ function buildAudio(blueprint, nodes) {
8560
8710
  const params = { model: FIXED_SFX_MODEL, text };
8561
8711
  if (typeof sfx.duration_s === "number") params.duration_seconds = Math.min(Math.max(sfx.duration_s, 0.5), 30);
8562
8712
  nodes.push({ id, type: "sound_effect", params });
8563
- tracks.push({ slot: `sfx_s${i}_${k}`, ref: `$ref:${id}.audio`, start_s: sfx.at_s ?? scene.start_s ?? 0 });
8713
+ tracks.push({
8714
+ slot: `sfx_s${i}_${k}`,
8715
+ ref: `$ref:${id}.audio`,
8716
+ start_s: sfx.at_s ?? scene.start_s ?? 0,
8717
+ kind: "sfx"
8718
+ });
8564
8719
  });
8565
8720
  });
8566
8721
  const musicPrompt = blueprint.global?.music?.music_prompt;
@@ -8572,10 +8727,90 @@ function buildAudio(blueprint, nodes) {
8572
8727
  type: "music",
8573
8728
  params: { model: FIXED_MUSIC_MODEL, prompt: musicBedPrompt(blueprint, musicPrompt), music_length_ms: musicMs }
8574
8729
  });
8575
- tracks.unshift({ slot: "music", ref: "$ref:music_bed.audio", start_s: 0, gain_db: MUSIC_BED_GAIN_DB });
8730
+ tracks.push({ slot: "music", ref: "$ref:music_bed.audio", start_s: 0, gain_db: MUSIC_BED_GAIN_DB, kind: "music" });
8576
8731
  }
8577
8732
  return tracks;
8578
8733
  }
8734
+ var OverlayStyle = z3.object({ color_hex: z3.string().optional(), background: z3.string().optional(), size: z3.string().optional() }).loose();
8735
+ var Overlay = z3.object({
8736
+ text: z3.string().optional(),
8737
+ appears_at_s: z3.number().optional(),
8738
+ duration_s: z3.number().optional(),
8739
+ position: z3.string().optional(),
8740
+ role: z3.string().optional(),
8741
+ animation: z3.string().optional(),
8742
+ animation_detail: z3.string().optional(),
8743
+ style: OverlayStyle.optional()
8744
+ }).loose();
8745
+ var FloatingElement = z3.object({
8746
+ kind: z3.string().optional(),
8747
+ description: z3.string().optional(),
8748
+ brand_name: z3.string().nullish(),
8749
+ what_it_represents: z3.string().optional(),
8750
+ appears_at_s: z3.number().optional(),
8751
+ duration_s: z3.number().optional(),
8752
+ position: z3.string().optional()
8753
+ }).loose();
8754
+ function escapeHtml(s) {
8755
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
8756
+ }
8757
+ function commentSafe(s) {
8758
+ return escapeHtml(s).replace(/-{2,}/g, "\u2013");
8759
+ }
8760
+ var SUPPORTED_ANIMS = /* @__PURE__ */ new Set(["fade", "slide_up", "slide_down", "pop"]);
8761
+ function normalizeAnim(animation) {
8762
+ if (!animation || animation === "none") return void 0;
8763
+ const mapped = animation === "slide" ? "slide_up" : animation;
8764
+ return SUPPORTED_ANIMS.has(mapped) ? mapped : void 0;
8765
+ }
8766
+ function positionClass(position) {
8767
+ const p = (position ?? "bottom_center").toLowerCase().replace(/[^a-z]+/g, "-");
8768
+ return `pos-${p}`;
8769
+ }
8770
+ function overlayElement(ov, sceneStart) {
8771
+ if (!ov.text?.trim()) return "";
8772
+ const at = ov.appears_at_s ?? sceneStart;
8773
+ const dur = ov.duration_s ?? 2.5;
8774
+ const role = ov.role ? ` data-role="${escapeHtml(ov.role)}"` : "";
8775
+ const normAnim = normalizeAnim(ov.animation);
8776
+ const anim = normAnim ? ` data-anim="${normAnim}"` : "";
8777
+ const detail = ov.animation_detail ? ` data-anim-detail="${escapeHtml(ov.animation_detail)}"` : "";
8778
+ return `<div class="ov ${positionClass(ov.position)}" data-start="${at}" data-dur="${dur}"${role}${anim}${detail}>${escapeHtml(ov.text.trim())}</div>`;
8779
+ }
8780
+ function floatingStub(fe, sceneStart) {
8781
+ const at = fe.appears_at_s ?? sceneStart;
8782
+ const dur = fe.duration_s ?? 2.5;
8783
+ const kind = commentSafe(fe.kind ?? "element");
8784
+ const label = commentSafe(fe.brand_name || fe.what_it_represents || fe.description || fe.kind || "element");
8785
+ const slug = (fe.kind ?? "element").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "element";
8786
+ return [
8787
+ `<!-- ${kind}: ${label} @ ${at}s for ${dur}s (${positionClass(fe.position)}). Drop an image in this dir and uncomment:`,
8788
+ `<img class="ov ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
8789
+ ].join("\n");
8790
+ }
8791
+ function buildOverlayHtml(input) {
8792
+ const blueprint = VideoBlueprint.parse(input);
8793
+ const blocks = [
8794
+ [
8795
+ "<!-- \u2B07 OVERLAY LAYER \u2014 this is YOUR HTML to paint. The reference's overlays are",
8796
+ " seeded below as plain elements (text + position class + data-start/data-dur).",
8797
+ " Restyle freely in <style>, regroup, animate, swap a logo placeholder for a",
8798
+ " real <img> you drop in this dir. The runtime only shows/hides by timestamp;",
8799
+ " it makes NO styling decisions. Positions: edit the .pos-* classes or add your own. -->"
8800
+ ].join("\n")
8801
+ ];
8802
+ for (const scene of blueprint.scenes) {
8803
+ const sceneStart = scene.start_s ?? 0;
8804
+ const overlays = z3.array(Overlay).safeParse(scene.overlays ?? []);
8805
+ const floats = z3.array(FloatingElement).safeParse(scene.floating_elements ?? []);
8806
+ const parts = [
8807
+ ...overlays.success ? overlays.data.map((ov) => overlayElement(ov, sceneStart)) : [],
8808
+ ...floats.success ? floats.data.map((fe) => floatingStub(fe, sceneStart)) : []
8809
+ ].filter(Boolean);
8810
+ if (parts.length > 0) blocks.push(parts.join("\n"));
8811
+ }
8812
+ return blocks.join("\n\n");
8813
+ }
8579
8814
  function lastSceneEnd(blueprint) {
8580
8815
  let end = 0;
8581
8816
  for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
@@ -8607,7 +8842,8 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
8607
8842
  params: { source: "path", path: todoPath2(elements[i], slot.label), expect: "image" }
8608
8843
  });
8609
8844
  });
8610
- const clipRefs = buildSceneVisuals(blueprint, slots, opts, nodes);
8845
+ const { tracks: voTracks, sceneTurns } = buildDialogue(blueprint, nodes);
8846
+ const clipRefs = buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns);
8611
8847
  const concatInputs = {};
8612
8848
  clipRefs.forEach((ref, i) => {
8613
8849
  concatInputs[`c${i}`] = ref;
@@ -8627,12 +8863,12 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
8627
8863
  id: "overlaid",
8628
8864
  type: "hyperframe_render",
8629
8865
  inputs: { background: videoRef },
8630
- params: { composition: opts.overlayCompositionPath, overlays, floating_elements: floating }
8866
+ params: { composition: opts.overlayCompositionPath }
8631
8867
  });
8632
8868
  videoRef = "$ref:overlaid.video";
8633
8869
  videoNode = "overlaid";
8634
8870
  }
8635
- const tracks = buildAudio(blueprint, nodes);
8871
+ const tracks = [...voTracks, ...buildSfxMusic(blueprint, nodes)];
8636
8872
  if (tracks.length > 0) {
8637
8873
  const mixInputs = {};
8638
8874
  for (const t of tracks) mixInputs[t.slot] = t.ref;
@@ -8665,8 +8901,18 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
8665
8901
  "1:a:0",
8666
8902
  "-c:v",
8667
8903
  "copy",
8904
+ // The raw mix is a quiet mono track (tts + ducked bed), which reads as
8905
+ // "no sound" in casual players. Normalize integrated loudness to the
8906
+ // social/broadcast target (-14 LUFS, -1.5 dBTP) and upmix to stereo so
8907
+ // every rendered ad is loud and plays everywhere.
8908
+ "-af",
8909
+ "loudnorm=I=-14:TP=-1.5:LRA=11,aformat=channel_layouts=stereo",
8668
8910
  "-c:a",
8669
8911
  "aac",
8912
+ "-b:a",
8913
+ "192k",
8914
+ "-ar",
8915
+ "48000",
8670
8916
  "-shortest",
8671
8917
  "{{out.video}}"
8672
8918
  ],
@@ -8680,43 +8926,69 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
8680
8926
  metadata: {
8681
8927
  name: "video reproduction",
8682
8928
  description: VIDEO_GUIDE,
8683
- todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts)
8929
+ todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts),
8930
+ // The timing plan `baker canvas validate` checks before any billed render:
8931
+ // sequenced voiceover turns (no overlap), audio ≈ video length, and which
8932
+ // scenes must be lip-synced.
8933
+ video: buildVideoMeta(blueprint, sceneTurns)
8684
8934
  },
8685
8935
  nodes,
8686
8936
  output: { node: videoNode, output: "video" }
8687
8937
  };
8688
8938
  }
8939
+ function buildVideoMeta(blueprint, sceneTurns) {
8940
+ const vo_segments = [];
8941
+ const talking_scenes = [];
8942
+ for (const [scene, turns] of [...sceneTurns.entries()].sort((a, b) => a[0] - b[0])) {
8943
+ for (const t of turns) {
8944
+ vo_segments.push({ slot: t.ttsId, start_s: t.start_s, end_s: t.end_s, scene, speaker: t.speaker });
8945
+ }
8946
+ if (turns.filter((t) => t.onCamera).length === 1) {
8947
+ talking_scenes.push({ scene, lipsync_node: `s${scene}_lipsync` });
8948
+ }
8949
+ }
8950
+ return {
8951
+ duration_s: blueprint.source?.duration_s ?? lastSceneEnd(blueprint),
8952
+ vo_segments,
8953
+ talking_scenes
8954
+ };
8955
+ }
8689
8956
  var VIDEO_GUIDE = [
8690
- "Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video. Each scene is two AI-generated boundary frames (start/end) animated into a clip, concatenated, overlaid with timed text, and mixed with voiceover + SFX + music. Edit it, supply the real assets, then run.",
8957
+ "Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video. Per scene: two AI-generated CLEAN-PLATE frames (no baked text) \u2192 a clip \u2192 trimmed to the real scene length so the picture stays on the audio timeline \u2192 optional lip-sync \u2192 concatenated. On-screen text is a separate HTML layer you paint; audio is sequenced voiceover + SFX + a ducked music bed, normalized stereo. It is a DRAFT: edit it, supply the real assets, then validate and run.",
8691
8958
  "",
8692
8959
  "WHAT TO DO NEXT:",
8693
8960
  "1. Edit each frame's prompt IN PLACE. Every `s<i>_start` / `s<i>_end` node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want.",
8694
- "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent.",
8695
- "3. Confirm the `voice_select` casting (one per speaker). The voiceover is ONE continuous `tts` per speaker \u2014 punctuation, ALL-CAPS, and spacing are read verbatim by eleven_v3 for emphasis/pauses, so edit `params.text` to shape delivery.",
8696
- "4. Text overlays are composited on top (not baked into frames) by the `overlaid` node \u2014 edit the `overlays` array there. For on-brand type, drop `brand-bold.otf` / `brand-regular.otf` into the `video-overlay-composition/` dir (referenced via @font-face); otherwise a system font is used. You don't always need text \u2014 it's often cleaner to overlay it than bake it in.",
8697
- "5. `baker canvas validate` then `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free.",
8961
+ "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
8962
+ "3. Confirm the `voice_select` casting (one per speaker). Voiceover is SEQUENCED: each contiguous same-speaker turn is its own `tts` placed at its real start, so dialogue alternates instead of stacking. Edit a turn's `params.text` (punctuation / ALL-CAPS / line breaks are read verbatim by eleven_v3 for emphasis and pauses) to shape delivery; re-author the words to be TRUE for your brand.",
8963
+ "4. Lip-sync: scenes with a single on-camera speaker route their clip through `video_lipsync` (~20 cr each) so the mouth matches the line. Two-speaker scenes are left un-synced \u2014 split them or pick a primary speaker if you want sync. Drop the node to skip.",
8964
+ "5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
8965
+ "6. `baker canvas validate` (proves audio/lip-sync timing for free) then `baker canvas run` (generates many billed image/video/audio assets \u2014 not free).",
8698
8966
  "",
8699
8967
  "Tip: `prompt.json` is the deconstruction provenance + the demoted GLOBAL STYLE REFERENCE each frame reads for shared palette/cast cohesion. It is NOT the per-frame editing surface \u2014 the frame nodes are."
8700
8968
  ].join("\n");
8701
8969
  function buildVideoTodo(report, overlayCount, floatingCount, opts) {
8702
8970
  return {
8703
- edit_frames_in_place: "Each s<i>_start / s<i>_end node has its own editable params.prompt (FRAME DESCRIPTION). Edit per frame; the blueprint is only a shared style reference.",
8971
+ edit_frames_in_place: "Each s<i>_start / s<i>_end node has its own editable params.prompt (FRAME DESCRIPTION). Edit per frame; the blueprint is only a shared style reference. Frames are CLEAN PLATES \u2014 they render no on-screen text; all text is the overlay HTML layer.",
8704
8972
  frames_mode: opts.frames ?? "generate",
8973
+ assets_required: "MANDATORY: drop a real image at every el_* [TODO] ingest before running \u2014 `baker canvas run` refuses to start (and bills nothing) until each placeholder holds a real source.",
8705
8974
  recurring_elements_to_supply: report.elements,
8975
+ text_strategy: "Decide per ad: text is either baked by the generated creative OR painted via the overlay HTML \u2014 not both. Default here is clean text-free frames + the HTML overlay layer (video-overlay-composition/index.html) as the single text source, which you fully control.",
8976
+ timeline: "Automatic: each clip is generated at >= its scene length then trimmed back to the real scene duration, so the concatenated picture stays on the same timeline as the absolute-timed audio (this is what makes the lips line up). You don't manage it.",
8706
8977
  voices_to_confirm: report.dialogue.map((d) => ({
8707
8978
  scene: d.scene,
8708
8979
  speaker: d.speaker,
8709
8980
  voice_description: d.voice_description,
8710
8981
  line: d.line
8711
8982
  })),
8712
- voiceover_note: "One continuous tts per speaker; same voice locked via voice_select.voice_id. Use punctuation / ALL CAPS / line breaks in params.text for emphasis and pacing (read verbatim).",
8983
+ voiceover_note: "Sequenced: one tts per contiguous same-speaker TURN, placed at its real start_s so turns alternate (no parallel monologues); same voice locked via voice_select.voice_id. Edit a turn's params.text (punctuation / ALL CAPS / line breaks read verbatim) to shape delivery.",
8984
+ lip_sync_note: "Scenes with a single on-camera speaker route their clip through video_lipsync (~20 cr each) so the mouth matches the line. Two-speaker scenes are left un-synced (one track can't drive two faces) \u2014 split or pick a primary. `baker canvas validate` checks every talking scene is synced.",
8713
8985
  text_overlays: {
8714
8986
  count: overlayCount,
8715
- note: "Composited by the `overlaid` node, animated per the blueprint (fade/pop/slide/typewriter/karaoke). Edit the `overlays` array. Drop brand-*.otf into video-overlay-composition/ for on-brand type."
8987
+ note: "Seeded as editable HTML inside `#overlay-root` in video-overlay-composition/index.html (text + a .pos-* class + data-start/data-dur). PAINT it: restyle the CSS, build lower-thirds/tickers, drop brand-*.otf for on-brand type. The runtime only shows/hides by timestamp."
8716
8988
  },
8717
8989
  floating_elements: {
8718
8990
  count: floatingCount,
8719
- note: floatingCount > 0 ? "Rendered as labeled placeholders. Replace with the real logo/sticker/cutout art (recurring logos are better handled as an el_* element baked into frames)." : "none detected"
8991
+ note: floatingCount > 0 ? "Seeded as labeled placeholders in index.html \u2014 replace each with a real <img> you drop into video-overlay-composition/. Recurring logos are also handled well as an el_* element baked into frames." : "none detected"
8720
8992
  },
8721
8993
  sound_effects: { count: report.sfx_count },
8722
8994
  music: {
@@ -8768,8 +9040,24 @@ function videoReport(input, elementsInput) {
8768
9040
  };
8769
9041
  }
8770
9042
 
9043
+ // src/commands/canvas/composition-path.ts
9044
+ import { existsSync as existsSync3 } from "fs";
9045
+ import path4 from "path";
9046
+ function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
9047
+ const rel = path4.join("canvas", name);
9048
+ let dir = startDir;
9049
+ for (let i = 0; i < maxDepth; i++) {
9050
+ const candidate = path4.join(dir, rel);
9051
+ if (exists(path4.join(candidate, "meta.json"))) return candidate;
9052
+ const parent = path4.dirname(dir);
9053
+ if (parent === dir) break;
9054
+ dir = parent;
9055
+ }
9056
+ return path4.resolve(startDir, "../../../", rel);
9057
+ }
9058
+
8771
9059
  // src/commands/canvas/scaffold-video.ts
8772
- var SHIPPED_COMPOSITION_DIR = path4.resolve(import.meta.dirname, "../../../canvas/video-overlay-composition");
9060
+ var SHIPPED_COMPOSITION_DIR = resolveShippedCanvasDir("video-overlay-composition", import.meta.dirname);
8773
9061
  function resolveModel2(kind, preferred) {
8774
9062
  const ids = Object.keys(MODEL_REGISTRY[kind]);
8775
9063
  return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
@@ -8890,13 +9178,19 @@ var scaffoldVideoCommand = defineCommand76({
8890
9178
  "video-model": { type: "string", description: "Override the video_generate model id for clips" }
8891
9179
  },
8892
9180
  async run({ args }) {
8893
- const videoPath = path4.resolve(String(args.file));
8894
- const base = path4.basename(videoPath, path4.extname(videoPath));
8895
- const outPath = args.out ? path4.resolve(String(args.out)) : path4.join(path4.dirname(videoPath), `${base}.video.canvas.json`);
8896
- const outDir = path4.dirname(outPath);
8897
- const blueprintPath = path4.join(outDir, "prompt.json");
9181
+ const videoPath = path5.resolve(String(args.file));
9182
+ const base = path5.basename(videoPath, path5.extname(videoPath));
9183
+ const outPath = args.out ? path5.resolve(String(args.out)) : path5.join(path5.dirname(videoPath), `${base}.video.canvas.json`);
9184
+ const outDir = path5.dirname(outPath);
9185
+ const blueprintPath = path5.join(outDir, "prompt.json");
8898
9186
  const frames = args.frames === "reuse" ? "reuse" : "generate";
8899
9187
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
9188
+ if (Number.isFinite(maxScenes)) {
9189
+ process.stderr.write(
9190
+ `\u26A0\uFE0F --max-scenes ${maxScenes} caps the deconstruct: any scenes beyond ${maxScenes} are MERGED away, reducing fidelity (fewer cuts, lost beats). Omit it to reproduce every scene.
9191
+ `
9192
+ );
9193
+ }
8900
9194
  const { deconstructModel, selectModel, imageModel, videoModel } = resolveModels2(args);
8901
9195
  const analysisCanvas = buildAnalysisCanvas(videoPath, deconstructModel, selectModel, {
8902
9196
  maxScenes: Number.isFinite(maxScenes) ? maxScenes : void 0,
@@ -8908,8 +9202,19 @@ var scaffoldVideoCommand = defineCommand76({
8908
9202
  const annotated = annotateBlueprintWithElements(blueprint, elements);
8909
9203
  await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
8910
9204
  `, "utf8");
8911
- const compositionDest = path4.join(outDir, "video-overlay-composition");
9205
+ const compositionDest = path5.join(outDir, "video-overlay-composition");
8912
9206
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
9207
+ const indexPath = path5.join(compositionDest, "index.html");
9208
+ const overlayHtml = buildOverlayHtml(blueprint);
9209
+ const indexHtml = await readFile4(indexPath, "utf8");
9210
+ const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
9211
+ if (injected === indexHtml && overlayHtml.trim()) {
9212
+ fail2(
9213
+ "composition_marker_missing",
9214
+ `video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
9215
+ );
9216
+ }
9217
+ await writeFile2(indexPath, injected, "utf8");
8913
9218
  const opts = {
8914
9219
  imageModel,
8915
9220
  videoModel,
@@ -8952,7 +9257,7 @@ var scaffoldVideoCommand = defineCommand76({
8952
9257
  run_estimated_credits: validation.estimatedCredits
8953
9258
  },
8954
9259
  checklist: {
8955
- edit_prompt: `Edit ${path4.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
9260
+ edit_prompt: `Edit ${path5.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
8956
9261
  recurring_elements_to_supply: report.elements,
8957
9262
  voices_to_confirm: report.dialogue.map((d) => ({
8958
9263
  scene: d.scene,
@@ -8977,7 +9282,7 @@ var scaffoldVideoCommand = defineCommand76({
8977
9282
 
8978
9283
  // src/commands/canvas/validate.ts
8979
9284
  import { readFile as readFile5 } from "fs/promises";
8980
- import path5 from "path";
9285
+ import path6 from "path";
8981
9286
  import { defineCommand as defineCommand77 } from "citty";
8982
9287
  var validateCommand = defineCommand77({
8983
9288
  meta: {
@@ -8986,7 +9291,7 @@ var validateCommand = defineCommand77({
8986
9291
  },
8987
9292
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
8988
9293
  async run({ args }) {
8989
- const filePath = path5.resolve(String(args.file));
9294
+ const filePath = path6.resolve(String(args.file));
8990
9295
  const raw = await readFile5(filePath, "utf8");
8991
9296
  let parsed;
8992
9297
  try {
@@ -9221,7 +9526,7 @@ Examples:
9221
9526
  });
9222
9527
 
9223
9528
  // src/commands/ga4/query.ts
9224
- import { appendFileSync as appendFileSync2, existsSync as existsSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
9529
+ import { appendFileSync as appendFileSync2, existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
9225
9530
  import { resolve as resolve2 } from "path";
9226
9531
  import { defineCommand as defineCommand81 } from "citty";
9227
9532
 
@@ -9295,7 +9600,7 @@ function writeRowsToFile2(filePath, rows, append) {
9295
9600
  const fields = extractFields2(rows);
9296
9601
  const ext = filePath.split(".").pop()?.toLowerCase();
9297
9602
  if (ext === "csv") {
9298
- if (!append || !existsSync3(filePath)) {
9603
+ if (!append || !existsSync4(filePath)) {
9299
9604
  writeFileSync4(filePath, `${toCsvRow(fields)}
9300
9605
  `, "utf-8");
9301
9606
  }
@@ -9306,12 +9611,12 @@ function writeRowsToFile2(filePath, rows, append) {
9306
9611
  const lines = rows.map((row) => JSON.stringify(row));
9307
9612
  const content = `${lines.join("\n")}
9308
9613
  `;
9309
- if (append && existsSync3(filePath)) {
9614
+ if (append && existsSync4(filePath)) {
9310
9615
  appendFileSync2(filePath, content, "utf-8");
9311
9616
  } else {
9312
9617
  writeFileSync4(filePath, content, "utf-8");
9313
9618
  }
9314
- } else if (append && existsSync3(filePath)) {
9619
+ } else if (append && existsSync4(filePath)) {
9315
9620
  const existing = JSON.parse(readFileSync6(filePath, "utf-8"));
9316
9621
  writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
9317
9622
  } else {
@@ -9452,7 +9757,7 @@ Examples:
9452
9757
  import { defineCommand as defineCommand86 } from "citty";
9453
9758
 
9454
9759
  // src/commands/gsc/query.ts
9455
- import { appendFileSync as appendFileSync3, existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9760
+ import { appendFileSync as appendFileSync3, existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
9456
9761
  import { resolve as resolve3 } from "path";
9457
9762
  import { defineCommand as defineCommand83 } from "citty";
9458
9763
 
@@ -9581,7 +9886,7 @@ function writeRowsToFile3(filePath, rows, append) {
9581
9886
  const fields = extractFields3(rows);
9582
9887
  const ext = filePath.split(".").pop()?.toLowerCase();
9583
9888
  if (ext === "csv") {
9584
- if (!append || !existsSync4(filePath)) {
9889
+ if (!append || !existsSync5(filePath)) {
9585
9890
  writeFileSync5(filePath, `${toCsvRow(fields)}
9586
9891
  `, "utf-8");
9587
9892
  }
@@ -9591,12 +9896,12 @@ function writeRowsToFile3(filePath, rows, append) {
9591
9896
  } else if (ext === "jsonl") {
9592
9897
  const content = `${rows.map((row) => JSON.stringify(row)).join("\n")}
9593
9898
  `;
9594
- if (append && existsSync4(filePath)) {
9899
+ if (append && existsSync5(filePath)) {
9595
9900
  appendFileSync3(filePath, content, "utf-8");
9596
9901
  } else {
9597
9902
  writeFileSync5(filePath, content, "utf-8");
9598
9903
  }
9599
- } else if (append && existsSync4(filePath)) {
9904
+ } else if (append && existsSync5(filePath)) {
9600
9905
  const existing = JSON.parse(readFileSync7(filePath, "utf-8"));
9601
9906
  writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
9602
9907
  } else {
@@ -9858,7 +10163,7 @@ Examples:
9858
10163
  });
9859
10164
 
9860
10165
  // src/commands/images/index.ts
9861
- import { defineCommand as defineCommand107 } from "citty";
10166
+ import { defineCommand as defineCommand109 } from "citty";
9862
10167
 
9863
10168
  // src/commands/images/crop.ts
9864
10169
  import { defineCommand as defineCommand87 } from "citty";
@@ -9914,9 +10219,9 @@ async function readImageBuffer(pathOrUrl) {
9914
10219
  }
9915
10220
  return readFile6(pathOrUrl);
9916
10221
  }
9917
- async function isDirectory(path6) {
10222
+ async function isDirectory(path7) {
9918
10223
  try {
9919
- const s = await stat2(path6);
10224
+ const s = await stat2(path7);
9920
10225
  return s.isDirectory();
9921
10226
  } catch {
9922
10227
  return false;
@@ -10275,8 +10580,157 @@ var findCommand = defineCommand91({
10275
10580
  }
10276
10581
  });
10277
10582
 
10278
- // src/commands/images/get.ts
10583
+ // src/commands/images/generate.ts
10584
+ import { readFile as readFile7 } from "fs/promises";
10279
10585
  import { defineCommand as defineCommand92 } from "citty";
10586
+ import sharp2 from "sharp";
10587
+ var GENERATE_TIMEOUT_MS = 18e4;
10588
+ var REFERENCE_MAX_EDGE = 1536;
10589
+ var MODEL_LIST = [...IMAGE_GENERATE_MODELS];
10590
+ registerSchema({
10591
+ command: "images.generate",
10592
+ description: "Generate an image with AI (OpenRouter image models) and ingest it into the library. Default model openai/gpt-5.4-image-2 \u2014 photo-realistic, cleanest text rendering. Pass --reference with image URLs AND/OR local file paths (brand logo, product shot, Pinterest pin, cropped photo) to ground the generation on real imagery for on-brand, realistic output.",
10593
+ args: {
10594
+ prompt: { type: "string", description: "What to generate", required: true },
10595
+ model: {
10596
+ type: "string",
10597
+ description: `Model (default openai/gpt-5.4-image-2). One of: ${MODEL_LIST.join(", ")}`,
10598
+ required: false,
10599
+ enum: MODEL_LIST
10600
+ },
10601
+ "aspect-ratio": {
10602
+ type: "string",
10603
+ description: "Aspect ratio, e.g. 1:1 (default), 16:9, 9:16, 4:5, 3:2 (Gemini flash also supports 1:4/8:1)",
10604
+ required: false
10605
+ },
10606
+ "image-size": {
10607
+ type: "string",
10608
+ description: "Output resolution: 1K (default) | 2K | 4K (Gemini flash also 0.5K)",
10609
+ required: false
10610
+ },
10611
+ reference: {
10612
+ type: "string",
10613
+ description: "Comma-separated visual references \u2014 each is a public image URL OR a local file path (a sandbox image: brand logo, cropped photo, screenshot). Local files are downscaled and inlined automatically. Grounds output in real imagery; applied in order.",
10614
+ required: false
10615
+ },
10616
+ strength: {
10617
+ type: "number",
10618
+ description: "Recraft v4.1 Pro Vector only: vectorization strength 0-1",
10619
+ required: false
10620
+ },
10621
+ "rgb-colors": {
10622
+ type: "string",
10623
+ description: 'Recraft only: JSON array of [r,g,b] palette triples, e.g. "[[10,10,10],[255,80,0]]"',
10624
+ required: false
10625
+ },
10626
+ "bg-rgb": {
10627
+ type: "string",
10628
+ description: 'Recraft only: JSON [r,g,b] background color, e.g. "[255,255,255]"',
10629
+ required: false
10630
+ },
10631
+ context: {
10632
+ type: "string",
10633
+ description: "Description context hint for the ingested row (overrides the prompt as the describe hint)",
10634
+ required: false
10635
+ }
10636
+ }
10637
+ });
10638
+ function parseJsonArg(raw, flag) {
10639
+ try {
10640
+ return JSON.parse(raw);
10641
+ } catch {
10642
+ throw new ApiError("VALIDATION_ERROR", `--${flag} must be valid JSON (got: ${raw})`);
10643
+ }
10644
+ }
10645
+ function buildGenerateBody(args, prompt) {
10646
+ const body = { prompt };
10647
+ if (args.model) body.model = args.model;
10648
+ if (args["aspect-ratio"]) body.aspectRatio = args["aspect-ratio"];
10649
+ if (args["image-size"]) body.imageSize = args["image-size"];
10650
+ if (args.strength !== void 0) body.strength = Number(args.strength);
10651
+ if (args["rgb-colors"]) body.rgbColors = parseJsonArg(args["rgb-colors"], "rgb-colors");
10652
+ if (args["bg-rgb"]) body.backgroundRgbColor = parseJsonArg(args["bg-rgb"], "bg-rgb");
10653
+ if (args.context) body.descriptionContext = args.context;
10654
+ return body;
10655
+ }
10656
+ async function resolveReferences(spec) {
10657
+ const trimmed = spec.trim();
10658
+ const entries = trimmed.startsWith("data:") ? [trimmed] : trimmed.split(",").map((s) => s.trim()).filter(Boolean);
10659
+ const out = [];
10660
+ for (const entry of entries) {
10661
+ if (isRemoteUrl(entry) || entry.startsWith("data:")) {
10662
+ out.push(entry);
10663
+ continue;
10664
+ }
10665
+ let raw;
10666
+ try {
10667
+ raw = await readFile7(entry);
10668
+ } catch {
10669
+ throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
10670
+ }
10671
+ let webp;
10672
+ try {
10673
+ webp = await sharp2(raw).resize({ width: REFERENCE_MAX_EDGE, height: REFERENCE_MAX_EDGE, fit: "inside", withoutEnlargement: true }).webp({ quality: 82 }).toBuffer();
10674
+ } catch {
10675
+ throw new ApiError("VALIDATION_ERROR", `Reference is not a readable image: ${entry}`);
10676
+ }
10677
+ out.push(`data:image/webp;base64,${webp.toString("base64")}`);
10678
+ }
10679
+ return out;
10680
+ }
10681
+ var generateCommand = defineCommand92({
10682
+ meta: {
10683
+ name: "generate",
10684
+ description: "Generate an image with AI and store it in the library (cost-tracked per request via OpenRouter usage). Models mirror the canvas: openai/gpt-5.4-image-2 (default \u2014 photoreal, cleanest text, best for ad/landing reproduction), google/gemini-3-pro-image-preview (Nano Banana Pro), google/gemini-3.5-flash & google/gemini-3.1-flash-image-preview (fast, extreme aspect ratios), recraft/recraft-v4.1-pro-vector (vector/SVG-style with palette control). The result is auto-ingested (describe + embed), so the next `baker images library` query finds it. Pass --reference with image URLs and/or local file paths (Pinterest, stock, brand assets, sandbox files) to ground generation in reality.\n\nExamples:\n baker images generate 'a friendly golden retriever sitting in a bright modern living room' --aspect-ratio 16:9\n baker images generate 'hero shot of a matte black water bottle on marble' --model google/gemini-3-pro-image-preview --image-size 2K\n baker images generate 'lifestyle photo matching this mood' --reference 'https://\u2026/ref1.jpg,https://\u2026/ref2.jpg'\n baker images generate 'put this product on a marble countertop, soft daylight' --reference './src/brand/logos/product.png,./refs/kitchen-mood.jpg'\n baker images generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
10685
+ },
10686
+ args: {
10687
+ prompt: { type: "positional", description: "What to generate", required: false },
10688
+ model: { type: "string", description: "Model id (default openai/gpt-5.4-image-2)", required: false },
10689
+ "aspect-ratio": { type: "string", description: "Aspect ratio (default 1:1)", required: false },
10690
+ "image-size": { type: "string", description: "1K (default) | 2K | 4K | 0.5K", required: false },
10691
+ reference: {
10692
+ type: "string",
10693
+ description: "Comma-separated reference image URLs and/or local file paths",
10694
+ required: false
10695
+ },
10696
+ strength: { type: "string", description: "Recraft vectorization strength 0-1", required: false },
10697
+ "rgb-colors": {
10698
+ type: "string",
10699
+ description: "Recraft palette JSON, e.g. [[10,10,10],[255,80,0]]",
10700
+ required: false
10701
+ },
10702
+ "bg-rgb": { type: "string", description: "Recraft background JSON, e.g. [255,255,255]", required: false },
10703
+ context: { type: "string", description: "Describe-hint override for the ingested row", required: false }
10704
+ },
10705
+ run: async ({ args }) => {
10706
+ try {
10707
+ const prompt = args.prompt;
10708
+ if (!prompt) {
10709
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Prompt is required" } });
10710
+ process.exit(1);
10711
+ }
10712
+ const body = buildGenerateBody(args, prompt);
10713
+ if (args.reference) {
10714
+ const refs = await resolveReferences(args.reference);
10715
+ if (refs.length > 0) body.referenceUrls = refs;
10716
+ }
10717
+ const data = await apiPost("/api/images/generate", body, {
10718
+ timeoutMs: GENERATE_TIMEOUT_MS
10719
+ });
10720
+ writeJson({ ok: true, data });
10721
+ } catch (err) {
10722
+ if (err instanceof ApiError) {
10723
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
10724
+ process.exit(1);
10725
+ }
10726
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
10727
+ process.exit(1);
10728
+ }
10729
+ }
10730
+ });
10731
+
10732
+ // src/commands/images/get.ts
10733
+ import { defineCommand as defineCommand93 } from "citty";
10280
10734
  registerSchema({
10281
10735
  command: "images.get",
10282
10736
  description: "Get a single image by ID",
@@ -10284,7 +10738,7 @@ registerSchema({
10284
10738
  id: { type: "string", description: "Image ID", required: true }
10285
10739
  }
10286
10740
  });
10287
- var getCommand2 = defineCommand92({
10741
+ var getCommand2 = defineCommand93({
10288
10742
  meta: { name: "get", description: "Get a single image by ID. Example: baker images get j571abc123" },
10289
10743
  args: {
10290
10744
  id: { type: "positional", description: "Image ID", required: false },
@@ -10320,7 +10774,7 @@ var getCommand2 = defineCommand92({
10320
10774
  });
10321
10775
 
10322
10776
  // src/commands/images/gif.ts
10323
- import { defineCommand as defineCommand93 } from "citty";
10777
+ import { defineCommand as defineCommand94 } from "citty";
10324
10778
  registerSchema({
10325
10779
  command: "images.gif",
10326
10780
  description: "Search Giphy for GIFs / reaction memes (paid social creative).",
@@ -10352,7 +10806,7 @@ registerSchema({
10352
10806
  }
10353
10807
  }
10354
10808
  });
10355
- var gifCommand = defineCommand93({
10809
+ var gifCommand = defineCommand94({
10356
10810
  meta: {
10357
10811
  name: "gif",
10358
10812
  description: "Search Giphy for GIFs / reaction memes \u2014 built for paid-social creative (Meta, TikTok, LinkedIn, X). Free API. Each hit carries WebP + GIF + MP4 URLs in providerMeta so you can pick the right format per platform.\n\nExample: baker images gif 'this is fine' --limit 10\nExample: baker images gif 'office reaction' --rating pg --auto-ingest 2\nExample: baker images gif --trending --limit 25"
@@ -10399,7 +10853,7 @@ var gifCommand = defineCommand93({
10399
10853
  });
10400
10854
 
10401
10855
  // src/commands/images/google.ts
10402
- import { defineCommand as defineCommand94 } from "citty";
10856
+ import { defineCommand as defineCommand95 } from "citty";
10403
10857
  registerSchema({
10404
10858
  command: "images.google",
10405
10859
  description: "Google Images search via the official Custom Search JSON API. Unverified source \u2014 inspect before placing.",
@@ -10435,7 +10889,7 @@ registerSchema({
10435
10889
  }
10436
10890
  }
10437
10891
  });
10438
- var googleCommand2 = defineCommand94({
10892
+ var googleCommand2 = defineCommand95({
10439
10893
  meta: {
10440
10894
  name: "google",
10441
10895
  description: "Google Images via the official Custom Search JSON API ($0.005/query, free 100/day). \u26A0 Source unverified \u2014 watermarks, low-res, mislabeled results are common. Use as last resort. With --auto-ingest, ingested hits return Baker-owned URLs.\n\nExample: baker images google 'industrial workshop' --type photo --size large --limit 20"
@@ -10483,7 +10937,7 @@ var googleCommand2 = defineCommand94({
10483
10937
  });
10484
10938
 
10485
10939
  // src/commands/images/icon.ts
10486
- import { defineCommand as defineCommand95 } from "citty";
10940
+ import { defineCommand as defineCommand96 } from "citty";
10487
10941
  registerSchema({
10488
10942
  command: "images.icon",
10489
10943
  description: "Icon lookup via Iconify (200+ icon sets, free CDN).",
@@ -10509,7 +10963,7 @@ registerSchema({
10509
10963
  }
10510
10964
  }
10511
10965
  });
10512
- var iconCommand = defineCommand95({
10966
+ var iconCommand = defineCommand96({
10513
10967
  meta: {
10514
10968
  name: "icon",
10515
10969
  description: "Icon via Iconify (simple-icons, logos, lucide, devicon, heroicons, tabler, phosphor, material-symbols, \u2026). Free CDN, no API key.\n\nExample: baker images icon react --set devicon\nExample: baker images icon lucide:check --color '#0a0a0a'"
@@ -10549,7 +11003,7 @@ var iconCommand = defineCommand95({
10549
11003
  });
10550
11004
 
10551
11005
  // src/commands/images/ingest.ts
10552
- import { defineCommand as defineCommand96 } from "citty";
11006
+ import { defineCommand as defineCommand97 } from "citty";
10553
11007
  registerSchema({
10554
11008
  command: "images.ingest",
10555
11009
  description: "Ingest a remote image URL into the library (full describe + embed).",
@@ -10561,7 +11015,7 @@ registerSchema({
10561
11015
  context: { type: "string", description: "Description context hint", required: false }
10562
11016
  }
10563
11017
  });
10564
- var ingestCommand = defineCommand96({
11018
+ var ingestCommand = defineCommand97({
10565
11019
  meta: {
10566
11020
  name: "ingest",
10567
11021
  description: "Download a remote URL and store it in the library. Hash-deduped on bytes + externalId.\n\nExample: baker images ingest https://img.freepik.com/free-photo/xyz.jpg --source magnific --external-id 12345"
@@ -10603,7 +11057,7 @@ var ingestCommand = defineCommand96({
10603
11057
  });
10604
11058
 
10605
11059
  // src/commands/images/library.ts
10606
- import { defineCommand as defineCommand97 } from "citty";
11060
+ import { defineCommand as defineCommand98 } from "citty";
10607
11061
  registerSchema({
10608
11062
  command: "images.library",
10609
11063
  description: "Search the company image library. Returns only ready images.",
@@ -10629,7 +11083,7 @@ registerSchema({
10629
11083
  }
10630
11084
  }
10631
11085
  });
10632
- var libraryCommand = defineCommand97({
11086
+ var libraryCommand = defineCommand98({
10633
11087
  meta: {
10634
11088
  name: "library",
10635
11089
  description: "Search the company image library (hybrid BM25 + vector + Cohere rerank). Use this BEFORE any external provider.\n\nExample: baker images library 'hero banner' --aspect-ratio 16:9 --source magnific"
@@ -10686,7 +11140,7 @@ var libraryCommand = defineCommand97({
10686
11140
  });
10687
11141
 
10688
11142
  // src/commands/images/logo.ts
10689
- import { defineCommand as defineCommand98 } from "citty";
11143
+ import { defineCommand as defineCommand99 } from "citty";
10690
11144
  registerSchema({
10691
11145
  command: "images.logo",
10692
11146
  description: "Brand logo lookup via Brandfetch CDN (fallback/404). Auto-ingests by default.",
@@ -10711,7 +11165,7 @@ registerSchema({
10711
11165
  }
10712
11166
  }
10713
11167
  });
10714
- var logoCommand = defineCommand98({
11168
+ var logoCommand = defineCommand99({
10715
11169
  meta: {
10716
11170
  name: "logo",
10717
11171
  description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nExample: baker images logo stripe.com --variant logo"
@@ -10749,7 +11203,7 @@ var logoCommand = defineCommand98({
10749
11203
  });
10750
11204
 
10751
11205
  // src/commands/images/normalize.ts
10752
- import { defineCommand as defineCommand99 } from "citty";
11206
+ import { defineCommand as defineCommand100 } from "citty";
10753
11207
 
10754
11208
  // src/lib/image/color-changer.ts
10755
11209
  import quantize from "quantize";
@@ -11097,7 +11551,7 @@ function solidifyEdges(data, saturationBoost = 0.25) {
11097
11551
  }
11098
11552
 
11099
11553
  // src/lib/image/image-processor.ts
11100
- import sharp2 from "sharp";
11554
+ import sharp3 from "sharp";
11101
11555
  function hasGradient(data) {
11102
11556
  const uniqueColors = /* @__PURE__ */ new Set();
11103
11557
  for (let i = 0; i < data.length; i += 4) {
@@ -11129,10 +11583,10 @@ function createSvgRecolorer(target, palette) {
11129
11583
  async function processGradient(data, info) {
11130
11584
  if (!hasGradient(data)) return { data, info };
11131
11585
  const solidified = solidifyEdges(data, 0.1);
11132
- const blurred = await sharp2(solidified, {
11586
+ const blurred = await sharp3(solidified, {
11133
11587
  raw: { channels: 4, width: info.width, height: info.height }
11134
11588
  }).png().blur(0.3).toBuffer();
11135
- const result = await sharp2(blurred).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
11589
+ const result = await sharp3(blurred).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
11136
11590
  return { data: result.data, info: result.info };
11137
11591
  }
11138
11592
  function isSvgBuffer(buffer) {
@@ -11141,13 +11595,13 @@ function isSvgBuffer(buffer) {
11141
11595
  }
11142
11596
  async function processInternal(inputBuffer, isSVG, options) {
11143
11597
  const stages = [];
11144
- const metadata = await sharp2(inputBuffer).metadata();
11598
+ const metadata = await sharp3(inputBuffer).metadata();
11145
11599
  let alreadyTransparent = false;
11146
11600
  if (metadata.hasAlpha) {
11147
- const { data: alphaData } = await sharp2(inputBuffer).raw().toBuffer({ resolveWithObject: true });
11601
+ const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
11148
11602
  alreadyTransparent = hasTransparency(alphaData, 0.05);
11149
11603
  }
11150
- let { data: processedData, info } = await sharp2(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
11604
+ let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
11151
11605
  if (options.color) {
11152
11606
  stages.push("recolor");
11153
11607
  processedData = applyQuantization(processedData, 8);
@@ -11218,7 +11672,7 @@ async function processInternal(inputBuffer, isSVG, options) {
11218
11672
  processedData = result.data;
11219
11673
  info = result.info;
11220
11674
  }
11221
- let pipeline = sharp2(processedData, {
11675
+ let pipeline = sharp3(processedData, {
11222
11676
  raw: { channels: 4, width: info.width, height: info.height }
11223
11677
  }).png();
11224
11678
  if (options.shrinkToContent) {
@@ -11231,12 +11685,12 @@ async function processInternal(inputBuffer, isSVG, options) {
11231
11685
  function applyResize(buffer, resize) {
11232
11686
  const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
11233
11687
  if (resize.size) {
11234
- return sharp2(buffer).resize(resize.size.width, resize.size.height, {
11688
+ return sharp3(buffer).resize(resize.size.width, resize.size.height, {
11235
11689
  fit: resize.fit ?? "contain",
11236
11690
  background: transparent
11237
11691
  }).toBuffer();
11238
11692
  }
11239
- return sharp2(buffer).resize(resize.width ?? null, resize.height ?? null, { fit: "inside", withoutEnlargement: false }).toBuffer();
11693
+ return sharp3(buffer).resize(resize.width ?? null, resize.height ?? null, { fit: "inside", withoutEnlargement: false }).toBuffer();
11240
11694
  }
11241
11695
  async function processImage(inputBuffer, options = {}) {
11242
11696
  const stages = [];
@@ -11245,7 +11699,7 @@ async function processImage(inputBuffer, options = {}) {
11245
11699
  if (options.resize) {
11246
11700
  stages.push("resize");
11247
11701
  const resized = await applyResize(inputBuffer, options.resize);
11248
- const meta3 = await sharp2(resized).metadata();
11702
+ const meta3 = await sharp3(resized).metadata();
11249
11703
  return {
11250
11704
  buffer: resized,
11251
11705
  format: "png",
@@ -11254,7 +11708,7 @@ async function processImage(inputBuffer, options = {}) {
11254
11708
  stages
11255
11709
  };
11256
11710
  }
11257
- const meta2 = await sharp2(inputBuffer).metadata();
11711
+ const meta2 = await sharp3(inputBuffer).metadata();
11258
11712
  return {
11259
11713
  buffer: inputBuffer,
11260
11714
  format: "svg",
@@ -11266,7 +11720,7 @@ async function processImage(inputBuffer, options = {}) {
11266
11720
  let workingBuffer = inputBuffer;
11267
11721
  if (isSVG) {
11268
11722
  stages.push("rasterize-svg");
11269
- workingBuffer = await sharp2(inputBuffer).png({ compressionLevel: 0, force: true, palette: false, quality: 100 }).toBuffer();
11723
+ workingBuffer = await sharp3(inputBuffer).png({ compressionLevel: 0, force: true, palette: false, quality: 100 }).toBuffer();
11270
11724
  }
11271
11725
  const pass = await processInternal(workingBuffer, isSVG, options);
11272
11726
  stages.push(...pass.stages);
@@ -11275,7 +11729,7 @@ async function processImage(inputBuffer, options = {}) {
11275
11729
  stages.push("resize");
11276
11730
  processed = await applyResize(processed, options.resize);
11277
11731
  }
11278
- const meta = await sharp2(processed).metadata();
11732
+ const meta = await sharp3(processed).metadata();
11279
11733
  return {
11280
11734
  buffer: processed,
11281
11735
  format: "png",
@@ -11481,7 +11935,7 @@ function coerceRawArgs(args) {
11481
11935
  "dry-run": bool(args["dry-run"])
11482
11936
  };
11483
11937
  }
11484
- var normalizeCommand = defineCommand99({
11938
+ var normalizeCommand = defineCommand100({
11485
11939
  meta: {
11486
11940
  name: "normalize",
11487
11941
  description: `Normalize logos / images: declarative recolor + bg removal + trim + resize. Operates on local files; writes in-place by default.
@@ -11535,8 +11989,68 @@ Examples:
11535
11989
  }
11536
11990
  });
11537
11991
 
11992
+ // src/commands/images/pinterest.ts
11993
+ import { defineCommand as defineCommand101 } from "citty";
11994
+ registerSchema({
11995
+ command: "images.pinterest",
11996
+ description: "Pinterest image search via ScrapeCreators. Reference-grade real-world photography, product styling, interiors, fashion, food, and aesthetic mood boards. Inspect before placing \u2014 Pinterest is unverified, trademark-bearing web content.",
11997
+ args: {
11998
+ query: { type: "string", description: "Search query", required: true },
11999
+ limit: { type: "number", description: "Max results (1-20, default 5)", required: false, default: 5 },
12000
+ "auto-ingest": {
12001
+ type: "number",
12002
+ description: "Auto-ingest top N and return Baker-owned URLs on ingested hits",
12003
+ required: false,
12004
+ default: 0
12005
+ },
12006
+ context: {
12007
+ type: "string",
12008
+ description: "Free-text hint applied to every auto-ingested hit (overrides the pin title)",
12009
+ required: false
12010
+ }
12011
+ }
12012
+ });
12013
+ var pinterestCommand = defineCommand101({
12014
+ meta: {
12015
+ name: "pinterest",
12016
+ description: "Pinterest image search via ScrapeCreators ($0.00188/request). Best for photo-realistic reference imagery \u2014 lifestyle, interiors, fashion, food, product styling, and mood boards to brief AI generation against. \u26A0 Unverified, trademark-bearing web content \u2014 inspect and respect rights before placing on a customer page. Browse first; auto-ingest only the pins you commit to.\n\nExamples:\n baker images pinterest 'scandinavian living room'\n baker images pinterest 'minimalist skincare product photography' --limit 20\n baker images pinterest 'cozy coffee shop interior' --auto-ingest 2 --context 'Mood reference for hero photography'"
12017
+ },
12018
+ args: {
12019
+ query: { type: "positional", description: "Search query", required: false },
12020
+ limit: { type: "string", description: "Max results (1-20)", required: false },
12021
+ "auto-ingest": {
12022
+ type: "string",
12023
+ description: "Auto-ingest top N and return Baker-owned URLs on ingested hits",
12024
+ required: false
12025
+ },
12026
+ context: { type: "string", description: "Description context hint applied to auto-ingested hits", required: false }
12027
+ },
12028
+ run: async ({ args }) => {
12029
+ try {
12030
+ const query = args.query;
12031
+ if (!query) {
12032
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Query is required" } });
12033
+ process.exit(1);
12034
+ }
12035
+ const body = { query };
12036
+ if (args.limit) body.limit = Number(args.limit);
12037
+ if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
12038
+ if (args.context) body.descriptionContext = args.context;
12039
+ const data = await apiPost("/api/images/pinterest", body);
12040
+ writeJson({ ok: true, data });
12041
+ } catch (err) {
12042
+ if (err instanceof ApiError) {
12043
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
12044
+ process.exit(1);
12045
+ }
12046
+ writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
12047
+ process.exit(1);
12048
+ }
12049
+ }
12050
+ });
12051
+
11538
12052
  // src/commands/images/screenshot.ts
11539
- import { defineCommand as defineCommand100 } from "citty";
12053
+ import { defineCommand as defineCommand102 } from "citty";
11540
12054
  registerSchema({
11541
12055
  command: "images.screenshot",
11542
12056
  description: "Capture a website screenshot via ScreenshotOne. Auto-ingests on success.",
@@ -11552,7 +12066,7 @@ registerSchema({
11552
12066
  }
11553
12067
  }
11554
12068
  });
11555
- var screenshotCommand = defineCommand100({
12069
+ var screenshotCommand = defineCommand102({
11556
12070
  meta: {
11557
12071
  name: "screenshot",
11558
12072
  description: "Screenshot a URL via ScreenshotOne. $0.009/capture. Auto-ingests to library.\n\nExample: baker images screenshot https://stripe.com --full-page"
@@ -11602,7 +12116,7 @@ var screenshotCommand = defineCommand100({
11602
12116
  });
11603
12117
 
11604
12118
  // src/commands/images/search.ts
11605
- import { defineCommand as defineCommand101 } from "citty";
12119
+ import { defineCommand as defineCommand103 } from "citty";
11606
12120
  registerSchema({
11607
12121
  command: "images.search",
11608
12122
  description: "Search images by text query. Only returns ready images.",
@@ -11618,7 +12132,7 @@ registerSchema({
11618
12132
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
11619
12133
  }
11620
12134
  });
11621
- var searchCommand = defineCommand101({
12135
+ var searchCommand = defineCommand103({
11622
12136
  meta: {
11623
12137
  name: "search",
11624
12138
  description: "Semantic search images by text query. Uses hybrid BM25 + vector + reranking. Example: baker images search 'hero banner' --aspect-ratio 16:9 --tags logo"
@@ -11678,7 +12192,7 @@ var searchCommand = defineCommand101({
11678
12192
  });
11679
12193
 
11680
12194
  // src/commands/images/sticker.ts
11681
- import { defineCommand as defineCommand102 } from "citty";
12195
+ import { defineCommand as defineCommand104 } from "citty";
11682
12196
  registerSchema({
11683
12197
  command: "images.sticker",
11684
12198
  description: "Search Giphy stickers \u2014 transparent-background overlays for ad creative.",
@@ -11710,7 +12224,7 @@ registerSchema({
11710
12224
  }
11711
12225
  }
11712
12226
  });
11713
- var stickerCommand = defineCommand102({
12227
+ var stickerCommand = defineCommand104({
11714
12228
  meta: {
11715
12229
  name: "sticker",
11716
12230
  description: "Search Giphy's sticker corpus \u2014 transparent-background WebPs / GIFs ideal for overlaying on ad creative (Meta, TikTok, Stories). Same Giphy free API as `baker images gif`; results carry WebP + GIF + MP4 URLs in providerMeta.\n\nExample: baker images sticker 'thumbs up' --limit 10\nExample: baker images sticker celebration --rating g --auto-ingest 3\nExample: baker images sticker --trending --limit 25"
@@ -11757,7 +12271,7 @@ var stickerCommand = defineCommand102({
11757
12271
  });
11758
12272
 
11759
12273
  // src/commands/images/stock.ts
11760
- import { defineCommand as defineCommand103 } from "citty";
12274
+ import { defineCommand as defineCommand105 } from "citty";
11761
12275
  registerSchema({
11762
12276
  command: "images.stock",
11763
12277
  description: "Stock photo, vector illustration, icon-set, and PSD search via Magnific (Freepik's developer API).",
@@ -11815,7 +12329,7 @@ registerSchema({
11815
12329
  }
11816
12330
  }
11817
12331
  });
11818
- var stockCommand = defineCommand103({
12332
+ var stockCommand = defineCommand105({
11819
12333
  meta: {
11820
12334
  name: "stock",
11821
12335
  description: "Stock search via Magnific \u2014 Freepik's developer API (~250M assets: photos, vectors, illustrations, icons, PSDs). $0.002/req. With --auto-ingest, ingested hits return Baker-owned URLs.\n\nExamples:\n baker images stock 'minimalist office'\n baker images stock 'flat office workers' --type vector\n baker images stock 'hero photo of a kitchen' --type photo --orientation landscape --ai exclude\n baker images stock 'brand pattern' --color '#0a0a0a' --license freemium --auto-ingest 2"
@@ -11871,9 +12385,9 @@ var stockCommand = defineCommand103({
11871
12385
  });
11872
12386
 
11873
12387
  // src/commands/images/upload.ts
11874
- import { readFile as readFile7 } from "fs/promises";
12388
+ import { readFile as readFile8 } from "fs/promises";
11875
12389
  import { extname as extname2 } from "path";
11876
- import { defineCommand as defineCommand104 } from "citty";
12390
+ import { defineCommand as defineCommand106 } from "citty";
11877
12391
  var MIME_MAP = {
11878
12392
  ".png": "image/png",
11879
12393
  ".jpg": "image/jpeg",
@@ -11928,7 +12442,7 @@ function detectContentType(filePath) {
11928
12442
  }
11929
12443
  return mime;
11930
12444
  }
11931
- var uploadCommand = defineCommand104({
12445
+ var uploadCommand = defineCommand106({
11932
12446
  meta: {
11933
12447
  name: "upload",
11934
12448
  description: "Upload an image to the library \u2014 accepts a local file path OR a remote http(s) URL.\n\nLocal: reads bytes, sends to /api/images/upload, content-type auto-detected from extension.\nRemote: dispatches to /api/images/ingest with hash-dedup on bytes + externalId.\n\nExamples:\n baker images upload ./logo.png --source uploaded\n baker images upload ./cert.png --context 'ISO 27001 badge \u2014 enterprise tier'\n baker images upload https://acme.com/hero.png --source firecrawl --context 'Acme competitor pricing hero'"
@@ -12011,7 +12525,7 @@ async function uploadLocal(target, args) {
12011
12525
  });
12012
12526
  return;
12013
12527
  }
12014
- const fileBuffer = await readFile7(target);
12528
+ const fileBuffer = await readFile8(target);
12015
12529
  const base64 = fileBuffer.toString("base64");
12016
12530
  const body = { base64, contentType };
12017
12531
  if (args.source) body.source = args.source;
@@ -12021,7 +12535,7 @@ async function uploadLocal(target, args) {
12021
12535
  }
12022
12536
 
12023
12537
  // src/commands/images/upscale.ts
12024
- import { defineCommand as defineCommand105 } from "citty";
12538
+ import { defineCommand as defineCommand107 } from "citty";
12025
12539
  registerSchema({
12026
12540
  command: "images.upscale",
12027
12541
  description: "Upscale a library image via the backend (Replicate, cost-tracked). Waits for completion by default. The image must be status 'ready' and raster (not SVG/AVIF).",
@@ -12036,7 +12550,7 @@ registerSchema({
12036
12550
  }
12037
12551
  });
12038
12552
  var POLL_INTERVAL_MS3 = 1500;
12039
- var upscaleCommand = defineCommand105({
12553
+ var upscaleCommand = defineCommand107({
12040
12554
  meta: {
12041
12555
  name: "upscale",
12042
12556
  description: "Upscale a library image via the Convex backend (Replicate, cost-tracked at $0.05/image). Waits for completion by default.\n\nExample: baker images upscale j571abc123def\nExample: baker images upscale j571abc123def --max-wait 0 # fire-and-forget"
@@ -12091,7 +12605,7 @@ var upscaleCommand = defineCommand105({
12091
12605
  });
12092
12606
 
12093
12607
  // src/commands/images/use.ts
12094
- import { defineCommand as defineCommand106 } from "citty";
12608
+ import { defineCommand as defineCommand108 } from "citty";
12095
12609
  registerSchema({
12096
12610
  command: "images.use",
12097
12611
  description: "Ingest a URL and wait for the library record to be ready.",
@@ -12107,7 +12621,7 @@ registerSchema({
12107
12621
  }
12108
12622
  });
12109
12623
  var POLL_INTERVAL_MS4 = 1500;
12110
- var useCommand = defineCommand106({
12624
+ var useCommand = defineCommand108({
12111
12625
  meta: {
12112
12626
  name: "use",
12113
12627
  description: "Sugar over `ingest`: download \u2192 store \u2192 wait until describe + embed complete \u2192 return ready library record.\n\nExample: baker images use https://cdn.example.com/hero.png --source uploaded"
@@ -12153,7 +12667,7 @@ var useCommand = defineCommand106({
12153
12667
  });
12154
12668
 
12155
12669
  // src/commands/images/index.ts
12156
- var imagesCommand = defineCommand107({
12670
+ var imagesCommand = defineCommand109({
12157
12671
  meta: {
12158
12672
  name: "images",
12159
12673
  description: `Find, source, and normalize images. Subcommands route by provider so cost + license are explicit.
@@ -12165,6 +12679,7 @@ Library + search:
12165
12679
  External providers:
12166
12680
  baker images stock <q> [--type photo|vector|psd] Magnific (Freepik's dev API) \u2014 photos, vectors, illustrations, PSDs
12167
12681
  baker images google <q> Google Images via the official Custom Search API
12682
+ baker images pinterest <q> Pinterest reference imagery via ScrapeCreators (photo-real mood boards)
12168
12683
  baker images logo <domain> Brand logo via Brandfetch CDN
12169
12684
  baker images icon <name> [--set \u2026] Iconify (200+ sets)
12170
12685
  baker images gif <q> [--trending] Giphy GIFs / reaction memes (paid-social creative)
@@ -12172,7 +12687,9 @@ External providers:
12172
12687
  baker images extract <url> Firecrawl page extract
12173
12688
  baker images screenshot <url> ScreenshotOne capture
12174
12689
 
12175
- Ingest + management:
12690
+ AI generation (run on the Convex backend, cost-tracked):
12691
+ baker images generate <prompt> [--model \u2026] [--aspect-ratio \u2026] [--reference url1,url2]
12692
+ Generate an image with AI (OpenRouter models) and ingest it
12176
12693
  baker images ingest <url> --source <enum> Save remote URL to library
12177
12694
  baker images use <url> Ingest and wait for ready
12178
12695
  baker images upload ./file Upload local file
@@ -12195,6 +12712,8 @@ Paid transforms (run on the Convex backend, cost-tracked):
12195
12712
  find: findCommand,
12196
12713
  stock: stockCommand,
12197
12714
  google: googleCommand2,
12715
+ pinterest: pinterestCommand,
12716
+ generate: generateCommand,
12198
12717
  logo: logoCommand,
12199
12718
  icon: iconCommand,
12200
12719
  gif: gifCommand,
@@ -12214,10 +12733,10 @@ Paid transforms (run on the Convex backend, cost-tracked):
12214
12733
  });
12215
12734
 
12216
12735
  // src/commands/research/index.ts
12217
- import { defineCommand as defineCommand118 } from "citty";
12736
+ import { defineCommand as defineCommand120 } from "citty";
12218
12737
 
12219
12738
  // src/commands/research/advertisers.ts
12220
- import { defineCommand as defineCommand108 } from "citty";
12739
+ import { defineCommand as defineCommand110 } from "citty";
12221
12740
 
12222
12741
  // src/commands/research/output.ts
12223
12742
  var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
@@ -12330,7 +12849,7 @@ var FIELDS3 = {
12330
12849
  etv: "Estimated traffic value (USD)",
12331
12850
  visibility: "SERP visibility score (0-1)"
12332
12851
  };
12333
- var advertisersCommand = defineCommand108({
12852
+ var advertisersCommand = defineCommand110({
12334
12853
  meta: {
12335
12854
  name: "advertisers",
12336
12855
  description: `Find domains competing for a keyword in Google SERPs.
@@ -12377,7 +12896,7 @@ Examples:
12377
12896
  });
12378
12897
 
12379
12898
  // src/commands/research/autocomplete.ts
12380
- import { defineCommand as defineCommand109 } from "citty";
12899
+ import { defineCommand as defineCommand111 } from "citty";
12381
12900
  registerSchema({
12382
12901
  command: "research.autocomplete",
12383
12902
  description: "Get Google Autocomplete suggestions for a seed keyword. Useful for keyword expansion and discovering what people actually search for. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
@@ -12400,7 +12919,7 @@ registerSchema({
12400
12919
  var FIELDS4 = {
12401
12920
  suggestion: "Autocomplete suggestion from Google"
12402
12921
  };
12403
- var autocompleteCommand = defineCommand109({
12922
+ var autocompleteCommand = defineCommand111({
12404
12923
  meta: {
12405
12924
  name: "autocomplete",
12406
12925
  description: `Get Google Autocomplete suggestions for keyword expansion.
@@ -12446,7 +12965,7 @@ Examples:
12446
12965
  });
12447
12966
 
12448
12967
  // src/commands/research/countries.ts
12449
- import { defineCommand as defineCommand110 } from "citty";
12968
+ import { defineCommand as defineCommand112 } from "citty";
12450
12969
  registerSchema({
12451
12970
  command: "research.countries",
12452
12971
  description: "List all supported country codes for --location flag in research commands.",
@@ -12503,7 +13022,7 @@ var FIELDS5 = {
12503
13022
  code: "Country code to pass as --location",
12504
13023
  name: "Country name"
12505
13024
  };
12506
- var countriesCommand = defineCommand110({
13025
+ var countriesCommand = defineCommand112({
12507
13026
  meta: {
12508
13027
  name: "countries",
12509
13028
  description: "List all supported country codes for --location flag."
@@ -12514,7 +13033,7 @@ var countriesCommand = defineCommand110({
12514
13033
  });
12515
13034
 
12516
13035
  // src/commands/research/intent.ts
12517
- import { defineCommand as defineCommand111 } from "citty";
13036
+ import { defineCommand as defineCommand113 } from "citty";
12518
13037
  registerSchema({
12519
13038
  command: "research.intent",
12520
13039
  description: "Classify Google Search intent for keywords. Determines if someone searching is looking to buy, research, or navigate. IMPORTANT: If --language is omitted, defaults to English (en). The response includes a query_context object showing which language was used.",
@@ -12537,7 +13056,7 @@ var FIELDS6 = {
12537
13056
  intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
12538
13057
  probability: "Confidence score 0.0-1.0"
12539
13058
  };
12540
- var intentCommand = defineCommand111({
13059
+ var intentCommand = defineCommand113({
12541
13060
  meta: {
12542
13061
  name: "intent",
12543
13062
  description: `Classify Google Search intent for keywords. Returns intent type and confidence.
@@ -12585,7 +13104,7 @@ Examples:
12585
13104
  });
12586
13105
 
12587
13106
  // src/commands/research/keyword-gap.ts
12588
- import { defineCommand as defineCommand112 } from "citty";
13107
+ import { defineCommand as defineCommand114 } from "citty";
12589
13108
  registerSchema({
12590
13109
  command: "research.keyword-gap",
12591
13110
  description: "Find keywords a competitor ranks for (organic or paid) that you don't. Discovers expansion opportunities. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
@@ -12614,7 +13133,7 @@ var FIELDS7 = {
12614
13133
  cpc: "Cost per click USD",
12615
13134
  their_position: "Competitor's ranking position"
12616
13135
  };
12617
- var keywordGapCommand = defineCommand112({
13136
+ var keywordGapCommand = defineCommand114({
12618
13137
  meta: {
12619
13138
  name: "keyword-gap",
12620
13139
  description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
@@ -12688,7 +13207,7 @@ Examples:
12688
13207
  });
12689
13208
 
12690
13209
  // src/commands/research/keywords-for-site.ts
12691
- import { defineCommand as defineCommand113 } from "citty";
13210
+ import { defineCommand as defineCommand115 } from "citty";
12692
13211
  registerSchema({
12693
13212
  command: "research.keywords-for-site",
12694
13213
  description: "Get keywords a competitor targets in Google. Use --type paid to see only paid keywords, --type organic for organic only. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
@@ -12721,7 +13240,7 @@ var FIELDS8 = {
12721
13240
  competition: "LOW, MEDIUM, or HIGH",
12722
13241
  competition_index: "Competition score 0-100"
12723
13242
  };
12724
- var keywordsForSiteCommand = defineCommand113({
13243
+ var keywordsForSiteCommand = defineCommand115({
12725
13244
  meta: {
12726
13245
  name: "keywords-for-site",
12727
13246
  description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
@@ -12774,7 +13293,7 @@ Examples:
12774
13293
  });
12775
13294
 
12776
13295
  // src/commands/research/languages.ts
12777
- import { defineCommand as defineCommand114 } from "citty";
13296
+ import { defineCommand as defineCommand116 } from "citty";
12778
13297
  registerSchema({
12779
13298
  command: "research.languages",
12780
13299
  description: "List all supported language codes for --language flag in research commands.",
@@ -12804,7 +13323,7 @@ var FIELDS9 = {
12804
13323
  code: "Language code to pass as --language",
12805
13324
  name: "Language name (also accepted by --language)"
12806
13325
  };
12807
- var languagesCommand2 = defineCommand114({
13326
+ var languagesCommand2 = defineCommand116({
12808
13327
  meta: {
12809
13328
  name: "languages",
12810
13329
  description: "List all supported language codes for --language flag."
@@ -12815,7 +13334,7 @@ var languagesCommand2 = defineCommand114({
12815
13334
  });
12816
13335
 
12817
13336
  // src/commands/research/lighthouse.ts
12818
- import { defineCommand as defineCommand115 } from "citty";
13337
+ import { defineCommand as defineCommand117 } from "citty";
12819
13338
  registerSchema({
12820
13339
  command: "research.lighthouse",
12821
13340
  description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
@@ -12834,7 +13353,7 @@ var FIELDS10 = {
12834
13353
  speed_index_ms: "Speed Index in ms (good: < 3400)",
12835
13354
  interactive_ms: "Time to Interactive in ms (good: < 3800)"
12836
13355
  };
12837
- var lighthouseCommand = defineCommand115({
13356
+ var lighthouseCommand = defineCommand117({
12838
13357
  meta: {
12839
13358
  name: "lighthouse",
12840
13359
  description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
@@ -12872,7 +13391,7 @@ Examples:
12872
13391
  });
12873
13392
 
12874
13393
  // src/commands/research/relevant-pages.ts
12875
- import { defineCommand as defineCommand116 } from "citty";
13394
+ import { defineCommand as defineCommand118 } from "citty";
12876
13395
  registerSchema({
12877
13396
  command: "research.relevant-pages",
12878
13397
  description: "Get the top pages of a competitor domain with organic traffic and ranking data. Shows which pages drive the most traffic. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
@@ -12898,7 +13417,7 @@ var FIELDS11 = {
12898
13417
  keywords: "Total organic keywords the page ranks for",
12899
13418
  top_10: "Keywords in positions 1-10"
12900
13419
  };
12901
- var relevantPagesCommand = defineCommand116({
13420
+ var relevantPagesCommand = defineCommand118({
12902
13421
  meta: {
12903
13422
  name: "relevant-pages",
12904
13423
  description: `Get the top pages of a competitor domain with traffic data.
@@ -12944,7 +13463,7 @@ Examples:
12944
13463
  });
12945
13464
 
12946
13465
  // src/commands/research/web.ts
12947
- import { defineCommand as defineCommand117 } from "citty";
13466
+ import { defineCommand as defineCommand119 } from "citty";
12948
13467
  registerSchema({
12949
13468
  command: "research.web",
12950
13469
  description: "Search the web with AI to answer marketing questions \u2014 competitors, ICP, pricing, pain points, market trends. Three depth levels: medium (quick, default), high (thorough), xhigh (exhaustive deep research).",
@@ -12995,7 +13514,7 @@ async function runDeepResearch(question) {
12995
13514
  }
12996
13515
  throw new Error("Deep research timed out");
12997
13516
  }
12998
- var webCommand = defineCommand117({
13517
+ var webCommand = defineCommand119({
12999
13518
  meta: {
13000
13519
  name: "web",
13001
13520
  description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
@@ -13055,7 +13574,7 @@ Examples:
13055
13574
  });
13056
13575
 
13057
13576
  // src/commands/research/index.ts
13058
- var researchCommand = defineCommand118({
13577
+ var researchCommand = defineCommand120({
13059
13578
  meta: {
13060
13579
  name: "research",
13061
13580
  description: `Competitive intelligence and AI-powered research commands.
@@ -13095,10 +13614,10 @@ Examples:
13095
13614
  });
13096
13615
 
13097
13616
  // src/commands/scheduled-actions/index.ts
13098
- import { defineCommand as defineCommand125 } from "citty";
13617
+ import { defineCommand as defineCommand127 } from "citty";
13099
13618
 
13100
13619
  // src/commands/scheduled-actions/create.ts
13101
- import { defineCommand as defineCommand119 } from "citty";
13620
+ import { defineCommand as defineCommand121 } from "citty";
13102
13621
 
13103
13622
  // src/commands/scheduled-actions/shared.ts
13104
13623
  var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
@@ -13203,7 +13722,7 @@ registerSchema({
13203
13722
  prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
13204
13723
  }
13205
13724
  });
13206
- var createCommand2 = defineCommand119({
13725
+ var createCommand2 = defineCommand121({
13207
13726
  meta: {
13208
13727
  name: "create",
13209
13728
  description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
@@ -13251,7 +13770,7 @@ var createCommand2 = defineCommand119({
13251
13770
  });
13252
13771
 
13253
13772
  // src/commands/scheduled-actions/delete.ts
13254
- import { defineCommand as defineCommand120 } from "citty";
13773
+ import { defineCommand as defineCommand122 } from "citty";
13255
13774
  registerSchema({
13256
13775
  command: "scheduled-actions.delete",
13257
13776
  description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
@@ -13259,7 +13778,7 @@ registerSchema({
13259
13778
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
13260
13779
  }
13261
13780
  });
13262
- var deleteCommand2 = defineCommand120({
13781
+ var deleteCommand2 = defineCommand122({
13263
13782
  meta: {
13264
13783
  name: "delete",
13265
13784
  description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
@@ -13288,7 +13807,7 @@ var deleteCommand2 = defineCommand120({
13288
13807
  });
13289
13808
 
13290
13809
  // src/commands/scheduled-actions/get.ts
13291
- import { defineCommand as defineCommand121 } from "citty";
13810
+ import { defineCommand as defineCommand123 } from "citty";
13292
13811
  registerSchema({
13293
13812
  command: "scheduled-actions.get",
13294
13813
  description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
@@ -13296,7 +13815,7 @@ registerSchema({
13296
13815
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
13297
13816
  }
13298
13817
  });
13299
- var getCommand3 = defineCommand121({
13818
+ var getCommand3 = defineCommand123({
13300
13819
  meta: {
13301
13820
  name: "get",
13302
13821
  description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
@@ -13333,13 +13852,13 @@ var getCommand3 = defineCommand121({
13333
13852
  });
13334
13853
 
13335
13854
  // src/commands/scheduled-actions/list.ts
13336
- import { defineCommand as defineCommand122 } from "citty";
13855
+ import { defineCommand as defineCommand124 } from "citty";
13337
13856
  registerSchema({
13338
13857
  command: "scheduled-actions.list",
13339
13858
  description: "List published scheduled actions. Includes draft state when BAKER_CHAT_ID is set.",
13340
13859
  args: {}
13341
13860
  });
13342
- var listCommand2 = defineCommand122({
13861
+ var listCommand2 = defineCommand124({
13343
13862
  meta: {
13344
13863
  name: "list",
13345
13864
  description: "List scheduled actions. Includes staged draft ops when BAKER_CHAT_ID is set."
@@ -13360,7 +13879,7 @@ var listCommand2 = defineCommand122({
13360
13879
  });
13361
13880
 
13362
13881
  // src/commands/scheduled-actions/trigger.ts
13363
- import { defineCommand as defineCommand123 } from "citty";
13882
+ import { defineCommand as defineCommand125 } from "citty";
13364
13883
  registerSchema({
13365
13884
  command: "scheduled-actions.trigger",
13366
13885
  description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
@@ -13368,7 +13887,7 @@ registerSchema({
13368
13887
  id: { type: "string", description: "Published scheduled action ID", required: true }
13369
13888
  }
13370
13889
  });
13371
- var triggerCommand = defineCommand123({
13890
+ var triggerCommand = defineCommand125({
13372
13891
  meta: {
13373
13892
  name: "trigger",
13374
13893
  description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
@@ -13405,7 +13924,7 @@ var triggerCommand = defineCommand123({
13405
13924
  });
13406
13925
 
13407
13926
  // src/commands/scheduled-actions/update.ts
13408
- import { defineCommand as defineCommand124 } from "citty";
13927
+ import { defineCommand as defineCommand126 } from "citty";
13409
13928
  registerSchema({
13410
13929
  command: "scheduled-actions.update",
13411
13930
  description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
@@ -13430,7 +13949,7 @@ registerSchema({
13430
13949
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
13431
13950
  }
13432
13951
  });
13433
- var updateCommand2 = defineCommand124({
13952
+ var updateCommand2 = defineCommand126({
13434
13953
  meta: {
13435
13954
  name: "update",
13436
13955
  description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
@@ -13500,7 +14019,7 @@ var updateCommand2 = defineCommand124({
13500
14019
  });
13501
14020
 
13502
14021
  // src/commands/scheduled-actions/index.ts
13503
- var scheduledActionsCommand = defineCommand125({
14022
+ var scheduledActionsCommand = defineCommand127({
13504
14023
  meta: {
13505
14024
  name: "scheduled-actions",
13506
14025
  description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
@@ -13526,8 +14045,8 @@ Examples:
13526
14045
  });
13527
14046
 
13528
14047
  // src/commands/schema.ts
13529
- import { defineCommand as defineCommand126 } from "citty";
13530
- var schemaCommand = defineCommand126({
14048
+ import { defineCommand as defineCommand128 } from "citty";
14049
+ var schemaCommand = defineCommand128({
13531
14050
  meta: {
13532
14051
  name: "schema",
13533
14052
  description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
@@ -13563,10 +14082,10 @@ var schemaCommand = defineCommand126({
13563
14082
  });
13564
14083
 
13565
14084
  // src/commands/testimonials/index.ts
13566
- import { defineCommand as defineCommand130 } from "citty";
14085
+ import { defineCommand as defineCommand132 } from "citty";
13567
14086
 
13568
14087
  // src/commands/testimonials/get.ts
13569
- import { defineCommand as defineCommand127 } from "citty";
14088
+ import { defineCommand as defineCommand129 } from "citty";
13570
14089
  registerSchema({
13571
14090
  command: "testimonials.get",
13572
14091
  description: "Get a single testimonial by ID",
@@ -13574,7 +14093,7 @@ registerSchema({
13574
14093
  id: { type: "string", description: "Testimonial ID", required: true }
13575
14094
  }
13576
14095
  });
13577
- var getCommand4 = defineCommand127({
14096
+ var getCommand4 = defineCommand129({
13578
14097
  meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
13579
14098
  args: {
13580
14099
  id: { type: "positional", description: "Testimonial ID", required: false },
@@ -13611,7 +14130,7 @@ var getCommand4 = defineCommand127({
13611
14130
  });
13612
14131
 
13613
14132
  // src/commands/testimonials/list.ts
13614
- import { defineCommand as defineCommand128 } from "citty";
14133
+ import { defineCommand as defineCommand130 } from "citty";
13615
14134
  registerSchema({
13616
14135
  command: "testimonials.list",
13617
14136
  description: "List testimonials with optional filters.",
@@ -13641,7 +14160,7 @@ registerSchema({
13641
14160
  limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
13642
14161
  }
13643
14162
  });
13644
- var listCommand3 = defineCommand128({
14163
+ var listCommand3 = defineCommand130({
13645
14164
  meta: {
13646
14165
  name: "list",
13647
14166
  description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
@@ -13690,7 +14209,7 @@ var listCommand3 = defineCommand128({
13690
14209
  });
13691
14210
 
13692
14211
  // src/commands/testimonials/search.ts
13693
- import { defineCommand as defineCommand129 } from "citty";
14212
+ import { defineCommand as defineCommand131 } from "citty";
13694
14213
  registerSchema({
13695
14214
  command: "testimonials.search",
13696
14215
  description: "Search testimonials by text query. Uses hybrid BM25 + vector + reranking.",
@@ -13721,7 +14240,7 @@ registerSchema({
13721
14240
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
13722
14241
  }
13723
14242
  });
13724
- var searchCommand2 = defineCommand129({
14243
+ var searchCommand2 = defineCommand131({
13725
14244
  meta: {
13726
14245
  name: "search",
13727
14246
  description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
@@ -13792,7 +14311,7 @@ var searchCommand2 = defineCommand129({
13792
14311
  });
13793
14312
 
13794
14313
  // src/commands/testimonials/index.ts
13795
- var testimonialsCommand = defineCommand130({
14314
+ var testimonialsCommand = defineCommand132({
13796
14315
  meta: {
13797
14316
  name: "testimonials",
13798
14317
  description: `Find and browse testimonials in Baker. Subcommands: search, get, list.
@@ -13811,10 +14330,10 @@ Examples:
13811
14330
  });
13812
14331
 
13813
14332
  // src/commands/videos/index.ts
13814
- import { defineCommand as defineCommand135 } from "citty";
14333
+ import { defineCommand as defineCommand137 } from "citty";
13815
14334
 
13816
14335
  // src/commands/videos/delete.ts
13817
- import { defineCommand as defineCommand131 } from "citty";
14336
+ import { defineCommand as defineCommand133 } from "citty";
13818
14337
  registerSchema({
13819
14338
  command: "videos.delete",
13820
14339
  description: "Delete a video by ID",
@@ -13828,7 +14347,7 @@ registerSchema({
13828
14347
  }
13829
14348
  }
13830
14349
  });
13831
- var deleteCommand3 = defineCommand131({
14350
+ var deleteCommand3 = defineCommand133({
13832
14351
  meta: {
13833
14352
  name: "delete",
13834
14353
  description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
@@ -13869,7 +14388,7 @@ var deleteCommand3 = defineCommand131({
13869
14388
  });
13870
14389
 
13871
14390
  // src/commands/videos/get.ts
13872
- import { defineCommand as defineCommand132 } from "citty";
14391
+ import { defineCommand as defineCommand134 } from "citty";
13873
14392
  registerSchema({
13874
14393
  command: "videos.get",
13875
14394
  description: "Get a single video by ID",
@@ -13877,7 +14396,7 @@ registerSchema({
13877
14396
  id: { type: "string", description: "Video ID", required: true }
13878
14397
  }
13879
14398
  });
13880
- var getCommand5 = defineCommand132({
14399
+ var getCommand5 = defineCommand134({
13881
14400
  meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
13882
14401
  args: {
13883
14402
  id: { type: "positional", description: "Video ID", required: false },
@@ -13914,7 +14433,7 @@ var getCommand5 = defineCommand132({
13914
14433
  });
13915
14434
 
13916
14435
  // src/commands/videos/search.ts
13917
- import { defineCommand as defineCommand133 } from "citty";
14436
+ import { defineCommand as defineCommand135 } from "citty";
13918
14437
  registerSchema({
13919
14438
  command: "videos.search",
13920
14439
  description: "Search videos by text query. Only returns ready videos.",
@@ -13924,7 +14443,7 @@ registerSchema({
13924
14443
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
13925
14444
  }
13926
14445
  });
13927
- var searchCommand3 = defineCommand133({
14446
+ var searchCommand3 = defineCommand135({
13928
14447
  meta: {
13929
14448
  name: "search",
13930
14449
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -13971,9 +14490,9 @@ var searchCommand3 = defineCommand133({
13971
14490
  });
13972
14491
 
13973
14492
  // src/commands/videos/upload.ts
13974
- import { readFile as readFile8, stat as stat3 } from "fs/promises";
14493
+ import { readFile as readFile9, stat as stat3 } from "fs/promises";
13975
14494
  import { extname as extname3 } from "path";
13976
- import { defineCommand as defineCommand134 } from "citty";
14495
+ import { defineCommand as defineCommand136 } from "citty";
13977
14496
  var MIME_MAP2 = {
13978
14497
  ".mp4": "video/mp4",
13979
14498
  ".mov": "video/quicktime",
@@ -14007,7 +14526,7 @@ function detectContentType2(filePath) {
14007
14526
  }
14008
14527
  return mime;
14009
14528
  }
14010
- var uploadCommand2 = defineCommand134({
14529
+ var uploadCommand2 = defineCommand136({
14011
14530
  meta: {
14012
14531
  name: "upload",
14013
14532
  description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
@@ -14036,7 +14555,7 @@ var uploadCommand2 = defineCommand134({
14036
14555
  return;
14037
14556
  }
14038
14557
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
14039
- const fileBuffer = await readFile8(filePath);
14558
+ const fileBuffer = await readFile9(filePath);
14040
14559
  const uploadResponse = await fetch(uploadUrl, {
14041
14560
  method: "PUT",
14042
14561
  headers: { "Content-Type": contentType },
@@ -14061,7 +14580,7 @@ var uploadCommand2 = defineCommand134({
14061
14580
  });
14062
14581
 
14063
14582
  // src/commands/videos/index.ts
14064
- var videosCommand = defineCommand135({
14583
+ var videosCommand = defineCommand137({
14065
14584
  meta: {
14066
14585
  name: "videos",
14067
14586
  description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete.
@@ -14098,7 +14617,7 @@ function getCliVersion() {
14098
14617
  }
14099
14618
 
14100
14619
  // src/cli.ts
14101
- var main = defineCommand136({
14620
+ var main = defineCommand138({
14102
14621
  meta: {
14103
14622
  name: "baker",
14104
14623
  version: getCliVersion(),