@koda-sl/baker-cli 0.180.1 → 0.182.0-dev.3c0641b3f

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
@@ -25,6 +25,7 @@ import {
25
25
  isPersistedAssetRef,
26
26
  looksLikeHttpUrl,
27
27
  nearestSupportedAspectRatio,
28
+ nearestSupportedImageSize,
28
29
  parseRefExpr,
29
30
  platformFormats,
30
31
  requireCredentialsFromEnv,
@@ -32,11 +33,12 @@ import {
32
33
  sha256Hex,
33
34
  spineInputFlags,
34
35
  spineInputOps,
36
+ supportsLastFrame,
35
37
  supportsParam,
36
38
  toModelSafeImage,
37
39
  ulid,
38
40
  validateCanvasDeep
39
- } from "./chunk-QPVJGKV7.js";
41
+ } from "./chunk-UZ37VVP4.js";
40
42
  import {
41
43
  csvOrJson,
42
44
  daysAgoIso,
@@ -79,7 +81,7 @@ import {
79
81
  } from "./chunk-YL3HDEIJ.js";
80
82
 
81
83
  // src/cli.ts
82
- import { defineCommand as defineCommand191, runMain } from "citty";
84
+ import { defineCommand as defineCommand199, runMain } from "citty";
83
85
 
84
86
  // src/commands/actions/index.ts
85
87
  import { defineCommand as defineCommand18 } from "citty";
@@ -3412,6 +3414,7 @@ var imageGenerateModelSchema = z11.enum([
3412
3414
  // Legacy — see the registry entry; kept so pre-switch canvases still run.
3413
3415
  "openai/gpt-5.4-image-2",
3414
3416
  "google/gemini-3.1-flash-image-preview",
3417
+ "google/gemini-3.1-flash-lite-image",
3415
3418
  "google/gemini-3-pro-image-preview",
3416
3419
  "recraft/recraft-v4.1-pro-vector"
3417
3420
  ]);
@@ -7385,19 +7388,19 @@ Examples:
7385
7388
  },
