@koda-sl/baker-cli 0.114.0-dev.249eaa8ed → 0.115.0-dev.3bcc79f9c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,18 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ BackendClient,
3
4
  ELEVENLABS_MAX_MUSIC_LENGTH_MS,
4
5
  IMAGE_GENERATE_MODELS,
5
6
  LayerExecutionError,
6
7
  MODEL_REGISTRY,
7
8
  SEEDANCE_DURATIONS,
8
9
  ValidationError,
10
+ collectAssetRefLikes,
9
11
  createEngineFromEnv,
10
12
  defaultRegistry,
11
13
  describeFailureReason,
12
14
  generateCatalog,
15
+ isPersistedAssetRef,
16
+ requireCredentialsFromEnv,
13
17
  resolveConcurrency,
18
+ sha256Hex,
19
+ ulid,
14
20
  validateCanvasDeep
15
- } from "./chunk-5AWO4BHJ.js";
21
+ } from "./chunk-OCMOQOIJ.js";
16
22
 
17
23
  // src/cli.ts
18
24
  import { defineCommand as defineCommand157, runMain } from "citty";
@@ -151,9 +157,9 @@ async function handleResponse(response) {
151
157
  throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
152
158
  }
153
159
  }
154
- async function apiGet(path12, params) {
160
+ async function apiGet(path14, params) {
155
161
  const env = getEnv();
156
- const url = new URL(path12, env.BAKER_API_URL);
162
+ const url = new URL(path14, env.BAKER_API_URL);
157
163
  if (params) {
158
164
  const clean = sanitizeParams(params);
159
165
  for (const [key, value] of Object.entries(clean)) {
@@ -178,12 +184,12 @@ async function apiGet(path12, params) {
178
184
  }
179
185
  return handleResponse(response);
180
186
  }
181
- async function apiPost(path12, body, opts) {
187
+ async function apiPost(path14, body, opts) {
182
188
  const env = getEnv();
183
189
  const timeoutMs = opts?.timeoutMs ?? 6e4;
184
190
  let response;
185
191
  try {
186
- response = await fetchWithRateLimitRetry(new URL(path12, env.BAKER_API_URL).toString(), {
192
+ response = await fetchWithRateLimitRetry(new URL(path14, env.BAKER_API_URL).toString(), {
187
193
  method: "POST",
188
194
  headers: {
189
195
  Authorization: `Bearer ${env.BAKER_API_KEY}`,
@@ -2875,31 +2881,31 @@ function cachePath(category, key) {
2875
2881
  return join2(dir, `${hashKey(key)}.json`);
2876
2882
  }
2877
2883
  function cacheGet(category, key) {
2878
- const path12 = cachePath(category, key);
2879
- if (!existsSync2(path12)) {
2884
+ const path14 = cachePath(category, key);
2885
+ if (!existsSync2(path14)) {
2880
2886
  return null;
2881
2887
  }
2882
2888
  try {
2883
- const raw = readFileSync2(path12, "utf-8");
2889
+ const raw = readFileSync2(path14, "utf-8");
2884
2890
  const entry = JSON.parse(raw);
2885
2891
  if (entry.expiresAt < Date.now()) {
2886
- rmSync(path12, { force: true });
2892
+ rmSync(path14, { force: true });
2887
2893
  return null;
2888
2894
  }
2889
2895
  return entry;
2890
2896
  } catch {
2891
- rmSync(path12, { force: true });
2897
+ rmSync(path14, { force: true });
2892
2898
  return null;
2893
2899
  }
2894
2900
  }
2895
2901
  function cacheSet(category, key, data, ttlMs, fields) {
2896
- const path12 = cachePath(category, key);
2902
+ const path14 = cachePath(category, key);
2897
2903
  const entry = {
2898
2904
  expiresAt: Date.now() + ttlMs,
2899
2905
  data,
2900
2906
  fields
2901
2907
  };
2902
- writeFileSync(path12, JSON.stringify(entry), "utf-8");
2908
+ writeFileSync(path14, JSON.stringify(entry), "utf-8");
2903
2909
  }
2904
2910
  var HOUR = 60 * 60 * 1e3;
2905
2911
  var MINUTE = 60 * 1e3;
@@ -4226,7 +4232,11 @@ var responsiveDisplayAdSchema = z8.object({
4226
4232
  longHeadline: z8.object({ text: z8.string().min(1).max(GOOGLE_ADS_LIMITS.responsiveDisplayAd.longHeadlineTextMax) }),
4227
4233
  descriptions: z8.array(z8.object({ text: z8.string().min(1).max(GOOGLE_ADS_LIMITS.responsiveDisplayAd.descriptionTextMax) })).min(1).max(5),
4228
4234
  businessName: z8.string().min(1).max(GOOGLE_ADS_LIMITS.responsiveDisplayAd.businessNameMax),
4235
+ // A Responsive Display Ad's images are fields on the ad's own content (never campaign-level
4236
+ // asset links). Google requires ≥1 landscape marketing image (1.91:1) AND ≥1 square marketing
4237
+ // image (1:1) to serve; the logo images are optional.
4229
4238
  marketingImageAssets: z8.array(refSchema).optional(),
4239
+ squareMarketingImageAssets: z8.array(refSchema).optional(),
4230
4240
  logoImageAssets: z8.array(refSchema).optional(),
4231
4241
  finalUrls: z8.array(httpsUrlSchema2).min(1)
4232
4242
  });
@@ -4411,7 +4421,11 @@ var adScheduleCriterionSchema = z8.object({
4411
4421
  var deviceCriterionSchema = z8.object({
4412
4422
  criterionType: z8.literal("device"),
4413
4423
  device: z8.enum(DEVICE_TYPES),
4414
- bidModifier: z8.number().min(0.1).max(10).optional()
4424
+ // Google's `CampaignCriterion.bid_modifier`: "The modifier must be in the range 0.1 - 10.0. Use 0
4425
+ // to opt out of a Device type." So 0 (exclude the device) and 0.1–10.0 are valid; the (0, 0.1) gap is not.
4426
+ bidModifier: z8.number().min(0).max(10).optional().refine((v) => v === void 0 || v === 0 || v >= 0.1, {
4427
+ message: "bid modifier must be 0 (exclude the device) or between 0.1 and 10.0"
4428
+ })
4415
4429
  });
4416
4430
  var campaignCriterionAddSchema = z8.object({
4417
4431
  campaign: refSchema,
@@ -4422,6 +4436,15 @@ var campaignCriterionAddSchema = z8.object({
4422
4436
  adScheduleCriterionSchema,
4423
4437
  deviceCriterionSchema
4424
4438
  ])
4439
+ }).superRefine((val, ctx) => {
4440
+ const c = val.criterion;
4441
+ if (c.criterionType === "adSchedule" && c.endHour === 24 && c.endMinute !== "ZERO") {
4442
+ ctx.addIssue({
4443
+ code: z8.ZodIssueCode.custom,
4444
+ message: "endHour 24 (midnight) cannot have a non-zero endMinute",
4445
+ path: ["criterion", "endMinute"]
4446
+ });
4447
+ }
4425
4448
  });
4426
4449
  var GOOGLE_DRAFT_OP_KINDS = [
4427
4450
  "google.budget.create",
@@ -4595,7 +4618,8 @@ var googleDraftStatusNodeSchema = z9.lazy(
4595
4618
  operation: googleDraftChangeOperationSchema.optional(),
4596
4619
  existing: z9.boolean(),
4597
4620
  collections: z9.array(googleDraftStatusCollectionSchema),
4598
- children: z9.array(googleDraftStatusNodeSchema)
4621
+ children: z9.array(googleDraftStatusNodeSchema),
4622
+ warnings: z9.array(z9.string()).optional()
4599
4623
  })
4600
4624
  );
4601
4625
  var googleDraftListResponseSchema = z9.object({
@@ -4663,6 +4687,9 @@ function collectionLine(collection) {
4663
4687
  function renderNode(node, depth, lines) {
4664
4688
  const indent = " ".repeat(depth);
4665
4689
  lines.push(`${indent}\u2022 ${node.name} \xB7 ${badge(node)}`);
4690
+ for (const warning of node.warnings ?? []) {
4691
+ lines.push(`${indent} \u26A0 ${warning}`);
4692
+ }
4666
4693
  for (const collection of node.collections) {
4667
4694
  const line = collectionLine(collection);
4668
4695
  if (line) {
@@ -4791,11 +4818,11 @@ function rawTextEntries(value) {
4791
4818
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
4792
4819
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
4793
4820
  }
4794
- function rawFileEntries(path12) {
4795
- if (typeof path12 !== "string" || path12.length === 0) {
4821
+ function rawFileEntries(path14) {
4822
+ if (typeof path14 !== "string" || path14.length === 0) {
4796
4823
  return [];
4797
4824
  }
4798
- return readFileSync3(path12, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4825
+ return readFileSync3(path14, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4799
4826
  }
4800
4827
  function keywordEntries(args) {
4801
4828
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -4818,19 +4845,19 @@ function keywordEntries(args) {
4818
4845
  }
4819
4846
  return entries;
4820
4847
  }
4821
- function loadJsonFileArg(path12) {
4822
- if (typeof path12 !== "string" || path12.length === 0) {
4848
+ function loadJsonFileArg(path14) {
4849
+ if (typeof path14 !== "string" || path14.length === 0) {
4823
4850
  return {};
4824
4851
  }
4825
4852
  try {
4826
- const parsed = JSON.parse(readFileSync3(path12, "utf8"));
4853
+ const parsed = JSON.parse(readFileSync3(path14, "utf8"));
4827
4854
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4828
- failWriteValidation(`${path12} must contain a JSON object`);
4855
+ failWriteValidation(`${path14} must contain a JSON object`);
4829
4856
  }
4830
4857
  return parsed;
4831
4858
  } catch (err) {
4832
4859
  if (err instanceof SyntaxError) {
4833
- failWriteValidation(`${path12} is not valid JSON: ${err.message}`);
4860
+ failWriteValidation(`${path14} is not valid JSON: ${err.message}`);
4834
4861
  }
4835
4862
  throw err;
4836
4863
  }
@@ -4919,10 +4946,10 @@ async function stageUpdate(kind, customerId, target, payload) {
4919
4946
  async function stageTarget(kind, customerId, target) {
4920
4947
  await stageGoogleOp({ kind, customerId, target });
4921
4948
  }
4922
- async function draftAction(path12, body) {
4949
+ async function draftAction(path14, body) {
4923
4950
  try {
4924
4951
  const chatId = requireChatId();
4925
- const response = await apiPost(path12, { chatId, ...body });
4952
+ const response = await apiPost(path14, { chatId, ...body });
4926
4953
  writeJsonEnvelope(response);
4927
4954
  } catch (err) {
4928
4955
  handleGoogleError(err);
@@ -5798,8 +5825,9 @@ function applyAutoFixes(query, limit) {
5798
5825
  if (/FROM\s+campaign_budget\b/i.test(corrected)) {
5799
5826
  const whereClause = corrected.split(/\bWHERE\b/i)[1] ?? "";
5800
5827
  const selectClause = corrected.split(/\bFROM\b/i)[0] ?? "";
5828
+ const selectFields = new Set(selectClause.match(/campaign\.[\w.]+/g) ?? []);
5801
5829
  const missing = [...new Set(whereClause.match(/campaign\.[\w.]+/g) ?? [])].filter(
5802
- (field) => !selectClause.includes(field)
5830
+ (field) => !selectFields.has(field)
5803
5831
  );
5804
5832
  if (missing.length > 0) {
5805
5833
  corrected = corrected.replace(/SELECT\s+/i, `SELECT ${missing.join(", ")}, `);
@@ -6654,6 +6682,52 @@ function rsaContentFromFlags(args) {
6654
6682
  finalUrls
6655
6683
  };
6656
6684
  }
6685
+ function rdaContentFromFlags(args, base) {
6686
+ const content = {
6687
+ format: "responsiveDisplay",
6688
+ ...base !== null && typeof base === "object" && !Array.isArray(base) ? base : {}
6689
+ };
6690
+ const headlines = listFlag(args.headlines);
6691
+ if (headlines) {
6692
+ content.headlines = headlines.map((text) => ({ text }));
6693
+ }
6694
+ const descriptions = listFlag(args.descriptions);
6695
+ if (descriptions) {
6696
+ content.descriptions = descriptions.map((text) => ({ text }));
6697
+ }
6698
+ if (typeof args["long-headline"] === "string") {
6699
+ content.longHeadline = { text: args["long-headline"] };
6700
+ }
6701
+ if (typeof args["business-name"] === "string") {
6702
+ content.businessName = args["business-name"];
6703
+ }
6704
+ const finalUrls = listFlag(args["final-url"]);
6705
+ if (finalUrls) {
6706
+ content.finalUrls = finalUrls;
6707
+ }
6708
+ const marketing = listFlag(args["marketing-images"]);
6709
+ if (marketing) {
6710
+ content.marketingImageAssets = marketing;
6711
+ }
6712
+ const square = listFlag(args["square-marketing-images"]);
6713
+ if (square) {
6714
+ content.squareMarketingImageAssets = square;
6715
+ }
6716
+ const logos = listFlag(args["logo-images"]);
6717
+ if (logos) {
6718
+ content.logoImageAssets = logos;
6719
+ }
6720
+ return content;
6721
+ }
6722
+ function adContentFromFlags(args, format, fileContent) {
6723
+ if (format === "responsiveSearch") {
6724
+ return fileContent ?? rsaContentFromFlags(args);
6725
+ }
6726
+ if (format === "responsiveDisplay") {
6727
+ return rdaContentFromFlags(args, fileContent);
6728
+ }
6729
+ return fileContent ?? failWriteValidation(`--format ${format} needs --file with the ad content`);
6730
+ }
6657
6731
  var adsCommand = defineCommand30({
6658
6732
  meta: { name: "ads", description: "Stage ad create/update/pause/resume/remove" },
6659
6733
  subCommands: {
@@ -6670,10 +6744,21 @@ var adsCommand = defineCommand30({
6670
6744
  type: "string",
6671
6745
  description: "responsiveSearch (default) | responsiveDisplay | performanceMaxAssetGroup | call | app | video | demandGen"
6672
6746
  },
6673
- headlines: { type: "string", description: "Comma-separated headlines (RSA)" },
6674
- descriptions: { type: "string", description: "Comma-separated descriptions (RSA)" },
6747
+ headlines: { type: "string", description: "Comma-separated headlines (RSA/RDA)" },
6748
+ descriptions: { type: "string", description: "Comma-separated descriptions (RSA/RDA)" },
6675
6749
  path1: { type: "string" },
6676
6750
  path2: { type: "string" },
6751
+ "long-headline": { type: "string", description: "Long headline (responsiveDisplay)" },
6752
+ "business-name": { type: "string", description: "Business name (responsiveDisplay)" },
6753
+ "marketing-images": {
6754
+ type: "string",
6755
+ description: "Comma-separated marketing image asset refs (responsiveDisplay)"
6756
+ },
6757
+ "square-marketing-images": {
6758
+ type: "string",
6759
+ description: "Comma-separated square marketing image asset refs (responsiveDisplay)"
6760
+ },
6761
+ "logo-images": { type: "string", description: "Comma-separated logo image asset refs (responsiveDisplay)" },
6677
6762
  "final-url": { type: "string", description: "Comma-separated final URLs" },
6678
6763
  status: { type: "string" }
6679
6764
  },
@@ -6681,7 +6766,7 @@ var adsCommand = defineCommand30({
6681
6766
  const customerId = requireCustomerId(args);
6682
6767
  const file = loadJsonFileArg(args.file);
6683
6768
  const format = args.format ?? "responsiveSearch";
6684
- const content = file.content ?? (format === "responsiveSearch" ? rsaContentFromFlags(args) : failWriteValidation(`--format ${format} needs --file with the ad content`));
6769
+ const content = adContentFromFlags(args, format, file.content);
6685
6770
  await stageCreate("google.ad.create", customerId, {
6686
6771
  adGroup: requireStringFlag(args["ad-group-ref"] ?? file.adGroup, "--ad-group-ref"),
6687
6772
  status: args.status ?? file.status,
@@ -8405,19 +8490,19 @@ function failWriteValidation2(message) {
8405
8490
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8406
8491
  process.exit(1);
8407
8492
  }
8408
- function loadJsonFileArg2(path12) {
8409
- if (typeof path12 !== "string" || path12.length === 0) {
8493
+ function loadJsonFileArg2(path14) {
8494
+ if (typeof path14 !== "string" || path14.length === 0) {
8410
8495
  return {};
8411
8496
  }
8412
8497
  try {
8413
- const parsed = JSON.parse(readFileSync7(path12, "utf8"));
8498
+ const parsed = JSON.parse(readFileSync7(path14, "utf8"));
8414
8499
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8415
- failWriteValidation2(`${path12} must contain a JSON object`);
8500
+ failWriteValidation2(`${path14} must contain a JSON object`);
8416
8501
  }
8417
8502
  return parsed;
8418
8503
  } catch (err) {
8419
8504
  if (err instanceof SyntaxError) {
8420
- failWriteValidation2(`${path12} is not valid JSON: ${err.message}`);
8505
+ failWriteValidation2(`${path14} is not valid JSON: ${err.message}`);
8421
8506
  }
8422
8507
  throw err;
8423
8508
  }
@@ -8480,15 +8565,15 @@ function parseLocaleFlag(value) {
8480
8565
  }
8481
8566
  return { language: match[1], country: match[2].toUpperCase() };
8482
8567
  }
8483
- function loadTargetingFileArg(path12) {
8484
- if (typeof path12 !== "string" || path12.length === 0) {
8568
+ function loadTargetingFileArg(path14) {
8569
+ if (typeof path14 !== "string" || path14.length === 0) {
8485
8570
  return void 0;
8486
8571
  }
8487
- const parsed = loadJsonFileArg2(path12);
8572
+ const parsed = loadJsonFileArg2(path14);
8488
8573
  const criteria = parsed.targetingCriteria ?? parsed;
8489
8574
  if (!criteria.include) {
8490
8575
  failWriteValidation2(
8491
- `${path12} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8576
+ `${path14} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8492
8577
  );
8493
8578
  }
8494
8579
  return criteria;
@@ -8523,14 +8608,14 @@ function parseCsvLine(line) {
8523
8608
  cells.push(current);
8524
8609
  return cells.map((cell) => cell.trim());
8525
8610
  }
8526
- function parseListFileArg(path12, maxRows) {
8527
- if (typeof path12 !== "string" || path12.length === 0) {
8611
+ function parseListFileArg(path14, maxRows) {
8612
+ if (typeof path14 !== "string" || path14.length === 0) {
8528
8613
  return void 0;
8529
8614
  }
8530
- const raw = readFileSync7(path12, "utf8");
8615
+ const raw = readFileSync7(path14, "utf8");
8531
8616
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
8532
8617
  if (lines.length < 2) {
8533
- failWriteValidation2(`${path12} needs a header row and at least one data row`);
8618
+ failWriteValidation2(`${path14} needs a header row and at least one data row`);
8534
8619
  }
8535
8620
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
8536
8621
  const rows = [];
@@ -8549,7 +8634,7 @@ function parseListFileArg(path12, maxRows) {
8549
8634
  }
8550
8635
  }
8551
8636
  if (rows.length > maxRows) {
8552
- failWriteValidation2(`${path12} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8637
+ failWriteValidation2(`${path14} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8553
8638
  }
8554
8639
  return { columns, rows };
8555
8640
  }
@@ -12752,7 +12837,7 @@ async function probeDuration(filePath) {
12752
12837
 
12753
12838
  // src/commands/canvas/run.ts
12754
12839
  import { readFile as readFile2 } from "fs/promises";
12755
- import path4 from "path";
12840
+ import path5 from "path";
12756
12841
  import { defineCommand as defineCommand85 } from "citty";
12757
12842
 
12758
12843
  // src/commands/canvas/placeholders.ts
@@ -12796,9 +12881,102 @@ function isResolvableRelative(value) {
12796
12881
  return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
12797
12882
  }
12798
12883
 
12884
+ // src/commands/canvas/run-record.ts
12885
+ import path3 from "path";
12886
+ var MAX_RUN_NODES = 200;
12887
+ var MAX_OUTPUTS_PER_NODE = 10;
12888
+ var MAX_FINAL_OUTPUTS = 10;
12889
+ var MAX_CREATIVE_SLUG_LENGTH = 100;
12890
+ function creativeSlugFromCanvasPath(filePath) {
12891
+ const normalized = filePath.split(path3.sep).join("/");
12892
+ const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
12893
+ const slug = match?.[1] ?? null;
12894
+ return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
12895
+ }
12896
+ var OUTPUT_KINDS = /* @__PURE__ */ new Set(["image", "video", "audio", "json", "text", "font"]);
12897
+ function toRecordOutput(slot, value) {
12898
+ const refs = collectAssetRefLikes(value);
12899
+ const ref = refs.length === 1 ? refs[0] : null;
12900
+ if (!ref || !isPersistedAssetRef(ref)) return null;
12901
+ const kind = typeof ref.kind === "string" && OUTPUT_KINDS.has(ref.kind) ? ref.kind : null;
12902
+ if (!kind) return null;
12903
+ return {
12904
+ slot,
12905
+ kind,
12906
+ sha256: ref.sha256,
12907
+ url: ref.url,
12908
+ mime: ref.mime,
12909
+ width: typeof ref.width === "number" ? ref.width : void 0,
12910
+ height: typeof ref.height === "number" ? ref.height : void 0,
12911
+ durationMs: typeof ref.duration_ms === "number" ? ref.duration_ms : void 0
12912
+ };
12913
+ }
12914
+ function nodeOutputsToRecord(nodeOutputs) {
12915
+ const out = [];
12916
+ for (const [slot, value] of Object.entries(nodeOutputs)) {
12917
+ if (Array.isArray(value)) {
12918
+ value.forEach((item, i) => {
12919
+ const rec = toRecordOutput(`${slot}#${i}`, item);
12920
+ if (rec) out.push(rec);
12921
+ });
12922
+ } else {
12923
+ const rec = toRecordOutput(slot, value);
12924
+ if (rec) out.push(rec);
12925
+ }
12926
+ }
12927
+ return out.slice(0, MAX_OUTPUTS_PER_NODE);
12928
+ }
12929
+ function finalOutputsToRecord(output) {
12930
+ if (Array.isArray(output)) {
12931
+ return output.map((item, i) => toRecordOutput(`final#${i}`, item)).filter((rec2) => rec2 !== null).slice(0, MAX_FINAL_OUTPUTS);
12932
+ }
12933
+ const rec = toRecordOutput("final", output);
12934
+ return rec ? [rec] : [];
12935
+ }
12936
+ function buildRunRecord(result, meta) {
12937
+ const nodes = result.node_runs.slice(0, MAX_RUN_NODES).map((run) => ({
12938
+ nodeId: run.node_id,
12939
+ nodeType: run.node_type,
12940
+ cached: run.cached,
12941
+ credits: run.credits,
12942
+ durationMs: run.duration_ms,
12943
+ outputs: nodeOutputsToRecord(result.outputs_by_node[run.node_id] ?? {})
12944
+ }));
12945
+ const finalOutputs = finalOutputsToRecord(result.output);
12946
+ return {
12947
+ runId: result.run_id,
12948
+ creativeSlug: meta.creativeSlug,
12949
+ canvasPath: meta.canvasPath,
12950
+ canvasSha: meta.canvasSha,
12951
+ chatId: meta.chatId,
12952
+ status: "completed",
12953
+ stats: {
12954
+ totalNodes: result.stats.total_nodes,
12955
+ cachedNodes: result.stats.cached_nodes,
12956
+ totalCredits: result.stats.total_credits,
12957
+ durationMs: result.stats.duration_ms
12958
+ },
12959
+ nodes,
12960
+ finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0
12961
+ };
12962
+ }
12963
+ function buildFailedRunRecord(runId, errorMessage, meta) {
12964
+ return {
12965
+ runId,
12966
+ creativeSlug: meta.creativeSlug,
12967
+ canvasPath: meta.canvasPath,
12968
+ canvasSha: meta.canvasSha,
12969
+ chatId: meta.chatId,
12970
+ status: "failed",
12971
+ errorMessage: errorMessage.slice(0, 2e3),
12972
+ stats: { totalNodes: 0, cachedNodes: 0, totalCredits: 0, durationMs: 0 },
12973
+ nodes: []
12974
+ };
12975
+ }
12976
+
12799
12977
  // src/commands/canvas/run-retention.ts
12800
12978
  import { rm } from "fs/promises";
12801
- import path3 from "path";
12979
+ import path4 from "path";
12802
12980
  function runDirsToPrune(entries, keep, currentRunId) {
12803
12981
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
12804
12982
  if (keep <= 0) return runs;
@@ -12815,7 +12993,7 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
12815
12993
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
12816
12994
  if (toPrune.length === 0) return;
12817
12995
  for (const dir of toPrune) {
12818
- await rm(path3.join(outputsDir, dir), { recursive: true, force: true }).catch(
12996
+ await rm(path4.join(outputsDir, dir), { recursive: true, force: true }).catch(
12819
12997
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
12820
12998
  );
12821
12999
  }
@@ -12835,13 +13013,29 @@ var runCommand = defineCommand85({
12835
13013
  type: "string",
12836
13014
  description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
12837
13015
  },
13016
+ parallel: {
13017
+ type: "string",
13018
+ description: "Alias for --concurrency: independent clips (and every other same-layer node) already fan out in parallel up to this bound"
13019
+ },
12838
13020
  "keep-runs": {
12839
13021
  type: "string",
12840
13022
  description: "After the run, prune old r_* run dirs, keeping the N newest (off by default)"
13023
+ },
13024
+ "remote-cache": {
13025
+ type: "string",
13026
+ description: "on | off \u2014 company-scoped remote cache + durable asset persistence (default on; env BAKER_CANVAS_REMOTE_CACHE)"
13027
+ },
13028
+ // citty consumes any `--no-<flag>` as a negation of `<flag>`, so the
13029
+ // opt-out spelling `--no-record` requires the flag to be named `record`
13030
+ // (a literal "no-record" arg would never receive a value).
13031
+ record: {
13032
+ type: "boolean",
13033
+ default: true,
13034
+ description: "Post the durable run-history record to Baker (disable with --no-record)"
12841
13035
  }
12842
13036
  },
12843
13037
  async run({ args }) {
12844
- const filePath = path4.resolve(String(args.file));
13038
+ const filePath = path5.resolve(String(args.file));
12845
13039
  const raw = await readFile2(filePath, "utf8");
12846
13040
  let parsed;
12847
13041
  try {
@@ -12852,7 +13046,7 @@ var runCommand = defineCommand85({
12852
13046
  `);
12853
13047
  process.exit(2);
12854
13048
  }
12855
- parsed = resolveRelativeCanvasPaths(parsed, path4.dirname(filePath));
13049
+ parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
12856
13050
  const pending = unsuppliedPlaceholderAssets(parsed);
12857
13051
  if (pending.length > 0) {
12858
13052
  process.stderr.write(
@@ -12872,25 +13066,37 @@ var runCommand = defineCommand85({
12872
13066
  );
12873
13067
  process.exit(2);
12874
13068
  }
13069
+ const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
12875
13070
  const engine = createEngineFromEnv({
12876
13071
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
12877
13072
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
12878
13073
  log: (line) => process.stdout.write(`${line}
12879
- `)
13074
+ `),
13075
+ remoteCache
12880
13076
  });
13077
+ const runId = args["run-id"] ? String(args["run-id"]) : `r_${ulid()}`;
13078
+ const recordMeta = {
13079
+ creativeSlug: creativeSlugFromCanvasPath(filePath) ?? void 0,
13080
+ canvasPath: path5.relative(process.cwd(), filePath) || void 0,
13081
+ canvasSha: sha256Hex(Buffer.from(raw)),
13082
+ chatId: getEnv().BAKER_CHAT_ID || void 0
13083
+ };
13084
+ const record = args.record === false ? null : buildRecorder();
12881
13085
  try {
12882
13086
  const policy = args["cache-policy"] ?? "read_write";
12883
13087
  const result = await engine.run(parsed, {
12884
- run_id: args["run-id"] ? String(args["run-id"]) : void 0,
13088
+ run_id: runId,
12885
13089
  cache_policy: policy,
12886
13090
  concurrency: resolveConcurrency(
12887
- args.concurrency !== void 0 ? String(args.concurrency) : void 0,
13091
+ // --concurrency wins; --parallel is the discoverable alias for the same bound.
13092
+ (args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
12888
13093
  process.env.BAKER_CANVAS_CONCURRENCY
12889
13094
  )
12890
13095
  });
13096
+ if (record) await record(buildRunRecord(result, recordMeta));
12891
13097
  const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
12892
13098
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
12893
- const outputsDir = args["outputs-dir"] ? path4.resolve(String(args["outputs-dir"])) : path4.resolve("canvas");
13099
+ const outputsDir = args["outputs-dir"] ? path5.resolve(String(args["outputs-dir"])) : path5.resolve("canvas");
12894
13100
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
12895
13101
  `));
12896
13102
  }
@@ -12918,6 +13124,7 @@ var runCommand = defineCommand85({
12918
13124
  }
12919
13125
  if (e instanceof LayerExecutionError) {
12920
13126
  const failures = e.failures.map((f) => ({ node_id: f.nodeId, message: describeFailureReason(f.reason) }));
13127
+ if (record) await record(buildFailedRunRecord(runId, e.message, recordMeta));
12921
13128
  process.stderr.write(
12922
13129
  `${JSON.stringify({ ok: false, error: { code: "runtime", message: e.message, failures } }, null, 2)}
12923
13130
  `
@@ -12925,16 +13132,30 @@ var runCommand = defineCommand85({
12925
13132
  process.exit(1);
12926
13133
  }
12927
13134
  const msg = e instanceof Error ? e.message : String(e);
13135
+ if (record) await record(buildFailedRunRecord(runId, msg, recordMeta));
12928
13136
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "runtime", message: msg } }, null, 2)}
12929
13137
  `);
12930
13138
  process.exit(1);
12931
13139
  }
12932
13140
  }
12933
13141
  });
13142
+ function buildRecorder() {
13143
+ return async (payload) => {
13144
+ try {
13145
+ const creds = requireCredentialsFromEnv();
13146
+ const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
13147
+ await client.recordRun(payload);
13148
+ } catch (e) {
13149
+ const msg = e instanceof Error ? e.message : String(e);
13150
+ process.stderr.write(`[warn] run record not persisted (${msg})
13151
+ `);
13152
+ }
13153
+ };
13154
+ }
12934
13155
 
12935
13156
  // src/commands/canvas/scaffold-static-ad.ts
12936
- import { readFile as readFile3, writeFile } from "fs/promises";
12937
- import path6 from "path";
13157
+ import { access, cp, mkdir, readFile as readFile3, writeFile } from "fs/promises";
13158
+ import path8 from "path";
12938
13159
  import { defineCommand as defineCommand86 } from "citty";
12939
13160
 
12940
13161
  // src/engine/scaffold/staticAd.ts
@@ -13120,18 +13341,106 @@ function staticAdReport(input, elementsInput, opts) {
13120
13341
  };
13121
13342
  }
13122
13343
 
13344
+ // src/commands/canvas/creative-definition.ts
13345
+ import path6 from "path";
13346
+ var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
13347
+ var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
13348
+ function titleFromSlug(slug) {
13349
+ const title = slug.split("-").filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
13350
+ return title || slug;
13351
+ }
13352
+ function resolvePlatform(platform) {
13353
+ const value = platform?.trim();
13354
+ return value && PLATFORM_VALUES.includes(value) ? value : "meta";
13355
+ }
13356
+ function resolveFormats(aspect) {
13357
+ const value = aspect?.trim();
13358
+ return value && FORMAT_VALUES.includes(value) ? [value] : ["4:5"];
13359
+ }
13360
+ function referenceRelativePath(kind, ext) {
13361
+ const name = kind === "video" ? "source" : "original";
13362
+ return `references/${name}${ext}`;
13363
+ }
13364
+ function sourceExtension(source, isUrl, kind) {
13365
+ const raw = isUrl ? urlPathname(source) : source;
13366
+ const ext = path6.extname(raw).toLowerCase();
13367
+ if (/^\.[a-z0-9]{1,5}$/.test(ext)) return ext;
13368
+ return kind === "video" ? ".mp4" : ".jpg";
13369
+ }
13370
+ function urlPathname(source) {
13371
+ try {
13372
+ return new URL(source).pathname;
13373
+ } catch {
13374
+ return source;
13375
+ }
13376
+ }
13377
+ function describeBlueprintIntent(blueprint) {
13378
+ const intent = blueprint?.ad_intent;
13379
+ if (typeof intent === "string" && intent.trim()) return intent.trim();
13380
+ if (intent && typeof intent === "object") {
13381
+ const summary = intent.summary ?? intent.feeling;
13382
+ if (typeof summary === "string" && summary.trim()) return summary.trim();
13383
+ }
13384
+ return void 0;
13385
+ }
13386
+ function yamlScalar(value) {
13387
+ return JSON.stringify(value);
13388
+ }
13389
+ function buildCreativeDefinition(input) {
13390
+ const lines = ["---", `title: ${yamlScalar(input.title)}`, `kind: ${input.kind}`, `platform: ${input.platform}`];
13391
+ lines.push(`formats: [${input.formats.map(yamlScalar).join(", ")}]`);
13392
+ lines.push(`status: ${input.status ?? "draft"}`);
13393
+ if (input.sourceReferenceUrl) lines.push(`sourceReferenceUrl: ${yamlScalar(input.sourceReferenceUrl)}`);
13394
+ if (input.sourceAdvertiser) lines.push(`sourceAdvertiser: ${yamlScalar(input.sourceAdvertiser)}`);
13395
+ if (input.sourceKind) lines.push(`sourceKind: ${input.sourceKind}`);
13396
+ if (input.sourcePath) lines.push(`sourcePath: ${yamlScalar(input.sourcePath)}`);
13397
+ lines.push("---", "");
13398
+ lines.push(input.description?.trim() || `${input.title} \u2014 canvas-built ${input.kind} ad for ${input.platform}.`);
13399
+ lines.push("");
13400
+ return lines.join("\n");
13401
+ }
13402
+
13123
13403
  // src/commands/canvas/scaffold-static-ad-paths.ts
13124
- import path5 from "path";
13125
- function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd()) {
13404
+ import path7 from "path";
13405
+ function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
13126
13406
  const file = rawFile.trim();
13127
13407
  const imageIsUrl = /^https?:\/\//i.test(file);
13128
- const imageSource = imageIsUrl ? file : path5.resolve(cwd, file);
13129
- const outPath = out ? path5.resolve(cwd, out) : imageIsUrl ? path5.join(cwd, "static-ad.canvas.json") : path5.join(path5.dirname(imageSource), "static-ad.canvas.json");
13130
- const blueprintPath = path5.join(path5.dirname(outPath), "prompt.json");
13131
- return { imageIsUrl, imageSource, outPath, blueprintPath };
13408
+ const imageSource = imageIsUrl ? file : path7.resolve(cwd, file);
13409
+ const outPath = out ? path7.resolve(cwd, out) : slug ? path7.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path7.join(cwd, "static-ad.canvas.json") : path7.join(path7.dirname(imageSource), "static-ad.canvas.json");
13410
+ const blueprintPath = path7.join(path7.dirname(outPath), "prompt.json");
13411
+ const creativeDir = slug ? path7.dirname(outPath) : null;
13412
+ const definitionPath = creativeDir ? path7.join(creativeDir, "_definition.md") : null;
13413
+ const referencesDir = creativeDir ? path7.join(creativeDir, "references") : null;
13414
+ return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
13415
+ }
13416
+ var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
13417
+ var SCAFFOLD_SLUG_MAX_LENGTH = 100;
13418
+ function isValidScaffoldSlug(slug) {
13419
+ return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
13132
13420
  }
13133
13421
 
13134
13422
  // src/commands/canvas/scaffold-static-ad.ts
13423
+ async function fileExists(target) {
13424
+ try {
13425
+ await access(target);
13426
+ return true;
13427
+ } catch {
13428
+ return false;
13429
+ }
13430
+ }
13431
+ async function copySourceIntoReferences(source, isUrl, referencesDir) {
13432
+ await mkdir(referencesDir, { recursive: true });
13433
+ const relPath = referenceRelativePath("image", sourceExtension(source, isUrl, "image"));
13434
+ const dest = path8.join(referencesDir, path8.basename(relPath));
13435
+ if (isUrl) {
13436
+ const res = await fetch(source);
13437
+ if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
13438
+ await writeFile(dest, Buffer.from(await res.arrayBuffer()));
13439
+ } else {
13440
+ await cp(source, dest);
13441
+ }
13442
+ return relPath;
13443
+ }
13135
13444
  function resolveModel(kind, preferred) {
13136
13445
  const ids = Object.keys(MODEL_REGISTRY[kind]);
13137
13446
  return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
@@ -13291,6 +13600,16 @@ var scaffoldStaticAdCommand = defineCommand86({
13291
13600
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
13292
13601
  context: { type: "string", description: "Known provenance (advertiser, category, market) to ground the describe" },
13293
13602
  out: { type: "string", description: "Output canvas path (default <image-dir>/static-ad.canvas.json)" },
13603
+ slug: {
13604
+ type: "string",
13605
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
13606
+ },
13607
+ title: { type: "string", description: "Creative title for _definition.md (default: title-cased slug)" },
13608
+ platform: {
13609
+ type: "string",
13610
+ description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
13611
+ },
13612
+ advertiser: { type: "string", description: "Source advertiser recorded in _definition.md" },
13294
13613
  "describe-model": { type: "string", description: "Override the image_describe model id" },
13295
13614
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
13296
13615
  "layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
@@ -13299,10 +13618,21 @@ var scaffoldStaticAdCommand = defineCommand86({
13299
13618
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
13300
13619
  },
13301
13620
  async run({ args }) {
13302
- const { imageIsUrl, imageSource, outPath, blueprintPath } = resolveScaffoldStaticAdPaths(
13621
+ const slug = args.slug ? String(args.slug) : void 0;
13622
+ if (slug && !isValidScaffoldSlug(slug)) {
13623
+ process.stderr.write(
13624
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
13625
+ `
13626
+ );
13627
+ process.exit(2);
13628
+ }
13629
+ const { imageIsUrl, imageSource, outPath, blueprintPath, definitionPath, referencesDir } = resolveScaffoldStaticAdPaths(
13303
13630
  String(args.file),
13304
- args.out ? String(args.out) : void 0
13631
+ args.out ? String(args.out) : void 0,
13632
+ process.cwd(),
13633
+ slug
13305
13634
  );
13635
+ await mkdir(path8.dirname(outPath), { recursive: true });
13306
13636
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
13307
13637
  const describeCanvas = buildDescribeCanvas(
13308
13638
  imageSource,
@@ -13319,11 +13649,21 @@ var scaffoldStaticAdCommand = defineCommand86({
13319
13649
  }
13320
13650
  await writeFile(blueprintPath, `${JSON.stringify(annotated, null, 2)}
13321
13651
  `, "utf8");
13652
+ let canvasImagePath = imageSource;
13653
+ let canvasImageIsUrl = imageIsUrl;
13654
+ let canvasBlueprintPath = blueprintPath;
13655
+ let sourceRelPath;
13656
+ if (referencesDir) {
13657
+ sourceRelPath = await copySourceIntoReferences(imageSource, imageIsUrl, referencesDir);
13658
+ canvasImagePath = sourceRelPath;
13659
+ canvasImageIsUrl = false;
13660
+ canvasBlueprintPath = "./prompt.json";
13661
+ }
13322
13662
  const opts = {
13323
13663
  genModel,
13324
- imagePath: imageSource,
13325
- imageIsUrl,
13326
- blueprintPath,
13664
+ imagePath: canvasImagePath,
13665
+ imageIsUrl: canvasImageIsUrl,
13666
+ blueprintPath: canvasBlueprintPath,
13327
13667
  aspectRatio: args.aspect ? String(args.aspect) : void 0,
13328
13668
  includeFont: !args["skip-font"]
13329
13669
  };
@@ -13345,12 +13685,31 @@ var scaffoldStaticAdCommand = defineCommand86({
13345
13685
  }
13346
13686
  await writeFile(outPath, `${JSON.stringify(canvas, null, 2)}
13347
13687
  `, "utf8");
13688
+ if (definitionPath && !await fileExists(definitionPath)) {
13689
+ await writeFile(
13690
+ definitionPath,
13691
+ buildCreativeDefinition({
13692
+ title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
13693
+ kind: "static",
13694
+ platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
13695
+ formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
13696
+ sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
13697
+ sourceAdvertiser: args.advertiser ? String(args.advertiser) : args.context ? String(args.context) : void 0,
13698
+ sourceKind: "image",
13699
+ sourcePath: sourceRelPath,
13700
+ description: describeBlueprintIntent(blueprint)
13701
+ }),
13702
+ "utf8"
13703
+ );
13704
+ }
13348
13705
  process.stdout.write(
13349
13706
  `${JSON.stringify(
13350
13707
  {
13351
13708
  ok: true,
13352
13709
  canvas_path: outPath,
13353
13710
  prompt_path: blueprintPath,
13711
+ definition_path: definitionPath ?? void 0,
13712
+ source_reference: sourceRelPath ?? void 0,
13354
13713
  output: canvas.output,
13355
13714
  models: { describe: describeModel, select: selectModel, layout: layoutModel, gen: opts.genModel },
13356
13715
  aspect_ratio: report.aspect_ratio,
@@ -13361,7 +13720,7 @@ var scaffoldStaticAdCommand = defineCommand86({
13361
13720
  run_estimated_credits: validation.estimatedCredits
13362
13721
  },
13363
13722
  checklist: {
13364
- edit_prompt: `Edit ${path6.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.`,
13723
+ edit_prompt: `Edit ${path8.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.`,
13365
13724
  assets_to_supply: report.elements,
13366
13725
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
13367
13726
  note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
@@ -13376,8 +13735,8 @@ var scaffoldStaticAdCommand = defineCommand86({
13376
13735
  });
13377
13736
 
13378
13737
  // src/commands/canvas/scaffold-video.ts
13379
- import { cp, mkdir, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
13380
- import path9 from "path";
13738
+ import { cp as cp2, mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
13739
+ import path11 from "path";
13381
13740
  import { defineCommand as defineCommand87 } from "citty";
13382
13741
 
13383
13742
  // src/engine/nodes/local/lib/sceneDetect.ts
@@ -15844,7 +16203,8 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
15844
16203
  }
15845
16204
  const todo = {
15846
16205
  ...buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts, blueprint),
15847
- ...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel)))
16206
+ ...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel))),
16207
+ model_constraints: buildModelConstraints()
15848
16208
  };
15849
16209
  return {
15850
16210
  schema: "baker-canvas/1",
@@ -15855,21 +16215,58 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
15855
16215
  // The timing plan `baker canvas validate` checks before any billed render:
15856
16216
  // sequenced voiceover turns (no overlap), audio ≈ video length, and which
15857
16217
  // scenes must be lip-synced.
15858
- video: buildVideoMeta(blueprint, { vo_segments, talking_scenes })
16218
+ video: buildVideoMeta(blueprint, { vo_segments, talking_scenes }, slots, nodes)
15859
16219
  },
15860
16220
  nodes,
15861
16221
  output: { node: videoNode, output: "video" }
15862
16222
  };
15863
16223
  }
15864
- function buildVideoMeta(blueprint, meta) {
16224
+ function buildVideoMeta(blueprint, meta, slots, nodes) {
15865
16225
  return {
15866
16226
  duration_s: blueprint.source?.duration_s ?? lastSceneEnd(blueprint),
15867
16227
  vo_segments: [...meta.vo_segments].sort((a, b) => a.start_s - b.start_s),
15868
16228
  talking_scenes: meta.talking_scenes,
15869
16229
  lip_sync_caution: buildLipSyncCaution(meta.vo_segments),
15870
- motion_board: buildMotionBoard(blueprint)
16230
+ motion_board: buildMotionBoard(blueprint),
16231
+ // The wired recurring-element registry (the reference-completeness check reads
16232
+ // `ref`/`label`/`type` to warn when a frame describes an element it doesn't wire).
16233
+ elements: slots.map((s) => ({
16234
+ ref: s.ref,
16235
+ label: s.label,
16236
+ type: s.type,
16237
+ ...s.description ? { description: s.description } : {}
16238
+ })),
16239
+ clip_spans: buildClipSpans(blueprint, nodes)
15871
16240
  };
15872
16241
  }
16242
+ function buildModelConstraints() {
16243
+ const models = {};
16244
+ for (const [id, spec] of Object.entries(MODEL_REGISTRY.video_generate)) {
16245
+ const p = spec.params;
16246
+ models[id] = {
16247
+ label: spec.label,
16248
+ aspect_ratios: p.aspect_ratio?.enum ?? null,
16249
+ durations_s: p.duration?.enum ?? null,
16250
+ person_generation: p.person_generation?.enum ?? null
16251
+ };
16252
+ }
16253
+ return {
16254
+ note: "Switching a clip's video model has cross-node constraints. ALL video_generate nodes in one canvas MUST share ONE aspect_ratio (the composite silently crops otherwise). Each model's `duration` is a hard enum and differs per model \u2014 a scene longer than the model's max must be split. `person_generation` differs (Veo requires \"allow_all\"). `baker canvas validate` gates duration, aspect agreement, per-model params, AND a scene-span-vs-model-max advisory before any billed run \u2014 run it after every model edit.",
16255
+ models
16256
+ };
16257
+ }
16258
+ function buildClipSpans(blueprint, nodes) {
16259
+ const spans = [];
16260
+ for (const n of nodes) {
16261
+ if (n.type !== "video_generate") continue;
16262
+ const m = n.id.match(/^s(\d+)/);
16263
+ const idx = m ? Number(m[1]) : Number.NaN;
16264
+ const scene = Number.isInteger(idx) ? blueprint.scenes[idx] : void 0;
16265
+ if (!scene) continue;
16266
+ spans.push({ node: n.id, span_s: Math.round(sceneDurationS(scene) * 100) / 100 });
16267
+ }
16268
+ return spans;
16269
+ }
15873
16270
  function buildLipSyncCaution(segments) {
15874
16271
  const out = [];
15875
16272
  const byScene = /* @__PURE__ */ new Map();
@@ -16123,23 +16520,23 @@ function videoReport(input, elementsInput) {
16123
16520
 
16124
16521
  // src/commands/canvas/composition-path.ts
16125
16522
  import { existsSync as existsSync4 } from "fs";
16126
- import path7 from "path";
16523
+ import path9 from "path";
16127
16524
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
16128
- const rel = path7.join("canvas", name);
16525
+ const rel = path9.join("canvas", name);
16129
16526
  let dir = startDir;
16130
16527
  for (let i = 0; i < maxDepth; i++) {
16131
- const candidate = path7.join(dir, rel);
16132
- if (exists(path7.join(candidate, "meta.json"))) return candidate;
16133
- const parent = path7.dirname(dir);
16528
+ const candidate = path9.join(dir, rel);
16529
+ if (exists(path9.join(candidate, "meta.json"))) return candidate;
16530
+ const parent = path9.dirname(dir);
16134
16531
  if (parent === dir) break;
16135
16532
  dir = parent;
16136
16533
  }
16137
- return path7.resolve(startDir, "../../../", rel);
16534
+ return path9.resolve(startDir, "../../../", rel);
16138
16535
  }
16139
16536
 
16140
16537
  // src/commands/canvas/gitignore.ts
16141
16538
  import { appendFile, readFile as readFile5 } from "fs/promises";
16142
- import path8 from "path";
16539
+ import path10 from "path";
16143
16540
  function missingGitignoreEntries(existing, entries) {
16144
16541
  const present = new Set(
16145
16542
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -16147,7 +16544,7 @@ function missingGitignoreEntries(existing, entries) {
16147
16544
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
16148
16545
  }
16149
16546
  async function ensureGitignore(dir, entries) {
16150
- const file = path8.join(dir, ".gitignore");
16547
+ const file = path10.join(dir, ".gitignore");
16151
16548
  let existing;
16152
16549
  try {
16153
16550
  existing = await readFile5(file, "utf8");
@@ -16208,8 +16605,8 @@ async function loadTranscriptBestEffort(ref) {
16208
16605
  async function stageCaptions(outDir, transcript) {
16209
16606
  const text = transcript?.trim();
16210
16607
  if (!text || text === "[]") return {};
16211
- const compositionPath = path9.join(outDir, "tiktok-captions-composition");
16212
- await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
16608
+ const compositionPath = path11.join(outDir, "tiktok-captions-composition");
16609
+ await cp2(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
16213
16610
  return { compositionPath };
16214
16611
  }
16215
16612
  function patchCompositionMeta(metaJson, dims) {
@@ -16226,10 +16623,10 @@ function patchCompositionHtml(html, dims) {
16226
16623
  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`);
16227
16624
  }
16228
16625
  async function stampCompositionDims(compositionDir, dims) {
16229
- const metaPath = path9.join(compositionDir, "meta.json");
16626
+ const metaPath = path11.join(compositionDir, "meta.json");
16230
16627
  const rawMeta = await readFile6(metaPath, "utf8");
16231
16628
  await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
16232
- const htmlPath = path9.join(compositionDir, "index.html");
16629
+ const htmlPath = path11.join(compositionDir, "index.html");
16233
16630
  const rawHtml = await readFile6(htmlPath, "utf8");
16234
16631
  await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
16235
16632
  }
@@ -16369,6 +16766,10 @@ var scaffoldVideoCommand = defineCommand87({
16369
16766
  args: {
16370
16767
  file: { type: "positional", required: true, description: "Path to the reference video" },
16371
16768
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
16769
+ slug: {
16770
+ type: "string",
16771
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
16772
+ },
16372
16773
  frames: { type: "string", description: '"generate" (default, anchored regen) or "reuse" (wire real frames in)' },
16373
16774
  ambient: {
16374
16775
  type: "boolean",
@@ -16395,11 +16796,19 @@ var scaffoldVideoCommand = defineCommand87({
16395
16796
  }
16396
16797
  },
16397
16798
  async run({ args }) {
16398
- const videoPath = path9.resolve(String(args.file));
16399
- const base = path9.basename(videoPath, path9.extname(videoPath));
16400
- const outPath = args.out ? path9.resolve(String(args.out)) : path9.join(path9.dirname(videoPath), `${base}.video.canvas.json`);
16401
- const outDir = path9.dirname(outPath);
16402
- const blueprintPath = path9.join(outDir, "prompt.json");
16799
+ const videoPath = path11.resolve(String(args.file));
16800
+ const base = path11.basename(videoPath, path11.extname(videoPath));
16801
+ const slug = args.slug ? String(args.slug) : void 0;
16802
+ if (slug && !isValidScaffoldSlug(slug)) {
16803
+ process.stderr.write(
16804
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
16805
+ `
16806
+ );
16807
+ process.exit(2);
16808
+ }
16809
+ const outPath = args.out ? path11.resolve(String(args.out)) : slug ? path11.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path11.join(path11.dirname(videoPath), `${base}.video.canvas.json`);
16810
+ const outDir = path11.dirname(outPath);
16811
+ const blueprintPath = path11.join(outDir, "prompt.json");
16403
16812
  const frames = args.frames === "reuse" ? "reuse" : "generate";
16404
16813
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
16405
16814
  if (Number.isFinite(maxScenes)) {
@@ -16418,7 +16827,7 @@ var scaffoldVideoCommand = defineCommand87({
16418
16827
  shotCuts
16419
16828
  });
16420
16829
  const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
16421
- await mkdir(outDir, { recursive: true });
16830
+ await mkdir2(outDir, { recursive: true });
16422
16831
  const annotated = annotateBlueprintWithElements(blueprint, elements);
16423
16832
  await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
16424
16833
  `, "utf8");
@@ -16439,10 +16848,10 @@ var scaffoldVideoCommand = defineCommand87({
16439
16848
  `
16440
16849
  );
16441
16850
  }
16442
- const compositionDest = path9.join(outDir, "video-overlay-composition");
16443
- await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
16851
+ const compositionDest = path11.join(outDir, "video-overlay-composition");
16852
+ await cp2(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
16444
16853
  await stampCompositionDims(compositionDest, outDims);
16445
- const indexPath = path9.join(compositionDest, "index.html");
16854
+ const indexPath = path11.join(compositionDest, "index.html");
16446
16855
  const overlayHtml = buildOverlayHtml(blueprint);
16447
16856
  const indexHtml = await readFile6(indexPath, "utf8");
16448
16857
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
@@ -16458,9 +16867,9 @@ var scaffoldVideoCommand = defineCommand87({
16458
16867
  const opts = {
16459
16868
  imageModel,
16460
16869
  videoModel,
16461
- overlayCompositionPath: path9.relative(outDir, compositionDest),
16462
- captionsCompositionPath: captions.compositionPath ? path9.relative(outDir, captions.compositionPath) : void 0,
16463
- blueprintPath: path9.relative(outDir, blueprintPath),
16870
+ overlayCompositionPath: path11.relative(outDir, compositionDest),
16871
+ captionsCompositionPath: captions.compositionPath ? path11.relative(outDir, captions.compositionPath) : void 0,
16872
+ blueprintPath: path11.relative(outDir, blueprintPath),
16464
16873
  frames,
16465
16874
  ambient: Boolean(args.ambient),
16466
16875
  ...args.aspect ? { aspect: String(args.aspect) } : {},
@@ -16502,7 +16911,7 @@ var scaffoldVideoCommand = defineCommand87({
16502
16911
  run_estimated_credits: validation.estimatedCredits
16503
16912
  },
16504
16913
  checklist: {
16505
- edit_prompt: `Edit ${path9.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
16914
+ edit_prompt: `Edit ${path11.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
16506
16915
  recurring_elements_to_supply: report.elements,
16507
16916
  voices_to_confirm: report.dialogue.map((d) => ({
16508
16917
  scene: d.scene,
@@ -16529,7 +16938,7 @@ var scaffoldVideoCommand = defineCommand87({
16529
16938
 
16530
16939
  // src/commands/canvas/set-prompt.ts
16531
16940
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
16532
- import path10 from "path";
16941
+ import path12 from "path";
16533
16942
  import { defineCommand as defineCommand88 } from "citty";
16534
16943
  function setNodePrompt(canvas, nodeId, text) {
16535
16944
  const nodes = canvas?.nodes;
@@ -16557,7 +16966,7 @@ var setPromptCommand = defineCommand88({
16557
16966
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
16558
16967
  },
16559
16968
  async run({ args }) {
16560
- const filePath = path10.resolve(String(args.file));
16969
+ const filePath = path12.resolve(String(args.file));
16561
16970
  let canvas;
16562
16971
  try {
16563
16972
  canvas = JSON.parse(await readFile7(filePath, "utf8"));
@@ -16567,7 +16976,7 @@ var setPromptCommand = defineCommand88({
16567
16976
  process.exit(2);
16568
16977
  }
16569
16978
  let text;
16570
- if (args["text-file"]) text = await readFile7(path10.resolve(String(args["text-file"])), "utf8");
16979
+ if (args["text-file"]) text = await readFile7(path12.resolve(String(args["text-file"])), "utf8");
16571
16980
  else if (args.text !== void 0) text = String(args.text);
16572
16981
  else {
16573
16982
  process.stderr.write(
@@ -16588,7 +16997,7 @@ var setPromptCommand = defineCommand88({
16588
16997
  process.exit(2);
16589
16998
  return;
16590
16999
  }
16591
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path10.dirname(filePath)), defaultRegistry());
17000
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path12.dirname(filePath)), defaultRegistry());
16592
17001
  if (!validation.ok) {
16593
17002
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
16594
17003
  `);
@@ -16604,7 +17013,7 @@ var setPromptCommand = defineCommand88({
16604
17013
 
16605
17014
  // src/commands/canvas/validate.ts
16606
17015
  import { readFile as readFile8 } from "fs/promises";
16607
- import path11 from "path";
17016
+ import path13 from "path";
16608
17017
  import { defineCommand as defineCommand89 } from "citty";
16609
17018
  var validateCommand = defineCommand89({
16610
17019
  meta: {
@@ -16613,7 +17022,7 @@ var validateCommand = defineCommand89({
16613
17022
  },
16614
17023
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
16615
17024
  async run({ args }) {
16616
- const filePath = path11.resolve(String(args.file));
17025
+ const filePath = path13.resolve(String(args.file));
16617
17026
  const raw = await readFile8(filePath, "utf8");
16618
17027
  let parsed;
16619
17028
  try {
@@ -16624,7 +17033,7 @@ var validateCommand = defineCommand89({
16624
17033
  `);
16625
17034
  process.exit(2);
16626
17035
  }
16627
- parsed = resolveRelativeCanvasPaths(parsed, path11.dirname(filePath));
17036
+ parsed = resolveRelativeCanvasPaths(parsed, path13.dirname(filePath));
16628
17037
  const result = await validateCanvasDeep(parsed, defaultRegistry());
16629
17038
  if (!result.ok) {
16630
17039
  process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
@@ -16638,7 +17047,8 @@ var validateCommand = defineCommand89({
16638
17047
  ok: true,
16639
17048
  total_nodes: result.canvas.nodes.length,
16640
17049
  estimated_credits: result.estimatedCredits,
16641
- cost_preview: result.perNodeCredits ?? []
17050
+ cost_preview: result.perNodeCredits ?? [],
17051
+ warnings: result.warnings ?? []
16642
17052
  },
16643
17053
  null,
16644
17054
  2
@@ -16760,6 +17170,16 @@ registerSchema({
16760
17170
  type: "string",
16761
17171
  description: "Optional URL of the original reference ad",
16762
17172
  required: false
17173
+ },
17174
+ slug: {
17175
+ type: "string",
17176
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17177
+ required: false
17178
+ },
17179
+ runId: {
17180
+ type: "string",
17181
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17182
+ required: false
16763
17183
  }
16764
17184
  }
16765
17185
  });
@@ -16769,6 +17189,13 @@ function detectCreativeContentType(filePath) {
16769
17189
  unsupportedMessage: "Unsupported creative image extension. Use PNG, JPG, or WebP."
16770
17190
  });
16771
17191
  }
17192
+ function chatIdFromEnv() {
17193
+ try {
17194
+ return getEnv().BAKER_CHAT_ID || void 0;
17195
+ } catch {
17196
+ return void 0;
17197
+ }
17198
+ }
16772
17199
  function parseOptionalUrl(value) {
16773
17200
  if (value === void 0 || value.trim() === "") {
16774
17201
  return void 0;
@@ -16799,7 +17226,11 @@ async function publishCreative(args, deps = defaultImageApiDeps) {
16799
17226
  return publishImageAsCreative(deps, {
16800
17227
  imageId: upload.imageId,
16801
17228
  title,
16802
- sourceReferenceUrl
17229
+ sourceReferenceUrl,
17230
+ slug: args.slug,
17231
+ runId: args.runId,
17232
+ // Attribute the publish to the driving chat (injected by the bridge).
17233
+ chatId: chatIdFromEnv()
16803
17234
  });
16804
17235
  }
16805
17236
  var publishCommand = defineCommand91({
@@ -16815,6 +17246,16 @@ var publishCommand = defineCommand91({
16815
17246
  type: "string",
16816
17247
  description: "Optional URL of the original reference ad",
16817
17248
  required: false
17249
+ },
17250
+ slug: {
17251
+ type: "string",
17252
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17253
+ required: false
17254
+ },
17255
+ runId: {
17256
+ type: "string",
17257
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17258
+ required: false
16818
17259
  }
16819
17260
  },
16820
17261
  run: async ({ args }) => {
@@ -16833,7 +17274,9 @@ var publishCommand = defineCommand91({
16833
17274
  file,
16834
17275
  title,
16835
17276
  context: args.context,
16836
- sourceReferenceUrl: args.sourceReferenceUrl
17277
+ sourceReferenceUrl: args.sourceReferenceUrl,
17278
+ slug: args.slug,
17279
+ runId: args.runId
16837
17280
  });
16838
17281
  writeJson({ ok: true, data });
16839
17282
  } catch (err) {
@@ -17731,9 +18174,9 @@ async function readImageBuffer(pathOrUrl) {
17731
18174
  }
17732
18175
  return readFile10(pathOrUrl);
17733
18176
  }
17734
- async function isDirectory(path12) {
18177
+ async function isDirectory(path14) {
17735
18178
  try {
17736
- const s = await stat2(path12);
18179
+ const s = await stat2(path14);
17737
18180
  return s.isDirectory();
17738
18181
  } catch {
17739
18182
  return false;