@koda-sl/baker-cli 0.115.0 → 0.116.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-ZWMMCLJ4.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(path13, params) {
155
161
  const env = getEnv();
156
- const url = new URL(path12, env.BAKER_API_URL);
162
+ const url = new URL(path13, 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(path13, 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(path13, 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 path13 = cachePath(category, key);
2885
+ if (!existsSync2(path13)) {
2880
2886
  return null;
2881
2887
  }
2882
2888
  try {
2883
- const raw = readFileSync2(path12, "utf-8");
2889
+ const raw = readFileSync2(path13, "utf-8");
2884
2890
  const entry = JSON.parse(raw);
2885
2891
  if (entry.expiresAt < Date.now()) {
2886
- rmSync(path12, { force: true });
2892
+ rmSync(path13, { force: true });
2887
2893
  return null;
2888
2894
  }
2889
2895
  return entry;
2890
2896
  } catch {
2891
- rmSync(path12, { force: true });
2897
+ rmSync(path13, { 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 path13 = 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(path13, JSON.stringify(entry), "utf-8");
2903
2909
  }
2904
2910
  var HOUR = 60 * 60 * 1e3;
2905
2911
  var MINUTE = 60 * 1e3;
@@ -4812,11 +4818,11 @@ function rawTextEntries(value) {
4812
4818
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
4813
4819
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
4814
4820
  }
4815
- function rawFileEntries(path12) {
4816
- if (typeof path12 !== "string" || path12.length === 0) {
4821
+ function rawFileEntries(path13) {
4822
+ if (typeof path13 !== "string" || path13.length === 0) {
4817
4823
  return [];
4818
4824
  }
4819
- return readFileSync3(path12, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4825
+ return readFileSync3(path13, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4820
4826
  }
4821
4827
  function keywordEntries(args) {
4822
4828
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -4839,19 +4845,19 @@ function keywordEntries(args) {
4839
4845
  }
4840
4846
  return entries;
4841
4847
  }
4842
- function loadJsonFileArg(path12) {
4843
- if (typeof path12 !== "string" || path12.length === 0) {
4848
+ function loadJsonFileArg(path13) {
4849
+ if (typeof path13 !== "string" || path13.length === 0) {
4844
4850
  return {};
4845
4851
  }
4846
4852
  try {
4847
- const parsed = JSON.parse(readFileSync3(path12, "utf8"));
4853
+ const parsed = JSON.parse(readFileSync3(path13, "utf8"));
4848
4854
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4849
- failWriteValidation(`${path12} must contain a JSON object`);
4855
+ failWriteValidation(`${path13} must contain a JSON object`);
4850
4856
  }
4851
4857
  return parsed;
4852
4858
  } catch (err) {
4853
4859
  if (err instanceof SyntaxError) {
4854
- failWriteValidation(`${path12} is not valid JSON: ${err.message}`);
4860
+ failWriteValidation(`${path13} is not valid JSON: ${err.message}`);
4855
4861
  }
4856
4862
  throw err;
4857
4863
  }
@@ -4940,10 +4946,10 @@ async function stageUpdate(kind, customerId, target, payload) {
4940
4946
  async function stageTarget(kind, customerId, target) {
4941
4947
  await stageGoogleOp({ kind, customerId, target });
4942
4948
  }
4943
- async function draftAction(path12, body) {
4949
+ async function draftAction(path13, body) {
4944
4950
  try {
4945
4951
  const chatId = requireChatId();
4946
- const response = await apiPost(path12, { chatId, ...body });
4952
+ const response = await apiPost(path13, { chatId, ...body });
4947
4953
  writeJsonEnvelope(response);
4948
4954
  } catch (err) {
4949
4955
  handleGoogleError(err);
@@ -8484,19 +8490,19 @@ function failWriteValidation2(message) {
8484
8490
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8485
8491
  process.exit(1);
8486
8492
  }
8487
- function loadJsonFileArg2(path12) {
8488
- if (typeof path12 !== "string" || path12.length === 0) {
8493
+ function loadJsonFileArg2(path13) {
8494
+ if (typeof path13 !== "string" || path13.length === 0) {
8489
8495
  return {};
8490
8496
  }
8491
8497
  try {
8492
- const parsed = JSON.parse(readFileSync7(path12, "utf8"));
8498
+ const parsed = JSON.parse(readFileSync7(path13, "utf8"));
8493
8499
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8494
- failWriteValidation2(`${path12} must contain a JSON object`);
8500
+ failWriteValidation2(`${path13} must contain a JSON object`);
8495
8501
  }
8496
8502
  return parsed;
8497
8503
  } catch (err) {
8498
8504
  if (err instanceof SyntaxError) {
8499
- failWriteValidation2(`${path12} is not valid JSON: ${err.message}`);
8505
+ failWriteValidation2(`${path13} is not valid JSON: ${err.message}`);
8500
8506
  }
8501
8507
  throw err;
8502
8508
  }
@@ -8559,15 +8565,15 @@ function parseLocaleFlag(value) {
8559
8565
  }
8560
8566
  return { language: match[1], country: match[2].toUpperCase() };
8561
8567
  }
8562
- function loadTargetingFileArg(path12) {
8563
- if (typeof path12 !== "string" || path12.length === 0) {
8568
+ function loadTargetingFileArg(path13) {
8569
+ if (typeof path13 !== "string" || path13.length === 0) {
8564
8570
  return void 0;
8565
8571
  }
8566
- const parsed = loadJsonFileArg2(path12);
8572
+ const parsed = loadJsonFileArg2(path13);
8567
8573
  const criteria = parsed.targetingCriteria ?? parsed;
8568
8574
  if (!criteria.include) {
8569
8575
  failWriteValidation2(
8570
- `${path12} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8576
+ `${path13} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8571
8577
  );
8572
8578
  }
8573
8579
  return criteria;
@@ -8602,14 +8608,14 @@ function parseCsvLine(line) {
8602
8608
  cells.push(current);
8603
8609
  return cells.map((cell) => cell.trim());
8604
8610
  }
8605
- function parseListFileArg(path12, maxRows) {
8606
- if (typeof path12 !== "string" || path12.length === 0) {
8611
+ function parseListFileArg(path13, maxRows) {
8612
+ if (typeof path13 !== "string" || path13.length === 0) {
8607
8613
  return void 0;
8608
8614
  }
8609
- const raw = readFileSync7(path12, "utf8");
8615
+ const raw = readFileSync7(path13, "utf8");
8610
8616
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
8611
8617
  if (lines.length < 2) {
8612
- failWriteValidation2(`${path12} needs a header row and at least one data row`);
8618
+ failWriteValidation2(`${path13} needs a header row and at least one data row`);
8613
8619
  }
8614
8620
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
8615
8621
  const rows = [];
@@ -8628,7 +8634,7 @@ function parseListFileArg(path12, maxRows) {
8628
8634
  }
8629
8635
  }
8630
8636
  if (rows.length > maxRows) {
8631
- failWriteValidation2(`${path12} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8637
+ failWriteValidation2(`${path13} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
8632
8638
  }
8633
8639
  return { columns, rows };
8634
8640
  }
@@ -12831,7 +12837,7 @@ async function probeDuration(filePath) {
12831
12837
 
12832
12838
  // src/commands/canvas/run.ts
12833
12839
  import { readFile as readFile2 } from "fs/promises";
12834
- import path4 from "path";
12840
+ import path5 from "path";
12835
12841
  import { defineCommand as defineCommand85 } from "citty";
12836
12842
 
12837
12843
  // src/commands/canvas/placeholders.ts
@@ -12875,9 +12881,102 @@ function isResolvableRelative(value) {
12875
12881
  return typeof value === "string" && value.length > 0 && !value.includes("[TODO") && !path2.isAbsolute(value);
12876
12882
  }
12877
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
+
12878
12977
  // src/commands/canvas/run-retention.ts
12879
12978
  import { rm } from "fs/promises";
12880
- import path3 from "path";
12979
+ import path4 from "path";
12881
12980
  function runDirsToPrune(entries, keep, currentRunId) {
12882
12981
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
12883
12982
  if (keep <= 0) return runs;
@@ -12894,7 +12993,7 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
12894
12993
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
12895
12994
  if (toPrune.length === 0) return;
12896
12995
  for (const dir of toPrune) {
12897
- await rm(path3.join(outputsDir, dir), { recursive: true, force: true }).catch(
12996
+ await rm(path4.join(outputsDir, dir), { recursive: true, force: true }).catch(
12898
12997
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
12899
12998
  );
12900
12999
  }
@@ -12921,10 +13020,22 @@ var runCommand = defineCommand85({
12921
13020
  "keep-runs": {
12922
13021
  type: "string",
12923
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)"
12924
13035
  }
12925
13036
  },
12926
13037
  async run({ args }) {
12927
- const filePath = path4.resolve(String(args.file));
13038
+ const filePath = path5.resolve(String(args.file));
12928
13039
  const raw = await readFile2(filePath, "utf8");
12929
13040
  let parsed;
12930
13041
  try {
@@ -12935,7 +13046,7 @@ var runCommand = defineCommand85({
12935
13046
  `);
12936
13047
  process.exit(2);
12937
13048
  }
12938
- parsed = resolveRelativeCanvasPaths(parsed, path4.dirname(filePath));
13049
+ parsed = resolveRelativeCanvasPaths(parsed, path5.dirname(filePath));
12939
13050
  const pending = unsuppliedPlaceholderAssets(parsed);
12940
13051
  if (pending.length > 0) {
12941
13052
  process.stderr.write(
@@ -12955,16 +13066,26 @@ var runCommand = defineCommand85({
12955
13066
  );
12956
13067
  process.exit(2);
12957
13068
  }
13069
+ const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
12958
13070
  const engine = createEngineFromEnv({
12959
13071
  cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
12960
13072
  outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
12961
13073
  log: (line) => process.stdout.write(`${line}
12962
- `)
13074
+ `),
13075
+ remoteCache
12963
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();
12964
13085
  try {
12965
13086
  const policy = args["cache-policy"] ?? "read_write";
12966
13087
  const result = await engine.run(parsed, {
12967
- run_id: args["run-id"] ? String(args["run-id"]) : void 0,
13088
+ run_id: runId,
12968
13089
  cache_policy: policy,
12969
13090
  concurrency: resolveConcurrency(
12970
13091
  // --concurrency wins; --parallel is the discoverable alias for the same bound.
@@ -12972,9 +13093,10 @@ var runCommand = defineCommand85({
12972
13093
  process.env.BAKER_CANVAS_CONCURRENCY
12973
13094
  )
12974
13095
  });
13096
+ if (record) await record(buildRunRecord(result, recordMeta));
12975
13097
  const keepRuns = args["keep-runs"] !== void 0 ? Number(args["keep-runs"]) : void 0;
12976
13098
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
12977
- 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");
12978
13100
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
12979
13101
  `));
12980
13102
  }
@@ -13002,6 +13124,7 @@ var runCommand = defineCommand85({
13002
13124
  }
13003
13125
  if (e instanceof LayerExecutionError) {
13004
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));
13005
13128
  process.stderr.write(
13006
13129
  `${JSON.stringify({ ok: false, error: { code: "runtime", message: e.message, failures } }, null, 2)}
13007
13130
  `
@@ -13009,16 +13132,30 @@ var runCommand = defineCommand85({
13009
13132
  process.exit(1);
13010
13133
  }
13011
13134
  const msg = e instanceof Error ? e.message : String(e);
13135
+ if (record) await record(buildFailedRunRecord(runId, msg, recordMeta));
13012
13136
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "runtime", message: msg } }, null, 2)}
13013
13137
  `);
13014
13138
  process.exit(1);
13015
13139
  }
13016
13140
  }
13017
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
+ }
13018
13155
 
13019
13156
  // src/commands/canvas/scaffold-static-ad.ts
13020
13157
  import { readFile as readFile3, writeFile } from "fs/promises";
13021
- import path6 from "path";
13158
+ import path7 from "path";
13022
13159
  import { defineCommand as defineCommand86 } from "citty";
13023
13160
 
13024
13161
  // src/engine/scaffold/staticAd.ts
@@ -13205,15 +13342,20 @@ function staticAdReport(input, elementsInput, opts) {
13205
13342
  }
13206
13343
 
13207
13344
  // src/commands/canvas/scaffold-static-ad-paths.ts
13208
- import path5 from "path";
13209
- function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd()) {
13345
+ import path6 from "path";
13346
+ function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
13210
13347
  const file = rawFile.trim();
13211
13348
  const imageIsUrl = /^https?:\/\//i.test(file);
13212
- const imageSource = imageIsUrl ? file : path5.resolve(cwd, file);
13213
- const outPath = out ? path5.resolve(cwd, out) : imageIsUrl ? path5.join(cwd, "static-ad.canvas.json") : path5.join(path5.dirname(imageSource), "static-ad.canvas.json");
13214
- const blueprintPath = path5.join(path5.dirname(outPath), "prompt.json");
13349
+ const imageSource = imageIsUrl ? file : path6.resolve(cwd, file);
13350
+ const outPath = out ? path6.resolve(cwd, out) : slug ? path6.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path6.join(cwd, "static-ad.canvas.json") : path6.join(path6.dirname(imageSource), "static-ad.canvas.json");
13351
+ const blueprintPath = path6.join(path6.dirname(outPath), "prompt.json");
13215
13352
  return { imageIsUrl, imageSource, outPath, blueprintPath };
13216
13353
  }
13354
+ var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
13355
+ var SCAFFOLD_SLUG_MAX_LENGTH = 100;
13356
+ function isValidScaffoldSlug(slug) {
13357
+ return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
13358
+ }
13217
13359
 
13218
13360
  // src/commands/canvas/scaffold-static-ad.ts
13219
13361
  function resolveModel(kind, preferred) {
@@ -13375,6 +13517,10 @@ var scaffoldStaticAdCommand = defineCommand86({
13375
13517
  file: { type: "positional", required: true, description: "Path or http(s) URL to the source/inspiration image" },
13376
13518
  context: { type: "string", description: "Known provenance (advertiser, category, market) to ground the describe" },
13377
13519
  out: { type: "string", description: "Output canvas path (default <image-dir>/static-ad.canvas.json)" },
13520
+ slug: {
13521
+ type: "string",
13522
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
13523
+ },
13378
13524
  "describe-model": { type: "string", description: "Override the image_describe model id" },
13379
13525
  "select-model": { type: "string", description: "Override the text_generate model id for element selection" },
13380
13526
  "layout-model": { type: "string", description: "Override the text_generate model id for the layout pass" },
@@ -13383,9 +13529,19 @@ var scaffoldStaticAdCommand = defineCommand86({
13383
13529
  "skip-font": { type: "boolean", description: "Skip the brand-font \u2192 type-specimen slot" }
13384
13530
  },
13385
13531
  async run({ args }) {
13532
+ const slug = args.slug ? String(args.slug) : void 0;
13533
+ if (slug && !isValidScaffoldSlug(slug)) {
13534
+ process.stderr.write(
13535
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
13536
+ `
13537
+ );
13538
+ process.exit(2);
13539
+ }
13386
13540
  const { imageIsUrl, imageSource, outPath, blueprintPath } = resolveScaffoldStaticAdPaths(
13387
13541
  String(args.file),
13388
- args.out ? String(args.out) : void 0
13542
+ args.out ? String(args.out) : void 0,
13543
+ process.cwd(),
13544
+ slug
13389
13545
  );
13390
13546
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
13391
13547
  const describeCanvas = buildDescribeCanvas(
@@ -13445,7 +13601,7 @@ var scaffoldStaticAdCommand = defineCommand86({
13445
13601
  run_estimated_credits: validation.estimatedCredits
13446
13602
  },
13447
13603
  checklist: {
13448
- 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.`,
13604
+ edit_prompt: `Edit ${path7.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.`,
13449
13605
  assets_to_supply: report.elements,
13450
13606
  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)",
13451
13607
  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."
@@ -13461,7 +13617,7 @@ var scaffoldStaticAdCommand = defineCommand86({
13461
13617
 
13462
13618
  // src/commands/canvas/scaffold-video.ts
13463
13619
  import { cp, mkdir, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
13464
- import path9 from "path";
13620
+ import path10 from "path";
13465
13621
  import { defineCommand as defineCommand87 } from "citty";
13466
13622
 
13467
13623
  // src/engine/nodes/local/lib/sceneDetect.ts
@@ -16245,23 +16401,23 @@ function videoReport(input, elementsInput) {
16245
16401
 
16246
16402
  // src/commands/canvas/composition-path.ts
16247
16403
  import { existsSync as existsSync4 } from "fs";
16248
- import path7 from "path";
16404
+ import path8 from "path";
16249
16405
  function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth = 8) {
16250
- const rel = path7.join("canvas", name);
16406
+ const rel = path8.join("canvas", name);
16251
16407
  let dir = startDir;
16252
16408
  for (let i = 0; i < maxDepth; i++) {
16253
- const candidate = path7.join(dir, rel);
16254
- if (exists(path7.join(candidate, "meta.json"))) return candidate;
16255
- const parent = path7.dirname(dir);
16409
+ const candidate = path8.join(dir, rel);
16410
+ if (exists(path8.join(candidate, "meta.json"))) return candidate;
16411
+ const parent = path8.dirname(dir);
16256
16412
  if (parent === dir) break;
16257
16413
  dir = parent;
16258
16414
  }
16259
- return path7.resolve(startDir, "../../../", rel);
16415
+ return path8.resolve(startDir, "../../../", rel);
16260
16416
  }
16261
16417
 
16262
16418
  // src/commands/canvas/gitignore.ts
16263
16419
  import { appendFile, readFile as readFile5 } from "fs/promises";
16264
- import path8 from "path";
16420
+ import path9 from "path";
16265
16421
  function missingGitignoreEntries(existing, entries) {
16266
16422
  const present = new Set(
16267
16423
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -16269,7 +16425,7 @@ function missingGitignoreEntries(existing, entries) {
16269
16425
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
16270
16426
  }
16271
16427
  async function ensureGitignore(dir, entries) {
16272
- const file = path8.join(dir, ".gitignore");
16428
+ const file = path9.join(dir, ".gitignore");
16273
16429
  let existing;
16274
16430
  try {
16275
16431
  existing = await readFile5(file, "utf8");
@@ -16330,7 +16486,7 @@ async function loadTranscriptBestEffort(ref) {
16330
16486
  async function stageCaptions(outDir, transcript) {
16331
16487
  const text = transcript?.trim();
16332
16488
  if (!text || text === "[]") return {};
16333
- const compositionPath = path9.join(outDir, "tiktok-captions-composition");
16489
+ const compositionPath = path10.join(outDir, "tiktok-captions-composition");
16334
16490
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
16335
16491
  return { compositionPath };
16336
16492
  }
@@ -16348,10 +16504,10 @@ function patchCompositionHtml(html, dims) {
16348
16504
  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`);
16349
16505
  }
16350
16506
  async function stampCompositionDims(compositionDir, dims) {
16351
- const metaPath = path9.join(compositionDir, "meta.json");
16507
+ const metaPath = path10.join(compositionDir, "meta.json");
16352
16508
  const rawMeta = await readFile6(metaPath, "utf8");
16353
16509
  await writeFile2(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
16354
- const htmlPath = path9.join(compositionDir, "index.html");
16510
+ const htmlPath = path10.join(compositionDir, "index.html");
16355
16511
  const rawHtml = await readFile6(htmlPath, "utf8");
16356
16512
  await writeFile2(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
16357
16513
  }
@@ -16491,6 +16647,10 @@ var scaffoldVideoCommand = defineCommand87({
16491
16647
  args: {
16492
16648
  file: { type: "positional", required: true, description: "Path to the reference video" },
16493
16649
  out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
16650
+ slug: {
16651
+ type: "string",
16652
+ description: "Creative slug \u2014 writes the canvas to src/creatives/<slug>/<slug>.canvas.json (repo convention)"
16653
+ },
16494
16654
  frames: { type: "string", description: '"generate" (default, anchored regen) or "reuse" (wire real frames in)' },
16495
16655
  ambient: {
16496
16656
  type: "boolean",
@@ -16517,11 +16677,19 @@ var scaffoldVideoCommand = defineCommand87({
16517
16677
  }
16518
16678
  },
16519
16679
  async run({ args }) {
16520
- const videoPath = path9.resolve(String(args.file));
16521
- const base = path9.basename(videoPath, path9.extname(videoPath));
16522
- const outPath = args.out ? path9.resolve(String(args.out)) : path9.join(path9.dirname(videoPath), `${base}.video.canvas.json`);
16523
- const outDir = path9.dirname(outPath);
16524
- const blueprintPath = path9.join(outDir, "prompt.json");
16680
+ const videoPath = path10.resolve(String(args.file));
16681
+ const base = path10.basename(videoPath, path10.extname(videoPath));
16682
+ const slug = args.slug ? String(args.slug) : void 0;
16683
+ if (slug && !isValidScaffoldSlug(slug)) {
16684
+ process.stderr.write(
16685
+ `${JSON.stringify({ ok: false, error: { code: "invalid_slug", message: "--slug must be lowercase kebab (a-z, 0-9, hyphens), max 100 chars" } }, null, 2)}
16686
+ `
16687
+ );
16688
+ process.exit(2);
16689
+ }
16690
+ const outPath = args.out ? path10.resolve(String(args.out)) : slug ? path10.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path10.join(path10.dirname(videoPath), `${base}.video.canvas.json`);
16691
+ const outDir = path10.dirname(outPath);
16692
+ const blueprintPath = path10.join(outDir, "prompt.json");
16525
16693
  const frames = args.frames === "reuse" ? "reuse" : "generate";
16526
16694
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
16527
16695
  if (Number.isFinite(maxScenes)) {
@@ -16561,10 +16729,10 @@ var scaffoldVideoCommand = defineCommand87({
16561
16729
  `
16562
16730
  );
16563
16731
  }
16564
- const compositionDest = path9.join(outDir, "video-overlay-composition");
16732
+ const compositionDest = path10.join(outDir, "video-overlay-composition");
16565
16733
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
16566
16734
  await stampCompositionDims(compositionDest, outDims);
16567
- const indexPath = path9.join(compositionDest, "index.html");
16735
+ const indexPath = path10.join(compositionDest, "index.html");
16568
16736
  const overlayHtml = buildOverlayHtml(blueprint);
16569
16737
  const indexHtml = await readFile6(indexPath, "utf8");
16570
16738
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
@@ -16580,9 +16748,9 @@ var scaffoldVideoCommand = defineCommand87({
16580
16748
  const opts = {
16581
16749
  imageModel,
16582
16750
  videoModel,
16583
- overlayCompositionPath: path9.relative(outDir, compositionDest),
16584
- captionsCompositionPath: captions.compositionPath ? path9.relative(outDir, captions.compositionPath) : void 0,
16585
- blueprintPath: path9.relative(outDir, blueprintPath),
16751
+ overlayCompositionPath: path10.relative(outDir, compositionDest),
16752
+ captionsCompositionPath: captions.compositionPath ? path10.relative(outDir, captions.compositionPath) : void 0,
16753
+ blueprintPath: path10.relative(outDir, blueprintPath),
16586
16754
  frames,
16587
16755
  ambient: Boolean(args.ambient),
16588
16756
  ...args.aspect ? { aspect: String(args.aspect) } : {},
@@ -16624,7 +16792,7 @@ var scaffoldVideoCommand = defineCommand87({
16624
16792
  run_estimated_credits: validation.estimatedCredits
16625
16793
  },
16626
16794
  checklist: {
16627
- 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.`,
16795
+ edit_prompt: `Edit ${path10.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.`,
16628
16796
  recurring_elements_to_supply: report.elements,
16629
16797
  voices_to_confirm: report.dialogue.map((d) => ({
16630
16798
  scene: d.scene,
@@ -16651,7 +16819,7 @@ var scaffoldVideoCommand = defineCommand87({
16651
16819
 
16652
16820
  // src/commands/canvas/set-prompt.ts
16653
16821
  import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
16654
- import path10 from "path";
16822
+ import path11 from "path";
16655
16823
  import { defineCommand as defineCommand88 } from "citty";
16656
16824
  function setNodePrompt(canvas, nodeId, text) {
16657
16825
  const nodes = canvas?.nodes;
@@ -16679,7 +16847,7 @@ var setPromptCommand = defineCommand88({
16679
16847
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
16680
16848
  },
16681
16849
  async run({ args }) {
16682
- const filePath = path10.resolve(String(args.file));
16850
+ const filePath = path11.resolve(String(args.file));
16683
16851
  let canvas;
16684
16852
  try {
16685
16853
  canvas = JSON.parse(await readFile7(filePath, "utf8"));
@@ -16689,7 +16857,7 @@ var setPromptCommand = defineCommand88({
16689
16857
  process.exit(2);
16690
16858
  }
16691
16859
  let text;
16692
- if (args["text-file"]) text = await readFile7(path10.resolve(String(args["text-file"])), "utf8");
16860
+ if (args["text-file"]) text = await readFile7(path11.resolve(String(args["text-file"])), "utf8");
16693
16861
  else if (args.text !== void 0) text = String(args.text);
16694
16862
  else {
16695
16863
  process.stderr.write(
@@ -16710,7 +16878,7 @@ var setPromptCommand = defineCommand88({
16710
16878
  process.exit(2);
16711
16879
  return;
16712
16880
  }
16713
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path10.dirname(filePath)), defaultRegistry());
16881
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path11.dirname(filePath)), defaultRegistry());
16714
16882
  if (!validation.ok) {
16715
16883
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
16716
16884
  `);
@@ -16726,7 +16894,7 @@ var setPromptCommand = defineCommand88({
16726
16894
 
16727
16895
  // src/commands/canvas/validate.ts
16728
16896
  import { readFile as readFile8 } from "fs/promises";
16729
- import path11 from "path";
16897
+ import path12 from "path";
16730
16898
  import { defineCommand as defineCommand89 } from "citty";
16731
16899
  var validateCommand = defineCommand89({
16732
16900
  meta: {
@@ -16735,7 +16903,7 @@ var validateCommand = defineCommand89({
16735
16903
  },
16736
16904
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
16737
16905
  async run({ args }) {
16738
- const filePath = path11.resolve(String(args.file));
16906
+ const filePath = path12.resolve(String(args.file));
16739
16907
  const raw = await readFile8(filePath, "utf8");
16740
16908
  let parsed;
16741
16909
  try {
@@ -16746,7 +16914,7 @@ var validateCommand = defineCommand89({
16746
16914
  `);
16747
16915
  process.exit(2);
16748
16916
  }
16749
- parsed = resolveRelativeCanvasPaths(parsed, path11.dirname(filePath));
16917
+ parsed = resolveRelativeCanvasPaths(parsed, path12.dirname(filePath));
16750
16918
  const result = await validateCanvasDeep(parsed, defaultRegistry());
16751
16919
  if (!result.ok) {
16752
16920
  process.stderr.write(`${JSON.stringify({ ok: false, issues: result.issues }, null, 2)}
@@ -16883,6 +17051,16 @@ registerSchema({
16883
17051
  type: "string",
16884
17052
  description: "Optional URL of the original reference ad",
16885
17053
  required: false
17054
+ },
17055
+ slug: {
17056
+ type: "string",
17057
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17058
+ required: false
17059
+ },
17060
+ runId: {
17061
+ type: "string",
17062
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17063
+ required: false
16886
17064
  }
16887
17065
  }
16888
17066
  });
@@ -16892,6 +17070,13 @@ function detectCreativeContentType(filePath) {
16892
17070
  unsupportedMessage: "Unsupported creative image extension. Use PNG, JPG, or WebP."
16893
17071
  });
16894
17072
  }
17073
+ function chatIdFromEnv() {
17074
+ try {
17075
+ return getEnv().BAKER_CHAT_ID || void 0;
17076
+ } catch {
17077
+ return void 0;
17078
+ }
17079
+ }
16895
17080
  function parseOptionalUrl(value) {
16896
17081
  if (value === void 0 || value.trim() === "") {
16897
17082
  return void 0;
@@ -16922,7 +17107,11 @@ async function publishCreative(args, deps = defaultImageApiDeps) {
16922
17107
  return publishImageAsCreative(deps, {
16923
17108
  imageId: upload.imageId,
16924
17109
  title,
16925
- sourceReferenceUrl
17110
+ sourceReferenceUrl,
17111
+ slug: args.slug,
17112
+ runId: args.runId,
17113
+ // Attribute the publish to the driving chat (injected by the bridge).
17114
+ chatId: chatIdFromEnv()
16926
17115
  });
16927
17116
  }
16928
17117
  var publishCommand = defineCommand91({
@@ -16938,6 +17127,16 @@ var publishCommand = defineCommand91({
16938
17127
  type: "string",
16939
17128
  description: "Optional URL of the original reference ad",
16940
17129
  required: false
17130
+ },
17131
+ slug: {
17132
+ type: "string",
17133
+ description: "Creative slug (src/creatives/<slug>/) \u2014 attaches the image to that creative's row",
17134
+ required: false
17135
+ },
17136
+ runId: {
17137
+ type: "string",
17138
+ description: "Canvas run id (r_\u2026) of the approved generation to pin as published",
17139
+ required: false
16941
17140
  }
16942
17141
  },
16943
17142
  run: async ({ args }) => {
@@ -16956,7 +17155,9 @@ var publishCommand = defineCommand91({
16956
17155
  file,
16957
17156
  title,
16958
17157
  context: args.context,
16959
- sourceReferenceUrl: args.sourceReferenceUrl
17158
+ sourceReferenceUrl: args.sourceReferenceUrl,
17159
+ slug: args.slug,
17160
+ runId: args.runId
16960
17161
  });
16961
17162
  writeJson({ ok: true, data });
16962
17163
  } catch (err) {
@@ -17854,9 +18055,9 @@ async function readImageBuffer(pathOrUrl) {
17854
18055
  }
17855
18056
  return readFile10(pathOrUrl);
17856
18057
  }
17857
- async function isDirectory(path12) {
18058
+ async function isDirectory(path13) {
17858
18059
  try {
17859
- const s = await stat2(path12);
18060
+ const s = await stat2(path13);
17860
18061
  return s.isDirectory();
17861
18062
  } catch {
17862
18063
  return false;