7386
7389
  run: async ({ args }) => {
7387
7390
  const customerId = await resolveCustomerId(args);
7388
- const window = resolveChangesWindow({
7391
+ const window2 = resolveChangesWindow({
7389
7392
  days: args.days ? Number(args.days) : void 0,
7390
7393
  scope: args.scope
7391
7394
  });
7392
- if (!window.ok) {
7393
- writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
7395
+ if (!window2.ok) {
7396
+ writeJsonEnvelope({ ok: false, error: { ...window2.error, retryable: false } });
7394
7397
  process.exit(1);
7395
7398
  return;
7396
7399
  }
7397
7400
  const body = {
7398
7401
  customerId,
7399
- days: window.days,
7400
- scope: window.scope,
7402
+ days: window2.days,
7403
+ scope: window2.scope,
7401
7404
  limit: args.limit ? Number(args.limit) : 50
7402
7405
  };
7403
7406
  const managerId = getManagerIdForCustomer(customerId);
@@ -7414,7 +7417,7 @@ Examples:
7414
7417
  const first = data[0];
7415
7418
  const fields = first ? Object.keys(first) : [];
7416
7419
  const fieldDescs = getFieldDescriptions(fields);
7417
- writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
7420
+ writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window2.hints });
7418
7421
  } catch (err) {
7419
7422
  if (err instanceof ApiError) {
7420
7423
  writeAdsJson(parseApiError(err.message, "", customerId, err.code));
@@ -7771,8 +7774,8 @@ function countStaccato(text) {
7771
7774
  let first = "";
7772
7775
  let runStart = 0;
7773
7776
  for (const [i, s] of sentences.entries()) {
7774
- const words2 = s.split(/\s+/).filter(Boolean).length;
7775
- if (words2 > 0 && words2 <= STACCATO_MAX_WORDS) {
7777
+ const words3 = s.split(/\s+/).filter(Boolean).length;
7778
+ if (words3 > 0 && words3 <= STACCATO_MAX_WORDS) {
7776
7779
  if (run === 0) runStart = i;
7777
7780
  run++;
7778
7781
  if (run === STACCATO_RUN) {
@@ -7790,10 +7793,10 @@ function countTitleCaseHeadings(text) {
7790
7793
  let first = "";
7791
7794
  for (const m of text.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
7792
7795
  const heading = (m[1] ?? "").trim();
7793
- const words2 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
7794
- if (words2.length < 4) continue;
7795
- const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
7796
- if (capitalized / words2.length < 0.8) continue;
7796
+ const words3 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
7797
+ if (words3.length < 4) continue;
7798
+ const capitalized = words3.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
7799
+ if (capitalized / words3.length < 0.8) continue;
7797
7800
  count++;
7798
7801
  if (!first) first = heading;
7799
7802
  }
@@ -8122,11 +8125,11 @@ function rawTextEntries(value) {
8122
8125
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
8123
8126
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
8124
8127
  }
8125
- function rawFileEntries(path28) {
8126
- if (typeof path28 !== "string" || path28.length === 0) {
8128
+ function rawFileEntries(path34) {
8129
+ if (typeof path34 !== "string" || path34.length === 0) {
8127
8130
  return [];
8128
8131
  }
8129
- return readFileSync2(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8132
+ return readFileSync2(path34, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8130
8133
  }
8131
8134
  function keywordEntries(args) {
8132
8135
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -8149,19 +8152,19 @@ function keywordEntries(args) {
8149
8152
  }
8150
8153
  return entries;
8151
8154
  }
8152
- function loadJsonFileArg(path28) {
8153
- if (typeof path28 !== "string" || path28.length === 0) {
8155
+ function loadJsonFileArg(path34) {
8156
+ if (typeof path34 !== "string" || path34.length === 0) {
8154
8157
  return {};
8155
8158
  }
8156
8159
  try {
8157
- const parsed = JSON.parse(readFileSync2(path28, "utf8"));
8160
+ const parsed = JSON.parse(readFileSync2(path34, "utf8"));
8158
8161
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8159
- failWriteValidation(`${path28} must contain a JSON object`);
8162
+ failWriteValidation(`${path34} must contain a JSON object`);
8160
8163
  }
8161
8164
  return parsed;
8162
8165
  } catch (err) {
8163
8166
  if (err instanceof SyntaxError) {
8164
- failWriteValidation(`${path28} is not valid JSON: ${err.message}`);
8167
+ failWriteValidation(`${path34} is not valid JSON: ${err.message}`);
8165
8168
  }
8166
8169
  throw err;
8167
8170
  }
@@ -8291,10 +8294,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
8291
8294
  async function stageTarget(kind, customerId, target, hints) {
8292
8295
  await stageGoogleOp({ kind, customerId, target }, hints);
8293
8296
  }
8294
- async function draftAction(path28, body, chat) {
8297
+ async function draftAction(path34, body, chat) {
8295
8298
  try {
8296
8299
  const chatId = resolveChatId(chat);
8297
- const response = await apiPost(path28, { chatId, ...body });
8300
+ const response = await apiPost(path34, { chatId, ...body });
8298
8301
  writeJsonEnvelope(response);
8299
8302
  } catch (err) {
8300
8303
  handleGoogleError(err);
@@ -10962,19 +10965,19 @@ function failWriteValidation2(message) {
10962
10965
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
10963
10966
  process.exit(1);
10964
10967
  }
10965
- function loadJsonFileArg2(path28) {
10966
- if (typeof path28 !== "string" || path28.length === 0) {
10968
+ function loadJsonFileArg2(path34) {
10969
+ if (typeof path34 !== "string" || path34.length === 0) {
10967
10970
  return {};
10968
10971
  }
10969
10972
  try {
10970
- const parsed = JSON.parse(readFileSync4(path28, "utf8"));
10973
+ const parsed = JSON.parse(readFileSync4(path34, "utf8"));
10971
10974
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
10972
- failWriteValidation2(`${path28} must contain a JSON object`);
10975
+ failWriteValidation2(`${path34} must contain a JSON object`);
10973
10976
  }
10974
10977
  return parsed;
10975
10978
  } catch (err) {
10976
10979
  if (err instanceof SyntaxError) {
10977
- failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
10980
+ failWriteValidation2(`${path34} is not valid JSON: ${err.message}`);
10978
10981
  }
10979
10982
  throw err;
10980
10983
  }
@@ -11059,15 +11062,15 @@ function parseLocaleFlag(value) {
11059
11062
  }
11060
11063
  return { language: match[1], country: match[2].toUpperCase() };
11061
11064
  }
11062
- function loadTargetingFileArg(path28) {
11063
- if (typeof path28 !== "string" || path28.length === 0) {
11065
+ function loadTargetingFileArg(path34) {
11066
+ if (typeof path34 !== "string" || path34.length === 0) {
11064
11067
  return void 0;
11065
11068
  }
11066
- const parsed = loadJsonFileArg2(path28);
11069
+ const parsed = loadJsonFileArg2(path34);
11067
11070
  const criteria = parsed.targetingCriteria ?? parsed;
11068
11071
  if (!criteria.include) {
11069
11072
  failWriteValidation2(
11070
- `${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11073
+ `${path34} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11071
11074
  );
11072
11075
  }
11073
11076
  return criteria;
@@ -11102,14 +11105,14 @@ function parseCsvLine(line) {
11102
11105
  cells.push(current);
11103
11106
  return cells.map((cell) => cell.trim());
11104
11107
  }
11105
- function parseListFileArg(path28, maxRows) {
11106
- if (typeof path28 !== "string" || path28.length === 0) {
11108
+ function parseListFileArg(path34, maxRows) {
11109
+ if (typeof path34 !== "string" || path34.length === 0) {
11107
11110
  return void 0;
11108
11111
  }
11109
- const raw = readFileSync4(path28, "utf8");
11112
+ const raw = readFileSync4(path34, "utf8");
11110
11113
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
11111
11114
  if (lines.length < 2) {
11112
- failWriteValidation2(`${path28} needs a header row and at least one data row`);
11115
+ failWriteValidation2(`${path34} needs a header row and at least one data row`);
11113
11116
  }
11114
11117
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
11115
11118
  const rows = [];
@@ -11128,7 +11131,7 @@ function parseListFileArg(path28, maxRows) {
11128
11131
  }
11129
11132
  }
11130
11133
  if (rows.length > maxRows) {
11131
- failWriteValidation2(`${path28} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
11134
+ failWriteValidation2(`${path34} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
11132
11135
  }
11133
11136
  return { columns, rows };
11134
11137
  }
@@ -11224,11 +11227,11 @@ function readPositionals(args) {
11224
11227
  function splitIdList(raw) {
11225
11228
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
11226
11229
  }
11227
- function idsFileEntries(path28) {
11228
- if (typeof path28 !== "string" || path28.length === 0) {
11230
+ function idsFileEntries(path34) {
11231
+ if (typeof path34 !== "string" || path34.length === 0) {
11229
11232
  return [];
11230
11233
  }
11231
- return readFileSync4(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
11234
+ return readFileSync4(path34, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
11232
11235
  }
11233
11236
  function requireTargets(args, entity) {
11234
11237
  const positionals = readPositionals(args);
@@ -13851,9 +13854,9 @@ function compactRow(row) {
13851
13854
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
13852
13855
  };
13853
13856
  }
13854
- function readPath(row, path28) {
13857
+ function readPath(row, path34) {
13855
13858
  let current = row;
13856
- for (const segment of path28.split(".")) {
13859
+ for (const segment of path34.split(".")) {
13857
13860
  const record = asRecord2(current);
13858
13861
  if (!record) return void 0;
13859
13862
  current = record[segment];
@@ -13863,10 +13866,10 @@ function readPath(row, path28) {
13863
13866
  function projectFields(rows, paths) {
13864
13867
  return rows.map((row) => {
13865
13868
  const projected = {};
13866
- for (const path28 of paths) {
13867
- const value = readPath(row, path28);
13869
+ for (const path34 of paths) {
13870
+ const value = readPath(row, path34);
13868
13871
  if (value !== void 0) {
13869
- projected[path28] = value;
13872
+ projected[path34] = value;
13870
13873
  }
13871
13874
  }
13872
13875
  return projected;
@@ -15073,11 +15076,11 @@ var updateStatusSchema = z18.enum(UPDATE_STATUSES);
15073
15076
  function currencyMinimums2(currencyCode) {
15074
15077
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
15075
15078
  }
15076
- function validateDailyBudgetFloor(money, ctx, path28) {
15079
+ function validateDailyBudgetFloor(money, ctx, path34) {
15077
15080
  if (money?.currencyCode) {
15078
15081
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
15079
15082
  if (Number(money.amount) < min) {
15080
- ctx.addIssue({ code: "custom", path: path28, message: `below the ${min} ${money.currencyCode} daily minimum` });
15083
+ ctx.addIssue({ code: "custom", path: path34, message: `below the ${min} ${money.currencyCode} daily minimum` });
15081
15084
  }
15082
15085
  }
15083
15086
  }
@@ -15720,19 +15723,19 @@ function failWriteValidation3(message) {
15720
15723
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
15721
15724
  process.exit(1);
15722
15725
  }
15723
- function loadJsonFileArg3(path28) {
15724
- if (typeof path28 !== "string" || path28.length === 0) {
15726
+ function loadJsonFileArg3(path34) {
15727
+ if (typeof path34 !== "string" || path34.length === 0) {
15725
15728
  return {};
15726
15729
  }
15727
15730
  try {
15728
- const parsed = JSON.parse(readFileSync8(path28, "utf8"));
15731
+ const parsed = JSON.parse(readFileSync8(path34, "utf8"));
15729
15732
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
15730
- failWriteValidation3(`${path28} must contain a JSON object`);
15733
+ failWriteValidation3(`${path34} must contain a JSON object`);
15731
15734
  }
15732
15735
  return parsed;
15733
15736
  } catch (err) {
15734
15737
  if (err instanceof SyntaxError) {
15735
- failWriteValidation3(`${path28} is not valid JSON: ${err.message}`);
15738
+ failWriteValidation3(`${path34} is not valid JSON: ${err.message}`);
15736
15739
  }
15737
15740
  throw err;
15738
15741
  }
@@ -19770,6 +19773,20 @@ function videoResolutionParam(videoModel, resolution) {
19770
19773
  if (!VIDEO_MODELS_WITH_RESOLUTION.has(videoModel)) return {};
19771
19774
  return { resolution: resolution ?? DEFAULT_VIDEO_RESOLUTION };
19772
19775
  }
19776
+ var VIDEO_MODELS_WITH_DURATION = new Set(
19777
+ Object.entries(MODEL_REGISTRY.video_generate).filter(([, spec]) => "duration" in spec.params).map(([id]) => id)
19778
+ );
19779
+ var VIDEO_MODELS_WITH_AUDIO_TOGGLE = new Set(
19780
+ Object.entries(MODEL_REGISTRY.video_generate).filter(([, spec]) => "generate_audio" in spec.params).map(([id]) => id)
19781
+ );
19782
+ function videoDurationParam(videoModel, duration) {
19783
+ if (!VIDEO_MODELS_WITH_DURATION.has(videoModel)) return {};
19784
+ return { duration };
19785
+ }
19786
+ function videoAudioParam(videoModel, generateAudio) {
19787
+ if (!VIDEO_MODELS_WITH_AUDIO_TOGGLE.has(videoModel)) return {};
19788
+ return { generate_audio: generateAudio };
19789
+ }
19773
19790
  var WORDS_PER_SECOND = 2.5;
19774
19791
  function wordCount(text) {
19775
19792
  return text.trim().split(/\s+/).filter(Boolean).length;
@@ -19780,11 +19797,11 @@ function estSpeechS(text) {
19780
19797
  var OBSERVED_WPS_MIN = 1;
19781
19798
  var OBSERVED_WPS_MAX = 6;
19782
19799
  function estSpeechWindowS(text, startS, endS) {
19783
- const words2 = wordCount(text);
19784
- const window = (endS ?? 0) - (startS ?? 0);
19785
- if (words2 > 0 && window > 0.3) {
19786
- const wps = words2 / window;
19787
- if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window;
19800
+ const words3 = wordCount(text);
19801
+ const window2 = (endS ?? 0) - (startS ?? 0);
19802
+ if (words3 > 0 && window2 > 0.3) {
19803
+ const wps = words3 / window2;
19804
+ if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
19788
19805
  }
19789
19806
  return estSpeechS(text);
19790
19807
  }
@@ -20374,6 +20391,7 @@ function extendPresenceByPromptMentions(slots, blueprint) {
20374
20391
  });
20375
20392
  }
20376
20393
  var ACTOR_SHEET_MODEL = "google/gemini-3-pro-image-preview";
20394
+ var KEYFRAME_IMAGE_SIZE = "2K";
20377
20395
  var SHEET_SUBJECT_TYPE = {
20378
20396
  person: "person",
20379
20397
  animal: "character",
@@ -20563,8 +20581,11 @@ function buildFrameRef(edge, url, framePrompt, present2, ctx, nodes) {
20563
20581
  const genParams = {
20564
20582
  model: ctx.imageModel,
20565
20583
  // gpt-image-2 derives pixel dimensions from the ratio and has no size knob,
20566
- // so asking for 2K there is an `unknown_param` at validate.
20567
- ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: "2K" } : {},
20584
+ // so asking for 2K there is an `unknown_param` at validate. Models that DO take
20585
+ // one still differ in how far up they go — Nano Banana 2 Lite renders 1K only —
20586
+ // so snap to the best tier the chosen model actually offers rather than pinning
20587
+ // a literal that a cheaper model would reject outright.
20588
+ ...supportsParam("image_generate", ctx.imageModel, "image_size") ? { image_size: nearestSupportedImageSize("image_generate", ctx.imageModel, KEYFRAME_IMAGE_SIZE) } : {},
20568
20589
  // Per-model image defaults (gpt-image: quality=high — OpenRouter forwards it; we do
20569
20590
  // NOT send input_fidelity, which gpt-image-2 forces high automatically).
20570
20591
  ...imageProfile?.paramDefaults ?? {},
@@ -20635,10 +20656,10 @@ function scrubFloatSentences(text, floatDescs) {
20635
20656
  if (floatDescs.length === 0 || !text) return text;
20636
20657
  const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
20637
20658
  const kept = text.split(/(?<=[.!?])\s+/).filter((sentence) => {
20638
- const words2 = new Set(floatTokens(sentence));
20659
+ const words3 = new Set(floatTokens(sentence));
20639
20660
  return !tokenSets.some((ts) => {
20640
20661
  let hits = 0;
20641
- for (const w of words2) if (ts.has(w)) hits++;
20662
+ for (const w of words3) if (ts.has(w)) hits++;
20642
20663
  return hits >= 2;
20643
20664
  });
20644
20665
  }).join(" ").trim();
@@ -20854,11 +20875,11 @@ function emitSceneClip(i, scene, present2, mode, nativeTurn, ambientBroll, frame
20854
20875
  opts.uiRouted,
20855
20876
  opts.videoModel
20856
20877
  ),
20857
- duration: lengths.genDur,
20878
+ ...videoDurationParam(opts.videoModel, lengths.genDur),
20858
20879
  ...videoResolutionParam(opts.videoModel, opts.resolution),
20859
- // Native talking scene → Seedance generates the spoken audio + lip-sync; an opt-in
20880
+ // Native talking scene → the model generates the spoken audio + lip-sync; an opt-in
20860
20881
  // ambient b-roll beat generates diegetic ambient only; otherwise the clip is silent.
20861
- generate_audio: Boolean(nativeTurn) || ambientBroll,
20882
+ ...videoAudioParam(opts.videoModel, Boolean(nativeTurn) || ambientBroll),
20862
20883
  // Intent-keyed "astonishing default" overrides — merged LAST so a hero/reveal
20863
20884
  // beat can claim the 1080p ceiling over the aspect-derived resolution.
20864
20885
  ...clipParamRecipe(profile, clipIntentOf(scene, i))
@@ -20867,7 +20888,13 @@ function emitSceneClip(i, scene, present2, mode, nativeTurn, ambientBroll, frame
20867
20888
  nodes.push({
20868
20889
  id: `s${i}${tag}_clip`,
20869
20890
  type: "video_generate",
20870
- inputs: { first_frame: frames.first, ...frames.last ? { last_frame: frames.last } : {} },
20891
+ // An end frame can exist purely as the NEXT scene's splice frame, so having one
20892
+ // is not permission to wire it — a model that takes a single conditioning image
20893
+ // silently drops it.
20894
+ inputs: {
20895
+ first_frame: frames.first,
20896
+ ...frames.last && supportsLastFrame(opts.videoModel) ? { last_frame: frames.last } : {}
20897
+ },
20871
20898
  params: clipParams
20872
20899
  });
20873
20900
  const base = `$ref:s${i}${tag}_clip.video`;
@@ -21010,7 +21037,7 @@ function buildCompositeScene(layout, regions, comp, scene, i, present2, mode, na
21010
21037
  const startPrompt = region.frame_prompt ?? scene.start_frame_prompt;
21011
21038
  const endPrompt = region.frame_prompt ?? scene.end_frame_prompt;
21012
21039
  const first = buildFrameRef("start", void 0, startPrompt, regionSlots, ctx, nodes);
21013
- const last = buildFrameRef("end", void 0, endPrompt, regionSlots, ctx, nodes);
21040
+ const last = supportsLastFrame(opts.videoModel) ? buildFrameRef("end", void 0, endPrompt, regionSlots, ctx, nodes) : void 0;
21014
21041
  const regionNative = isPresenter ? nativeTurn : void 0;
21015
21042
  const regionScene = {
21016
21043
  ...scene,
@@ -21578,14 +21605,16 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
21578
21605
  );
21579
21606
  const lastShown = phrase.shownScenes[phrase.shownScenes.length - 1] ?? anchor;
21580
21607
  const lastScene = env.blueprint.scenes[lastShown] ?? anchorScene;
21581
- const last = buildFrameRef(
21608
+ const modelTakesLastFrame = supportsLastFrame(env.opts.videoModel);
21609
+ const nextSplicesFromEnd = Boolean(env.blueprint.scenes[lastShown + 1]?.continues_previous);
21610
+ const last = modelTakesLastFrame || nextSplicesFromEnd ? buildFrameRef(
21582
21611
  "end",
21583
21612
  lastScene.end_frame_asset?.url,
21584
21613
  lastScene.end_frame_prompt,
21585
21614
  slotsForFrame(env.slots, lastShown, "end"),
21586
21615
  ctx,
21587
21616
  nodes
21588
- );
21617
+ ) : void 0;
21589
21618
  const clipStart = phrase.shownScenes.reduce(
21590
21619
  (m, s) => Math.min(m, env.blueprint.scenes[s]?.start_s ?? phrase.start_s),
21591
21620
  phrase.start_s
@@ -21608,19 +21637,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
21608
21637
  env.uiRouted.has(anchor),
21609
21638
  env.opts.videoModel
21610
21639
  ),
21611
- duration: genDur,
21640
+ ...videoDurationParam(env.opts.videoModel, genDur),
21612
21641
  ...videoResolutionParam(env.opts.videoModel, env.opts.resolution),
21613
- generate_audio: true,
21642
+ ...videoAudioParam(env.opts.videoModel, true),
21614
21643
  ...clipParamRecipe(clipProfile, clipIntentOf(anchorScene, anchor))
21615
21644
  };
21616
21645
  clipParams.aspect_ratio = env.genAr;
21617
21646
  nodes.push({
21618
21647
  id: `s${anchor}_clip`,
21619
21648
  type: "video_generate",
21620
- inputs: { first_frame: first, last_frame: last },
21649
+ inputs: { first_frame: first, ...last && modelTakesLastFrame ? { last_frame: last } : {} },
21621
21650
  params: clipParams
21622
21651
  });
21623
- out.phraseEnd = { ref: last, scene: lastShown };
21652
+ out.phraseEnd = last ? { ref: last, scene: lastShown } : void 0;
21624
21653
  const clipRef = `$ref:s${anchor}_clip.video`;
21625
21654
  const speechOffset = Math.max(0, phrase.start_s - clipStart);
21626
21655
  const extractLen = Math.min(Math.max(0.5, phrase.end_s - phrase.start_s), Math.max(0.5, genDur - speechOffset));
@@ -21828,14 +21857,15 @@ function brollKeyframes(scene, i, env, ctx, lengths, prevEndFrame, nodes) {
21828
21857
  nodes
21829
21858
  );
21830
21859
  const nextContinues = Boolean(env.blueprint.scenes[i + 1]?.continues_previous);
21831
- const last = lengths.trimTarget <= 4 && !nextContinues ? void 0 : buildFrameRef(
21860
+ const guidesMotion = supportsLastFrame(env.opts.videoModel) && lengths.trimTarget > 4;
21861
+ const last = guidesMotion || nextContinues ? buildFrameRef(
21832
21862
  "end",
21833
21863
  scene.end_frame_asset?.url,
21834
21864
  scene.end_frame_prompt,
21835
21865
  slotsForFrame(env.slots, i, "end"),
21836
21866
  ctx,
21837
21867
  nodes
21838
- );
21868
+ ) : void 0;
21839
21869
  return { first, last, sharesPrevFrame };
21840
21870
  }
21841
21871
  function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
@@ -22685,7 +22715,7 @@ var VIDEO_GUIDE = [
22685
22715
  "1. Edit each frame's prompt IN PLACE. Every `s<i>_start` keyframe node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want. The frame is RECAST to the el_* reference images you drop (the source ad's people are never reused), so describe pose/action/framing here and let the references carry identity.",
22686
22716
  "1b. STORYBOARD FIRST \u2014 align the look on the cheap stills before clips bill. Each scene's keyframe IS your storyboard; `metadata.video.motion_board` lays out each scene's frame, time window, spoken line, and the graphics scheduled in it. Lock the keyframes + check each graphic lands on its spoken beat, THEN run the clips (images are cheap, videos aren't; the cache re-bills only what you change). See references/video-flow.md.",
22687
22717
  "2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
22688
- "3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by Seedance itself (the line is in the clip's prompt + `generate_audio`), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
22718
+ "3. Talking heads are NATIVE: a scene with one on-camera speaker is voiced by the video model itself (the line goes in the clip's prompt; models with a `generate_audio` toggle get it set, and the default model always renders audio), so lips and voice are generated together \u2014 no separate tts, no post-hoc lip-sync. Edit the line in the scene's `s<i>_clip` prompt to re-author the words TRUE for your brand. Off-camera narration scenes still use a sequenced `tts` per turn.",
22689
22719
  "4. Voice consistency: every native talking clip's audio is re-voiced to ONE brand voice via `audio_voice_convert` (timing preserved \u2192 lips stay matched). Confirm the `voice_select` casting (one per speaker) \u2014 its `voice_id` is the brand voice; set its gender/language so the voice matches the creator.",
22690
22720
  "5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you source (`baker images icon/sticker/gif/logo`) and drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
22691
22721
  "6. `baker canvas validate` (proves native-audio + timing for free) then `baker canvas run` (generates many billed image/video/audio assets \u2014 not free).",
@@ -22762,9 +22792,9 @@ function buildVideoTodo(report, overlayCount, floatingCount, opts, blueprint) {
22762
22792
  voice_description: d.voice_description,
22763
22793
  line: d.line
22764
22794
  })),
22765
- talking_head_note: "SHOT-NATIVE: a presenter shot is ONE Seedance clip (its line quoted in s<anchor>_clip's prompt + generate_audio) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip whose on-camera windows are cut inside the spine's per-input chains (no extra nodes). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
22795
+ talking_head_note: "SHOT-NATIVE: a presenter shot is ONE video clip (its line quoted in s<anchor>_clip's prompt, plus generate_audio on the models that expose it) so lips+voice are generated together \u2014 no tts, no veed-lipsync. Two adjacent presenter shots at a hard cut are SEPARATE clips; a cutaway phrase (the presenter on camera, cut to b-roll, back on camera) stays one clip whose on-camera windows are cut inside the spine's per-input chains (no extra nodes). Edit the line in the s<anchor>_clip prompt to re-author it. A pure-voiceover phrase (speaker never shown) is one ElevenLabs tts read instead.",
22766
22796
  voice_note: "ONE voice per person: a single voice_select is reused across all that person's shots (on-camera AND off \u2014 the deconstruct's `voiceover` label folds into the sole presenter). Every presenter clip's native audio is extracted and re-voiced to that brand voice through a SINGLE merged audio_voice_convert per speaker (<voice>_conv, eleven_multilingual_sts_v2, timing preserved so lips stay matched) \u2014 so timbre stays consistent across the separate shot clips. Set voice_select.voice_id's gender/language to match the creator.",
22767
- native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 Seedance exposes no word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point). NOTE: a native clip's voice is extracted for the SPOKEN window only (`s<i>_voextract`'s `-t`) \u2014 when the scene runs longer than the line, the picture tail is deliberately silent (extracting the full clip would put post-line breathing/babble on the voice bus); trim the scene or extend the line if the dead air reads wrong.",
22797
+ native_timing: "Clips separate at COMPLETE BREAKS between shots, but the VOICE stays continuous where it should: a voiceover narration is ONE read across the b-roll it plays over, and a cutaway leaves the presenter's read continuous under the insert. Each clip is generated long enough for its estimated speech. `metadata.video.talking_scenes` carries each shot's scene_s vs est_speech_s. CAVEAT: a b-roll cutaway INSIDE a phrase lands at an approximate (proportional) time \u2014 no video model exposes word timing \u2014 so if a cutaway is off its beat, nudge the scene boundary (it's a starting point). NOTE: a native clip's voice is extracted for the SPOKEN window only (`s<i>_voextract`'s `-t`) \u2014 when the scene runs longer than the line, the picture tail is deliberately silent (extracting the full clip would put post-line breathing/babble on the voice bus); trim the scene or extend the line if the dead air reads wrong.",
22768
22798
  craft: {
22769
22799
  note: "Production-craft principles that raise every clip's realism. Full rationale: references/video-craft.md (production craft); references/script-craft.md + meta-ads-playbook for the hook/message layer.",
22770
22800
  principles: [
@@ -22820,15 +22850,15 @@ function collectClipAdvisories(scene, i, out) {
22820
22850
  const round22 = (n) => Math.round(n * 100) / 100;
22821
22851
  const original = scene.duration_s ?? 5;
22822
22852
  if (original > 15) out.clamped.push({ scene: i, original_s: original, clip_s: snapToSeedance(original) });
22823
- const window = sceneDurationS(scene);
22824
- if (window > SEEDANCE_SAFE_MAX_S)
22825
- out.oversize.push({ scene: i, scene_s: round22(window), clip_s: ceilToSeedance(window) });
22853
+ const window2 = sceneDurationS(scene);
22854
+ if (window2 > SEEDANCE_SAFE_MAX_S)
22855
+ out.oversize.push({ scene: i, scene_s: round22(window2), clip_s: ceilToSeedance(window2) });
22826
22856
  const speech = (scene.dialogue ?? []).reduce(
22827
22857
  (s, line) => s + (line.line ? estSpeechWindowS(line.line, line.start_s, line.end_s) : 0),
22828
22858
  0
22829
22859
  );
22830
- if (speech > window * OVERSTUFF_RATIO)
22831
- out.overstuffed.push({ scene: i, scene_s: round22(window), est_speech_s: round22(speech) });
22860
+ if (speech > window2 * OVERSTUFF_RATIO)
22861
+ out.overstuffed.push({ scene: i, scene_s: round22(window2), est_speech_s: round22(speech) });
22832
22862
  }
22833
22863
  function videoReport(input, elementsInput) {
22834
22864
  const blueprint = VideoBlueprint.parse(input);
@@ -24967,6 +24997,7 @@ var SEEDANCE = "bytedance/seedance-2.0";
24967
24997
  var VEO = "google/veo-3.1";
24968
24998
  var VEO_FAST = "google/veo-3.1-fast";
24969
24999
  var KLING = "kwaivgi/kling-v3.0-pro";
25000
+ var GEMINI_OMNI = "google/gemini-omni-flash";
24970
25001
  function routeVideoModel(input) {
24971
25002
  const budget = input.budget ?? "standard";
24972
25003
  const signals = [];
@@ -24979,8 +25010,8 @@ function routeVideoModel(input) {
24979
25010
  if (input.needsIdentity && !input.hasRealFace) {
24980
25011
  signals.push({ model: SEEDANCE, weight: 300, because: "identity/product consistency \u2192 Seedance workhorse" });
24981
25012
  }
24982
- signals.push({ model: SEEDANCE, weight: 100, because: "default identity/product workhorse" });
24983
- const scores = { [SEEDANCE]: 0, [VEO]: 0, [VEO_FAST]: 0, [KLING]: 0 };
25013
+ signals.push({ model: GEMINI_OMNI, weight: 100, because: "default \u2014 native audio in one call" });
25014
+ const scores = { [SEEDANCE]: 0, [VEO]: 0, [VEO_FAST]: 0, [KLING]: 0, [GEMINI_OMNI]: 0 };
24984
25015
  let top = signals[0];
24985
25016
  for (const s of signals) {
24986
25017
  scores[s.model] = (scores[s.model] ?? 0) + s.weight;
@@ -26500,12 +26531,12 @@ function listFlowSlugs() {
26500
26531
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
26501
26532
  }
26502
26533
  function readFlowTree(slug) {
26503
- const path28 = join3(flowsDir(), slug, "_data.json");
26504
- if (!existsSync4(path28)) {
26534
+ const path34 = join3(flowsDir(), slug, "_data.json");
26535
+ if (!existsSync4(path34)) {
26505
26536
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
26506
26537
  }
26507
26538
  try {
26508
- return JSON.parse(readFileSync9(path28, "utf-8"));
26539
+ return JSON.parse(readFileSync9(path34, "utf-8"));
26509
26540
  } catch (error) {
26510
26541
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
26511
26542
  }
@@ -26918,10 +26949,10 @@ async function stageOps(ops) {
26918
26949
  handleError(err);
26919
26950
  }
26920
26951
  }
26921
- async function draftAction2(path28, body, chat) {
26952
+ async function draftAction2(path34, body, chat) {
26922
26953
  const chatId = resolveChatId(chat);
26923
26954
  try {
26924
- const data = await apiPost(path28, { chatId, ...body });
26955
+ const data = await apiPost(path34, { chatId, ...body });
26925
26956
  writeJsonEnvelope({ ok: true, data });
26926
26957
  return data;
26927
26958
  } catch (err) {
@@ -28945,9 +28976,9 @@ async function readImageBuffer(pathOrUrl) {
28945
28976
  }
28946
28977
  return readFile19(pathOrUrl);
28947
28978
  }
28948
- async function isDirectory(path28) {
28979
+ async function isDirectory(path34) {
28949
28980
  try {
28950
- const s = await stat4(path28);
28981
+ const s = await stat4(path34);
28951
28982
  return s.isDirectory();
28952
28983
  } catch {
28953
28984
  return false;
@@ -31567,11 +31598,11 @@ Full guide: __tooling__/docs/tools/baker/images.md`
31567
31598
  });
31568
31599
 
31569
31600
  // src/commands/landing/index.ts
31570
- import { defineCommand as defineCommand143 } from "citty";
31601
+ import { defineCommand as defineCommand151 } from "citty";
31571
31602
 
31572
31603
  // src/commands/landing/critique.ts
31573
31604
  import { readdir as readdir8, stat as stat6 } from "fs/promises";
31574
- import path27 from "path";
31605
+ import path28 from "path";
31575
31606
  import { defineCommand as defineCommand142 } from "citty";
31576
31607
 
31577
31608
  // src/engine/landing/lib/brand-tokens.ts
@@ -31980,6 +32011,11 @@ var RULE_META = {
31980
32011
  severity: "block",
31981
32012
  note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
31982
32013
  },
32014
+ "copied-reference-copy": {
32015
+ family: "originality",
32016
+ severity: "block",
32017
+ note: "This line is lifted from a section you consulted in the inspiration library. Reference sections are for structure and mechanism, never words \u2014 a visitor who has seen the original reads this as a clone, and the claim is not yours to make. Rewrite it from the client's own offer."
32018
+ },
31983
32019
  "broken-image": {
31984
32020
  family: "integrity",
31985
32021
  severity: "block",
@@ -32160,6 +32196,64 @@ var SEVERITY_WEIGHT = {
32160
32196
  advisory: 0.05
32161
32197
  };
32162
32198
 
32199
+ // src/engine/landing/lib/originality.ts
32200
+ var MIN_COMPARABLE_LENGTH = 12;
32201
+ var NEAR_MATCH_RATIO = 0.8;
32202
+ function normalize(value) {
32203
+ return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
32204
+ }
32205
+ function words2(value) {
32206
+ return normalize(value).split(" ").filter(Boolean);
32207
+ }
32208
+ function copyOverlapRatio(candidate, reference) {
32209
+ const referenceWords = words2(reference);
32210
+ if (referenceWords.length === 0) return 0;
32211
+ const candidateWords = new Set(words2(candidate));
32212
+ const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
32213
+ return shared / referenceWords.length;
32214
+ }
32215
+ function isVerbatimReuse(candidate, reference) {
32216
+ const normalizedCandidate = normalize(candidate);
32217
+ const normalizedReference = normalize(reference);
32218
+ if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
32219
+ if (normalizedCandidate.includes(normalizedReference)) return true;
32220
+ return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
32221
+ }
32222
+ function extractVisibleStrings(text) {
32223
+ const found = [];
32224
+ const lines = text.split("\n");
32225
+ for (const [index, line] of lines.entries()) {
32226
+ for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
32227
+ const value = match[1]?.trim();
32228
+ if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
32229
+ }
32230
+ }
32231
+ return found;
32232
+ }
32233
+ function detectOriginality(sources, references) {
32234
+ if (references.length === 0) return [];
32235
+ const findings = [];
32236
+ const seen = /* @__PURE__ */ new Set();
32237
+ for (const source of sources) {
32238
+ for (const { value, line } of extractVisibleStrings(source.text)) {
32239
+ for (const reference of references) {
32240
+ const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
32241
+ if (!hit) continue;
32242
+ const key = `${source.path}:${line}:${normalize(hit)}`;
32243
+ if (seen.has(key)) continue;
32244
+ seen.add(key);
32245
+ findings.push({
32246
+ id: "copied-reference-copy",
32247
+ snippet: value.slice(0, 120),
32248
+ file: source.path,
32249
+ line
32250
+ });
32251
+ }
32252
+ }
32253
+ }
32254
+ return findings;
32255
+ }
32256
+
32163
32257
  // src/engine/landing/lib/rules.ts
32164
32258
  var cap2 = (m, i) => m[i] ?? "";
32165
32259
  var num = (m, i) => Number(m[i] ?? 0);
@@ -32564,8 +32658,8 @@ var ANALYZERS = [
32564
32658
  const lines = text.split("\n");
32565
32659
  for (const m of text.matchAll(/(?:-webkit-)?background-clip\s*:\s*text/gi)) {
32566
32660
  const line = lineOf(text, m.index ?? 0);
32567
- const window = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
32568
- if (/gradient\(/i.test(window)) {
32661
+ const window2 = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
32662
+ if (/gradient\(/i.test(window2)) {
32569
32663
  out.push({ id: "gradient-text", snippet: "background-clip: text + gradient", file, line });
32570
32664
  }
32571
32665
  }
@@ -32687,7 +32781,16 @@ function dedupe(findings) {
32687
32781
  }
32688
32782
 
32689
32783
  // src/engine/landing/lib/critique.ts
32690
- var FAMILIES = ["typography", "color", "borders_depth", "motion", "spacing", "copy", "integrity"];
32784
+ var FAMILIES = [
32785
+ "typography",
32786
+ "color",
32787
+ "borders_depth",
32788
+ "motion",
32789
+ "spacing",
32790
+ "copy",
32791
+ "integrity",
32792
+ "originality"
32793
+ ];
32691
32794
  function round4(n) {
32692
32795
  return Math.round(n * 100) / 100;
32693
32796
  }
@@ -32706,6 +32809,7 @@ function critiqueLanding(input) {
32706
32809
  const raws = [];
32707
32810
  for (const source of sources) raws.push(...detectSource(source));
32708
32811
  raws.push(...detectPage(sources));
32812
+ raws.push(...detectOriginality(sources, input.references ?? []));
32709
32813
  for (const raw of raws) {
32710
32814
  const meta = RULE_META[raw.id];
32711
32815
  if (!meta) continue;
@@ -32743,41 +32847,77 @@ function describeCounts(findings) {
32743
32847
  return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
32744
32848
  }
32745
32849
 
32746
- // src/commands/landing/snapshot.ts
32747
- import { mkdir as mkdir8, rename as rename2, writeFile as writeFile11 } from "fs/promises";
32850
+ // src/engine/landing/lib/referenceStore.ts
32851
+ import { mkdir as mkdir8, readFile as readFile22, writeFile as writeFile11 } from "fs/promises";
32748
32852
  import path25 from "path";
32853
+ var REFERENCES_FILE = ".cache/inspiration-refs.json";
32854
+ var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
32855
+ async function readReferences(projectRoot) {
32856
+ try {
32857
+ const raw = await readFile22(path25.join(projectRoot, REFERENCES_FILE), "utf8");
32858
+ const parsed = JSON.parse(raw);
32859
+ if (!Array.isArray(parsed)) return [];
32860
+ const cutoff = Date.now() - REFERENCE_TTL_MS;
32861
+ return parsed.filter((entry) => {
32862
+ if (typeof entry !== "object" || entry === null) return false;
32863
+ const candidate = entry;
32864
+ if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
32865
+ const at = Date.parse(candidate.consultedAt ?? "");
32866
+ return Number.isNaN(at) ? true : at >= cutoff;
32867
+ });
32868
+ } catch {
32869
+ return [];
32870
+ }
32871
+ }
32872
+ async function recordReference(projectRoot, reference) {
32873
+ try {
32874
+ const existing = await readReferences(projectRoot);
32875
+ const merged = [...existing.filter((entry) => entry.sectionId !== reference.sectionId), reference];
32876
+ const file = path25.join(projectRoot, REFERENCES_FILE);
32877
+ await mkdir8(path25.dirname(file), { recursive: true });
32878
+ await writeFile11(file, `${JSON.stringify(merged, null, 2)}
32879
+ `);
32880
+ return true;
32881
+ } catch {
32882
+ return false;
32883
+ }
32884
+ }
32885
+
32886
+ // src/commands/landing/snapshot.ts
32887
+ import { mkdir as mkdir9, rename as rename2, writeFile as writeFile12 } from "fs/promises";
32888
+ import path26 from "path";
32749
32889
  var CRITIC_VERSION = "2";
32750
32890
  function critiqueCacheDir(projectRoot) {
32751
- return path25.join(projectRoot, ".cache", "landing-critique");
32891
+ return path26.join(projectRoot, ".cache", "landing-critique");
32752
32892
  }
32753
32893
  function snapshotPath(projectRoot, slug) {
32754
- return path25.join(critiqueCacheDir(projectRoot), `${slug}.json`);
32894
+ return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
32755
32895
  }
32756
32896
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
32757
- await mkdir8(critiqueCacheDir(projectRoot), { recursive: true });
32897
+ await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
32758
32898
  const dest = snapshotPath(projectRoot, snapshot.slug);
32759
32899
  const tmp = `${dest}.tmp`;
32760
- await writeFile11(tmp, `${JSON.stringify(snapshot, null, 2)}
32900
+ await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
32761
32901
  `, "utf8");
32762
32902
  await rename2(tmp, dest);
32763
32903
  }
32764
32904
 
32765
32905
  // src/commands/landing/source-version.ts
32766
- import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
32767
- import path26 from "path";
32906
+ import { readdir as readdir7, readFile as readFile23, stat as stat5 } from "fs/promises";
32907
+ import path27 from "path";
32768
32908
  async function landingSourceRelPaths(landingDir) {
32769
32909
  const rel = [];
32770
- if (await isFile(path26.join(landingDir, "index.astro"))) rel.push("index.astro");
32771
- const componentsDir = path26.join(landingDir, "_components");
32910
+ if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
32911
+ const componentsDir = path27.join(landingDir, "_components");
32772
32912
  for (const abs of await walkAstro(componentsDir)) {
32773
- rel.push(path26.relative(landingDir, abs).split(path26.sep).join("/"));
32913
+ rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
32774
32914
  }
32775
32915
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
32776
32916
  }
32777
32917
  async function readLandingSources(landingDir) {
32778
32918
  const rel = await landingSourceRelPaths(landingDir);
32779
32919
  const out = [];
32780
- for (const r of rel) out.push({ path: r, text: await readFile22(path26.join(landingDir, r), "utf8") });
32920
+ for (const r of rel) out.push({ path: r, text: await readFile23(path27.join(landingDir, r), "utf8") });
32781
32921
  return out;
32782
32922
  }
32783
32923
  async function computeLandingSourceSha(landingDir) {
@@ -32786,7 +32926,7 @@ async function computeLandingSourceSha(landingDir) {
32786
32926
  for (const r of rel) {
32787
32927
  let bytes;
32788
32928
  try {
32789
- bytes = await readFile22(path26.join(landingDir, r));
32929
+ bytes = await readFile23(path27.join(landingDir, r));
32790
32930
  } catch {
32791
32931
  bytes = Buffer.alloc(0);
32792
32932
  }
@@ -32810,7 +32950,7 @@ async function walkAstro(dir) {
32810
32950
  }
32811
32951
  const out = [];
32812
32952
  for (const entry of entries) {
32813
- const abs = path26.join(dir, entry.name);
32953
+ const abs = path27.join(dir, entry.name);
32814
32954
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
32815
32955
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
32816
32956
  }
@@ -32871,14 +33011,14 @@ var critiqueCommand2 = defineCommand142({
32871
33011
  { availableSlugs: await listLandingSlugs(projectRoot) }
32872
33012
  );
32873
33013
  }
32874
- if (!await isDir(path27.resolve(projectRoot, "src", "pages", slug))) {
33014
+ if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
32875
33015
  fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
32876
33016
  availableSlugs: await listLandingSlugs(projectRoot)
32877
33017
  });
32878
33018
  }
32879
33019
  }
32880
- const brand = await loadBrandTokens(projectRoot);
32881
- const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
33020
+ const [brand, references] = await Promise.all([loadBrandTokens(projectRoot), readReferences(projectRoot)]);
33021
+ const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, references)));
32882
33022
  const landings = results.map(({ slug, report }) => ({
32883
33023
  slug,
32884
33024
  overall: report.overall,
@@ -32909,10 +33049,10 @@ var critiqueCommand2 = defineCommand142({
32909
33049
  );
32910
33050
  }
32911
33051
  });
32912
- async function critiqueOne(projectRoot, slug, brand) {
32913
- const landingDir = path27.resolve(projectRoot, "src", "pages", slug);
33052
+ async function critiqueOne(projectRoot, slug, brand, references) {
33053
+ const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
32914
33054
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
32915
- const report = critiqueLanding({ slug, sources, brand });
33055
+ const report = critiqueLanding({ slug, sources, brand, references });
32916
33056
  let snapshotFailed = false;
32917
33057
  try {
32918
33058
  await writeCritiqueSnapshot(projectRoot, {
@@ -32930,7 +33070,7 @@ async function critiqueOne(projectRoot, slug, brand) {
32930
33070
  }
32931
33071
  async function listLandingSlugs(projectRoot) {
32932
33072
  try {
32933
- const entries = await readdir8(path27.join(projectRoot, "src", "pages"), { withFileTypes: true });
33073
+ const entries = await readdir8(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
32934
33074
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
32935
33075
  } catch {
32936
33076
  return [];
@@ -32955,8 +33095,2256 @@ async function isDir(p) {
32955
33095
  }
32956
33096
  }
32957
33097
 
33098
+ // src/commands/landing/inspiration/index.ts
33099
+ import { defineCommand as defineCommand150 } from "citty";
33100
+
33101
+ // src/commands/landing/inspiration/add.ts
33102
+ import { defineCommand as defineCommand143 } from "citty";
33103
+
33104
+ // src/commands/landing/inspiration/shared.ts
33105
+ var INSPIRATION_HINTS = {
33106
+ adapt: "Reference only. Re-express this in the client's BRAND.md palette, type and imagery register. Reusing a headline, subhead or CTA verbatim is a Tier 0 message-match failure (references/gotchas.md) and `baker landing critique` will block the publish.",
33107
+ structureNotCopy: "Take the structural decision, not the furniture: what the eye hits first, the grid ratio, what was deliberately left out. Your copy must come from the client's own offer."
33108
+ };
33109
+ function fidelityHint(fidelity) {
33110
+ if (fidelity == null) return null;
33111
+ if (fidelity >= 0.9) return null;
33112
+ if (fidelity >= 0.75) {
33113
+ return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 the markup is close but not exact. Trust the screenshot over the code.`;
33114
+ }
33115
+ return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 this section is scroll- or JS-driven and the extracted markup does NOT render like the original. Use the screenshot and the notes; treat the code as a hint only.`;
33116
+ }
33117
+ function reportError(error) {
33118
+ if (error instanceof ApiError) {
33119
+ writeJson({ ok: false, error: { code: error.code, message: error.message } });
33120
+ } else {
33121
+ writeJson({
33122
+ ok: false,
33123
+ error: { code: "UNKNOWN", message: error instanceof Error ? error.message : String(error) }
33124
+ });
33125
+ }
33126
+ process.exit(1);
33127
+ }
33128
+ function splitList(value) {
33129
+ if (!value) return void 0;
33130
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
33131
+ return parts.length > 0 ? parts : void 0;
33132
+ }
33133
+ function parseNumber(value) {
33134
+ if (value === void 0 || value === "") return void 0;
33135
+ const parsed = Number(value);
33136
+ return Number.isFinite(parsed) ? parsed : void 0;
33137
+ }
33138
+
33139
+ // src/commands/landing/inspiration/add.ts
33140
+ registerSchema({
33141
+ command: "landing.inspiration.add",
33142
+ description: "Add a landing page to the reference library and save it to this company. Returns immediately \u2014 studying a page takes a few minutes, so never wait on it. Use it when the client names a site they admire, or when you find a competitor page worth learning from.",
33143
+ args: {
33144
+ url: { type: "string", description: "Landing page address", required: true },
33145
+ note: { type: "string", description: "Why this page is worth keeping", required: false }
33146
+ }
33147
+ });
33148
+ var addCommand = defineCommand143({
33149
+ meta: {
33150
+ name: "add",
33151
+ description: "Add a landing page to the reference library. Example: baker landing inspiration add https://linear.app --note 'the client likes this density'"
33152
+ },
33153
+ args: {
33154
+ url: { type: "positional", description: "Landing page address", required: true },
33155
+ note: { type: "string", description: "Why this page is worth keeping", required: false }
33156
+ },
33157
+ run: async ({ args }) => {
33158
+ try {
33159
+ const data = await apiPost("/api/landing-inspiration/add", {
33160
+ url: args.url,
33161
+ note: args.note,
33162
+ favorite: true
33163
+ });
33164
+ const hints = data.alreadyKnown ? [
33165
+ "This page was already in the library, so nothing was re-studied \u2014 its sections are searchable now.",
33166
+ `Search it with: baker landing inspiration search --domain ${new URL(data.canonicalUrl).hostname}`
33167
+ ] : [
33168
+ "Studying this page takes a few minutes. Do NOT wait on it \u2014 carry on, and search for it later in the turn or in a later one.",
33169
+ "Meanwhile, search what is already in the library: baker landing inspiration search '<what you want to see>' --scope all"
33170
+ ];
33171
+ writeJson({
33172
+ ok: true,
33173
+ data: { id: data.sourceId, url: data.canonicalUrl, status: data.status, already_known: data.alreadyKnown },
33174
+ hints
33175
+ });
33176
+ } catch (error) {
33177
+ reportError(error);
33178
+ }
33179
+ }
33180
+ });
33181
+
33182
+ // src/commands/landing/inspiration/code.ts
33183
+ import { mkdir as mkdir10, writeFile as writeFile13 } from "fs/promises";
33184
+ import path29 from "path";
33185
+ import { defineCommand as defineCommand144 } from "citty";
33186
+ registerSchema({
33187
+ command: "landing.inspiration.code",
33188
+ description: "Write a reference section's standalone HTML+CSS to .baker/inspiration/<id>/ so you can read how it is built. Reference only \u2014 the structure is the lesson, the words are not yours to reuse. Consulting a section records it, and `baker landing critique` blocks a publish that ships its copy verbatim.",
33189
+ args: {
33190
+ id: { type: "string", description: "Section id from search", required: true },
33191
+ full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
33192
+ }
33193
+ });
33194
+ var codeCommand = defineCommand144({
33195
+ meta: {
33196
+ name: "code",
33197
+ description: "Write one reference section's standalone markup to disk. Example: baker landing inspiration code k57abc\u2026 \u2014 read it for structure, then build your own."
33198
+ },
33199
+ args: {
33200
+ id: { type: "positional", description: "Section id from search", required: true },
33201
+ full: { type: "boolean", description: "Also print the markup inline", required: false, default: false }
33202
+ },
33203
+ run: async ({ args }) => {
33204
+ try {
33205
+ const id = args.id;
33206
+ const data = await apiGet("/api/landing-inspiration/section-code", { id });
33207
+ const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
33208
+ await mkdir10(dir, { recursive: true });
33209
+ const file = path29.join(dir, "section.html");
33210
+ await writeFile13(file, data.html);
33211
+ const recorded = await recordReference(process.cwd(), {
33212
+ sectionId: id,
33213
+ sourceUrl: data.sourceUrl,
33214
+ copyStrings: data.copyStrings,
33215
+ consultedAt: (/* @__PURE__ */ new Date()).toISOString()
33216
+ });
33217
+ const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
33218
+ const fidelity = fidelityHint(data.fidelity);
33219
+ if (fidelity) hints.push(fidelity);
33220
+ if (data.fidelityNote) hints.push(data.fidelityNote);
33221
+ if (!recorded) {
33222
+ hints.push(
33223
+ "Could not record this reference (.cache/ not writable?) \u2014 the originality check at publish will not be able to see it, so be especially careful not to reuse its copy."
33224
+ );
33225
+ }
33226
+ writeJson({
33227
+ ok: true,
33228
+ data: {
33229
+ id,
33230
+ file: path29.relative(process.cwd(), file),
33231
+ bytes: data.html.length,
33232
+ fidelity: data.fidelity,
33233
+ css_custom_properties: data.cssCustomProperties,
33234
+ reproduction_notes: data.reproductionNotes,
33235
+ adaptation_notes: data.adaptationNotes,
33236
+ source_url: data.sourceUrl,
33237
+ ...args.full ? { html: data.html } : {}
33238
+ },
33239
+ hints
33240
+ });
33241
+ } catch (error) {
33242
+ reportError(error);
33243
+ }
33244
+ }
33245
+ });
33246
+
33247
+ // src/commands/landing/inspiration/favorites.ts
33248
+ import { defineCommand as defineCommand145 } from "citty";
33249
+ registerSchema({
33250
+ command: "landing.inspiration.favorites",
33251
+ description: "List the reference sections this company has saved. This is what `search` looks at by default, so it is the client's own taste profile \u2014 read it before proposing a direction.",
33252
+ args: {
33253
+ type: { type: "string", description: "Comma list of section types to filter by", required: false },
33254
+ limit: { type: "number", description: "Max results (default 30)", required: false }
33255
+ }
33256
+ });
33257
+ var favoritesCommand = defineCommand145({
33258
+ meta: {
33259
+ name: "favorites",
33260
+ description: "List this company's saved reference sections. Example: baker landing inspiration favorites --type hero,pricing"
33261
+ },
33262
+ args: {
33263
+ type: { type: "string", description: "Comma list of section types", required: false },
33264
+ limit: { type: "string", description: "Max results (default 30)", required: false }
33265
+ },
33266
+ run: async ({ args }) => {
33267
+ try {
33268
+ const params = {};
33269
+ const types = splitList(args.type);
33270
+ if (types) params.type = types.join(",");
33271
+ const limit = parseNumber(args.limit);
33272
+ params.limit = String(limit ?? 30);
33273
+ const data = await apiGet("/api/landing-inspiration/favorites", params);
33274
+ const sections = Array.isArray(data?.sections) ? data.sections : [];
33275
+ writeJson({
33276
+ ok: true,
33277
+ data: {
33278
+ sections: sections.map((section) => ({
33279
+ id: section.id,
33280
+ section: section.sectionType,
33281
+ composition: section.composition,
33282
+ look: section.visualRegister,
33283
+ why_it_works: section.whyItWorks,
33284
+ domain: section.domain
33285
+ }))
33286
+ },
33287
+ hints: sections.length === 0 ? [
33288
+ "Nothing saved yet. Add a page the client admires with `baker landing inspiration add <url>`, or browse the whole library with `baker landing inspiration search --scope all`."
33289
+ ] : [
33290
+ "These are the client's taste signals \u2014 read them as direction, not as a component library.",
33291
+ INSPIRATION_HINTS.adapt
33292
+ ]
33293
+ });
33294
+ } catch (error) {
33295
+ reportError(error);
33296
+ }
33297
+ }
33298
+ });
33299
+ registerSchema({
33300
+ command: "landing.inspiration.favorite",
33301
+ description: "Save a reference section (or a whole page) to this company, so it shows up in the default search scope.",
33302
+ args: {
33303
+ id: { type: "string", description: "Section id, or page id with --page", required: true },
33304
+ page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false },
33305
+ note: { type: "string", description: "Why this is worth keeping", required: false }
33306
+ }
33307
+ });
33308
+ var favoriteCommand = defineCommand145({
33309
+ meta: {
33310
+ name: "favorite",
33311
+ description: "Save a reference section to this company. Example: baker landing inspiration favorite k57abc\u2026"
33312
+ },
33313
+ args: {
33314
+ id: { type: "positional", description: "Section id (or page id with --page)", required: true },
33315
+ page: { type: "boolean", description: "Treat the id as a page", required: false, default: false },
33316
+ note: { type: "string", description: "Why this is worth keeping", required: false }
33317
+ },
33318
+ run: async ({ args }) => {
33319
+ try {
33320
+ const id = args.id;
33321
+ const body = args.page ? { sourceId: id } : { sectionId: id };
33322
+ const data = await apiPost("/api/landing-inspiration/favorite", {
33323
+ ...body,
33324
+ note: args.note
33325
+ });
33326
+ writeJson({ ok: true, data: { id, favorited: data.favorited } });
33327
+ } catch (error) {
33328
+ reportError(error);
33329
+ }
33330
+ }
33331
+ });
33332
+ registerSchema({
33333
+ command: "landing.inspiration.unfavorite",
33334
+ description: "Remove a reference section (or page) from this company's saved set.",
33335
+ args: {
33336
+ id: { type: "string", description: "Section id, or page id with --page", required: true },
33337
+ page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false }
33338
+ }
33339
+ });
33340
+ var unfavoriteCommand = defineCommand145({
33341
+ meta: {
33342
+ name: "unfavorite",
33343
+ description: "Remove a reference section from this company's saved set. Example: baker landing inspiration unfavorite k57abc\u2026"
33344
+ },
33345
+ args: {
33346
+ id: { type: "positional", description: "Section id (or page id with --page)", required: true },
33347
+ page: { type: "boolean", description: "Treat the id as a page", required: false, default: false }
33348
+ },
33349
+ run: async ({ args }) => {
33350
+ try {
33351
+ const id = args.id;
33352
+ const body = args.page ? { sourceId: id } : { sectionId: id };
33353
+ const data = await apiPost("/api/landing-inspiration/unfavorite", body);
33354
+ writeJson({ ok: true, data: { id, favorited: data.favorited } });
33355
+ } catch (error) {
33356
+ reportError(error);
33357
+ }
33358
+ }
33359
+ });
33360
+
33361
+ // src/commands/landing/inspiration/page.ts
33362
+ import { defineCommand as defineCommand146 } from "citty";
33363
+ registerSchema({
33364
+ command: "landing.inspiration.page",
33365
+ description: "Show a whole reference page as a sequence: every section top to bottom with its type and the idea behind it. This is the view to use when the question is how a good page is ORDERED rather than what one section looks like.",
33366
+ args: { id: { type: "string", description: "Page id (from a search result's source id)", required: true } }
33367
+ });
33368
+ var pageCommand = defineCommand146({
33369
+ meta: {
33370
+ name: "page",
33371
+ description: "Show how a reference page sequences its sections. Example: baker landing inspiration page j91xyz\u2026 \u2014 the blueprint, not the pixels."
33372
+ },
33373
+ args: { id: { type: "positional", description: "Page id", required: true } },
33374
+ run: async ({ args }) => {
33375
+ try {
33376
+ const data = await apiGet("/api/landing-inspiration/page", { id: args.id });
33377
+ if (data.status !== "indexed") {
33378
+ writeJson({
33379
+ ok: true,
33380
+ data: { id: data.id, url: data.url, status: data.status, sections: [] },
33381
+ hints: [
33382
+ data.status === "error" ? "We couldn't read this page. Try a different address, or a different page on the same site." : "Still being studied \u2014 check back later in the turn or in a later one."
33383
+ ]
33384
+ });
33385
+ return;
33386
+ }
33387
+ writeJson({
33388
+ ok: true,
33389
+ data: {
33390
+ id: data.id,
33391
+ url: data.url,
33392
+ domain: data.domain,
33393
+ title: data.title,
33394
+ archetype: data.archetype,
33395
+ stack: data.detectedStack,
33396
+ page_fidelity: data.pageFidelity,
33397
+ blueprint: data.sectionOrder,
33398
+ sections: data.sections.map((section) => ({
33399
+ id: section.id,
33400
+ position: section.index,
33401
+ section: section.sectionType,
33402
+ composition: section.composition,
33403
+ look: section.visualRegister,
33404
+ motion: section.motionSummary,
33405
+ why_it_works: section.whyItWorks,
33406
+ craft: section.craftScore
33407
+ }))
33408
+ },
33409
+ hints: [
33410
+ "The blueprint is the section order top to bottom \u2014 the answer to 'how do good pages in this category sequence themselves'.",
33411
+ "Use `baker landing inspiration view <section id>` for any section worth a closer look.",
33412
+ INSPIRATION_HINTS.adapt
33413
+ ]
33414
+ });
33415
+ } catch (error) {
33416
+ reportError(error);
33417
+ }
33418
+ }
33419
+ });
33420
+
33421
+ // src/commands/landing/inspiration/scrape.ts
33422
+ import { defineCommand as defineCommand147 } from "citty";
33423
+
33424
+ // src/engine/landing-library/blocked.ts
33425
+ var CHALLENGE_PHRASES = [
33426
+ "just a moment",
33427
+ "attention required",
33428
+ "verify you are human",
33429
+ "checking your browser",
33430
+ "enable javascript and cookies to continue",
33431
+ "unusual traffic",
33432
+ "access denied",
33433
+ "you have been blocked",
33434
+ "request unsuccessful",
33435
+ "are you a robot",
33436
+ "security check",
33437
+ "ddos protection",
33438
+ "captcha"
33439
+ ];
33440
+ var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
33441
+ function detectBlockedPage(page) {
33442
+ if (page.status !== null && page.status >= 400) {
33443
+ return {
33444
+ code: "HTTP_ERROR",
33445
+ message: `The site returned ${page.status} instead of the page \u2014 it may be blocking automated visits.`
33446
+ };
33447
+ }
33448
+ const haystack = `${page.title}
33449
+ ${page.bodyText.slice(0, 2e3)}`.toLowerCase();
33450
+ const phrase = CHALLENGE_PHRASES.find((candidate) => haystack.includes(candidate));
33451
+ if (phrase) {
33452
+ return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
33453
+ }
33454
+ const html = page.html?.toLowerCase() ?? "";
33455
+ if (CHALLENGE_MARKERS.some((marker) => html.includes(marker))) {
33456
+ return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
33457
+ }
33458
+ return null;
33459
+ }
33460
+ var BlockedPageError = class extends Error {
33461
+ code;
33462
+ constructor(blocked) {
33463
+ super(blocked.message);
33464
+ this.name = "BlockedPageError";
33465
+ this.code = blocked.code;
33466
+ }
33467
+ };
33468
+
33469
+ // src/engine/landing-library/run.ts
33470
+ import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
33471
+ import path31 from "path";
33472
+
33473
+ // src/engine/landing-library/browser.ts
33474
+ import { createRequire } from "module";
33475
+ var require_ = createRequire(import.meta.url);
33476
+ var pwSpecifier = ["play", "wright"].join("");
33477
+ var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
33478
+ var MOBILE_VIEWPORT = { width: 390, height: 844 };
33479
+ var DEVICE_SCALE_FACTOR = 2;
33480
+ async function launchBrowser() {
33481
+ const playwright = require_(pwSpecifier);
33482
+ return await playwright.chromium.launch({
33483
+ headless: true,
33484
+ args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
33485
+ });
33486
+ }
33487
+ async function newPage(browser, viewport, opts = { motion: false }) {
33488
+ const context = await browser.newContext({
33489
+ viewport,
33490
+ deviceScaleFactor: DEVICE_SCALE_FACTOR,
33491
+ // Still captures freeze motion so screenshots are reproducible; the motion
33492
+ // pass re-opens a context with animation enabled.
33493
+ reducedMotion: opts.motion ? "no-preference" : "reduce",
33494
+ userAgent: viewport.width < 500 ? "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
33495
+ });
33496
+ const page = await context.newPage();
33497
+ return { context, page };
33498
+ }
33499
+
33500
+ // src/engine/landing-library/usedCss.ts
33501
+ async function collectUsedCss(page, selector) {
33502
+ const collected = await page.evaluate(inPageCollectUsedCss, selector);
33503
+ const extra = [];
33504
+ for (const href of collected.unreadableHrefs) {
33505
+ const text = await fetchStylesheet(page, href);
33506
+ if (text) extra.push(`/* ${href} */
33507
+ ${text}`);
33508
+ }
33509
+ return { ...collected, css: [collected.css, ...extra].filter(Boolean).join("\n\n") };
33510
+ }
33511
+ async function fetchStylesheet(page, href) {
33512
+ try {
33513
+ const response = await page.request.get(href, { timeout: 1e4 });
33514
+ if (!response.ok()) return null;
33515
+ return await response.text();
33516
+ } catch {
33517
+ return null;
33518
+ }
33519
+ }
33520
+ var inPageCollectUsedCss = (selector) => {
33521
+ const root = document.querySelector(selector);
33522
+ const empty = {
33523
+ css: "",
33524
+ variables: "",
33525
+ inheritedSeed: "",
33526
+ unreadableHrefs: [],
33527
+ stats: { totalRules: 0, keptRules: 0, fontFaces: 0, keyframes: 0 }
33528
+ };
33529
+ if (!root) return empty;
33530
+ const INHERITED = [
33531
+ "color",
33532
+ "font-family",
33533
+ "font-size",
33534
+ "font-weight",
33535
+ "line-height",
33536
+ "letter-spacing",
33537
+ "text-align",
33538
+ "background-color",
33539
+ "-webkit-font-smoothing"
33540
+ ];
33541
+ const unreadableHrefs = [];
33542
+ const kept = [];
33543
+ const fontFaces = [];
33544
+ const keyframesByName = /* @__PURE__ */ new Map();
33545
+ let totalRules = 0;
33546
+ let keptStyleRules = 0;
33547
+ const matchesInSection = (selectorText) => {
33548
+ for (const part of selectorText.split(",")) {
33549
+ const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
33550
+ if (!base) continue;
33551
+ try {
33552
+ if (root.matches(base) || root.querySelector(base)) return true;
33553
+ } catch {
33554
+ return true;
33555
+ }
33556
+ }
33557
+ return false;
33558
+ };
33559
+ const walk = (rules, sink) => {
33560
+ for (const rule of Array.from(rules)) {
33561
+ totalRules++;
33562
+ if (rule instanceof CSSStyleRule) {
33563
+ if (matchesInSection(rule.selectorText)) {
33564
+ sink.push(rule.cssText);
33565
+ keptStyleRules++;
33566
+ }
33567
+ continue;
33568
+ }
33569
+ if (rule instanceof CSSFontFaceRule) {
33570
+ fontFaces.push(rule.cssText);
33571
+ continue;
33572
+ }
33573
+ if (rule instanceof CSSKeyframesRule) {
33574
+ keyframesByName.set(rule.name, rule.cssText);
33575
+ continue;
33576
+ }
33577
+ const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
33578
+ if (grouping) {
33579
+ const inner = [];
33580
+ walk(rule.cssRules, inner);
33581
+ if (inner.length === 0) continue;
33582
+ const condition = rule.conditionText ?? "";
33583
+ const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
33584
+ sink.push(`${prelude} {
33585
+ ${inner.join("\n")}
33586
+ }`);
33587
+ }
33588
+ }
33589
+ };
33590
+ for (const sheet of Array.from(document.styleSheets)) {
33591
+ const owner = sheet.ownerNode;
33592
+ if (owner?.hasAttribute?.("data-baker-freeze")) continue;
33593
+ try {
33594
+ walk(sheet.cssRules, kept);
33595
+ } catch {
33596
+ if (sheet.href) unreadableHrefs.push(sheet.href);
33597
+ }
33598
+ }
33599
+ const keptText = kept.join("\n");
33600
+ const usedKeyframes = [];
33601
+ for (const [name, text] of keyframesByName) {
33602
+ if (new RegExp(`(^|[\\s:,])${name}([\\s;,}]|$)`).test(keptText)) usedKeyframes.push(text);
33603
+ }
33604
+ const rootStyle = getComputedStyle(root);
33605
+ const variableDeclarations = [];
33606
+ for (const property of Array.from(rootStyle)) {
33607
+ if (!property.startsWith("--")) continue;
33608
+ const value = rootStyle.getPropertyValue(property).trim();
33609
+ if (value) variableDeclarations.push(` ${property}: ${value};`);
33610
+ }
33611
+ const seed = INHERITED.map((property) => ` ${property}: ${rootStyle.getPropertyValue(property)};`).join("\n");
33612
+ return {
33613
+ css: [...fontFaces, ...usedKeyframes, ...kept].join("\n"),
33614
+ variables: variableDeclarations.length ? `:root {
33615
+ ${variableDeclarations.join("\n")}
33616
+ }` : "",
33617
+ inheritedSeed: seed,
33618
+ unreadableHrefs,
33619
+ stats: {
33620
+ totalRules,
33621
+ keptRules: keptStyleRules,
33622
+ fontFaces: fontFaces.length,
33623
+ keyframes: usedKeyframes.length
33624
+ }
33625
+ };
33626
+ };
33627
+
33628
+ // src/engine/landing-library/bundle.ts
33629
+ async function buildSectionBundle(page, selector, pageUrl) {
33630
+ const used = await collectUsedCss(page, selector);
33631
+ const extracted = await page.evaluate(inPageExtractMarkup, {
33632
+ selector,
33633
+ pageUrl,
33634
+ rootAttribute: SECTION_ROOT_ATTRIBUTE
33635
+ });
33636
+ const css = [
33637
+ "*, *::before, *::after { box-sizing: border-box; }",
33638
+ "html, body { margin: 0; padding: 0; }",
33639
+ used.variables,
33640
+ `body {
33641
+ ${used.inheritedSeed}
33642
+ }`,
33643
+ used.css
33644
+ ].filter(Boolean).join("\n\n");
33645
+ const html = `<!doctype html>
33646
+ <html ${extracted.rootAttributes}>
33647
+ <head>
33648
+ <meta charset="utf-8">
33649
+ <meta name="viewport" content="width=device-width, initial-scale=1">
33650
+ <title>Section from ${escapeHtml2(extracted.host)}</title>
33651
+ <style>
33652
+ ${css}
33653
+ </style>
33654
+ </head>
33655
+ <body ${extracted.bodyAttributes}>
33656
+ ${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
33657
+ </body>
33658
+ </html>
33659
+ `;
33660
+ return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
33661
+ }
33662
+ function escapeHtml2(value) {
33663
+ return value.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
33664
+ }
33665
+ var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
33666
+ function wrapInLayoutContext(html, context) {
33667
+ let wrapped = context.width > 0 && context.width < context.viewportWidth ? `<div style="width:${context.width}px;margin:0 auto;">
33668
+ ${html}
33669
+ </div>` : html;
33670
+ for (const ancestor of [...context.ancestors].reverse()) {
33671
+ const attributes = ancestor.attributes ? ` ${ancestor.attributes}` : "";
33672
+ const container = ancestor.containerType && ancestor.containerType !== "normal" ? `container-type:${ancestor.containerType};${ancestor.containerName && ancestor.containerName !== "none" ? `container-name:${ancestor.containerName};` : ""}` : "";
33673
+ wrapped = `<${ancestor.tag}${attributes} style="${NEUTRALIZED_ANCESTOR_STYLE}${container}">
33674
+ ${wrapped}
33675
+ </${ancestor.tag}>`;
33676
+ }
33677
+ return wrapped;
33678
+ }
33679
+ var NEUTRALIZED_ANCESTOR_STYLE = [
33680
+ "display:block !important",
33681
+ "position:static !important",
33682
+ "margin:0 !important",
33683
+ "padding:0 !important",
33684
+ "border:0 !important",
33685
+ "width:auto !important",
33686
+ "min-width:0 !important",
33687
+ "max-width:none !important",
33688
+ "height:auto !important",
33689
+ "min-height:0 !important",
33690
+ "max-height:none !important",
33691
+ "transform:none !important",
33692
+ "overflow:visible !important",
33693
+ ""
33694
+ ].join(";");
33695
+ var inPageExtractMarkup = ({
33696
+ selector,
33697
+ pageUrl,
33698
+ rootAttribute
33699
+ }) => {
33700
+ const root = document.querySelector(selector);
33701
+ const emptyContext = { ancestors: [], width: 0, viewportWidth: 0 };
33702
+ const serializeAttributes = (element) => {
33703
+ if (!element) return "";
33704
+ return Array.from(element.attributes).filter((attribute) => attribute.name !== "style").map((attribute) => `${attribute.name}="${attribute.value.replace(/"/g, "&quot;")}"`).join(" ");
33705
+ };
33706
+ const rootAttributes = serializeAttributes(document.documentElement) || 'lang="en"';
33707
+ const bodyAttributes = serializeAttributes(document.body);
33708
+ if (!root) {
33709
+ return {
33710
+ html: "",
33711
+ assetUrls: [],
33712
+ host: "",
33713
+ layoutContext: emptyContext,
33714
+ rootAttributes,
33715
+ bodyAttributes
33716
+ };
33717
+ }
33718
+ const ancestors = [];
33719
+ for (let ancestor = root.parentElement; ancestor && ancestor !== document.body; ) {
33720
+ const style = getComputedStyle(ancestor);
33721
+ ancestors.unshift({
33722
+ tag: /^[a-zA-Z][a-zA-Z0-9-]*$/.test(ancestor.tagName) ? ancestor.tagName.toLowerCase() : "div",
33723
+ attributes: Array.from(ancestor.attributes).filter((a) => a.name === "class" || a.name === "id" || a.name.startsWith("data-")).map((a) => `${a.name}="${a.value.replace(/"/g, "&quot;")}"`).join(" "),
33724
+ containerType: style.containerType,
33725
+ containerName: style.containerName
33726
+ });
33727
+ ancestor = ancestor.parentElement;
33728
+ }
33729
+ const layoutContext = {
33730
+ ancestors,
33731
+ width: Math.round(root.getBoundingClientRect().width),
33732
+ viewportWidth: window.innerWidth
33733
+ };
33734
+ const absolute = (value) => {
33735
+ try {
33736
+ return new URL(value, pageUrl).href;
33737
+ } catch {
33738
+ return value;
33739
+ }
33740
+ };
33741
+ const clone = root.cloneNode(true);
33742
+ clone.setAttribute(rootAttribute, "");
33743
+ const assetUrls = /* @__PURE__ */ new Set();
33744
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
33745
+ for (const attribute of ["src", "href", "poster"]) {
33746
+ const value = element.getAttribute(attribute);
33747
+ if (!value || value.startsWith("data:") || value.startsWith("#")) continue;
33748
+ const url = absolute(value);
33749
+ element.setAttribute(attribute, url);
33750
+ if (attribute !== "href" || element.tagName === "LINK") assetUrls.add(url);
33751
+ }
33752
+ const srcset = element.getAttribute("srcset");
33753
+ if (srcset) {
33754
+ element.setAttribute(
33755
+ "srcset",
33756
+ srcset.split(",").map((candidate) => {
33757
+ const [url, descriptor] = candidate.trim().split(/\s+/, 2);
33758
+ if (!url) return candidate;
33759
+ const resolved = absolute(url);
33760
+ assetUrls.add(resolved);
33761
+ return descriptor ? `${resolved} ${descriptor}` : resolved;
33762
+ }).join(", ")
33763
+ );
33764
+ }
33765
+ const style = element.getAttribute("style");
33766
+ if (style?.includes("url(")) {
33767
+ element.setAttribute(
33768
+ "style",
33769
+ style.replace(/url\((['"]?)([^'")]+)\1\)/g, (_match, quote, url) => {
33770
+ if (url.startsWith("data:")) return `url(${quote}${url}${quote})`;
33771
+ const resolved = absolute(url);
33772
+ assetUrls.add(resolved);
33773
+ return `url(${quote}${resolved}${quote})`;
33774
+ })
33775
+ );
33776
+ }
33777
+ }
33778
+ return {
33779
+ html: clone.outerHTML,
33780
+ assetUrls: Array.from(assetUrls),
33781
+ host: location.host,
33782
+ layoutContext,
33783
+ rootAttributes,
33784
+ bodyAttributes
33785
+ };
33786
+ };
33787
+
33788
+ // src/engine/landing-library/capture.ts
33789
+ async function captureSection(page, section) {
33790
+ const locator = page.locator(section.selector).first();
33791
+ if (await locator.count().catch(() => 0)) {
33792
+ const shot = await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
33793
+ if (shot) return shot;
33794
+ }
33795
+ const doc = await page.evaluate(() => ({
33796
+ width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
33797
+ height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
33798
+ }));
33799
+ const width = Math.min(section.rect.width, doc.width - section.rect.left);
33800
+ const height = Math.min(section.rect.height, doc.height - section.rect.top);
33801
+ if (width <= 0 || height <= 0) return null;
33802
+ return await page.screenshot({
33803
+ type: "png",
33804
+ clip: { x: section.rect.left, y: section.rect.top, width, height },
33805
+ timeout: 15e3
33806
+ }).catch(() => null);
33807
+ }
33808
+ async function captureSectionOnMobile(page, section) {
33809
+ const locator = page.locator(section.selector).first();
33810
+ if (!await locator.count().catch(() => 0)) return null;
33811
+ if (!await locator.isVisible().catch(() => false)) return null;
33812
+ return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
33813
+ }
33814
+
33815
+ // src/engine/landing-library/fidelity.ts
33816
+ import sharp4 from "sharp";
33817
+ var COMPARISON_SIZE = 32;
33818
+ function pixelSimilarity(a, b) {
33819
+ const length = Math.min(a.length, b.length);
33820
+ if (length === 0) return 0;
33821
+ let total = 0;
33822
+ for (let i = 0; i < length; i++) {
33823
+ total += Math.abs((a[i] ?? 0) - (b[i] ?? 0));
33824
+ }
33825
+ return 1 - total / (length * 255);
33826
+ }
33827
+ async function scoreFidelity(live, rendered) {
33828
+ const [liveMeta, renderedMeta] = await Promise.all([sharp4(live).metadata(), sharp4(rendered).metadata()]);
33829
+ const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
33830
+ const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
33831
+ const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
33832
+ const score = pixelSimilarity(livePixels, renderedPixels);
33833
+ const heightRatio = liveSize.height > 0 && renderedSize.height > 0 ? Math.min(liveSize.height, renderedSize.height) / Math.max(liveSize.height, renderedSize.height) : 0;
33834
+ return {
33835
+ score,
33836
+ liveSize,
33837
+ renderedSize,
33838
+ ...heightRatio < 0.8 ? { note: `height differs by ${Math.round((1 - heightRatio) * 100)}% \u2014 the bundle reflowed` } : {}
33839
+ };
33840
+ }
33841
+ async function toGreyGrid(image) {
33842
+ const raw = await sharp4(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
33843
+ return new Uint8Array(raw);
33844
+ }
33845
+
33846
+ // src/engine/landing-library/motion.ts
33847
+ var EMPTY_MOTION = {
33848
+ hasMotion: false,
33849
+ summary: "no motion",
33850
+ entrance: [],
33851
+ hover: [],
33852
+ scroll: [],
33853
+ loop: [],
33854
+ libraries: [],
33855
+ respectsReducedMotion: false
33856
+ };
33857
+ function isWorthFilming(motion) {
33858
+ return motion.entrance.length + motion.scroll.length + motion.loop.length > 0;
33859
+ }
33860
+ async function collectMotion(page, selector) {
33861
+ const raw = await page.evaluate(inPageCollectMotion, selector).catch(() => null);
33862
+ if (!raw) return EMPTY_MOTION;
33863
+ return { ...raw, summary: summarizeMotion(raw) };
33864
+ }
33865
+ function summarizeMotion(motion) {
33866
+ const parts = [];
33867
+ const describe = (label, effects) => {
33868
+ if (effects.length === 0) return;
33869
+ const kinds = [...new Set(effects.map((effect) => effect.kind))].slice(0, 3);
33870
+ parts.push(`${label} ${kinds.join("/")}`);
33871
+ };
33872
+ describe("entrance", motion.entrance);
33873
+ describe("hover", motion.hover);
33874
+ describe("scroll", motion.scroll);
33875
+ describe("loop", motion.loop);
33876
+ if (motion.libraries.length > 0) parts.push(`via ${motion.libraries.join("/")}`);
33877
+ if (parts.length === 0) return "no motion";
33878
+ return parts.join(", ") + (motion.respectsReducedMotion ? "" : " (ignores reduced-motion)");
33879
+ }
33880
+ var inPageCollectMotion = (selector) => {
33881
+ const root = document.querySelector(selector);
33882
+ const empty = {
33883
+ hasMotion: false,
33884
+ entrance: [],
33885
+ hover: [],
33886
+ scroll: [],
33887
+ loop: [],
33888
+ libraries: [],
33889
+ respectsReducedMotion: false
33890
+ };
33891
+ if (!root) return empty;
33892
+ const entrance = [];
33893
+ const hover = [];
33894
+ const scroll = [];
33895
+ const loop = [];
33896
+ const keyframesByName = /* @__PURE__ */ new Map();
33897
+ let respectsReducedMotion = false;
33898
+ const inSection = (selectorText) => {
33899
+ for (const part of selectorText.split(",")) {
33900
+ const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
33901
+ if (!base) continue;
33902
+ try {
33903
+ if (root.matches(base) || root.querySelector(base)) return true;
33904
+ } catch {
33905
+ return true;
33906
+ }
33907
+ }
33908
+ return false;
33909
+ };
33910
+ const toMs = (value) => {
33911
+ const first = value.split(",")[0]?.trim() ?? "";
33912
+ if (first.endsWith("ms")) return Number.parseFloat(first);
33913
+ if (first.endsWith("s")) return Number.parseFloat(first) * 1e3;
33914
+ return void 0;
33915
+ };
33916
+ const classifyKeyframes = (body) => {
33917
+ const text = body.toLowerCase();
33918
+ const fades = /opacity\s*:\s*0(\.0+)?\s*[;}]/.test(text);
33919
+ if (/translatey\(\s*-?\d/.test(text)) {
33920
+ const upward = /translatey\(\s*(\d|\.)/.test(text);
33921
+ return fades ? upward ? "fade-up" : "fade-down" : upward ? "slide-up" : "slide-down";
33922
+ }
33923
+ if (/translatex\(\s*-?\d/.test(text)) return fades ? "fade-in-x" : "slide-in-x";
33924
+ if (/scale\(/.test(text)) return fades ? "fade-zoom" : "zoom";
33925
+ if (/rotate\(/.test(text)) return "spin";
33926
+ if (fades) return "fade";
33927
+ return "animate";
33928
+ };
33929
+ const classifyHover = (style) => {
33930
+ const transform = style.transform ?? "";
33931
+ if (/translatey\(\s*-/i.test(transform)) return "lift";
33932
+ if (/scale\(\s*(1\.\d|[2-9])/i.test(transform)) return "grow";
33933
+ if (/rotate\(/i.test(transform)) return "tilt";
33934
+ if (transform && transform !== "none") return "shift";
33935
+ if (style.boxShadow) return "shadow";
33936
+ if (style.opacity) return "dim";
33937
+ if (style.backgroundColor || style.color) return "recolor";
33938
+ return null;
33939
+ };
33940
+ const readStyleRule = (rule, insideScrollTimeline) => {
33941
+ const style = rule.style;
33942
+ const isHover = /:hover\b/.test(rule.selectorText);
33943
+ if (isHover) {
33944
+ const kind = classifyHover(style);
33945
+ if (!kind || !inSection(rule.selectorText)) return;
33946
+ hover.push({
33947
+ kind,
33948
+ selector: rule.selectorText.slice(0, 120),
33949
+ durationMs: toMs(style.transitionDuration ?? ""),
33950
+ easing: style.transitionTimingFunction || void 0
33951
+ });
33952
+ return;
33953
+ }
33954
+ const animationName = style.animationName;
33955
+ if (!animationName || animationName === "none") return;
33956
+ if (!inSection(rule.selectorText)) return;
33957
+ const effect = {
33958
+ kind: animationName,
33959
+ selector: rule.selectorText.slice(0, 120),
33960
+ durationMs: toMs(style.animationDuration ?? ""),
33961
+ delayMs: toMs(style.animationDelay ?? ""),
33962
+ easing: style.animationTimingFunction || void 0
33963
+ };
33964
+ const infinite = (style.animationIterationCount ?? "").includes("infinite");
33965
+ const scrollDriven = insideScrollTimeline || Boolean(style.getPropertyValue("animation-timeline"));
33966
+ if (scrollDriven) scroll.push(effect);
33967
+ else if (infinite) loop.push(effect);
33968
+ else entrance.push(effect);
33969
+ };
33970
+ const walk = (rules, insideScrollTimeline) => {
33971
+ for (const rule of Array.from(rules)) {
33972
+ if (rule instanceof CSSKeyframesRule) {
33973
+ keyframesByName.set(rule.name, rule.cssText);
33974
+ continue;
33975
+ }
33976
+ if (rule instanceof CSSStyleRule) {
33977
+ try {
33978
+ readStyleRule(rule, insideScrollTimeline);
33979
+ } catch {
33980
+ }
33981
+ continue;
33982
+ }
33983
+ if (rule instanceof CSSMediaRule) {
33984
+ if (rule.conditionText.includes("prefers-reduced-motion")) {
33985
+ respectsReducedMotion = true;
33986
+ continue;
33987
+ }
33988
+ walk(rule.cssRules, insideScrollTimeline);
33989
+ continue;
33990
+ }
33991
+ const grouping = rule;
33992
+ if (grouping.cssRules) walk(grouping.cssRules, insideScrollTimeline);
33993
+ }
33994
+ };
33995
+ for (const sheet of Array.from(document.styleSheets)) {
33996
+ if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
33997
+ try {
33998
+ walk(sheet.cssRules, false);
33999
+ } catch {
34000
+ }
34001
+ }
34002
+ for (const effect of [...entrance, ...loop, ...scroll]) {
34003
+ const body = keyframesByName.get(effect.kind);
34004
+ if (!body) continue;
34005
+ const classified = classifyKeyframes(body);
34006
+ effect.kind = loop.includes(effect) && classified.startsWith("slide") ? "marquee" : classified;
34007
+ }
34008
+ const libraries = [];
34009
+ const scoped = window ?? {};
34010
+ if (scoped.gsap || document.querySelector("[data-gsap]")) libraries.push("gsap");
34011
+ if (document.querySelector("[data-framer-name], [data-projection-id]")) libraries.push("framer-motion");
34012
+ if (document.querySelector("[data-aos]")) libraries.push("aos");
34013
+ if (scoped.Lenis || document.querySelector("[data-lenis]")) libraries.push("lenis");
34014
+ if (document.querySelector("[data-scroll], [data-scroll-container]")) libraries.push("locomotive");
34015
+ return {
34016
+ hasMotion: entrance.length + hover.length + scroll.length + loop.length + libraries.length > 0,
34017
+ entrance: entrance.slice(0, 12),
34018
+ hover: hover.slice(0, 12),
34019
+ scroll: scroll.slice(0, 12),
34020
+ loop: loop.slice(0, 12),
34021
+ libraries,
34022
+ respectsReducedMotion
34023
+ };
34024
+ };
34025
+
34026
+ // src/engine/landing-library/motionTake.ts
34027
+ import sharp5 from "sharp";
34028
+
34029
+ // src/engine/landing-library/prepare.ts
34030
+ var CONSENT_SELECTORS = [
34031
+ "#onetrust-accept-btn-handler",
34032
+ "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
34033
+ "button#didomi-notice-agree-button",
34034
+ "[aria-label='Accept all']",
34035
+ "[data-testid='uc-accept-all-button']",
34036
+ ".cc-allow",
34037
+ ".cookie-accept"
34038
+ ];
34039
+ var CONSENT_TEXTS = ["Accept all", "Accept All", "Allow all", "I agree", "Got it", "Aceptar todo"];
34040
+ var CONSENT_HOSTS = [
34041
+ "transcend-cdn.com",
34042
+ "cookielaw.org",
34043
+ "onetrust.com",
34044
+ "cookiebot.com",
34045
+ "osano.com",
34046
+ "trustarc.com",
34047
+ "truste.com",
34048
+ "usercentrics.eu",
34049
+ "didomi.io",
34050
+ "privacy-center.org",
34051
+ "iubenda.com",
34052
+ "termly.io",
34053
+ "cookieyes.com",
34054
+ "sp-prod.net",
34055
+ "quantcast.com",
34056
+ "consensu.org",
34057
+ "ketch.com",
34058
+ "secureprivacy.ai",
34059
+ "civicuk.com"
34060
+ ];
34061
+ async function blockConsentManagers(page) {
34062
+ await page.route("**/*", (route) => {
34063
+ let host = "";
34064
+ try {
34065
+ host = new URL(route.request().url()).host;
34066
+ } catch {
34067
+ return route.continue();
34068
+ }
34069
+ const isConsentVendor = CONSENT_HOSTS.some((vendor) => host === vendor || host.endsWith(`.${vendor}`));
34070
+ return isConsentVendor ? route.abort() : route.continue();
34071
+ });
34072
+ }
34073
+ var ignore = () => void 0;
34074
+ async function preparePage(page, url, timeoutMs) {
34075
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
34076
+ const status = response?.status() ?? null;
34077
+ await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(ignore);
34078
+ await dismissConsent(page, 4e3);
34079
+ await scrollThroughPage(page);
34080
+ await dismissConsent(page, 1e3);
34081
+ await page.evaluate(async () => {
34082
+ await document.fonts.ready;
34083
+ });
34084
+ await freezeMotion(page);
34085
+ await unpinOverlays(page);
34086
+ const measured = await page.evaluate(() => ({
34087
+ finalUrl: location.href,
34088
+ title: document.title,
34089
+ documentHeight: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
34090
+ bodyText: (document.body?.innerText ?? "").slice(0, 2e3)
34091
+ }));
34092
+ return { ...measured, status };
34093
+ }
34094
+ async function settleConsent(page) {
34095
+ await dismissConsent(page, 4e3);
34096
+ await page.mouse.wheel(0, 400).catch(ignore);
34097
+ await page.waitForTimeout(1500);
34098
+ await dismissConsent(page, 2e3);
34099
+ await page.evaluate(() => window.scrollTo(0, 0)).catch(ignore);
34100
+ }
34101
+ async function dismissConsent(page, waitForBannerMs) {
34102
+ await page.locator(CONSENT_SELECTORS.join(", ")).first().waitFor({ state: "attached", timeout: waitForBannerMs }).catch(ignore);
34103
+ for (const selector of CONSENT_SELECTORS) {
34104
+ const found = page.locator(selector).first();
34105
+ if (!await found.count().catch(() => 0)) continue;
34106
+ await found.click({ timeout: 2e3, force: true }).catch(ignore);
34107
+ await page.waitForTimeout(400);
34108
+ return;
34109
+ }
34110
+ for (const text of CONSENT_TEXTS) {
34111
+ const button = page.getByRole("button", { name: text, exact: false }).first();
34112
+ if (!await button.count().catch(() => 0)) continue;
34113
+ if (!await button.isVisible().catch(() => false)) continue;
34114
+ await button.click({ timeout: 2e3, force: true }).catch(ignore);
34115
+ await page.waitForTimeout(400);
34116
+ return;
34117
+ }
34118
+ }
34119
+ async function scrollThroughPage(page) {
34120
+ await page.evaluate(async () => {
34121
+ const step = 600;
34122
+ const pause = () => new Promise((resolve5) => setTimeout(resolve5, 250));
34123
+ for (let i = 0; i < 40; i++) {
34124
+ window.scrollBy(0, step);
34125
+ await pause();
34126
+ const reachedBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
34127
+ if (reachedBottom) break;
34128
+ }
34129
+ window.scrollTo(0, 0);
34130
+ await pause();
34131
+ });
34132
+ await page.waitForTimeout(500);
34133
+ }
34134
+ var MAX_HEADER_HEIGHT = 220;
34135
+ async function unpinOverlays(page) {
34136
+ await page.evaluate((maxHeaderHeight) => {
34137
+ window.scrollTo(0, 0);
34138
+ for (const element of Array.from(document.querySelectorAll("*"))) {
34139
+ const position = getComputedStyle(element).position;
34140
+ if (position === "sticky") {
34141
+ element.style.setProperty("position", "static", "important");
34142
+ continue;
34143
+ }
34144
+ if (position !== "fixed") continue;
34145
+ const rect = element.getBoundingClientRect();
34146
+ const isTopAnchoredHeader = rect.top <= 8 && rect.height > 0 && rect.height <= maxHeaderHeight;
34147
+ if (isTopAnchoredHeader) {
34148
+ element.style.setProperty("position", "absolute", "important");
34149
+ element.style.setProperty("bottom", "auto", "important");
34150
+ } else {
34151
+ element.style.setProperty("display", "none", "important");
34152
+ }
34153
+ }
34154
+ }, MAX_HEADER_HEIGHT);
34155
+ await page.waitForTimeout(250);
34156
+ }
34157
+ async function freezeMotion(page) {
34158
+ await page.evaluate(() => {
34159
+ const highestTimer = window.setTimeout(() => void 0, 0);
34160
+ for (let id = 1; id <= highestTimer; id++) window.clearInterval(id);
34161
+ const style = document.createElement("style");
34162
+ style.setAttribute("data-baker-freeze", "true");
34163
+ style.textContent = `*, *::before, *::after {
34164
+ animation-play-state: paused !important;
34165
+ animation-delay: 0s !important;
34166
+ transition: none !important;
34167
+ }`;
34168
+ document.head.appendChild(style);
34169
+ for (const video of Array.from(document.querySelectorAll("video"))) {
34170
+ video.pause();
34171
+ }
34172
+ });
34173
+ await page.waitForTimeout(250);
34174
+ }
34175
+
34176
+ // src/engine/landing-library/geometry.ts
34177
+ function adaptiveGapThreshold(bands) {
34178
+ const sorted = [...bands].sort((a, b) => a.top - b.top);
34179
+ const gaps = [];
34180
+ for (let i = 0; i < sorted.length - 1; i++) {
34181
+ const current = sorted[i];
34182
+ const next = sorted[i + 1];
34183
+ if (!current || !next) continue;
34184
+ if (next.top > current.bottom) {
34185
+ const gap = next.top - current.bottom;
34186
+ if (gap < 200) gaps.push(gap);
34187
+ }
34188
+ }
34189
+ gaps.sort((a, b) => a - b);
34190
+ const p75 = gaps[Math.floor(gaps.length * 0.75)];
34191
+ if (p75 === void 0) return 15;
34192
+ return Math.max(10, Math.min(50, p75));
34193
+ }
34194
+ function bandsCollide(a, b, margin, maxGap) {
34195
+ const shrunkA = { top: a.top + margin, bottom: a.bottom - margin };
34196
+ const shrunkB = { top: b.top + margin, bottom: b.bottom - margin };
34197
+ const overlaps = shrunkA.top <= shrunkB.bottom && shrunkA.bottom >= shrunkB.top || shrunkB.top <= shrunkA.bottom && shrunkB.bottom >= shrunkA.top;
34198
+ if (overlaps) return true;
34199
+ const gap = Math.min(Math.abs(shrunkA.bottom - shrunkB.top), Math.abs(shrunkB.bottom - shrunkA.top));
34200
+ return gap <= maxGap;
34201
+ }
34202
+ function shouldPromoteToParent(group) {
34203
+ const isRunt = group.childCount < 3 || group.height < 80;
34204
+ return isRunt && group.parentHeight < 1600;
34205
+ }
34206
+
34207
+ // src/engine/landing-library/segment.ts
34208
+ async function installPageRuntime(page) {
34209
+ await page.addInitScript({
34210
+ content: `
34211
+ window.__name = window.__name || function (target) { return target; };
34212
+ window.__bakerGeom = {
34213
+ adaptiveGapThreshold: ${adaptiveGapThreshold.toString()},
34214
+ bandsCollide: ${bandsCollide.toString()},
34215
+ shouldPromoteToParent: ${shouldPromoteToParent.toString()},
34216
+ };`
34217
+ });
34218
+ }
34219
+ async function segmentPage(page, options) {
34220
+ return await page.evaluate(inPageSegment, options);
34221
+ }
34222
+ var inPageSegment = (options) => {
34223
+ const geom = window.__bakerGeom;
34224
+ const scrollX = window.scrollX;
34225
+ const scrollY = window.scrollY;
34226
+ const docWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0);
34227
+ const docHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0);
34228
+ const rectOf = (element) => {
34229
+ const r = element.getBoundingClientRect();
34230
+ const top = Math.max(0, Math.round(r.top + scrollY));
34231
+ const left = Math.max(0, Math.round(r.left + scrollX));
34232
+ return {
34233
+ top,
34234
+ left,
34235
+ bottom: Math.min(docHeight, Math.round(r.bottom + scrollY)),
34236
+ right: Math.min(docWidth, Math.round(r.right + scrollX))
34237
+ };
34238
+ };
34239
+ const syntheticRect = (element) => {
34240
+ const children = Array.from(element.children);
34241
+ if (children.length === 0) return null;
34242
+ let top = Number.POSITIVE_INFINITY;
34243
+ let left = Number.POSITIVE_INFINITY;
34244
+ let bottom = Number.NEGATIVE_INFINITY;
34245
+ let right = Number.NEGATIVE_INFINITY;
34246
+ for (const child of children) {
34247
+ const r = rectOf(child);
34248
+ if (r.bottom - r.top <= 0 && r.right - r.left <= 0) continue;
34249
+ top = Math.min(top, r.top);
34250
+ left = Math.min(left, r.left);
34251
+ bottom = Math.max(bottom, r.bottom);
34252
+ right = Math.max(right, r.right);
34253
+ }
34254
+ if (!Number.isFinite(top) || !Number.isFinite(bottom)) return null;
34255
+ return { top, left, bottom, right };
34256
+ };
34257
+ const boxOf = (element) => {
34258
+ const style = getComputedStyle(element);
34259
+ const r = style.display === "contents" ? syntheticRect(element) : rectOf(element);
34260
+ if (!r) return null;
34261
+ return { ...r, height: r.bottom - r.top, width: r.right - r.left };
34262
+ };
34263
+ const hasHiddenAncestor = (element) => {
34264
+ let current = element;
34265
+ while (current && current !== document.documentElement) {
34266
+ const style = getComputedStyle(current);
34267
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return true;
34268
+ current = current.parentElement;
34269
+ }
34270
+ return false;
34271
+ };
34272
+ const isClippedByAncestor = (element, box) => {
34273
+ let parent = element.parentElement;
34274
+ while (parent && parent !== document.documentElement) {
34275
+ const style = getComputedStyle(parent);
34276
+ const clips = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden" || style.overflow === "clip";
34277
+ if (clips) {
34278
+ const p = rectOf(parent);
34279
+ const intersects = box.left < p.right && box.right > p.left && box.top < p.bottom && box.bottom > p.top;
34280
+ if (!intersects) return true;
34281
+ }
34282
+ parent = parent.parentElement;
34283
+ }
34284
+ return false;
34285
+ };
34286
+ const directText = (element) => {
34287
+ let text = "";
34288
+ for (const node of Array.from(element.childNodes)) {
34289
+ if (node.nodeType === 3) text += node.textContent ?? "";
34290
+ }
34291
+ return text.trim();
34292
+ };
34293
+ const NON_COPY_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
34294
+ const visibleText = (root) => {
34295
+ let text = "";
34296
+ const hiddenCache = /* @__PURE__ */ new Map();
34297
+ const isHidden = (element) => {
34298
+ const cached = hiddenCache.get(element);
34299
+ if (cached !== void 0) return cached;
34300
+ const style = getComputedStyle(element);
34301
+ const hidden = style.display === "none" || style.visibility === "hidden";
34302
+ hiddenCache.set(element, hidden);
34303
+ return hidden;
34304
+ };
34305
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
34306
+ acceptNode: (node) => {
34307
+ let parent = node.parentElement;
34308
+ while (parent) {
34309
+ if (NON_COPY_TAGS.has(parent.tagName) || isHidden(parent)) return NodeFilter.FILTER_REJECT;
34310
+ if (parent === root) break;
34311
+ parent = parent.parentElement;
34312
+ }
34313
+ return NodeFilter.FILTER_ACCEPT;
34314
+ }
34315
+ });
34316
+ while (walker.nextNode() && text.length < 400) {
34317
+ text += ` ${walker.currentNode.textContent ?? ""}`;
34318
+ }
34319
+ return text.replace(/\s+/g, " ").trim();
34320
+ };
34321
+ const GRAPHIC_TAGS = /* @__PURE__ */ new Set(["img", "svg", "video", "picture", "canvas", "iframe"]);
34322
+ const hasBackgroundImage = (element) => {
34323
+ const bg = getComputedStyle(element).backgroundImage;
34324
+ return Boolean(bg) && bg !== "none" && bg.includes("url(");
34325
+ };
34326
+ const isContentLeaf = (element) => {
34327
+ const tag = element.tagName.toLowerCase();
34328
+ if (GRAPHIC_TAGS.has(tag)) return true;
34329
+ if (directText(element).length > 0) return true;
34330
+ return hasBackgroundImage(element);
34331
+ };
34332
+ const selectorFor = (element) => {
34333
+ const parts = [];
34334
+ let current = element;
34335
+ while (current && current !== document.documentElement) {
34336
+ const tag = current.tagName.toLowerCase();
34337
+ if (tag === "body") {
34338
+ parts.unshift("body");
34339
+ break;
34340
+ }
34341
+ const parent = current.parentElement;
34342
+ if (!parent) {
34343
+ parts.unshift(tag);
34344
+ break;
34345
+ }
34346
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current?.tagName);
34347
+ const position = sameTag.indexOf(current) + 1;
34348
+ parts.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${position})` : tag);
34349
+ current = parent;
34350
+ }
34351
+ return parts.join(" > ");
34352
+ };
34353
+ const leaves = [];
34354
+ const all = document.querySelectorAll("*");
34355
+ const limit = Math.min(all.length, options.maxElements);
34356
+ for (let i = 0; i < limit; i++) {
34357
+ const element = all[i];
34358
+ if (!element) continue;
34359
+ const tag = element.tagName.toLowerCase();
34360
+ if (tag === "script" || tag === "style" || tag === "noscript" || tag === "link" || tag === "head") continue;
34361
+ if (!isContentLeaf(element)) continue;
34362
+ const box = boxOf(element);
34363
+ if (!box || box.height <= 0 || box.width <= 0) continue;
34364
+ if (box.height > options.maxSectionHeight) continue;
34365
+ if (hasHiddenAncestor(element)) continue;
34366
+ if (isClippedByAncestor(element, box)) continue;
34367
+ leaves.push({
34368
+ element,
34369
+ top: box.top,
34370
+ bottom: box.bottom,
34371
+ left: box.left,
34372
+ right: box.right,
34373
+ height: box.height
34374
+ });
34375
+ }
34376
+ if (leaves.length === 0) return [];
34377
+ const gapThreshold = geom.adaptiveGapThreshold(leaves.map((l) => ({ top: l.top, bottom: l.bottom })));
34378
+ const commonAncestor = (elements) => {
34379
+ let ancestor = elements[0] ?? null;
34380
+ while (ancestor && !elements.every((e) => ancestor?.contains(e))) {
34381
+ ancestor = ancestor.parentElement;
34382
+ }
34383
+ return ancestor;
34384
+ };
34385
+ const abandon = (members) => members.map((m) => ({ ...m, sealed: true }));
34386
+ const findCoveringAncestor = (start, bounds) => {
34387
+ let element = start;
34388
+ while (element) {
34389
+ const box = boxOf(element);
34390
+ if (!box) return null;
34391
+ if (box.height > options.maxSectionHeight) return null;
34392
+ const covers = box.top <= bounds.top && box.bottom >= bounds.bottom && box.left <= bounds.left && box.right >= bounds.right;
34393
+ if (covers) return { element, box };
34394
+ if (!element.parentElement || element.parentElement === document.documentElement) return null;
34395
+ element = element.parentElement;
34396
+ }
34397
+ return null;
34398
+ };
34399
+ const mergeBucket = (members) => {
34400
+ const bounds = {
34401
+ top: Math.min(...members.map((m) => m.top)),
34402
+ bottom: Math.max(...members.map((m) => m.bottom)),
34403
+ left: Math.min(...members.map((m) => m.left)),
34404
+ right: Math.max(...members.map((m) => m.right))
34405
+ };
34406
+ const covering = findCoveringAncestor(commonAncestor(members.map((m) => m.root)), bounds);
34407
+ if (!covering) return abandon(members);
34408
+ return [
34409
+ {
34410
+ root: covering.element,
34411
+ top: covering.box.top,
34412
+ bottom: covering.box.bottom,
34413
+ left: covering.box.left,
34414
+ right: covering.box.right,
34415
+ height: covering.box.height,
34416
+ leaves: members.flatMap((m) => m.leaves),
34417
+ sealed: false
34418
+ }
34419
+ ];
34420
+ };
34421
+ const clusterOnce = (groups2) => {
34422
+ const buckets = [];
34423
+ for (const group of groups2) {
34424
+ if (group.sealed) {
34425
+ buckets.push([group]);
34426
+ continue;
34427
+ }
34428
+ const target = buckets.find(
34429
+ (bucket) => bucket.some(
34430
+ (member) => !member.sealed && geom.bandsCollide(
34431
+ { top: member.top, bottom: member.bottom },
34432
+ { top: group.top, bottom: group.bottom },
34433
+ options.collisionMargin,
34434
+ gapThreshold
34435
+ )
34436
+ )
34437
+ );
34438
+ if (target) target.push(group);
34439
+ else buckets.push([group]);
34440
+ }
34441
+ if (buckets.length === groups2.length) return groups2;
34442
+ return buckets.flatMap((bucket) => bucket.length === 1 ? [bucket[0]] : mergeBucket(bucket));
34443
+ };
34444
+ let groups = leaves.map((leaf) => ({
34445
+ root: leaf.element,
34446
+ top: leaf.top,
34447
+ bottom: leaf.bottom,
34448
+ left: leaf.left,
34449
+ right: leaf.right,
34450
+ height: leaf.height,
34451
+ leaves: [leaf],
34452
+ sealed: false
34453
+ }));
34454
+ for (let pass = 0; pass < 40; pass++) {
34455
+ const next = clusterOnce(groups);
34456
+ if (next.length === groups.length) break;
34457
+ groups = next;
34458
+ }
34459
+ for (let pass = 0; pass < 10; pass++) {
34460
+ let promoted = false;
34461
+ groups = groups.map((group) => {
34462
+ const parent = group.root.parentElement;
34463
+ if (!parent || parent === document.documentElement || parent === document.body) return group;
34464
+ const parentBox = boxOf(parent);
34465
+ if (!parentBox) return group;
34466
+ if (!geom.shouldPromoteToParent({
34467
+ childCount: group.leaves.length,
34468
+ height: group.height,
34469
+ parentHeight: parentBox.height
34470
+ })) {
34471
+ return group;
34472
+ }
34473
+ promoted = true;
34474
+ return {
34475
+ ...group,
34476
+ root: parent,
34477
+ top: parentBox.top,
34478
+ bottom: parentBox.bottom,
34479
+ left: parentBox.left,
34480
+ right: parentBox.right,
34481
+ height: parentBox.height
34482
+ };
34483
+ });
34484
+ if (!promoted) break;
34485
+ groups = clusterOnce(groups);
34486
+ }
34487
+ const sameBox = (a, b) => Math.abs(a.top - b.top) <= 4 && Math.abs(a.bottom - b.bottom) <= 4 && Math.abs(a.left - b.left) <= 4 && Math.abs(a.right - b.right) <= 4;
34488
+ const containsBox = (outer, inner) => outer.top - 4 <= inner.top && outer.bottom + 4 >= inner.bottom && outer.left - 4 <= inner.left && outer.right + 4 >= inner.right;
34489
+ const deduped = [];
34490
+ const area = (g) => (g.right - g.left) * g.height;
34491
+ for (const group of groups.slice().sort((a, b) => area(b) - area(a) || b.leaves.length - a.leaves.length)) {
34492
+ const duplicate = deduped.some(
34493
+ (kept) => kept.root === group.root || kept.root.contains(group.root) || sameBox(kept, group) || containsBox(kept, group)
34494
+ );
34495
+ if (!duplicate) deduped.push(group);
34496
+ }
34497
+ return deduped.filter((group) => group.height > 0 && group.right - group.left > 0).sort((a, b) => a.top - b.top).map((group, index) => ({
34498
+ index,
34499
+ selector: selectorFor(group.root),
34500
+ rect: {
34501
+ top: group.top,
34502
+ left: group.left,
34503
+ width: group.right - group.left,
34504
+ height: group.height
34505
+ },
34506
+ leafCount: group.leaves.length,
34507
+ textPreview: visibleText(group.root).slice(0, 200)
34508
+ }));
34509
+ };
34510
+
34511
+ // src/engine/landing-library/motionTake.ts
34512
+ var FRAME_TIMES_MS = [0, 120, 260, 450, 800, 1400];
34513
+ var FRAME_WIDTH = 460;
34514
+ var GRID_COLUMNS = 3;
34515
+ var LABEL_HEIGHT = 22;
34516
+ async function captureMotionTake(browser, url, selector, timeoutMs = 45e3) {
34517
+ const { context, page } = await newPage(browser, DESKTOP_VIEWPORT, { motion: true });
34518
+ try {
34519
+ await blockConsentManagers(page);
34520
+ await installPageRuntime(page);
34521
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
34522
+ await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => void 0);
34523
+ await settleConsent(page);
34524
+ const target = page.locator(selector).first();
34525
+ if (!await target.count().catch(() => 0)) return null;
34526
+ await page.evaluate((sectionSelector) => {
34527
+ const element = document.querySelector(sectionSelector);
34528
+ if (!element) return;
34529
+ const top = element.getBoundingClientRect().top + window.scrollY;
34530
+ window.scrollTo(0, Math.max(0, top - window.innerHeight - 200));
34531
+ }, selector);
34532
+ await page.waitForTimeout(600);
34533
+ await page.evaluate((sectionSelector) => {
34534
+ document.querySelector(sectionSelector)?.scrollIntoView({ block: "center" });
34535
+ }, selector);
34536
+ const frames = [];
34537
+ let previous = 0;
34538
+ for (const time of FRAME_TIMES_MS) {
34539
+ await page.waitForTimeout(Math.max(0, time - previous));
34540
+ previous = time;
34541
+ const shot = await page.screenshot({ type: "png", timeout: 1e4 }).catch(() => null);
34542
+ if (shot) frames.push(shot);
34543
+ }
34544
+ if (frames.length === 0) return null;
34545
+ return { filmstrip: await composeFilmstrip(frames), frameCount: frames.length };
34546
+ } catch {
34547
+ return null;
34548
+ } finally {
34549
+ await context.close();
34550
+ }
34551
+ }
34552
+ async function composeFilmstrip(frames) {
34553
+ const scaled = await Promise.all(frames.map((frame) => sharp5(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
34554
+ const first = await sharp5(scaled[0]).metadata();
34555
+ const frameHeight = first.height ?? 300;
34556
+ const cellHeight = frameHeight + LABEL_HEIGHT;
34557
+ const rows = Math.ceil(scaled.length / GRID_COLUMNS);
34558
+ const width = FRAME_WIDTH * Math.min(GRID_COLUMNS, scaled.length);
34559
+ const composites = scaled.flatMap((frame, index) => {
34560
+ const column = index % GRID_COLUMNS;
34561
+ const row = Math.floor(index / GRID_COLUMNS);
34562
+ const label = Buffer.from(
34563
+ `<svg width="${FRAME_WIDTH}" height="${LABEL_HEIGHT}">
34564
+ <rect width="100%" height="100%" fill="#111827"/>
34565
+ <text x="8" y="15" font-family="monospace" font-size="12" fill="#f9fafb">+${FRAME_TIMES_MS[index] ?? 0}ms</text>
34566
+ </svg>`
34567
+ );
34568
+ return [
34569
+ { input: label, left: column * FRAME_WIDTH, top: row * cellHeight },
34570
+ { input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
34571
+ ];
34572
+ });
34573
+ return await sharp5({
34574
+ create: {
34575
+ width,
34576
+ height: cellHeight * rows,
34577
+ channels: 3,
34578
+ background: { r: 17, g: 24, b: 39 }
34579
+ }
34580
+ }).composite(composites).png().toBuffer();
34581
+ }
34582
+
34583
+ // src/engine/landing-library/renderBundle.ts
34584
+ var SETTLE_ANIMATIONS_CSS = `*, *::before, *::after {
34585
+ animation-play-state: running !important;
34586
+ animation-delay: 0s !important;
34587
+ animation-duration: 1ms !important;
34588
+ animation-iteration-count: 1 !important;
34589
+ animation-fill-mode: forwards !important;
34590
+ transition: none !important;
34591
+ }`;
34592
+ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
34593
+ const { wholePage = false, timeoutMs = 2e4 } = options;
34594
+ const { context, page } = await newPage(browser, { width: viewportWidth, height: 900 });
34595
+ try {
34596
+ await page.setContent(html, { waitUntil: "load", timeout: timeoutMs });
34597
+ await page.addStyleTag({ content: SETTLE_ANIMATIONS_CSS });
34598
+ await page.evaluate(async () => {
34599
+ await document.fonts.ready;
34600
+ });
34601
+ await page.waitForTimeout(300);
34602
+ if (!wholePage) {
34603
+ for (const selector of [`[${SECTION_ROOT_ATTRIBUTE}]`, "body > *"]) {
34604
+ const target = page.locator(selector).first();
34605
+ if (!await target.count()) continue;
34606
+ const shot = await target.screenshot({ type: "png", timeout: timeoutMs }).catch(() => null);
34607
+ if (shot) return shot;
34608
+ }
34609
+ }
34610
+ return await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
34611
+ } catch {
34612
+ return null;
34613
+ } finally {
34614
+ await context.close();
34615
+ }
34616
+ }
34617
+
34618
+ // src/engine/landing-library/report.ts
34619
+ import { writeFile as writeFile14 } from "fs/promises";
34620
+ import path30 from "path";
34621
+ async function writeCaptureReport(manifest, outDir) {
34622
+ const file = path30.join(outDir, "report.html");
34623
+ await writeFile14(file, renderReport(manifest));
34624
+ return file;
34625
+ }
34626
+ function escapeHtml3(value) {
34627
+ return value.replace(
34628
+ /[&<>"']/g,
34629
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character
34630
+ );
34631
+ }
34632
+ function fidelityTone(fidelity) {
34633
+ if (fidelity === null) return "unknown";
34634
+ if (fidelity >= 0.95) return "good";
34635
+ if (fidelity >= 0.85) return "fair";
34636
+ return "poor";
34637
+ }
34638
+ function medianFidelity(sections) {
34639
+ const scored = sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
34640
+ return scored.length === 0 ? null : scored[Math.floor(scored.length / 2)] ?? null;
34641
+ }
34642
+ function formatFidelity(fidelity) {
34643
+ return fidelity === null ? "\u2014" : fidelity.toFixed(2);
34644
+ }
34645
+ function renderSection3(section) {
34646
+ const tone = fidelityTone(section.fidelity);
34647
+ const rendered = section.bundle ? section.bundle.replace(/section\.html$/, "section-rendered.png") : null;
34648
+ const shot = (label, src, note) => {
34649
+ if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
34650
+ return `<figure class="shot">
34651
+ <figcaption>${label}${note ? ` <span class="note">${escapeHtml3(note)}</span>` : ""}</figcaption>
34652
+ <a href="${escapeHtml3(src)}" target="_blank" rel="noopener"><img src="${escapeHtml3(src)}" alt="" loading="lazy"></a>
34653
+ </figure>`;
34654
+ };
34655
+ return `<section class="card">
34656
+ <header>
34657
+ <h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
34658
+ <span class="badge ${tone}">fidelity ${formatFidelity(section.fidelity)}</span>
34659
+ ${section.fidelityNote ? `<span class="badge warn">${escapeHtml3(section.fidelityNote)}</span>` : ""}
34660
+ ${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml3(section.motion.summary)}</span>` : ""}
34661
+ </header>
34662
+ <p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
34663
+ <div class="shots">
34664
+ ${shot("Live", section.desktopShot)}
34665
+ ${shot("Reproduction", rendered, "rendered from the extracted bundle")}
34666
+ ${shot("Mobile", section.mobileShot)}
34667
+ </div>
34668
+ ${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
34669
+ <footer>
34670
+ <code>${escapeHtml3(section.selector)}</code>
34671
+ ${section.bundle ? `<a href="${escapeHtml3(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
34672
+ </footer>
34673
+ </section>`;
34674
+ }
34675
+ function renderReport(manifest) {
34676
+ const median = medianFidelity(manifest.sections);
34677
+ const moving = manifest.sections.filter((section) => section.motionFilmstrip !== null).length;
34678
+ return `<!doctype html>
34679
+ <html lang="en">
34680
+ <head>
34681
+ <meta charset="utf-8">
34682
+ <meta name="viewport" content="width=device-width, initial-scale=1">
34683
+ <title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
34684
+ <style>
34685
+ :root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
34686
+ @media (prefers-color-scheme: dark) {
34687
+ :root { --line: #27272a; --muted: #a1a1aa; --bg: #09090b; --card: #131316; }
34688
+ }
34689
+ * { box-sizing: border-box; }
34690
+ body { margin: 0; padding: 24px; background: var(--bg);
34691
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
34692
+ h1 { margin: 0 0 4px; font-size: 20px; }
34693
+ a { color: inherit; }
34694
+ .sub { color: var(--muted); margin: 0 0 20px; }
34695
+ .stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
34696
+ .stat { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; background: var(--card); }
34697
+ .stat b { display: block; font-size: 18px; }
34698
+ .stat span { color: var(--muted); font-size: 12px; }
34699
+ .card { border: 1px solid var(--line); border-radius: 12px; background: var(--card);
34700
+ padding: 16px; margin-bottom: 16px; }
34701
+ .card header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
34702
+ .card h2 { font-size: 15px; margin: 0; font-weight: 600; }
34703
+ .index { display: inline-block; min-width: 26px; color: var(--muted); }
34704
+ .badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
34705
+ .badge.good { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
34706
+ .badge.fair { background: #fef3c7; color: #92400e; border-color: #fde68a; }
34707
+ .badge.poor { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
34708
+ .badge.warn { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
34709
+ .badge.motion { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
34710
+ .preview { color: var(--muted); margin: 0 0 12px; font-size: 13px; }
34711
+ .shots { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; align-items: start; }
34712
+ .filmstrip { margin-top: 12px; }
34713
+ figure { margin: 0; }
34714
+ figcaption { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
34715
+ figcaption .note { opacity: .75; }
34716
+ .shot img { width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px;
34717
+ background: #fff; display: block; }
34718
+ .shot.empty { border: 1px dashed var(--line); border-radius: 8px; padding: 20px; text-align: center; }
34719
+ .card footer { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px;
34720
+ font-size: 12px; color: var(--muted); }
34721
+ code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
34722
+ </style>
34723
+ </head>
34724
+ <body>
34725
+ <h1>${escapeHtml3(manifest.title || "Capture report")}</h1>
34726
+ <p class="sub"><a href="${escapeHtml3(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml3(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml3(manifest.capturedAt)}</p>
34727
+
34728
+ <div class="stats">
34729
+ <div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
34730
+ <div class="stat"><b>${formatFidelity(median)}</b><span>median fidelity</span></div>
34731
+ <div class="stat"><b>${formatFidelity(manifest.page.fidelity)}</b><span>whole page</span></div>
34732
+ <div class="stat"><b>${moving}</b><span>filmed moving</span></div>
34733
+ <div class="stat"><b>${manifest.documentHeight}px</b><span>page height</span></div>
34734
+ </div>
34735
+
34736
+ <section class="card">
34737
+ <header>
34738
+ <h2>Whole page</h2>
34739
+ <span class="badge ${fidelityTone(manifest.page.fidelity)}">fidelity ${formatFidelity(manifest.page.fidelity)}</span>
34740
+ </header>
34741
+ <p class="preview">The same extractor rooted at &lt;body&gt; \u2014 one standalone file that should render like the original.</p>
34742
+ <div class="shots">
34743
+ <figure class="shot"><figcaption>Live</figcaption><a href="full-page.png" target="_blank" rel="noopener"><img src="full-page.png" alt="" loading="lazy"></a></figure>
34744
+ ${manifest.page.bundle ? `<figure class="shot"><figcaption>Reproduction <span class="note">from page.html</span></figcaption><a href="page-rendered.png" target="_blank" rel="noopener"><img src="page-rendered.png" alt="" loading="lazy"></a></figure>` : `<figure class="shot empty"><figcaption>Reproduction \u2014 none</figcaption></figure>`}
34745
+ </div>
34746
+ </section>
34747
+
34748
+ ${manifest.sections.map(renderSection3).join("\n")}
34749
+ </body>
34750
+ </html>
34751
+ `;
34752
+ }
34753
+
34754
+ // src/engine/landing-library/types.ts
34755
+ var DEFAULT_SEGMENT_OPTIONS = {
34756
+ maxSectionHeight: 2160,
34757
+ collisionMargin: 2,
34758
+ maxElements: 15e3
34759
+ };
34760
+
34761
+ // src/engine/landing-library/visualHash.ts
34762
+ import sharp6 from "sharp";
34763
+ var HASH_WIDTH = 9;
34764
+ var HASH_HEIGHT = 8;
34765
+ async function perceptualHash(image) {
34766
+ try {
34767
+ const raw = await sharp6(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
34768
+ return bitsToHex(rowGradientBits(new Uint8Array(raw)));
34769
+ } catch {
34770
+ return null;
34771
+ }
34772
+ }
34773
+ function rowGradientBits(pixels) {
34774
+ const bits = [];
34775
+ for (let y = 0; y < HASH_HEIGHT; y++) {
34776
+ for (let x = 0; x < HASH_WIDTH - 1; x++) {
34777
+ const left = pixels[y * HASH_WIDTH + x] ?? 0;
34778
+ const right = pixels[y * HASH_WIDTH + x + 1] ?? 0;
34779
+ bits.push(left > right);
34780
+ }
34781
+ }
34782
+ return bits;
34783
+ }
34784
+ function bitsToHex(bits) {
34785
+ let hex = "";
34786
+ for (let index = 0; index < bits.length; index += 4) {
34787
+ let nibble = 0;
34788
+ for (let offset = 0; offset < 4; offset++) {
34789
+ if (bits[index + offset]) nibble |= 1 << 3 - offset;
34790
+ }
34791
+ hex += nibble.toString(16);
34792
+ }
34793
+ return hex;
34794
+ }
34795
+
34796
+ // src/engine/landing-library/run.ts
34797
+ async function reproducePage(args) {
34798
+ const { browser, page, outDir, pageUrl, livePageShot } = args;
34799
+ const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
34800
+ if (!built) return { bundle: null, fidelity: null };
34801
+ await writeFile15(path31.join(outDir, "page.html"), built.html);
34802
+ const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
34803
+ wholePage: true,
34804
+ timeoutMs: 6e4
34805
+ });
34806
+ if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
34807
+ await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
34808
+ const { score, note } = await scoreFidelity(livePageShot, rendered);
34809
+ return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
34810
+ }
34811
+ async function captureOneSection(args) {
34812
+ const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
34813
+ const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
34814
+ await mkdir11(dir, { recursive: true });
34815
+ const desktop = await captureSection(page, candidate);
34816
+ if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
34817
+ const visualHash = desktop ? await perceptualHash(desktop) : null;
34818
+ const motion = await collectMotion(page, candidate.selector);
34819
+ const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
34820
+ let fidelity = null;
34821
+ let fidelityNote;
34822
+ if (built) {
34823
+ await writeFile15(path31.join(dir, "section.html"), built.html);
34824
+ const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
34825
+ if (rendered && desktop) {
34826
+ await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
34827
+ const result = await scoreFidelity(desktop, rendered);
34828
+ fidelity = result.score;
34829
+ fidelityNote = result.note;
34830
+ }
34831
+ }
34832
+ return {
34833
+ ...candidate,
34834
+ desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
34835
+ mobileShot: null,
34836
+ bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
34837
+ fidelity,
34838
+ ...fidelityNote ? { fidelityNote } : {},
34839
+ ...built ? { cssStats: built.stats } : {},
34840
+ motion,
34841
+ motionFilmstrip: null,
34842
+ visualHash
34843
+ };
34844
+ }
34845
+ async function captureMobileShots(args) {
34846
+ const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs } = args;
34847
+ const mobile = await newPage(browser, MOBILE_VIEWPORT);
34848
+ try {
34849
+ await blockConsentManagers(mobile.page);
34850
+ await installPageRuntime(mobile.page);
34851
+ await preparePage(mobile.page, pageUrl, timeoutMs);
34852
+ for (const section of sections) {
34853
+ const shot = await captureSectionOnMobile(mobile.page, section);
34854
+ if (!shot) continue;
34855
+ const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
34856
+ await writeFile15(file, shot);
34857
+ section.mobileShot = path31.relative(outDir, file);
34858
+ }
34859
+ } finally {
34860
+ await mobile.context.close();
34861
+ }
34862
+ }
34863
+ async function captureMotionTakes(args) {
34864
+ const { browser, sections, sectionsDir, outDir, pageUrl, log } = args;
34865
+ const moving = sections.filter((section) => isWorthFilming(section.motion));
34866
+ if (moving.length === 0) return;
34867
+ log(`filming ${moving.length} moving sections`);
34868
+ for (const section of moving) {
34869
+ const take = await captureMotionTake(browser, pageUrl, section.selector);
34870
+ if (!take) continue;
34871
+ const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
34872
+ const file = path31.join(dir, "motion-filmstrip.png");
34873
+ await writeFile15(file, take.filmstrip);
34874
+ section.motionFilmstrip = path31.relative(outDir, file);
34875
+ log(` [${section.index}] ${section.motion.summary}`);
34876
+ }
34877
+ }
34878
+ async function scrapeLanding(options) {
34879
+ const timeoutMs = options.timeoutMs ?? 45e3;
34880
+ const log = options.onProgress ?? (() => void 0);
34881
+ const sectionsDir = path31.join(options.outDir, "sections");
34882
+ const browser = await launchBrowser();
34883
+ try {
34884
+ const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
34885
+ await blockConsentManagers(page);
34886
+ await installPageRuntime(page);
34887
+ log(`loading ${options.url}`);
34888
+ const prepared = await preparePage(page, options.url, timeoutMs);
34889
+ const blocked = detectBlockedPage({
34890
+ status: prepared.status,
34891
+ title: prepared.title,
34892
+ bodyText: prepared.bodyText,
34893
+ html: await page.content().catch(() => "")
34894
+ });
34895
+ if (blocked) throw new BlockedPageError(blocked);
34896
+ await mkdir11(sectionsDir, { recursive: true });
34897
+ log("segmenting");
34898
+ const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
34899
+ log(`found ${candidates.length} sections`);
34900
+ const sections = [];
34901
+ for (const candidate of candidates) {
34902
+ const section = await captureOneSection({
34903
+ browser,
34904
+ page,
34905
+ candidate,
34906
+ sectionsDir,
34907
+ outDir: options.outDir,
34908
+ pageUrl: prepared.finalUrl,
34909
+ withCode: options.code !== false
34910
+ });
34911
+ sections.push(section);
34912
+ log(
34913
+ ` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
34914
+ );
34915
+ }
34916
+ const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
34917
+ if (fullPage) await writeFile15(path31.join(options.outDir, "full-page.png"), fullPage);
34918
+ const reproduction = options.code === false ? { bundle: null, fidelity: null } : await reproducePage({
34919
+ browser,
34920
+ page,
34921
+ outDir: options.outDir,
34922
+ pageUrl: prepared.finalUrl,
34923
+ livePageShot: fullPage
34924
+ });
34925
+ log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
34926
+ await context.close();
34927
+ if (options.mobile !== false) {
34928
+ log("capturing mobile");
34929
+ await captureMobileShots({
34930
+ browser,
34931
+ sections,
34932
+ sectionsDir,
34933
+ outDir: options.outDir,
34934
+ pageUrl: prepared.finalUrl,
34935
+ timeoutMs
34936
+ });
34937
+ }
34938
+ if (options.motion !== false) {
34939
+ await captureMotionTakes({
34940
+ browser,
34941
+ sections,
34942
+ sectionsDir,
34943
+ outDir: options.outDir,
34944
+ pageUrl: prepared.finalUrl,
34945
+ log
34946
+ });
34947
+ }
34948
+ const manifest = {
34949
+ url: options.url,
34950
+ finalUrl: prepared.finalUrl,
34951
+ title: prepared.title,
34952
+ documentHeight: prepared.documentHeight,
34953
+ viewport: DESKTOP_VIEWPORT,
34954
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
34955
+ sections,
34956
+ page: reproduction
34957
+ };
34958
+ await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
34959
+ `);
34960
+ if (options.report !== false) {
34961
+ const reportPath = await writeCaptureReport(manifest, options.outDir);
34962
+ log(`report: ${reportPath}`);
34963
+ }
34964
+ return manifest;
34965
+ } finally {
34966
+ await browser.close();
34967
+ }
34968
+ }
34969
+
34970
+ // src/commands/landing/inspiration/scrape.ts
34971
+ registerSchema({
34972
+ command: "landing.inspiration.scrape",
34973
+ description: "Internal/ops: capture one landing page into a directory of section screenshots, standalone HTML bundles and motion filmstrips. The ingest job runs this; use it locally to debug a page that studied badly.",
34974
+ args: {
34975
+ url: { type: "string", description: "Page to capture", required: true },
34976
+ out: { type: "string", description: "Output directory", required: true },
34977
+ "no-mobile": { type: "boolean", description: "Skip the phone-viewport pass", required: false },
34978
+ "no-code": {
34979
+ type: "boolean",
34980
+ description: "Segment and screenshot only \u2014 no bundles, no fidelity",
34981
+ required: false
34982
+ },
34983
+ "no-motion": {
34984
+ type: "boolean",
34985
+ description: "Skip filming sections that move (roughly halves runtime)",
34986
+ required: false
34987
+ },
34988
+ "no-report": { type: "boolean", description: "Skip writing report.html", required: false }
34989
+ }
34990
+ });
34991
+ var scrapeCommand = defineCommand147({
34992
+ meta: {
34993
+ name: "scrape",
34994
+ description: "Internal/ops: capture a landing page to a directory. Example: baker landing inspiration scrape https://linear.app --out /tmp/linear"
34995
+ },
34996
+ args: {
34997
+ url: { type: "positional", description: "Page to capture", required: true },
34998
+ out: { type: "string", description: "Output directory", required: true },
34999
+ "no-mobile": { type: "boolean", description: "Skip the phone-viewport pass", required: false, default: false },
35000
+ "no-code": { type: "boolean", description: "Segment and screenshot only", required: false, default: false },
35001
+ "no-motion": { type: "boolean", description: "Skip filming sections that move", required: false, default: false },
35002
+ "no-report": { type: "boolean", description: "Skip writing report.html", required: false, default: false }
35003
+ },
35004
+ run: async ({ args }) => {
35005
+ try {
35006
+ const manifest = await scrapeLanding({
35007
+ url: args.url,
35008
+ outDir: args.out,
35009
+ mobile: !args["no-mobile"],
35010
+ code: !args["no-code"],
35011
+ motion: !args["no-motion"],
35012
+ report: !args["no-report"],
35013
+ // Progress goes to stderr so stdout stays a clean JSON envelope.
35014
+ onProgress: (message) => process.stderr.write(`${message}
35015
+ `)
35016
+ });
35017
+ const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
35018
+ writeJson({
35019
+ ok: true,
35020
+ data: {
35021
+ title: manifest.title,
35022
+ sections: manifest.sections.length,
35023
+ medianFidelity: scored.length > 0 ? scored[Math.floor(scored.length / 2)] : null,
35024
+ pageFidelity: manifest.page.fidelity,
35025
+ out: args.out,
35026
+ report: args["no-report"] ? null : `${args.out}/report.html`
35027
+ }
35028
+ });
35029
+ } catch (error) {
35030
+ if (error instanceof BlockedPageError) {
35031
+ writeJson({ ok: false, error: { code: error.code, message: error.message } });
35032
+ process.exit(1);
35033
+ }
35034
+ reportError(error);
35035
+ }
35036
+ }
35037
+ });
35038
+
35039
+ // src/commands/landing/inspiration/search.ts
35040
+ import { mkdir as mkdir12, writeFile as writeFile16 } from "fs/promises";
35041
+ import path32 from "path";
35042
+ import { defineCommand as defineCommand148 } from "citty";
35043
+ registerSchema({
35044
+ command: "landing.inspiration.search",
35045
+ description: "Search the shared library of real landing-page sections for reference before you design. Start here: baker landing inspiration search 'pricing with a monthly/annual toggle'. Returns the idea behind each section plus a downloaded screenshot you can Read. Defaults to this company's saved sections; pass --scope all for the whole library.",
35046
+ args: {
35047
+ query: { type: "string", description: "What you want to see, in plain English", required: false },
35048
+ type: {
35049
+ type: "string",
35050
+ description: "Comma list of section types: hero,pricing,faq,testimonials,\u2026",
35051
+ required: false
35052
+ },
35053
+ composition: {
35054
+ type: "string",
35055
+ description: "Comma list of composition patterns (references/composition.md)",
35056
+ required: false
35057
+ },
35058
+ register: {
35059
+ type: "string",
35060
+ description: "Comma list of visual registers: dev-tool-minimal,luxury-high-end,\u2026",
35061
+ required: false
35062
+ },
35063
+ interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
35064
+ motion: {
35065
+ type: "string",
35066
+ description: "Comma list of motion kinds: scroll-reveal,marquee,parallax,\u2026",
35067
+ required: false
35068
+ },
35069
+ media: { type: "string", description: "Comma list of media kinds", required: false },
35070
+ device: { type: "string", description: "Comma list of content devices", required: false },
35071
+ theme: { type: "string", description: "light | dark | mixed", required: false },
35072
+ "max-rank": {
35073
+ type: "number",
35074
+ description: "Show>Tell rank ceiling 1-7; 3 means 'rank 3 or better'",
35075
+ required: false
35076
+ },
35077
+ "min-craft": { type: "number", description: "Craft floor 0-1", required: false },
35078
+ "min-fidelity": {
35079
+ type: "number",
35080
+ description: "Only sections whose markup reproduces this well (0-1)",
35081
+ required: false
35082
+ },
35083
+ domain: { type: "string", description: "Restrict to one site", required: false },
35084
+ "similar-to": {
35085
+ type: "string",
35086
+ description: "Section id \u2014 find sections that look like this one",
35087
+ required: false
35088
+ },
35089
+ scope: { type: "string", description: "favorites (default) | all", required: false },
35090
+ limit: { type: "number", description: "Max results (default 8)", required: false },
35091
+ "no-images": { type: "boolean", description: "Skip downloading screenshots", required: false }
35092
+ }
35093
+ });
35094
+ function buildSearchBody(args) {
35095
+ const body = {};
35096
+ const setList2 = (key, value) => {
35097
+ const list = splitList(value);
35098
+ if (list) Object.assign(body, { [key]: list });
35099
+ };
35100
+ if (args.query) body.query = String(args.query);
35101
+ if (args["similar-to"]) body.similarToSectionId = String(args["similar-to"]);
35102
+ setList2("sectionType", args.type);
35103
+ setList2("composition", args.composition);
35104
+ setList2("visualRegister", args.register);
35105
+ setList2("interaction", args.interaction);
35106
+ setList2("motion", args.motion);
35107
+ setList2("mediaKind", args.media);
35108
+ setList2("contentDevice", args.device);
35109
+ if (args.theme) body.theme = String(args.theme);
35110
+ if (args.domain) body.domain = String(args.domain);
35111
+ const maxRank = parseNumber(args["max-rank"]);
35112
+ if (maxRank !== void 0) body.maxShowTellRank = maxRank;
35113
+ const minCraft = parseNumber(args["min-craft"]);
35114
+ if (minCraft !== void 0) body.minCraftScore = minCraft;
35115
+ const minFidelity = parseNumber(args["min-fidelity"]);
35116
+ if (minFidelity !== void 0) body.minFidelity = minFidelity;
35117
+ const limit = parseNumber(args.limit);
35118
+ body.limit = limit ?? 8;
35119
+ body.scope = args.scope === "all" ? "all" : "favorites";
35120
+ return body;
35121
+ }
35122
+ async function downloadShots(results) {
35123
+ const dir = path32.join(process.cwd(), ".baker", "inspiration");
35124
+ await mkdir12(dir, { recursive: true });
35125
+ const saved = /* @__PURE__ */ new Map();
35126
+ await Promise.all(
35127
+ results.map(async (result) => {
35128
+ if (!result.desktopShotUrl) return;
35129
+ try {
35130
+ const response = await fetch(result.desktopShotUrl);
35131
+ if (!response.ok) return;
35132
+ const file = path32.join(dir, `${result.id}.png`);
35133
+ await writeFile16(file, Buffer.from(await response.arrayBuffer()));
35134
+ saved.set(result.id, path32.relative(process.cwd(), file));
35135
+ } catch {
35136
+ }
35137
+ })
35138
+ );
35139
+ return saved;
35140
+ }
35141
+ var searchCommand2 = defineCommand148({
35142
+ meta: {
35143
+ name: "search",
35144
+ description: "Search real landing-page sections for reference. Example: baker landing inspiration search 'dark developer hero with a terminal' --register dev-tool-minimal --scope all"
35145
+ },
35146
+ args: {
35147
+ query: { type: "positional", description: "What you want to see, in plain English", required: false },
35148
+ type: { type: "string", description: "Comma list of section types", required: false },
35149
+ composition: { type: "string", description: "Comma list of composition patterns", required: false },
35150
+ register: { type: "string", description: "Comma list of visual registers", required: false },
35151
+ interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
35152
+ motion: { type: "string", description: "Comma list of motion kinds", required: false },
35153
+ media: { type: "string", description: "Comma list of media kinds", required: false },
35154
+ device: { type: "string", description: "Comma list of content devices", required: false },
35155
+ theme: { type: "string", description: "light | dark | mixed", required: false },
35156
+ "max-rank": { type: "string", description: "Show>Tell rank ceiling 1-7", required: false },
35157
+ "min-craft": { type: "string", description: "Craft floor 0-1", required: false },
35158
+ "min-fidelity": { type: "string", description: "Reproduction-fidelity floor 0-1", required: false },
35159
+ domain: { type: "string", description: "Restrict to one site", required: false },
35160
+ "similar-to": { type: "string", description: "Section id to find lookalikes of", required: false },
35161
+ scope: { type: "string", description: "favorites (default) | all", required: false, default: "favorites" },
35162
+ limit: { type: "string", description: "Max results (default 8)", required: false },
35163
+ "no-images": { type: "boolean", description: "Skip downloading screenshots", required: false, default: false },
35164
+ full: { type: "boolean", description: "Include every classification facet", required: false, default: false }
35165
+ },
35166
+ run: async ({ args }) => {
35167
+ try {
35168
+ const body = buildSearchBody(args);
35169
+ const data = await apiPost("/api/landing-inspiration/search", body);
35170
+ const results = Array.isArray(data?.results) ? data.results : [];
35171
+ const shots = args["no-images"] ? /* @__PURE__ */ new Map() : await downloadShots(results);
35172
+ const full = args.full;
35173
+ const rows = results.map((result) => ({
35174
+ id: result.id,
35175
+ section: result.sectionType,
35176
+ composition: result.composition,
35177
+ look: result.visualRegister,
35178
+ motion: result.motionSummary,
35179
+ why_it_works: result.whyItWorks,
35180
+ craft: Number(result.craftScore?.toFixed?.(2) ?? result.craftScore),
35181
+ fidelity: result.fidelity,
35182
+ domain: result.domain,
35183
+ // Only worth saying when it means something: >1 marks a section the site
35184
+ // reuses across its pages, which is a stronger signal than a one-off.
35185
+ ...result.pageCount > 1 ? { used_on_pages: result.pageCount } : {},
35186
+ screenshot: shots.get(result.id) ?? null,
35187
+ ...full ? {
35188
+ theme: result.theme,
35189
+ density: result.density,
35190
+ show_tell_rank: result.showTellRank,
35191
+ interactions: result.interactions,
35192
+ media: result.mediaKinds,
35193
+ devices: result.contentDevices,
35194
+ tags: result.tags,
35195
+ headline: result.headline,
35196
+ source_url: result.sourceUrl
35197
+ } : {}
35198
+ }));
35199
+ const hints = [INSPIRATION_HINTS.adapt];
35200
+ if (rows.length === 0) {
35201
+ hints.push(
35202
+ body.scope === "favorites" ? "No matches in this company's saved sections. Retry with --scope all to search the whole library, or add a page with `baker landing inspiration add <url>`." : "No matches. Loosen the filters, or add reference pages with `baker landing inspiration add <url>`."
35203
+ );
35204
+ } else if (data.moreInFullLibrary > 0) {
35205
+ hints.push(`${data.moreInFullLibrary} more match in the full library \u2014 re-run with --scope all.`);
35206
+ }
35207
+ if (shots.size > 0) hints.push(`Screenshots saved to .baker/inspiration/ \u2014 Read them before deciding.`);
35208
+ writeJson({
35209
+ ok: true,
35210
+ data: { results: rows, scope: data.scope, more_in_full_library: data.moreInFullLibrary, total: data.total },
35211
+ hints
35212
+ });
35213
+ } catch (error) {
35214
+ reportError(error);
35215
+ }
35216
+ }
35217
+ });
35218
+
35219
+ // src/commands/landing/inspiration/view.ts
35220
+ import { mkdir as mkdir13, writeFile as writeFile17 } from "fs/promises";
35221
+ import path33 from "path";
35222
+ import { defineCommand as defineCommand149 } from "citty";
35223
+ registerSchema({
35224
+ command: "landing.inspiration.view",
35225
+ description: "Everything known about one section: composition, motion, design tokens, the copy it uses, why it works, and what must change to make it yours. Downloads the desktop and mobile screenshots plus the motion filmstrip so you can look at them.",
35226
+ args: { id: { type: "string", description: "Section id from search", required: true } }
35227
+ });
35228
+ async function download(url, file) {
35229
+ if (!url) return null;
35230
+ try {
35231
+ const response = await fetch(url);
35232
+ if (!response.ok) return null;
35233
+ await mkdir13(path33.dirname(file), { recursive: true });
35234
+ await writeFile17(file, Buffer.from(await response.arrayBuffer()));
35235
+ return path33.relative(process.cwd(), file);
35236
+ } catch {
35237
+ return null;
35238
+ }
35239
+ }
35240
+ var viewCommand2 = defineCommand149({
35241
+ meta: {
35242
+ name: "view",
35243
+ description: "Full detail for one reference section. Example: baker landing inspiration view k57abc\u2026 \u2014 read the screenshots it saves before you build."
35244
+ },
35245
+ args: { id: { type: "positional", description: "Section id from search", required: true } },
35246
+ run: async ({ args }) => {
35247
+ try {
35248
+ const id = args.id;
35249
+ const data = await apiGet("/api/landing-inspiration/section", { id });
35250
+ const section = data.section;
35251
+ const dir = path33.join(process.cwd(), ".baker", "inspiration", id);
35252
+ const [desktop, mobile, filmstrip] = await Promise.all([
35253
+ download(section.desktopShotUrl, path33.join(dir, "desktop.png")),
35254
+ download(section.mobileShotUrl, path33.join(dir, "mobile.png")),
35255
+ download(section.motionFilmstripUrl, path33.join(dir, "motion-filmstrip.png"))
35256
+ ]);
35257
+ const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
35258
+ const fidelity = fidelityHint(section.fidelity);
35259
+ if (fidelity) hints.push(fidelity);
35260
+ if (filmstrip) {
35261
+ hints.push(
35262
+ "motion-filmstrip.png shows this section animating in, six frames left-to-right then top-to-bottom, with the elapsed time on each. Read it if you want the motion, not just the layout."
35263
+ );
35264
+ }
35265
+ writeJson({
35266
+ ok: true,
35267
+ data: {
35268
+ id: section.id,
35269
+ section: section.sectionType,
35270
+ composition: section.composition,
35271
+ look: section.visualRegister,
35272
+ theme: section.theme,
35273
+ density: section.density,
35274
+ show_tell_rank: section.showTellRank,
35275
+ interactions: section.interactions,
35276
+ media: section.mediaKinds,
35277
+ devices: section.contentDevices,
35278
+ tags: section.tags,
35279
+ motion: section.motionSummary,
35280
+ design_tokens: section.designTokens,
35281
+ size: section.boundingBox,
35282
+ copy: {
35283
+ headline: section.headline,
35284
+ subhead: section.subhead,
35285
+ ctas: section.ctaLabels,
35286
+ proof: section.proofSignals
35287
+ },
35288
+ why_it_works: section.whyItWorks,
35289
+ adaptation_notes: section.adaptationNotes,
35290
+ reproduction_notes: section.reproductionNotes,
35291
+ craft: section.craftScore,
35292
+ fidelity: section.fidelity,
35293
+ source_url: section.sourceUrl,
35294
+ screenshots: { desktop, mobile, motion_filmstrip: filmstrip }
35295
+ },
35296
+ hints
35297
+ });
35298
+ } catch (error) {
35299
+ reportError(error);
35300
+ }
35301
+ }
35302
+ });
35303
+
35304
+ // src/commands/landing/inspiration/index.ts
35305
+ var inspirationCommand = defineCommand150({
35306
+ meta: {
35307
+ name: "inspiration",
35308
+ description: `Reference library of real landing-page sections \u2014 look at how good pages actually solve a problem before you design one.
35309
+
35310
+ Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
35311
+
35312
+ This is inspiration, never a clipboard. Take the mechanism \u2014 what the eye hits first, what proof arrives before the ask, how the grid is split. The words are never yours to reuse: shipping a reference's headline is a Tier 0 message-match failure and \`baker landing critique\` will block the publish.
35313
+
35314
+ Subcommands:
35315
+ baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
35316
+ baker landing inspiration view <id> \u2014 one section in full: tokens, motion filmstrip, why it works, what to change
35317
+ baker landing inspiration code <id> \u2014 its standalone HTML+CSS, for structure only
35318
+ baker landing inspiration page <id> \u2014 a whole page as a sequence: how it orders its sections
35319
+ baker landing inspiration add <url> \u2014 add a page to the library and save it to this company (returns immediately)
35320
+ baker landing inspiration favorites \u2014 what this company has saved; the default search scope
35321
+ baker landing inspiration favorite <id> \u2014 save a section
35322
+ baker landing inspiration unfavorite <id> \u2014 unsave a section
35323
+ baker landing inspiration scrape <url> \u2014 internal/ops: run the capture locally
35324
+
35325
+ Examples:
35326
+ baker landing inspiration search "pricing with a monthly/annual toggle" --max-rank 3
35327
+ baker landing inspiration search "dark developer hero with a terminal" --register dev-tool-minimal --scope all
35328
+ baker landing inspiration search "testimonial wall with faces and company logos" --motion scroll-reveal
35329
+ baker landing inspiration add https://linear.app
35330
+
35331
+ Full guide: __tooling__/docs/tools/baker/landing.md`
35332
+ },
35333
+ subCommands: {
35334
+ search: searchCommand2,
35335
+ view: viewCommand2,
35336
+ code: codeCommand,
35337
+ page: pageCommand,
35338
+ add: addCommand,
35339
+ favorites: favoritesCommand,
35340
+ favorite: favoriteCommand,
35341
+ unfavorite: unfavoriteCommand,
35342
+ scrape: scrapeCommand
35343
+ }
35344
+ });
35345
+
32958
35346
  // src/commands/landing/index.ts
32959
- var landingCommand = defineCommand143({
35347
+ var landingCommand = defineCommand151({
32960
35348
  meta: {
32961
35349
  name: "landing",
32962
35350
  description: `Design-quality tools for landing pages (src/pages/<slug>/).
@@ -32964,15 +35352,17 @@ var landingCommand = defineCommand143({
32964
35352
  Start here: \`baker landing critique <slug>\` after building or editing a landing.
32965
35353
 
32966
35354
  Subcommands:
35355
+ baker landing inspiration \u2014 reference library of real landing-page sections; search it during research, BEFORE writing the Direction Contract. Inspiration only: take the mechanism, never the words.
32967
35356
  baker landing critique <slug> \u2014 deterministic design-quality critic (advisory): flags the known AI 'slop' tells (gradient text, overused fonts, side-tab borders, cream palettes, buzzword copy, broken images) tiered block/warn/advisory, respecting the client's BRAND.md. Records the critique the publish quality gate requires \u2014 run it before finishing a landing.`
32968
35357
  },
32969
35358
  subCommands: {
35359
+ inspiration: inspirationCommand,
32970
35360
  critique: critiqueCommand2
32971
35361
  }
32972
35362
  });
32973
35363
 
32974
35364
  // src/commands/mcp/index.ts
32975
- import { defineCommand as defineCommand144 } from "citty";
35365
+ import { defineCommand as defineCommand152 } from "citty";
32976
35366
 
32977
35367
  // src/commands/mcp/platforms.ts
32978
35368
  function readsKey(label) {
@@ -33041,7 +35431,7 @@ registerSchema({
33041
35431
  description: "List everything this chat can reach: managed integrations (Attio, Slack, Gmail, Google Sheets, \u2026), custom MCP servers, and the platforms the company signed in to (HubSpot, Google Ads, GA4, Search Console, Tag Manager) which you read through their own `baker` commands. Start here when the user mentions an external tool or platform.",
33042
35432
  args: {}
33043
35433
  });
33044
- var connectedCommand = defineCommand144({
35434
+ var connectedCommand = defineCommand152({
33045
35435
  meta: {
33046
35436
  name: "connected",
33047
35437
  description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
@@ -33100,7 +35490,7 @@ registerSchema({
33100
35490
  description: "List the custom MCP servers this company's chats see (org + company + your own user scope).",
33101
35491
  args: {}
33102
35492
  });
33103
- var listCommand13 = defineCommand144({
35493
+ var listCommand13 = defineCommand152({
33104
35494
  meta: { name: "list", description: "List custom MCP servers visible to this company's chats." },
33105
35495
  run: async () => {
33106
35496
  try {
@@ -33137,7 +35527,7 @@ registerSchema({
33137
35527
  header: { type: "string", description: 'Auth header "Key: Value" (repeatable)', required: false }
33138
35528
  }
33139
35529
  });
33140
- var addCommand = defineCommand144({
35530
+ var addCommand2 = defineCommand152({
33141
35531
  meta: {
33142
35532
  name: "add",
33143
35533
  description: `Register a custom MCP server. Tools appear as mcp__<name>__* on the NEXT message.
@@ -33189,7 +35579,7 @@ registerSchema({
33189
35579
  description: "Remove a company custom MCP server by name.",
33190
35580
  args: { name: { type: "string", description: "Server name to remove", required: true } }
33191
35581
  });
33192
- var removeCommand4 = defineCommand144({
35582
+ var removeCommand4 = defineCommand152({
33193
35583
  meta: {
33194
35584
  name: "remove",
33195
35585
  description: `Remove a company custom MCP server by name.
@@ -33211,7 +35601,7 @@ Example:
33211
35601
  }
33212
35602
  }
33213
35603
  });
33214
- var mcpCommand = defineCommand144({
35604
+ var mcpCommand = defineCommand152({
33215
35605
  meta: {
33216
35606
  name: "mcp",
33217
35607
  description: `Third-party tools for this company \u2014 see what's connected, register custom HTTPS MCP endpoints.
@@ -33231,16 +35621,16 @@ Full guide: __tooling__/docs/tools/baker/mcp.md`
33231
35621
  subCommands: {
33232
35622
  connected: connectedCommand,
33233
35623
  list: listCommand13,
33234
- add: addCommand,
35624
+ add: addCommand2,
33235
35625
  remove: removeCommand4
33236
35626
  }
33237
35627
  });
33238
35628
 
33239
35629
  // src/commands/research/index.ts
33240
- import { defineCommand as defineCommand155 } from "citty";
35630
+ import { defineCommand as defineCommand163 } from "citty";
33241
35631
 
33242
35632
  // src/commands/research/advertisers.ts
33243
- import { defineCommand as defineCommand145 } from "citty";
35633
+ import { defineCommand as defineCommand153 } from "citty";
33244
35634
 
33245
35635
  // src/commands/research/output.ts
33246
35636
  var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
@@ -33353,7 +35743,7 @@ var FIELDS3 = {
33353
35743
  etv: "Estimated traffic value (USD)",
33354
35744
  visibility: "SERP visibility score (0-1)"
33355
35745
  };
33356
- var advertisersCommand = defineCommand145({
35746
+ var advertisersCommand = defineCommand153({
33357
35747
  meta: {
33358
35748
  name: "advertisers",
33359
35749
  description: `Find domains competing for a keyword in Google SERPs.
@@ -33373,15 +35763,15 @@ Examples:
33373
35763
  },
33374
35764
  run: async ({ args }) => {
33375
35765
  const keyword = args.keyword;
33376
- const location = args.location || void 0;
35766
+ const location2 = args.location || void 0;
33377
35767
  const language = args.language || void 0;
33378
35768
  const limit = args.limit ? Number(args.limit) : void 0;
33379
35769
  const skipCache = args["no-cache"] ? true : void 0;
33380
- const queryContext = buildResearchQueryContext(location, language);
35770
+ const queryContext = buildResearchQueryContext(location2, language);
33381
35771
  try {
33382
35772
  const data = await apiPost("/api/research/advertisers", {
33383
35773
  keyword,
33384
- location,
35774
+ location: location2,
33385
35775
  language,
33386
35776
  limit,
33387
35777
  skipCache
@@ -33400,7 +35790,7 @@ Examples:
33400
35790
  });
33401
35791
 
33402
35792
  // src/commands/research/autocomplete.ts
33403
- import { defineCommand as defineCommand146 } from "citty";
35793
+ import { defineCommand as defineCommand154 } from "citty";
33404
35794
  registerSchema({
33405
35795
  command: "research.autocomplete",
33406
35796
  description: "Get Google Autocomplete suggestions for a seed keyword. Useful for keyword expansion and discovering what people actually search for. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
@@ -33423,7 +35813,7 @@ registerSchema({
33423
35813
  var FIELDS4 = {
33424
35814
  suggestion: "Autocomplete suggestion from Google"
33425
35815
  };
33426
- var autocompleteCommand = defineCommand146({
35816
+ var autocompleteCommand = defineCommand154({
33427
35817
  meta: {
33428
35818
  name: "autocomplete",
33429
35819
  description: `Get Google Autocomplete suggestions for keyword expansion.
@@ -33442,15 +35832,15 @@ Examples:
33442
35832
  },
33443
35833
  run: async ({ args }) => {
33444
35834
  const keyword = args.keyword;
33445
- const location = args.location || void 0;
35835
+ const location2 = args.location || void 0;
33446
35836
  const language = args.language || void 0;
33447
35837
  const limit = args.limit ? Number(args.limit) : void 0;
33448
35838
  const skipCache = args["no-cache"] ? true : void 0;
33449
- const queryContext = buildResearchQueryContext(location, language);
35839
+ const queryContext = buildResearchQueryContext(location2, language);
33450
35840
  try {
33451
35841
  const data = await apiPost("/api/research/autocomplete", {
33452
35842
  keyword,
33453
- location,
35843
+ location: location2,
33454
35844
  language,
33455
35845
  limit,
33456
35846
  skipCache
@@ -33469,7 +35859,7 @@ Examples:
33469
35859
  });
33470
35860
 
33471
35861
  // src/commands/research/countries.ts
33472
- import { defineCommand as defineCommand147 } from "citty";
35862
+ import { defineCommand as defineCommand155 } from "citty";
33473
35863
  registerSchema({
33474
35864
  command: "research.countries",
33475
35865
  description: "List all supported country codes for --location flag in research commands.",
@@ -33526,7 +35916,7 @@ var FIELDS5 = {
33526
35916
  code: "Country code to pass as --location",
33527
35917
  name: "Country name"
33528
35918
  };
33529
- var countriesCommand = defineCommand147({
35919
+ var countriesCommand = defineCommand155({
33530
35920
  meta: {
33531
35921
  name: "countries",
33532
35922
  description: "List all supported country codes for --location flag."
@@ -33537,7 +35927,7 @@ var countriesCommand = defineCommand147({
33537
35927
  });
33538
35928
 
33539
35929
  // src/commands/research/intent.ts
33540
- import { defineCommand as defineCommand148 } from "citty";
35930
+ import { defineCommand as defineCommand156 } from "citty";
33541
35931
  registerSchema({
33542
35932
  command: "research.intent",
33543
35933
  description: "Classify Google Search intent for keywords. Determines if someone searching is looking to buy, research, or navigate. IMPORTANT: If --language is omitted, defaults to English (en). The response includes a query_context object showing which language was used.",
@@ -33560,7 +35950,7 @@ var FIELDS6 = {
33560
35950
  intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
33561
35951
  probability: "Confidence score 0.0-1.0"
33562
35952
  };
33563
- var intentCommand = defineCommand148({
35953
+ var intentCommand = defineCommand156({
33564
35954
  meta: {
33565
35955
  name: "intent",
33566
35956
  description: `Classify Google Search intent for keywords. Returns intent type and confidence.
@@ -33608,7 +35998,7 @@ Examples:
33608
35998
  });
33609
35999
 
33610
36000
  // src/commands/research/keyword-gap.ts
33611
- import { defineCommand as defineCommand149 } from "citty";
36001
+ import { defineCommand as defineCommand157 } from "citty";
33612
36002
  registerSchema({
33613
36003
  command: "research.keyword-gap",
33614
36004
  description: "Find keywords a competitor ranks for (organic or paid) that you don't. Discovers expansion opportunities. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
@@ -33637,7 +36027,7 @@ var FIELDS7 = {
33637
36027
  cpc: "Cost per click USD",
33638
36028
  their_position: "Competitor's ranking position"
33639
36029
  };
33640
- var keywordGapCommand = defineCommand149({
36030
+ var keywordGapCommand = defineCommand157({
33641
36031
  meta: {
33642
36032
  name: "keyword-gap",
33643
36033
  description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
@@ -33661,18 +36051,18 @@ Examples:
33661
36051
  run: async ({ args }) => {
33662
36052
  const competitor = args.competitor;
33663
36053
  const ours = args.ours;
33664
- const location = args.location || void 0;
36054
+ const location2 = args.location || void 0;
33665
36055
  const language = args.language || void 0;
33666
36056
  const type = args.type || void 0;
33667
36057
  const limit = args.limit ? Number(args.limit) : void 0;
33668
36058
  const offset = args.offset ? Number(args.offset) : void 0;
33669
36059
  const skipCache = args["no-cache"] ? true : void 0;
33670
- const queryContext = buildResearchQueryContext(location, language);
36060
+ const queryContext = buildResearchQueryContext(location2, language);
33671
36061
  try {
33672
36062
  const result = await apiPost("/api/research/keyword-gap", {
33673
36063
  competitor,
33674
36064
  ours,
33675
- location,
36065
+ location: location2,
33676
36066
  language,
33677
36067
  type,
33678
36068
  limit,
@@ -33711,7 +36101,7 @@ Examples:
33711
36101
  });
33712
36102
 
33713
36103
  // src/commands/research/keywords-for-site.ts
33714
- import { defineCommand as defineCommand150 } from "citty";
36104
+ import { defineCommand as defineCommand158 } from "citty";
33715
36105
  registerSchema({
33716
36106
  command: "research.keywords-for-site",
33717
36107
  description: "Get keywords a competitor targets in Google. Use --type paid to see only paid keywords, --type organic for organic only. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en). The response includes a query_context object showing which location/language were used.",
@@ -33744,7 +36134,7 @@ var FIELDS8 = {
33744
36134
  competition: "LOW, MEDIUM, or HIGH",
33745
36135
  competition_index: "Competition score 0-100"
33746
36136
  };
33747
- var keywordsForSiteCommand = defineCommand150({
36137
+ var keywordsForSiteCommand = defineCommand158({
33748
36138
  meta: {
33749
36139
  name: "keywords-for-site",
33750
36140
  description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
@@ -33766,17 +36156,17 @@ Examples:
33766
36156
  },
33767
36157
  run: async ({ args }) => {
33768
36158
  const target = args.target;
33769
- const location = args.location || void 0;
36159
+ const location2 = args.location || void 0;
33770
36160
  const language = args.language || void 0;
33771
36161
  const sort = args.sort || void 0;
33772
36162
  const type = args.type || void 0;
33773
36163
  const limit = args.limit ? Number(args.limit) : void 0;
33774
36164
  const skipCache = args["no-cache"] ? true : void 0;
33775
- const queryContext = buildResearchQueryContext(location, language);
36165
+ const queryContext = buildResearchQueryContext(location2, language);
33776
36166
  try {
33777
36167
  const data = await apiPost("/api/research/keywords-for-site", {
33778
36168
  target,
33779
- location,
36169
+ location: location2,
33780
36170
  language,
33781
36171
  sort,
33782
36172
  type,
@@ -33797,7 +36187,7 @@ Examples:
33797
36187
  });
33798
36188
 
33799
36189
  // src/commands/research/languages.ts
33800
- import { defineCommand as defineCommand151 } from "citty";
36190
+ import { defineCommand as defineCommand159 } from "citty";
33801
36191
  registerSchema({
33802
36192
  command: "research.languages",
33803
36193
  description: "List all supported language codes for --language flag in research commands.",
@@ -33827,7 +36217,7 @@ var FIELDS9 = {
33827
36217
  code: "Language code to pass as --language",
33828
36218
  name: "Language name (also accepted by --language)"
33829
36219
  };
33830
- var languagesCommand2 = defineCommand151({
36220
+ var languagesCommand2 = defineCommand159({
33831
36221
  meta: {
33832
36222
  name: "languages",
33833
36223
  description: "List all supported language codes for --language flag."
@@ -33838,7 +36228,7 @@ var languagesCommand2 = defineCommand151({
33838
36228
  });
33839
36229
 
33840
36230
  // src/commands/research/lighthouse.ts
33841
- import { defineCommand as defineCommand152 } from "citty";
36231
+ import { defineCommand as defineCommand160 } from "citty";
33842
36232
  registerSchema({
33843
36233
  command: "research.lighthouse",
33844
36234
  description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
@@ -33857,7 +36247,7 @@ var FIELDS10 = {
33857
36247
  speed_index_ms: "Speed Index in ms (good: < 3400)",
33858
36248
  interactive_ms: "Time to Interactive in ms (good: < 3800)"
33859
36249
  };
33860
- var lighthouseCommand = defineCommand152({
36250
+ var lighthouseCommand = defineCommand160({
33861
36251
  meta: {
33862
36252
  name: "lighthouse",
33863
36253
  description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
@@ -33895,7 +36285,7 @@ Examples:
33895
36285
  });
33896
36286
 
33897
36287
  // src/commands/research/relevant-pages.ts
33898
- import { defineCommand as defineCommand153 } from "citty";
36288
+ import { defineCommand as defineCommand161 } from "citty";
33899
36289
  registerSchema({
33900
36290
  command: "research.relevant-pages",
33901
36291
  description: "Get the top pages of a competitor domain with organic traffic and ranking data. Shows which pages drive the most traffic. IMPORTANT: If --location and --language are omitted, defaults to United States (us) and English (en).",
@@ -33921,7 +36311,7 @@ var FIELDS11 = {
33921
36311
  keywords: "Total organic keywords the page ranks for",
33922
36312
  top_10: "Keywords in positions 1-10"
33923
36313
  };
33924
- var relevantPagesCommand = defineCommand153({
36314
+ var relevantPagesCommand = defineCommand161({
33925
36315
  meta: {
33926
36316
  name: "relevant-pages",
33927
36317
  description: `Get the top pages of a competitor domain with traffic data.
@@ -33940,15 +36330,15 @@ Examples:
33940
36330
  },
33941
36331
  run: async ({ args }) => {
33942
36332
  const target = args.target;
33943
- const location = args.location || void 0;
36333
+ const location2 = args.location || void 0;
33944
36334
  const language = args.language || void 0;
33945
36335
  const limit = args.limit ? Number(args.limit) : void 0;
33946
36336
  const skipCache = args["no-cache"] ? true : void 0;
33947
- const queryContext = buildResearchQueryContext(location, language);
36337
+ const queryContext = buildResearchQueryContext(location2, language);
33948
36338
  try {
33949
36339
  const data = await apiPost("/api/research/relevant-pages", {
33950
36340
  target,
33951
- location,
36341
+ location: location2,
33952
36342
  language,
33953
36343
  limit,
33954
36344
  skipCache
@@ -33967,7 +36357,7 @@ Examples:
33967
36357
  });
33968
36358
 
33969
36359
  // src/commands/research/web.ts
33970
- import { defineCommand as defineCommand154 } from "citty";
36360
+ import { defineCommand as defineCommand162 } from "citty";
33971
36361
  registerSchema({
33972
36362
  command: "research.web",
33973
36363
  description: "Search the web with AI to answer marketing questions \u2014 competitors, ICP, pricing, pain points, market trends. Three depth levels: medium (quick, default), high (thorough), xhigh (exhaustive deep research).",
@@ -34018,7 +36408,7 @@ async function runDeepResearch(question) {
34018
36408
  }
34019
36409
  throw new Error("Deep research timed out");
34020
36410
  }
34021
- var webCommand = defineCommand154({
36411
+ var webCommand = defineCommand162({
34022
36412
  meta: {
34023
36413
  name: "web",
34024
36414
  description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
@@ -34078,7 +36468,7 @@ Examples:
34078
36468
  });
34079
36469
 
34080
36470
  // src/commands/research/index.ts
34081
- var researchCommand = defineCommand155({
36471
+ var researchCommand = defineCommand163({
34082
36472
  meta: {
34083
36473
  name: "research",
34084
36474
  description: `Competitive intelligence and AI-powered research commands.
@@ -34119,10 +36509,10 @@ Full guide: __tooling__/docs/tools/baker/research.md`
34119
36509
  });
34120
36510
 
34121
36511
  // src/commands/scheduled-actions/index.ts
34122
- import { defineCommand as defineCommand162 } from "citty";
36512
+ import { defineCommand as defineCommand170 } from "citty";
34123
36513
 
34124
36514
  // src/commands/scheduled-actions/create.ts
34125
- import { defineCommand as defineCommand156 } from "citty";
36515
+ import { defineCommand as defineCommand164 } from "citty";
34126
36516
 
34127
36517
  // src/commands/scheduled-actions/shared.ts
34128
36518
  var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
@@ -34237,7 +36627,7 @@ registerSchema({
34237
36627
  prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
34238
36628
  }
34239
36629
  });
34240
- var createCommand2 = defineCommand156({
36630
+ var createCommand2 = defineCommand164({
34241
36631
  meta: {
34242
36632
  name: "create",
34243
36633
  description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
@@ -34286,7 +36676,7 @@ var createCommand2 = defineCommand156({
34286
36676
  });
34287
36677
 
34288
36678
  // src/commands/scheduled-actions/delete.ts
34289
- import { defineCommand as defineCommand157 } from "citty";
36679
+ import { defineCommand as defineCommand165 } from "citty";
34290
36680
  registerSchema({
34291
36681
  command: "scheduled-actions.delete",
34292
36682
  description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
@@ -34294,7 +36684,7 @@ registerSchema({
34294
36684
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
34295
36685
  }
34296
36686
  });
34297
- var deleteCommand2 = defineCommand157({
36687
+ var deleteCommand2 = defineCommand165({
34298
36688
  meta: {
34299
36689
  name: "delete",
34300
36690
  description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
@@ -34323,7 +36713,7 @@ var deleteCommand2 = defineCommand157({
34323
36713
  });
34324
36714
 
34325
36715
  // src/commands/scheduled-actions/get.ts
34326
- import { defineCommand as defineCommand158 } from "citty";
36716
+ import { defineCommand as defineCommand166 } from "citty";
34327
36717
  registerSchema({
34328
36718
  command: "scheduled-actions.get",
34329
36719
  description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
@@ -34332,7 +36722,7 @@ registerSchema({
34332
36722
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
34333
36723
  }
34334
36724
  });
34335
- var getCommand3 = defineCommand158({
36725
+ var getCommand3 = defineCommand166({
34336
36726
  meta: {
34337
36727
  name: "get",
34338
36728
  description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
@@ -34371,7 +36761,7 @@ var getCommand3 = defineCommand158({
34371
36761
  });
34372
36762
 
34373
36763
  // src/commands/scheduled-actions/list.ts
34374
- import { defineCommand as defineCommand159 } from "citty";
36764
+ import { defineCommand as defineCommand167 } from "citty";
34375
36765
  registerSchema({
34376
36766
  command: "scheduled-actions.list",
34377
36767
  description: "List published scheduled actions. Includes draft state when BAKER_CHAT_ID is set, or --chat <id> to read an earlier chat's staged schedules instead.",
@@ -34379,7 +36769,7 @@ registerSchema({
34379
36769
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
34380
36770
  }
34381
36771
  });
34382
- var listCommand14 = defineCommand159({
36772
+ var listCommand14 = defineCommand167({
34383
36773
  meta: {
34384
36774
  name: "list",
34385
36775
  description: "List scheduled actions. Includes staged draft ops when BAKER_CHAT_ID is set, or --chat <id> to read an earlier chat's staged schedules instead."
@@ -34402,7 +36792,7 @@ var listCommand14 = defineCommand159({
34402
36792
  });
34403
36793
 
34404
36794
  // src/commands/scheduled-actions/trigger.ts
34405
- import { defineCommand as defineCommand160 } from "citty";
36795
+ import { defineCommand as defineCommand168 } from "citty";
34406
36796
  registerSchema({
34407
36797
  command: "scheduled-actions.trigger",
34408
36798
  description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
@@ -34410,7 +36800,7 @@ registerSchema({
34410
36800
  id: { type: "string", description: "Published scheduled action ID", required: true }
34411
36801
  }
34412
36802
  });
34413
- var triggerCommand = defineCommand160({
36803
+ var triggerCommand = defineCommand168({
34414
36804
  meta: {
34415
36805
  name: "trigger",
34416
36806
  description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
@@ -34447,7 +36837,7 @@ var triggerCommand = defineCommand160({
34447
36837
  });
34448
36838
 
34449
36839
  // src/commands/scheduled-actions/update.ts
34450
- import { defineCommand as defineCommand161 } from "citty";
36840
+ import { defineCommand as defineCommand169 } from "citty";
34451
36841
  registerSchema({
34452
36842
  command: "scheduled-actions.update",
34453
36843
  description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
@@ -34472,7 +36862,7 @@ registerSchema({
34472
36862
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
34473
36863
  }
34474
36864
  });
34475
- var updateCommand2 = defineCommand161({
36865
+ var updateCommand2 = defineCommand169({
34476
36866
  meta: {
34477
36867
  name: "update",
34478
36868
  description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
@@ -34543,7 +36933,7 @@ var updateCommand2 = defineCommand161({
34543
36933
  });
34544
36934
 
34545
36935
  // src/commands/scheduled-actions/index.ts
34546
- var scheduledActionsCommand = defineCommand162({
36936
+ var scheduledActionsCommand = defineCommand170({
34547
36937
  meta: {
34548
36938
  name: "scheduled-actions",
34549
36939
  description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
@@ -34570,14 +36960,14 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
34570
36960
  });
34571
36961
 
34572
36962
  // src/commands/schema.ts
34573
- import { defineCommand as defineCommand163 } from "citty";
36963
+ import { defineCommand as defineCommand171 } from "citty";
34574
36964
  function narrowToFamily(commandName, available) {
34575
36965
  const segments = commandName.split(".");
34576
36966
  const prefix = segments[0] === "ads" && segments[1] ? `ads.${segments[1]}.` : `${segments[0]}.`;
34577
36967
  const siblings = available.filter((name) => name.startsWith(prefix));
34578
36968
  return siblings.length > 0 ? siblings : available;
34579
36969
  }
34580
- var schemaCommand = defineCommand163({
36970
+ var schemaCommand = defineCommand171({
34581
36971
  meta: {
34582
36972
  name: "schema",
34583
36973
  description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
@@ -34621,10 +37011,10 @@ var schemaCommand = defineCommand163({
34621
37011
  });
34622
37012
 
34623
37013
  // src/commands/tag-manager/index.ts
34624
- import { defineCommand as defineCommand167 } from "citty";
37014
+ import { defineCommand as defineCommand175 } from "citty";
34625
37015
 
34626
37016
  // src/commands/tag-manager/draft.ts
34627
- import { defineCommand as defineCommand164 } from "citty";
37017
+ import { defineCommand as defineCommand172 } from "citty";
34628
37018
 
34629
37019
  // src/commands/tag-manager/shared.ts
34630
37020
  import { readFileSync as readFileSync13 } from "fs";
@@ -34691,10 +37081,10 @@ async function stageOp4(op) {
34691
37081
  handleError4(err);
34692
37082
  }
34693
37083
  }
34694
- async function draftAction3(path28, body, chat) {
37084
+ async function draftAction3(path34, body, chat) {
34695
37085
  const chatId = resolveChatId(chat);
34696
37086
  try {
34697
- const data = await apiPost(path28, { chatId, ...body });
37087
+ const data = await apiPost(path34, { chatId, ...body });
34698
37088
  writeJsonEnvelope({ ok: true, data });
34699
37089
  return data;
34700
37090
  } catch (err) {
@@ -34747,13 +37137,13 @@ registerSchema({
34747
37137
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
34748
37138
  }
34749
37139
  });
34750
- var draftCommand4 = defineCommand164({
37140
+ var draftCommand4 = defineCommand172({
34751
37141
  meta: {
34752
37142
  name: "draft",
34753
37143
  description: "List, show, amend, remove, or clear staged Tag Manager changes for this chat. `list` and `show` take --chat <id> to read an earlier chat's changes instead."
34754
37144
  },
34755
37145
  subCommands: {
34756
- list: defineCommand164({
37146
+ list: defineCommand172({
34757
37147
  meta: {
34758
37148
  name: "list",
34759
37149
  description: "Review everything staged on this chat (--json for the raw envelope)"
@@ -34766,7 +37156,7 @@ var draftCommand4 = defineCommand164({
34766
37156
  await draftList2(args.json === true, args.chat);
34767
37157
  }
34768
37158
  }),
34769
- show: defineCommand164({
37159
+ show: defineCommand172({
34770
37160
  meta: {
34771
37161
  name: "show",
34772
37162
  description: "Print the full staged payload for one change \u2014 the receipt to verify it looks right before publish (never truncated)."
@@ -34783,7 +37173,7 @@ var draftCommand4 = defineCommand164({
34783
37173
  );
34784
37174
  }
34785
37175
  }),
34786
- amend: defineCommand164({
37176
+ amend: defineCommand172({
34787
37177
  meta: {
34788
37178
  name: "amend",
34789
37179
  description: "Update a staged change in place \u2014 merges a JSON patch into its payload (objects deep-merge, null deletes a key, arrays/scalars replace) and re-validates. Use this instead of remove + re-create."
@@ -34800,7 +37190,7 @@ var draftCommand4 = defineCommand164({
34800
37190
  });
34801
37191
  }
34802
37192
  }),
34803
- remove: defineCommand164({
37193
+ remove: defineCommand172({
34804
37194
  meta: { name: "remove", description: "Remove one staged change (cascades to anything depending on it)" },
34805
37195
  args: { ref: { type: "positional", description: "Staged ref (gtm_temp_*) or target", required: false } },
34806
37196
  run: async ({ args }) => {
@@ -34809,7 +37199,7 @@ var draftCommand4 = defineCommand164({
34809
37199
  });
34810
37200
  }
34811
37201
  }),
34812
- clear: defineCommand164({
37202
+ clear: defineCommand172({
34813
37203
  meta: { name: "clear", description: "Discard all Tag Manager changes staged on this chat" },
34814
37204
  run: async () => {
34815
37205
  await draftAction3("/api/tag-manager/draft/clear", {});
@@ -34819,7 +37209,7 @@ var draftCommand4 = defineCommand164({
34819
37209
  });
34820
37210
 
34821
37211
  // src/commands/tag-manager/read.ts
34822
- import { defineCommand as defineCommand165 } from "citty";
37212
+ import { defineCommand as defineCommand173 } from "citty";
34823
37213
  registerSchema({
34824
37214
  command: "tagManager.containers",
34825
37215
  description: "List the Google Tag Manager containers this company's connection can reach. Every container the company connected is flagged `connected: true` \u2014 there can be several, and Baker may read and change all of them. Start here to confirm which containers you are managing.",
@@ -34860,7 +37250,7 @@ function containersHints(containers) {
34860
37250
  }))
34861
37251
  });
34862
37252
  }
34863
- var containersCommand = defineCommand165({
37253
+ var containersCommand = defineCommand173({
34864
37254
  meta: {
34865
37255
  name: "containers",
34866
37256
  description: `List Tag Manager containers reachable by this company's connection.
@@ -34877,7 +37267,7 @@ Start here:
34877
37267
  }
34878
37268
  }
34879
37269
  });
34880
- var readCommand = defineCommand165({
37270
+ var readCommand = defineCommand173({
34881
37271
  meta: {
34882
37272
  name: "read",
34883
37273
  description: `Read the current contents of the Tag Manager container \u2014 always do this before staging changes.
@@ -34919,7 +37309,7 @@ Examples:
34919
37309
  });
34920
37310
 
34921
37311
  // src/commands/tag-manager/write-commands.ts
34922
- import { defineCommand as defineCommand166 } from "citty";
37312
+ import { defineCommand as defineCommand174 } from "citty";
34923
37313
  var CONTAINER_ARG_DESCRIPTION = "Numeric container id (optional only when one container is connected \u2014 run `baker tag-manager containers`)";
34924
37314
  var ENTITIES = [
34925
37315
  {
@@ -34975,10 +37365,10 @@ for (const { entity, noun, createHint } of ENTITIES) {
34975
37365
  });
34976
37366
  }
34977
37367
  function entityCommand(entity, noun, example) {
34978
- return defineCommand166({
37368
+ return defineCommand174({
34979
37369
  meta: { name: entity, description: `Stage ${noun} changes on this chat's Tag Manager draft` },
34980
37370
  subCommands: {
34981
- create: defineCommand166({
37371
+ create: defineCommand174({
34982
37372
  meta: {
34983
37373
  name: "create",
34984
37374
  description: `Stage a new ${noun}
@@ -35000,7 +37390,7 @@ Examples:
35000
37390
  });
35001
37391
  }
35002
37392
  }),
35003
- update: defineCommand166({
37393
+ update: defineCommand174({
35004
37394
  meta: {
35005
37395
  name: "update",
35006
37396
  description: `Stage an update to an existing ${noun} (pass its id or path)`
@@ -35020,7 +37410,7 @@ Examples:
35020
37410
  });
35021
37411
  }
35022
37412
  }),
35023
- delete: defineCommand166({
37413
+ delete: defineCommand174({
35024
37414
  meta: { name: "delete", description: `Stage the deletion of a ${noun} (pass its id or path)` },
35025
37415
  args: {
35026
37416
  id: { type: "positional", description: `${noun} id or path`, required: false },
@@ -35061,7 +37451,7 @@ function builtinTypes(args) {
35061
37451
  }
35062
37452
  return raw.split(",").map((entry) => entry.trim());
35063
37453
  }
35064
- var builtinCommand = defineCommand166({
37454
+ var builtinCommand = defineCommand174({
35065
37455
  meta: {
35066
37456
  name: "builtin",
35067
37457
  description: `Enable or disable built-in variables
@@ -35071,7 +37461,7 @@ Examples:
35071
37461
  baker tag-manager builtin disable --types formId`
35072
37462
  },
35073
37463
  subCommands: {
35074
- enable: defineCommand166({
37464
+ enable: defineCommand174({
35075
37465
  meta: { name: "enable", description: "Stage enabling built-in variables" },
35076
37466
  args: {
35077
37467
  types: { type: "string", description: "Comma-separated types", required: false },
@@ -35085,7 +37475,7 @@ Examples:
35085
37475
  });
35086
37476
  }
35087
37477
  }),
35088
- disable: defineCommand166({
37478
+ disable: defineCommand174({
35089
37479
  meta: { name: "disable", description: "Stage disabling built-in variables" },
35090
37480
  args: {
35091
37481
  types: { type: "string", description: "Comma-separated types", required: false },
@@ -35103,7 +37493,7 @@ Examples:
35103
37493
  });
35104
37494
 
35105
37495
  // src/commands/tag-manager/index.ts
35106
- var tagManagerCommand = defineCommand167({
37496
+ var tagManagerCommand = defineCommand175({
35107
37497
  meta: {
35108
37498
  name: "tag-manager",
35109
37499
  description: `Read and change what lives inside the client's Google Tag Manager container \u2014 tags, triggers, variables, folders and built-in variables.
@@ -35140,7 +37530,7 @@ Full guide: __tooling__/docs/tools/baker/tag-manager.md`
35140
37530
  });
35141
37531
 
35142
37532
  // src/commands/tags/index.ts
35143
- import { defineCommand as defineCommand168 } from "citty";
37533
+ import { defineCommand as defineCommand176 } from "citty";
35144
37534
 
35145
37535
  // src/commands/tags/shared.ts
35146
37536
  function failApi3(err) {
@@ -35209,7 +37599,7 @@ async function listTags(json) {
35209
37599
  var listArgs9 = {
35210
37600
  json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable list" }
35211
37601
  };
35212
- var listCommand15 = defineCommand168({
37602
+ var listCommand15 = defineCommand176({
35213
37603
  meta: {
35214
37604
  name: "list",
35215
37605
  description: "Effective tags for this chat (production + staged), with each tag's full readable config (secrets excluded) \u2014 reuse a stored value to pre-fill a change rather than asking the user. Refs printed here are what flow side-effect tagIds should use. Example: baker tags list"
@@ -35228,7 +37618,7 @@ async function listDraft3(chat) {
35228
37618
  failApi3(err);
35229
37619
  }
35230
37620
  }
35231
- var draftCommand5 = defineCommand168({
37621
+ var draftCommand5 = defineCommand176({
35232
37622
  meta: {
35233
37623
  name: "draft",
35234
37624
  description: "Review the tag changes staged in this chat (read-only). Staged changes were approved via request_tag_input and apply when the chat is published; to amend or drop one, propose a follow-up change through the same tool (a delete on a tag_temp_* ref drops the staged create). Takes --chat <id> to read an earlier chat's staged changes instead."
@@ -35238,7 +37628,7 @@ var draftCommand5 = defineCommand168({
35238
37628
  await listDraft3(args.chat);
35239
37629
  }
35240
37630
  });
35241
- var tagsCommand3 = defineCommand168({
37631
+ var tagsCommand3 = defineCommand176({
35242
37632
  meta: {
35243
37633
  name: "tags",
35244
37634
  description: `Read the client's marketing/analytics tags (Meta pixel, GA4, Google Ads, GTM, Clarity, Hotjar, \u2026) \u2014 production tags plus the changes staged in this chat.
@@ -35267,10 +37657,10 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
35267
37657
  });
35268
37658
 
35269
37659
  // src/commands/testimonials/index.ts
35270
- import { defineCommand as defineCommand172 } from "citty";
37660
+ import { defineCommand as defineCommand180 } from "citty";
35271
37661
 
35272
37662
  // src/commands/testimonials/get.ts
35273
- import { defineCommand as defineCommand169 } from "citty";
37663
+ import { defineCommand as defineCommand177 } from "citty";
35274
37664
  registerSchema({
35275
37665
  command: "testimonials.get",
35276
37666
  description: "Get a single testimonial by ID",
@@ -35278,7 +37668,7 @@ registerSchema({
35278
37668
  id: { type: "string", description: "Testimonial ID", required: true }
35279
37669
  }
35280
37670
  });
35281
- var getCommand4 = defineCommand169({
37671
+ var getCommand4 = defineCommand177({
35282
37672
  meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
35283
37673
  args: {
35284
37674
  id: { type: "positional", description: "Testimonial ID", required: false },
@@ -35315,7 +37705,7 @@ var getCommand4 = defineCommand169({
35315
37705
  });
35316
37706
 
35317
37707
  // src/commands/testimonials/list.ts
35318
- import { defineCommand as defineCommand170 } from "citty";
37708
+ import { defineCommand as defineCommand178 } from "citty";
35319
37709
  registerSchema({
35320
37710
  command: "testimonials.list",
35321
37711
  description: "List testimonials with optional filters.",
@@ -35345,7 +37735,7 @@ registerSchema({
35345
37735
  limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
35346
37736
  }
35347
37737
  });
35348
- var listCommand16 = defineCommand170({
37738
+ var listCommand16 = defineCommand178({
35349
37739
  meta: {
35350
37740
  name: "list",
35351
37741
  description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
@@ -35394,7 +37784,7 @@ var listCommand16 = defineCommand170({
35394
37784
  });
35395
37785
 
35396
37786
  // src/commands/testimonials/search.ts
35397
- import { defineCommand as defineCommand171 } from "citty";
37787
+ import { defineCommand as defineCommand179 } from "citty";
35398
37788
  function languageBiasHint(results, requestedLanguage) {
35399
37789
  if (requestedLanguage) {
35400
37790
  return null;
@@ -35472,7 +37862,7 @@ function buildSearchRequest(query, args) {
35472
37862
  }
35473
37863
  return body;
35474
37864
  }
35475
- var searchCommand2 = defineCommand171({
37865
+ var searchCommand3 = defineCommand179({
35476
37866
  meta: {
35477
37867
  name: "search",
35478
37868
  description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
@@ -35528,7 +37918,7 @@ var searchCommand2 = defineCommand171({
35528
37918
  var tagsCommand4 = makeTagsCommand("testimonials", "testimonial", "/api/testimonials/tags");
35529
37919
 
35530
37920
  // src/commands/testimonials/index.ts
35531
- var testimonialsCommand = defineCommand172({
37921
+ var testimonialsCommand = defineCommand180({
35532
37922
  meta: {
35533
37923
  name: "testimonials",
35534
37924
  description: `Find and browse testimonials in Baker. Subcommands: search, get, list, tags.
@@ -35543,17 +37933,17 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
35543
37933
  },
35544
37934
  subCommands: {
35545
37935
  get: getCommand4,
35546
- search: searchCommand2,
37936
+ search: searchCommand3,
35547
37937
  list: listCommand16,
35548
37938
  tags: tagsCommand4
35549
37939
  }
35550
37940
  });
35551
37941
 
35552
37942
  // src/commands/videos/index.ts
35553
- import { defineCommand as defineCommand177 } from "citty";
37943
+ import { defineCommand as defineCommand185 } from "citty";
35554
37944
 
35555
37945
  // src/commands/videos/delete.ts
35556
- import { defineCommand as defineCommand173 } from "citty";
37946
+ import { defineCommand as defineCommand181 } from "citty";
35557
37947
  registerSchema({
35558
37948
  command: "videos.delete",
35559
37949
  description: "Delete a video by ID",
@@ -35567,7 +37957,7 @@ registerSchema({
35567
37957
  }
35568
37958
  }
35569
37959
  });
35570
- var deleteCommand3 = defineCommand173({
37960
+ var deleteCommand3 = defineCommand181({
35571
37961
  meta: {
35572
37962
  name: "delete",
35573
37963
  description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
@@ -35608,7 +37998,7 @@ var deleteCommand3 = defineCommand173({
35608
37998
  });
35609
37999
 
35610
38000
  // src/commands/videos/get.ts
35611
- import { defineCommand as defineCommand174 } from "citty";
38001
+ import { defineCommand as defineCommand182 } from "citty";
35612
38002
  registerSchema({
35613
38003
  command: "videos.get",
35614
38004
  description: "Get a single video by ID",
@@ -35616,7 +38006,7 @@ registerSchema({
35616
38006
  id: { type: "string", description: "Video ID", required: true }
35617
38007
  }
35618
38008
  });
35619
- var getCommand5 = defineCommand174({
38009
+ var getCommand5 = defineCommand182({
35620
38010
  meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
35621
38011
  args: {
35622
38012
  id: { type: "positional", description: "Video ID", required: false },
@@ -35653,7 +38043,7 @@ var getCommand5 = defineCommand174({
35653
38043
  });
35654
38044
 
35655
38045
  // src/commands/videos/search.ts
35656
- import { defineCommand as defineCommand175 } from "citty";
38046
+ import { defineCommand as defineCommand183 } from "citty";
35657
38047
  registerSchema({
35658
38048
  command: "videos.search",
35659
38049
  description: "Search videos by text query. Only returns ready videos.",
@@ -35663,7 +38053,7 @@ registerSchema({
35663
38053
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
35664
38054
  }
35665
38055
  });
35666
- var searchCommand3 = defineCommand175({
38056
+ var searchCommand4 = defineCommand183({
35667
38057
  meta: {
35668
38058
  name: "search",
35669
38059
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -35713,9 +38103,9 @@ var searchCommand3 = defineCommand175({
35713
38103
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
35714
38104
 
35715
38105
  // src/commands/videos/upload.ts
35716
- import { readFile as readFile23, stat as stat7 } from "fs/promises";
38106
+ import { readFile as readFile24, stat as stat7 } from "fs/promises";
35717
38107
  import { extname as extname3 } from "path";
35718
- import { defineCommand as defineCommand176 } from "citty";
38108
+ import { defineCommand as defineCommand184 } from "citty";
35719
38109
  var MIME_MAP = {
35720
38110
  ".mp4": "video/mp4",
35721
38111
  ".mov": "video/quicktime",
@@ -35749,7 +38139,7 @@ function detectContentType(filePath) {
35749
38139
  }
35750
38140
  return mime;
35751
38141
  }
35752
- var uploadCommand2 = defineCommand176({
38142
+ var uploadCommand2 = defineCommand184({
35753
38143
  meta: {
35754
38144
  name: "upload",
35755
38145
  description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
@@ -35778,7 +38168,7 @@ var uploadCommand2 = defineCommand176({
35778
38168
  return;
35779
38169
  }
35780
38170
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
35781
- const fileBuffer = await readFile23(filePath);
38171
+ const fileBuffer = await readFile24(filePath);
35782
38172
  const uploadResponse = await fetch(uploadUrl, {
35783
38173
  method: "PUT",
35784
38174
  headers: { "Content-Type": contentType },
@@ -35803,7 +38193,7 @@ var uploadCommand2 = defineCommand176({
35803
38193
  });
35804
38194
 
35805
38195
  // src/commands/videos/index.ts
35806
- var videosCommand = defineCommand177({
38196
+ var videosCommand = defineCommand185({
35807
38197
  meta: {
35808
38198
  name: "videos",
35809
38199
  description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
@@ -35819,7 +38209,7 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
35819
38209
  },
35820
38210
  subCommands: {
35821
38211
  get: getCommand5,
35822
- search: searchCommand3,
38212
+ search: searchCommand4,
35823
38213
  upload: uploadCommand2,
35824
38214
  delete: deleteCommand3,
35825
38215
  tags: tagsCommand5
@@ -35827,19 +38217,19 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
35827
38217
  });
35828
38218
 
35829
38219
  // src/commands/winning-ads/index.ts
35830
- import { defineCommand as defineCommand190 } from "citty";
38220
+ import { defineCommand as defineCommand198 } from "citty";
35831
38221
 
35832
38222
  // src/commands/winning-ads/advertisers.ts
35833
- import { defineCommand as defineCommand178 } from "citty";
38223
+ import { defineCommand as defineCommand186 } from "citty";
35834
38224
 
35835
38225
  // src/commands/winning-ads/shared.ts
35836
- function splitList(value) {
38226
+ function splitList2(value) {
35837
38227
  if (!value) {
35838
38228
  return [];
35839
38229
  }
35840
38230
  return value.split(",").map((v) => v.trim()).filter(Boolean);
35841
38231
  }
35842
- function reportError(err) {
38232
+ function reportError2(err) {
35843
38233
  if (err instanceof ApiError) {
35844
38234
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
35845
38235
  process.exit(1);
@@ -35883,7 +38273,7 @@ function advertiserNormalizer(record, full) {
35883
38273
  last_synced_at: record.last_synced_at ?? null
35884
38274
  };
35885
38275
  }
35886
- var advertisersCommand2 = defineCommand178({
38276
+ var advertisersCommand2 = defineCommand186({
35887
38277
  meta: {
35888
38278
  name: "advertisers",
35889
38279
  description: 'List corpus advertisers by name or domain. Find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id / winners. Example: baker winning-ads advertisers "Deel" --output md'
@@ -35935,13 +38325,13 @@ var advertisersCommand2 = defineCommand178({
35935
38325
  advertiserNormalizer
35936
38326
  );
35937
38327
  } catch (err) {
35938
- reportError(err);
38328
+ reportError2(err);
35939
38329
  }
35940
38330
  }
35941
38331
  });
35942
38332
 
35943
38333
  // src/commands/winning-ads/brief.ts
35944
- import { defineCommand as defineCommand179 } from "citty";
38334
+ import { defineCommand as defineCommand187 } from "citty";
35945
38335
  registerSchema({
35946
38336
  command: "winning-ads.brief",
35947
38337
  description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
@@ -35987,7 +38377,7 @@ function parseDna(raw) {
35987
38377
  }
35988
38378
  return parsed;
35989
38379
  }
35990
- var briefCommand = defineCommand179({
38380
+ var briefCommand = defineCommand187({
35991
38381
  meta: {
35992
38382
  name: "brief",
35993
38383
  description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
@@ -36017,13 +38407,13 @@ var briefCommand = defineCommand179({
36017
38407
  const data = await apiPost("/api/ad-library/brief", body);
36018
38408
  writeJson({ ok: true, data });
36019
38409
  } catch (err) {
36020
- reportError(err);
38410
+ reportError2(err);
36021
38411
  }
36022
38412
  }
36023
38413
  });
36024
38414
 
36025
38415
  // src/commands/winning-ads/content.ts
36026
- import { defineCommand as defineCommand180 } from "citty";
38416
+ import { defineCommand as defineCommand188 } from "citty";
36027
38417
  registerSchema({
36028
38418
  command: "winning-ads.content",
36029
38419
  description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
@@ -36036,7 +38426,7 @@ registerSchema({
36036
38426
  }
36037
38427
  }
36038
38428
  });
36039
- var contentCommand = defineCommand180({
38429
+ var contentCommand = defineCommand188({
36040
38430
  meta: {
36041
38431
  name: "content",
36042
38432
  description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
@@ -36079,16 +38469,16 @@ var contentCommand = defineCommand180({
36079
38469
  adContentNormalizer
36080
38470
  );
36081
38471
  } catch (err) {
36082
- reportError(err);
38472
+ reportError2(err);
36083
38473
  }
36084
38474
  }
36085
38475
  });
36086
38476
 
36087
38477
  // src/commands/winning-ads/feed.ts
36088
- import { defineCommand as defineCommand181 } from "citty";
38478
+ import { defineCommand as defineCommand189 } from "citty";
36089
38479
  function buildFeedParams(input) {
36090
38480
  const params = {};
36091
- const advertiser = splitList(input.advertiser);
38481
+ const advertiser = splitList2(input.advertiser);
36092
38482
  if (advertiser.length > 0) {
36093
38483
  params.advertiser = advertiser.join(",");
36094
38484
  }
@@ -36101,11 +38491,11 @@ function buildFeedParams(input) {
36101
38491
  if (input.limit !== void 0 && input.limit !== "") {
36102
38492
  params.limit = input.limit;
36103
38493
  }
36104
- const winnerCategory = splitList(input.winnerCategory);
38494
+ const winnerCategory = splitList2(input.winnerCategory);
36105
38495
  if (winnerCategory.length > 0) {
36106
38496
  params.winner_category = winnerCategory.join(",");
36107
38497
  }
36108
- const format = splitList(input.format);
38498
+ const format = splitList2(input.format);
36109
38499
  if (format.length > 0) {
36110
38500
  params.format = format.join(",");
36111
38501
  }
@@ -36137,7 +38527,7 @@ registerSchema({
36137
38527
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
36138
38528
  }
36139
38529
  });
36140
- var feedCommand = defineCommand181({
38530
+ var feedCommand = defineCommand189({
36141
38531
  meta: {
36142
38532
  name: "feed",
36143
38533
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -36216,13 +38606,13 @@ var feedCommand = defineCommand181({
36216
38606
  `);
36217
38607
  }
36218
38608
  } catch (err) {
36219
- reportError(err);
38609
+ reportError2(err);
36220
38610
  }
36221
38611
  }
36222
38612
  });
36223
38613
 
36224
38614
  // src/commands/winning-ads/follow.ts
36225
- import { defineCommand as defineCommand182 } from "citty";
38615
+ import { defineCommand as defineCommand190 } from "citty";
36226
38616
  var PLATFORMS = ["meta", "linkedin"];
36227
38617
  registerSchema({
36228
38618
  command: "winning-ads.follow",
@@ -36237,7 +38627,7 @@ registerSchema({
36237
38627
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
36238
38628
  }
36239
38629
  });
36240
- var followCommand = defineCommand182({
38630
+ var followCommand = defineCommand190({
36241
38631
  meta: {
36242
38632
  name: "follow",
36243
38633
  description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
@@ -36278,19 +38668,19 @@ var followCommand = defineCommand182({
36278
38668
  }
36279
38669
  writeJson({ ok: true, data, hints });
36280
38670
  } catch (err) {
36281
- reportError(err);
38671
+ reportError2(err);
36282
38672
  }
36283
38673
  }
36284
38674
  });
36285
38675
 
36286
38676
  // src/commands/winning-ads/follow-competitors.ts
36287
- import { defineCommand as defineCommand183 } from "citty";
38677
+ import { defineCommand as defineCommand191 } from "citty";
36288
38678
  var PLATFORMS2 = ["meta", "linkedin"];
36289
38679
  var BATCH_TIMEOUT_MS = 3e5;
36290
38680
  function buildFollowBatchBody(input) {
36291
38681
  const seen = /* @__PURE__ */ new Set();
36292
38682
  const inputs = [];
36293
- for (const domain of splitList(input.domains)) {
38683
+ for (const domain of splitList2(input.domains)) {
36294
38684
  const key = domain.toLowerCase();
36295
38685
  if (seen.has(key)) {
36296
38686
  continue;
@@ -36317,7 +38707,7 @@ registerSchema({
36317
38707
  }
36318
38708
  }
36319
38709
  });
36320
- var followCompetitorsCommand = defineCommand183({
38710
+ var followCompetitorsCommand = defineCommand191({
36321
38711
  meta: {
36322
38712
  name: "follow-competitors",
36323
38713
  description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
@@ -36386,13 +38776,13 @@ var followCompetitorsCommand = defineCommand183({
36386
38776
  }
36387
38777
  writeJson({ ok: true, data, hints: hints.length > 0 ? hints : void 0 });
36388
38778
  } catch (err) {
36389
- reportError(err);
38779
+ reportError2(err);
36390
38780
  }
36391
38781
  }
36392
38782
  });
36393
38783
 
36394
38784
  // src/commands/winning-ads/following.ts
36395
- import { defineCommand as defineCommand184 } from "citty";
38785
+ import { defineCommand as defineCommand192 } from "citty";
36396
38786
  registerSchema({
36397
38787
  command: "winning-ads.following",
36398
38788
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -36425,7 +38815,7 @@ function followingNormalizer(record, full) {
36425
38815
  platforms: Array.isArray(record.platforms) ? record.platforms : []
36426
38816
  };
36427
38817
  }
36428
- var followingCommand = defineCommand184({
38818
+ var followingCommand = defineCommand192({
36429
38819
  meta: {
36430
38820
  name: "following",
36431
38821
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -36454,13 +38844,13 @@ var followingCommand = defineCommand184({
36454
38844
  followingNormalizer
36455
38845
  );
36456
38846
  } catch (err) {
36457
- reportError(err);
38847
+ reportError2(err);
36458
38848
  }
36459
38849
  }
36460
38850
  });
36461
38851
 
36462
38852
  // src/commands/winning-ads/patterns.ts
36463
- import { defineCommand as defineCommand185 } from "citty";
38853
+ import { defineCommand as defineCommand193 } from "citty";
36464
38854
  registerSchema({
36465
38855
  command: "winning-ads.patterns",
36466
38856
  description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
@@ -36475,8 +38865,8 @@ registerSchema({
36475
38865
  }
36476
38866
  });
36477
38867
  function buildPatternsBody(args) {
36478
- const winners = splitList(args.winners);
36479
- const duds = splitList(args.duds);
38868
+ const winners = splitList2(args.winners);
38869
+ const duds = splitList2(args.duds);
36480
38870
  if (!winners.length || !duds.length) {
36481
38871
  throw new Error("Provide at least one ad id for both --winners and --duds");
36482
38872
  }
@@ -36499,7 +38889,7 @@ function discriminatorRow(record) {
36499
38889
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
36500
38890
  };
36501
38891
  }
36502
- var patternsCommand = defineCommand185({
38892
+ var patternsCommand = defineCommand193({
36503
38893
  meta: {
36504
38894
  name: "patterns",
36505
38895
  description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
@@ -36549,13 +38939,13 @@ var patternsCommand = defineCommand185({
36549
38939
  (record) => discriminatorRow(record)
36550
38940
  );
36551
38941
  } catch (err) {
36552
- reportError(err);
38942
+ reportError2(err);
36553
38943
  }
36554
38944
  }
36555
38945
  });
36556
38946
 
36557
38947
  // src/commands/winning-ads/search.ts
36558
- import { defineCommand as defineCommand186 } from "citty";
38948
+ import { defineCommand as defineCommand194 } from "citty";
36559
38949
  registerSchema({
36560
38950
  command: "winning-ads.search",
36561
38951
  description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
@@ -36625,7 +39015,7 @@ function setNumber(body, key, value) {
36625
39015
  }
36626
39016
  }
36627
39017
  function setList(target, key, value) {
36628
- const list = splitList(value);
39018
+ const list = splitList2(value);
36629
39019
  if (list.length) {
36630
39020
  target[key] = list;
36631
39021
  }
@@ -36635,7 +39025,7 @@ function setString(target, key, value) {
36635
39025
  target[key] = value;
36636
39026
  }
36637
39027
  }
36638
- function buildSearchBody(args) {
39028
+ function buildSearchBody2(args) {
36639
39029
  const body = {};
36640
39030
  if (args.query) {
36641
39031
  body.free_text_query = args.query;
@@ -36663,7 +39053,7 @@ function buildSearchBody(args) {
36663
39053
  }
36664
39054
  return body;
36665
39055
  }
36666
- var searchCommand4 = defineCommand186({
39056
+ var searchCommand5 = defineCommand194({
36667
39057
  meta: {
36668
39058
  name: "search",
36669
39059
  description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
@@ -36739,7 +39129,7 @@ var searchCommand4 = defineCommand186({
36739
39129
  firstSeenBefore: args["first-seen-before"],
36740
39130
  hookArchetype: args["hook-archetype"]
36741
39131
  };
36742
- const body = buildSearchBody(searchArgs);
39132
+ const body = buildSearchBody2(searchArgs);
36743
39133
  if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
36744
39134
  writeJson({
36745
39135
  ok: false,
@@ -36772,13 +39162,13 @@ var searchCommand4 = defineCommand186({
36772
39162
  winningAdNormalizer
36773
39163
  );
36774
39164
  } catch (err) {
36775
- reportError(err);
39165
+ reportError2(err);
36776
39166
  }
36777
39167
  }
36778
39168
  });
36779
39169
 
36780
39170
  // src/commands/winning-ads/seeds.ts
36781
- import { defineCommand as defineCommand187 } from "citty";
39171
+ import { defineCommand as defineCommand195 } from "citty";
36782
39172
  function leanRow(r) {
36783
39173
  return {
36784
39174
  key: r.key,
@@ -36806,7 +39196,7 @@ function makeSeedCommand(opts) {
36806
39196
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
36807
39197
  }
36808
39198
  });
36809
- return defineCommand187({
39199
+ return defineCommand195({
36810
39200
  meta: { name: opts.name, description: opts.description },
36811
39201
  args: {
36812
39202
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -36833,7 +39223,7 @@ function makeSeedCommand(opts) {
36833
39223
  }
36834
39224
  writeOutput({ ok: true, data: projected }, output);
36835
39225
  } catch (err) {
36836
- reportError(err);
39226
+ reportError2(err);
36837
39227
  }
36838
39228
  }
36839
39229
  });
@@ -36855,7 +39245,7 @@ var formatsCommand = makeSeedCommand({
36855
39245
  });
36856
39246
 
36857
39247
  // src/commands/winning-ads/unfollow.ts
36858
- import { defineCommand as defineCommand188 } from "citty";
39248
+ import { defineCommand as defineCommand196 } from "citty";
36859
39249
  registerSchema({
36860
39250
  command: "winning-ads.unfollow",
36861
39251
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -36863,7 +39253,7 @@ registerSchema({
36863
39253
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
36864
39254
  }
36865
39255
  });
36866
- var unfollowCommand = defineCommand188({
39256
+ var unfollowCommand = defineCommand196({
36867
39257
  meta: {
36868
39258
  name: "unfollow",
36869
39259
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -36878,13 +39268,13 @@ var unfollowCommand = defineCommand188({
36878
39268
  });
36879
39269
  writeJson({ ok: true, data });
36880
39270
  } catch (err) {
36881
- reportError(err);
39271
+ reportError2(err);
36882
39272
  }
36883
39273
  }
36884
39274
  });
36885
39275
 
36886
39276
  // src/commands/winning-ads/winners.ts
36887
- import { defineCommand as defineCommand189 } from "citty";
39277
+ import { defineCommand as defineCommand197 } from "citty";
36888
39278
  registerSchema({
36889
39279
  command: "winning-ads.winners",
36890
39280
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -36894,7 +39284,7 @@ registerSchema({
36894
39284
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
36895
39285
  }
36896
39286
  });
36897
- var winnersCommand = defineCommand189({
39287
+ var winnersCommand = defineCommand197({
36898
39288
  meta: {
36899
39289
  name: "winners",
36900
39290
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -36938,13 +39328,13 @@ var winnersCommand = defineCommand189({
36938
39328
  winningAdNormalizer
36939
39329
  );
36940
39330
  } catch (err) {
36941
- reportError(err);
39331
+ reportError2(err);
36942
39332
  }
36943
39333
  }
36944
39334
  });
36945
39335
 
36946
39336
  // src/commands/winning-ads/index.ts
36947
- var winningAdsCommand = defineCommand190({
39337
+ var winningAdsCommand = defineCommand198({
36948
39338
  meta: {
36949
39339
  name: "winning-ads",
36950
39340
  description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
@@ -36982,7 +39372,7 @@ Examples:
36982
39372
  Full guide: __tooling__/docs/tools/baker/winning-ads.md`
36983
39373
  },
36984
39374
  subCommands: {
36985
- search: searchCommand4,
39375
+ search: searchCommand5,
36986
39376
  advertisers: advertisersCommand2,
36987
39377
  follow: followCommand,
36988
39378
  "follow-competitors": followCompetitorsCommand,
@@ -37016,7 +39406,7 @@ function getCliVersion() {
37016
39406
  }
37017
39407
 
37018
39408
  // src/cli.ts
37019
- var main = defineCommand191({
39409
+ var main = defineCommand199({
37020
39410
  meta: {
37021
39411
  name: "baker",
37022
39412
  version: getCliVersion(),