@koda-sl/baker-cli 0.149.0-dev.7b64ca6b5 → 0.150.0-dev.a970fa118

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
@@ -24,6 +24,7 @@ import {
24
24
  imageProfileFor,
25
25
  isPersistedAssetRef,
26
26
  looksLikeHttpUrl,
27
+ nearestSupportedAspectRatio,
27
28
  parseRefExpr,
28
29
  platformFormats,
29
30
  requireCredentialsFromEnv,
@@ -31,10 +32,11 @@ import {
31
32
  sha256Hex,
32
33
  spineInputFlags,
33
34
  spineInputOps,
35
+ supportsParam,
34
36
  toModelSafeImage,
35
37
  ulid,
36
38
  validateCanvasDeep
37
- } from "./chunk-ZBRVPPJP.js";
39
+ } from "./chunk-JBDSJZBZ.js";
38
40
  import {
39
41
  csvOrJson,
40
42
  daysAgoIso,
@@ -73,7 +75,7 @@ import {
73
75
  } from "./chunk-RK67WL4O.js";
74
76
 
75
77
  // src/cli.ts
76
- import { defineCommand as defineCommand178, runMain } from "citty";
78
+ import { defineCommand as defineCommand180, runMain } from "citty";
77
79
 
78
80
  // src/commands/actions/index.ts
79
81
  import { defineCommand as defineCommand18 } from "citty";
@@ -2544,13 +2546,6 @@ var imageDocSchema = z8.object({
2544
2546
  width: z8.number().optional(),
2545
2547
  height: z8.number().optional(),
2546
2548
  aspectRatio: z8.number().optional(),
2547
- /** Any non-opaque pixel in the decoded image. */
2548
- hasAlpha: z8.boolean().optional(),
2549
- /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2550
- * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2551
- * before placing it — the library keeps assets forever, so a bad ingest is
2552
- * otherwise indistinguishable from a good one months later. */
2553
- solidity: z8.number().optional(),
2554
2549
  dominantColor: z8.string().optional(),
2555
2550
  imagePalette: z8.array(z8.string()).optional(),
2556
2551
  thumbhashDataUri: z8.string().optional(),
@@ -2633,13 +2628,6 @@ var imageSearchResultSchema = z8.object({
2633
2628
  width: z8.number().optional(),
2634
2629
  height: z8.number().optional(),
2635
2630
  aspectRatio: z8.number().optional(),
2636
- /** Any non-opaque pixel in the decoded image. */
2637
- hasAlpha: z8.boolean().optional(),
2638
- /** Opaque pixels ÷ their bounding-box area, 0..1. Feed with `aspectRatio`
2639
- * into `classifyLogoShape` to tell a brand wordmark from an app-icon plate
2640
- * before placing it — the library keeps assets forever, so a bad ingest is
2641
- * otherwise indistinguishable from a good one months later. */
2642
- solidity: z8.number().optional(),
2643
2631
  dominantColor: z8.string().optional(),
2644
2632
  imagePalette: z8.array(z8.string()).optional(),
2645
2633
  source: z8.string(),
@@ -2721,8 +2709,9 @@ var rgbTriple = z8.tuple([
2721
2709
  z8.number().int().min(0).max(255)
2722
2710
  ]);
2723
2711
  var imageGenerateModelSchema = z8.enum([
2712
+ "openai/gpt-image-2",
2713
+ // Legacy — see the registry entry; kept so pre-switch canvases still run.
2724
2714
  "openai/gpt-5.4-image-2",
2725
- "google/gemini-3.5-flash",
2726
2715
  "google/gemini-3.1-flash-image-preview",
2727
2716
  "google/gemini-3-pro-image-preview",
2728
2717
  "recraft/recraft-v4.1-pro-vector"
@@ -2763,13 +2752,10 @@ var imagesLogoRequestSchema = z8.object({
2763
2752
  descriptionContext: z8.string().optional()
2764
2753
  });
2765
2754
  var imagesLogoResponseSchema = providerHitsResponseSchema();
2766
- var hexColorSchema = z8.string().transform((value) => value.trim().replace(/^%23/i, "#")).refine((value) => /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value), {
2767
- message: "Expected a hex color like #D7FFA4"
2768
- }).transform((value) => value.startsWith("#") ? value : `#${value}`);
2769
2755
  var imagesIconRequestSchema = z8.object({
2770
2756
  name: z8.string().min(1),
2771
2757
  set: z8.string().optional(),
2772
- color: hexColorSchema.optional(),
2758
+ color: z8.string().optional(),
2773
2759
  width: z8.coerce.number().int().positive().optional(),
2774
2760
  autoIngest: z8.coerce.number().int().min(0).max(20).optional(),
2775
2761
  descriptionContext: z8.string().optional()
@@ -2819,19 +2805,6 @@ var imagesIngestResponseSchema = z8.object({
2819
2805
  contentHash: z8.string()
2820
2806
  });
2821
2807
 
2822
- // ../api/src/logoShape.ts
2823
- var PLATE_SOLIDITY_THRESHOLD = 0.6;
2824
- var WORDMARK_MIN_ASPECT = 1.8;
2825
- function classifyLogoShape({
2826
- aspectRatio: aspectRatio2,
2827
- hasAlpha,
2828
- solidity
2829
- }) {
2830
- if (solidity === void 0 || aspectRatio2 === void 0) return "unknown";
2831
- if (solidity >= PLATE_SOLIDITY_THRESHOLD || hasAlpha === false) return "plated-icon";
2832
- return aspectRatio2 >= WORDMARK_MIN_ASPECT ? "wordmark" : "symbol";
2833
- }
2834
-
2835
2808
  // ../api/src/tags.ts
2836
2809
  import { z as z9 } from "zod";
2837
2810
  var TAG_TYPES = [
@@ -5425,17 +5398,27 @@ var googleDraftStageRequestSchema = z13.object({
5425
5398
  chatId: z13.string(),
5426
5399
  op: googleDraftOpInputSchema
5427
5400
  });
5428
- var googleDraftStageResponseSchema = z13.object({
5429
- staged: z13.literal(true),
5430
- ref: z13.string(),
5431
- kind: googleDraftOpKindSchema,
5432
- mode: googleWriteModeSchema,
5433
- dependsOn: z13.array(z13.string()),
5434
- summary: z13.string(),
5435
- warnings: z13.array(z13.string()),
5436
- /** True when the op amended an already-staged op in place instead of appending a new one. */
5437
- amended: z13.boolean().optional()
5438
- });
5401
+ var googleDraftStageResponseSchema = z13.discriminatedUnion("staged", [
5402
+ z13.object({
5403
+ staged: z13.literal(true),
5404
+ ref: z13.string(),
5405
+ kind: googleDraftOpKindSchema,
5406
+ mode: googleWriteModeSchema,
5407
+ dependsOn: z13.array(z13.string()),
5408
+ summary: z13.string(),
5409
+ warnings: z13.array(z13.string()),
5410
+ /** True when the op amended an already-staged op in place instead of appending a new one. */
5411
+ amended: z13.boolean().optional()
5412
+ }),
5413
+ z13.object({
5414
+ staged: z13.literal(false),
5415
+ noop: z13.literal(true),
5416
+ kind: googleDraftOpKindSchema,
5417
+ mode: googleWriteModeSchema,
5418
+ summary: z13.string(),
5419
+ reason: z13.string()
5420
+ })
5421
+ ]);
5439
5422
  var googleDraftAmendRequestSchema = z13.object({
5440
5423
  chatId: z13.string(),
5441
5424
  ref: z13.string(),
@@ -5462,7 +5445,8 @@ var googleDraftStageBatchResponseSchema = z13.object({
5462
5445
  summary: z13.string(),
5463
5446
  warnings: z13.array(z13.string())
5464
5447
  })
5465
- )
5448
+ ),
5449
+ skipped: z13.array(z13.object({ kind: googleDraftOpKindSchema, summary: z13.string(), reason: z13.string() })).optional()
5466
5450
  });
5467
5451
  var googleDraftOpViewSchema = z13.object({
5468
5452
  ref: z13.string(),
@@ -5702,11 +5686,11 @@ function rawTextEntries(value) {
5702
5686
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
5703
5687
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
5704
5688
  }
5705
- function rawFileEntries(path27) {
5706
- if (typeof path27 !== "string" || path27.length === 0) {
5689
+ function rawFileEntries(path28) {
5690
+ if (typeof path28 !== "string" || path28.length === 0) {
5707
5691
  return [];
5708
5692
  }
5709
- return readFileSync2(path27, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
5693
+ return readFileSync2(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
5710
5694
  }
5711
5695
  function keywordEntries(args) {
5712
5696
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -5721,7 +5705,7 @@ function keywordEntries(args) {
5721
5705
  const seen = /* @__PURE__ */ new Set();
5722
5706
  for (const item of raw) {
5723
5707
  const entry = parseKeywordEntry(item, defaultMatch);
5724
- const key = `${entry.text}\0${entry.matchType}`;
5708
+ const key = JSON.stringify([entry.text, entry.matchType]);
5725
5709
  if (!seen.has(key)) {
5726
5710
  seen.add(key);
5727
5711
  entries.push(entry);
@@ -5729,19 +5713,19 @@ function keywordEntries(args) {
5729
5713
  }
5730
5714
  return entries;
5731
5715
  }
5732
- function loadJsonFileArg(path27) {
5733
- if (typeof path27 !== "string" || path27.length === 0) {
5716
+ function loadJsonFileArg(path28) {
5717
+ if (typeof path28 !== "string" || path28.length === 0) {
5734
5718
  return {};
5735
5719
  }
5736
5720
  try {
5737
- const parsed = JSON.parse(readFileSync2(path27, "utf8"));
5721
+ const parsed = JSON.parse(readFileSync2(path28, "utf8"));
5738
5722
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5739
- failWriteValidation(`${path27} must contain a JSON object`);
5723
+ failWriteValidation(`${path28} must contain a JSON object`);
5740
5724
  }
5741
5725
  return parsed;
5742
5726
  } catch (err) {
5743
5727
  if (err instanceof SyntaxError) {
5744
- failWriteValidation(`${path27} is not valid JSON: ${err.message}`);
5728
+ failWriteValidation(`${path28} is not valid JSON: ${err.message}`);
5745
5729
  }
5746
5730
  throw err;
5747
5731
  }
@@ -5788,6 +5772,9 @@ function handleGoogleError(err) {
5788
5772
  });
5789
5773
  process.exit(1);
5790
5774
  }
5775
+ function noopHints(data) {
5776
+ return data.staged ? [] : [`NOT STAGED \u2014 ${data.reason}. Nothing to publish for this change; say so rather than reporting it as done.`];
5777
+ }
5791
5778
  async function stageGoogleOp(raw, hints) {
5792
5779
  const preflight = googleDraftOpInputSchema.safeParse(raw);
5793
5780
  if (!preflight.success) {
@@ -5805,7 +5792,8 @@ async function stageGoogleOp(raw, hints) {
5805
5792
  try {
5806
5793
  const chatId = requireChatId();
5807
5794
  const response = await apiPost("/api/ads/google/draft/stage", { chatId, op: preflight.data });
5808
- writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5795
+ const allHints = [...noopHints(response.data), ...hints ?? []];
5796
+ writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5809
5797
  } catch (err) {
5810
5798
  handleGoogleError(err);
5811
5799
  }
@@ -5841,7 +5829,14 @@ async function stageGoogleOps(rawOps, hints) {
5841
5829
  try {
5842
5830
  const chatId = requireChatId();
5843
5831
  const response = await apiPost("/api/ads/google/draft/stage-batch", { chatId, ops });
5844
- writeJsonEnvelope(hints && hints.length > 0 ? { ...response, hints } : response);
5832
+ const skipped = response.data.skipped ?? [];
5833
+ const allHints = [
5834
+ ...skipped.length > 0 ? [
5835
+ `${skipped.length} of ${ops.length} op(s) NOT STAGED \u2014 already in the requested state: ${skipped.map((s) => s.reason).join("; ")}`
5836
+ ] : [],
5837
+ ...hints ?? []
5838
+ ];
5839
+ writeJsonEnvelope(allHints.length > 0 ? { ...response, hints: allHints } : response);
5845
5840
  } catch (err) {
5846
5841
  handleGoogleError(err);
5847
5842
  }
@@ -5852,10 +5847,10 @@ async function stageUpdate(kind, customerId, target, payload) {
5852
5847
  async function stageTarget(kind, customerId, target) {
5853
5848
  await stageGoogleOp({ kind, customerId, target });
5854
5849
  }
5855
- async function draftAction(path27, body) {
5850
+ async function draftAction(path28, body) {
5856
5851
  try {
5857
5852
  const chatId = requireChatId();
5858
- const response = await apiPost(path27, { chatId, ...body });
5853
+ const response = await apiPost(path28, { chatId, ...body });
5859
5854
  writeJsonEnvelope(response);
5860
5855
  } catch (err) {
5861
5856
  handleGoogleError(err);
@@ -9438,19 +9433,19 @@ function failWriteValidation2(message) {
9438
9433
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
9439
9434
  process.exit(1);
9440
9435
  }
9441
- function loadJsonFileArg2(path27) {
9442
- if (typeof path27 !== "string" || path27.length === 0) {
9436
+ function loadJsonFileArg2(path28) {
9437
+ if (typeof path28 !== "string" || path28.length === 0) {
9443
9438
  return {};
9444
9439
  }
9445
9440
  try {
9446
- const parsed = JSON.parse(readFileSync6(path27, "utf8"));
9441
+ const parsed = JSON.parse(readFileSync6(path28, "utf8"));
9447
9442
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
9448
- failWriteValidation2(`${path27} must contain a JSON object`);
9443
+ failWriteValidation2(`${path28} must contain a JSON object`);
9449
9444
  }
9450
9445
  return parsed;
9451
9446
  } catch (err) {
9452
9447
  if (err instanceof SyntaxError) {
9453
- failWriteValidation2(`${path27} is not valid JSON: ${err.message}`);
9448
+ failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
9454
9449
  }
9455
9450
  throw err;
9456
9451
  }
@@ -9535,15 +9530,15 @@ function parseLocaleFlag(value) {
9535
9530
  }
9536
9531
  return { language: match[1], country: match[2].toUpperCase() };
9537
9532
  }
9538
- function loadTargetingFileArg(path27) {
9539
- if (typeof path27 !== "string" || path27.length === 0) {
9533
+ function loadTargetingFileArg(path28) {
9534
+ if (typeof path28 !== "string" || path28.length === 0) {
9540
9535
  return void 0;
9541
9536
  }
9542
- const parsed = loadJsonFileArg2(path27);
9537
+ const parsed = loadJsonFileArg2(path28);
9543
9538
  const criteria = parsed.targetingCriteria ?? parsed;
9544
9539
  if (!criteria.include) {
9545
9540
  failWriteValidation2(
9546
- `${path27} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
9541
+ `${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
9547
9542
  );
9548
9543
  }
9549
9544
  return criteria;
@@ -9578,14 +9573,14 @@ function parseCsvLine(line) {
9578
9573
  cells.push(current);
9579
9574
  return cells.map((cell) => cell.trim());
9580
9575
  }
9581
- function parseListFileArg(path27, maxRows) {
9582
- if (typeof path27 !== "string" || path27.length === 0) {
9576
+ function parseListFileArg(path28, maxRows) {
9577
+ if (typeof path28 !== "string" || path28.length === 0) {
9583
9578
  return void 0;
9584
9579
  }
9585
- const raw = readFileSync6(path27, "utf8");
9580
+ const raw = readFileSync6(path28, "utf8");
9586
9581
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
9587
9582
  if (lines.length < 2) {
9588
- failWriteValidation2(`${path27} needs a header row and at least one data row`);
9583
+ failWriteValidation2(`${path28} needs a header row and at least one data row`);
9589
9584
  }
9590
9585
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
9591
9586
  const rows = [];
@@ -9604,7 +9599,7 @@ function parseListFileArg(path27, maxRows) {
9604
9599
  }
9605
9600
  }
9606
9601
  if (rows.length > maxRows) {
9607
- failWriteValidation2(`${path27} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
9602
+ failWriteValidation2(`${path28} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
9608
9603
  }
9609
9604
  return { columns, rows };
9610
9605
  }
@@ -11704,11 +11699,11 @@ var updateStatusSchema = z14.enum(UPDATE_STATUSES);
11704
11699
  function currencyMinimums2(currencyCode) {
11705
11700
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
11706
11701
  }
11707
- function validateDailyBudgetFloor(money, ctx, path27) {
11702
+ function validateDailyBudgetFloor(money, ctx, path28) {
11708
11703
  if (money?.currencyCode) {
11709
11704
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
11710
11705
  if (Number(money.amount) < min) {
11711
- ctx.addIssue({ code: "custom", path: path27, message: `below the ${min} ${money.currencyCode} daily minimum` });
11706
+ ctx.addIssue({ code: "custom", path: path28, message: `below the ${min} ${money.currencyCode} daily minimum` });
11712
11707
  }
11713
11708
  }
11714
11709
  }
@@ -12340,19 +12335,19 @@ function failWriteValidation3(message) {
12340
12335
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
12341
12336
  process.exit(1);
12342
12337
  }
12343
- function loadJsonFileArg3(path27) {
12344
- if (typeof path27 !== "string" || path27.length === 0) {
12338
+ function loadJsonFileArg3(path28) {
12339
+ if (typeof path28 !== "string" || path28.length === 0) {
12345
12340
  return {};
12346
12341
  }
12347
12342
  try {
12348
- const parsed = JSON.parse(readFileSync8(path27, "utf8"));
12343
+ const parsed = JSON.parse(readFileSync8(path28, "utf8"));
12349
12344
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
12350
- failWriteValidation3(`${path27} must contain a JSON object`);
12345
+ failWriteValidation3(`${path28} must contain a JSON object`);
12351
12346
  }
12352
12347
  return parsed;
12353
12348
  } catch (err) {
12354
12349
  if (err instanceof SyntaxError) {
12355
- failWriteValidation3(`${path27} is not valid JSON: ${err.message}`);
12350
+ failWriteValidation3(`${path28} is not valid JSON: ${err.message}`);
12356
12351
  }
12357
12352
  throw err;
12358
12353
  }
@@ -15291,12 +15286,327 @@ Full guides: __tooling__/docs/tools/baker/ads-<platform>.md (google|meta|linkedi
15291
15286
  }
15292
15287
  });
15293
15288
 
15289
+ // src/commands/brand/index.ts
15290
+ import { defineCommand as defineCommand86 } from "citty";
15291
+
15292
+ // src/commands/brand/fonts.ts
15293
+ import { mkdir, readFile, writeFile } from "fs/promises";
15294
+ import path from "path";
15295
+ import { defineCommand as defineCommand85 } from "citty";
15296
+
15297
+ // src/engine/brand/fonts.ts
15298
+ var GOOGLE_CSS2 = "https://fonts.googleapis.com/css2";
15299
+ function parseGoogleFontFaces(css) {
15300
+ const faces = [];
15301
+ const re = /\/\*\s*([\w-]+)\s*\*\/\s*@font-face\s*\{([^}]*)\}/g;
15302
+ for (const match of String(css).matchAll(re)) {
15303
+ const subset = match[1] ?? "";
15304
+ const body = match[2] ?? "";
15305
+ const family = body.match(/font-family\s*:\s*['"]?([^'";]+)['"]?\s*;/i)?.[1]?.trim();
15306
+ const url = body.match(/src\s*:\s*url\(\s*['"]?([^'")]+)['"]?\s*\)/i)?.[1]?.trim();
15307
+ const weight = body.match(/font-weight\s*:\s*(\d{3})\s*;/i)?.[1];
15308
+ if (!family || !url || !weight) continue;
15309
+ faces.push({
15310
+ subset,
15311
+ family,
15312
+ style: body.match(/font-style\s*:\s*([\w-]+)\s*;/i)?.[1]?.trim() ?? "normal",
15313
+ weight: Number(weight),
15314
+ url,
15315
+ unicodeRange: body.match(/unicode-range\s*:\s*([^;]+);/i)?.[1]?.trim() ?? ""
15316
+ });
15317
+ }
15318
+ return faces;
15319
+ }
15320
+ function fontFileName(face) {
15321
+ const family = face.family.replace(/[^A-Za-z0-9]/g, "");
15322
+ const style = face.style === "italic" ? "italic" : "";
15323
+ return `${family}-${face.weight}${style}-${face.subset}.woff2`;
15324
+ }
15325
+ function renderFontFaceCss(faces, hrefFor) {
15326
+ return faces.map(
15327
+ (face) => [
15328
+ "@font-face {",
15329
+ ` font-family: '${face.family}';`,
15330
+ ` font-style: ${face.style};`,
15331
+ ` font-weight: ${face.weight};`,
15332
+ " font-display: swap;",
15333
+ ` src: url('${hrefFor(face)}') format('woff2');`,
15334
+ ...face.unicodeRange ? [` unicode-range: ${face.unicodeRange};`] : [],
15335
+ "}"
15336
+ ].join("\n")
15337
+ ).join("\n");
15338
+ }
15339
+ function parseWeightAxis(spec) {
15340
+ const axis = spec.match(/wght@([^&]+)/i)?.[1] ?? "";
15341
+ const weights = /* @__PURE__ */ new Set();
15342
+ for (const tuple of axis.split(";")) {
15343
+ const last = tuple.split(",").pop()?.trim() ?? "";
15344
+ const range = last.match(/^(\d{3})\.\.(\d{3})$/);
15345
+ if (range) {
15346
+ for (let w = Number(range[1]); w <= Number(range[2]); w += 100) weights.add(w);
15347
+ } else if (/^\d{3}$/.test(last)) {
15348
+ weights.add(Number(last));
15349
+ }
15350
+ }
15351
+ return [...weights].sort((a, b) => a - b);
15352
+ }
15353
+ function googleFontRequests(css) {
15354
+ const out = [];
15355
+ for (const link of String(css).matchAll(/fonts\.googleapis\.com\/[^\s"')]+/gi)) {
15356
+ for (const param of link[0].matchAll(/[?&]family=([^&]+)/gi)) {
15357
+ const [namePart, ...specParts] = (param[1] ?? "").split(":");
15358
+ const family = decodeURIComponent((namePart ?? "").replaceAll("+", " ")).trim();
15359
+ if (!family) continue;
15360
+ out.push({ family, weights: parseWeightAxis(specParts.join(":")) });
15361
+ }
15362
+ }
15363
+ return out;
15364
+ }
15365
+ function resolveFetchWeights(flag, declared) {
15366
+ if (flag) {
15367
+ return String(flag).split(",").map((w) => Number(w.trim())).filter((w) => Number.isInteger(w) && w >= 100 && w <= 900);
15368
+ }
15369
+ return declared?.length ? declared : [400];
15370
+ }
15371
+ function googleFontCssUrl(family, weights) {
15372
+ const name = family.trim().replace(/\s+/g, "+");
15373
+ const axis = [...new Set(weights)].sort((a, b) => a - b).join(";");
15374
+ const spec = axis ? `${name}:wght@${axis}` : name;
15375
+ return `${GOOGLE_CSS2}?family=${spec}&display=swap`;
15376
+ }
15377
+
15378
+ // src/commands/brand/fonts.ts
15379
+ var GLOBAL_CSS = path.join("src", "styles", "global.css");
15380
+ var FONTS_DIR = path.join("src", "brand", "fonts");
15381
+ var DEFAULT_SUBSETS = "latin";
15382
+ var BROWSER_UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36";
15383
+ registerSchema({
15384
+ command: "brand.fonts.check",
15385
+ description: "Start here when a brand's fonts are 'confirmed' but nobody verified it. Asks Google Fonts what it ACTUALLY serves for every family/weight src/styles/global.css requests, and reports any the brand promises but Google will not serve.",
15386
+ args: {}
15387
+ });
15388
+ registerSchema({
15389
+ command: "brand.fonts.fetch",
15390
+ description: "Download a Google font into src/brand/fonts/ so the brand stops depending on a third party at page load, and print the @font-face block to paste above @theme in src/styles/global.css.",
15391
+ args: {
15392
+ family: { type: "string", description: 'Font family, e.g. "DM Sans"', required: true },
15393
+ weights: {
15394
+ type: "string",
15395
+ description: "Comma-separated weights (default: what global.css requests, else 400)",
15396
+ required: false
15397
+ },
15398
+ subsets: {
15399
+ type: "string",
15400
+ description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})`,
15401
+ required: false
15402
+ }
15403
+ }
15404
+ });
15405
+ function fail(code, message, fix) {
15406
+ writeJson({ ok: false, error: { code, message, ...fix ? { fix } : {} } });
15407
+ process.exit(2);
15408
+ }
15409
+ async function fetchGoogleCss(url) {
15410
+ try {
15411
+ const res = await fetch(url, { headers: { "User-Agent": BROWSER_UA } });
15412
+ if (!res.ok) return null;
15413
+ return await res.text();
15414
+ } catch {
15415
+ return null;
15416
+ }
15417
+ }
15418
+ async function diagnoseUnserved(family) {
15419
+ const bare = await fetchGoogleCss(googleFontCssUrl(family, []));
15420
+ return bare === null ? "unknown-family" : "unavailable-weight";
15421
+ }
15422
+ async function readGlobalCss() {
15423
+ try {
15424
+ return await readFile(path.resolve(process.cwd(), GLOBAL_CSS), "utf8");
15425
+ } catch {
15426
+ return "";
15427
+ }
15428
+ }
15429
+ var fontsCheckCommand = defineCommand85({
15430
+ meta: {
15431
+ name: "check",
15432
+ description: "Verify the brand's fonts really exist. For every family/weight src/styles/global.css requests from Google Fonts, ask Google what it actually serves and report anything missing \u2014 a weight nobody serves renders as a synthesized fallback and looks off-brand everywhere."
15433
+ },
15434
+ async run() {
15435
+ const css = await readGlobalCss();
15436
+ const requests = googleFontRequests(css);
15437
+ if (requests.length === 0) {
15438
+ writeJson({
15439
+ ok: true,
15440
+ data: { families: [], selfHosted: true },
15441
+ hints: [
15442
+ `No Google Fonts requested in ${GLOBAL_CSS}. If the brand self-hosts, the offline validator already checks those files; nothing to verify against Google.`
15443
+ ]
15444
+ });
15445
+ return;
15446
+ }
15447
+ const families = [];
15448
+ const unavailable = [];
15449
+ for (const request of requests) {
15450
+ const url = googleFontCssUrl(request.family, request.weights);
15451
+ const served = await fetchGoogleCss(url);
15452
+ if (served === null) {
15453
+ const cause = await diagnoseUnserved(request.family);
15454
+ unavailable.push(
15455
+ cause === "unknown-family" ? `${request.family} (no such family on Google Fonts \u2014 check the exact spelling)` : `${request.family} ${request.weights.join("/")} (the family exists, these weights do not)`
15456
+ );
15457
+ families.push({
15458
+ family: request.family,
15459
+ requested: request.weights,
15460
+ served: [],
15461
+ missing: request.weights,
15462
+ cause
15463
+ });
15464
+ continue;
15465
+ }
15466
+ const faces = parseGoogleFontFaces(served);
15467
+ const servedWeights = [...new Set(faces.map((f) => f.weight))].sort((a, b) => a - b);
15468
+ const missing = request.weights.filter((w) => !servedWeights.includes(w));
15469
+ if (missing.length > 0) unavailable.push(`${request.family} ${missing.join("/")}`);
15470
+ families.push({
15471
+ family: request.family,
15472
+ requested: request.weights,
15473
+ served: servedWeights,
15474
+ missing,
15475
+ subsets: [...new Set(faces.map((f) => f.subset))]
15476
+ });
15477
+ }
15478
+ if (unavailable.length > 0) {
15479
+ fail("FONT_NOT_SERVED", `Google Fonts does not serve: ${unavailable.join("; ")}`, {
15480
+ action: `Correct the family name or weight in ${GLOBAL_CSS} and src/brand/BRAND.md, or run \`baker brand fonts fetch\` for a weight that does exist.`,
15481
+ explanation: "A requested weight Google does not serve is silently synthesized by the browser, so the page looks subtly wrong with nothing failing."
15482
+ });
15483
+ }
15484
+ writeJson({
15485
+ ok: true,
15486
+ data: { families },
15487
+ hints: [
15488
+ "Every requested family and weight is really served by Google.",
15489
+ `To stop depending on Google at page load, run \`baker brand fonts fetch "<family>"\` to self-host into ${FONTS_DIR}.`
15490
+ ]
15491
+ });
15492
+ }
15493
+ });
15494
+ var fontsFetchCommand = defineCommand85({
15495
+ meta: {
15496
+ name: "fetch",
15497
+ description: "Download a Google font into src/brand/fonts/ and print the @font-face block to paste above @theme in src/styles/global.css. Use when a brand font should be self-hosted rather than requested from Google on every page load."
15498
+ },
15499
+ args: {
15500
+ family: { type: "positional", required: true, description: 'Font family, e.g. "DM Sans"' },
15501
+ weights: { type: "string", description: "Comma-separated weights (default: what global.css requests, else 400)" },
15502
+ subsets: { type: "string", description: `Comma-separated unicode subsets (default: ${DEFAULT_SUBSETS})` }
15503
+ },
15504
+ async run({ args }) {
15505
+ const family = String(args.family).trim();
15506
+ if (!family) fail("INVALID_FAMILY", 'Pass a font family, e.g. `baker brand fonts fetch "DM Sans"`.');
15507
+ const css = await readGlobalCss();
15508
+ const declared = googleFontRequests(css).find((r) => r.family.toLowerCase() === family.toLowerCase());
15509
+ const weights = resolveFetchWeights(args.weights, declared?.weights);
15510
+ if (weights.length === 0) {
15511
+ fail("INVALID_WEIGHTS", `"${args.weights}" has no usable weight \u2014 pass whole hundreds, e.g. --weights 400,700.`);
15512
+ }
15513
+ const wanted = new Set(
15514
+ String(args.subsets ?? DEFAULT_SUBSETS).split(",").map((s) => s.trim()).filter(Boolean)
15515
+ );
15516
+ const servedCss = await fetchGoogleCss(googleFontCssUrl(family, weights));
15517
+ if (servedCss === null) {
15518
+ const cause = await diagnoseUnserved(family);
15519
+ fail(
15520
+ "FONT_NOT_SERVED",
15521
+ cause === "unknown-family" ? `Google Fonts has no family called "${family}".` : `"${family}" exists on Google Fonts but not at weight ${weights.join("/")}.`,
15522
+ cause === "unknown-family" ? {
15523
+ action: "Check the exact family name at fonts.google.com, then retry.",
15524
+ explanation: "Family names are case- and space-sensitive in the Google Fonts API."
15525
+ } : {
15526
+ action: `Run \`baker brand fonts check\` to see which weights this family really ships, then retry with --weights set to one of those.`,
15527
+ explanation: "Google rejects the whole request when one requested weight does not exist."
15528
+ }
15529
+ );
15530
+ }
15531
+ const faces = parseGoogleFontFaces(servedCss).filter((f) => wanted.has(f.subset));
15532
+ if (faces.length === 0) {
15533
+ fail("SUBSET_NOT_SERVED", `"${family}" has no ${[...wanted].join("/")} subset.`, {
15534
+ action: `Retry with --subsets set to one Google actually serves for this family.`,
15535
+ explanation: "Google splits each family into unicode subsets; not every family ships every subset."
15536
+ });
15537
+ }
15538
+ const fontsDir = path.resolve(process.cwd(), FONTS_DIR);
15539
+ await mkdir(fontsDir, { recursive: true });
15540
+ const downloaded = [];
15541
+ for (const face of faces) {
15542
+ const res = await fetch(face.url, { headers: { "User-Agent": BROWSER_UA } });
15543
+ if (!res.ok) continue;
15544
+ await writeFile(path.join(fontsDir, fontFileName(face)), Buffer.from(await res.arrayBuffer()));
15545
+ downloaded.push(face);
15546
+ }
15547
+ const written = downloaded.map(fontFileName);
15548
+ if (written.length === 0) {
15549
+ fail("DOWNLOAD_FAILED", `Could not download any file for "${family}".`, {
15550
+ action: "Retry; if it keeps failing, keep the Google @import and note it in BRAND.md.",
15551
+ explanation: "The font stylesheet resolved but the font files themselves did not download."
15552
+ });
15553
+ }
15554
+ const fontFace = renderFontFaceCss(downloaded, (f) => `/${FONTS_DIR}/${fontFileName(f)}`);
15555
+ const downloadedWeights = [...new Set(downloaded.map((f) => f.weight))].sort((a, b) => a - b);
15556
+ const incomplete = weights.filter((w) => !downloadedWeights.includes(w));
15557
+ writeJson({
15558
+ ok: true,
15559
+ data: { family, weights: downloadedWeights, files: written, fontFace },
15560
+ hints: [
15561
+ `Paste the \`fontFace\` block ABOVE the @theme block in ${GLOBAL_CSS}, then remove "${family}" from the Google @import so the page stops fetching it at load.`,
15562
+ `Record the exact weights (${downloadedWeights.join(", ")}) in src/brand/BRAND.md \u2014 a weight listed there but not downloaded renders as a synthesized fallback.`,
15563
+ ...incomplete.length > 0 ? [
15564
+ `PARTIAL: ${incomplete.join(", ")} did not download and is NOT in the \`fontFace\` block. Keep "${family}" in the Google @import for those weights, or re-run \`baker brand fonts fetch "${family}" --weights ${incomplete.join(",")}\`.`
15565
+ ] : [],
15566
+ "Licensing: Google Fonts are open-licensed and safe to self-host. A client's own commercial face is not \u2014 confirm before re-hosting one."
15567
+ ]
15568
+ });
15569
+ }
15570
+ });
15571
+ var fontsCommand = defineCommand85({
15572
+ meta: {
15573
+ name: "fonts",
15574
+ description: `Verify and self-host the brand's typefaces.
15575
+
15576
+ Start here: \`baker brand fonts check\` \u2014 confirms the fonts the brand claims are really served.
15577
+
15578
+ Subcommands:
15579
+ baker brand fonts check \u2014 ask Google what it actually serves for every family/weight global.css requests
15580
+ baker brand fonts fetch <family> \u2014 download a family into src/brand/fonts/ and print its @font-face block`
15581
+ },
15582
+ subCommands: {
15583
+ check: fontsCheckCommand,
15584
+ fetch: fontsFetchCommand
15585
+ }
15586
+ });
15587
+
15588
+ // src/commands/brand/index.ts
15589
+ var brandCommand = defineCommand86({
15590
+ meta: {
15591
+ name: "brand",
15592
+ description: `Brand asset verification for src/brand/.
15593
+
15594
+ Start here: \`baker brand fonts check\` after defining or editing the brand's typography.
15595
+
15596
+ Subcommands:
15597
+ baker brand fonts \u2014 verify the brand's typefaces really load, and self-host them into src/brand/fonts/`
15598
+ },
15599
+ subCommands: {
15600
+ fonts: fontsCommand
15601
+ }
15602
+ });
15603
+
15294
15604
  // src/commands/canvas/index.ts
15295
- import { defineCommand as defineCommand95 } from "citty";
15605
+ import { defineCommand as defineCommand97 } from "citty";
15296
15606
 
15297
15607
  // src/commands/canvas/catalog.ts
15298
- import { defineCommand as defineCommand85 } from "citty";
15299
- var catalogCommand = defineCommand85({
15608
+ import { defineCommand as defineCommand87 } from "citty";
15609
+ var catalogCommand = defineCommand87({
15300
15610
  meta: {
15301
15611
  name: "catalog",
15302
15612
  description: "Print the agent-facing node catalog (JSON Schema). Includes every registered node grouped by category."
@@ -15309,9 +15619,9 @@ var catalogCommand = defineCommand85({
15309
15619
  });
15310
15620
 
15311
15621
  // src/commands/canvas/critique.ts
15312
- import { readFile } from "fs/promises";
15313
- import path from "path";
15314
- import { defineCommand as defineCommand86 } from "citty";
15622
+ import { readFile as readFile2 } from "fs/promises";
15623
+ import path2 from "path";
15624
+ import { defineCommand as defineCommand88 } from "citty";
15315
15625
 
15316
15626
  // src/engine/scaffold/lib/critique.ts
15317
15627
  var VIBE_CUE = /\b(light|lighting|golden|moody|warm|cool|tone|grade|contrast|shadow|glow|rim|backlit|neon|soft)\b/i;
@@ -15431,15 +15741,15 @@ function critiqueCanvas(canvas) {
15431
15741
  }
15432
15742
 
15433
15743
  // src/commands/canvas/critique.ts
15434
- var critiqueCommand = defineCommand86({
15744
+ var critiqueCommand = defineCommand88({
15435
15745
  meta: {
15436
15746
  name: "critique",
15437
15747
  description: "Pre-spend creative critic (ADVISORY \u2014 never blocks). Scores a scaffolded creative BEFORE the billed render, so weak spots get fixed for free. A VIDEO creative is scored on hook strength, sound-off first-frame legibility, retention risk, and mechanism clarity; a STATIC ad on baked-text legibility risk, identity grounding, brand-type fidelity, and mechanism clarity. Ground a video hook against `baker winning-ads hooks` for a proven pattern."
15438
15748
  },
15439
15749
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
15440
15750
  async run({ args }) {
15441
- const filePath = path.resolve(String(args.file));
15442
- const raw = await readFile(filePath, "utf8");
15751
+ const filePath = path2.resolve(String(args.file));
15752
+ const raw = await readFile2(filePath, "utf8");
15443
15753
  let parsed;
15444
15754
  try {
15445
15755
  parsed = JSON.parse(raw);
@@ -15470,12 +15780,12 @@ var critiqueCommand = defineCommand86({
15470
15780
 
15471
15781
  // src/commands/canvas/inspect.ts
15472
15782
  import { execFile } from "child_process";
15473
- import { readdir, readFile as readFile2, stat } from "fs/promises";
15474
- import path2 from "path";
15783
+ import { readdir, readFile as readFile3, stat } from "fs/promises";
15784
+ import path3 from "path";
15475
15785
  import { promisify } from "util";
15476
- import { defineCommand as defineCommand87 } from "citty";
15786
+ import { defineCommand as defineCommand89 } from "citty";
15477
15787
  var execFileAsync = promisify(execFile);
15478
- var inspectCommand = defineCommand87({
15788
+ var inspectCommand = defineCommand89({
15479
15789
  meta: {
15480
15790
  name: "inspect",
15481
15791
  description: "Dump a one-page summary of a canvas run: per-node duration + cache status, list of output files in the run dir, and optionally three thumbnail frames per video output. Pass either a run_id (resolved against --outputs-dir) or an absolute run directory."
@@ -15489,7 +15799,7 @@ var inspectCommand = defineCommand87({
15489
15799
  }
15490
15800
  },
15491
15801
  async run({ args }) {
15492
- const outputsDir = path2.resolve(String(args["outputs-dir"] ?? "canvas"));
15802
+ const outputsDir = path3.resolve(String(args["outputs-dir"] ?? "canvas"));
15493
15803
  const runArg = String(args.run);
15494
15804
  const runDir = await resolveRunDir(runArg, outputsDir);
15495
15805
  const manifest = await loadManifest(runDir);
@@ -15501,7 +15811,7 @@ var inspectCommand = defineCommand87({
15501
15811
  }
15502
15812
  const summary = {
15503
15813
  ok: true,
15504
- run_id: manifest.run_id ?? path2.basename(runDir),
15814
+ run_id: manifest.run_id ?? path3.basename(runDir),
15505
15815
  run_dir: runDir,
15506
15816
  stats: manifest.stats ?? null,
15507
15817
  output: manifest.output ?? null,
@@ -15514,20 +15824,20 @@ var inspectCommand = defineCommand87({
15514
15824
  }
15515
15825
  });
15516
15826
  async function resolveRunDir(run, outputsDir) {
15517
- if (path2.isAbsolute(run)) {
15827
+ if (path3.isAbsolute(run)) {
15518
15828
  const s2 = await stat(run).catch(() => null);
15519
15829
  if (s2?.isDirectory()) return run;
15520
15830
  throw new Error(`inspect: ${run} is not a directory`);
15521
15831
  }
15522
- const candidate = path2.join(outputsDir, run);
15832
+ const candidate = path3.join(outputsDir, run);
15523
15833
  const s = await stat(candidate).catch(() => null);
15524
15834
  if (s?.isDirectory()) return candidate;
15525
15835
  throw new Error(`inspect: no run directory at ${candidate}`);
15526
15836
  }
15527
15837
  async function loadManifest(runDir) {
15528
- const manifestPath = path2.join(runDir, "manifest.json");
15838
+ const manifestPath = path3.join(runDir, "manifest.json");
15529
15839
  try {
15530
- const raw = await readFile2(manifestPath, "utf-8");
15840
+ const raw = await readFile3(manifestPath, "utf-8");
15531
15841
  return JSON.parse(raw);
15532
15842
  } catch {
15533
15843
  return {};
@@ -15537,7 +15847,7 @@ async function listRunFiles(runDir) {
15537
15847
  const out = [];
15538
15848
  const names = await readdir(runDir);
15539
15849
  for (const name of names) {
15540
- const abs = path2.join(runDir, name);
15850
+ const abs = path3.join(runDir, name);
15541
15851
  const s = await stat(abs).catch(() => null);
15542
15852
  if (!s?.isFile()) continue;
15543
15853
  out.push({ name, path: abs, size: s.size });
@@ -15582,13 +15892,13 @@ async function probeDuration(filePath) {
15582
15892
  }
15583
15893
 
15584
15894
  // src/commands/canvas/rerun.ts
15585
- import path13 from "path";
15586
- import { defineCommand as defineCommand89 } from "citty";
15895
+ import path14 from "path";
15896
+ import { defineCommand as defineCommand91 } from "citty";
15587
15897
 
15588
15898
  // src/commands/canvas/run.ts
15589
- import { readFile as readFile9 } from "fs/promises";
15590
- import path12 from "path";
15591
- import { defineCommand as defineCommand88 } from "citty";
15899
+ import { readFile as readFile10 } from "fs/promises";
15900
+ import path13 from "path";
15901
+ import { defineCommand as defineCommand90 } from "citty";
15592
15902
 
15593
15903
  // src/commands/canvas/placeholders.ts
15594
15904
  function unsuppliedPlaceholderAssets(canvas) {
@@ -15607,7 +15917,7 @@ function unsuppliedPlaceholderAssets(canvas) {
15607
15917
  }
15608
15918
 
15609
15919
  // src/commands/canvas/resolve-paths.ts
15610
- import path3 from "path";
15920
+ import path4 from "path";
15611
15921
  function resolveRelativeCanvasPaths(canvas, baseDir) {
15612
15922
  if (!canvas || typeof canvas !== "object") return canvas;
15613
15923
  const c = canvas;
@@ -15620,24 +15930,24 @@ function resolveNode(node, baseDir) {
15620
15930
  const params = n.params;
15621
15931
  if (!params || typeof params !== "object") return node;
15622
15932
  if (n.type === "ingest" && params.source === "path" && isResolvableRelative(params.path)) {
15623
- return { ...node, params: { ...params, path: path3.resolve(baseDir, params.path) } };
15933
+ return { ...node, params: { ...params, path: path4.resolve(baseDir, params.path) } };
15624
15934
  }
15625
15935
  if (n.type === "hyperframe_render" && isResolvableRelative(params.composition)) {
15626
- return { ...node, params: { ...params, composition: path3.resolve(baseDir, params.composition) } };
15936
+ return { ...node, params: { ...params, composition: path4.resolve(baseDir, params.composition) } };
15627
15937
  }
15628
15938
  return node;
15629
15939
  }
15630
15940
  function isResolvableRelative(value) {
15631
- return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path3.isAbsolute(value);
15941
+ return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !looksLikeHttpUrl(value) && !path4.isAbsolute(value);
15632
15942
  }
15633
15943
 
15634
15944
  // src/commands/canvas/source-version.ts
15635
- import { readFile as readFile4 } from "fs/promises";
15636
- import path5 from "path";
15945
+ import { readFile as readFile5 } from "fs/promises";
15946
+ import path6 from "path";
15637
15947
 
15638
15948
  // src/commands/canvas/scene-files.ts
15639
- import { mkdir, readFile as readFile3, readdir as readdir2, rm, writeFile } from "fs/promises";
15640
- import path4 from "path";
15949
+ import { mkdir as mkdir2, readFile as readFile4, readdir as readdir2, rm, writeFile as writeFile2 } from "fs/promises";
15950
+ import path5 from "path";
15641
15951
  var SCENES_DIR = "scenes";
15642
15952
  var GLOBAL_PROMPT_FILE = "prompt.json";
15643
15953
  var REBUILD_FILE = "prompt.rebuild.json";
@@ -15654,19 +15964,19 @@ function splitBlueprint(blueprint) {
15654
15964
  }
15655
15965
  async function writeSceneFiles(outDir, blueprint) {
15656
15966
  const { global, scenes } = splitBlueprint(blueprint);
15657
- await writeFile(path4.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
15967
+ await writeFile2(path5.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
15658
15968
  `, "utf8");
15659
- const scenesDir = path4.join(outDir, SCENES_DIR);
15660
- await mkdir(scenesDir, { recursive: true });
15969
+ const scenesDir = path5.join(outDir, SCENES_DIR);
15970
+ await mkdir2(scenesDir, { recursive: true });
15661
15971
  const written = /* @__PURE__ */ new Set();
15662
15972
  for (let i = 0; i < scenes.length; i++) {
15663
15973
  const name = sceneFileName(i, scenes.length);
15664
15974
  written.add(name);
15665
- await writeFile(path4.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
15975
+ await writeFile2(path5.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
15666
15976
  `, "utf8");
15667
15977
  }
15668
15978
  for (const name of await listSceneFileNames(scenesDir)) {
15669
- if (!written.has(name)) await rm(path4.join(scenesDir, name), { force: true });
15979
+ if (!written.has(name)) await rm(path5.join(scenesDir, name), { force: true });
15670
15980
  }
15671
15981
  }
15672
15982
  async function listSceneFileNames(scenesDir) {
@@ -15679,17 +15989,17 @@ async function listSceneFileNames(scenesDir) {
15679
15989
  return entries.filter((n) => /^s\d+\.json$/.test(n)).sort(bySceneIndex);
15680
15990
  }
15681
15991
  async function listSceneFiles(creativeDir) {
15682
- const scenesDir = path4.join(creativeDir, SCENES_DIR);
15683
- return (await listSceneFileNames(scenesDir)).map((n) => path4.join(scenesDir, n));
15992
+ const scenesDir = path5.join(creativeDir, SCENES_DIR);
15993
+ return (await listSceneFileNames(scenesDir)).map((n) => path5.join(scenesDir, n));
15684
15994
  }
15685
15995
  async function reassembleBlueprint(creativeDir) {
15686
15996
  const files = await listSceneFiles(creativeDir);
15687
15997
  if (files.length === 0) return null;
15688
- const globalRaw = await readFile3(path4.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
15998
+ const globalRaw = await readFile4(path5.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
15689
15999
  const global = JSON.parse(globalRaw);
15690
16000
  const scenes = [];
15691
16001
  for (const file of files) {
15692
- scenes.push(JSON.parse(await readFile3(file, "utf8")));
16002
+ scenes.push(JSON.parse(await readFile4(file, "utf8")));
15693
16003
  }
15694
16004
  return { ...global, scenes };
15695
16005
  }
@@ -15703,15 +16013,15 @@ function bySceneIndex(a, b) {
15703
16013
  async function computeSourceSha(canvasPath) {
15704
16014
  let canvasBytes;
15705
16015
  try {
15706
- canvasBytes = await readFile4(canvasPath);
16016
+ canvasBytes = await readFile5(canvasPath);
15707
16017
  } catch {
15708
16018
  return void 0;
15709
16019
  }
15710
- const canvasDir = path5.dirname(canvasPath);
15711
- const promptPath = path5.join(canvasDir, "prompt.json");
16020
+ const canvasDir = path6.dirname(canvasPath);
16021
+ const promptPath = path6.join(canvasDir, "prompt.json");
15712
16022
  let promptBytes;
15713
16023
  try {
15714
- promptBytes = await readFile4(promptPath);
16024
+ promptBytes = await readFile5(promptPath);
15715
16025
  } catch {
15716
16026
  promptBytes = Buffer.alloc(0);
15717
16027
  }
@@ -15724,7 +16034,7 @@ async function computeSourceSha(canvasPath) {
15724
16034
  for (const sceneFile of await listSceneFiles(canvasDir)) {
15725
16035
  let sceneBytes;
15726
16036
  try {
15727
- sceneBytes = await readFile4(sceneFile);
16037
+ sceneBytes = await readFile5(sceneFile);
15728
16038
  } catch {
15729
16039
  sceneBytes = Buffer.alloc(0);
15730
16040
  }
@@ -15734,8 +16044,8 @@ async function computeSourceSha(canvasPath) {
15734
16044
  }
15735
16045
 
15736
16046
  // src/commands/canvas/scene-projection.ts
15737
- import { readFile as readFile5 } from "fs/promises";
15738
- import path6 from "path";
16047
+ import { readFile as readFile6 } from "fs/promises";
16048
+ import path7 from "path";
15739
16049
 
15740
16050
  // src/engine/scaffold/video.ts
15741
16051
  import { toCardinal as nwAr } from "n2words/ar-SA";
@@ -16661,7 +16971,9 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16661
16971
  const imageProfile = imageProfileFor(ctx.imageModel);
16662
16972
  const genParams = {
16663
16973
  model: ctx.imageModel,
16664
- image_size: "2K",
16974
+ // gpt-image-2 derives pixel dimensions from the ratio and has no size knob,
16975
+ // so asking for 2K there is an `unknown_param` at validate.
16976
+ ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: "2K" } : {},
16665
16977
  // Per-model image defaults (gpt-image: quality=high — OpenRouter forwards it; we do
16666
16978
  // NOT send input_fidelity, which gpt-image-2 forces high automatically).
16667
16979
  ...imageProfile?.paramDefaults ?? {},
@@ -16676,7 +16988,7 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
16676
16988
  ctx.imageModel
16677
16989
  )
16678
16990
  };
16679
- if (ctx.genAr) genParams.aspect_ratio = ctx.genAr;
16991
+ if (ctx.genAr) genParams.aspect_ratio = nearestSupportedAspectRatio("image_generate", ctx.imageModel, ctx.genAr);
16680
16992
  const genId = `s${ctx.sceneIndex}${tag}_${edge}`;
16681
16993
  nodes.push({
16682
16994
  id: genId,
@@ -18992,12 +19304,12 @@ function nonPromptParamsDiverge(live = {}, rebuilt = {}) {
18992
19304
  return false;
18993
19305
  }
18994
19306
  async function syncSceneNodeParams(canvas, canvasPath, log) {
18995
- const creativeDir = path6.dirname(canvasPath);
19307
+ const creativeDir = path7.dirname(canvasPath);
18996
19308
  const blueprint = await reassembleBlueprint(creativeDir);
18997
19309
  if (!blueprint) return "not_applicable";
18998
19310
  let rebuildRaw;
18999
19311
  try {
19000
- rebuildRaw = await readFile5(path6.join(creativeDir, REBUILD_FILE), "utf8");
19312
+ rebuildRaw = await readFile6(path7.join(creativeDir, REBUILD_FILE), "utf8");
19001
19313
  } catch {
19002
19314
  return "not_applicable";
19003
19315
  }
@@ -19038,7 +19350,7 @@ async function syncSceneNodeParams(canvas, canvasPath, log) {
19038
19350
  }
19039
19351
 
19040
19352
  // src/commands/canvas/style-projection.ts
19041
- import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
19353
+ import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
19042
19354
  function findBlueprintProjection(canvas) {
19043
19355
  if (!canvas || typeof canvas !== "object") return null;
19044
19356
  const nodes = canvas.nodes;
@@ -19066,10 +19378,10 @@ function renderStyleProjectionFromValue(blueprint) {
19066
19378
  async function syncStyleProjection(canvas, log) {
19067
19379
  const pair = findBlueprintProjection(canvas);
19068
19380
  if (!pair) return "not_applicable";
19069
- const rendered = renderStyleProjection(await readFile6(pair.promptPath, "utf8"));
19070
- const current = await readFile6(pair.stylePath, "utf8").catch(() => null);
19381
+ const rendered = renderStyleProjection(await readFile7(pair.promptPath, "utf8"));
19382
+ const current = await readFile7(pair.stylePath, "utf8").catch(() => null);
19071
19383
  if (current === rendered) return "up_to_date";
19072
- await writeFile2(pair.stylePath, rendered, "utf8");
19384
+ await writeFile3(pair.stylePath, rendered, "utf8");
19073
19385
  log(
19074
19386
  "[style] prompt.style.json regenerated from prompt.json \u2014 every frame's shared ad spec changed; affected image frames will re-bill on the next run"
19075
19387
  );
@@ -19129,13 +19441,13 @@ ${body}` : header || body || compactJson(record);
19129
19441
  }
19130
19442
 
19131
19443
  // src/commands/canvas/run-record.ts
19132
- import path7 from "path";
19444
+ import path8 from "path";
19133
19445
  var MAX_RUN_NODES = 200;
19134
19446
  var MAX_OUTPUTS_PER_NODE = 10;
19135
19447
  var MAX_FINAL_OUTPUTS = 10;
19136
19448
  var MAX_CREATIVE_SLUG_LENGTH = 100;
19137
19449
  function creativeSlugFromCanvasPath(filePath) {
19138
- const normalized = filePath.split(path7.sep).join("/");
19450
+ const normalized = filePath.split(path8.sep).join("/");
19139
19451
  const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
19140
19452
  const slug = match?.[1] ?? null;
19141
19453
  return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
@@ -19423,7 +19735,7 @@ var RunRecordPoster = class {
19423
19735
 
19424
19736
  // src/commands/canvas/run-retention.ts
19425
19737
  import { rm as rm2 } from "fs/promises";
19426
- import path8 from "path";
19738
+ import path9 from "path";
19427
19739
  function runDirsToPrune(entries, keep, currentRunId) {
19428
19740
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
19429
19741
  if (keep <= 0) return runs;
@@ -19440,7 +19752,7 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
19440
19752
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
19441
19753
  if (toPrune.length === 0) return;
19442
19754
  for (const dir of toPrune) {
19443
- await rm2(path8.join(outputsDir, dir), { recursive: true, force: true }).catch(
19755
+ await rm2(path9.join(outputsDir, dir), { recursive: true, force: true }).catch(
19444
19756
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
19445
19757
  );
19446
19758
  }
@@ -19448,13 +19760,13 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
19448
19760
  }
19449
19761
 
19450
19762
  // src/commands/canvas/dirty-marker.ts
19451
- import { mkdir as mkdir2, readdir as readdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
19452
- import path9 from "path";
19763
+ import { mkdir as mkdir3, readdir as readdir3, rm as rm3, writeFile as writeFile4 } from "fs/promises";
19764
+ import path10 from "path";
19453
19765
  function creativeDirtyDir(base) {
19454
- return base ?? path9.resolve("canvas", ".dirty");
19766
+ return base ?? path10.resolve("canvas", ".dirty");
19455
19767
  }
19456
19768
  function dirtyMarkerFile(slug, base) {
19457
- return path9.join(creativeDirtyDir(base), `${slug}.json`);
19769
+ return path10.join(creativeDirtyDir(base), `${slug}.json`);
19458
19770
  }
19459
19771
  async function clearCreativeDirty(slug, base) {
19460
19772
  try {
@@ -19464,18 +19776,18 @@ async function clearCreativeDirty(slug, base) {
19464
19776
  }
19465
19777
 
19466
19778
  // src/commands/canvas/run-resume.ts
19467
- import { mkdir as mkdir3, readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
19468
- import path10 from "path";
19779
+ import { mkdir as mkdir4, readFile as readFile8, rm as rm4, writeFile as writeFile5 } from "fs/promises";
19780
+ import path11 from "path";
19469
19781
  function markerKey(canvasPath) {
19470
19782
  const slug = creativeSlugFromCanvasPath(canvasPath);
19471
- const identity = slug ?? path10.relative(process.cwd(), path10.resolve(canvasPath));
19783
+ const identity = slug ?? path11.relative(process.cwd(), path11.resolve(canvasPath));
19472
19784
  return sha256Hex(Buffer.from(identity)).slice(0, 32);
19473
19785
  }
19474
19786
  function legacyMarkerKey(canvasPath) {
19475
- return sha256Hex(Buffer.from(path10.resolve(canvasPath))).slice(0, 32);
19787
+ return sha256Hex(Buffer.from(path11.resolve(canvasPath))).slice(0, 32);
19476
19788
  }
19477
19789
  function markerFile(outputsDir, key) {
19478
- return path10.join(outputsDir, ".inflight", `${key}.json`);
19790
+ return path11.join(outputsDir, ".inflight", `${key}.json`);
19479
19791
  }
19480
19792
  var REMOTE_ADOPT_STALE_MS = 12e4;
19481
19793
  function classifyRemoteRun(run, now) {
@@ -19511,7 +19823,7 @@ async function resolveRunId(opts) {
19511
19823
  async function readMarkerRunId(outputsDir, canvasPath) {
19512
19824
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
19513
19825
  try {
19514
- const raw = await readFile7(markerFile(outputsDir, key), "utf8");
19826
+ const raw = await readFile8(markerFile(outputsDir, key), "utf8");
19515
19827
  const parsed = JSON.parse(raw);
19516
19828
  if (typeof parsed.runId === "string" && parsed.runId.length > 0) return parsed.runId;
19517
19829
  } catch {
@@ -19522,8 +19834,8 @@ async function readMarkerRunId(outputsDir, canvasPath) {
19522
19834
  async function markRunInFlight(outputsDir, canvasPath, runId) {
19523
19835
  try {
19524
19836
  const file = markerFile(outputsDir, markerKey(canvasPath));
19525
- await mkdir3(path10.dirname(file), { recursive: true });
19526
- await writeFile4(file, JSON.stringify({ runId, canvasPath: path10.resolve(canvasPath), startedAt: Date.now() }));
19837
+ await mkdir4(path11.dirname(file), { recursive: true });
19838
+ await writeFile5(file, JSON.stringify({ runId, canvasPath: path11.resolve(canvasPath), startedAt: Date.now() }));
19527
19839
  } catch {
19528
19840
  }
19529
19841
  }
@@ -19537,8 +19849,8 @@ async function clearRunMarker(outputsDir, canvasPath) {
19537
19849
  }
19538
19850
 
19539
19851
  // src/commands/canvas/run-snapshot.ts
19540
- import { mkdir as mkdir4, readdir as readdir4, readFile as readFile8, stat as stat2, writeFile as writeFile5 } from "fs/promises";
19541
- import path11 from "path";
19852
+ import { mkdir as mkdir5, readdir as readdir4, readFile as readFile9, stat as stat2, writeFile as writeFile6 } from "fs/promises";
19853
+ import path12 from "path";
19542
19854
  var SNAPSHOT_SCHEMA = "baker-canvas-snapshot/1";
19543
19855
  var MAX_SNAPSHOT_FILE_BYTES = 32 * 1024 * 1024;
19544
19856
  var EXT_TO_MIME = {
@@ -19565,11 +19877,11 @@ var EXT_TO_MIME = {
19565
19877
  woff2: "font/woff2"
19566
19878
  };
19567
19879
  function mimeForFile(filePath) {
19568
- const ext = path11.extname(filePath).slice(1).toLowerCase();
19880
+ const ext = path12.extname(filePath).slice(1).toLowerCase();
19569
19881
  return EXT_TO_MIME[ext] ?? "application/octet-stream";
19570
19882
  }
19571
19883
  function toPosix(p) {
19572
- return p.split(path11.sep).join("/");
19884
+ return p.split(path12.sep).join("/");
19573
19885
  }
19574
19886
  function localPathRefsFromCanvas(parsed) {
19575
19887
  const nodes = parsed?.nodes;
@@ -19594,37 +19906,37 @@ function isSnapshotablePath(value) {
19594
19906
  async function sourceRefsToSnapshot(canvasDir, parsed) {
19595
19907
  const refs = new Set(localPathRefsFromCanvas(parsed));
19596
19908
  for (const sceneFile of await listSceneFiles(canvasDir)) {
19597
- refs.add(toPosix(path11.relative(canvasDir, sceneFile)));
19909
+ refs.add(toPosix(path12.relative(canvasDir, sceneFile)));
19598
19910
  }
19599
19911
  refs.add(REBUILD_FILE);
19600
19912
  return [...refs];
19601
19913
  }
19602
19914
  async function uploadRunSnapshot(client, opts) {
19603
19915
  try {
19604
- const canvasDir = path11.dirname(opts.canvasPath);
19916
+ const canvasDir = path12.dirname(opts.canvasPath);
19605
19917
  const put = (bytes, mime) => putContentAddressed(client, bytes, mime, opts.signal);
19606
19918
  const canvasBytes = Buffer.from(opts.raw);
19607
19919
  const canvasUpload = await put(canvasBytes, "application/json");
19608
19920
  const files = [];
19609
19921
  const skipped = [];
19610
19922
  for (const refPath of await sourceRefsToSnapshot(canvasDir, opts.parsed)) {
19611
- const abs = path11.isAbsolute(refPath) ? refPath : path11.resolve(canvasDir, refPath);
19923
+ const abs = path12.isAbsolute(refPath) ? refPath : path12.resolve(canvasDir, refPath);
19612
19924
  let st;
19613
19925
  try {
19614
19926
  st = await stat2(abs);
19615
19927
  } catch {
19616
- skipped.push({ path: toPosix(path11.relative(canvasDir, abs)), reason: "missing" });
19928
+ skipped.push({ path: toPosix(path12.relative(canvasDir, abs)), reason: "missing" });
19617
19929
  continue;
19618
19930
  }
19619
19931
  const fileList = st.isDirectory() ? await listFilesRecursive(abs) : [abs];
19620
19932
  for (const file of fileList) {
19621
- const rel = toPosix(path11.relative(canvasDir, file));
19933
+ const rel = toPosix(path12.relative(canvasDir, file));
19622
19934
  const size = (await stat2(file)).size;
19623
19935
  if (size > MAX_SNAPSHOT_FILE_BYTES) {
19624
19936
  skipped.push({ path: rel, reason: `too large (${size} bytes)` });
19625
19937
  continue;
19626
19938
  }
19627
- const bytes = await readFile8(file);
19939
+ const bytes = await readFile9(file);
19628
19940
  const upload = await put(bytes, mimeForFile(file));
19629
19941
  files.push({ path: rel, sha256: upload.sha256, url: upload.url });
19630
19942
  }
@@ -19633,7 +19945,7 @@ async function uploadRunSnapshot(client, opts) {
19633
19945
  schema: SNAPSHOT_SCHEMA,
19634
19946
  creativeSlug: opts.creativeSlug,
19635
19947
  canvasSha: canvasUpload.sha256,
19636
- canvas: { path: path11.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
19948
+ canvas: { path: path12.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
19637
19949
  files,
19638
19950
  skipped: skipped.length > 0 ? skipped : void 0
19639
19951
  };
@@ -19660,7 +19972,7 @@ async function putContentAddressed(client, bytes, mime, signal) {
19660
19972
  }
19661
19973
  async function listFilesRecursive(dir) {
19662
19974
  const entries = await readdir4(dir, { recursive: true, withFileTypes: true });
19663
- return entries.filter((d) => d.isFile()).map((d) => path11.join(d.parentPath, d.name));
19975
+ return entries.filter((d) => d.isFile()).map((d) => path12.join(d.parentPath, d.name));
19664
19976
  }
19665
19977
  var SnapshotConflictError = class extends Error {
19666
19978
  conflicts;
@@ -19672,19 +19984,19 @@ var SnapshotConflictError = class extends Error {
19672
19984
  };
19673
19985
  async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
19674
19986
  const entries = [manifest.canvas, ...manifest.files];
19675
- const resolvedTarget = path11.resolve(targetDir);
19987
+ const resolvedTarget = path12.resolve(targetDir);
19676
19988
  const planned = [];
19677
19989
  const conflicts = [];
19678
19990
  const upToDate = [];
19679
19991
  for (const entry of entries) {
19680
- if (path11.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
19992
+ if (path12.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
19681
19993
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
19682
19994
  }
19683
- const target = path11.resolve(resolvedTarget, entry.path);
19684
- if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path11.sep)) {
19995
+ const target = path12.resolve(resolvedTarget, entry.path);
19996
+ if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path12.sep)) {
19685
19997
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
19686
19998
  }
19687
- const existing = await readFile8(target).catch(() => null);
19999
+ const existing = await readFile9(target).catch(() => null);
19688
20000
  if (existing) {
19689
20001
  if (sha256Hex(existing) === entry.sha256) {
19690
20002
  upToDate.push(entry.path);
@@ -19706,15 +20018,15 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
19706
20018
  if (sha256Hex(bytes) !== entry.sha256) {
19707
20019
  throw new Error(`snapshot download for ${entry.path} does not match its recorded sha256`);
19708
20020
  }
19709
- await mkdir4(path11.dirname(target), { recursive: true });
19710
- await writeFile5(target, bytes);
20021
+ await mkdir5(path12.dirname(target), { recursive: true });
20022
+ await writeFile6(target, bytes);
19711
20023
  restored.push(entry.path);
19712
20024
  }
19713
- return { canvasPath: path11.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
20025
+ return { canvasPath: path12.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
19714
20026
  }
19715
20027
 
19716
20028
  // src/commands/canvas/run.ts
19717
- var runCommand = defineCommand88({
20029
+ var runCommand = defineCommand90({
19718
20030
  meta: { name: "run", description: "Validate and execute a canvas JSON file." },
19719
20031
  args: {
19720
20032
  file: { type: "positional", required: true, description: "Path to canvas JSON" },
@@ -19790,8 +20102,8 @@ function resolveMaxCredits(...candidates) {
19790
20102
  return void 0;
19791
20103
  }
19792
20104
  async function executeCanvasRun(opts) {
19793
- const filePath = path12.resolve(opts.file);
19794
- const raw = await readFile9(filePath, "utf8");
20105
+ const filePath = path13.resolve(opts.file);
20106
+ const raw = await readFile10(filePath, "utf8");
19795
20107
  let parsed;
19796
20108
  try {
19797
20109
  parsed = JSON.parse(raw);
@@ -19803,7 +20115,7 @@ async function executeCanvasRun(opts) {
19803
20115
  }
19804
20116
  const attemptedSlug = creativeSlugFromCanvasPath(filePath);
19805
20117
  if (attemptedSlug) await clearCreativeDirty(attemptedSlug);
19806
- parsed = resolveRelativeCanvasPaths(parsed, path12.dirname(filePath));
20118
+ parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
19807
20119
  try {
19808
20120
  await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
19809
20121
  `));
@@ -19909,7 +20221,7 @@ async function executeCanvasRun(opts) {
19909
20221
  const canvasSha = sha256Hex(Buffer.from(raw));
19910
20222
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
19911
20223
  const client = opts.record === false ? null : buildBackendClient();
19912
- const outputsDir = opts.outputsDir ? path12.resolve(opts.outputsDir) : path12.resolve("canvas");
20224
+ const outputsDir = opts.outputsDir ? path13.resolve(opts.outputsDir) : path13.resolve("canvas");
19913
20225
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
19914
20226
  explicitRunId: opts.runId,
19915
20227
  fresh: opts.fresh === true,
@@ -19943,7 +20255,7 @@ async function executeCanvasRun(opts) {
19943
20255
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw, creativeSlug, parsed }) ?? void 0 : void 0;
19944
20256
  const recordMeta = {
19945
20257
  creativeSlug,
19946
- canvasPath: path12.relative(process.cwd(), filePath) || void 0,
20258
+ canvasPath: path13.relative(process.cwd(), filePath) || void 0,
19947
20259
  canvasSha,
19948
20260
  // The fingerprint the dashboard compares against the current source to flag
19949
20261
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -20084,7 +20396,7 @@ async function postInitialRunRecord(client, payload) {
20084
20396
 
20085
20397
  // src/commands/canvas/rerun.ts
20086
20398
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
20087
- var rerunCommand = defineCommand89({
20399
+ var rerunCommand = defineCommand91({
20088
20400
  meta: {
20089
20401
  name: "rerun",
20090
20402
  description: "Re-run a creative's latest recorded canvas. Restores its definition files from run history first, so it works in a fresh workspace that never had the source chat's files \u2014 an interrupted run resumes (in-flight jobs re-attach), a completed one re-renders from the cache."
@@ -20113,24 +20425,24 @@ var rerunCommand = defineCommand89({
20113
20425
  async run({ args }) {
20114
20426
  const slug = String(args.slug);
20115
20427
  if (!SLUG_PATTERN.test(slug)) {
20116
- fail("invalid_slug", `"${slug}" is not a valid creative slug`);
20428
+ fail2("invalid_slug", `"${slug}" is not a valid creative slug`);
20117
20429
  }
20118
20430
  const client = buildBackendClient();
20119
20431
  if (!client) {
20120
- fail("missing_credentials", "rerun needs backend credentials (BAKER_API_URL / BAKER_API_KEY)");
20432
+ fail2("missing_credentials", "rerun needs backend credentials (BAKER_API_URL / BAKER_API_KEY)");
20121
20433
  }
20122
20434
  const latest = await client.getLatestSnapshotRun(slug);
20123
20435
  if (!latest) {
20124
- fail(
20436
+ fail2(
20125
20437
  "no_snapshot_run",
20126
20438
  `no rerunnable history for "${slug}" \u2014 the creative was never run by a CLI that records canvas snapshots`
20127
20439
  );
20128
20440
  }
20129
20441
  const manifest = await fetchManifest(latest.canvasSnapshotUrl);
20130
20442
  if (latest.canvasSha && manifest.canvasSha !== latest.canvasSha) {
20131
- fail("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
20443
+ fail2("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
20132
20444
  }
20133
- const targetDir = path13.resolve("src", "creatives", slug);
20445
+ const targetDir = path14.resolve("src", "creatives", slug);
20134
20446
  let restoredCanvasPath;
20135
20447
  try {
20136
20448
  const restore = await restoreRunSnapshot(manifest, targetDir, { force: args["force-remote"] === true });
@@ -20148,7 +20460,7 @@ var rerunCommand = defineCommand89({
20148
20460
  }
20149
20461
  } catch (e) {
20150
20462
  if (e instanceof SnapshotConflictError) {
20151
- fail(
20463
+ fail2(
20152
20464
  "local_files_differ",
20153
20465
  `local files differ from the recorded snapshot: ${e.conflicts.join(", ")}. Re-run with --force-remote to overwrite them, or run the local canvas directly with \`baker canvas run\`.`
20154
20466
  );
@@ -20163,25 +20475,25 @@ var rerunCommand = defineCommand89({
20163
20475
  });
20164
20476
  }
20165
20477
  });
20166
- function fail(code, message) {
20478
+ function fail2(code, message) {
20167
20479
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}
20168
20480
  `);
20169
20481
  process.exit(2);
20170
20482
  }
20171
20483
  async function fetchManifest(url) {
20172
20484
  const res = await fetch(url);
20173
- if (!res.ok) fail("snapshot_unavailable", `snapshot manifest download failed: ${res.status} ${res.statusText}`);
20485
+ if (!res.ok) fail2("snapshot_unavailable", `snapshot manifest download failed: ${res.status} ${res.statusText}`);
20174
20486
  const manifest = await res.json();
20175
20487
  if (manifest?.schema !== SNAPSHOT_SCHEMA || typeof manifest.canvas?.path !== "string") {
20176
- fail("snapshot_invalid", "snapshot manifest has an unexpected shape");
20488
+ fail2("snapshot_invalid", "snapshot manifest has an unexpected shape");
20177
20489
  }
20178
20490
  return manifest;
20179
20491
  }
20180
20492
 
20181
20493
  // src/commands/canvas/scaffold-static-ad.ts
20182
- import { access, mkdir as mkdir5, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
20183
- import path17 from "path";
20184
- import { defineCommand as defineCommand91 } from "citty";
20494
+ import { access, mkdir as mkdir6, readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
20495
+ import path18 from "path";
20496
+ import { defineCommand as defineCommand93 } from "citty";
20185
20497
 
20186
20498
  // src/engine/scaffold/staticAd.ts
20187
20499
  import { z as z17 } from "zod";
@@ -20403,8 +20715,13 @@ function scaffoldStaticAd(input, elementsInput, opts) {
20403
20715
  inputs: { reference, target_blueprint: "$ref:prompt.asset" },
20404
20716
  params: {
20405
20717
  model: opts.genModel,
20406
- aspect_ratio: baseAspectRatio(blueprint, opts),
20407
- image_size: "2K",
20718
+ // The hero renders at the closest ratio its model actually accepts; the
20719
+ // placement fan-out below adapts it to the exact platform formats, which
20720
+ // is also how a 4:5 Meta feed ad gets made on a model that has no 4:5.
20721
+ aspect_ratio: nearestSupportedAspectRatio("image_generate", opts.genModel, baseAspectRatio(blueprint, opts)),
20722
+ ...supportsParam("image_generate", opts.genModel, "image_size") ? { image_size: "2K" } : {},
20723
+ // Per-model image defaults (gpt-image: quality=high).
20724
+ ...imageProfileFor(opts.genModel)?.paramDefaults ?? {},
20408
20725
  prompt
20409
20726
  }
20410
20727
  });
@@ -20453,7 +20770,7 @@ function staticAdReport(input, elementsInput, opts) {
20453
20770
  }
20454
20771
 
20455
20772
  // src/commands/canvas/creative-definition.ts
20456
- import path14 from "path";
20773
+ import path15 from "path";
20457
20774
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
20458
20775
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
20459
20776
  function titleFromSlug(slug) {
@@ -20501,16 +20818,16 @@ function buildCreativeDefinition(input) {
20501
20818
  }
20502
20819
 
20503
20820
  // src/commands/canvas/scaffold-static-ad-paths.ts
20504
- import path15 from "path";
20821
+ import path16 from "path";
20505
20822
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
20506
20823
  const file = rawFile.trim();
20507
20824
  const imageIsUrl = /^https?:\/\//i.test(file);
20508
- const imageSource = imageIsUrl ? file : path15.resolve(cwd, file);
20509
- const outPath = out ? path15.resolve(cwd, out) : slug ? path15.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path15.join(cwd, "static-ad.canvas.json") : path15.join(path15.dirname(imageSource), "static-ad.canvas.json");
20510
- const blueprintPath = path15.join(path15.dirname(outPath), "prompt.json");
20511
- const creativeDir = slug ? path15.dirname(outPath) : null;
20512
- const definitionPath = creativeDir ? path15.join(creativeDir, "_definition.md") : null;
20513
- const referencesDir = creativeDir ? path15.join(creativeDir, "references") : null;
20825
+ const imageSource = imageIsUrl ? file : path16.resolve(cwd, file);
20826
+ const outPath = out ? path16.resolve(cwd, out) : slug ? path16.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path16.join(cwd, "static-ad.canvas.json") : path16.join(path16.dirname(imageSource), "static-ad.canvas.json");
20827
+ const blueprintPath = path16.join(path16.dirname(outPath), "prompt.json");
20828
+ const creativeDir = slug ? path16.dirname(outPath) : null;
20829
+ const definitionPath = creativeDir ? path16.join(creativeDir, "_definition.md") : null;
20830
+ const referencesDir = creativeDir ? path16.join(creativeDir, "references") : null;
20514
20831
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
20515
20832
  }
20516
20833
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -20520,9 +20837,9 @@ function isValidScaffoldSlug(slug) {
20520
20837
  }
20521
20838
 
20522
20839
  // src/commands/canvas/sync-definition.ts
20523
- import { readdir as readdir5, readFile as readFile10, stat as stat3 } from "fs/promises";
20524
- import path16 from "path";
20525
- import { defineCommand as defineCommand90 } from "citty";
20840
+ import { readdir as readdir5, readFile as readFile11, stat as stat3 } from "fs/promises";
20841
+ import path17 from "path";
20842
+ import { defineCommand as defineCommand92 } from "citty";
20526
20843
 
20527
20844
  // src/commands/canvas/definition-graph.ts
20528
20845
  var MAX_NODES = 300;
@@ -20614,15 +20931,15 @@ async function syncCreativeDefinitionBestEffort(input) {
20614
20931
  }
20615
20932
  }
20616
20933
  async function resolveCanvasPath(inputPath) {
20617
- const resolved = path16.resolve(inputPath);
20934
+ const resolved = path17.resolve(inputPath);
20618
20935
  let dir = resolved;
20619
20936
  try {
20620
20937
  if ((await stat3(resolved)).isFile()) {
20621
20938
  if (resolved.endsWith(".canvas.json")) return resolved;
20622
- dir = path16.dirname(resolved);
20939
+ dir = path17.dirname(resolved);
20623
20940
  }
20624
20941
  } catch {
20625
- dir = resolved.endsWith(".canvas.json") ? path16.dirname(resolved) : resolved;
20942
+ dir = resolved.endsWith(".canvas.json") ? path17.dirname(resolved) : resolved;
20626
20943
  }
20627
20944
  let entries;
20628
20945
  try {
@@ -20633,9 +20950,9 @@ async function resolveCanvasPath(inputPath) {
20633
20950
  const canvases = entries.filter((name) => name.endsWith(".canvas.json"));
20634
20951
  const slug = creativeSlugFromCanvasPath(`${dir}/x/`);
20635
20952
  const chosen = (slug ? canvases.find((name) => name === `${slug}.canvas.json`) : void 0) ?? canvases[0];
20636
- return chosen ? path16.join(dir, chosen) : null;
20953
+ return chosen ? path17.join(dir, chosen) : null;
20637
20954
  }
20638
- var syncDefinitionCommand = defineCommand90({
20955
+ var syncDefinitionCommand = defineCommand92({
20639
20956
  meta: {
20640
20957
  name: "sync-definition",
20641
20958
  description: "Push a creative's current node-graph + input thumbnails to the dashboard without rendering. Runs automatically when you edit a creative's canvas.json or prompt.json."
@@ -20656,7 +20973,7 @@ var syncDefinitionCommand = defineCommand90({
20656
20973
  if (!slug) return;
20657
20974
  let canvas;
20658
20975
  try {
20659
- canvas = JSON.parse(await readFile10(canvasPath, "utf8"));
20976
+ canvas = JSON.parse(await readFile11(canvasPath, "utf8"));
20660
20977
  } catch {
20661
20978
  return;
20662
20979
  }
@@ -20681,7 +20998,7 @@ async function uploadSourceAsReference(source, isUrl, client) {
20681
20998
  if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
20682
20999
  bytes = Buffer.from(await res.arrayBuffer());
20683
21000
  } else {
20684
- bytes = await readFile11(source);
21001
+ bytes = await readFile12(source);
20685
21002
  }
20686
21003
  const safe = await toModelSafeImage(bytes);
20687
21004
  const sha256 = sha256Hex(safe.bytes);
@@ -20736,7 +21053,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
20736
21053
  For each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject, and its castable attributes \u2014 breed/species for an animal, apparent age band, apparent origin/ethnicity, and wardrobe/setting for a person \u2014 so it can be recast to fit OUR audience/market), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.`;
20737
21054
  async function loadAssetText(ref, label) {
20738
21055
  const r = ref;
20739
- if (typeof r?.path === "string") return readFile11(r.path, "utf8");
21056
+ if (typeof r?.path === "string") return readFile12(r.path, "utf8");
20740
21057
  if (typeof r?.url === "string") {
20741
21058
  const res = await fetch(r.url);
20742
21059
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -20760,7 +21077,7 @@ function parseLayout(raw) {
20760
21077
  }
20761
21078
  return null;
20762
21079
  }
20763
- function fail2(code, message) {
21080
+ function fail3(code, message) {
20764
21081
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}
20765
21082
  `);
20766
21083
  process.exit(2);
@@ -20771,7 +21088,7 @@ function resolveModels(args) {
20771
21088
  describeModel: pick("describe-model", "image_describe", "~google/gemini-pro-latest"),
20772
21089
  selectModel: pick("select-model", "text_generate", "~google/gemini-flash-latest"),
20773
21090
  layoutModel: pick("layout-model", "text_generate", "~google/gemini-flash-latest"),
20774
- genModel: pick("gen-model", "image_generate", "openai/gpt-5.4-image-2")
21091
+ genModel: pick("gen-model", "image_generate", "openai/gpt-image-2")
20775
21092
  };
20776
21093
  }
20777
21094
  var PLATFORM_PLACEMENTS = {
@@ -20851,8 +21168,8 @@ async function runVisionPasses(canvas) {
20851
21168
  outputsByNode = result.outputs_by_node;
20852
21169
  creditsSpent = result.stats?.total_credits;
20853
21170
  } catch (e) {
20854
- if (e instanceof ValidationError) return fail2("validation", JSON.stringify(e.issues));
20855
- return fail2("describe", e instanceof Error ? e.message : String(e));
21171
+ if (e instanceof ValidationError) return fail3("validation", JSON.stringify(e.issues));
21172
+ return fail3("describe", e instanceof Error ? e.message : String(e));
20856
21173
  }
20857
21174
  try {
20858
21175
  const blueprint = JSON.parse(await loadAssetText(outputsByNode.describe?.description, "describe output"));
@@ -20860,10 +21177,10 @@ async function runVisionPasses(canvas) {
20860
21177
  const layout = parseLayout(await loadAssetText(outputsByNode.layout?.text, "layout output"));
20861
21178
  return { blueprint, elements, layout, creditsSpent };
20862
21179
  } catch (e) {
20863
- return fail2("read_outputs", e instanceof Error ? e.message : String(e));
21180
+ return fail3("read_outputs", e instanceof Error ? e.message : String(e));
20864
21181
  }
20865
21182
  }
20866
- var scaffoldStaticAdCommand = defineCommand91({
21183
+ var scaffoldStaticAdCommand = defineCommand93({
20867
21184
  meta: {
20868
21185
  name: "scaffold-static-ad",
20869
21186
  description: "Turn a source/inspiration image into a runnable static-ad canvas. Runs billed passes \u2014 image_describe (the blueprint, baked to prompt.json as the editable 'prompt'), an AI selection of the image's MAIN identity elements, and a structured global-layout pass (the column/row grid with per-region bounds and text sizes) \u2014 then scaffolds a canvas that wires one [TODO] ingest slot per element (logo/product/subject/badge + brand font) into image_generate. By default it also fans the hero out to the platform's placement ratios (via image_aspect_adapt \u2014 the hero ratio is free, the rest are billed AI recompositions); pass --placements none for a single base ad, or a preset to pick the set. Edit prompt.json and drop the real assets, then `baker canvas run` it."
@@ -20912,7 +21229,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20912
21229
  process.cwd(),
20913
21230
  slug
20914
21231
  );
20915
- await mkdir5(path17.dirname(outPath), { recursive: true });
21232
+ await mkdir6(path18.dirname(outPath), { recursive: true });
20916
21233
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
20917
21234
  let durableSourceUrl;
20918
21235
  if (referencesDir) {
@@ -20921,7 +21238,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20921
21238
  try {
20922
21239
  durableSourceUrl = await uploadSourceAsReference(imageSource, imageIsUrl, client);
20923
21240
  } catch (e) {
20924
- return fail2("source_unavailable", e instanceof Error ? e.message : String(e));
21241
+ return fail3("source_unavailable", e instanceof Error ? e.message : String(e));
20925
21242
  }
20926
21243
  }
20927
21244
  }
@@ -20938,7 +21255,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20938
21255
  if (layout && annotated && typeof annotated === "object") {
20939
21256
  annotated.layout = layout;
20940
21257
  }
20941
- await writeFile6(blueprintPath, `${JSON.stringify(annotated, null, 2)}
21258
+ await writeFile7(blueprintPath, `${JSON.stringify(annotated, null, 2)}
20942
21259
  `, "utf8");
20943
21260
  let canvasImagePath = imageSource;
20944
21261
  let canvasImageIsUrl = imageIsUrl;
@@ -20955,7 +21272,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20955
21272
  args.placements ? String(args.placements) : void 0,
20956
21273
  definitionPlatform
20957
21274
  );
20958
- if (!placementsResult.ok) return fail2("invalid_placements", placementsResult.message);
21275
+ if (!placementsResult.ok) return fail3("invalid_placements", placementsResult.message);
20959
21276
  const placements = placementsResult.placements;
20960
21277
  const opts = {
20961
21278
  genModel,
@@ -20973,7 +21290,7 @@ var scaffoldStaticAdCommand = defineCommand91({
20973
21290
  canvas = scaffoldStaticAd(blueprint, elements, opts);
20974
21291
  report = staticAdReport(blueprint, elements, opts);
20975
21292
  } catch (e) {
20976
- return fail2("scaffold", e instanceof Error ? e.message : String(e));
21293
+ return fail3("scaffold", e instanceof Error ? e.message : String(e));
20977
21294
  }
20978
21295
  const validation = await validateCanvasDeep(canvas, defaultRegistry());
20979
21296
  if (!validation.ok) {
@@ -20983,10 +21300,10 @@ var scaffoldStaticAdCommand = defineCommand91({
20983
21300
  );
20984
21301
  process.exit(2);
20985
21302
  }
20986
- await writeFile6(outPath, `${JSON.stringify(canvas, null, 2)}
21303
+ await writeFile7(outPath, `${JSON.stringify(canvas, null, 2)}
20987
21304
  `, "utf8");
20988
21305
  if (definitionPath && !await fileExists(definitionPath)) {
20989
- await writeFile6(
21306
+ await writeFile7(
20990
21307
  definitionPath,
20991
21308
  buildCreativeDefinition({
20992
21309
  title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
@@ -21032,7 +21349,7 @@ var scaffoldStaticAdCommand = defineCommand91({
21032
21349
  run_estimated_credits: validation.estimatedCredits
21033
21350
  },
21034
21351
  checklist: {
21035
- edit_prompt: `Edit ${path17.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
21352
+ edit_prompt: `Edit ${path18.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
21036
21353
  assets_to_supply: report.elements,
21037
21354
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
21038
21355
  actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
@@ -21049,10 +21366,10 @@ var scaffoldStaticAdCommand = defineCommand91({
21049
21366
  });
21050
21367
 
21051
21368
  // src/commands/canvas/scaffold-video.ts
21052
- import { access as access2, cp, mkdir as mkdir6, readFile as readFile14, rm as rm6, writeFile as writeFile7 } from "fs/promises";
21369
+ import { access as access2, cp, mkdir as mkdir7, readFile as readFile15, rm as rm6, writeFile as writeFile8 } from "fs/promises";
21053
21370
  import { tmpdir as tmpdir2 } from "os";
21054
- import path20 from "path";
21055
- import { defineCommand as defineCommand92 } from "citty";
21371
+ import path21 from "path";
21372
+ import { defineCommand as defineCommand94 } from "citty";
21056
21373
 
21057
21374
  // src/engine/scaffold/lib/model-router.ts
21058
21375
  var SEEDANCE = "bytedance/seedance-2.0";
@@ -21090,7 +21407,7 @@ function routeVideoModel(input) {
21090
21407
 
21091
21408
  // src/engine/nodes/local/lib/sceneDetect.ts
21092
21409
  import { execFile as execFile2 } from "child_process";
21093
- import { mkdtemp, readdir as readdir6, readFile as readFile12, rm as rm5 } from "fs/promises";
21410
+ import { mkdtemp, readdir as readdir6, readFile as readFile13, rm as rm5 } from "fs/promises";
21094
21411
  import { tmpdir } from "os";
21095
21412
  import { join as join2 } from "path";
21096
21413
  import { promisify as promisify2 } from "util";
@@ -21166,7 +21483,7 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
21166
21483
  );
21167
21484
  const csvName = (await readdir6(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
21168
21485
  if (!csvName) return [];
21169
- return parsePySceneDetectCsvCuts(await readFile12(join2(outDir, csvName), "utf-8"));
21486
+ return parsePySceneDetectCsvCuts(await readFile13(join2(outDir, csvName), "utf-8"));
21170
21487
  } finally {
21171
21488
  await rm5(outDir, { recursive: true, force: true });
21172
21489
  }
@@ -21191,23 +21508,23 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
21191
21508
 
21192
21509
  // src/commands/canvas/composition-path.ts
21193
21510
  import { existsSync as existsSync3 } from "fs";
21194
- import path18 from "path";
21511
+ import path19 from "path";
21195
21512
  function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
21196
- const rel = path18.join("canvas", name);
21513
+ const rel = path19.join("canvas", name);
21197
21514
  let dir = startDir;
21198
21515
  for (let i = 0; i < maxDepth; i++) {
21199
- const candidate = path18.join(dir, rel);
21200
- if (exists(path18.join(candidate, "meta.json"))) return candidate;
21201
- const parent = path18.dirname(dir);
21516
+ const candidate = path19.join(dir, rel);
21517
+ if (exists(path19.join(candidate, "meta.json"))) return candidate;
21518
+ const parent = path19.dirname(dir);
21202
21519
  if (parent === dir) break;
21203
21520
  dir = parent;
21204
21521
  }
21205
- return path18.resolve(startDir, "../../../", rel);
21522
+ return path19.resolve(startDir, "../../../", rel);
21206
21523
  }
21207
21524
 
21208
21525
  // src/commands/canvas/gitignore.ts
21209
- import { appendFile, readFile as readFile13 } from "fs/promises";
21210
- import path19 from "path";
21526
+ import { appendFile, readFile as readFile14 } from "fs/promises";
21527
+ import path20 from "path";
21211
21528
  function missingGitignoreEntries(existing, entries) {
21212
21529
  const present2 = new Set(
21213
21530
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -21215,10 +21532,10 @@ function missingGitignoreEntries(existing, entries) {
21215
21532
  return entries.filter((e) => !present2.has(e.trim().replace(/\/+$/, "")));
21216
21533
  }
21217
21534
  async function ensureGitignore(dir, entries) {
21218
- const file = path19.join(dir, ".gitignore");
21535
+ const file = path20.join(dir, ".gitignore");
21219
21536
  let existing;
21220
21537
  try {
21221
- existing = await readFile13(file, "utf8");
21538
+ existing = await readFile14(file, "utf8");
21222
21539
  } catch {
21223
21540
  return;
21224
21541
  }
@@ -21257,7 +21574,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
21257
21574
  For each kept element return: { "type": one of person|animal|product|logo|badge|location, "label": a short UPPER_SNAKE_CASE name (e.g. HERO, CREATOR_SKEPTIC, INSURANCE_CARD, LOGO), "description": a concrete reusable description to source/shoot the real asset \u2014 for a person/animal give a NEUTRAL castable role (e.g. "hero pet-owner, woman in her 30s" or "a small beagle"), NOT the original individual's literal face/identity: we RECAST with a FRESH person/animal, so never tell the agent to reuse the original. "expression": a living subject's typical expression or null, "cast_id": the global.cast id if it maps to one else null, "same_as": the label of another element this is the SAME individual as (different wardrobe/persona) else null, "scenes": the 0-based indices of ONLY the scenes where the element is ACTUALLY VISIBLE ON SCREEN \u2014 judged from that scene's start_frame_prompt / end_frame_prompt subjects and its action_detail, NOT from who is merely speaking. A narrator heard over b-roll is NOT present in that b-roll scene; a dog-running cutaway does NOT contain the couch creator just because she talks across it. Do NOT pad the list \u2014 an element wrongly listed in a scene makes the reproduction render the wrong subject there (e.g. the creator appearing in a pure-dog b-roll). When in doubt, leave a scene OUT. Output ONLY the JSON object.`;
21258
21575
  async function loadAssetText2(ref, label) {
21259
21576
  const r = ref;
21260
- if (typeof r?.path === "string") return readFile14(r.path, "utf8");
21577
+ if (typeof r?.path === "string") return readFile15(r.path, "utf8");
21261
21578
  if (typeof r?.url === "string") {
21262
21579
  const res = await fetch(r.url);
21263
21580
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -21276,7 +21593,7 @@ async function loadTranscriptBestEffort(ref) {
21276
21593
  async function stageCaptions(outDir, transcript) {
21277
21594
  const text = transcript?.trim();
21278
21595
  if (!text || text === "[]") return {};
21279
- const compositionPath = path20.join(outDir, "tiktok-captions-composition");
21596
+ const compositionPath = path21.join(outDir, "tiktok-captions-composition");
21280
21597
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
21281
21598
  return { compositionPath };
21282
21599
  }
@@ -21294,12 +21611,12 @@ function patchCompositionHtml(html, dims) {
21294
21611
  return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
21295
21612
  }
21296
21613
  async function stampCompositionDims(compositionDir, dims) {
21297
- const metaPath = path20.join(compositionDir, "meta.json");
21298
- const rawMeta = await readFile14(metaPath, "utf8");
21299
- await writeFile7(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
21300
- const htmlPath = path20.join(compositionDir, "index.html");
21301
- const rawHtml = await readFile14(htmlPath, "utf8");
21302
- await writeFile7(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
21614
+ const metaPath = path21.join(compositionDir, "meta.json");
21615
+ const rawMeta = await readFile15(metaPath, "utf8");
21616
+ await writeFile8(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
21617
+ const htmlPath = path21.join(compositionDir, "index.html");
21618
+ const rawHtml = await readFile15(htmlPath, "utf8");
21619
+ await writeFile8(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
21303
21620
  }
21304
21621
  function parseElements2(raw) {
21305
21622
  const parsed = JSON.parse(raw);
@@ -21334,7 +21651,7 @@ async function detectShotCutsBestEffort(videoPath, threshold) {
21334
21651
  return void 0;
21335
21652
  }
21336
21653
  }
21337
- function fail3(code, message) {
21654
+ function fail4(code, message) {
21338
21655
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}
21339
21656
  `);
21340
21657
  process.exit(2);
@@ -21346,7 +21663,7 @@ var VIDEO_EXT_BY_MIME = {
21346
21663
  "video/x-matroska": ".mkv"
21347
21664
  };
21348
21665
  function referenceVideoExt(url, contentType) {
21349
- const fromPath = path20.extname(new URL(url).pathname).toLowerCase();
21666
+ const fromPath = path21.extname(new URL(url).pathname).toLowerCase();
21350
21667
  if (fromPath && fromPath.length <= 5) return fromPath;
21351
21668
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
21352
21669
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -21372,7 +21689,7 @@ function videoDefinitionDescription(blueprint) {
21372
21689
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
21373
21690
  }
21374
21691
  async function materializeReferenceVideo(fileArg2) {
21375
- if (!/^https?:\/\//i.test(fileArg2)) return path20.resolve(fileArg2);
21692
+ if (!/^https?:\/\//i.test(fileArg2)) return path21.resolve(fileArg2);
21376
21693
  let res;
21377
21694
  try {
21378
21695
  res = await fetch(fileArg2);
@@ -21382,11 +21699,11 @@ async function materializeReferenceVideo(fileArg2) {
21382
21699
  if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
21383
21700
  const bytes = Buffer.from(await res.arrayBuffer());
21384
21701
  if (bytes.length === 0) throw new Error("reference video download was empty");
21385
- const dest = path20.join(
21702
+ const dest = path21.join(
21386
21703
  tmpdir2(),
21387
21704
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
21388
21705
  );
21389
- await writeFile7(dest, bytes);
21706
+ await writeFile8(dest, bytes);
21390
21707
  return dest;
21391
21708
  }
21392
21709
  function resolveSeamDedup(raw) {
@@ -21413,7 +21730,7 @@ function resolveModels2(args) {
21413
21730
  // Default to the strongest image model (matches the static-ad scaffold); the
21414
21731
  // frame generators need the most faithful text/identity reproduction. Override
21415
21732
  // with --image-model for a cheaper/faster pass.
21416
- imageModel: pick("image-model", "image_generate", "openai/gpt-5.4-image-2")
21733
+ imageModel: pick("image-model", "image_generate", "openai/gpt-image-2")
21417
21734
  };
21418
21735
  }
21419
21736
  function hasPhotorealCast(elements) {
@@ -21494,9 +21811,9 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
21494
21811
  blueprint = JSON.parse(await loadAssetText2(r1.outputs_by_node.deconstruct?.analysis, "deconstruct output"));
21495
21812
  transcript = await loadTranscriptBestEffort(r1.outputs_by_node.deconstruct?.transcript);
21496
21813
  } catch (e) {
21497
- if (e instanceof ValidationError) return fail3("validation", JSON.stringify(e.issues));
21498
- if (e instanceof SyntaxError) return fail3("read_outputs", e.message);
21499
- return fail3("deconstruct", e instanceof Error ? e.message : String(e));
21814
+ if (e instanceof ValidationError) return fail4("validation", JSON.stringify(e.issues));
21815
+ if (e instanceof SyntaxError) return fail4("read_outputs", e.message);
21816
+ return fail4("deconstruct", e instanceof Error ? e.message : String(e));
21500
21817
  }
21501
21818
  const slimJson = JSON.stringify(slimBlueprintForSelection(blueprint));
21502
21819
  try {
@@ -21505,12 +21822,12 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
21505
21822
  const elements = parseElements2(await loadAssetText2(r2.outputs_by_node.select?.text, "selection output"));
21506
21823
  return { blueprint, elements, transcript, creditsSpent: sawCredits ? credits : void 0 };
21507
21824
  } catch (e) {
21508
- if (e instanceof ValidationError) return fail3("validation", JSON.stringify(e.issues));
21509
- if (e instanceof SyntaxError) return fail3("read_outputs", e.message);
21510
- return fail3("deconstruct", e instanceof Error ? e.message : String(e));
21825
+ if (e instanceof ValidationError) return fail4("validation", JSON.stringify(e.issues));
21826
+ if (e instanceof SyntaxError) return fail4("read_outputs", e.message);
21827
+ return fail4("deconstruct", e instanceof Error ? e.message : String(e));
21511
21828
  }
21512
21829
  }
21513
- var scaffoldVideoCommand = defineCommand92({
21830
+ var scaffoldVideoCommand = defineCommand94({
21514
21831
  meta: {
21515
21832
  name: "scaffold-video",
21516
21833
  description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand), drop the real source images, then `baker canvas run`."
@@ -21591,7 +21908,7 @@ var scaffoldVideoCommand = defineCommand92({
21591
21908
  }
21592
21909
  const isUrl = /^https?:\/\//i.test(fileArg2);
21593
21910
  if (isUrl && !slug && !args.out) {
21594
- return fail3(
21911
+ return fail4(
21595
21912
  "missing_output_target",
21596
21913
  "When the reference is a URL, pass --slug (writes src/creatives/<slug>/) or --out <path> so the scaffolded canvas has a home in the repo."
21597
21914
  );
@@ -21600,13 +21917,13 @@ var scaffoldVideoCommand = defineCommand92({
21600
21917
  try {
21601
21918
  videoPath = await materializeReferenceVideo(fileArg2);
21602
21919
  } catch (e) {
21603
- return fail3("download", e instanceof Error ? e.message : String(e));
21920
+ return fail4("download", e instanceof Error ? e.message : String(e));
21604
21921
  }
21605
- const base = path20.basename(videoPath, path20.extname(videoPath));
21606
- const outPath = args.out ? path20.resolve(String(args.out)) : slug ? path20.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path20.join(path20.dirname(videoPath), `${base}.video.canvas.json`);
21607
- const outDir = path20.dirname(outPath);
21608
- const blueprintPath = path20.join(outDir, "prompt.json");
21609
- const blueprintStylePath = path20.join(outDir, "prompt.style.json");
21922
+ const base = path21.basename(videoPath, path21.extname(videoPath));
21923
+ const outPath = args.out ? path21.resolve(String(args.out)) : slug ? path21.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path21.join(path21.dirname(videoPath), `${base}.video.canvas.json`);
21924
+ const outDir = path21.dirname(outPath);
21925
+ const blueprintPath = path21.join(outDir, "prompt.json");
21926
+ const blueprintStylePath = path21.join(outDir, "prompt.style.json");
21610
21927
  const frames = args.frames === "reuse" ? "reuse" : "generate";
21611
21928
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
21612
21929
  if (Number.isFinite(maxScenes)) {
@@ -21630,10 +21947,10 @@ var scaffoldVideoCommand = defineCommand92({
21630
21947
  process.stderr.write(`\u{1F3AC} Photoreal on-camera cast detected \u2192 generating clips on ${videoModel} (dodges Seedance's real-person filter).
21631
21948
  `);
21632
21949
  }
21633
- await mkdir6(outDir, { recursive: true });
21950
+ await mkdir7(outDir, { recursive: true });
21634
21951
  const annotated = annotateBlueprintWithElements(blueprint, elements);
21635
21952
  await writeSceneFiles(outDir, annotated);
21636
- await writeFile7(blueprintStylePath, renderStyleProjectionFromValue(annotated), "utf8");
21953
+ await writeFile8(blueprintStylePath, renderStyleProjectionFromValue(annotated), "utf8");
21637
21954
  let aspect;
21638
21955
  try {
21639
21956
  aspect = resolveAspect(
@@ -21642,7 +21959,7 @@ var scaffoldVideoCommand = defineCommand92({
21642
21959
  genAspectsFor(videoModel)
21643
21960
  );
21644
21961
  } catch (e) {
21645
- return fail3("aspect", e instanceof Error ? e.message : String(e));
21962
+ return fail4("aspect", e instanceof Error ? e.message : String(e));
21646
21963
  }
21647
21964
  const outDims = canvasDims(aspect.outAr);
21648
21965
  if (aspect.remapped) {
@@ -21651,29 +21968,29 @@ var scaffoldVideoCommand = defineCommand92({
21651
21968
  `
21652
21969
  );
21653
21970
  }
21654
- const compositionDest = path20.join(outDir, "video-overlay-composition");
21971
+ const compositionDest = path21.join(outDir, "video-overlay-composition");
21655
21972
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
21656
21973
  await stampCompositionDims(compositionDest, outDims);
21657
- const indexPath = path20.join(compositionDest, "index.html");
21974
+ const indexPath = path21.join(compositionDest, "index.html");
21658
21975
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
21659
- const indexHtml = await readFile14(indexPath, "utf8");
21976
+ const indexHtml = await readFile15(indexPath, "utf8");
21660
21977
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
21661
21978
  if (injected === indexHtml && overlayHtml.trim()) {
21662
- fail3(
21979
+ fail4(
21663
21980
  "composition_marker_missing",
21664
21981
  `video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
21665
21982
  );
21666
21983
  }
21667
- await writeFile7(indexPath, injected, "utf8");
21984
+ await writeFile8(indexPath, injected, "utf8");
21668
21985
  const captions = await stageCaptions(outDir, transcript);
21669
21986
  if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
21670
21987
  const opts = {
21671
21988
  imageModel,
21672
21989
  videoModel,
21673
- overlayCompositionPath: path20.relative(outDir, compositionDest),
21674
- captionsCompositionPath: captions.compositionPath ? path20.relative(outDir, captions.compositionPath) : void 0,
21675
- blueprintPath: path20.relative(outDir, blueprintPath),
21676
- blueprintStylePath: path20.relative(outDir, blueprintStylePath),
21990
+ overlayCompositionPath: path21.relative(outDir, compositionDest),
21991
+ captionsCompositionPath: captions.compositionPath ? path21.relative(outDir, captions.compositionPath) : void 0,
21992
+ blueprintPath: path21.relative(outDir, blueprintPath),
21993
+ blueprintStylePath: path21.relative(outDir, blueprintStylePath),
21677
21994
  frames,
21678
21995
  ambient: Boolean(args.ambient),
21679
21996
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -21686,7 +22003,7 @@ var scaffoldVideoCommand = defineCommand92({
21686
22003
  canvas = scaffoldVideoCanvas(blueprint, elements, opts);
21687
22004
  report = videoReport(blueprint, elements);
21688
22005
  } catch (e) {
21689
- return fail3("scaffold", e instanceof Error ? e.message : String(e));
22006
+ return fail4("scaffold", e instanceof Error ? e.message : String(e));
21690
22007
  }
21691
22008
  if (captions.compositionPath && !canvas.nodes.some((n) => n.id === "captions")) {
21692
22009
  await rm6(captions.compositionPath, { recursive: true, force: true });
@@ -21698,10 +22015,10 @@ var scaffoldVideoCommand = defineCommand92({
21698
22015
  todo.blocking_validation_issues = validation.issues;
21699
22016
  meta.todo = todo;
21700
22017
  }
21701
- await writeFile7(outPath, `${JSON.stringify(canvas, null, 2)}
22018
+ await writeFile8(outPath, `${JSON.stringify(canvas, null, 2)}
21702
22019
  `, "utf8");
21703
- await writeFile7(
21704
- path20.join(outDir, REBUILD_FILE),
22020
+ await writeFile8(
22021
+ path21.join(outDir, REBUILD_FILE),
21705
22022
  `${JSON.stringify({ elements, opts }, null, 2)}
21706
22023
  `,
21707
22024
  "utf8"
@@ -21726,9 +22043,9 @@ var scaffoldVideoCommand = defineCommand92({
21726
22043
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
21727
22044
  const sourceRef = videoSourceReference(blueprint, fileArg2);
21728
22045
  if (slug) {
21729
- const definitionPath = path20.join(outDir, "_definition.md");
22046
+ const definitionPath = path21.join(outDir, "_definition.md");
21730
22047
  if (!await fileExists2(definitionPath)) {
21731
- await writeFile7(
22048
+ await writeFile8(
21732
22049
  definitionPath,
21733
22050
  buildCreativeDefinition({
21734
22051
  title: titleFromSlug(slug),
@@ -21781,7 +22098,7 @@ var scaffoldVideoCommand = defineCommand92({
21781
22098
  graph: canvas.metadata?.video?.graph_stats
21782
22099
  },
21783
22100
  checklist: {
21784
- edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path20.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path20.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path20.basename(blueprintStylePath)}; both are regenerated.`,
22101
+ edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path21.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path21.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path21.basename(blueprintStylePath)}; both are regenerated.`,
21785
22102
  recurring_elements_to_supply: report.elements,
21786
22103
  voices_to_confirm: report.dialogue.map((d) => ({
21787
22104
  scene: d.scene,
@@ -21818,9 +22135,9 @@ var scaffoldVideoCommand = defineCommand92({
21818
22135
  });
21819
22136
 
21820
22137
  // src/commands/canvas/set-prompt.ts
21821
- import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
21822
- import path21 from "path";
21823
- import { defineCommand as defineCommand93 } from "citty";
22138
+ import { readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
22139
+ import path22 from "path";
22140
+ import { defineCommand as defineCommand95 } from "citty";
21824
22141
  function setNodePrompt(canvas, nodeId, text) {
21825
22142
  const nodes = canvas?.nodes;
21826
22143
  if (!Array.isArray(nodes)) throw new Error("canvas has no nodes array");
@@ -21835,7 +22152,7 @@ function setNodePrompt(canvas, nodeId, text) {
21835
22152
  newNodes[idx] = newNode;
21836
22153
  return { ...canvas, nodes: newNodes };
21837
22154
  }
21838
- var setPromptCommand = defineCommand93({
22155
+ var setPromptCommand = defineCommand95({
21839
22156
  meta: {
21840
22157
  name: "set-prompt",
21841
22158
  description: "Safely set a node's params.prompt (a frame description, motion prompt, etc.) without hand-editing the JSON. Prefer --text-file for multi-line/accented copy \u2014 it preserves UTF-8 exactly, unlike shell-quoted jq."
@@ -21847,17 +22164,17 @@ var setPromptCommand = defineCommand93({
21847
22164
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
21848
22165
  },
21849
22166
  async run({ args }) {
21850
- const filePath = path21.resolve(String(args.file));
22167
+ const filePath = path22.resolve(String(args.file));
21851
22168
  let canvas;
21852
22169
  try {
21853
- canvas = JSON.parse(await readFile15(filePath, "utf8"));
22170
+ canvas = JSON.parse(await readFile16(filePath, "utf8"));
21854
22171
  } catch (e) {
21855
22172
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
21856
22173
  `);
21857
22174
  process.exit(2);
21858
22175
  }
21859
22176
  let text;
21860
- if (args["text-file"]) text = await readFile15(path21.resolve(String(args["text-file"])), "utf8");
22177
+ if (args["text-file"]) text = await readFile16(path22.resolve(String(args["text-file"])), "utf8");
21861
22178
  else if (args.text !== void 0) text = String(args.text);
21862
22179
  else {
21863
22180
  process.stderr.write(
@@ -21878,14 +22195,14 @@ var setPromptCommand = defineCommand93({
21878
22195
  process.exit(2);
21879
22196
  return;
21880
22197
  }
21881
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path21.dirname(filePath)), defaultRegistry());
22198
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path22.dirname(filePath)), defaultRegistry());
21882
22199
  if (!validation.ok) {
21883
22200
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
21884
22201
  `);
21885
22202
  process.exit(2);
21886
22203
  return;
21887
22204
  }
21888
- await writeFile8(filePath, `${JSON.stringify(updated, null, 2)}
22205
+ await writeFile9(filePath, `${JSON.stringify(updated, null, 2)}
21889
22206
  `, "utf8");
21890
22207
  process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
21891
22208
  `);
@@ -21893,18 +22210,18 @@ var setPromptCommand = defineCommand93({
21893
22210
  });
21894
22211
 
21895
22212
  // src/commands/canvas/validate.ts
21896
- import { readFile as readFile16 } from "fs/promises";
21897
- import path22 from "path";
21898
- import { defineCommand as defineCommand94 } from "citty";
21899
- var validateCommand = defineCommand94({
22213
+ import { readFile as readFile17 } from "fs/promises";
22214
+ import path23 from "path";
22215
+ import { defineCommand as defineCommand96 } from "citty";
22216
+ var validateCommand = defineCommand96({
21900
22217
  meta: {
21901
22218
  name: "validate",
21902
22219
  description: "Validate a canvas JSON file (no execution). Includes a per-node cost preview and runs each node's deep validators (composition meta checks for hyperframe_render/_snapshot)."
21903
22220
  },
21904
22221
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
21905
22222
  async run({ args }) {
21906
- const filePath = path22.resolve(String(args.file));
21907
- const raw = await readFile16(filePath, "utf8");
22223
+ const filePath = path23.resolve(String(args.file));
22224
+ const raw = await readFile17(filePath, "utf8");
21908
22225
  let parsed;
21909
22226
  try {
21910
22227
  parsed = JSON.parse(raw);
@@ -21914,7 +22231,7 @@ var validateCommand = defineCommand94({
21914
22231
  `);
21915
22232
  process.exit(2);
21916
22233
  }
21917
- parsed = resolveRelativeCanvasPaths(parsed, path22.dirname(filePath));
22234
+ parsed = resolveRelativeCanvasPaths(parsed, path23.dirname(filePath));
21918
22235
  let styleProjection = "not_applicable";
21919
22236
  try {
21920
22237
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -21966,7 +22283,7 @@ var validateCommand = defineCommand94({
21966
22283
  });
21967
22284
 
21968
22285
  // src/commands/canvas/index.ts
21969
- var canvasCommand = defineCommand95({
22286
+ var canvasCommand = defineCommand97({
21970
22287
  meta: {
21971
22288
  name: "canvas",
21972
22289
  description: `Run Baker creative canvas JSON files locally. Local nodes execute in-process; remote nodes POST to the Convex backend gateway.
@@ -22000,7 +22317,7 @@ Full guide: __tooling__/docs/tools/baker/canvas.md`
22000
22317
  });
22001
22318
 
22002
22319
  // src/commands/chats/index.ts
22003
- import { defineCommand as defineCommand96 } from "citty";
22320
+ import { defineCommand as defineCommand98 } from "citty";
22004
22321
  var STATUS_GROUPS = ["active", "archived", "all"];
22005
22322
  var REPO_SURFACES = ["knowledge", "company", "brand", "landings", "flows", "creatives"];
22006
22323
  function parseBoundedInt(raw, name, min, max) {
@@ -22022,7 +22339,7 @@ registerSchema({
22022
22339
  full: { type: "boolean", description: "Include each Session's full change list", required: false, default: false }
22023
22340
  }
22024
22341
  });
22025
- var listCommand5 = defineCommand96({
22342
+ var listCommand5 = defineCommand98({
22026
22343
  meta: { name: "list", description: "List other Sessions on this account, newest first." },
22027
22344
  args: {
22028
22345
  status: { type: "string", description: "Filter: active|archived|all (default: all)", required: false },
@@ -22077,7 +22394,7 @@ registerSchema({
22077
22394
  }
22078
22395
  }
22079
22396
  });
22080
- var viewCommand = defineCommand96({
22397
+ var viewCommand = defineCommand98({
22081
22398
  meta: {
22082
22399
  name: "view",
22083
22400
  description: "Inspect one Session's full effects \u2014 git content + actions/tags/ads, kickoff, commits."
@@ -22121,7 +22438,7 @@ registerSchema({
22121
22438
  }
22122
22439
  }
22123
22440
  });
22124
- var transcriptCommand = defineCommand96({
22441
+ var transcriptCommand = defineCommand98({
22125
22442
  meta: { name: "transcript", description: "Read another Session's conversation." },
22126
22443
  args: {
22127
22444
  id: { type: "positional", description: "The Session id", required: true },
@@ -22173,7 +22490,7 @@ registerSchema({
22173
22490
  }
22174
22491
  }
22175
22492
  });
22176
- var diffCommand = defineCommand96({
22493
+ var diffCommand = defineCommand98({
22177
22494
  meta: { name: "diff", description: "See a published Session's real file-level changes." },
22178
22495
  args: {
22179
22496
  id: { type: "positional", description: "The Session id", required: true },
@@ -22217,7 +22534,7 @@ var diffCommand = defineCommand96({
22217
22534
  }
22218
22535
  }
22219
22536
  });
22220
- var chatsCommand = defineCommand96({
22537
+ var chatsCommand = defineCommand98({
22221
22538
  meta: {
22222
22539
  name: "chats",
22223
22540
  description: `Inspect other Sessions on this account (read-only): list them, view what they produced, read their conversation, and see their real file changes \u2014 so you can reuse past work for a new goal.
@@ -22232,13 +22549,13 @@ Full guide: __tooling__/docs/tools/baker/chats.md`
22232
22549
  });
22233
22550
 
22234
22551
  // src/commands/creatives/index.ts
22235
- import { defineCommand as defineCommand98 } from "citty";
22552
+ import { defineCommand as defineCommand100 } from "citty";
22236
22553
 
22237
22554
  // src/commands/creatives/publish.ts
22238
- import { defineCommand as defineCommand97 } from "citty";
22555
+ import { defineCommand as defineCommand99 } from "citty";
22239
22556
 
22240
22557
  // src/commands/images/api.ts
22241
- import { readFile as readFile17 } from "fs/promises";
22558
+ import { readFile as readFile18 } from "fs/promises";
22242
22559
  import { extname } from "path";
22243
22560
  var imageProcessingTimeoutMs = 18e4;
22244
22561
  var imageReadyPollIntervalMs = 2e3;
@@ -22252,7 +22569,7 @@ var mimeMap = {
22252
22569
  ".avif": "image/avif"
22253
22570
  };
22254
22571
  var defaultImageApiDeps = {
22255
- readFile: readFile17,
22572
+ readFile: readFile18,
22256
22573
  post: apiPost,
22257
22574
  get: apiGet,
22258
22575
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -22379,7 +22696,7 @@ async function publishCreative(args, deps = defaultImageApiDeps) {
22379
22696
  chatId: chatIdFromEnv()
22380
22697
  });
22381
22698
  }
22382
- var publishCommand = defineCommand97({
22699
+ var publishCommand = defineCommand99({
22383
22700
  meta: {
22384
22701
  name: "publish",
22385
22702
  description: "Publish a final static creative image to Baker Creatives and print the creative reference JSON."
@@ -22437,7 +22754,7 @@ var publishCommand = defineCommand97({
22437
22754
  });
22438
22755
 
22439
22756
  // src/commands/creatives/index.ts
22440
- var creativesCommand3 = defineCommand98({
22757
+ var creativesCommand3 = defineCommand100({
22441
22758
  meta: {
22442
22759
  name: "creatives",
22443
22760
  description: `Publish static ad creatives as first-class Baker outputs.
@@ -22454,7 +22771,7 @@ Full guide: __tooling__/docs/tools/baker/creatives.md`
22454
22771
  });
22455
22772
 
22456
22773
  // src/commands/flows/index.ts
22457
- import { defineCommand as defineCommand99 } from "citty";
22774
+ import { defineCommand as defineCommand101 } from "citty";
22458
22775
 
22459
22776
  // src/commands/flows/shared.ts
22460
22777
  import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
@@ -22487,12 +22804,12 @@ function listFlowSlugs() {
22487
22804
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
22488
22805
  }
22489
22806
  function readFlowTree(slug) {
22490
- const path27 = join3(flowsDir(), slug, "_data.json");
22491
- if (!existsSync4(path27)) {
22807
+ const path28 = join3(flowsDir(), slug, "_data.json");
22808
+ if (!existsSync4(path28)) {
22492
22809
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
22493
22810
  }
22494
22811
  try {
22495
- return JSON.parse(readFileSync9(path27, "utf-8"));
22812
+ return JSON.parse(readFileSync9(path28, "utf-8"));
22496
22813
  } catch (error) {
22497
22814
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
22498
22815
  }
@@ -22609,7 +22926,7 @@ function displayName(slug) {
22609
22926
  const name = tree.displayName;
22610
22927
  return typeof name === "string" && name.trim() ? name.trim() : slug;
22611
22928
  }
22612
- var listCommand6 = defineCommand99({
22929
+ var listCommand6 = defineCommand101({
22613
22930
  meta: {
22614
22931
  name: "list",
22615
22932
  description: "List Forms in this workspace with the count of confidential fields still needing setup. Example: baker flows list"
@@ -22623,7 +22940,7 @@ var listCommand6 = defineCommand99({
22623
22940
  writeJson({ ok: true, data: { flows } });
22624
22941
  }
22625
22942
  });
22626
- var showCommand2 = defineCommand99({
22943
+ var showCommand2 = defineCommand101({
22627
22944
  meta: {
22628
22945
  name: "show",
22629
22946
  description: "Show a Form's confidential fields and their configuration status (never secret values). Example: baker flows show contact"
@@ -22644,7 +22961,7 @@ var showCommand2 = defineCommand99({
22644
22961
  writeJson({ ok: true, data: response });
22645
22962
  }
22646
22963
  });
22647
- var flowsCommand = defineCommand99({
22964
+ var flowsCommand = defineCommand101({
22648
22965
  meta: {
22649
22966
  name: "flows",
22650
22967
  description: `Read this workspace's Forms (flows) and the configuration status of their confidential fields \u2014 connection secrets, OAuth connections, and third-party field definitions (HubSpot, Calendly, HighLevel, SavvyCal).
@@ -22672,10 +22989,10 @@ Full guide: __tooling__/docs/tools/baker/flows.md`
22672
22989
  });
22673
22990
 
22674
22991
  // src/commands/ga4/index.ts
22675
- import { defineCommand as defineCommand103 } from "citty";
22992
+ import { defineCommand as defineCommand105 } from "citty";
22676
22993
 
22677
22994
  // src/commands/ga4/audit.ts
22678
- import { defineCommand as defineCommand100 } from "citty";
22995
+ import { defineCommand as defineCommand102 } from "citty";
22679
22996
 
22680
22997
  // src/commands/ga4/resolve.ts
22681
22998
  async function fetchProperties(useCache = true) {
@@ -22738,7 +23055,7 @@ registerSchema({
22738
23055
  "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false }
22739
23056
  }
22740
23057
  });
22741
- var auditCommand2 = defineCommand100({
23058
+ var auditCommand2 = defineCommand102({
22742
23059
  meta: {
22743
23060
  name: "audit",
22744
23061
  description: `Run all GA4 admin health checks. Returns property config with playbook warnings.
@@ -22790,7 +23107,7 @@ Examples:
22790
23107
  });
22791
23108
 
22792
23109
  // src/commands/ga4/properties.ts
22793
- import { defineCommand as defineCommand101 } from "citty";
23110
+ import { defineCommand as defineCommand103 } from "citty";
22794
23111
  registerSchema({
22795
23112
  command: "ga4.properties",
22796
23113
  description: "List all accessible GA4 properties. Returns property IDs needed for query and audit commands. Run this first to find property IDs.",
@@ -22798,7 +23115,7 @@ registerSchema({
22798
23115
  "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false }
22799
23116
  }
22800
23117
  });
22801
- var propertiesCommand = defineCommand101({
23118
+ var propertiesCommand = defineCommand103({
22802
23119
  meta: {
22803
23120
  name: "properties",
22804
23121
  description: `List accessible GA4 properties.
@@ -22848,7 +23165,7 @@ Examples:
22848
23165
  // src/commands/ga4/query.ts
22849
23166
  import { appendFileSync as appendFileSync2, existsSync as existsSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
22850
23167
  import { resolve as resolve2 } from "path";
22851
- import { defineCommand as defineCommand102 } from "citty";
23168
+ import { defineCommand as defineCommand104 } from "citty";
22852
23169
 
22853
23170
  // src/commands/ga4/presets.ts
22854
23171
  var GA4_PRESETS = [
@@ -22980,7 +23297,7 @@ function handleError(err) {
22980
23297
  });
22981
23298
  process.exit(1);
22982
23299
  }
22983
- var queryCommand2 = defineCommand102({
23300
+ var queryCommand2 = defineCommand104({
22984
23301
  meta: {
22985
23302
  name: "query",
22986
23303
  description: `Run GA4 Data API reports. Preset-first with free-form escape hatch.
@@ -23051,7 +23368,7 @@ Free-form (escape hatch):
23051
23368
  });
23052
23369
 
23053
23370
  // src/commands/ga4/index.ts
23054
- var ga4Command = defineCommand103({
23371
+ var ga4Command = defineCommand105({
23055
23372
  meta: {
23056
23373
  name: "ga4",
23057
23374
  description: `Google Analytics 4 commands. Audit property config, run playbook-aligned reports.
@@ -23075,12 +23392,12 @@ Full guide: __tooling__/docs/tools/baker/ga4.md`
23075
23392
  });
23076
23393
 
23077
23394
  // src/commands/gsc/index.ts
23078
- import { defineCommand as defineCommand107 } from "citty";
23395
+ import { defineCommand as defineCommand109 } from "citty";
23079
23396
 
23080
23397
  // src/commands/gsc/query.ts
23081
23398
  import { appendFileSync as appendFileSync3, existsSync as existsSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
23082
23399
  import { resolve as resolve3 } from "path";
23083
- import { defineCommand as defineCommand104 } from "citty";
23400
+ import { defineCommand as defineCommand106 } from "citty";
23084
23401
 
23085
23402
  // src/commands/gsc/presets.ts
23086
23403
  var GSC_PRESETS = [
@@ -23268,7 +23585,7 @@ function handleError2(err) {
23268
23585
  });
23269
23586
  process.exit(1);
23270
23587
  }
23271
- var queryCommand3 = defineCommand104({
23588
+ var queryCommand3 = defineCommand106({
23272
23589
  meta: {
23273
23590
  name: "query",
23274
23591
  description: `Run GSC Search Analytics queries. Preset-first with free-form escape hatch.
@@ -23346,7 +23663,7 @@ Free-form (escape hatch):
23346
23663
  });
23347
23664
 
23348
23665
  // src/commands/gsc/sitemaps.ts
23349
- import { defineCommand as defineCommand105 } from "citty";
23666
+ import { defineCommand as defineCommand107 } from "citty";
23350
23667
  registerSchema({
23351
23668
  command: "gsc.sitemaps",
23352
23669
  description: "List sitemaps for a Search Console site. Check sitemap health and errors.",
@@ -23355,7 +23672,7 @@ registerSchema({
23355
23672
  "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false }
23356
23673
  }
23357
23674
  });
23358
- var sitemapsCommand = defineCommand105({
23675
+ var sitemapsCommand = defineCommand107({
23359
23676
  meta: {
23360
23677
  name: "sitemaps",
23361
23678
  description: `List sitemaps for a site. Check health and errors.
@@ -23405,7 +23722,7 @@ Examples:
23405
23722
  });
23406
23723
 
23407
23724
  // src/commands/gsc/sites.ts
23408
- import { defineCommand as defineCommand106 } from "citty";
23725
+ import { defineCommand as defineCommand108 } from "citty";
23409
23726
  registerSchema({
23410
23727
  command: "gsc.sites",
23411
23728
  description: "List all verified Google Search Console sites. Returns site URLs needed for query and sitemaps commands.",
@@ -23413,7 +23730,7 @@ registerSchema({
23413
23730
  "no-cache": { type: "boolean", description: "Skip cache, hit API directly", required: false }
23414
23731
  }
23415
23732
  });
23416
- var sitesCommand = defineCommand106({
23733
+ var sitesCommand = defineCommand108({
23417
23734
  meta: {
23418
23735
  name: "sites",
23419
23736
  description: `List verified Search Console sites.
@@ -23461,7 +23778,7 @@ Examples:
23461
23778
  });
23462
23779
 
23463
23780
  // src/commands/gsc/index.ts
23464
- var gscCommand = defineCommand107({
23781
+ var gscCommand = defineCommand109({
23465
23782
  meta: {
23466
23783
  name: "gsc",
23467
23784
  description: `Google Search Console commands. PPC-SEO arbitrage, brand halo analysis, negative keyword discovery.
@@ -23485,7 +23802,7 @@ Full guide: __tooling__/docs/tools/baker/gsc.md`
23485
23802
  });
23486
23803
 
23487
23804
  // src/commands/history/index.ts
23488
- import { defineCommand as defineCommand108 } from "citty";
23805
+ import { defineCommand as defineCommand110 } from "citty";
23489
23806
  registerSchema({
23490
23807
  command: "history.list",
23491
23808
  description: "Start here: unified account history (audit log) \u2014 everything that changed on this account, newest first: publishes, chat lifecycle, backlog actions, team changes, setup links, tags, schedules, ad-platform writes, followed advertisers, media, creatives, reports, domains, and integrations. Use it to see what happened recently before planning work. Compact by default; add --full for raw metadata per entry.",
@@ -23540,7 +23857,7 @@ function parseBoundedInt2(raw, name, min, max) {
23540
23857
  }
23541
23858
  return value;
23542
23859
  }
23543
- var listCommand7 = defineCommand108({
23860
+ var listCommand7 = defineCommand110({
23544
23861
  meta: {
23545
23862
  name: "list",
23546
23863
  description: "List recent account changes (unified audit log), newest first."
@@ -23586,7 +23903,7 @@ var listCommand7 = defineCommand108({
23586
23903
  }
23587
23904
  }
23588
23905
  });
23589
- var historyCommand = defineCommand108({
23906
+ var historyCommand = defineCommand110({
23590
23907
  meta: {
23591
23908
  name: "history",
23592
23909
  description: `Unified account history (audit log): what changed, who did it, and when.
@@ -23596,10 +23913,10 @@ Full guide: __tooling__/docs/tools/baker/history.md`
23596
23913
  });
23597
23914
 
23598
23915
  // src/commands/images/index.ts
23599
- import { defineCommand as defineCommand132 } from "citty";
23916
+ import { defineCommand as defineCommand134 } from "citty";
23600
23917
 
23601
23918
  // src/commands/images/crop.ts
23602
- import { defineCommand as defineCommand109 } from "citty";
23919
+ import { defineCommand as defineCommand111 } from "citty";
23603
23920
 
23604
23921
  // src/lib/image/crop-sprite.ts
23605
23922
  import sharp from "sharp";
@@ -23614,7 +23931,7 @@ function cropSprite(input, region) {
23614
23931
 
23615
23932
  // src/lib/image/io.ts
23616
23933
  import { randomBytes } from "crypto";
23617
- import { glob as fsGlob, readFile as readFile18, rename, stat as stat4, writeFile as writeFile9 } from "fs/promises";
23934
+ import { glob as fsGlob, readFile as readFile19, rename, stat as stat4, writeFile as writeFile10 } from "fs/promises";
23618
23935
  import { dirname as dirname2, extname as extname2, join as join4, resolve as resolve4 } from "path";
23619
23936
  var REMOTE_RE = /^https?:\/\//i;
23620
23937
  var GLOB_RE = /[*?[\]{}]/;
@@ -23650,11 +23967,11 @@ async function readImageBuffer(pathOrUrl) {
23650
23967
  }
23651
23968
  return Buffer.from(await response.arrayBuffer());
23652
23969
  }
23653
- return readFile18(pathOrUrl);
23970
+ return readFile19(pathOrUrl);
23654
23971
  }
23655
- async function isDirectory(path27) {
23972
+ async function isDirectory(path28) {
23656
23973
  try {
23657
- const s = await stat4(path27);
23974
+ const s = await stat4(path28);
23658
23975
  return s.isDirectory();
23659
23976
  } catch {
23660
23977
  return false;
@@ -23673,7 +23990,7 @@ async function atomicWrite(targetPath, data) {
23673
23990
  const absolute = resolve4(targetPath);
23674
23991
  const dir = dirname2(absolute);
23675
23992
  const tmp = join4(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
23676
- await writeFile9(tmp, data);
23993
+ await writeFile10(tmp, data);
23677
23994
  await rename(tmp, absolute);
23678
23995
  }
23679
23996
 
@@ -23724,7 +24041,7 @@ function emitError2(err) {
23724
24041
  }
23725
24042
  process.exit(1);
23726
24043
  }
23727
- var cropCommand = defineCommand109({
24044
+ var cropCommand = defineCommand111({
23728
24045
  meta: {
23729
24046
  name: "crop",
23730
24047
  description: "Crop a rectangular region from an image.\n\nExample: baker images crop sprite.png --x 0 --y 0 --width 64 --height 64 --output icon.png"
@@ -23760,7 +24077,7 @@ var cropCommand = defineCommand109({
23760
24077
  });
23761
24078
 
23762
24079
  // src/commands/images/delete.ts
23763
- import { defineCommand as defineCommand110 } from "citty";
24080
+ import { defineCommand as defineCommand112 } from "citty";
23764
24081
  registerSchema({
23765
24082
  command: "images.delete",
23766
24083
  description: "Delete an image by ID",
@@ -23774,7 +24091,7 @@ registerSchema({
23774
24091
  }
23775
24092
  }
23776
24093
  });
23777
- var deleteCommand = defineCommand110({
24094
+ var deleteCommand = defineCommand112({
23778
24095
  meta: {
23779
24096
  name: "delete",
23780
24097
  description: "Delete an image by ID. Use --dry-run to preview. Example: baker images delete j571abc123 --dry-run"
@@ -23815,7 +24132,7 @@ var deleteCommand = defineCommand110({
23815
24132
  });
23816
24133
 
23817
24134
  // src/commands/images/dimensions.ts
23818
- import { defineCommand as defineCommand111 } from "citty";
24135
+ import { defineCommand as defineCommand113 } from "citty";
23819
24136
 
23820
24137
  // src/lib/image/dimensions.ts
23821
24138
  import { imageSize } from "image-size";
@@ -23838,7 +24155,7 @@ registerSchema({
23838
24155
  target: { type: "string", description: "Local file path or remote http(s) URL", required: true }
23839
24156
  }
23840
24157
  });
23841
- var dimensionsCommand = defineCommand111({
24158
+ var dimensionsCommand = defineCommand113({
23842
24159
  meta: {
23843
24160
  name: "dimensions",
23844
24161
  description: "Read image dimensions without decoding the full file.\n\nExample: baker images dimensions ./logo.png\nExample: baker images dimensions https://acme.com/hero.png"
@@ -23882,7 +24199,7 @@ var dimensionsCommand = defineCommand111({
23882
24199
  });
23883
24200
 
23884
24201
  // src/commands/images/extract.ts
23885
- import { defineCommand as defineCommand112 } from "citty";
24202
+ import { defineCommand as defineCommand114 } from "citty";
23886
24203
  registerSchema({
23887
24204
  command: "images.extract",
23888
24205
  description: "Extract images from a URL via Firecrawl (formats: images).",
@@ -23898,7 +24215,7 @@ registerSchema({
23898
24215
  }
23899
24216
  }
23900
24217
  });
23901
- var extractCommand = defineCommand112({
24218
+ var extractCommand = defineCommand114({
23902
24219
  meta: {
23903
24220
  name: "extract",
23904
24221
  description: "Pull every image from a single URL via Firecrawl. ~$0.001/scrape. Cap auto-ingest at 20.\n\nExample: baker images extract https://stripe.com --auto-ingest 5"
@@ -23936,7 +24253,7 @@ var extractCommand = defineCommand112({
23936
24253
  });
23937
24254
 
23938
24255
  // src/commands/images/find.ts
23939
- import { defineCommand as defineCommand113 } from "citty";
24256
+ import { defineCommand as defineCommand115 } from "citty";
23940
24257
  registerSchema({
23941
24258
  command: "images.find",
23942
24259
  description: "Fanout image search: library first, then opted-in external providers.",
@@ -23968,7 +24285,7 @@ registerSchema({
23968
24285
  }
23969
24286
  }
23970
24287
  });
23971
- var findCommand = defineCommand113({
24288
+ var findCommand = defineCommand115({
23972
24289
  meta: {
23973
24290
  name: "find",
23974
24291
  description: "Library-first fanout image search. Opt in to providers with --sources. `--fallback` short-circuits to externals only when library is thin. With --auto-ingest, ingested external hits return Baker-owned URLs.\n\nExample: baker images find 'office' --sources library,magnific --limit 20"
@@ -24016,8 +24333,8 @@ var findCommand = defineCommand113({
24016
24333
  });
24017
24334
 
24018
24335
  // src/commands/images/generate.ts
24019
- import { readFile as readFile19 } from "fs/promises";
24020
- import { defineCommand as defineCommand114 } from "citty";
24336
+ import { readFile as readFile20 } from "fs/promises";
24337
+ import { defineCommand as defineCommand116 } from "citty";
24021
24338
  import sharp2 from "sharp";
24022
24339
  var GENERATE_TIMEOUT_MS = 18e4;
24023
24340
  var REFERENCE_MAX_EDGE = 1536;
@@ -24106,7 +24423,7 @@ async function resolveReferences(spec) {
24106
24423
  }
24107
24424
  let raw;
24108
24425
  try {
24109
- raw = await readFile19(entry);
24426
+ raw = await readFile20(entry);
24110
24427
  } catch {
24111
24428
  throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
24112
24429
  }
@@ -24120,10 +24437,10 @@ async function resolveReferences(spec) {
24120
24437
  }
24121
24438
  return out;
24122
24439
  }
24123
- var generateCommand = defineCommand114({
24440
+ var generateCommand = defineCommand116({
24124
24441
  meta: {
24125
24442
  name: "generate",
24126
- description: "Generate an image with AI and store it in the library (cost-tracked per request via OpenRouter usage). Models mirror the canvas: google/gemini-3.1-flash-image-preview (Nano Banana flash \u2014 default, fast, extreme aspect ratios) & google/gemini-3.5-flash (fast), google/gemini-3-pro-image-preview (Nano Banana Pro \u2014 highest fidelity), openai/gpt-5.4-image-2 (photoreal, cleanest in-image text, best for ad/landing reproduction), 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 openai/gpt-5.4-image-2 --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]]'"
24443
+ description: "Generate an image with AI and store it in the library (cost-tracked per request via OpenRouter usage). Models mirror the canvas: google/gemini-3.1-flash-image-preview (Nano Banana flash \u2014 default, fast, extreme aspect ratios), google/gemini-3-pro-image-preview (Nano Banana Pro \u2014 highest fidelity), openai/gpt-image-2 (photoreal, cleanest in-image text, best for ad/landing reproduction \u2014 no --image-size, and no 4:5 / 5:4), 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 openai/gpt-image-2 --aspect-ratio 3:2\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]]'"
24127
24444
  },
24128
24445
  args: {
24129
24446
  prompt: { type: "positional", description: "What to generate", required: false },
@@ -24172,7 +24489,7 @@ var generateCommand = defineCommand114({
24172
24489
  });
24173
24490
 
24174
24491
  // src/commands/images/get.ts
24175
- import { defineCommand as defineCommand115 } from "citty";
24492
+ import { defineCommand as defineCommand117 } from "citty";
24176
24493
  registerSchema({
24177
24494
  command: "images.get",
24178
24495
  description: "Get a single image by ID",
@@ -24180,7 +24497,7 @@ registerSchema({
24180
24497
  id: { type: "string", description: "Image ID", required: true }
24181
24498
  }
24182
24499
  });
24183
- var getCommand2 = defineCommand115({
24500
+ var getCommand2 = defineCommand117({
24184
24501
  meta: { name: "get", description: "Get a single image by ID. Example: baker images get j571abc123" },
24185
24502
  args: {
24186
24503
  id: { type: "positional", description: "Image ID", required: false },
@@ -24216,7 +24533,7 @@ var getCommand2 = defineCommand115({
24216
24533
  });
24217
24534
 
24218
24535
  // src/commands/images/gif.ts
24219
- import { defineCommand as defineCommand116 } from "citty";
24536
+ import { defineCommand as defineCommand118 } from "citty";
24220
24537
  registerSchema({
24221
24538
  command: "images.gif",
24222
24539
  description: "Search Giphy for GIFs / reaction memes (paid social creative).",
@@ -24248,7 +24565,7 @@ registerSchema({
24248
24565
  }
24249
24566
  }
24250
24567
  });
24251
- var gifCommand = defineCommand116({
24568
+ var gifCommand = defineCommand118({
24252
24569
  meta: {
24253
24570
  name: "gif",
24254
24571
  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"
@@ -24295,7 +24612,35 @@ var gifCommand = defineCommand116({
24295
24612
  });
24296
24613
 
24297
24614
  // src/commands/images/google.ts
24298
- import { defineCommand as defineCommand117 } from "citty";
24615
+ import { defineCommand as defineCommand119 } from "citty";
24616
+
24617
+ // src/commands/images/searchHints.ts
24618
+ var FALLBACK = {
24619
+ stock: "Still empty \u2192 `baker images find <query> --sources library,pinterest,google` (one call across the providers stock does not cover \u2014 `--sources` is required, `find` alone searches the library only) or `baker images generate` to make the asset. Do not re-run this search with reshuffled flags.",
24620
+ google: "Still empty \u2192 `baker images generate` to make the asset. Google is the last-resort provider; there is nothing below it to retry."
24621
+ };
24622
+ function emptyResultHints({ provider, hitCount, activeFilters }) {
24623
+ if (hitCount > 0) {
24624
+ return [];
24625
+ }
24626
+ const hints = [];
24627
+ if (activeFilters.length > 0) {
24628
+ hints.push(
24629
+ `No hits. Drop the filters before touching the query \u2014 ${activeFilters.join(", ")} narrowed this search and filter combinations are the usual cause of an empty result.`
24630
+ );
24631
+ }
24632
+ hints.push(FALLBACK[provider]);
24633
+ return hints;
24634
+ }
24635
+ function activeFilterFlags(args, candidates) {
24636
+ return candidates.filter((flag) => args[flag] !== void 0 && args[flag] !== "").map((flag) => `--${flag}`);
24637
+ }
24638
+
24639
+ // src/commands/images/google.ts
24640
+ var GOOGLE_ERROR_FIX = {
24641
+ action: "use_different_resource",
24642
+ explanation: "Generate the asset instead of retrying Google. Google is the last-resort image provider. Run `baker images generate` to make the asset. Never sleep-and-retry this command \u2014 the CLI already backs off on rate limits. If no image can be sourced, continue the rest of the task with a placeholder rather than aborting it."
24643
+ };
24299
24644
  registerSchema({
24300
24645
  command: "images.google",
24301
24646
  description: "Google Images search via the official Custom Search JSON API. Unverified source \u2014 inspect before placing.",
@@ -24331,7 +24676,7 @@ registerSchema({
24331
24676
  }
24332
24677
  }
24333
24678
  });
24334
- var googleCommand2 = defineCommand117({
24679
+ var googleCommand2 = defineCommand119({
24335
24680
  meta: {
24336
24681
  name: "google",
24337
24682
  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"
@@ -24366,10 +24711,18 @@ var googleCommand2 = defineCommand117({
24366
24711
  if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
24367
24712
  if (args.context) body.descriptionContext = args.context;
24368
24713
  const data = await apiPost("/api/images/google", body);
24369
- writeJson({ ok: true, data });
24714
+ const hints = emptyResultHints({
24715
+ provider: "google",
24716
+ hitCount: data.hits.length,
24717
+ activeFilters: activeFilterFlags(args, ["type", "size", "color", "safe"])
24718
+ });
24719
+ writeJson({ ok: true, data, ...hints.length ? { hints } : {} });
24370
24720
  } catch (err) {
24371
24721
  if (err instanceof ApiError) {
24372
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
24722
+ writeJson({
24723
+ ok: false,
24724
+ error: { code: err.code, message: err.message, fix: GOOGLE_ERROR_FIX }
24725
+ });
24373
24726
  process.exit(1);
24374
24727
  }
24375
24728
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -24379,7 +24732,7 @@ var googleCommand2 = defineCommand117({
24379
24732
  });
24380
24733
 
24381
24734
  // src/commands/images/icon.ts
24382
- import { defineCommand as defineCommand118 } from "citty";
24735
+ import { defineCommand as defineCommand120 } from "citty";
24383
24736
  registerSchema({
24384
24737
  command: "images.icon",
24385
24738
  description: "Icon lookup via Iconify (200+ icon sets, free CDN).",
@@ -24405,7 +24758,7 @@ registerSchema({
24405
24758
  }
24406
24759
  }
24407
24760
  });
24408
- var iconCommand = defineCommand118({
24761
+ var iconCommand = defineCommand120({
24409
24762
  meta: {
24410
24763
  name: "icon",
24411
24764
  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'"
@@ -24445,7 +24798,7 @@ var iconCommand = defineCommand118({
24445
24798
  });
24446
24799
 
24447
24800
  // src/commands/images/ingest.ts
24448
- import { defineCommand as defineCommand119 } from "citty";
24801
+ import { defineCommand as defineCommand121 } from "citty";
24449
24802
  registerSchema({
24450
24803
  command: "images.ingest",
24451
24804
  description: "Ingest a remote image URL into the library (full describe + embed).",
@@ -24457,7 +24810,7 @@ registerSchema({
24457
24810
  context: { type: "string", description: "Description context hint", required: false }
24458
24811
  }
24459
24812
  });
24460
- var ingestCommand = defineCommand119({
24813
+ var ingestCommand = defineCommand121({
24461
24814
  meta: {
24462
24815
  name: "ingest",
24463
24816
  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"
@@ -24499,32 +24852,7 @@ var ingestCommand = defineCommand119({
24499
24852
  });
24500
24853
 
24501
24854
  // src/commands/images/library.ts
24502
- import { defineCommand as defineCommand120 } from "citty";
24503
-
24504
- // src/commands/images/logoHints.ts
24505
- var LOGO_QUERY_RE = /\blogos?\b|\bwordmark\b|\bbrand mark\b/i;
24506
- function withLogoShape(row) {
24507
- return {
24508
- ...row,
24509
- logoShape: classifyLogoShape({
24510
- aspectRatio: row.aspectRatio,
24511
- hasAlpha: row.hasAlpha,
24512
- solidity: row.solidity
24513
- })
24514
- };
24515
- }
24516
- function buildLogoLibraryHints(query, rows) {
24517
- if (!LOGO_QUERY_RE.test(query)) return [];
24518
- const plates = rows.filter((row) => row.logoShape === "plated-icon");
24519
- if (plates.length === 0) return [];
24520
- const names = plates.map((row) => row.name).filter((name) => Boolean(name)).slice(0, 4);
24521
- const subject = names.length > 0 ? names.join(", ") : `${plates.length} result(s)`;
24522
- return [
24523
- `PLATED ICON: ${subject} \u2014 solid tile with the mark knocked out, not a wordmark. In a logo strip it renders as a filled box and verify blocks it (logo-plate). Re-source with 'baker images logo <domain> --variant logo', or strip the plate with 'baker images normalize <file> --remove-bg --shrink-to-content'. Only use a plate in slots under ~32px.`
24524
- ];
24525
- }
24526
-
24527
- // src/commands/images/library.ts
24855
+ import { defineCommand as defineCommand122 } from "citty";
24528
24856
  registerSchema({
24529
24857
  command: "images.library",
24530
24858
  description: "Search the company image library. Returns only ready images.",
@@ -24550,7 +24878,7 @@ registerSchema({
24550
24878
  }
24551
24879
  }
24552
24880
  });
24553
- var libraryCommand = defineCommand120({
24881
+ var libraryCommand = defineCommand122({
24554
24882
  meta: {
24555
24883
  name: "library",
24556
24884
  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"
@@ -24589,10 +24917,8 @@ var libraryCommand = defineCommand120({
24589
24917
  if (minScore !== void 0) {
24590
24918
  data = data.filter((r) => typeof r.score === "number" && r.score >= minScore);
24591
24919
  }
24592
- const shaped = data.map(withLogoShape);
24593
- const hints = buildLogoLibraryHints(query, shaped);
24594
24920
  writeOutput(
24595
- { ok: true, data: shaped, ...hints.length > 0 ? { hints } : {} },
24921
+ { ok: true, data },
24596
24922
  args.output || "json",
24597
24923
  args.fields ? args.fields.split(",") : void 0,
24598
24924
  args.full
@@ -24609,17 +24935,12 @@ var libraryCommand = defineCommand120({
24609
24935
  });
24610
24936
 
24611
24937
  // src/commands/images/logo.ts
24612
- import { defineCommand as defineCommand121 } from "citty";
24613
- var MAX_DOMAINS = 20;
24938
+ import { defineCommand as defineCommand123 } from "citty";
24614
24939
  registerSchema({
24615
24940
  command: "images.logo",
24616
24941
  description: "Brand logo lookup via Brandfetch CDN (fallback/404). Auto-ingests by default.",
24617
24942
  args: {
24618
- domain: {
24619
- type: "string",
24620
- description: "Brand domain, or a comma-separated list for a whole strip (e.g. stripe.com,intercom.com)",
24621
- required: true
24622
- },
24943
+ domain: { type: "string", description: "Brand domain (e.g. stripe.com)", required: true },
24623
24944
  variant: { type: "string", description: "icon | logo | symbol", required: false },
24624
24945
  "auto-ingest": {
24625
24946
  type: "number",
@@ -24639,13 +24960,13 @@ registerSchema({
24639
24960
  }
24640
24961
  }
24641
24962
  });
24642
- var logoCommand = defineCommand121({
24963
+ var logoCommand = defineCommand123({
24643
24964
  meta: {
24644
24965
  name: "logo",
24645
- description: "Brand logo via Brandfetch CDN. Returns up to 5 variants (icon, light/dark logo, light/dark symbol). Auto-ingests the first variant.\n\nSource a whole strip in one call by passing a comma-separated list \u2014 a text-rendered brand name is never the cheaper option.\n\nExamples:\n baker images logo stripe.com --variant logo\n baker images logo stripe.com,intercom.com,notion.so --variant logo"
24966
+ 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"
24646
24967
  },
24647
24968
  args: {
24648
- domain: { type: "positional", description: "Brand domain, or a comma-separated list", required: false },
24969
+ domain: { type: "positional", description: "Brand domain", required: false },
24649
24970
  variant: { type: "string", description: "icon|logo|symbol", required: false },
24650
24971
  "auto-ingest": { type: "string", description: "Ingest top N (0-20, default 1)", required: false },
24651
24972
  "no-auto-ingest": { type: "boolean", description: "Skip auto-ingest", required: false },
@@ -24653,50 +24974,18 @@ var logoCommand = defineCommand121({
24653
24974
  },
24654
24975
  run: async ({ args }) => {
24655
24976
  try {
24656
- const raw = args.domain;
24657
- const domains = [
24658
- ...new Set(
24659
- (raw ?? "").split(",").map((d) => d.trim()).filter(Boolean)
24660
- )
24661
- ];
24662
- if (domains.length === 0) {
24977
+ const domain = args.domain;
24978
+ if (!domain) {
24663
24979
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Domain is required" } });
24664
24980
  process.exit(1);
24665
24981
  }
24666
- if (domains.length > MAX_DOMAINS) {
24667
- writeJson({
24668
- ok: false,
24669
- error: {
24670
- code: "VALIDATION_ERROR",
24671
- message: `Too many domains (${domains.length}); pass at most ${MAX_DOMAINS} per call`
24672
- }
24673
- });
24674
- process.exit(1);
24675
- }
24676
- const base = {};
24677
- if (args.variant) base.variant = args.variant;
24678
- if (args["auto-ingest"] !== void 0) base.autoIngest = Number(args["auto-ingest"]);
24679
- else if (args["no-auto-ingest"]) base.autoIngest = 0;
24680
- if (args.context) base.descriptionContext = args.context;
24681
- const fetchOne = (domain) => apiPost("/api/images/logo", { ...base, domain });
24682
- if (domains.length === 1) {
24683
- writeJson({ ok: true, data: await fetchOne(domains[0]) });
24684
- return;
24685
- }
24686
- const settled = await Promise.all(
24687
- domains.map(async (domain) => {
24688
- try {
24689
- return { domain, ...await fetchOne(domain) };
24690
- } catch (err) {
24691
- return { domain, hits: [], error: err instanceof ApiError ? err.message : "Lookup failed" };
24692
- }
24693
- })
24694
- );
24695
- const empty = settled.filter((r) => r.hits.length === 0).map((r) => r.domain);
24696
- const hints = empty.length > 0 ? [
24697
- `NO LOGO: ${empty.join(", ")} \u2014 Brandfetch doesn't know ${empty.length === 1 ? "this brand" : "these brands"}. Fall back per domain: 'baker images extract <domain> --auto-ingest 5', then 'baker images icon <brand> --set simple-icons'. Still nothing \u2192 'baker actions create' to acquire the file. Do not render the name as text.`
24698
- ] : [];
24699
- writeJson({ ok: true, data: { results: settled }, ...hints.length > 0 ? { hints } : {} });
24982
+ const body = { domain };
24983
+ if (args.variant) body.variant = args.variant;
24984
+ if (args["auto-ingest"] !== void 0) body.autoIngest = Number(args["auto-ingest"]);
24985
+ else if (args["no-auto-ingest"]) body.autoIngest = 0;
24986
+ if (args.context) body.descriptionContext = args.context;
24987
+ const data = await apiPost("/api/images/logo", body);
24988
+ writeJson({ ok: true, data });
24700
24989
  } catch (err) {
24701
24990
  if (err instanceof ApiError) {
24702
24991
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
@@ -24709,7 +24998,7 @@ var logoCommand = defineCommand121({
24709
24998
  });
24710
24999
 
24711
25000
  // src/commands/images/normalize.ts
24712
- import { defineCommand as defineCommand122 } from "citty";
25001
+ import { defineCommand as defineCommand124 } from "citty";
24713
25002
 
24714
25003
  // src/lib/image/color-changer.ts
24715
25004
  import quantize from "quantize";
@@ -24740,7 +25029,6 @@ function getDominantEdgeColor(data, width, height) {
24740
25029
  const colorCount = {};
24741
25030
  function accumulateColor(i, j) {
24742
25031
  const idx = (i * width + j) * 4;
24743
- if ((data[idx + 3] ?? 0) < 10) return;
24744
25032
  const colorKey = `${data[idx]},${data[idx + 1]},${data[idx + 2]}`;
24745
25033
  colorCount[colorKey] = (colorCount[colorKey] ?? 0) + 1;
24746
25034
  }
@@ -24753,7 +25041,7 @@ function getDominantEdgeColor(data, width, height) {
24753
25041
  accumulateColor(i, width - 1);
24754
25042
  }
24755
25043
  let maxCount = 0;
24756
- let dominantColor = null;
25044
+ let dominantColor = { r: 0, g: 0, b: 0 };
24757
25045
  for (const key in colorCount) {
24758
25046
  const count = colorCount[key];
24759
25047
  if (count > maxCount) {
@@ -24764,27 +25052,6 @@ function getDominantEdgeColor(data, width, height) {
24764
25052
  }
24765
25053
  return dominantColor;
24766
25054
  }
24767
- function opaqueSolidity(data, width, height) {
24768
- let minX = width;
24769
- let minY = height;
24770
- let maxX = -1;
24771
- let maxY = -1;
24772
- let opaque = 0;
24773
- for (let y = 0; y < height; y++) {
24774
- for (let x = 0; x < width; x++) {
24775
- if ((data[(y * width + x) * 4 + 3] ?? 0) <= 200) continue;
24776
- opaque++;
24777
- if (x < minX) minX = x;
24778
- if (x > maxX) maxX = x;
24779
- if (y < minY) minY = y;
24780
- if (y > maxY) maxY = y;
24781
- }
24782
- }
24783
- if (maxX < 0) return 0;
24784
- const boxArea = (maxX - minX + 1) * (maxY - minY + 1);
24785
- return boxArea === 0 ? 0 : opaque / boxArea;
24786
- }
24787
- var PLATE_SOLIDITY_THRESHOLD2 = 0.6;
24788
25055
  function hasTransparency(data, threshold = 0.02) {
24789
25056
  let transparentPixels = 0;
24790
25057
  let totalPixels = 0;
@@ -24849,9 +25116,6 @@ function removeBackground(data, width, height, colorRangeThreshold = COLOR_RANGE
24849
25116
  return data;
24850
25117
  }
24851
25118
  const dominantEdgeColor = getDominantEdgeColor(data, width, height);
24852
- if (!dominantEdgeColor) {
24853
- return data;
24854
- }
24855
25119
  const isGradient = hasGradientColors(data);
24856
25120
  const result = Buffer.from(data);
24857
25121
  if (isGradient) {
@@ -25129,8 +25393,8 @@ async function processInternal(inputBuffer, isSVG, options) {
25129
25393
  const metadata = await sharp3(inputBuffer).metadata();
25130
25394
  let alreadyTransparent = false;
25131
25395
  if (metadata.hasAlpha) {
25132
- const { data: alphaData, info: info2 } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25133
- alreadyTransparent = opaqueSolidity(alphaData, info2.width, info2.height) < PLATE_SOLIDITY_THRESHOLD2;
25396
+ const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
25397
+ alreadyTransparent = hasTransparency(alphaData, 0.05);
25134
25398
  }
25135
25399
  let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
25136
25400
  if (options.color) {
@@ -25466,7 +25730,7 @@ function coerceRawArgs(args) {
25466
25730
  "dry-run": bool(args["dry-run"])
25467
25731
  };
25468
25732
  }
25469
- var normalizeCommand = defineCommand122({
25733
+ var normalizeCommand = defineCommand124({
25470
25734
  meta: {
25471
25735
  name: "normalize",
25472
25736
  description: `Normalize logos / images: declarative recolor + bg removal + trim + resize. Operates on local files; writes in-place by default.
@@ -25521,7 +25785,7 @@ Examples:
25521
25785
  });
25522
25786
 
25523
25787
  // src/commands/images/pinterest.ts
25524
- import { defineCommand as defineCommand123 } from "citty";
25788
+ import { defineCommand as defineCommand125 } from "citty";
25525
25789
  registerSchema({
25526
25790
  command: "images.pinterest",
25527
25791
  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.",
@@ -25541,7 +25805,7 @@ registerSchema({
25541
25805
  }
25542
25806
  }
25543
25807
  });
25544
- var pinterestCommand = defineCommand123({
25808
+ var pinterestCommand = defineCommand125({
25545
25809
  meta: {
25546
25810
  name: "pinterest",
25547
25811
  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'"
@@ -25581,7 +25845,7 @@ var pinterestCommand = defineCommand123({
25581
25845
  });
25582
25846
 
25583
25847
  // src/commands/images/screenshot.ts
25584
- import { defineCommand as defineCommand124 } from "citty";
25848
+ import { defineCommand as defineCommand126 } from "citty";
25585
25849
  registerSchema({
25586
25850
  command: "images.screenshot",
25587
25851
  description: "Capture a website screenshot via ScreenshotOne. Auto-ingests on success.",
@@ -25597,7 +25861,7 @@ registerSchema({
25597
25861
  }
25598
25862
  }
25599
25863
  });
25600
- var screenshotCommand = defineCommand124({
25864
+ var screenshotCommand = defineCommand126({
25601
25865
  meta: {
25602
25866
  name: "screenshot",
25603
25867
  description: "Screenshot a URL via ScreenshotOne. $0.009/capture. Auto-ingests to library.\n\nExample: baker images screenshot https://stripe.com --full-page"
@@ -25660,7 +25924,7 @@ var screenshotCommand = defineCommand124({
25660
25924
  });
25661
25925
 
25662
25926
  // src/commands/images/search.ts
25663
- import { defineCommand as defineCommand125 } from "citty";
25927
+ import { defineCommand as defineCommand127 } from "citty";
25664
25928
  registerSchema({
25665
25929
  command: "images.search",
25666
25930
  description: "Search images by text query. Only returns ready images.",
@@ -25676,7 +25940,7 @@ registerSchema({
25676
25940
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
25677
25941
  }
25678
25942
  });
25679
- var searchCommand = defineCommand125({
25943
+ var searchCommand = defineCommand127({
25680
25944
  meta: {
25681
25945
  name: "search",
25682
25946
  description: "Semantic search images by text query. Uses hybrid BM25 + vector + reranking. Example: baker images search 'hero banner' --aspect-ratio 16:9 --tags logo"
@@ -25736,7 +26000,7 @@ var searchCommand = defineCommand125({
25736
26000
  });
25737
26001
 
25738
26002
  // src/commands/images/sticker.ts
25739
- import { defineCommand as defineCommand126 } from "citty";
26003
+ import { defineCommand as defineCommand128 } from "citty";
25740
26004
  registerSchema({
25741
26005
  command: "images.sticker",
25742
26006
  description: "Search Giphy stickers \u2014 transparent-background overlays for ad creative.",
@@ -25768,7 +26032,7 @@ registerSchema({
25768
26032
  }
25769
26033
  }
25770
26034
  });
25771
- var stickerCommand = defineCommand126({
26035
+ var stickerCommand = defineCommand128({
25772
26036
  meta: {
25773
26037
  name: "sticker",
25774
26038
  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"
@@ -25815,7 +26079,11 @@ var stickerCommand = defineCommand126({
25815
26079
  });
25816
26080
 
25817
26081
  // src/commands/images/stock.ts
25818
- import { defineCommand as defineCommand127 } from "citty";
26082
+ import { defineCommand as defineCommand129 } from "citty";
26083
+ var STOCK_ERROR_FIX = {
26084
+ action: "use_different_resource",
26085
+ explanation: "Switch provider instead of retrying stock search. Stock search is one of several image sources. Run `baker images find <query> --sources library,pinterest,google` (`--sources` is required \u2014 `find` alone searches the library only) or `baker images generate` to make the asset. Never sleep-and-retry this command \u2014 the CLI already backs off on rate limits. If no image can be sourced, continue the rest of the task with a placeholder rather than aborting it."
26086
+ };
25819
26087
  registerSchema({
25820
26088
  command: "images.stock",
25821
26089
  description: "Stock photo, vector illustration, icon-set, and PSD search via Magnific (Freepik's developer API).",
@@ -25873,7 +26141,22 @@ registerSchema({
25873
26141
  }
25874
26142
  }
25875
26143
  });
25876
- var stockCommand = defineCommand127({
26144
+ function buildStockRequest(query, args) {
26145
+ const body = { query };
26146
+ if (args.type) body.contentType = args.type;
26147
+ if (args.orientation) body.orientation = args.orientation;
26148
+ if (args.license) body.license = args.license;
26149
+ if (args.color) body.color = args.color;
26150
+ if (args.ai) body.aiGenerated = args.ai;
26151
+ if (args.people) body.people = args.people;
26152
+ if (args.order) body.order = args.order;
26153
+ if (args.limit) body.limit = Number(args.limit);
26154
+ if (args.page) body.page = Number(args.page);
26155
+ if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
26156
+ if (args.context) body.descriptionContext = args.context;
26157
+ return body;
26158
+ }
26159
+ var stockCommand = defineCommand129({
25877
26160
  meta: {
25878
26161
  name: "stock",
25879
26162
  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"
@@ -25903,23 +26186,23 @@ var stockCommand = defineCommand127({
25903
26186
  writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "Query is required" } });
25904
26187
  process.exit(1);
25905
26188
  }
25906
- const body = { query };
25907
- if (args.type) body.contentType = args.type;
25908
- if (args.orientation) body.orientation = args.orientation;
25909
- if (args.license) body.license = args.license;
25910
- if (args.color) body.color = args.color;
25911
- if (args.ai) body.aiGenerated = args.ai;
25912
- if (args.people) body.people = args.people;
25913
- if (args.order) body.order = args.order;
25914
- if (args.limit) body.limit = Number(args.limit);
25915
- if (args.page) body.page = Number(args.page);
25916
- if (args["auto-ingest"]) body.autoIngest = Number(args["auto-ingest"]);
25917
- if (args.context) body.descriptionContext = args.context;
25918
- const data = await apiPost("/api/images/stock", body);
25919
- writeJson({ ok: true, data });
26189
+ const data = await apiPost("/api/images/stock", buildStockRequest(query, args));
26190
+ const hints = emptyResultHints({
26191
+ provider: "stock",
26192
+ hitCount: data.hits.length,
26193
+ activeFilters: activeFilterFlags(args, ["type", "orientation", "license", "color", "ai", "people"])
26194
+ });
26195
+ writeJson({ ok: true, data, ...hints.length ? { hints } : {} });
25920
26196
  } catch (err) {
25921
26197
  if (err instanceof ApiError) {
25922
- writeJson({ ok: false, error: { code: err.code, message: err.message } });
26198
+ writeJson({
26199
+ ok: false,
26200
+ error: {
26201
+ code: err.code,
26202
+ message: err.message,
26203
+ fix: STOCK_ERROR_FIX
26204
+ }
26205
+ });
25923
26206
  process.exit(1);
25924
26207
  }
25925
26208
  writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
@@ -25929,7 +26212,7 @@ var stockCommand = defineCommand127({
25929
26212
  });
25930
26213
 
25931
26214
  // src/lib/tags-command.ts
25932
- import { defineCommand as defineCommand128 } from "citty";
26215
+ import { defineCommand as defineCommand130 } from "citty";
25933
26216
  function makeTagsCommand(command, label, endpoint) {
25934
26217
  registerSchema({
25935
26218
  command: `${command}.tags`,
@@ -25938,7 +26221,7 @@ function makeTagsCommand(command, label, endpoint) {
25938
26221
  output: { type: "string", description: "Output format: md|json", required: false, default: "md" }
25939
26222
  }
25940
26223
  });
25941
- return defineCommand128({
26224
+ return defineCommand130({
25942
26225
  meta: {
25943
26226
  name: "tags",
25944
26227
  description: `List the available ${label} tag names (defaults + company custom tags). Use before filtering with --tags. Example: baker ${command} tags`
@@ -25974,7 +26257,7 @@ function makeTagsCommand(command, label, endpoint) {
25974
26257
  var tagsCommand2 = makeTagsCommand("images", "image", "/api/images/tags");
25975
26258
 
25976
26259
  // src/commands/images/upload.ts
25977
- import { defineCommand as defineCommand129 } from "citty";
26260
+ import { defineCommand as defineCommand131 } from "citty";
25978
26261
  registerSchema({
25979
26262
  command: "images.upload",
25980
26263
  description: "Upload an image to the library \u2014 local file path or remote http(s) URL.",
@@ -26012,7 +26295,7 @@ registerSchema({
26012
26295
  function isRemoteUrl2(value) {
26013
26296
  return /^https?:\/\//i.test(value);
26014
26297
  }
26015
- var uploadCommand = defineCommand129({
26298
+ var uploadCommand = defineCommand131({
26016
26299
  meta: {
26017
26300
  name: "upload",
26018
26301
  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'"
@@ -26105,7 +26388,7 @@ async function uploadLocal(target, args) {
26105
26388
  }
26106
26389
 
26107
26390
  // src/commands/images/upscale.ts
26108
- import { defineCommand as defineCommand130 } from "citty";
26391
+ import { defineCommand as defineCommand132 } from "citty";
26109
26392
  registerSchema({
26110
26393
  command: "images.upscale",
26111
26394
  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).",
@@ -26120,7 +26403,7 @@ registerSchema({
26120
26403
  }
26121
26404
  });
26122
26405
  var POLL_INTERVAL_MS3 = 1500;
26123
- var upscaleCommand = defineCommand130({
26406
+ var upscaleCommand = defineCommand132({
26124
26407
  meta: {
26125
26408
  name: "upscale",
26126
26409
  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"
@@ -26175,7 +26458,7 @@ var upscaleCommand = defineCommand130({
26175
26458
  });
26176
26459
 
26177
26460
  // src/commands/images/use.ts
26178
- import { defineCommand as defineCommand131 } from "citty";
26461
+ import { defineCommand as defineCommand133 } from "citty";
26179
26462
  registerSchema({
26180
26463
  command: "images.use",
26181
26464
  description: "Ingest a URL and wait for the library record to be ready.",
@@ -26191,7 +26474,7 @@ registerSchema({
26191
26474
  }
26192
26475
  });
26193
26476
  var POLL_INTERVAL_MS4 = 1500;
26194
- var useCommand = defineCommand131({
26477
+ var useCommand = defineCommand133({
26195
26478
  meta: {
26196
26479
  name: "use",
26197
26480
  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"
@@ -26237,7 +26520,7 @@ var useCommand = defineCommand131({
26237
26520
  });
26238
26521
 
26239
26522
  // src/commands/images/index.ts
26240
- var imagesCommand = defineCommand132({
26523
+ var imagesCommand = defineCommand134({
26241
26524
  meta: {
26242
26525
  name: "images",
26243
26526
  description: `Find, source, and normalize images. Subcommands route by provider so cost + license are explicit.
@@ -26308,16 +26591,16 @@ Full guide: __tooling__/docs/tools/baker/images.md`
26308
26591
  });
26309
26592
 
26310
26593
  // src/commands/landing/index.ts
26311
- import { defineCommand as defineCommand134 } from "citty";
26594
+ import { defineCommand as defineCommand136 } from "citty";
26312
26595
 
26313
26596
  // src/commands/landing/critique.ts
26314
26597
  import { readdir as readdir8, stat as stat6 } from "fs/promises";
26315
- import path26 from "path";
26316
- import { defineCommand as defineCommand133 } from "citty";
26598
+ import path27 from "path";
26599
+ import { defineCommand as defineCommand135 } from "citty";
26317
26600
 
26318
26601
  // src/engine/landing/lib/brand-tokens.ts
26319
- import { readFile as readFile20 } from "fs/promises";
26320
- import path23 from "path";
26602
+ import { readFile as readFile21 } from "fs/promises";
26603
+ import path24 from "path";
26321
26604
 
26322
26605
  // src/engine/landing/lib/color.ts
26323
26606
  var NEUTRAL_COLOR_KEYWORDS = /* @__PURE__ */ new Set([
@@ -26479,14 +26762,14 @@ function parseBrandMd(brandMd, fonts, colors) {
26479
26762
  }
26480
26763
  }
26481
26764
  async function loadBrandTokens(projectRoot) {
26482
- const globalCss = await safeRead(path23.join(projectRoot, "src", "styles", "global.css"));
26483
- const brandMd = await safeRead(path23.join(projectRoot, "src", "brand", "BRAND.md"));
26765
+ const globalCss = await safeRead(path24.join(projectRoot, "src", "styles", "global.css"));
26766
+ const brandMd = await safeRead(path24.join(projectRoot, "src", "brand", "BRAND.md"));
26484
26767
  if (!globalCss && !brandMd) return EMPTY;
26485
26768
  return parseBrandTokens(globalCss, brandMd);
26486
26769
  }
26487
26770
  async function safeRead(file) {
26488
26771
  try {
26489
- return await readFile20(file, "utf8");
26772
+ return await readFile21(file, "utf8");
26490
26773
  } catch {
26491
26774
  return "";
26492
26775
  }
@@ -27272,40 +27555,40 @@ function describeCounts(findings) {
27272
27555
  }
27273
27556
 
27274
27557
  // src/commands/landing/snapshot.ts
27275
- import { mkdir as mkdir7, rename as rename2, writeFile as writeFile10 } from "fs/promises";
27276
- import path24 from "path";
27558
+ import { mkdir as mkdir8, rename as rename2, writeFile as writeFile11 } from "fs/promises";
27559
+ import path25 from "path";
27277
27560
  var CRITIC_VERSION = "1";
27278
27561
  function critiqueCacheDir(projectRoot) {
27279
- return path24.join(projectRoot, ".cache", "landing-critique");
27562
+ return path25.join(projectRoot, ".cache", "landing-critique");
27280
27563
  }
27281
27564
  function snapshotPath(projectRoot, slug) {
27282
- return path24.join(critiqueCacheDir(projectRoot), `${slug}.json`);
27565
+ return path25.join(critiqueCacheDir(projectRoot), `${slug}.json`);
27283
27566
  }
27284
27567
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
27285
- await mkdir7(critiqueCacheDir(projectRoot), { recursive: true });
27568
+ await mkdir8(critiqueCacheDir(projectRoot), { recursive: true });
27286
27569
  const dest = snapshotPath(projectRoot, snapshot.slug);
27287
27570
  const tmp = `${dest}.tmp`;
27288
- await writeFile10(tmp, `${JSON.stringify(snapshot, null, 2)}
27571
+ await writeFile11(tmp, `${JSON.stringify(snapshot, null, 2)}
27289
27572
  `, "utf8");
27290
27573
  await rename2(tmp, dest);
27291
27574
  }
27292
27575
 
27293
27576
  // src/commands/landing/source-version.ts
27294
- import { readdir as readdir7, readFile as readFile21, stat as stat5 } from "fs/promises";
27295
- import path25 from "path";
27577
+ import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
27578
+ import path26 from "path";
27296
27579
  async function landingSourceRelPaths(landingDir) {
27297
27580
  const rel = [];
27298
- if (await isFile(path25.join(landingDir, "index.astro"))) rel.push("index.astro");
27299
- const componentsDir = path25.join(landingDir, "_components");
27581
+ if (await isFile(path26.join(landingDir, "index.astro"))) rel.push("index.astro");
27582
+ const componentsDir = path26.join(landingDir, "_components");
27300
27583
  for (const abs of await walkAstro(componentsDir)) {
27301
- rel.push(path25.relative(landingDir, abs).split(path25.sep).join("/"));
27584
+ rel.push(path26.relative(landingDir, abs).split(path26.sep).join("/"));
27302
27585
  }
27303
27586
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
27304
27587
  }
27305
27588
  async function readLandingSources(landingDir) {
27306
27589
  const rel = await landingSourceRelPaths(landingDir);
27307
27590
  const out = [];
27308
- for (const r of rel) out.push({ path: r, text: await readFile21(path25.join(landingDir, r), "utf8") });
27591
+ for (const r of rel) out.push({ path: r, text: await readFile22(path26.join(landingDir, r), "utf8") });
27309
27592
  return out;
27310
27593
  }
27311
27594
  async function computeLandingSourceSha(landingDir) {
@@ -27314,7 +27597,7 @@ async function computeLandingSourceSha(landingDir) {
27314
27597
  for (const r of rel) {
27315
27598
  let bytes;
27316
27599
  try {
27317
- bytes = await readFile21(path25.join(landingDir, r));
27600
+ bytes = await readFile22(path26.join(landingDir, r));
27318
27601
  } catch {
27319
27602
  bytes = Buffer.alloc(0);
27320
27603
  }
@@ -27338,7 +27621,7 @@ async function walkAstro(dir) {
27338
27621
  }
27339
27622
  const out = [];
27340
27623
  for (const entry of entries) {
27341
- const abs = path25.join(dir, entry.name);
27624
+ const abs = path26.join(dir, entry.name);
27342
27625
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
27343
27626
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
27344
27627
  }
@@ -27359,14 +27642,14 @@ registerSchema({
27359
27642
  }
27360
27643
  });
27361
27644
  var SLUG_RE = /^[a-z0-9][a-z0-9._-]*$/i;
27362
- function fail4(code, message, fix) {
27645
+ function fail5(code, message, fix) {
27363
27646
  process.stderr.write(
27364
27647
  `${JSON.stringify({ ok: false, error: { code, message, ...fix ? { fix } : {} } }, null, 2)}
27365
27648
  `
27366
27649
  );
27367
27650
  process.exit(2);
27368
27651
  }
27369
- var critiqueCommand2 = defineCommand133({
27652
+ var critiqueCommand2 = defineCommand135({
27370
27653
  meta: {
27371
27654
  name: "critique",
27372
27655
  description: "Start here: `baker landing critique <slug>` after building or editing a landing. Deterministic design-quality critic (ADVISORY \u2014 findings never fail it). Flags the known AI design tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) tiered block/warn/advisory, respecting the client's BRAND.md as the allowlist. Also records the critique that publishing requires \u2014 run it before finishing a landing."
@@ -27379,15 +27662,15 @@ var critiqueCommand2 = defineCommand133({
27379
27662
  const slug = String(args.slug);
27380
27663
  const projectRoot = process.cwd();
27381
27664
  if (!SLUG_RE.test(slug) || slug.includes("..")) {
27382
- fail4(
27665
+ fail5(
27383
27666
  "INVALID_SLUG",
27384
27667
  `"${slug}" is not a landing slug \u2014 use the folder name directly under src/pages/ (letters, digits, dashes; not a path, not a _-private folder).`,
27385
27668
  { availableSlugs: await listLandingSlugs(projectRoot) }
27386
27669
  );
27387
27670
  }
27388
- const landingDir = path26.resolve(projectRoot, "src", "pages", slug);
27671
+ const landingDir = path27.resolve(projectRoot, "src", "pages", slug);
27389
27672
  if (!await isDir(landingDir)) {
27390
- fail4("NOT_FOUND", `No landing at src/pages/${slug}/`, {
27673
+ fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
27391
27674
  availableSlugs: await listLandingSlugs(projectRoot)
27392
27675
  });
27393
27676
  }
@@ -27440,7 +27723,7 @@ var critiqueCommand2 = defineCommand133({
27440
27723
  });
27441
27724
  async function listLandingSlugs(projectRoot) {
27442
27725
  try {
27443
- const entries = await readdir8(path26.join(projectRoot, "src", "pages"), { withFileTypes: true });
27726
+ const entries = await readdir8(path27.join(projectRoot, "src", "pages"), { withFileTypes: true });
27444
27727
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
27445
27728
  } catch {
27446
27729
  return [];
@@ -27466,7 +27749,7 @@ async function isDir(p) {
27466
27749
  }
27467
27750
 
27468
27751
  // src/commands/landing/index.ts
27469
- var landingCommand = defineCommand134({
27752
+ var landingCommand = defineCommand136({
27470
27753
  meta: {
27471
27754
  name: "landing",
27472
27755
  description: `Design-quality tools for landing pages (src/pages/<slug>/).
@@ -27482,7 +27765,7 @@ Subcommands:
27482
27765
  });
27483
27766
 
27484
27767
  // src/commands/mcp/index.ts
27485
- import { defineCommand as defineCommand135 } from "citty";
27768
+ import { defineCommand as defineCommand137 } from "citty";
27486
27769
  var SCOPES = ["user", "user_org", "company", "org"];
27487
27770
  function parseScope(raw) {
27488
27771
  const scope = raw === void 0 ? "company" : String(raw);
@@ -27508,7 +27791,7 @@ function parseHeaders(raw) {
27508
27791
  }
27509
27792
  return Object.keys(headers).length > 0 ? headers : void 0;
27510
27793
  }
27511
- function fail5(err) {
27794
+ function fail6(err) {
27512
27795
  if (err instanceof ApiError) {
27513
27796
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
27514
27797
  process.exit(1);
@@ -27521,7 +27804,7 @@ registerSchema({
27521
27804
  description: "List every third-party tool this chat can reach: managed integrations (Attio, Slack, Gmail, Google Sheets, \u2026) plus custom MCP servers. Start here when the user mentions an external tool.",
27522
27805
  args: {}
27523
27806
  });
27524
- var connectedCommand = defineCommand135({
27807
+ var connectedCommand = defineCommand137({
27525
27808
  meta: {
27526
27809
  name: "connected",
27527
27810
  description: `List all connected third-party tools \u2014 managed integrations plus custom MCP servers.
@@ -27559,7 +27842,7 @@ A tool the user names that is NOT listed here is simply not connected yet.`
27559
27842
  hints
27560
27843
  });
27561
27844
  } catch (err) {
27562
- fail5(err);
27845
+ fail6(err);
27563
27846
  }
27564
27847
  }
27565
27848
  });
@@ -27568,7 +27851,7 @@ registerSchema({
27568
27851
  description: "List the custom MCP servers this company's chats see (org + company + your own user scope).",
27569
27852
  args: {}
27570
27853
  });
27571
- var listCommand8 = defineCommand135({
27854
+ var listCommand8 = defineCommand137({
27572
27855
  meta: { name: "list", description: "List custom MCP servers visible to this company's chats." },
27573
27856
  run: async () => {
27574
27857
  try {
@@ -27579,7 +27862,7 @@ var listCommand8 = defineCommand135({
27579
27862
  );
27580
27863
  writeJson({ ok: true, data: data.servers });
27581
27864
  } catch (err) {
27582
- fail5(err);
27865
+ fail6(err);
27583
27866
  }
27584
27867
  }
27585
27868
  });
@@ -27593,7 +27876,7 @@ registerSchema({
27593
27876
  header: { type: "string", description: 'Auth header "Key: Value" (repeatable)', required: false }
27594
27877
  }
27595
27878
  });
27596
- var addCommand = defineCommand135({
27879
+ var addCommand = defineCommand137({
27597
27880
  meta: {
27598
27881
  name: "add",
27599
27882
  description: `Register a custom MCP server. Tools appear as mcp__<name>__* on the NEXT message.
@@ -27625,7 +27908,7 @@ Examples:
27625
27908
  });
27626
27909
  writeJson({ ok: true, data });
27627
27910
  } catch (err) {
27628
- fail5(err);
27911
+ fail6(err);
27629
27912
  }
27630
27913
  }
27631
27914
  });
@@ -27634,7 +27917,7 @@ registerSchema({
27634
27917
  description: "Remove a company custom MCP server by name.",
27635
27918
  args: { name: { type: "string", description: "Server name to remove", required: true } }
27636
27919
  });
27637
- var removeCommand4 = defineCommand135({
27920
+ var removeCommand4 = defineCommand137({
27638
27921
  meta: {
27639
27922
  name: "remove",
27640
27923
  description: `Remove a company custom MCP server by name.
@@ -27652,11 +27935,11 @@ Example:
27652
27935
  });
27653
27936
  writeJson({ ok: true, data });
27654
27937
  } catch (err) {
27655
- fail5(err);
27938
+ fail6(err);
27656
27939
  }
27657
27940
  }
27658
27941
  });
27659
- var mcpCommand = defineCommand135({
27942
+ var mcpCommand = defineCommand137({
27660
27943
  meta: {
27661
27944
  name: "mcp",
27662
27945
  description: `Third-party tools for this company \u2014 see what's connected, register custom HTTPS MCP endpoints.
@@ -27682,10 +27965,10 @@ Full guide: __tooling__/docs/tools/baker/mcp.md`
27682
27965
  });
27683
27966
 
27684
27967
  // src/commands/research/index.ts
27685
- import { defineCommand as defineCommand146 } from "citty";
27968
+ import { defineCommand as defineCommand148 } from "citty";
27686
27969
 
27687
27970
  // src/commands/research/advertisers.ts
27688
- import { defineCommand as defineCommand136 } from "citty";
27971
+ import { defineCommand as defineCommand138 } from "citty";
27689
27972
 
27690
27973
  // src/commands/research/output.ts
27691
27974
  var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
@@ -27798,7 +28081,7 @@ var FIELDS3 = {
27798
28081
  etv: "Estimated traffic value (USD)",
27799
28082
  visibility: "SERP visibility score (0-1)"
27800
28083
  };
27801
- var advertisersCommand = defineCommand136({
28084
+ var advertisersCommand = defineCommand138({
27802
28085
  meta: {
27803
28086
  name: "advertisers",
27804
28087
  description: `Find domains competing for a keyword in Google SERPs.
@@ -27845,7 +28128,7 @@ Examples:
27845
28128
  });
27846
28129
 
27847
28130
  // src/commands/research/autocomplete.ts
27848
- import { defineCommand as defineCommand137 } from "citty";
28131
+ import { defineCommand as defineCommand139 } from "citty";
27849
28132
  registerSchema({
27850
28133
  command: "research.autocomplete",
27851
28134
  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).",
@@ -27868,7 +28151,7 @@ registerSchema({
27868
28151
  var FIELDS4 = {
27869
28152
  suggestion: "Autocomplete suggestion from Google"
27870
28153
  };
27871
- var autocompleteCommand = defineCommand137({
28154
+ var autocompleteCommand = defineCommand139({
27872
28155
  meta: {
27873
28156
  name: "autocomplete",
27874
28157
  description: `Get Google Autocomplete suggestions for keyword expansion.
@@ -27914,7 +28197,7 @@ Examples:
27914
28197
  });
27915
28198
 
27916
28199
  // src/commands/research/countries.ts
27917
- import { defineCommand as defineCommand138 } from "citty";
28200
+ import { defineCommand as defineCommand140 } from "citty";
27918
28201
  registerSchema({
27919
28202
  command: "research.countries",
27920
28203
  description: "List all supported country codes for --location flag in research commands.",
@@ -27971,7 +28254,7 @@ var FIELDS5 = {
27971
28254
  code: "Country code to pass as --location",
27972
28255
  name: "Country name"
27973
28256
  };
27974
- var countriesCommand = defineCommand138({
28257
+ var countriesCommand = defineCommand140({
27975
28258
  meta: {
27976
28259
  name: "countries",
27977
28260
  description: "List all supported country codes for --location flag."
@@ -27982,7 +28265,7 @@ var countriesCommand = defineCommand138({
27982
28265
  });
27983
28266
 
27984
28267
  // src/commands/research/intent.ts
27985
- import { defineCommand as defineCommand139 } from "citty";
28268
+ import { defineCommand as defineCommand141 } from "citty";
27986
28269
  registerSchema({
27987
28270
  command: "research.intent",
27988
28271
  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.",
@@ -28005,7 +28288,7 @@ var FIELDS6 = {
28005
28288
  intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
28006
28289
  probability: "Confidence score 0.0-1.0"
28007
28290
  };
28008
- var intentCommand = defineCommand139({
28291
+ var intentCommand = defineCommand141({
28009
28292
  meta: {
28010
28293
  name: "intent",
28011
28294
  description: `Classify Google Search intent for keywords. Returns intent type and confidence.
@@ -28053,7 +28336,7 @@ Examples:
28053
28336
  });
28054
28337
 
28055
28338
  // src/commands/research/keyword-gap.ts
28056
- import { defineCommand as defineCommand140 } from "citty";
28339
+ import { defineCommand as defineCommand142 } from "citty";
28057
28340
  registerSchema({
28058
28341
  command: "research.keyword-gap",
28059
28342
  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.",
@@ -28082,7 +28365,7 @@ var FIELDS7 = {
28082
28365
  cpc: "Cost per click USD",
28083
28366
  their_position: "Competitor's ranking position"
28084
28367
  };
28085
- var keywordGapCommand = defineCommand140({
28368
+ var keywordGapCommand = defineCommand142({
28086
28369
  meta: {
28087
28370
  name: "keyword-gap",
28088
28371
  description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
@@ -28156,7 +28439,7 @@ Examples:
28156
28439
  });
28157
28440
 
28158
28441
  // src/commands/research/keywords-for-site.ts
28159
- import { defineCommand as defineCommand141 } from "citty";
28442
+ import { defineCommand as defineCommand143 } from "citty";
28160
28443
  registerSchema({
28161
28444
  command: "research.keywords-for-site",
28162
28445
  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.",
@@ -28189,7 +28472,7 @@ var FIELDS8 = {
28189
28472
  competition: "LOW, MEDIUM, or HIGH",
28190
28473
  competition_index: "Competition score 0-100"
28191
28474
  };
28192
- var keywordsForSiteCommand = defineCommand141({
28475
+ var keywordsForSiteCommand = defineCommand143({
28193
28476
  meta: {
28194
28477
  name: "keywords-for-site",
28195
28478
  description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
@@ -28242,7 +28525,7 @@ Examples:
28242
28525
  });
28243
28526
 
28244
28527
  // src/commands/research/languages.ts
28245
- import { defineCommand as defineCommand142 } from "citty";
28528
+ import { defineCommand as defineCommand144 } from "citty";
28246
28529
  registerSchema({
28247
28530
  command: "research.languages",
28248
28531
  description: "List all supported language codes for --language flag in research commands.",
@@ -28272,7 +28555,7 @@ var FIELDS9 = {
28272
28555
  code: "Language code to pass as --language",
28273
28556
  name: "Language name (also accepted by --language)"
28274
28557
  };
28275
- var languagesCommand2 = defineCommand142({
28558
+ var languagesCommand2 = defineCommand144({
28276
28559
  meta: {
28277
28560
  name: "languages",
28278
28561
  description: "List all supported language codes for --language flag."
@@ -28283,7 +28566,7 @@ var languagesCommand2 = defineCommand142({
28283
28566
  });
28284
28567
 
28285
28568
  // src/commands/research/lighthouse.ts
28286
- import { defineCommand as defineCommand143 } from "citty";
28569
+ import { defineCommand as defineCommand145 } from "citty";
28287
28570
  registerSchema({
28288
28571
  command: "research.lighthouse",
28289
28572
  description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
@@ -28302,7 +28585,7 @@ var FIELDS10 = {
28302
28585
  speed_index_ms: "Speed Index in ms (good: < 3400)",
28303
28586
  interactive_ms: "Time to Interactive in ms (good: < 3800)"
28304
28587
  };
28305
- var lighthouseCommand = defineCommand143({
28588
+ var lighthouseCommand = defineCommand145({
28306
28589
  meta: {
28307
28590
  name: "lighthouse",
28308
28591
  description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
@@ -28340,7 +28623,7 @@ Examples:
28340
28623
  });
28341
28624
 
28342
28625
  // src/commands/research/relevant-pages.ts
28343
- import { defineCommand as defineCommand144 } from "citty";
28626
+ import { defineCommand as defineCommand146 } from "citty";
28344
28627
  registerSchema({
28345
28628
  command: "research.relevant-pages",
28346
28629
  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).",
@@ -28366,7 +28649,7 @@ var FIELDS11 = {
28366
28649
  keywords: "Total organic keywords the page ranks for",
28367
28650
  top_10: "Keywords in positions 1-10"
28368
28651
  };
28369
- var relevantPagesCommand = defineCommand144({
28652
+ var relevantPagesCommand = defineCommand146({
28370
28653
  meta: {
28371
28654
  name: "relevant-pages",
28372
28655
  description: `Get the top pages of a competitor domain with traffic data.
@@ -28412,7 +28695,7 @@ Examples:
28412
28695
  });
28413
28696
 
28414
28697
  // src/commands/research/web.ts
28415
- import { defineCommand as defineCommand145 } from "citty";
28698
+ import { defineCommand as defineCommand147 } from "citty";
28416
28699
  registerSchema({
28417
28700
  command: "research.web",
28418
28701
  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).",
@@ -28463,7 +28746,7 @@ async function runDeepResearch(question) {
28463
28746
  }
28464
28747
  throw new Error("Deep research timed out");
28465
28748
  }
28466
- var webCommand = defineCommand145({
28749
+ var webCommand = defineCommand147({
28467
28750
  meta: {
28468
28751
  name: "web",
28469
28752
  description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
@@ -28523,7 +28806,7 @@ Examples:
28523
28806
  });
28524
28807
 
28525
28808
  // src/commands/research/index.ts
28526
- var researchCommand = defineCommand146({
28809
+ var researchCommand = defineCommand148({
28527
28810
  meta: {
28528
28811
  name: "research",
28529
28812
  description: `Competitive intelligence and AI-powered research commands.
@@ -28564,10 +28847,10 @@ Full guide: __tooling__/docs/tools/baker/research.md`
28564
28847
  });
28565
28848
 
28566
28849
  // src/commands/scheduled-actions/index.ts
28567
- import { defineCommand as defineCommand153 } from "citty";
28850
+ import { defineCommand as defineCommand155 } from "citty";
28568
28851
 
28569
28852
  // src/commands/scheduled-actions/create.ts
28570
- import { defineCommand as defineCommand147 } from "citty";
28853
+ import { defineCommand as defineCommand149 } from "citty";
28571
28854
 
28572
28855
  // src/commands/scheduled-actions/shared.ts
28573
28856
  var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
@@ -28682,7 +28965,7 @@ registerSchema({
28682
28965
  prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
28683
28966
  }
28684
28967
  });
28685
- var createCommand2 = defineCommand147({
28968
+ var createCommand2 = defineCommand149({
28686
28969
  meta: {
28687
28970
  name: "create",
28688
28971
  description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
@@ -28731,7 +29014,7 @@ var createCommand2 = defineCommand147({
28731
29014
  });
28732
29015
 
28733
29016
  // src/commands/scheduled-actions/delete.ts
28734
- import { defineCommand as defineCommand148 } from "citty";
29017
+ import { defineCommand as defineCommand150 } from "citty";
28735
29018
  registerSchema({
28736
29019
  command: "scheduled-actions.delete",
28737
29020
  description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
@@ -28739,7 +29022,7 @@ registerSchema({
28739
29022
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
28740
29023
  }
28741
29024
  });
28742
- var deleteCommand2 = defineCommand148({
29025
+ var deleteCommand2 = defineCommand150({
28743
29026
  meta: {
28744
29027
  name: "delete",
28745
29028
  description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
@@ -28768,7 +29051,7 @@ var deleteCommand2 = defineCommand148({
28768
29051
  });
28769
29052
 
28770
29053
  // src/commands/scheduled-actions/get.ts
28771
- import { defineCommand as defineCommand149 } from "citty";
29054
+ import { defineCommand as defineCommand151 } from "citty";
28772
29055
  registerSchema({
28773
29056
  command: "scheduled-actions.get",
28774
29057
  description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
@@ -28776,7 +29059,7 @@ registerSchema({
28776
29059
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
28777
29060
  }
28778
29061
  });
28779
- var getCommand3 = defineCommand149({
29062
+ var getCommand3 = defineCommand151({
28780
29063
  meta: {
28781
29064
  name: "get",
28782
29065
  description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
@@ -28813,13 +29096,13 @@ var getCommand3 = defineCommand149({
28813
29096
  });
28814
29097
 
28815
29098
  // src/commands/scheduled-actions/list.ts
28816
- import { defineCommand as defineCommand150 } from "citty";
29099
+ import { defineCommand as defineCommand152 } from "citty";
28817
29100
  registerSchema({
28818
29101
  command: "scheduled-actions.list",
28819
29102
  description: "List published scheduled actions. Includes draft state when BAKER_CHAT_ID is set.",
28820
29103
  args: {}
28821
29104
  });
28822
- var listCommand9 = defineCommand150({
29105
+ var listCommand9 = defineCommand152({
28823
29106
  meta: {
28824
29107
  name: "list",
28825
29108
  description: "List scheduled actions. Includes staged draft ops when BAKER_CHAT_ID is set."
@@ -28840,7 +29123,7 @@ var listCommand9 = defineCommand150({
28840
29123
  });
28841
29124
 
28842
29125
  // src/commands/scheduled-actions/trigger.ts
28843
- import { defineCommand as defineCommand151 } from "citty";
29126
+ import { defineCommand as defineCommand153 } from "citty";
28844
29127
  registerSchema({
28845
29128
  command: "scheduled-actions.trigger",
28846
29129
  description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
@@ -28848,7 +29131,7 @@ registerSchema({
28848
29131
  id: { type: "string", description: "Published scheduled action ID", required: true }
28849
29132
  }
28850
29133
  });
28851
- var triggerCommand = defineCommand151({
29134
+ var triggerCommand = defineCommand153({
28852
29135
  meta: {
28853
29136
  name: "trigger",
28854
29137
  description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
@@ -28885,7 +29168,7 @@ var triggerCommand = defineCommand151({
28885
29168
  });
28886
29169
 
28887
29170
  // src/commands/scheduled-actions/update.ts
28888
- import { defineCommand as defineCommand152 } from "citty";
29171
+ import { defineCommand as defineCommand154 } from "citty";
28889
29172
  registerSchema({
28890
29173
  command: "scheduled-actions.update",
28891
29174
  description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
@@ -28910,7 +29193,7 @@ registerSchema({
28910
29193
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
28911
29194
  }
28912
29195
  });
28913
- var updateCommand2 = defineCommand152({
29196
+ var updateCommand2 = defineCommand154({
28914
29197
  meta: {
28915
29198
  name: "update",
28916
29199
  description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
@@ -28981,7 +29264,7 @@ var updateCommand2 = defineCommand152({
28981
29264
  });
28982
29265
 
28983
29266
  // src/commands/scheduled-actions/index.ts
28984
- var scheduledActionsCommand = defineCommand153({
29267
+ var scheduledActionsCommand = defineCommand155({
28985
29268
  meta: {
28986
29269
  name: "scheduled-actions",
28987
29270
  description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
@@ -29008,8 +29291,8 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
29008
29291
  });
29009
29292
 
29010
29293
  // src/commands/schema.ts
29011
- import { defineCommand as defineCommand154 } from "citty";
29012
- var schemaCommand = defineCommand154({
29294
+ import { defineCommand as defineCommand156 } from "citty";
29295
+ var schemaCommand = defineCommand156({
29013
29296
  meta: {
29014
29297
  name: "schema",
29015
29298
  description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
@@ -29051,7 +29334,7 @@ var schemaCommand = defineCommand154({
29051
29334
  });
29052
29335
 
29053
29336
  // src/commands/tags/index.ts
29054
- import { defineCommand as defineCommand155 } from "citty";
29337
+ import { defineCommand as defineCommand157 } from "citty";
29055
29338
 
29056
29339
  // src/commands/tags/shared.ts
29057
29340
  function failApi3(err) {
@@ -29117,7 +29400,7 @@ async function listTags(json) {
29117
29400
  failApi3(err);
29118
29401
  }
29119
29402
  }
29120
- var listCommand10 = defineCommand155({
29403
+ var listCommand10 = defineCommand157({
29121
29404
  meta: {
29122
29405
  name: "list",
29123
29406
  description: "Effective tags for this chat (production + staged), with each tag's full readable config (secrets excluded) \u2014 reuse a stored value to pre-fill a change rather than asking the user. Refs printed here are what flow side-effect tagIds should use. Example: baker tags list"
@@ -29136,7 +29419,7 @@ async function listDraft3() {
29136
29419
  failApi3(err);
29137
29420
  }
29138
29421
  }
29139
- var draftCommand3 = defineCommand155({
29422
+ var draftCommand3 = defineCommand157({
29140
29423
  meta: {
29141
29424
  name: "draft",
29142
29425
  description: "Review the tag changes staged in this chat (read-only). Staged changes were approved via request_tag_input and apply when the chat is published; to amend or drop one, propose a follow-up change through the same tool (a delete on a tag_temp_* ref drops the staged create)."
@@ -29145,7 +29428,7 @@ var draftCommand3 = defineCommand155({
29145
29428
  await listDraft3();
29146
29429
  }
29147
29430
  });
29148
- var tagsCommand3 = defineCommand155({
29431
+ var tagsCommand3 = defineCommand157({
29149
29432
  meta: {
29150
29433
  name: "tags",
29151
29434
  description: `Read the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, \u2026) \u2014 production tags plus the changes staged in this chat.
@@ -29171,10 +29454,10 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
29171
29454
  });
29172
29455
 
29173
29456
  // src/commands/testimonials/index.ts
29174
- import { defineCommand as defineCommand159 } from "citty";
29457
+ import { defineCommand as defineCommand161 } from "citty";
29175
29458
 
29176
29459
  // src/commands/testimonials/get.ts
29177
- import { defineCommand as defineCommand156 } from "citty";
29460
+ import { defineCommand as defineCommand158 } from "citty";
29178
29461
  registerSchema({
29179
29462
  command: "testimonials.get",
29180
29463
  description: "Get a single testimonial by ID",
@@ -29182,7 +29465,7 @@ registerSchema({
29182
29465
  id: { type: "string", description: "Testimonial ID", required: true }
29183
29466
  }
29184
29467
  });
29185
- var getCommand4 = defineCommand156({
29468
+ var getCommand4 = defineCommand158({
29186
29469
  meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
29187
29470
  args: {
29188
29471
  id: { type: "positional", description: "Testimonial ID", required: false },
@@ -29219,7 +29502,7 @@ var getCommand4 = defineCommand156({
29219
29502
  });
29220
29503
 
29221
29504
  // src/commands/testimonials/list.ts
29222
- import { defineCommand as defineCommand157 } from "citty";
29505
+ import { defineCommand as defineCommand159 } from "citty";
29223
29506
  registerSchema({
29224
29507
  command: "testimonials.list",
29225
29508
  description: "List testimonials with optional filters.",
@@ -29249,7 +29532,7 @@ registerSchema({
29249
29532
  limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
29250
29533
  }
29251
29534
  });
29252
- var listCommand11 = defineCommand157({
29535
+ var listCommand11 = defineCommand159({
29253
29536
  meta: {
29254
29537
  name: "list",
29255
29538
  description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
@@ -29298,7 +29581,7 @@ var listCommand11 = defineCommand157({
29298
29581
  });
29299
29582
 
29300
29583
  // src/commands/testimonials/search.ts
29301
- import { defineCommand as defineCommand158 } from "citty";
29584
+ import { defineCommand as defineCommand160 } from "citty";
29302
29585
  function languageBiasHint(results, requestedLanguage) {
29303
29586
  if (requestedLanguage) {
29304
29587
  return null;
@@ -29376,7 +29659,7 @@ function buildSearchRequest(query, args) {
29376
29659
  }
29377
29660
  return body;
29378
29661
  }
29379
- var searchCommand2 = defineCommand158({
29662
+ var searchCommand2 = defineCommand160({
29380
29663
  meta: {
29381
29664
  name: "search",
29382
29665
  description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
@@ -29432,7 +29715,7 @@ var searchCommand2 = defineCommand158({
29432
29715
  var tagsCommand4 = makeTagsCommand("testimonials", "testimonial", "/api/testimonials/tags");
29433
29716
 
29434
29717
  // src/commands/testimonials/index.ts
29435
- var testimonialsCommand = defineCommand159({
29718
+ var testimonialsCommand = defineCommand161({
29436
29719
  meta: {
29437
29720
  name: "testimonials",
29438
29721
  description: `Find and browse testimonials in Baker. Subcommands: search, get, list, tags.
@@ -29454,10 +29737,10 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
29454
29737
  });
29455
29738
 
29456
29739
  // src/commands/videos/index.ts
29457
- import { defineCommand as defineCommand164 } from "citty";
29740
+ import { defineCommand as defineCommand166 } from "citty";
29458
29741
 
29459
29742
  // src/commands/videos/delete.ts
29460
- import { defineCommand as defineCommand160 } from "citty";
29743
+ import { defineCommand as defineCommand162 } from "citty";
29461
29744
  registerSchema({
29462
29745
  command: "videos.delete",
29463
29746
  description: "Delete a video by ID",
@@ -29471,7 +29754,7 @@ registerSchema({
29471
29754
  }
29472
29755
  }
29473
29756
  });
29474
- var deleteCommand3 = defineCommand160({
29757
+ var deleteCommand3 = defineCommand162({
29475
29758
  meta: {
29476
29759
  name: "delete",
29477
29760
  description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
@@ -29512,7 +29795,7 @@ var deleteCommand3 = defineCommand160({
29512
29795
  });
29513
29796
 
29514
29797
  // src/commands/videos/get.ts
29515
- import { defineCommand as defineCommand161 } from "citty";
29798
+ import { defineCommand as defineCommand163 } from "citty";
29516
29799
  registerSchema({
29517
29800
  command: "videos.get",
29518
29801
  description: "Get a single video by ID",
@@ -29520,7 +29803,7 @@ registerSchema({
29520
29803
  id: { type: "string", description: "Video ID", required: true }
29521
29804
  }
29522
29805
  });
29523
- var getCommand5 = defineCommand161({
29806
+ var getCommand5 = defineCommand163({
29524
29807
  meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
29525
29808
  args: {
29526
29809
  id: { type: "positional", description: "Video ID", required: false },
@@ -29557,7 +29840,7 @@ var getCommand5 = defineCommand161({
29557
29840
  });
29558
29841
 
29559
29842
  // src/commands/videos/search.ts
29560
- import { defineCommand as defineCommand162 } from "citty";
29843
+ import { defineCommand as defineCommand164 } from "citty";
29561
29844
  registerSchema({
29562
29845
  command: "videos.search",
29563
29846
  description: "Search videos by text query. Only returns ready videos.",
@@ -29567,7 +29850,7 @@ registerSchema({
29567
29850
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
29568
29851
  }
29569
29852
  });
29570
- var searchCommand3 = defineCommand162({
29853
+ var searchCommand3 = defineCommand164({
29571
29854
  meta: {
29572
29855
  name: "search",
29573
29856
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -29617,9 +29900,9 @@ var searchCommand3 = defineCommand162({
29617
29900
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
29618
29901
 
29619
29902
  // src/commands/videos/upload.ts
29620
- import { readFile as readFile22, stat as stat7 } from "fs/promises";
29903
+ import { readFile as readFile23, stat as stat7 } from "fs/promises";
29621
29904
  import { extname as extname3 } from "path";
29622
- import { defineCommand as defineCommand163 } from "citty";
29905
+ import { defineCommand as defineCommand165 } from "citty";
29623
29906
  var MIME_MAP = {
29624
29907
  ".mp4": "video/mp4",
29625
29908
  ".mov": "video/quicktime",
@@ -29653,7 +29936,7 @@ function detectContentType(filePath) {
29653
29936
  }
29654
29937
  return mime;
29655
29938
  }
29656
- var uploadCommand2 = defineCommand163({
29939
+ var uploadCommand2 = defineCommand165({
29657
29940
  meta: {
29658
29941
  name: "upload",
29659
29942
  description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
@@ -29682,7 +29965,7 @@ var uploadCommand2 = defineCommand163({
29682
29965
  return;
29683
29966
  }
29684
29967
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
29685
- const fileBuffer = await readFile22(filePath);
29968
+ const fileBuffer = await readFile23(filePath);
29686
29969
  const uploadResponse = await fetch(uploadUrl, {
29687
29970
  method: "PUT",
29688
29971
  headers: { "Content-Type": contentType },
@@ -29707,7 +29990,7 @@ var uploadCommand2 = defineCommand163({
29707
29990
  });
29708
29991
 
29709
29992
  // src/commands/videos/index.ts
29710
- var videosCommand = defineCommand164({
29993
+ var videosCommand = defineCommand166({
29711
29994
  meta: {
29712
29995
  name: "videos",
29713
29996
  description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
@@ -29731,10 +30014,10 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
29731
30014
  });
29732
30015
 
29733
30016
  // src/commands/winning-ads/index.ts
29734
- import { defineCommand as defineCommand177 } from "citty";
30017
+ import { defineCommand as defineCommand179 } from "citty";
29735
30018
 
29736
30019
  // src/commands/winning-ads/advertisers.ts
29737
- import { defineCommand as defineCommand165 } from "citty";
30020
+ import { defineCommand as defineCommand167 } from "citty";
29738
30021
 
29739
30022
  // src/commands/winning-ads/shared.ts
29740
30023
  function splitList(value) {
@@ -29787,7 +30070,7 @@ function advertiserNormalizer(record, full) {
29787
30070
  last_synced_at: record.last_synced_at ?? null
29788
30071
  };
29789
30072
  }
29790
- var advertisersCommand2 = defineCommand165({
30073
+ var advertisersCommand2 = defineCommand167({
29791
30074
  meta: {
29792
30075
  name: "advertisers",
29793
30076
  description: 'List corpus advertisers by name or domain. Find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id / winners. Example: baker winning-ads advertisers "Deel" --output md'
@@ -29845,7 +30128,7 @@ var advertisersCommand2 = defineCommand165({
29845
30128
  });
29846
30129
 
29847
30130
  // src/commands/winning-ads/brief.ts
29848
- import { defineCommand as defineCommand166 } from "citty";
30131
+ import { defineCommand as defineCommand168 } from "citty";
29849
30132
  registerSchema({
29850
30133
  command: "winning-ads.brief",
29851
30134
  description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
@@ -29891,7 +30174,7 @@ function parseDna(raw) {
29891
30174
  }
29892
30175
  return parsed;
29893
30176
  }
29894
- var briefCommand = defineCommand166({
30177
+ var briefCommand = defineCommand168({
29895
30178
  meta: {
29896
30179
  name: "brief",
29897
30180
  description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
@@ -29927,7 +30210,7 @@ var briefCommand = defineCommand166({
29927
30210
  });
29928
30211
 
29929
30212
  // src/commands/winning-ads/content.ts
29930
- import { defineCommand as defineCommand167 } from "citty";
30213
+ import { defineCommand as defineCommand169 } from "citty";
29931
30214
  registerSchema({
29932
30215
  command: "winning-ads.content",
29933
30216
  description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
@@ -29940,7 +30223,7 @@ registerSchema({
29940
30223
  }
29941
30224
  }
29942
30225
  });
29943
- var contentCommand = defineCommand167({
30226
+ var contentCommand = defineCommand169({
29944
30227
  meta: {
29945
30228
  name: "content",
29946
30229
  description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
@@ -29989,7 +30272,7 @@ var contentCommand = defineCommand167({
29989
30272
  });
29990
30273
 
29991
30274
  // src/commands/winning-ads/feed.ts
29992
- import { defineCommand as defineCommand168 } from "citty";
30275
+ import { defineCommand as defineCommand170 } from "citty";
29993
30276
  function buildFeedParams(input) {
29994
30277
  const params = {};
29995
30278
  const advertiser = splitList(input.advertiser);
@@ -30041,7 +30324,7 @@ registerSchema({
30041
30324
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
30042
30325
  }
30043
30326
  });
30044
- var feedCommand = defineCommand168({
30327
+ var feedCommand = defineCommand170({
30045
30328
  meta: {
30046
30329
  name: "feed",
30047
30330
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -30126,7 +30409,7 @@ var feedCommand = defineCommand168({
30126
30409
  });
30127
30410
 
30128
30411
  // src/commands/winning-ads/follow.ts
30129
- import { defineCommand as defineCommand169 } from "citty";
30412
+ import { defineCommand as defineCommand171 } from "citty";
30130
30413
  var PLATFORMS = ["meta", "linkedin"];
30131
30414
  registerSchema({
30132
30415
  command: "winning-ads.follow",
@@ -30141,7 +30424,7 @@ registerSchema({
30141
30424
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
30142
30425
  }
30143
30426
  });
30144
- var followCommand = defineCommand169({
30427
+ var followCommand = defineCommand171({
30145
30428
  meta: {
30146
30429
  name: "follow",
30147
30430
  description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
@@ -30188,7 +30471,7 @@ var followCommand = defineCommand169({
30188
30471
  });
30189
30472
 
30190
30473
  // src/commands/winning-ads/follow-competitors.ts
30191
- import { defineCommand as defineCommand170 } from "citty";
30474
+ import { defineCommand as defineCommand172 } from "citty";
30192
30475
  var PLATFORMS2 = ["meta", "linkedin"];
30193
30476
  var BATCH_TIMEOUT_MS = 3e5;
30194
30477
  function buildFollowBatchBody(input) {
@@ -30221,7 +30504,7 @@ registerSchema({
30221
30504
  }
30222
30505
  }
30223
30506
  });
30224
- var followCompetitorsCommand = defineCommand170({
30507
+ var followCompetitorsCommand = defineCommand172({
30225
30508
  meta: {
30226
30509
  name: "follow-competitors",
30227
30510
  description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
@@ -30296,7 +30579,7 @@ var followCompetitorsCommand = defineCommand170({
30296
30579
  });
30297
30580
 
30298
30581
  // src/commands/winning-ads/following.ts
30299
- import { defineCommand as defineCommand171 } from "citty";
30582
+ import { defineCommand as defineCommand173 } from "citty";
30300
30583
  registerSchema({
30301
30584
  command: "winning-ads.following",
30302
30585
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -30329,7 +30612,7 @@ function followingNormalizer(record, full) {
30329
30612
  platforms: Array.isArray(record.platforms) ? record.platforms : []
30330
30613
  };
30331
30614
  }
30332
- var followingCommand = defineCommand171({
30615
+ var followingCommand = defineCommand173({
30333
30616
  meta: {
30334
30617
  name: "following",
30335
30618
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -30364,7 +30647,7 @@ var followingCommand = defineCommand171({
30364
30647
  });
30365
30648
 
30366
30649
  // src/commands/winning-ads/patterns.ts
30367
- import { defineCommand as defineCommand172 } from "citty";
30650
+ import { defineCommand as defineCommand174 } from "citty";
30368
30651
  registerSchema({
30369
30652
  command: "winning-ads.patterns",
30370
30653
  description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
@@ -30403,7 +30686,7 @@ function discriminatorRow(record) {
30403
30686
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
30404
30687
  };
30405
30688
  }
30406
- var patternsCommand = defineCommand172({
30689
+ var patternsCommand = defineCommand174({
30407
30690
  meta: {
30408
30691
  name: "patterns",
30409
30692
  description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
@@ -30459,7 +30742,7 @@ var patternsCommand = defineCommand172({
30459
30742
  });
30460
30743
 
30461
30744
  // src/commands/winning-ads/search.ts
30462
- import { defineCommand as defineCommand173 } from "citty";
30745
+ import { defineCommand as defineCommand175 } from "citty";
30463
30746
  registerSchema({
30464
30747
  command: "winning-ads.search",
30465
30748
  description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
@@ -30567,7 +30850,7 @@ function buildSearchBody(args) {
30567
30850
  }
30568
30851
  return body;
30569
30852
  }
30570
- var searchCommand4 = defineCommand173({
30853
+ var searchCommand4 = defineCommand175({
30571
30854
  meta: {
30572
30855
  name: "search",
30573
30856
  description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
@@ -30682,7 +30965,7 @@ var searchCommand4 = defineCommand173({
30682
30965
  });
30683
30966
 
30684
30967
  // src/commands/winning-ads/seeds.ts
30685
- import { defineCommand as defineCommand174 } from "citty";
30968
+ import { defineCommand as defineCommand176 } from "citty";
30686
30969
  function leanRow(r) {
30687
30970
  return {
30688
30971
  key: r.key,
@@ -30710,7 +30993,7 @@ function makeSeedCommand(opts) {
30710
30993
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
30711
30994
  }
30712
30995
  });
30713
- return defineCommand174({
30996
+ return defineCommand176({
30714
30997
  meta: { name: opts.name, description: opts.description },
30715
30998
  args: {
30716
30999
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -30759,7 +31042,7 @@ var formatsCommand = makeSeedCommand({
30759
31042
  });
30760
31043
 
30761
31044
  // src/commands/winning-ads/unfollow.ts
30762
- import { defineCommand as defineCommand175 } from "citty";
31045
+ import { defineCommand as defineCommand177 } from "citty";
30763
31046
  registerSchema({
30764
31047
  command: "winning-ads.unfollow",
30765
31048
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -30767,7 +31050,7 @@ registerSchema({
30767
31050
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
30768
31051
  }
30769
31052
  });
30770
- var unfollowCommand = defineCommand175({
31053
+ var unfollowCommand = defineCommand177({
30771
31054
  meta: {
30772
31055
  name: "unfollow",
30773
31056
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -30788,7 +31071,7 @@ var unfollowCommand = defineCommand175({
30788
31071
  });
30789
31072
 
30790
31073
  // src/commands/winning-ads/winners.ts
30791
- import { defineCommand as defineCommand176 } from "citty";
31074
+ import { defineCommand as defineCommand178 } from "citty";
30792
31075
  registerSchema({
30793
31076
  command: "winning-ads.winners",
30794
31077
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -30798,7 +31081,7 @@ registerSchema({
30798
31081
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
30799
31082
  }
30800
31083
  });
30801
- var winnersCommand = defineCommand176({
31084
+ var winnersCommand = defineCommand178({
30802
31085
  meta: {
30803
31086
  name: "winners",
30804
31087
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -30848,7 +31131,7 @@ var winnersCommand = defineCommand176({
30848
31131
  });
30849
31132
 
30850
31133
  // src/commands/winning-ads/index.ts
30851
- var winningAdsCommand = defineCommand177({
31134
+ var winningAdsCommand = defineCommand179({
30852
31135
  meta: {
30853
31136
  name: "winning-ads",
30854
31137
  description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
@@ -30920,7 +31203,7 @@ function getCliVersion() {
30920
31203
  }
30921
31204
 
30922
31205
  // src/cli.ts
30923
- var main = defineCommand178({
31206
+ var main = defineCommand180({
30924
31207
  meta: {
30925
31208
  name: "baker",
30926
31209
  version: getCliVersion(),
@@ -30936,6 +31219,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
30936
31219
  actions: actionsCommand,
30937
31220
  "scheduled-actions": scheduledActionsCommand,
30938
31221
  ads: adsCommand2,
31222
+ brand: brandCommand,
30939
31223
  ga4: ga4Command,
30940
31224
  gsc: gscCommand,
30941
31225
  research: researchCommand,