@koda-sl/baker-cli 0.188.0 → 0.189.1

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
@@ -90,7 +90,7 @@ import {
90
90
  } from "./chunk-YL3HDEIJ.js";
91
91
 
92
92
  // src/cli.ts
93
- import { defineCommand as defineCommand201, runMain } from "citty";
93
+ import { defineCommand as defineCommand209, runMain } from "citty";
94
94
 
95
95
  // src/commands/actions/index.ts
96
96
  import { defineCommand as defineCommand18 } from "citty";
@@ -2635,6 +2635,8 @@ var chatChangeTypeSchema = z7.enum([
2635
2635
  "image",
2636
2636
  "video",
2637
2637
  "followed-advertiser",
2638
+ // A landing page saved to the reference library, keyed on the page.
2639
+ "landing-reference",
2638
2640
  // One Studio batch — the takes of one brief, as a set. See convex/lib/validators.ts.
2639
2641
  "studio-generation",
2640
2642
  "briefs"
@@ -5099,6 +5101,20 @@ var FIELD_DESCRIPTIONS = {
5099
5101
  "ad_group_criterion.keyword.match_type": "EXACT, PHRASE, or BROAD",
5100
5102
  "ad_group_criterion.status": "ENABLED, PAUSED, or REMOVED",
5101
5103
  "ad_group_criterion.quality_info.quality_score": "Quality score 1-10 (higher is better)",
5104
+ "ad_group_criterion.quality_info.creative_quality_score": "Ad relevance bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
5105
+ "ad_group_criterion.quality_info.post_click_quality_score": "Landing page experience bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
5106
+ "ad_group_criterion.quality_info.search_predicted_ctr": "Expected click-through rate bucket: ABOVE_AVERAGE, AVERAGE, BELOW_AVERAGE",
5107
+ "ad_group_criterion.primary_status": "Whether the keyword can serve: ELIGIBLE, PAUSED, REMOVED, PENDING, NOT_ELIGIBLE. There is no LIMITED value \u2014 Google Ads' 'Eligible (Limited)' is ELIGIBLE plus a limiting entry in primary_status_reasons, so read both.",
5108
+ "ad_group_criterion.primary_status_reasons": "Why the keyword is in that status (array). AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID and AD_GROUP_CRITERION_LOW_QUALITY are the two that render as '(Limited)'; the CAMPAIGN_*/AD_GROUP_* entries just restate a paused or removed parent.",
5109
+ "ad_group_criterion.system_serving_status": "ELIGIBLE or RARELY_SERVED (too few searches to show)",
5110
+ "ad_group_criterion.approval_status": "Ad-approval state: APPROVED, DISAPPROVED, PENDING_REVIEW, UNDER_REVIEW",
5111
+ "ad_group_criterion.disapproval_reasons": "Policy reasons the keyword was disapproved (array)",
5112
+ "ad_group_criterion.cpc_bid_micros": "Max CPC bid set on this keyword, in micros (\xF7 1,000,000)",
5113
+ "ad_group_criterion.effective_cpc_bid_micros": "Max CPC actually in force, in micros (\xF7 1,000,000) \u2014 the keyword's own bid, or the ad group's when the keyword has none",
5114
+ "ad_group_criterion.effective_cpc_bid_source": "Where the effective max CPC comes from: ADGROUP, CRITERION, or CAMPAIGN_BIDDING_STRATEGY",
5115
+ "ad_group_criterion.position_estimates.first_page_cpc_micros": "Bid estimated to reach the first page of results, in micros (\xF7 1,000,000). The number in Google Ads' 'Below first page bid (\u20AC3.63)' label \u2014 compare it against effective_cpc_bid_micros.",
5116
+ "ad_group_criterion.position_estimates.top_of_page_cpc_micros": "Bid estimated to reach the top of the first page, in micros (\xF7 1,000,000)",
5117
+ "ad_group_criterion.position_estimates.first_position_cpc_micros": "Bid estimated to reach the first ad position, in micros (\xF7 1,000,000)",
5102
5118
  // Search terms
5103
5119
  "search_term_view.search_term": "Actual user search query that triggered the ad",
5104
5120
  "search_term_view.status": "Whether the search term is added, excluded, or none",
@@ -7758,19 +7774,19 @@ Examples:
7758
7774
  },
7759
7775
  run: async ({ args }) => {
7760
7776
  const customerId = await resolveCustomerId(args);
7761
- const window = resolveChangesWindow({
7777
+ const window2 = resolveChangesWindow({
7762
7778
  days: args.days ? Number(args.days) : void 0,
7763
7779
  scope: args.scope
7764
7780
  });
7765
- if (!window.ok) {
7766
- writeJsonEnvelope({ ok: false, error: { ...window.error, retryable: false } });
7781
+ if (!window2.ok) {
7782
+ writeJsonEnvelope({ ok: false, error: { ...window2.error, retryable: false } });
7767
7783
  process.exit(1);
7768
7784
  return;
7769
7785
  }
7770
7786
  const body = {
7771
7787
  customerId,
7772
- days: window.days,
7773
- scope: window.scope,
7788
+ days: window2.days,
7789
+ scope: window2.scope,
7774
7790
  limit: args.limit ? Number(args.limit) : 50
7775
7791
  };
7776
7792
  const managerId = getManagerIdForCustomer(customerId);
@@ -7787,7 +7803,7 @@ Examples:
7787
7803
  const first = data[0];
7788
7804
  const fields = first ? Object.keys(first) : [];
7789
7805
  const fieldDescs = getFieldDescriptions(fields);
7790
- writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window.hints });
7806
+ writeJsonEnvelope({ ok: true, data, fields: fieldDescs, hints: window2.hints });
7791
7807
  } catch (err) {
7792
7808
  if (err instanceof ApiError) {
7793
7809
  writeAdsJson(parseApiError(err.message, "", customerId, err.code));
@@ -8144,8 +8160,8 @@ function countStaccato(text) {
8144
8160
  let first = "";
8145
8161
  let runStart = 0;
8146
8162
  for (const [i, s] of sentences.entries()) {
8147
- const words2 = s.split(/\s+/).filter(Boolean).length;
8148
- if (words2 > 0 && words2 <= STACCATO_MAX_WORDS) {
8163
+ const words3 = s.split(/\s+/).filter(Boolean).length;
8164
+ if (words3 > 0 && words3 <= STACCATO_MAX_WORDS) {
8149
8165
  if (run === 0) runStart = i;
8150
8166
  run++;
8151
8167
  if (run === STACCATO_RUN) {
@@ -8163,10 +8179,10 @@ function countTitleCaseHeadings(text) {
8163
8179
  let first = "";
8164
8180
  for (const m of text.matchAll(/^\s{0,3}#{1,6}\s+(.+)$/gm)) {
8165
8181
  const heading = (m[1] ?? "").trim();
8166
- const words2 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
8167
- if (words2.length < 4) continue;
8168
- const capitalized = words2.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
8169
- if (capitalized / words2.length < 0.8) continue;
8182
+ const words3 = heading.split(/\s+/).filter((w) => new RegExp("\\p{L}", "u").test(w));
8183
+ if (words3.length < 4) continue;
8184
+ const capitalized = words3.filter((w) => new RegExp("^\\p{Lu}", "u").test(w)).length;
8185
+ if (capitalized / words3.length < 0.8) continue;
8170
8186
  count++;
8171
8187
  if (!first) first = heading;
8172
8188
  }
@@ -8498,11 +8514,11 @@ function rawTextEntries(value) {
8498
8514
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
8499
8515
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
8500
8516
  }
8501
- function rawFileEntries(path28) {
8502
- if (typeof path28 !== "string" || path28.length === 0) {
8517
+ function rawFileEntries(path35) {
8518
+ if (typeof path35 !== "string" || path35.length === 0) {
8503
8519
  return [];
8504
8520
  }
8505
- return readFileSync2(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8521
+ return readFileSync2(path35, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8506
8522
  }
8507
8523
  function keywordEntries(args) {
8508
8524
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -8525,19 +8541,19 @@ function keywordEntries(args) {
8525
8541
  }
8526
8542
  return entries;
8527
8543
  }
8528
- function loadJsonFileArg(path28) {
8529
- if (typeof path28 !== "string" || path28.length === 0) {
8544
+ function loadJsonFileArg(path35) {
8545
+ if (typeof path35 !== "string" || path35.length === 0) {
8530
8546
  return {};
8531
8547
  }
8532
8548
  try {
8533
- const parsed = JSON.parse(readFileSync2(path28, "utf8"));
8549
+ const parsed = JSON.parse(readFileSync2(path35, "utf8"));
8534
8550
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8535
- failWriteValidation(`${path28} must contain a JSON object`);
8551
+ failWriteValidation(`${path35} must contain a JSON object`);
8536
8552
  }
8537
8553
  return parsed;
8538
8554
  } catch (err) {
8539
8555
  if (err instanceof SyntaxError) {
8540
- failWriteValidation(`${path28} is not valid JSON: ${err.message}`);
8556
+ failWriteValidation(`${path35} is not valid JSON: ${err.message}`);
8541
8557
  }
8542
8558
  throw err;
8543
8559
  }
@@ -8667,10 +8683,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
8667
8683
  async function stageTarget(kind, customerId, target, hints) {
8668
8684
  await stageGoogleOp({ kind, customerId, target }, hints);
8669
8685
  }
8670
- async function draftAction(path28, body, chat) {
8686
+ async function draftAction(path35, body, chat) {
8671
8687
  try {
8672
8688
  const chatId = resolveChatId(chat);
8673
- const response = await apiPost(path28, { chatId, ...body });
8689
+ const response = await apiPost(path35, { chatId, ...body });
8674
8690
  writeJsonEnvelope(response);
8675
8691
  } catch (err) {
8676
8692
  handleGoogleError(err);
@@ -9229,6 +9245,121 @@ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync
9229
9245
  import { resolve } from "path";
9230
9246
  import { defineCommand as defineCommand28 } from "citty";
9231
9247
 
9248
+ // src/commands/ads/google/keyword-serving.ts
9249
+ var BELOW_FIRST_PAGE_BID = "AD_GROUP_CRITERION_BELOW_FIRST_PAGE_BID";
9250
+ var ACTIONABLE_REASONS = {
9251
+ [BELOW_FIRST_PAGE_BID]: {
9252
+ label: "Below first page bid",
9253
+ advice: "raise the max CPC (keywords update --cpc-bid) to at least the first-page estimate, or move the ad group onto a smart bidding strategy that sets the bid itself"
9254
+ },
9255
+ AD_GROUP_CRITERION_LOW_QUALITY: {
9256
+ label: "Low quality score",
9257
+ advice: "a bid raise does not lift quality \u2014 tighten ad relevance to the keyword and the landing page it points at (ad_group_criterion.quality_info.* breaks the score into its three parts)"
9258
+ },
9259
+ AD_GROUP_CRITERION_RARELY_SERVED: {
9260
+ label: "Rarely served",
9261
+ advice: "too few people search this term \u2014 broaden the match type or drop it, don't bid it up"
9262
+ },
9263
+ AD_GROUP_CRITERION_DISAPPROVED: {
9264
+ label: "Disapproved",
9265
+ advice: "select ad_group_criterion.disapproval_reasons to see which policy blocked it"
9266
+ },
9267
+ AD_GROUP_CRITERION_RESTRICTED: {
9268
+ label: "Restricted",
9269
+ advice: "policy limits where and to whom this keyword may show"
9270
+ },
9271
+ AD_GROUP_CRITERION_UNDER_REVIEW: {
9272
+ label: "Under review",
9273
+ advice: "waiting on Google's review \u2014 nothing to change yet"
9274
+ },
9275
+ AD_GROUP_CRITERION_PENDING_REVIEW: {
9276
+ label: "Pending review",
9277
+ advice: "waiting on Google's review \u2014 nothing to change yet"
9278
+ },
9279
+ AD_GROUP_CRITERION_PAUSED_DUE_TO_LOW_ACTIVITY: {
9280
+ label: "Auto-paused for low activity",
9281
+ advice: "Google paused it after prolonged low serving \u2014 resume it only alongside a change that gives it traffic"
9282
+ }
9283
+ };
9284
+ var REASON_ORDER = Object.keys(ACTIONABLE_REASONS);
9285
+ var MAX_NAMED_KEYWORDS = 5;
9286
+ function toMicros2(value) {
9287
+ if (typeof value === "number" && Number.isFinite(value)) return value;
9288
+ if (typeof value === "string" && value.trim() !== "" && Number.isFinite(Number(value))) return Number(value);
9289
+ return null;
9290
+ }
9291
+ function formatMoney(micros) {
9292
+ return (micros / 1e6).toFixed(2);
9293
+ }
9294
+ function keywordLabel(row) {
9295
+ const text = row["ad_group_criterion.keyword.text"];
9296
+ const id = row["ad_group_criterion.criterion_id"];
9297
+ const name = typeof text === "string" && text !== "" ? text : id == null ? "(unnamed keyword)" : `criterion ${id}`;
9298
+ const adGroup = row["ad_group.name"];
9299
+ return typeof adGroup === "string" && adGroup !== "" ? `${name} (ad group ${adGroup})` : name;
9300
+ }
9301
+ function keywordKey(row) {
9302
+ const adGroup = row["ad_group.id"] ?? row["ad_group.name"] ?? "";
9303
+ const criterion = row["ad_group_criterion.criterion_id"] ?? row["ad_group_criterion.keyword.text"] ?? "";
9304
+ return `${String(adGroup)}~${String(criterion)}` === "~" ? keywordLabel(row) : `${adGroup}~${criterion}`;
9305
+ }
9306
+ function reasonsOn(row) {
9307
+ const raw = row["ad_group_criterion.primary_status_reasons"];
9308
+ if (!Array.isArray(raw)) return null;
9309
+ return raw.filter((r) => typeof r === "string");
9310
+ }
9311
+ function bidShortfall(row) {
9312
+ const bid = toMicros2(row["ad_group_criterion.effective_cpc_bid_micros"] ?? row["ad_group_criterion.cpc_bid_micros"]);
9313
+ const firstPage = toMicros2(row["ad_group_criterion.position_estimates.first_page_cpc_micros"]);
9314
+ if (bid == null || firstPage == null || bid >= firstPage) return null;
9315
+ return `bids ${formatMoney(bid)} against a ${formatMoney(firstPage)} first-page estimate`;
9316
+ }
9317
+ function limitsOn(row, options) {
9318
+ const reasons = reasonsOn(row);
9319
+ const shortfall = bidShortfall(row);
9320
+ const label = keywordLabel(row);
9321
+ const applicable = reasons ? reasons.filter((reason) => reason in ACTIONABLE_REASONS) : options.statusReasonsSelected || shortfall === null ? [] : [BELOW_FIRST_PAGE_BID];
9322
+ return applicable.map((reason) => ({
9323
+ reason,
9324
+ detail: reason === BELOW_FIRST_PAGE_BID && shortfall ? `"${label}" ${shortfall}` : `"${label}"`
9325
+ }));
9326
+ }
9327
+ function describeLimit(reason, details) {
9328
+ const named = details.slice(0, MAX_NAMED_KEYWORDS).join("; ");
9329
+ const rest = details.length > MAX_NAMED_KEYWORDS ? ` (+${details.length - MAX_NAMED_KEYWORDS} more)` : "";
9330
+ const { label, advice } = ACTIONABLE_REASONS[reason];
9331
+ return `${label} \u2014 ${details.length}: ${named}${rest}. Fix: ${advice}.`;
9332
+ }
9333
+ function collectKeywordLimits(rows, options, acc = { detailsByReason: /* @__PURE__ */ new Map(), keywords: /* @__PURE__ */ new Set() }) {
9334
+ for (const row of rows) {
9335
+ const limits = limitsOn(row, options);
9336
+ if (limits.length === 0) continue;
9337
+ acc.keywords.add(keywordKey(row));
9338
+ for (const { reason, detail } of limits) {
9339
+ const bucket = acc.detailsByReason.get(reason);
9340
+ if (bucket) bucket.push(detail);
9341
+ else acc.detailsByReason.set(reason, [detail]);
9342
+ }
9343
+ }
9344
+ return acc;
9345
+ }
9346
+ function renderKeywordLimits(limits) {
9347
+ if (limits.detailsByReason.size === 0) return [];
9348
+ const sentences = REASON_ORDER.filter((reason) => limits.detailsByReason.has(reason)).map(
9349
+ (reason) => describeLimit(reason, limits.detailsByReason.get(reason) ?? [])
9350
+ );
9351
+ const count = limits.keywords.size;
9352
+ return [
9353
+ {
9354
+ code: "KEYWORD_SERVING_LIMITED",
9355
+ message: `${count} ${count === 1 ? "keyword is" : "keywords are"} limited and not getting the traffic they could. ${sentences.join(" ")} Amounts are in the account currency (baker ads google currency).`
9356
+ }
9357
+ ];
9358
+ }
9359
+ function keywordServingWarnings(rows, options) {
9360
+ return renderKeywordLimits(collectKeywordLimits(rows, options));
9361
+ }
9362
+
9232
9363
  // src/commands/ads/google/preflight.ts
9233
9364
  function buildCommand2(query, customerId) {
9234
9365
  return `baker ads google query "${query}" --customer-id ${customerId}`;
@@ -9418,11 +9549,19 @@ var PRESETS = [
9418
9549
  {
9419
9550
  name: "keyword-analysis",
9420
9551
  description: "Keyword performance with match type and quality",
9421
- gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
9552
+ gaqlTemplate: `SELECT campaign.id, campaign.name, campaign.status, ad_group.id, ad_group.name, ad_group.status, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.quality_info.quality_score, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions, metrics.ctr FROM keyword_view WHERE segments.date DURING {dateRange}{statusScope} ORDER BY metrics.impressions DESC LIMIT {limit}`,
9422
9553
  defaultDateRange: "LAST_30_DAYS",
9423
9554
  defaultLimit: 200,
9424
9555
  statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
9425
9556
  },
9557
+ {
9558
+ name: "keyword-serving",
9559
+ description: "Why a keyword is limited \u2014 Google Ads' 'Eligible (Limited) / Below first page bid' column, with the first-page bid estimate, the current max CPC and quality score",
9560
+ gaqlTemplate: `SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.criterion_id, ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.status, ad_group_criterion.primary_status, ad_group_criterion.primary_status_reasons, ad_group_criterion.system_serving_status, ad_group_criterion.approval_status, ad_group_criterion.effective_cpc_bid_micros, ad_group_criterion.effective_cpc_bid_source, ad_group_criterion.position_estimates.first_page_cpc_micros, ad_group_criterion.position_estimates.top_of_page_cpc_micros, ad_group_criterion.quality_info.quality_score, ad_group_criterion.quality_info.creative_quality_score, ad_group_criterion.quality_info.post_click_quality_score, ad_group_criterion.quality_info.search_predicted_ctr FROM ad_group_criterion WHERE ad_group_criterion.type = 'KEYWORD' AND ad_group_criterion.negative = FALSE{statusScope} ORDER BY campaign.name LIMIT {limit}`,
9561
+ defaultDateRange: "ALL_TIME",
9562
+ defaultLimit: 500,
9563
+ statusFields: ["campaign.status", "ad_group.status", "ad_group_criterion.status"]
9564
+ },
9426
9565
  {
9427
9566
  name: "positive-keywords",
9428
9567
  description: "Positive (targeting) keywords only \u2014 excludes negatives",
@@ -9527,7 +9666,7 @@ registerSchema({
9527
9666
  },
9528
9667
  preset: {
9529
9668
  type: "string",
9530
- description: "Named query preset. Reads (campaign-performance, keyword-analysis, positive-keywords, negative-keywords, negative-keyword-lists, negative-list-attachments, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary). Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default \u2014 pass --include-paused to include paused ones.",
9669
+ description: "Named query preset. Reads (campaign-performance, keyword-analysis, keyword-serving, positive-keywords, negative-keywords, negative-keyword-lists, negative-list-attachments, search-terms, ad-copy-performance, asset-performance, shopping-products, account-summary). Presets return only actually-serving entities (enabled campaign AND ad group AND ad) by default \u2014 pass --include-paused to include paused ones.",
9531
9670
  required: false
9532
9671
  },
9533
9672
  "date-range": {
@@ -9759,10 +9898,14 @@ function missingFieldWarnings(gaql, rows) {
9759
9898
  }
9760
9899
  ];
9761
9900
  }
9762
- async function fetchAllPages(body, args) {
9901
+ function keywordServingOptions(gaql) {
9902
+ return { statusReasonsSelected: parseSelectedFields(gaql).includes("ad_group_criterion.primary_status_reasons") };
9903
+ }
9904
+ async function fetchAllPages(body, args, servingOptions) {
9763
9905
  let allRows = [];
9764
9906
  let pageToken = args.cursor;
9765
9907
  let totalRowsWritten = 0;
9908
+ let keywordLimits;
9766
9909
  do {
9767
9910
  const requestBody = { ...body };
9768
9911
  if (pageToken) requestBody.pageToken = pageToken;
@@ -9774,12 +9917,18 @@ async function fetchAllPages(body, args) {
9774
9917
  const shouldAppend = args.append || totalRowsWritten > 0;
9775
9918
  writeRowsToFile(filePath, rows, extractFields(rows), shouldAppend);
9776
9919
  totalRowsWritten += rows.length;
9920
+ keywordLimits = collectKeywordLimits(rows, servingOptions, keywordLimits);
9777
9921
  } else {
9778
9922
  allRows = allRows.concat(rows);
9779
9923
  }
9780
9924
  if (!args.all) break;
9781
9925
  } while (pageToken);
9782
- return { allRows, lastPageToken: pageToken, totalRowsWritten };
9926
+ return {
9927
+ allRows,
9928
+ lastPageToken: pageToken,
9929
+ totalRowsWritten,
9930
+ keywordLimits: keywordLimits ?? collectKeywordLimits([], servingOptions)
9931
+ };
9783
9932
  }
9784
9933
  async function executeQuery(finalQuery, args, customerId, limit, warnings, useCache, cacheKey) {
9785
9934
  const body = { customerId, query: finalQuery, pageSize: limit };
@@ -9787,9 +9936,12 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
9787
9936
  if (managerId) body.managerId = managerId;
9788
9937
  if (args.cursor) body.pageToken = args.cursor;
9789
9938
  if (!useCache) body.skipCache = true;
9790
- const { allRows, lastPageToken, totalRowsWritten } = await fetchAllPages(body, args);
9939
+ const servingOptions = keywordServingOptions(finalQuery);
9940
+ const { allRows, lastPageToken, totalRowsWritten, keywordLimits } = await fetchAllPages(body, args, servingOptions);
9791
9941
  if (args.out) {
9792
9942
  const summary = { ok: true, fields: {}, file: resolve(args.out), rows: totalRowsWritten };
9943
+ const limitWarnings = renderKeywordLimits(keywordLimits);
9944
+ if (limitWarnings.length > 0) summary.warnings = limitWarnings;
9793
9945
  writeAdsJson(summary);
9794
9946
  return;
9795
9947
  }
@@ -9801,7 +9953,7 @@ async function executeQuery(finalQuery, args, customerId, limit, warnings, useCa
9801
9953
  outputResults(
9802
9954
  allRows,
9803
9955
  getFieldDescriptions(fields),
9804
- [...warnings, ...missingFieldWarnings(finalQuery, allRows)],
9956
+ [...warnings, ...missingFieldWarnings(finalQuery, allRows), ...keywordServingWarnings(allRows, servingOptions)],
9805
9957
  args,
9806
9958
  false,
9807
9959
  lastPageToken
@@ -9903,7 +10055,11 @@ Examples:
9903
10055
  outputResults(
9904
10056
  cached.data,
9905
10057
  cached.fields ?? getFieldDescriptions(fields),
9906
- [...warnings, ...missingFieldWarnings(finalQuery, cached.data)],
10058
+ [
10059
+ ...warnings,
10060
+ ...missingFieldWarnings(finalQuery, cached.data),
10061
+ ...keywordServingWarnings(cached.data, keywordServingOptions(finalQuery))
10062
+ ],
9907
10063
  args,
9908
10064
  true
9909
10065
  );
@@ -11388,19 +11544,19 @@ function failWriteValidation2(message) {
11388
11544
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11389
11545
  process.exit(1);
11390
11546
  }
11391
- function loadJsonFileArg2(path28) {
11392
- if (typeof path28 !== "string" || path28.length === 0) {
11547
+ function loadJsonFileArg2(path35) {
11548
+ if (typeof path35 !== "string" || path35.length === 0) {
11393
11549
  return {};
11394
11550
  }
11395
11551
  try {
11396
- const parsed = JSON.parse(readFileSync4(path28, "utf8"));
11552
+ const parsed = JSON.parse(readFileSync4(path35, "utf8"));
11397
11553
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11398
- failWriteValidation2(`${path28} must contain a JSON object`);
11554
+ failWriteValidation2(`${path35} must contain a JSON object`);
11399
11555
  }
11400
11556
  return parsed;
11401
11557
  } catch (err) {
11402
11558
  if (err instanceof SyntaxError) {
11403
- failWriteValidation2(`${path28} is not valid JSON: ${err.message}`);
11559
+ failWriteValidation2(`${path35} is not valid JSON: ${err.message}`);
11404
11560
  }
11405
11561
  throw err;
11406
11562
  }
@@ -11485,15 +11641,15 @@ function parseLocaleFlag(value) {
11485
11641
  }
11486
11642
  return { language: match[1], country: match[2].toUpperCase() };
11487
11643
  }
11488
- function loadTargetingFileArg(path28) {
11489
- if (typeof path28 !== "string" || path28.length === 0) {
11644
+ function loadTargetingFileArg(path35) {
11645
+ if (typeof path35 !== "string" || path35.length === 0) {
11490
11646
  return void 0;
11491
11647
  }
11492
- const parsed = loadJsonFileArg2(path28);
11648
+ const parsed = loadJsonFileArg2(path35);
11493
11649
  const criteria = parsed.targetingCriteria ?? parsed;
11494
11650
  if (!criteria.include) {
11495
11651
  failWriteValidation2(
11496
- `${path28} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11652
+ `${path35} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11497
11653
  );
11498
11654
  }
11499
11655
  return criteria;
@@ -11528,14 +11684,14 @@ function parseCsvLine(line) {
11528
11684
  cells.push(current);
11529
11685
  return cells.map((cell) => cell.trim());
11530
11686
  }
11531
- function parseListFileArg(path28, maxRows) {
11532
- if (typeof path28 !== "string" || path28.length === 0) {
11687
+ function parseListFileArg(path35, maxRows) {
11688
+ if (typeof path35 !== "string" || path35.length === 0) {
11533
11689
  return void 0;
11534
11690
  }
11535
- const raw = readFileSync4(path28, "utf8");
11691
+ const raw = readFileSync4(path35, "utf8");
11536
11692
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
11537
11693
  if (lines.length < 2) {
11538
- failWriteValidation2(`${path28} needs a header row and at least one data row`);
11694
+ failWriteValidation2(`${path35} needs a header row and at least one data row`);
11539
11695
  }
11540
11696
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
11541
11697
  const rows = [];
@@ -11554,7 +11710,7 @@ function parseListFileArg(path28, maxRows) {
11554
11710
  }
11555
11711
  }
11556
11712
  if (rows.length > maxRows) {
11557
- failWriteValidation2(`${path28} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
11713
+ failWriteValidation2(`${path35} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
11558
11714
  }
11559
11715
  return { columns, rows };
11560
11716
  }
@@ -11650,11 +11806,11 @@ function readPositionals(args) {
11650
11806
  function splitIdList(raw) {
11651
11807
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
11652
11808
  }
11653
- function idsFileEntries(path28) {
11654
- if (typeof path28 !== "string" || path28.length === 0) {
11809
+ function idsFileEntries(path35) {
11810
+ if (typeof path35 !== "string" || path35.length === 0) {
11655
11811
  return [];
11656
11812
  }
11657
- return readFileSync4(path28, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
11813
+ return readFileSync4(path35, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
11658
11814
  }
11659
11815
  function requireTargets(args, entity) {
11660
11816
  const positionals = readPositionals(args);
@@ -14277,9 +14433,9 @@ function compactRow(row) {
14277
14433
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
14278
14434
  };
14279
14435
  }
14280
- function readPath(row, path28) {
14436
+ function readPath(row, path35) {
14281
14437
  let current = row;
14282
- for (const segment of path28.split(".")) {
14438
+ for (const segment of path35.split(".")) {
14283
14439
  const record = asRecord2(current);
14284
14440
  if (!record) return void 0;
14285
14441
  current = record[segment];
@@ -14289,10 +14445,10 @@ function readPath(row, path28) {
14289
14445
  function projectFields(rows, paths) {
14290
14446
  return rows.map((row) => {
14291
14447
  const projected = {};
14292
- for (const path28 of paths) {
14293
- const value = readPath(row, path28);
14448
+ for (const path35 of paths) {
14449
+ const value = readPath(row, path35);
14294
14450
  if (value !== void 0) {
14295
- projected[path28] = value;
14451
+ projected[path35] = value;
14296
14452
  }
14297
14453
  }
14298
14454
  return projected;
@@ -15499,11 +15655,11 @@ var updateStatusSchema = z20.enum(UPDATE_STATUSES);
15499
15655
  function currencyMinimums2(currencyCode) {
15500
15656
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
15501
15657
  }
15502
- function validateDailyBudgetFloor(money, ctx, path28) {
15658
+ function validateDailyBudgetFloor(money, ctx, path35) {
15503
15659
  if (money?.currencyCode) {
15504
15660
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
15505
15661
  if (Number(money.amount) < min) {
15506
- ctx.addIssue({ code: "custom", path: path28, message: `below the ${min} ${money.currencyCode} daily minimum` });
15662
+ ctx.addIssue({ code: "custom", path: path35, message: `below the ${min} ${money.currencyCode} daily minimum` });
15507
15663
  }
15508
15664
  }
15509
15665
  }
@@ -16146,19 +16302,19 @@ function failWriteValidation3(message) {
16146
16302
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
16147
16303
  process.exit(1);
16148
16304
  }
16149
- function loadJsonFileArg3(path28) {
16150
- if (typeof path28 !== "string" || path28.length === 0) {
16305
+ function loadJsonFileArg3(path35) {
16306
+ if (typeof path35 !== "string" || path35.length === 0) {
16151
16307
  return {};
16152
16308
  }
16153
16309
  try {
16154
- const parsed = JSON.parse(readFileSync8(path28, "utf8"));
16310
+ const parsed = JSON.parse(readFileSync8(path35, "utf8"));
16155
16311
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16156
- failWriteValidation3(`${path28} must contain a JSON object`);
16312
+ failWriteValidation3(`${path35} must contain a JSON object`);
16157
16313
  }
16158
16314
  return parsed;
16159
16315
  } catch (err) {
16160
16316
  if (err instanceof SyntaxError) {
16161
- failWriteValidation3(`${path28} is not valid JSON: ${err.message}`);
16317
+ failWriteValidation3(`${path35} is not valid JSON: ${err.message}`);
16162
16318
  }
16163
16319
  throw err;
16164
16320
  }
@@ -20220,11 +20376,11 @@ function estSpeechS(text) {
20220
20376
  var OBSERVED_WPS_MIN = 1;
20221
20377
  var OBSERVED_WPS_MAX = 6;
20222
20378
  function estSpeechWindowS(text, startS, endS) {
20223
- const words2 = wordCount(text);
20224
- const window = (endS ?? 0) - (startS ?? 0);
20225
- if (words2 > 0 && window > 0.3) {
20226
- const wps = words2 / window;
20227
- if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window;
20379
+ const words3 = wordCount(text);
20380
+ const window2 = (endS ?? 0) - (startS ?? 0);
20381
+ if (words3 > 0 && window2 > 0.3) {
20382
+ const wps = words3 / window2;
20383
+ if (wps >= OBSERVED_WPS_MIN && wps <= OBSERVED_WPS_MAX) return window2;
20228
20384
  }
20229
20385
  return estSpeechS(text);
20230
20386
  }
@@ -21079,10 +21235,10 @@ function scrubFloatSentences(text, floatDescs) {
21079
21235
  if (floatDescs.length === 0 || !text) return text;
21080
21236
  const tokenSets = floatDescs.map((d) => new Set(floatTokens(d)));
21081
21237
  const kept = text.split(/(?<=[.!?])\s+/).filter((sentence) => {
21082
- const words2 = new Set(floatTokens(sentence));
21238
+ const words3 = new Set(floatTokens(sentence));
21083
21239
  return !tokenSets.some((ts) => {
21084
21240
  let hits = 0;
21085
- for (const w of words2) if (ts.has(w)) hits++;
21241
+ for (const w of words3) if (ts.has(w)) hits++;
21086
21242
  return hits >= 2;
21087
21243
  });
21088
21244
  }).join(" ").trim();
@@ -23273,15 +23429,15 @@ function collectClipAdvisories(scene, i, out) {
23273
23429
  const round22 = (n) => Math.round(n * 100) / 100;
23274
23430
  const original = scene.duration_s ?? 5;
23275
23431
  if (original > 15) out.clamped.push({ scene: i, original_s: original, clip_s: snapToSeedance(original) });
23276
- const window = sceneDurationS(scene);
23277
- if (window > SEEDANCE_SAFE_MAX_S)
23278
- out.oversize.push({ scene: i, scene_s: round22(window), clip_s: ceilToSeedance(window) });
23432
+ const window2 = sceneDurationS(scene);
23433
+ if (window2 > SEEDANCE_SAFE_MAX_S)
23434
+ out.oversize.push({ scene: i, scene_s: round22(window2), clip_s: ceilToSeedance(window2) });
23279
23435
  const speech = (scene.dialogue ?? []).reduce(
23280
23436
  (s, line) => s + (line.line ? estSpeechWindowS(line.line, line.start_s, line.end_s) : 0),
23281
23437
  0
23282
23438
  );
23283
- if (speech > window * OVERSTUFF_RATIO)
23284
- out.overstuffed.push({ scene: i, scene_s: round22(window), est_speech_s: round22(speech) });
23439
+ if (speech > window2 * OVERSTUFF_RATIO)
23440
+ out.overstuffed.push({ scene: i, scene_s: round22(window2), est_speech_s: round22(speech) });
23285
23441
  }
23286
23442
  function videoReport(input, elementsInput) {
23287
23443
  const blueprint = VideoBlueprint.parse(input);
@@ -26957,12 +27113,12 @@ function listFlowSlugs() {
26957
27113
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
26958
27114
  }
26959
27115
  function readFlowTree(slug) {
26960
- const path28 = join3(flowsDir(), slug, "_data.json");
26961
- if (!existsSync4(path28)) {
27116
+ const path35 = join3(flowsDir(), slug, "_data.json");
27117
+ if (!existsSync4(path35)) {
26962
27118
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
26963
27119
  }
26964
27120
  try {
26965
- return JSON.parse(readFileSync9(path28, "utf-8"));
27121
+ return JSON.parse(readFileSync9(path35, "utf-8"));
26966
27122
  } catch (error) {
26967
27123
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
26968
27124
  }
@@ -27375,10 +27531,10 @@ async function stageOps(ops) {
27375
27531
  handleError(err);
27376
27532
  }
27377
27533
  }
27378
- async function draftAction2(path28, body, chat) {
27534
+ async function draftAction2(path35, body, chat) {
27379
27535
  const chatId = resolveChatId(chat);
27380
27536
  try {
27381
- const data = await apiPost(path28, { chatId, ...body });
27537
+ const data = await apiPost(path35, { chatId, ...body });
27382
27538
  writeJsonEnvelope({ ok: true, data });
27383
27539
  return data;
27384
27540
  } catch (err) {
@@ -29402,9 +29558,9 @@ async function readImageBuffer(pathOrUrl) {
29402
29558
  }
29403
29559
  return readFile19(pathOrUrl);
29404
29560
  }
29405
- async function isDirectory(path28) {
29561
+ async function isDirectory(path35) {
29406
29562
  try {
29407
- const s = await stat4(path28);
29563
+ const s = await stat4(path35);
29408
29564
  return s.isDirectory();
29409
29565
  } catch {
29410
29566
  return false;
@@ -31942,11 +32098,11 @@ Full guide: __tooling__/docs/tools/baker/images.md`
31942
32098
  });
31943
32099
 
31944
32100
  // src/commands/landing/index.ts
31945
- import { defineCommand as defineCommand143 } from "citty";
32101
+ import { defineCommand as defineCommand151 } from "citty";
31946
32102
 
31947
32103
  // src/commands/landing/critique.ts
31948
32104
  import { readdir as readdir8, stat as stat6 } from "fs/promises";
31949
- import path27 from "path";
32105
+ import path28 from "path";
31950
32106
  import { defineCommand as defineCommand142 } from "citty";
31951
32107
 
31952
32108
  // src/engine/landing/lib/brand-tokens.ts
@@ -32355,6 +32511,11 @@ var RULE_META = {
32355
32511
  severity: "block",
32356
32512
  note: "Gradient text is a top AI tell. Emphasis comes from weight or size, not a clipped gradient fill."
32357
32513
  },
32514
+ "copied-reference-copy": {
32515
+ family: "originality",
32516
+ severity: "block",
32517
+ 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."
32518
+ },
32358
32519
  "broken-image": {
32359
32520
  family: "integrity",
32360
32521
  severity: "block",
@@ -32535,6 +32696,64 @@ var SEVERITY_WEIGHT = {
32535
32696
  advisory: 0.05
32536
32697
  };
32537
32698
 
32699
+ // src/engine/landing/lib/originality.ts
32700
+ var MIN_COMPARABLE_LENGTH = 12;
32701
+ var NEAR_MATCH_RATIO = 0.8;
32702
+ function normalize(value) {
32703
+ return value.toLowerCase().replace(/[‘’“”]/g, "'").replace(/[^a-z0-9']+/g, " ").trim();
32704
+ }
32705
+ function words2(value) {
32706
+ return normalize(value).split(" ").filter(Boolean);
32707
+ }
32708
+ function copyOverlapRatio(candidate, reference) {
32709
+ const referenceWords = words2(reference);
32710
+ if (referenceWords.length === 0) return 0;
32711
+ const candidateWords = new Set(words2(candidate));
32712
+ const shared = referenceWords.filter((word) => candidateWords.has(word)).length;
32713
+ return shared / referenceWords.length;
32714
+ }
32715
+ function isVerbatimReuse(candidate, reference) {
32716
+ const normalizedCandidate = normalize(candidate);
32717
+ const normalizedReference = normalize(reference);
32718
+ if (normalizedReference.length < MIN_COMPARABLE_LENGTH) return false;
32719
+ if (normalizedCandidate.includes(normalizedReference)) return true;
32720
+ return copyOverlapRatio(candidate, reference) >= NEAR_MATCH_RATIO;
32721
+ }
32722
+ function extractVisibleStrings(text) {
32723
+ const found = [];
32724
+ const lines = text.split("\n");
32725
+ for (const [index, line] of lines.entries()) {
32726
+ for (const match of line.matchAll(/>([^<>{}]{12,200})</g)) {
32727
+ const value = match[1]?.trim();
32728
+ if (value && /[a-zA-Z]{3}/.test(value)) found.push({ value, line: index + 1 });
32729
+ }
32730
+ }
32731
+ return found;
32732
+ }
32733
+ function detectOriginality(sources, references) {
32734
+ if (references.length === 0) return [];
32735
+ const findings = [];
32736
+ const seen = /* @__PURE__ */ new Set();
32737
+ for (const source of sources) {
32738
+ for (const { value, line } of extractVisibleStrings(source.text)) {
32739
+ for (const reference of references) {
32740
+ const hit = reference.copyStrings.find((copy) => isVerbatimReuse(value, copy));
32741
+ if (!hit) continue;
32742
+ const key = `${source.path}:${line}:${normalize(hit)}`;
32743
+ if (seen.has(key)) continue;
32744
+ seen.add(key);
32745
+ findings.push({
32746
+ id: "copied-reference-copy",
32747
+ snippet: value.slice(0, 120),
32748
+ file: source.path,
32749
+ line
32750
+ });
32751
+ }
32752
+ }
32753
+ }
32754
+ return findings;
32755
+ }
32756
+
32538
32757
  // src/engine/landing/lib/rules.ts
32539
32758
  var cap2 = (m, i) => m[i] ?? "";
32540
32759
  var num = (m, i) => Number(m[i] ?? 0);
@@ -32939,8 +33158,8 @@ var ANALYZERS = [
32939
33158
  const lines = text.split("\n");
32940
33159
  for (const m of text.matchAll(/(?:-webkit-)?background-clip\s*:\s*text/gi)) {
32941
33160
  const line = lineOf(text, m.index ?? 0);
32942
- const window = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
32943
- if (/gradient\(/i.test(window)) {
33161
+ const window2 = lines.slice(Math.max(0, line - 7), Math.min(lines.length, line + 6)).join("\n");
33162
+ if (/gradient\(/i.test(window2)) {
32944
33163
  out.push({ id: "gradient-text", snippet: "background-clip: text + gradient", file, line });
32945
33164
  }
32946
33165
  }
@@ -33062,7 +33281,16 @@ function dedupe(findings) {
33062
33281
  }
33063
33282
 
33064
33283
  // src/engine/landing/lib/critique.ts
33065
- var FAMILIES = ["typography", "color", "borders_depth", "motion", "spacing", "copy", "integrity"];
33284
+ var FAMILIES = [
33285
+ "typography",
33286
+ "color",
33287
+ "borders_depth",
33288
+ "motion",
33289
+ "spacing",
33290
+ "copy",
33291
+ "integrity",
33292
+ "originality"
33293
+ ];
33066
33294
  function round4(n) {
33067
33295
  return Math.round(n * 100) / 100;
33068
33296
  }
@@ -33081,6 +33309,7 @@ function critiqueLanding(input) {
33081
33309
  const raws = [];
33082
33310
  for (const source of sources) raws.push(...detectSource(source));
33083
33311
  raws.push(...detectPage(sources));
33312
+ raws.push(...detectOriginality(sources, input.references ?? []));
33084
33313
  for (const raw of raws) {
33085
33314
  const meta = RULE_META[raw.id];
33086
33315
  if (!meta) continue;
@@ -33118,41 +33347,82 @@ function describeCounts(findings) {
33118
33347
  return [b ? `${b} block` : "", w ? `${w} warn` : "", a ? `${a} advisory` : ""].filter(Boolean).join(", ");
33119
33348
  }
33120
33349
 
33121
- // src/commands/landing/snapshot.ts
33122
- import { mkdir as mkdir8, rename as rename2, writeFile as writeFile11 } from "fs/promises";
33350
+ // src/engine/landing/lib/referenceStore.ts
33351
+ import { mkdir as mkdir8, readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
33123
33352
  import path25 from "path";
33353
+ var REFERENCES_FILE = ".cache/inspiration-refs.json";
33354
+ var REFERENCE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
33355
+ async function readReferences(projectRoot) {
33356
+ try {
33357
+ const raw = await readFile21(path25.join(projectRoot, REFERENCES_FILE), "utf8");
33358
+ const parsed = JSON.parse(raw);
33359
+ if (!Array.isArray(parsed)) return [];
33360
+ const cutoff = Date.now() - REFERENCE_TTL_MS;
33361
+ return parsed.filter((entry) => {
33362
+ if (typeof entry !== "object" || entry === null) return false;
33363
+ const candidate = entry;
33364
+ if (typeof candidate.sectionId !== "string" || !Array.isArray(candidate.copyStrings)) return false;
33365
+ const at = Date.parse(candidate.consultedAt ?? "");
33366
+ return Number.isNaN(at) ? true : at >= cutoff;
33367
+ });
33368
+ } catch {
33369
+ return [];
33370
+ }
33371
+ }
33372
+ async function recordReference(projectRoot, reference) {
33373
+ return await recordReferences(projectRoot, [reference]);
33374
+ }
33375
+ async function recordReferences(projectRoot, references) {
33376
+ if (references.length === 0) return true;
33377
+ try {
33378
+ const existing = await readReferences(projectRoot);
33379
+ const replaced = new Set(references.map((reference) => reference.sectionId));
33380
+ const merged = [...existing.filter((entry) => !replaced.has(entry.sectionId)), ...references];
33381
+ const file = path25.join(projectRoot, REFERENCES_FILE);
33382
+ await mkdir8(path25.dirname(file), { recursive: true });
33383
+ await writeFile11(file, `${JSON.stringify(merged, null, 2)}
33384
+ `);
33385
+ return true;
33386
+ } catch {
33387
+ return false;
33388
+ }
33389
+ }
33390
+
33391
+ // src/commands/landing/snapshot.ts
33392
+ import { mkdir as mkdir9, rename as rename2, writeFile as writeFile12 } from "fs/promises";
33393
+ import path26 from "path";
33124
33394
  var CRITIC_VERSION = "2";
33125
33395
  function critiqueCacheDir(projectRoot) {
33126
- return path25.join(projectRoot, ".cache", "landing-critique");
33396
+ return path26.join(projectRoot, ".cache", "landing-critique");
33127
33397
  }
33128
33398
  function snapshotPath(projectRoot, slug) {
33129
- return path25.join(critiqueCacheDir(projectRoot), `${slug}.json`);
33399
+ return path26.join(critiqueCacheDir(projectRoot), `${slug}.json`);
33130
33400
  }
33131
33401
  async function writeCritiqueSnapshot(projectRoot, snapshot) {
33132
- await mkdir8(critiqueCacheDir(projectRoot), { recursive: true });
33402
+ await mkdir9(critiqueCacheDir(projectRoot), { recursive: true });
33133
33403
  const dest = snapshotPath(projectRoot, snapshot.slug);
33134
33404
  const tmp = `${dest}.tmp`;
33135
- await writeFile11(tmp, `${JSON.stringify(snapshot, null, 2)}
33405
+ await writeFile12(tmp, `${JSON.stringify(snapshot, null, 2)}
33136
33406
  `, "utf8");
33137
33407
  await rename2(tmp, dest);
33138
33408
  }
33139
33409
 
33140
33410
  // src/commands/landing/source-version.ts
33141
- import { readdir as readdir7, readFile as readFile21, stat as stat5 } from "fs/promises";
33142
- import path26 from "path";
33411
+ import { readdir as readdir7, readFile as readFile22, stat as stat5 } from "fs/promises";
33412
+ import path27 from "path";
33143
33413
  async function landingSourceRelPaths(landingDir) {
33144
33414
  const rel = [];
33145
- if (await isFile(path26.join(landingDir, "index.astro"))) rel.push("index.astro");
33146
- const componentsDir = path26.join(landingDir, "_components");
33415
+ if (await isFile(path27.join(landingDir, "index.astro"))) rel.push("index.astro");
33416
+ const componentsDir = path27.join(landingDir, "_components");
33147
33417
  for (const abs of await walkAstro(componentsDir)) {
33148
- rel.push(path26.relative(landingDir, abs).split(path26.sep).join("/"));
33418
+ rel.push(path27.relative(landingDir, abs).split(path27.sep).join("/"));
33149
33419
  }
33150
33420
  return rel.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
33151
33421
  }
33152
33422
  async function readLandingSources(landingDir) {
33153
33423
  const rel = await landingSourceRelPaths(landingDir);
33154
33424
  const out = [];
33155
- for (const r of rel) out.push({ path: r, text: await readFile21(path26.join(landingDir, r), "utf8") });
33425
+ for (const r of rel) out.push({ path: r, text: await readFile22(path27.join(landingDir, r), "utf8") });
33156
33426
  return out;
33157
33427
  }
33158
33428
  async function computeLandingSourceSha(landingDir) {
@@ -33161,7 +33431,7 @@ async function computeLandingSourceSha(landingDir) {
33161
33431
  for (const r of rel) {
33162
33432
  let bytes;
33163
33433
  try {
33164
- bytes = await readFile21(path26.join(landingDir, r));
33434
+ bytes = await readFile22(path27.join(landingDir, r));
33165
33435
  } catch {
33166
33436
  bytes = Buffer.alloc(0);
33167
33437
  }
@@ -33185,7 +33455,7 @@ async function walkAstro(dir) {
33185
33455
  }
33186
33456
  const out = [];
33187
33457
  for (const entry of entries) {
33188
- const abs = path26.join(dir, entry.name);
33458
+ const abs = path27.join(dir, entry.name);
33189
33459
  if (entry.isDirectory()) out.push(...await walkAstro(abs));
33190
33460
  else if (entry.isFile() && entry.name.endsWith(".astro")) out.push(abs);
33191
33461
  }
@@ -33246,14 +33516,14 @@ var critiqueCommand2 = defineCommand142({
33246
33516
  { availableSlugs: await listLandingSlugs(projectRoot) }
33247
33517
  );
33248
33518
  }
33249
- if (!await isDir(path27.resolve(projectRoot, "src", "pages", slug))) {
33519
+ if (!await isDir(path28.resolve(projectRoot, "src", "pages", slug))) {
33250
33520
  fail5("NOT_FOUND", `No landing at src/pages/${slug}/`, {
33251
33521
  availableSlugs: await listLandingSlugs(projectRoot)
33252
33522
  });
33253
33523
  }
33254
33524
  }
33255
- const brand = await loadBrandTokens(projectRoot);
33256
- const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand)));
33525
+ const [brand, references] = await Promise.all([loadBrandTokens(projectRoot), readReferences(projectRoot)]);
33526
+ const results = await Promise.all(slugs.map((slug) => critiqueOne(projectRoot, slug, brand, references)));
33257
33527
  const landings = results.map(({ slug, report }) => ({
33258
33528
  slug,
33259
33529
  overall: report.overall,
@@ -33284,10 +33554,10 @@ var critiqueCommand2 = defineCommand142({
33284
33554
  );
33285
33555
  }
33286
33556
  });
33287
- async function critiqueOne(projectRoot, slug, brand) {
33288
- const landingDir = path27.resolve(projectRoot, "src", "pages", slug);
33557
+ async function critiqueOne(projectRoot, slug, brand, references) {
33558
+ const landingDir = path28.resolve(projectRoot, "src", "pages", slug);
33289
33559
  const [sources, sourceSha] = await Promise.all([readLandingSources(landingDir), computeLandingSourceSha(landingDir)]);
33290
- const report = critiqueLanding({ slug, sources, brand });
33560
+ const report = critiqueLanding({ slug, sources, brand, references });
33291
33561
  let snapshotFailed = false;
33292
33562
  try {
33293
33563
  await writeCritiqueSnapshot(projectRoot, {
@@ -33305,7 +33575,7 @@ async function critiqueOne(projectRoot, slug, brand) {
33305
33575
  }
33306
33576
  async function listLandingSlugs(projectRoot) {
33307
33577
  try {
33308
- const entries = await readdir8(path27.join(projectRoot, "src", "pages"), { withFileTypes: true });
33578
+ const entries = await readdir8(path28.join(projectRoot, "src", "pages"), { withFileTypes: true });
33309
33579
  return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
33310
33580
  } catch {
33311
33581
  return [];
@@ -33330,8 +33600,2531 @@ async function isDir(p) {
33330
33600
  }
33331
33601
  }
33332
33602
 
33603
+ // src/commands/landing/inspiration/index.ts
33604
+ import { defineCommand as defineCommand150 } from "citty";
33605
+
33606
+ // src/commands/landing/inspiration/add.ts
33607
+ import { defineCommand as defineCommand143 } from "citty";
33608
+
33609
+ // src/commands/landing/inspiration/shared.ts
33610
+ var INSPIRATION_HINTS = {
33611
+ 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.",
33612
+ 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."
33613
+ };
33614
+ function fidelityHint(fidelity) {
33615
+ if (fidelity == null) return null;
33616
+ if (fidelity >= 0.9) return null;
33617
+ if (fidelity >= 0.75) {
33618
+ return `Reproduction fidelity ${fidelity.toFixed(2)} \u2014 the markup is close but not exact. Trust the screenshot over the code.`;
33619
+ }
33620
+ 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.`;
33621
+ }
33622
+ function reportError(error) {
33623
+ if (error instanceof ApiError) {
33624
+ writeJson({ ok: false, error: { code: error.code, message: error.message } });
33625
+ } else {
33626
+ writeJson({
33627
+ ok: false,
33628
+ error: { code: "UNKNOWN", message: error instanceof Error ? error.message : String(error) }
33629
+ });
33630
+ }
33631
+ process.exit(1);
33632
+ }
33633
+ function splitList(value) {
33634
+ if (!value) return void 0;
33635
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
33636
+ return parts.length > 0 ? parts : void 0;
33637
+ }
33638
+ function parseNumber(value) {
33639
+ if (value === void 0 || value === "") return void 0;
33640
+ const parsed = Number(value);
33641
+ return Number.isFinite(parsed) ? parsed : void 0;
33642
+ }
33643
+
33644
+ // src/commands/landing/inspiration/add.ts
33645
+ registerSchema({
33646
+ command: "landing.inspiration.add",
33647
+ description: "Add someone else's 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. Never point it at a page we built: it is refused, because the source is already in the project and reading a screenshot of it tells you less than opening the file.",
33648
+ args: {
33649
+ url: { type: "string", description: "Landing page address", required: true },
33650
+ note: { type: "string", description: "Why this page is worth keeping", required: false }
33651
+ }
33652
+ });
33653
+ var addCommand = defineCommand143({
33654
+ meta: {
33655
+ name: "add",
33656
+ description: "Add someone else's landing page to the reference library. Example: baker landing inspiration add https://linear.app --note 'the client likes this density'"
33657
+ },
33658
+ args: {
33659
+ url: { type: "positional", description: "Landing page address", required: true },
33660
+ note: { type: "string", description: "Why this page is worth keeping", required: false }
33661
+ },
33662
+ run: async ({ args }) => {
33663
+ try {
33664
+ const data = await apiPost("/api/landing-inspiration/add", {
33665
+ url: args.url,
33666
+ note: args.note,
33667
+ favorite: true
33668
+ });
33669
+ const hints = data.alreadyKnown ? [
33670
+ "This page was already in the library, so nothing was re-studied \u2014 its sections are searchable now.",
33671
+ `Search it with: baker landing inspiration search --domain ${new URL(data.canonicalUrl).hostname}`
33672
+ ] : [
33673
+ "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.",
33674
+ "Meanwhile, search what is already in the library: baker landing inspiration search '<what you want to see>' --scope all",
33675
+ "This is saved and being studied now, so it is listed as already applied \u2014 tell the user it is kept whatever they do with this chat."
33676
+ ];
33677
+ writeJson({
33678
+ ok: true,
33679
+ data: { id: data.sourceId, url: data.canonicalUrl, status: data.status, already_known: data.alreadyKnown },
33680
+ hints
33681
+ });
33682
+ } catch (error) {
33683
+ reportError(error);
33684
+ }
33685
+ }
33686
+ });
33687
+
33688
+ // src/commands/landing/inspiration/code.ts
33689
+ import { mkdir as mkdir10, writeFile as writeFile13 } from "fs/promises";
33690
+ import path29 from "path";
33691
+ import { defineCommand as defineCommand144 } from "citty";
33692
+ registerSchema({
33693
+ command: "landing.inspiration.code",
33694
+ 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.",
33695
+ args: {
33696
+ id: { type: "string", description: "Section id from search", required: true },
33697
+ full: { type: "boolean", description: "Print the markup inline as well as writing it", required: false }
33698
+ }
33699
+ });
33700
+ var codeCommand = defineCommand144({
33701
+ meta: {
33702
+ name: "code",
33703
+ 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."
33704
+ },
33705
+ args: {
33706
+ id: { type: "positional", description: "Section id from search", required: true },
33707
+ full: { type: "boolean", description: "Also print the markup inline", required: false, default: false }
33708
+ },
33709
+ run: async ({ args }) => {
33710
+ try {
33711
+ const id = args.id;
33712
+ const data = await apiGet("/api/landing-inspiration/section-code", { id });
33713
+ const dir = path29.join(process.cwd(), ".baker", "inspiration", id);
33714
+ await mkdir10(dir, { recursive: true });
33715
+ const file = path29.join(dir, "section.html");
33716
+ await writeFile13(file, data.html);
33717
+ const recorded = await recordReference(process.cwd(), {
33718
+ sectionId: id,
33719
+ sourceUrl: data.sourceUrl,
33720
+ copyStrings: data.copyStrings,
33721
+ consultedAt: (/* @__PURE__ */ new Date()).toISOString()
33722
+ });
33723
+ const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
33724
+ const fidelity = fidelityHint(data.fidelity);
33725
+ if (fidelity) hints.push(fidelity);
33726
+ if (data.fidelityNote) hints.push(data.fidelityNote);
33727
+ if (!recorded) {
33728
+ hints.push(
33729
+ "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."
33730
+ );
33731
+ }
33732
+ writeJson({
33733
+ ok: true,
33734
+ data: {
33735
+ id,
33736
+ file: path29.relative(process.cwd(), file),
33737
+ bytes: data.html.length,
33738
+ fidelity: data.fidelity,
33739
+ reproduction_notes: data.reproductionNotes,
33740
+ adaptation_notes: data.adaptationNotes,
33741
+ source_url: data.sourceUrl,
33742
+ // Behind --full with the markup it belongs to. Up to 60 custom-property
33743
+ // *names*, one per line, was over half the default response — and the
33744
+ // file on disk already has them, next to the values that give them
33745
+ // meaning. A list of variable names on its own decides nothing.
33746
+ ...args.full ? { css_custom_properties: data.cssCustomProperties, html: data.html } : {}
33747
+ },
33748
+ hints
33749
+ });
33750
+ } catch (error) {
33751
+ reportError(error);
33752
+ }
33753
+ }
33754
+ });
33755
+
33756
+ // src/commands/landing/inspiration/favorites.ts
33757
+ import { defineCommand as defineCommand145 } from "citty";
33758
+ registerSchema({
33759
+ command: "landing.inspiration.favorites",
33760
+ 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.",
33761
+ args: {
33762
+ type: { type: "string", description: "Comma list of section types to filter by", required: false },
33763
+ limit: { type: "number", description: "Max results (default 30, capped at 100)", required: false },
33764
+ full: {
33765
+ type: "boolean",
33766
+ description: "Add the transferable idea behind each saved section. Costs roughly 60% more; read the list first",
33767
+ required: false
33768
+ }
33769
+ }
33770
+ });
33771
+ var favoritesCommand = defineCommand145({
33772
+ meta: {
33773
+ name: "favorites",
33774
+ description: "List this company's saved reference sections. Example: baker landing inspiration favorites --type hero,pricing"
33775
+ },
33776
+ args: {
33777
+ type: { type: "string", description: "Comma list of section types", required: false },
33778
+ limit: { type: "string", description: "Max results (default 30, capped at 100)", required: false },
33779
+ full: { type: "boolean", description: "Include why_it_works per section", required: false, default: false }
33780
+ },
33781
+ run: async ({ args }) => {
33782
+ try {
33783
+ const params = {};
33784
+ const types = splitList(args.type);
33785
+ if (types) params.type = types.join(",");
33786
+ const limit = parseNumber(args.limit);
33787
+ if (limit !== void 0) params.limit = String(limit);
33788
+ const data = await apiGet("/api/landing-inspiration/favorites", params);
33789
+ const sections = Array.isArray(data?.sections) ? data.sections : [];
33790
+ const full = args.full;
33791
+ writeJson({
33792
+ ok: true,
33793
+ data: {
33794
+ sections: sections.map((section) => ({
33795
+ id: section.id,
33796
+ page_id: section.sourceId,
33797
+ section: section.sectionType,
33798
+ composition: section.composition,
33799
+ look: section.visualRegister,
33800
+ domain: section.domain,
33801
+ // Behind --full: this is a 600-character paragraph, and at the
33802
+ // default 30 rows it was about 60% of the response. Reading a taste
33803
+ // profile is a question about *what* the client keeps saving —
33804
+ // types, compositions, which sites — and the essay per row answers
33805
+ // a question you only have once you have picked a row.
33806
+ ...full ? { why_it_works: section.whyItWorks } : {}
33807
+ }))
33808
+ },
33809
+ hints: sections.length === 0 ? [
33810
+ "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`."
33811
+ ] : [
33812
+ "These are the client's taste signals \u2014 read them as direction, not as a component library.",
33813
+ ...full ? [] : ["Add --full for the transferable idea behind each one."],
33814
+ INSPIRATION_HINTS.adapt
33815
+ ]
33816
+ });
33817
+ } catch (error) {
33818
+ reportError(error);
33819
+ }
33820
+ }
33821
+ });
33822
+ registerSchema({
33823
+ command: "landing.inspiration.favorite",
33824
+ description: "Save a reference section (or a whole page) to this company, so it shows up in the default search scope.",
33825
+ args: {
33826
+ id: { type: "string", description: "Section id, or page id with --page", required: true },
33827
+ page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false },
33828
+ note: { type: "string", description: "Why this is worth keeping", required: false }
33829
+ }
33830
+ });
33831
+ var favoriteCommand = defineCommand145({
33832
+ meta: {
33833
+ name: "favorite",
33834
+ description: "Save a reference section to this company. Example: baker landing inspiration favorite k57abc\u2026"
33835
+ },
33836
+ args: {
33837
+ id: { type: "positional", description: "Section id (or page id with --page)", required: true },
33838
+ page: { type: "boolean", description: "Treat the id as a page", required: false, default: false },
33839
+ note: { type: "string", description: "Why this is worth keeping", required: false }
33840
+ },
33841
+ run: async ({ args }) => {
33842
+ try {
33843
+ const id = args.id;
33844
+ const body = args.page ? { sourceId: id } : { sectionId: id };
33845
+ const data = await apiPost("/api/landing-inspiration/favorite", {
33846
+ ...body,
33847
+ note: args.note
33848
+ });
33849
+ writeJson({
33850
+ ok: true,
33851
+ data: { id, favorited: data.favorited },
33852
+ hints: [
33853
+ "Saved. `search` looks at this company's saved sections by default, so this now shows up without --scope all."
33854
+ ]
33855
+ });
33856
+ } catch (error) {
33857
+ reportError(error);
33858
+ }
33859
+ }
33860
+ });
33861
+ registerSchema({
33862
+ command: "landing.inspiration.unfavorite",
33863
+ description: "Remove a reference section (or page) from this company's saved set.",
33864
+ args: {
33865
+ id: { type: "string", description: "Section id, or page id with --page", required: true },
33866
+ page: { type: "boolean", description: "Treat the id as a page rather than a section", required: false }
33867
+ }
33868
+ });
33869
+ var unfavoriteCommand = defineCommand145({
33870
+ meta: {
33871
+ name: "unfavorite",
33872
+ description: "Remove a reference section from this company's saved set. Example: baker landing inspiration unfavorite k57abc\u2026"
33873
+ },
33874
+ args: {
33875
+ id: { type: "positional", description: "Section id (or page id with --page)", required: true },
33876
+ page: { type: "boolean", description: "Treat the id as a page", required: false, default: false }
33877
+ },
33878
+ run: async ({ args }) => {
33879
+ try {
33880
+ const id = args.id;
33881
+ const body = args.page ? { sourceId: id } : { sectionId: id };
33882
+ const data = await apiPost("/api/landing-inspiration/unfavorite", body);
33883
+ writeJson({ ok: true, data: { id, favorited: data.favorited } });
33884
+ } catch (error) {
33885
+ reportError(error);
33886
+ }
33887
+ }
33888
+ });
33889
+
33890
+ // src/commands/landing/inspiration/page.ts
33891
+ import { defineCommand as defineCommand146 } from "citty";
33892
+ registerSchema({
33893
+ command: "landing.inspiration.page",
33894
+ 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.",
33895
+ args: {
33896
+ id: {
33897
+ type: "string",
33898
+ description: "Page id \u2014 the `page_id` on any search result or favorites row",
33899
+ required: true
33900
+ },
33901
+ full: {
33902
+ type: "boolean",
33903
+ description: "Add the transferable idea behind every section. A long page roughly triples the response",
33904
+ required: false
33905
+ }
33906
+ }
33907
+ });
33908
+ var pageCommand = defineCommand146({
33909
+ meta: {
33910
+ name: "page",
33911
+ description: "Show how a reference page sequences its sections. Example: baker landing inspiration page j91xyz\u2026 \u2014 the blueprint, not the pixels."
33912
+ },
33913
+ args: {
33914
+ id: { type: "positional", description: "Page id (the `page_id` on a search result)", required: true },
33915
+ full: { type: "boolean", description: "Include why_it_works per section", required: false, default: false }
33916
+ },
33917
+ run: async ({ args }) => {
33918
+ try {
33919
+ const data = await apiGet("/api/landing-inspiration/page", { id: args.id });
33920
+ if (data.status !== "indexed") {
33921
+ writeJson({
33922
+ ok: true,
33923
+ data: { id: data.id, url: data.url, status: data.status, sections: [] },
33924
+ hints: [
33925
+ 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."
33926
+ ]
33927
+ });
33928
+ return;
33929
+ }
33930
+ const full = args.full;
33931
+ writeJson({
33932
+ ok: true,
33933
+ data: {
33934
+ id: data.id,
33935
+ url: data.url,
33936
+ domain: data.domain,
33937
+ title: data.title,
33938
+ archetype: data.archetype,
33939
+ stack: data.detectedStack,
33940
+ page_fidelity: data.pageFidelity,
33941
+ blueprint: data.sectionOrder,
33942
+ sections: data.sections.map((section) => ({
33943
+ id: section.id,
33944
+ position: section.index,
33945
+ section: section.sectionType,
33946
+ composition: section.composition,
33947
+ look: section.visualRegister,
33948
+ motion: section.motionSummary,
33949
+ craft: section.craftScore,
33950
+ // Behind --full. This command exists to answer an ordering
33951
+ // question, and `blueprint` answers it in a line; a 600-character
33952
+ // paragraph per section turned a 40-section page into ~5,900 tokens
33953
+ // of material about individual sections nobody had chosen yet.
33954
+ ...full ? { why_it_works: section.whyItWorks } : {}
33955
+ }))
33956
+ },
33957
+ hints: [
33958
+ "The blueprint is the section order top to bottom \u2014 the answer to 'how do good pages in this category sequence themselves'.",
33959
+ "Use `baker landing inspiration view <section id>` for any section worth a closer look.",
33960
+ ...full ? [] : ["Add --full for the idea behind every section, once you know which page you care about."],
33961
+ INSPIRATION_HINTS.adapt
33962
+ ]
33963
+ });
33964
+ } catch (error) {
33965
+ reportError(error);
33966
+ }
33967
+ }
33968
+ });
33969
+
33970
+ // src/commands/landing/inspiration/scrape.ts
33971
+ import { readFile as readFile23 } from "fs/promises";
33972
+ import path32 from "path";
33973
+ import { defineCommand as defineCommand147 } from "citty";
33974
+
33975
+ // src/engine/landing/lib/capturedReferences.ts
33976
+ var MAX_STRINGS_PER_SECTION = 40;
33977
+ function bodyOf(markup) {
33978
+ const body = markup.indexOf("<body");
33979
+ return body === -1 ? markup : markup.slice(body);
33980
+ }
33981
+ function capturedCopyStrings(markup) {
33982
+ const seen = /* @__PURE__ */ new Set();
33983
+ for (const { value } of extractVisibleStrings(bodyOf(markup))) {
33984
+ seen.add(value);
33985
+ if (seen.size >= MAX_STRINGS_PER_SECTION) break;
33986
+ }
33987
+ return [...seen];
33988
+ }
33989
+
33990
+ // src/engine/landing-library/blocked.ts
33991
+ var CHALLENGE_PHRASES = [
33992
+ "just a moment",
33993
+ "attention required",
33994
+ "verify you are human",
33995
+ "checking your browser",
33996
+ "enable javascript and cookies to continue",
33997
+ "unusual traffic",
33998
+ "access denied",
33999
+ "you have been blocked",
34000
+ "request unsuccessful",
34001
+ "are you a robot",
34002
+ "security check",
34003
+ "ddos protection",
34004
+ "captcha"
34005
+ ];
34006
+ var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
34007
+ function detectBlockedPage(page) {
34008
+ if (page.status !== null && page.status >= 400) {
34009
+ return {
34010
+ code: "HTTP_ERROR",
34011
+ message: `The site returned ${page.status} instead of the page \u2014 it may be blocking automated visits.`
34012
+ };
34013
+ }
34014
+ const haystack = `${page.title}
34015
+ ${page.bodyText.slice(0, 2e3)}`.toLowerCase();
34016
+ const phrase = CHALLENGE_PHRASES.find((candidate) => haystack.includes(candidate));
34017
+ if (phrase) {
34018
+ return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
34019
+ }
34020
+ const html = page.html?.toLowerCase() ?? "";
34021
+ if (CHALLENGE_MARKERS.some((marker) => html.includes(marker))) {
34022
+ return { code: "BOT_CHALLENGE", message: "The site showed a security check instead of the page." };
34023
+ }
34024
+ return null;
34025
+ }
34026
+ var BlockedPageError = class extends Error {
34027
+ code;
34028
+ constructor(blocked) {
34029
+ super(blocked.message);
34030
+ this.name = "BlockedPageError";
34031
+ this.code = blocked.code;
34032
+ }
34033
+ };
34034
+
34035
+ // src/engine/landing-library/run.ts
34036
+ import { mkdir as mkdir11, writeFile as writeFile15 } from "fs/promises";
34037
+ import path31 from "path";
34038
+
34039
+ // src/engine/landing-library/browser.ts
34040
+ import { createRequire } from "module";
34041
+ var require_ = createRequire(import.meta.url);
34042
+ var pwSpecifier = ["play", "wright"].join("");
34043
+ var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
34044
+ var MOBILE_VIEWPORT = { width: 390, height: 844 };
34045
+ var DEVICE_SCALE_FACTOR = 2;
34046
+ async function launchBrowser() {
34047
+ const playwright = require_(pwSpecifier);
34048
+ return await playwright.chromium.launch({
34049
+ headless: true,
34050
+ args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
34051
+ });
34052
+ }
34053
+ async function newPage(browser, viewport, opts = { motion: false }) {
34054
+ const context = await browser.newContext({
34055
+ viewport,
34056
+ deviceScaleFactor: DEVICE_SCALE_FACTOR,
34057
+ // Still captures freeze motion so screenshots are reproducible; the motion
34058
+ // pass re-opens a context with animation enabled.
34059
+ reducedMotion: opts.motion ? "no-preference" : "reduce",
34060
+ 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"
34061
+ });
34062
+ const page = await context.newPage();
34063
+ return { context, page };
34064
+ }
34065
+
34066
+ // src/engine/landing-library/usedCss.ts
34067
+ async function collectUsedCss(page, selector) {
34068
+ const collected = await page.evaluate(inPageCollectUsedCss, selector);
34069
+ const extra = [];
34070
+ for (const href of collected.unreadableHrefs) {
34071
+ const text = await fetchStylesheet(page, href);
34072
+ if (text) extra.push(`/* ${href} */
34073
+ ${text}`);
34074
+ }
34075
+ return { ...collected, css: [collected.css, ...extra].filter(Boolean).join("\n\n") };
34076
+ }
34077
+ async function fetchStylesheet(page, href) {
34078
+ try {
34079
+ const response = await page.request.get(href, { timeout: 1e4 });
34080
+ if (!response.ok()) return null;
34081
+ return await response.text();
34082
+ } catch {
34083
+ return null;
34084
+ }
34085
+ }
34086
+ var inPageCollectUsedCss = (selector) => {
34087
+ const root = document.querySelector(selector);
34088
+ const empty = {
34089
+ css: "",
34090
+ variables: "",
34091
+ inheritedSeed: "",
34092
+ unreadableHrefs: [],
34093
+ stats: { totalRules: 0, keptRules: 0, fontFaces: 0, keyframes: 0 }
34094
+ };
34095
+ if (!root) return empty;
34096
+ const INHERITED = [
34097
+ "color",
34098
+ "font-family",
34099
+ "font-size",
34100
+ "font-weight",
34101
+ "line-height",
34102
+ "letter-spacing",
34103
+ "text-align",
34104
+ "background-color",
34105
+ "-webkit-font-smoothing"
34106
+ ];
34107
+ const unreadableHrefs = [];
34108
+ const kept = [];
34109
+ const fontFaces = [];
34110
+ const keyframesByName = /* @__PURE__ */ new Map();
34111
+ let totalRules = 0;
34112
+ let keptStyleRules = 0;
34113
+ const matchesInSection = (selectorText) => {
34114
+ for (const part of selectorText.split(",")) {
34115
+ const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
34116
+ if (!base) continue;
34117
+ try {
34118
+ if (root.matches(base) || root.querySelector(base)) return true;
34119
+ } catch {
34120
+ return true;
34121
+ }
34122
+ }
34123
+ return false;
34124
+ };
34125
+ const walk = (rules, sink) => {
34126
+ for (const rule of Array.from(rules)) {
34127
+ totalRules++;
34128
+ if (rule instanceof CSSStyleRule) {
34129
+ if (matchesInSection(rule.selectorText)) {
34130
+ sink.push(rule.cssText);
34131
+ keptStyleRules++;
34132
+ }
34133
+ continue;
34134
+ }
34135
+ if (rule instanceof CSSFontFaceRule) {
34136
+ fontFaces.push(rule.cssText);
34137
+ continue;
34138
+ }
34139
+ if (rule instanceof CSSKeyframesRule) {
34140
+ keyframesByName.set(rule.name, rule.cssText);
34141
+ continue;
34142
+ }
34143
+ const grouping = rule instanceof CSSMediaRule || rule instanceof CSSSupportsRule || typeof CSSLayerBlockRule !== "undefined" && rule instanceof CSSLayerBlockRule || typeof CSSContainerRule !== "undefined" && rule instanceof CSSContainerRule;
34144
+ if (grouping) {
34145
+ const inner = [];
34146
+ walk(rule.cssRules, inner);
34147
+ if (inner.length === 0) continue;
34148
+ const condition = rule.conditionText ?? "";
34149
+ const prelude = rule instanceof CSSMediaRule ? `@media ${condition}` : rule.cssText.split("{")[0]?.trim();
34150
+ sink.push(`${prelude} {
34151
+ ${inner.join("\n")}
34152
+ }`);
34153
+ }
34154
+ }
34155
+ };
34156
+ for (const sheet of Array.from(document.styleSheets)) {
34157
+ const owner = sheet.ownerNode;
34158
+ if (owner?.hasAttribute?.("data-baker-freeze")) continue;
34159
+ try {
34160
+ walk(sheet.cssRules, kept);
34161
+ } catch {
34162
+ if (sheet.href) unreadableHrefs.push(sheet.href);
34163
+ }
34164
+ }
34165
+ const keptText = kept.join("\n");
34166
+ const usedKeyframes = [];
34167
+ for (const [name, text] of keyframesByName) {
34168
+ if (new RegExp(`(^|[\\s:,])${name}([\\s;,}]|$)`).test(keptText)) usedKeyframes.push(text);
34169
+ }
34170
+ const rootStyle = getComputedStyle(root);
34171
+ const variableDeclarations = [];
34172
+ for (const property of Array.from(rootStyle)) {
34173
+ if (!property.startsWith("--")) continue;
34174
+ const value = rootStyle.getPropertyValue(property).trim();
34175
+ if (value) variableDeclarations.push(` ${property}: ${value};`);
34176
+ }
34177
+ const seed = INHERITED.map((property) => ` ${property}: ${rootStyle.getPropertyValue(property)};`).join("\n");
34178
+ return {
34179
+ css: [...fontFaces, ...usedKeyframes, ...kept].join("\n"),
34180
+ variables: variableDeclarations.length ? `:root {
34181
+ ${variableDeclarations.join("\n")}
34182
+ }` : "",
34183
+ inheritedSeed: seed,
34184
+ unreadableHrefs,
34185
+ stats: {
34186
+ totalRules,
34187
+ keptRules: keptStyleRules,
34188
+ fontFaces: fontFaces.length,
34189
+ keyframes: usedKeyframes.length
34190
+ }
34191
+ };
34192
+ };
34193
+
34194
+ // src/engine/landing-library/bundle.ts
34195
+ async function buildSectionBundle(page, selector, pageUrl) {
34196
+ const used = await collectUsedCss(page, selector);
34197
+ const extracted = await page.evaluate(inPageExtractMarkup, {
34198
+ selector,
34199
+ pageUrl,
34200
+ rootAttribute: SECTION_ROOT_ATTRIBUTE
34201
+ });
34202
+ const css = [
34203
+ "*, *::before, *::after { box-sizing: border-box; }",
34204
+ "html, body { margin: 0; padding: 0; }",
34205
+ used.variables,
34206
+ `body {
34207
+ ${used.inheritedSeed}
34208
+ }`,
34209
+ used.css
34210
+ ].filter(Boolean).join("\n\n");
34211
+ const html = `<!doctype html>
34212
+ <html ${extracted.rootAttributes}>
34213
+ <head>
34214
+ <meta charset="utf-8">
34215
+ <meta name="viewport" content="width=device-width, initial-scale=1">
34216
+ <title>Section from ${escapeHtml2(extracted.host)}</title>
34217
+ <style>
34218
+ ${css}
34219
+ </style>
34220
+ </head>
34221
+ <body ${extracted.bodyAttributes}>
34222
+ ${wrapInLayoutContext(extracted.html, extracted.layoutContext)}
34223
+ </body>
34224
+ </html>
34225
+ `;
34226
+ return { html, css, assetUrls: extracted.assetUrls, stats: used.stats };
34227
+ }
34228
+ function escapeHtml2(value) {
34229
+ return value.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c] ?? c);
34230
+ }
34231
+ var SECTION_ROOT_ATTRIBUTE = "data-baker-section-root";
34232
+ function wrapInLayoutContext(html, context) {
34233
+ let wrapped = context.width > 0 && context.width < context.viewportWidth ? `<div style="width:${context.width}px;margin:0 auto;">
34234
+ ${html}
34235
+ </div>` : html;
34236
+ for (const ancestor of [...context.ancestors].reverse()) {
34237
+ const attributes = ancestor.attributes ? ` ${ancestor.attributes}` : "";
34238
+ const container = ancestor.containerType && ancestor.containerType !== "normal" ? `container-type:${ancestor.containerType};${ancestor.containerName && ancestor.containerName !== "none" ? `container-name:${ancestor.containerName};` : ""}` : "";
34239
+ wrapped = `<${ancestor.tag}${attributes} style="${NEUTRALIZED_ANCESTOR_STYLE}${container}">
34240
+ ${wrapped}
34241
+ </${ancestor.tag}>`;
34242
+ }
34243
+ return wrapped;
34244
+ }
34245
+ var NEUTRALIZED_ANCESTOR_STYLE = [
34246
+ "display:block !important",
34247
+ "position:static !important",
34248
+ "margin:0 !important",
34249
+ "padding:0 !important",
34250
+ "border:0 !important",
34251
+ "width:auto !important",
34252
+ "min-width:0 !important",
34253
+ "max-width:none !important",
34254
+ "height:auto !important",
34255
+ "min-height:0 !important",
34256
+ "max-height:none !important",
34257
+ "transform:none !important",
34258
+ "overflow:visible !important",
34259
+ ""
34260
+ ].join(";");
34261
+ var inPageExtractMarkup = ({
34262
+ selector,
34263
+ pageUrl,
34264
+ rootAttribute
34265
+ }) => {
34266
+ const root = document.querySelector(selector);
34267
+ const emptyContext = { ancestors: [], width: 0, viewportWidth: 0 };
34268
+ const serializeAttributes = (element) => {
34269
+ if (!element) return "";
34270
+ return Array.from(element.attributes).filter((attribute) => attribute.name !== "style").map((attribute) => `${attribute.name}="${attribute.value.replace(/"/g, "&quot;")}"`).join(" ");
34271
+ };
34272
+ const rootAttributes = serializeAttributes(document.documentElement) || 'lang="en"';
34273
+ const bodyAttributes = serializeAttributes(document.body);
34274
+ if (!root) {
34275
+ return {
34276
+ html: "",
34277
+ assetUrls: [],
34278
+ host: "",
34279
+ layoutContext: emptyContext,
34280
+ rootAttributes,
34281
+ bodyAttributes
34282
+ };
34283
+ }
34284
+ const ancestors = [];
34285
+ for (let ancestor = root.parentElement; ancestor && ancestor !== document.body; ) {
34286
+ const style = getComputedStyle(ancestor);
34287
+ ancestors.unshift({
34288
+ tag: /^[a-zA-Z][a-zA-Z0-9-]*$/.test(ancestor.tagName) ? ancestor.tagName.toLowerCase() : "div",
34289
+ 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(" "),
34290
+ containerType: style.containerType,
34291
+ containerName: style.containerName
34292
+ });
34293
+ ancestor = ancestor.parentElement;
34294
+ }
34295
+ const layoutContext = {
34296
+ ancestors,
34297
+ width: Math.round(root.getBoundingClientRect().width),
34298
+ viewportWidth: window.innerWidth
34299
+ };
34300
+ const absolute = (value) => {
34301
+ try {
34302
+ return new URL(value, pageUrl).href;
34303
+ } catch {
34304
+ return value;
34305
+ }
34306
+ };
34307
+ const clone = root.cloneNode(true);
34308
+ clone.setAttribute(rootAttribute, "");
34309
+ const assetUrls = /* @__PURE__ */ new Set();
34310
+ for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
34311
+ for (const attribute of ["src", "href", "poster"]) {
34312
+ const value = element.getAttribute(attribute);
34313
+ if (!value || value.startsWith("data:") || value.startsWith("#")) continue;
34314
+ const url = absolute(value);
34315
+ element.setAttribute(attribute, url);
34316
+ if (attribute !== "href" || element.tagName === "LINK") assetUrls.add(url);
34317
+ }
34318
+ const srcset = element.getAttribute("srcset");
34319
+ if (srcset) {
34320
+ element.setAttribute(
34321
+ "srcset",
34322
+ srcset.split(",").map((candidate) => {
34323
+ const [url, descriptor] = candidate.trim().split(/\s+/, 2);
34324
+ if (!url) return candidate;
34325
+ const resolved = absolute(url);
34326
+ assetUrls.add(resolved);
34327
+ return descriptor ? `${resolved} ${descriptor}` : resolved;
34328
+ }).join(", ")
34329
+ );
34330
+ }
34331
+ const style = element.getAttribute("style");
34332
+ if (style?.includes("url(")) {
34333
+ element.setAttribute(
34334
+ "style",
34335
+ style.replace(/url\((['"]?)([^'")]+)\1\)/g, (_match, quote, url) => {
34336
+ if (url.startsWith("data:")) return `url(${quote}${url}${quote})`;
34337
+ const resolved = absolute(url);
34338
+ assetUrls.add(resolved);
34339
+ return `url(${quote}${resolved}${quote})`;
34340
+ })
34341
+ );
34342
+ }
34343
+ }
34344
+ return {
34345
+ html: clone.outerHTML,
34346
+ assetUrls: Array.from(assetUrls),
34347
+ host: location.host,
34348
+ layoutContext,
34349
+ rootAttributes,
34350
+ bodyAttributes
34351
+ };
34352
+ };
34353
+
34354
+ // src/engine/landing-library/capture.ts
34355
+ async function captureSection(page, section) {
34356
+ const locator = page.locator(section.selector).first();
34357
+ if (await locator.count().catch(() => 0)) {
34358
+ const shot = await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
34359
+ if (shot) return shot;
34360
+ }
34361
+ const doc = await page.evaluate(() => ({
34362
+ width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
34363
+ height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0)
34364
+ }));
34365
+ const width = Math.min(section.rect.width, doc.width - section.rect.left);
34366
+ const height = Math.min(section.rect.height, doc.height - section.rect.top);
34367
+ if (width <= 0 || height <= 0) return null;
34368
+ return await page.screenshot({
34369
+ type: "png",
34370
+ clip: { x: section.rect.left, y: section.rect.top, width, height },
34371
+ timeout: 15e3
34372
+ }).catch(() => null);
34373
+ }
34374
+ async function captureSectionOnMobile(page, section) {
34375
+ const locator = page.locator(section.selector).first();
34376
+ if (!await locator.count().catch(() => 0)) return null;
34377
+ if (!await locator.isVisible().catch(() => false)) return null;
34378
+ return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
34379
+ }
34380
+
34381
+ // src/engine/landing-library/fidelity.ts
34382
+ import sharp3 from "sharp";
34383
+ var COMPARISON_SIZE = 32;
34384
+ function pixelSimilarity(a, b) {
34385
+ const length = Math.min(a.length, b.length);
34386
+ if (length === 0) return 0;
34387
+ let total = 0;
34388
+ for (let i = 0; i < length; i++) {
34389
+ total += Math.abs((a[i] ?? 0) - (b[i] ?? 0));
34390
+ }
34391
+ return 1 - total / (length * 255);
34392
+ }
34393
+ async function scoreFidelity(live, rendered) {
34394
+ const [liveMeta, renderedMeta] = await Promise.all([sharp3(live).metadata(), sharp3(rendered).metadata()]);
34395
+ const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
34396
+ const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
34397
+ const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
34398
+ const score = pixelSimilarity(livePixels, renderedPixels);
34399
+ const heightRatio = liveSize.height > 0 && renderedSize.height > 0 ? Math.min(liveSize.height, renderedSize.height) / Math.max(liveSize.height, renderedSize.height) : 0;
34400
+ return {
34401
+ score,
34402
+ liveSize,
34403
+ renderedSize,
34404
+ ...heightRatio < 0.8 ? { note: `height differs by ${Math.round((1 - heightRatio) * 100)}% \u2014 the bundle reflowed` } : {}
34405
+ };
34406
+ }
34407
+ async function toGreyGrid(image) {
34408
+ const raw = await sharp3(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
34409
+ return new Uint8Array(raw);
34410
+ }
34411
+
34412
+ // src/engine/landing-library/motion.ts
34413
+ var EMPTY_MOTION = {
34414
+ hasMotion: false,
34415
+ // `none` rather than "no motion": it is the `MOTION_KINDS` member for this,
34416
+ // so a section with no movement is findable by the same facet as everything
34417
+ // else instead of being a special case only prose describes.
34418
+ summary: "none",
34419
+ entrance: [],
34420
+ hover: [],
34421
+ scroll: [],
34422
+ loop: [],
34423
+ libraries: [],
34424
+ respectsReducedMotion: false
34425
+ };
34426
+ function isWorthFilming(motion) {
34427
+ return motion.entrance.length + motion.scroll.length + motion.loop.length > 0;
34428
+ }
34429
+ async function collectMotion(page, selector) {
34430
+ const raw = await page.evaluate(inPageCollectMotion, selector).catch(() => null);
34431
+ if (!raw) return EMPTY_MOTION;
34432
+ return { ...raw, summary: summarizeMotion(raw) };
34433
+ }
34434
+ var HOVER_TRANSFORMS = /* @__PURE__ */ new Set(["lift", "grow", "tilt", "shift", "spin", "shadow", "animate"]);
34435
+ var KEYFRAME_PATTERNS = [
34436
+ [/marquee|ticker|scroll(ing)?-?(x|left|right)/, "marquee"],
34437
+ [/parallax/, "parallax"],
34438
+ [/sticky|pin(ned)?/, "sticky-pin"],
34439
+ [/count(er|up)/, "counter"],
34440
+ [/zoom|scale|pulse|grow/, "zoom"],
34441
+ [/fade|opacity|appear/, "fade"],
34442
+ [/slide|translate|in-?(left|right|up|down)/, "slide-in"]
34443
+ ];
34444
+ function toMotionKinds(kind, channel) {
34445
+ if (channel === "hover") return [HOVER_TRANSFORMS.has(kind) ? "hover-lift" : "fade"];
34446
+ const name = kind.toLowerCase();
34447
+ const matched = KEYFRAME_PATTERNS.filter(([pattern]) => pattern.test(name)).map(([, motionKind]) => motionKind);
34448
+ if (matched.length > 0) return matched;
34449
+ return [channel === "loop" ? "marquee" : "scroll-reveal"];
34450
+ }
34451
+ function motionKindsOf(motion) {
34452
+ const kinds = /* @__PURE__ */ new Set();
34453
+ const collect = (effects, channel) => {
34454
+ for (const effect of effects) {
34455
+ for (const kind of toMotionKinds(effect.kind, channel)) kinds.add(kind);
34456
+ }
34457
+ };
34458
+ collect(motion.entrance, "entrance");
34459
+ collect(motion.hover, "hover");
34460
+ collect(motion.scroll, "scroll");
34461
+ collect(motion.loop, "loop");
34462
+ const delays = new Set(motion.entrance.map((effect) => effect.delayMs ?? 0).filter((ms) => ms > 0));
34463
+ if (delays.size > 1) kinds.add("stagger");
34464
+ return [...kinds];
34465
+ }
34466
+ function summarizeMotion(motion) {
34467
+ const kinds = motionKindsOf(motion);
34468
+ if (kinds.length === 0 && motion.libraries.length === 0) return "none";
34469
+ const parts = [...kinds];
34470
+ if (motion.libraries.length > 0) parts.push(`via ${motion.libraries.join("/")}`);
34471
+ return parts.join(", ") + (motion.respectsReducedMotion ? "" : " (ignores reduced-motion)");
34472
+ }
34473
+ var inPageCollectMotion = (selector) => {
34474
+ const root = document.querySelector(selector);
34475
+ const empty = {
34476
+ hasMotion: false,
34477
+ entrance: [],
34478
+ hover: [],
34479
+ scroll: [],
34480
+ loop: [],
34481
+ libraries: [],
34482
+ respectsReducedMotion: false
34483
+ };
34484
+ if (!root) return empty;
34485
+ const entrance = [];
34486
+ const hover = [];
34487
+ const scroll = [];
34488
+ const loop = [];
34489
+ const keyframesByName = /* @__PURE__ */ new Map();
34490
+ let respectsReducedMotion = false;
34491
+ const inSection = (selectorText) => {
34492
+ for (const part of selectorText.split(",")) {
34493
+ const base = part.replace(/::[a-zA-Z-]+(\([^)]*\))?/g, "").replace(/:(hover|focus|focus-visible|focus-within|active|visited|target|checked|disabled)\b/g, "").trim();
34494
+ if (!base) continue;
34495
+ try {
34496
+ if (root.matches(base) || root.querySelector(base)) return true;
34497
+ } catch {
34498
+ return true;
34499
+ }
34500
+ }
34501
+ return false;
34502
+ };
34503
+ const toMs = (value) => {
34504
+ const first = value.split(",")[0]?.trim() ?? "";
34505
+ if (first.endsWith("ms")) return Number.parseFloat(first);
34506
+ if (first.endsWith("s")) return Number.parseFloat(first) * 1e3;
34507
+ return void 0;
34508
+ };
34509
+ const classifyKeyframes = (body) => {
34510
+ const text = body.toLowerCase();
34511
+ const fades = /opacity\s*:\s*0(\.0+)?\s*[;}]/.test(text);
34512
+ if (/translatey\(\s*-?\d/.test(text)) {
34513
+ const upward = /translatey\(\s*(\d|\.)/.test(text);
34514
+ return fades ? upward ? "fade-up" : "fade-down" : upward ? "slide-up" : "slide-down";
34515
+ }
34516
+ if (/translatex\(\s*-?\d/.test(text)) return fades ? "fade-in-x" : "slide-in-x";
34517
+ if (/scale\(/.test(text)) return fades ? "fade-zoom" : "zoom";
34518
+ if (/rotate\(/.test(text)) return "spin";
34519
+ if (fades) return "fade";
34520
+ return "animate";
34521
+ };
34522
+ const classifyHover = (style) => {
34523
+ const transform = style.transform ?? "";
34524
+ if (/translatey\(\s*-/i.test(transform)) return "lift";
34525
+ if (/scale\(\s*(1\.\d|[2-9])/i.test(transform)) return "grow";
34526
+ if (/rotate\(/i.test(transform)) return "tilt";
34527
+ if (transform && transform !== "none") return "shift";
34528
+ if (style.boxShadow) return "shadow";
34529
+ if (style.opacity) return "dim";
34530
+ if (style.backgroundColor || style.color) return "recolor";
34531
+ return null;
34532
+ };
34533
+ const readStyleRule = (rule, insideScrollTimeline) => {
34534
+ const style = rule.style;
34535
+ const isHover = /:hover\b/.test(rule.selectorText);
34536
+ if (isHover) {
34537
+ const kind = classifyHover(style);
34538
+ if (!kind || !inSection(rule.selectorText)) return;
34539
+ hover.push({
34540
+ kind,
34541
+ selector: rule.selectorText.slice(0, 120),
34542
+ durationMs: toMs(style.transitionDuration ?? ""),
34543
+ easing: style.transitionTimingFunction || void 0
34544
+ });
34545
+ return;
34546
+ }
34547
+ const animationName = style.animationName;
34548
+ if (!animationName || animationName === "none") return;
34549
+ if (!inSection(rule.selectorText)) return;
34550
+ const effect = {
34551
+ kind: animationName,
34552
+ selector: rule.selectorText.slice(0, 120),
34553
+ durationMs: toMs(style.animationDuration ?? ""),
34554
+ delayMs: toMs(style.animationDelay ?? ""),
34555
+ easing: style.animationTimingFunction || void 0
34556
+ };
34557
+ const infinite = (style.animationIterationCount ?? "").includes("infinite");
34558
+ const scrollDriven = insideScrollTimeline || Boolean(style.getPropertyValue("animation-timeline"));
34559
+ if (scrollDriven) scroll.push(effect);
34560
+ else if (infinite) loop.push(effect);
34561
+ else entrance.push(effect);
34562
+ };
34563
+ const walk = (rules, insideScrollTimeline) => {
34564
+ for (const rule of Array.from(rules)) {
34565
+ if (rule instanceof CSSKeyframesRule) {
34566
+ keyframesByName.set(rule.name, rule.cssText);
34567
+ continue;
34568
+ }
34569
+ if (rule instanceof CSSStyleRule) {
34570
+ try {
34571
+ readStyleRule(rule, insideScrollTimeline);
34572
+ } catch {
34573
+ }
34574
+ continue;
34575
+ }
34576
+ if (rule instanceof CSSMediaRule) {
34577
+ if (rule.conditionText.includes("prefers-reduced-motion")) {
34578
+ respectsReducedMotion = true;
34579
+ continue;
34580
+ }
34581
+ walk(rule.cssRules, insideScrollTimeline);
34582
+ continue;
34583
+ }
34584
+ const grouping = rule;
34585
+ if (grouping.cssRules) walk(grouping.cssRules, insideScrollTimeline);
34586
+ }
34587
+ };
34588
+ for (const sheet of Array.from(document.styleSheets)) {
34589
+ if (sheet.ownerNode?.hasAttribute?.("data-baker-freeze")) continue;
34590
+ try {
34591
+ walk(sheet.cssRules, false);
34592
+ } catch {
34593
+ }
34594
+ }
34595
+ for (const effect of [...entrance, ...loop, ...scroll]) {
34596
+ const body = keyframesByName.get(effect.kind);
34597
+ if (!body) continue;
34598
+ const classified = classifyKeyframes(body);
34599
+ effect.kind = loop.includes(effect) && classified.startsWith("slide") ? "marquee" : classified;
34600
+ }
34601
+ const libraries = [];
34602
+ const scoped = window ?? {};
34603
+ if (scoped.gsap || document.querySelector("[data-gsap]")) libraries.push("gsap");
34604
+ if (document.querySelector("[data-framer-name], [data-projection-id]")) libraries.push("framer-motion");
34605
+ if (document.querySelector("[data-aos]")) libraries.push("aos");
34606
+ if (scoped.Lenis || document.querySelector("[data-lenis]")) libraries.push("lenis");
34607
+ if (document.querySelector("[data-scroll], [data-scroll-container]")) libraries.push("locomotive");
34608
+ return {
34609
+ hasMotion: entrance.length + hover.length + scroll.length + loop.length + libraries.length > 0,
34610
+ entrance: entrance.slice(0, 12),
34611
+ hover: hover.slice(0, 12),
34612
+ scroll: scroll.slice(0, 12),
34613
+ loop: loop.slice(0, 12),
34614
+ libraries,
34615
+ respectsReducedMotion
34616
+ };
34617
+ };
34618
+
34619
+ // src/engine/landing-library/motionTake.ts
34620
+ import sharp4 from "sharp";
34621
+
34622
+ // src/engine/landing-library/prepare.ts
34623
+ function isNavigationTeardown(error) {
34624
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
34625
+ return message.includes("Execution context was destroyed") || message.includes("Cannot find context with specified id") || message.includes("frame was detached") || message.includes("has been closed");
34626
+ }
34627
+ var CONSENT_SELECTORS = [
34628
+ "#onetrust-accept-btn-handler",
34629
+ "#CybotCookiebotDialogBodyLevelButtonLevelOptinAllowAll",
34630
+ "button#didomi-notice-agree-button",
34631
+ "[aria-label='Accept all']",
34632
+ "[data-testid='uc-accept-all-button']",
34633
+ ".cc-allow",
34634
+ ".cookie-accept"
34635
+ ];
34636
+ var CONSENT_TEXTS = ["Accept all", "Accept All", "Allow all", "I agree", "Got it", "Aceptar todo"];
34637
+ var CONSENT_HOSTS = [
34638
+ "transcend-cdn.com",
34639
+ "cookielaw.org",
34640
+ "onetrust.com",
34641
+ "cookiebot.com",
34642
+ "osano.com",
34643
+ "trustarc.com",
34644
+ "truste.com",
34645
+ "usercentrics.eu",
34646
+ "didomi.io",
34647
+ "privacy-center.org",
34648
+ "iubenda.com",
34649
+ "termly.io",
34650
+ "cookieyes.com",
34651
+ "sp-prod.net",
34652
+ "quantcast.com",
34653
+ "consensu.org",
34654
+ "ketch.com",
34655
+ "secureprivacy.ai",
34656
+ "civicuk.com"
34657
+ ];
34658
+ async function blockConsentManagers(page) {
34659
+ await page.route("**/*", (route) => {
34660
+ let host = "";
34661
+ try {
34662
+ host = new URL(route.request().url()).host;
34663
+ } catch {
34664
+ return route.continue();
34665
+ }
34666
+ const isConsentVendor = CONSENT_HOSTS.some((vendor) => host === vendor || host.endsWith(`.${vendor}`));
34667
+ return isConsentVendor ? route.abort() : route.continue();
34668
+ });
34669
+ }
34670
+ var ignore = () => void 0;
34671
+ async function preparePage(page, url, timeoutMs) {
34672
+ const documentStatus = trackMainDocumentStatus(page);
34673
+ try {
34674
+ const response = await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
34675
+ await settleNavigation(page);
34676
+ await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(ignore);
34677
+ await dismissConsent(page, 4e3);
34678
+ await scrollThroughPage(page);
34679
+ await dismissConsent(page, 1e3);
34680
+ await acrossNavigation(
34681
+ page,
34682
+ () => page.evaluate(async () => {
34683
+ await document.fonts.ready;
34684
+ })
34685
+ );
34686
+ await freezeMotion(page);
34687
+ await unpinOverlays(page);
34688
+ const measured = await acrossNavigation(
34689
+ page,
34690
+ () => page.evaluate(() => ({
34691
+ finalUrl: location.href,
34692
+ title: document.title,
34693
+ documentHeight: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
34694
+ bodyText: (document.body?.innerText ?? "").slice(0, 2e3)
34695
+ }))
34696
+ );
34697
+ return { ...measured, status: documentStatus.last() ?? response?.status() ?? null };
34698
+ } finally {
34699
+ documentStatus.stop();
34700
+ }
34701
+ }
34702
+ function trackMainDocumentStatus(page) {
34703
+ let status = null;
34704
+ const onResponse = (response) => {
34705
+ const request = response.request();
34706
+ if (request.resourceType() !== "document" || request.frame() !== page.mainFrame()) return;
34707
+ const code = response.status();
34708
+ if (code >= 300 && code < 400) return;
34709
+ status = code;
34710
+ };
34711
+ page.on("response", onResponse);
34712
+ return { last: () => status, stop: () => page.off("response", onResponse) };
34713
+ }
34714
+ var NAVIGATION_QUIET_MS = 1500;
34715
+ var NAVIGATION_SETTLE_BUDGET_MS = 2e4;
34716
+ async function settleNavigation(page, budgetMs = NAVIGATION_SETTLE_BUDGET_MS) {
34717
+ let lastNavigatedAt = Date.now();
34718
+ const onNavigated = (frame) => {
34719
+ if (frame === page.mainFrame()) lastNavigatedAt = Date.now();
34720
+ };
34721
+ page.on("framenavigated", onNavigated);
34722
+ try {
34723
+ const deadline = Date.now() + budgetMs;
34724
+ while (Date.now() < deadline && Date.now() - lastNavigatedAt < NAVIGATION_QUIET_MS) {
34725
+ await page.waitForTimeout(250);
34726
+ }
34727
+ } finally {
34728
+ page.off("framenavigated", onNavigated);
34729
+ }
34730
+ }
34731
+ async function acrossNavigation(page, run) {
34732
+ try {
34733
+ return await run();
34734
+ } catch (error) {
34735
+ if (!isNavigationTeardown(error)) throw error;
34736
+ await settleNavigation(page);
34737
+ return await run();
34738
+ }
34739
+ }
34740
+ async function settleConsent(page) {
34741
+ await dismissConsent(page, 4e3);
34742
+ await page.mouse.wheel(0, 400).catch(ignore);
34743
+ await page.waitForTimeout(1500);
34744
+ await dismissConsent(page, 2e3);
34745
+ await page.evaluate(() => window.scrollTo(0, 0)).catch(ignore);
34746
+ }
34747
+ async function dismissConsent(page, waitForBannerMs) {
34748
+ await page.locator(CONSENT_SELECTORS.join(", ")).first().waitFor({ state: "attached", timeout: waitForBannerMs }).catch(ignore);
34749
+ for (const selector of CONSENT_SELECTORS) {
34750
+ const found = page.locator(selector).first();
34751
+ if (!await found.count().catch(() => 0)) continue;
34752
+ await found.click({ timeout: 2e3, force: true }).catch(ignore);
34753
+ await page.waitForTimeout(400);
34754
+ return;
34755
+ }
34756
+ for (const text of CONSENT_TEXTS) {
34757
+ const button = page.getByRole("button", { name: text, exact: false }).first();
34758
+ if (!await button.count().catch(() => 0)) continue;
34759
+ if (!await button.isVisible().catch(() => false)) continue;
34760
+ await button.click({ timeout: 2e3, force: true }).catch(ignore);
34761
+ await page.waitForTimeout(400);
34762
+ return;
34763
+ }
34764
+ }
34765
+ async function scrollThroughPage(page) {
34766
+ await acrossNavigation(
34767
+ page,
34768
+ () => page.evaluate(async () => {
34769
+ const step = 600;
34770
+ const pause = () => new Promise((resolve5) => setTimeout(resolve5, 250));
34771
+ for (let i = 0; i < 40; i++) {
34772
+ window.scrollBy(0, step);
34773
+ await pause();
34774
+ const reachedBottom = window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
34775
+ if (reachedBottom) break;
34776
+ }
34777
+ window.scrollTo(0, 0);
34778
+ await pause();
34779
+ })
34780
+ );
34781
+ await page.waitForTimeout(500);
34782
+ }
34783
+ var MAX_HEADER_HEIGHT = 220;
34784
+ function inPageUnpinOverlays(maxHeaderHeight) {
34785
+ window.scrollTo(0, 0);
34786
+ for (const element of Array.from(document.querySelectorAll("*"))) {
34787
+ const position = getComputedStyle(element).position;
34788
+ if (position === "sticky") {
34789
+ element.style.setProperty("position", "static", "important");
34790
+ continue;
34791
+ }
34792
+ if (position !== "fixed") continue;
34793
+ const rect = element.getBoundingClientRect();
34794
+ const isTopAnchoredHeader = rect.top <= 8 && rect.height > 0 && rect.height <= maxHeaderHeight;
34795
+ if (isTopAnchoredHeader) {
34796
+ element.style.setProperty("position", "absolute", "important");
34797
+ element.style.setProperty("bottom", "auto", "important");
34798
+ } else {
34799
+ element.style.setProperty("display", "none", "important");
34800
+ }
34801
+ }
34802
+ }
34803
+ async function unpinOverlays(page) {
34804
+ await acrossNavigation(page, () => page.evaluate(inPageUnpinOverlays, MAX_HEADER_HEIGHT));
34805
+ await page.waitForTimeout(250);
34806
+ }
34807
+ async function freezeMotion(page) {
34808
+ await acrossNavigation(
34809
+ page,
34810
+ () => page.evaluate(() => {
34811
+ const highestTimer = window.setTimeout(() => void 0, 0);
34812
+ for (let id = 1; id <= highestTimer; id++) window.clearInterval(id);
34813
+ const style = document.createElement("style");
34814
+ style.setAttribute("data-baker-freeze", "true");
34815
+ style.textContent = `*, *::before, *::after {
34816
+ animation-play-state: paused !important;
34817
+ animation-delay: 0s !important;
34818
+ transition: none !important;
34819
+ }`;
34820
+ document.head.appendChild(style);
34821
+ for (const video of Array.from(document.querySelectorAll("video"))) {
34822
+ video.pause();
34823
+ }
34824
+ })
34825
+ );
34826
+ await page.waitForTimeout(250);
34827
+ }
34828
+
34829
+ // src/engine/landing-library/geometry.ts
34830
+ function adaptiveGapThreshold(bands) {
34831
+ const sorted = [...bands].sort((a, b) => a.top - b.top);
34832
+ const gaps = [];
34833
+ for (let i = 0; i < sorted.length - 1; i++) {
34834
+ const current = sorted[i];
34835
+ const next = sorted[i + 1];
34836
+ if (!current || !next) continue;
34837
+ if (next.top > current.bottom) {
34838
+ const gap = next.top - current.bottom;
34839
+ if (gap < 200) gaps.push(gap);
34840
+ }
34841
+ }
34842
+ gaps.sort((a, b) => a - b);
34843
+ const p75 = gaps[Math.floor(gaps.length * 0.75)];
34844
+ if (p75 === void 0) return 15;
34845
+ return Math.max(10, Math.min(50, p75));
34846
+ }
34847
+ function bandsCollide(a, b, margin, maxGap) {
34848
+ const shrunkA = { top: a.top + margin, bottom: a.bottom - margin };
34849
+ const shrunkB = { top: b.top + margin, bottom: b.bottom - margin };
34850
+ const overlaps = shrunkA.top <= shrunkB.bottom && shrunkA.bottom >= shrunkB.top || shrunkB.top <= shrunkA.bottom && shrunkB.bottom >= shrunkA.top;
34851
+ if (overlaps) return true;
34852
+ const gap = Math.min(Math.abs(shrunkA.bottom - shrunkB.top), Math.abs(shrunkB.bottom - shrunkA.top));
34853
+ return gap <= maxGap;
34854
+ }
34855
+ function shouldPromoteToParent(group) {
34856
+ const isRunt = group.childCount < 3 || group.height < 80;
34857
+ return isRunt && group.parentHeight < 1600;
34858
+ }
34859
+
34860
+ // src/engine/landing-library/segment.ts
34861
+ async function installPageRuntime(page) {
34862
+ await page.addInitScript({
34863
+ content: `
34864
+ window.__name = window.__name || function (target) { return target; };
34865
+ window.__bakerGeom = {
34866
+ adaptiveGapThreshold: ${adaptiveGapThreshold.toString()},
34867
+ bandsCollide: ${bandsCollide.toString()},
34868
+ shouldPromoteToParent: ${shouldPromoteToParent.toString()},
34869
+ };`
34870
+ });
34871
+ }
34872
+ async function segmentPage(page, options) {
34873
+ return await page.evaluate(inPageSegment, options);
34874
+ }
34875
+ var inPageSegment = (options) => {
34876
+ const geom = window.__bakerGeom;
34877
+ const scrollX = window.scrollX;
34878
+ const scrollY = window.scrollY;
34879
+ const docWidth = Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0);
34880
+ const docHeight = Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0);
34881
+ const rectOf = (element) => {
34882
+ const r = element.getBoundingClientRect();
34883
+ const top = Math.max(0, Math.round(r.top + scrollY));
34884
+ const left = Math.max(0, Math.round(r.left + scrollX));
34885
+ return {
34886
+ top,
34887
+ left,
34888
+ bottom: Math.min(docHeight, Math.round(r.bottom + scrollY)),
34889
+ right: Math.min(docWidth, Math.round(r.right + scrollX))
34890
+ };
34891
+ };
34892
+ const syntheticRect = (element) => {
34893
+ const children = Array.from(element.children);
34894
+ if (children.length === 0) return null;
34895
+ let top = Number.POSITIVE_INFINITY;
34896
+ let left = Number.POSITIVE_INFINITY;
34897
+ let bottom = Number.NEGATIVE_INFINITY;
34898
+ let right = Number.NEGATIVE_INFINITY;
34899
+ for (const child of children) {
34900
+ const r = rectOf(child);
34901
+ if (r.bottom - r.top <= 0 && r.right - r.left <= 0) continue;
34902
+ top = Math.min(top, r.top);
34903
+ left = Math.min(left, r.left);
34904
+ bottom = Math.max(bottom, r.bottom);
34905
+ right = Math.max(right, r.right);
34906
+ }
34907
+ if (!Number.isFinite(top) || !Number.isFinite(bottom)) return null;
34908
+ return { top, left, bottom, right };
34909
+ };
34910
+ const boxOf = (element) => {
34911
+ const style = getComputedStyle(element);
34912
+ const r = style.display === "contents" ? syntheticRect(element) : rectOf(element);
34913
+ if (!r) return null;
34914
+ return { ...r, height: r.bottom - r.top, width: r.right - r.left };
34915
+ };
34916
+ const hasHiddenAncestor = (element) => {
34917
+ let current = element;
34918
+ while (current && current !== document.documentElement) {
34919
+ const style = getComputedStyle(current);
34920
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return true;
34921
+ current = current.parentElement;
34922
+ }
34923
+ return false;
34924
+ };
34925
+ const isClippedByAncestor = (element, box) => {
34926
+ let parent = element.parentElement;
34927
+ while (parent && parent !== document.documentElement) {
34928
+ const style = getComputedStyle(parent);
34929
+ const clips = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden" || style.overflow === "clip";
34930
+ if (clips) {
34931
+ const p = rectOf(parent);
34932
+ const intersects = box.left < p.right && box.right > p.left && box.top < p.bottom && box.bottom > p.top;
34933
+ if (!intersects) return true;
34934
+ }
34935
+ parent = parent.parentElement;
34936
+ }
34937
+ return false;
34938
+ };
34939
+ const directText = (element) => {
34940
+ let text = "";
34941
+ for (const node of Array.from(element.childNodes)) {
34942
+ if (node.nodeType === 3) text += node.textContent ?? "";
34943
+ }
34944
+ return text.trim();
34945
+ };
34946
+ const NON_COPY_TAGS = /* @__PURE__ */ new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE"]);
34947
+ const visibleText = (root) => {
34948
+ let text = "";
34949
+ const hiddenCache = /* @__PURE__ */ new Map();
34950
+ const isHidden = (element) => {
34951
+ const cached = hiddenCache.get(element);
34952
+ if (cached !== void 0) return cached;
34953
+ const style = getComputedStyle(element);
34954
+ const hidden = style.display === "none" || style.visibility === "hidden";
34955
+ hiddenCache.set(element, hidden);
34956
+ return hidden;
34957
+ };
34958
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
34959
+ acceptNode: (node) => {
34960
+ let parent = node.parentElement;
34961
+ while (parent) {
34962
+ if (NON_COPY_TAGS.has(parent.tagName) || isHidden(parent)) return NodeFilter.FILTER_REJECT;
34963
+ if (parent === root) break;
34964
+ parent = parent.parentElement;
34965
+ }
34966
+ return NodeFilter.FILTER_ACCEPT;
34967
+ }
34968
+ });
34969
+ while (walker.nextNode() && text.length < 400) {
34970
+ text += ` ${walker.currentNode.textContent ?? ""}`;
34971
+ }
34972
+ return text.replace(/\s+/g, " ").trim();
34973
+ };
34974
+ const GRAPHIC_TAGS = /* @__PURE__ */ new Set(["img", "svg", "video", "picture", "canvas", "iframe"]);
34975
+ const hasBackgroundImage = (element) => {
34976
+ const bg = getComputedStyle(element).backgroundImage;
34977
+ return Boolean(bg) && bg !== "none" && bg.includes("url(");
34978
+ };
34979
+ const isContentLeaf = (element) => {
34980
+ const tag = element.tagName.toLowerCase();
34981
+ if (GRAPHIC_TAGS.has(tag)) return true;
34982
+ if (directText(element).length > 0) return true;
34983
+ return hasBackgroundImage(element);
34984
+ };
34985
+ const selectorFor = (element) => {
34986
+ const parts = [];
34987
+ let current = element;
34988
+ while (current && current !== document.documentElement) {
34989
+ const tag = current.tagName.toLowerCase();
34990
+ if (tag === "body") {
34991
+ parts.unshift("body");
34992
+ break;
34993
+ }
34994
+ const parent = current.parentElement;
34995
+ if (!parent) {
34996
+ parts.unshift(tag);
34997
+ break;
34998
+ }
34999
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === current?.tagName);
35000
+ const position = sameTag.indexOf(current) + 1;
35001
+ parts.unshift(sameTag.length > 1 ? `${tag}:nth-of-type(${position})` : tag);
35002
+ current = parent;
35003
+ }
35004
+ return parts.join(" > ");
35005
+ };
35006
+ const leaves = [];
35007
+ const all = document.querySelectorAll("*");
35008
+ const limit = Math.min(all.length, options.maxElements);
35009
+ for (let i = 0; i < limit; i++) {
35010
+ const element = all[i];
35011
+ if (!element) continue;
35012
+ const tag = element.tagName.toLowerCase();
35013
+ if (tag === "script" || tag === "style" || tag === "noscript" || tag === "link" || tag === "head") continue;
35014
+ if (!isContentLeaf(element)) continue;
35015
+ const box = boxOf(element);
35016
+ if (!box || box.height <= 0 || box.width <= 0) continue;
35017
+ if (box.height > options.maxSectionHeight) continue;
35018
+ if (hasHiddenAncestor(element)) continue;
35019
+ if (isClippedByAncestor(element, box)) continue;
35020
+ leaves.push({
35021
+ element,
35022
+ top: box.top,
35023
+ bottom: box.bottom,
35024
+ left: box.left,
35025
+ right: box.right,
35026
+ height: box.height
35027
+ });
35028
+ }
35029
+ if (leaves.length === 0) return [];
35030
+ const gapThreshold = geom.adaptiveGapThreshold(leaves.map((l) => ({ top: l.top, bottom: l.bottom })));
35031
+ const commonAncestor = (elements) => {
35032
+ let ancestor = elements[0] ?? null;
35033
+ while (ancestor && !elements.every((e) => ancestor?.contains(e))) {
35034
+ ancestor = ancestor.parentElement;
35035
+ }
35036
+ return ancestor;
35037
+ };
35038
+ const abandon = (members) => members.map((m) => ({ ...m, sealed: true }));
35039
+ const findCoveringAncestor = (start, bounds) => {
35040
+ let element = start;
35041
+ while (element) {
35042
+ const box = boxOf(element);
35043
+ if (!box) return null;
35044
+ if (box.height > options.maxSectionHeight) return null;
35045
+ const covers = box.top <= bounds.top && box.bottom >= bounds.bottom && box.left <= bounds.left && box.right >= bounds.right;
35046
+ if (covers) return { element, box };
35047
+ if (!element.parentElement || element.parentElement === document.documentElement) return null;
35048
+ element = element.parentElement;
35049
+ }
35050
+ return null;
35051
+ };
35052
+ const mergeBucket = (members) => {
35053
+ const bounds = {
35054
+ top: Math.min(...members.map((m) => m.top)),
35055
+ bottom: Math.max(...members.map((m) => m.bottom)),
35056
+ left: Math.min(...members.map((m) => m.left)),
35057
+ right: Math.max(...members.map((m) => m.right))
35058
+ };
35059
+ const covering = findCoveringAncestor(commonAncestor(members.map((m) => m.root)), bounds);
35060
+ if (!covering) return abandon(members);
35061
+ return [
35062
+ {
35063
+ root: covering.element,
35064
+ top: covering.box.top,
35065
+ bottom: covering.box.bottom,
35066
+ left: covering.box.left,
35067
+ right: covering.box.right,
35068
+ height: covering.box.height,
35069
+ leaves: members.flatMap((m) => m.leaves),
35070
+ sealed: false
35071
+ }
35072
+ ];
35073
+ };
35074
+ const clusterOnce = (groups2) => {
35075
+ const buckets = [];
35076
+ for (const group of groups2) {
35077
+ if (group.sealed) {
35078
+ buckets.push([group]);
35079
+ continue;
35080
+ }
35081
+ const target = buckets.find(
35082
+ (bucket) => bucket.some(
35083
+ (member) => !member.sealed && geom.bandsCollide(
35084
+ { top: member.top, bottom: member.bottom },
35085
+ { top: group.top, bottom: group.bottom },
35086
+ options.collisionMargin,
35087
+ gapThreshold
35088
+ )
35089
+ )
35090
+ );
35091
+ if (target) target.push(group);
35092
+ else buckets.push([group]);
35093
+ }
35094
+ if (buckets.length === groups2.length) return groups2;
35095
+ return buckets.flatMap((bucket) => bucket.length === 1 ? [bucket[0]] : mergeBucket(bucket));
35096
+ };
35097
+ let groups = leaves.map((leaf) => ({
35098
+ root: leaf.element,
35099
+ top: leaf.top,
35100
+ bottom: leaf.bottom,
35101
+ left: leaf.left,
35102
+ right: leaf.right,
35103
+ height: leaf.height,
35104
+ leaves: [leaf],
35105
+ sealed: false
35106
+ }));
35107
+ for (let pass = 0; pass < 40; pass++) {
35108
+ const next = clusterOnce(groups);
35109
+ if (next.length === groups.length) break;
35110
+ groups = next;
35111
+ }
35112
+ for (let pass = 0; pass < 10; pass++) {
35113
+ let promoted = false;
35114
+ groups = groups.map((group) => {
35115
+ const parent = group.root.parentElement;
35116
+ if (!parent || parent === document.documentElement || parent === document.body) return group;
35117
+ const parentBox = boxOf(parent);
35118
+ if (!parentBox) return group;
35119
+ if (!geom.shouldPromoteToParent({
35120
+ childCount: group.leaves.length,
35121
+ height: group.height,
35122
+ parentHeight: parentBox.height
35123
+ })) {
35124
+ return group;
35125
+ }
35126
+ promoted = true;
35127
+ return {
35128
+ ...group,
35129
+ root: parent,
35130
+ top: parentBox.top,
35131
+ bottom: parentBox.bottom,
35132
+ left: parentBox.left,
35133
+ right: parentBox.right,
35134
+ height: parentBox.height
35135
+ };
35136
+ });
35137
+ if (!promoted) break;
35138
+ groups = clusterOnce(groups);
35139
+ }
35140
+ 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;
35141
+ const containsBox = (outer, inner) => outer.top - 4 <= inner.top && outer.bottom + 4 >= inner.bottom && outer.left - 4 <= inner.left && outer.right + 4 >= inner.right;
35142
+ const deduped = [];
35143
+ const area = (g) => (g.right - g.left) * g.height;
35144
+ for (const group of groups.slice().sort((a, b) => area(b) - area(a) || b.leaves.length - a.leaves.length)) {
35145
+ const duplicate = deduped.some(
35146
+ (kept) => kept.root === group.root || kept.root.contains(group.root) || sameBox(kept, group) || containsBox(kept, group)
35147
+ );
35148
+ if (!duplicate) deduped.push(group);
35149
+ }
35150
+ return deduped.filter((group) => group.height > 0 && group.right - group.left > 0).sort((a, b) => a.top - b.top).map((group, index) => ({
35151
+ index,
35152
+ selector: selectorFor(group.root),
35153
+ rect: {
35154
+ top: group.top,
35155
+ left: group.left,
35156
+ width: group.right - group.left,
35157
+ height: group.height
35158
+ },
35159
+ leafCount: group.leaves.length,
35160
+ textPreview: visibleText(group.root).slice(0, 200)
35161
+ }));
35162
+ };
35163
+
35164
+ // src/engine/landing-library/motionTake.ts
35165
+ var FRAME_TIMES_MS = [0, 120, 260, 450, 800, 1400];
35166
+ var FRAME_WIDTH = 460;
35167
+ var GRID_COLUMNS = 3;
35168
+ var LABEL_HEIGHT = 22;
35169
+ var FRAME_MARGIN_PX = 16;
35170
+ function clampToViewport(box, viewport) {
35171
+ const left = Math.max(0, Math.min(box.left, viewport.width));
35172
+ const top = Math.max(0, Math.min(box.top, viewport.height));
35173
+ const width = Math.min(box.width - (left - box.left), viewport.width - left);
35174
+ const height = Math.min(box.height - (top - box.top), viewport.height - top);
35175
+ if (width < 1 || height < 1) return null;
35176
+ return { left, top, width, height };
35177
+ }
35178
+ async function captureMotionTake(browser, url, selector, timeoutMs = 45e3) {
35179
+ const { context, page } = await newPage(browser, DESKTOP_VIEWPORT, { motion: true });
35180
+ try {
35181
+ await blockConsentManagers(page);
35182
+ await installPageRuntime(page);
35183
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs });
35184
+ await settleNavigation(page);
35185
+ await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => void 0);
35186
+ await settleConsent(page);
35187
+ const target = page.locator(selector).first();
35188
+ if (!await target.count().catch(() => 0)) return null;
35189
+ await page.evaluate((sectionSelector) => {
35190
+ const element = document.querySelector(sectionSelector);
35191
+ if (!element) return;
35192
+ const top = element.getBoundingClientRect().top + window.scrollY;
35193
+ window.scrollTo({ top: Math.max(0, top - window.innerHeight - 200), behavior: "instant" });
35194
+ }, selector);
35195
+ await page.waitForTimeout(600);
35196
+ const clip = await aimAtSection(page, selector);
35197
+ if (!clip) return null;
35198
+ await hideFloatingFurniture(page, selector);
35199
+ const frames = [];
35200
+ let previous = 0;
35201
+ for (const time of FRAME_TIMES_MS) {
35202
+ await page.waitForTimeout(Math.max(0, time - previous));
35203
+ previous = time;
35204
+ const shot = await page.screenshot({
35205
+ type: "png",
35206
+ timeout: 1e4,
35207
+ clip: { x: clip.left, y: clip.top, width: clip.width, height: clip.height }
35208
+ }).catch(() => null);
35209
+ if (shot) frames.push(shot);
35210
+ }
35211
+ if (frames.length === 0) return null;
35212
+ return { filmstrip: await composeFilmstrip(frames), frameCount: frames.length };
35213
+ } catch {
35214
+ return null;
35215
+ } finally {
35216
+ await context.close();
35217
+ }
35218
+ }
35219
+ async function aimAtSection(page, selector) {
35220
+ const box = await page.evaluate(
35221
+ ([sectionSelector, margin]) => {
35222
+ const element = document.querySelector(sectionSelector);
35223
+ if (!element) return null;
35224
+ const before = element.getBoundingClientRect();
35225
+ window.scrollTo({ top: Math.max(0, before.top + window.scrollY - margin), behavior: "instant" });
35226
+ const after = element.getBoundingClientRect();
35227
+ return {
35228
+ left: after.left,
35229
+ top: after.top - margin,
35230
+ width: after.width,
35231
+ height: after.height + margin,
35232
+ viewport: { width: window.innerWidth, height: window.innerHeight }
35233
+ };
35234
+ },
35235
+ [selector, FRAME_MARGIN_PX]
35236
+ );
35237
+ if (!box) return null;
35238
+ return clampToViewport(box, box.viewport);
35239
+ }
35240
+ async function hideFloatingFurniture(page, selector) {
35241
+ await page.evaluate((sectionSelector) => {
35242
+ const target = document.querySelector(sectionSelector);
35243
+ if (!target) return;
35244
+ for (const element of Array.from(document.querySelectorAll("*"))) {
35245
+ const position = getComputedStyle(element).position;
35246
+ if (position !== "fixed" && position !== "sticky") continue;
35247
+ if (element.contains(target) || target.contains(element) || element === target) continue;
35248
+ element.style.setProperty("visibility", "hidden", "important");
35249
+ }
35250
+ }, selector);
35251
+ }
35252
+ async function composeFilmstrip(frames) {
35253
+ const scaled = await Promise.all(frames.map((frame) => sharp4(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
35254
+ const first = await sharp4(scaled[0]).metadata();
35255
+ const frameHeight = first.height ?? 300;
35256
+ const cellHeight = frameHeight + LABEL_HEIGHT;
35257
+ const rows = Math.ceil(scaled.length / GRID_COLUMNS);
35258
+ const width = FRAME_WIDTH * Math.min(GRID_COLUMNS, scaled.length);
35259
+ const composites = scaled.flatMap((frame, index) => {
35260
+ const column = index % GRID_COLUMNS;
35261
+ const row = Math.floor(index / GRID_COLUMNS);
35262
+ const label = Buffer.from(
35263
+ `<svg width="${FRAME_WIDTH}" height="${LABEL_HEIGHT}">
35264
+ <rect width="100%" height="100%" fill="#111827"/>
35265
+ <text x="8" y="15" font-family="monospace" font-size="12" fill="#f9fafb">+${FRAME_TIMES_MS[index] ?? 0}ms</text>
35266
+ </svg>`
35267
+ );
35268
+ return [
35269
+ { input: label, left: column * FRAME_WIDTH, top: row * cellHeight },
35270
+ { input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
35271
+ ];
35272
+ });
35273
+ return await sharp4({
35274
+ create: {
35275
+ width,
35276
+ height: cellHeight * rows,
35277
+ channels: 3,
35278
+ background: { r: 17, g: 24, b: 39 }
35279
+ }
35280
+ }).composite(composites).png().toBuffer();
35281
+ }
35282
+
35283
+ // src/engine/landing-library/renderBundle.ts
35284
+ var SETTLE_ANIMATIONS_CSS = `*, *::before, *::after {
35285
+ animation-play-state: running !important;
35286
+ animation-delay: 0s !important;
35287
+ animation-duration: 1ms !important;
35288
+ animation-iteration-count: 1 !important;
35289
+ animation-fill-mode: forwards !important;
35290
+ transition: none !important;
35291
+ }`;
35292
+ async function renderBundleToPng(browser, html, viewportWidth, options = {}) {
35293
+ const { wholePage = false, timeoutMs = 2e4 } = options;
35294
+ const { context, page } = await newPage(browser, { width: viewportWidth, height: 900 });
35295
+ try {
35296
+ await page.setContent(html, { waitUntil: "load", timeout: timeoutMs });
35297
+ await page.addStyleTag({ content: SETTLE_ANIMATIONS_CSS });
35298
+ await page.evaluate(async () => {
35299
+ await document.fonts.ready;
35300
+ });
35301
+ await page.waitForTimeout(300);
35302
+ if (!wholePage) {
35303
+ for (const selector of [`[${SECTION_ROOT_ATTRIBUTE}]`, "body > *"]) {
35304
+ const target = page.locator(selector).first();
35305
+ if (!await target.count()) continue;
35306
+ const shot = await target.screenshot({ type: "png", timeout: timeoutMs }).catch(() => null);
35307
+ if (shot) return shot;
35308
+ }
35309
+ }
35310
+ return await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
35311
+ } catch {
35312
+ return null;
35313
+ } finally {
35314
+ await context.close();
35315
+ }
35316
+ }
35317
+
35318
+ // src/engine/landing-library/report.ts
35319
+ import { writeFile as writeFile14 } from "fs/promises";
35320
+ import path30 from "path";
35321
+ async function writeCaptureReport(manifest, outDir) {
35322
+ const file = path30.join(outDir, "report.html");
35323
+ await writeFile14(file, renderReport(manifest));
35324
+ return file;
35325
+ }
35326
+ function escapeHtml3(value) {
35327
+ return value.replace(
35328
+ /[&<>"']/g,
35329
+ (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character] ?? character
35330
+ );
35331
+ }
35332
+ function fidelityTone(fidelity) {
35333
+ if (fidelity === null) return "unknown";
35334
+ if (fidelity >= 0.95) return "good";
35335
+ if (fidelity >= 0.85) return "fair";
35336
+ return "poor";
35337
+ }
35338
+ function medianFidelity(sections) {
35339
+ const scored = sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
35340
+ return scored.length === 0 ? null : scored[Math.floor(scored.length / 2)] ?? null;
35341
+ }
35342
+ function formatFidelity(fidelity) {
35343
+ return fidelity === null ? "\u2014" : fidelity.toFixed(2);
35344
+ }
35345
+ function renderSection3(section) {
35346
+ const tone = fidelityTone(section.fidelity);
35347
+ const rendered = section.bundle ? section.bundle.replace(/section\.html$/, "section-rendered.png") : null;
35348
+ const shot = (label, src, note) => {
35349
+ if (!src) return `<figure class="shot empty"><figcaption>${label} \u2014 none</figcaption></figure>`;
35350
+ return `<figure class="shot">
35351
+ <figcaption>${label}${note ? ` <span class="note">${escapeHtml3(note)}</span>` : ""}</figcaption>
35352
+ <a href="${escapeHtml3(src)}" target="_blank" rel="noopener"><img src="${escapeHtml3(src)}" alt="" loading="lazy"></a>
35353
+ </figure>`;
35354
+ };
35355
+ return `<section class="card">
35356
+ <header>
35357
+ <h2><span class="index">${String(section.index).padStart(2, "0")}</span> ${section.rect.width}\xD7${section.rect.height}</h2>
35358
+ <span class="badge ${tone}">fidelity ${formatFidelity(section.fidelity)}</span>
35359
+ ${section.fidelityNote ? `<span class="badge warn">${escapeHtml3(section.fidelityNote)}</span>` : ""}
35360
+ ${section.motion.hasMotion ? `<span class="badge motion">${escapeHtml3(section.motion.summary)}</span>` : ""}
35361
+ </header>
35362
+ <p class="preview">${escapeHtml3(section.textPreview.slice(0, 220)) || "<em>no text</em>"}</p>
35363
+ <div class="shots">
35364
+ ${shot("Live", section.desktopShot)}
35365
+ ${shot("Reproduction", rendered, "rendered from the extracted bundle")}
35366
+ ${shot("Mobile", section.mobileShot)}
35367
+ </div>
35368
+ ${section.motionFilmstrip ? `<div class="filmstrip">${shot("Motion \u2014 six frames, left to right", section.motionFilmstrip)}</div>` : ""}
35369
+ <footer>
35370
+ <code>${escapeHtml3(section.selector)}</code>
35371
+ ${section.bundle ? `<a href="${escapeHtml3(section.bundle)}" target="_blank" rel="noopener">open the standalone bundle \u2192</a>` : ""}
35372
+ </footer>
35373
+ </section>`;
35374
+ }
35375
+ function renderReport(manifest) {
35376
+ const median = medianFidelity(manifest.sections);
35377
+ const moving = manifest.sections.filter((section) => section.motionFilmstrip !== null).length;
35378
+ return `<!doctype html>
35379
+ <html lang="en">
35380
+ <head>
35381
+ <meta charset="utf-8">
35382
+ <meta name="viewport" content="width=device-width, initial-scale=1">
35383
+ <title>Capture report \u2014 ${escapeHtml3(manifest.title || manifest.url)}</title>
35384
+ <style>
35385
+ :root { color-scheme: light dark; --line: #e5e7eb; --muted: #6b7280; --bg: #fafafa; --card: #fff; }
35386
+ @media (prefers-color-scheme: dark) {
35387
+ :root { --line: #27272a; --muted: #a1a1aa; --bg: #09090b; --card: #131316; }
35388
+ }
35389
+ * { box-sizing: border-box; }
35390
+ body { margin: 0; padding: 24px; background: var(--bg);
35391
+ font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
35392
+ h1 { margin: 0 0 4px; font-size: 20px; }
35393
+ a { color: inherit; }
35394
+ .sub { color: var(--muted); margin: 0 0 20px; }
35395
+ .stats { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
35396
+ .stat { border: 1px solid var(--line); border-radius: 10px; padding: 8px 12px; background: var(--card); }
35397
+ .stat b { display: block; font-size: 18px; }
35398
+ .stat span { color: var(--muted); font-size: 12px; }
35399
+ .card { border: 1px solid var(--line); border-radius: 12px; background: var(--card);
35400
+ padding: 16px; margin-bottom: 16px; }
35401
+ .card header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
35402
+ .card h2 { font-size: 15px; margin: 0; font-weight: 600; }
35403
+ .index { display: inline-block; min-width: 26px; color: var(--muted); }
35404
+ .badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); }
35405
+ .badge.good { background: #dcfce7; color: #166534; border-color: #bbf7d0; }
35406
+ .badge.fair { background: #fef3c7; color: #92400e; border-color: #fde68a; }
35407
+ .badge.poor { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
35408
+ .badge.warn { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
35409
+ .badge.motion { background: #ede9fe; color: #5b21b6; border-color: #ddd6fe; }
35410
+ .preview { color: var(--muted); margin: 0 0 12px; font-size: 13px; }
35411
+ .shots { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; align-items: start; }
35412
+ .filmstrip { margin-top: 12px; }
35413
+ figure { margin: 0; }
35414
+ figcaption { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
35415
+ figcaption .note { opacity: .75; }
35416
+ .shot img { width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px;
35417
+ background: #fff; display: block; }
35418
+ .shot.empty { border: 1px dashed var(--line); border-radius: 8px; padding: 20px; text-align: center; }
35419
+ .card footer { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px;
35420
+ font-size: 12px; color: var(--muted); }
35421
+ code { font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
35422
+ </style>
35423
+ </head>
35424
+ <body>
35425
+ <h1>${escapeHtml3(manifest.title || "Capture report")}</h1>
35426
+ <p class="sub"><a href="${escapeHtml3(manifest.finalUrl)}" target="_blank" rel="noopener">${escapeHtml3(manifest.finalUrl)}</a> \xB7 captured ${escapeHtml3(manifest.capturedAt)}</p>
35427
+
35428
+ <div class="stats">
35429
+ <div class="stat"><b>${manifest.sections.length}</b><span>sections</span></div>
35430
+ <div class="stat"><b>${formatFidelity(median)}</b><span>median fidelity</span></div>
35431
+ <div class="stat"><b>${formatFidelity(manifest.page.fidelity)}</b><span>whole page</span></div>
35432
+ <div class="stat"><b>${moving}</b><span>filmed moving</span></div>
35433
+ <div class="stat"><b>${manifest.documentHeight}px</b><span>page height</span></div>
35434
+ </div>
35435
+
35436
+ <section class="card">
35437
+ <header>
35438
+ <h2>Whole page</h2>
35439
+ <span class="badge ${fidelityTone(manifest.page.fidelity)}">fidelity ${formatFidelity(manifest.page.fidelity)}</span>
35440
+ </header>
35441
+ <p class="preview">The same extractor rooted at &lt;body&gt; \u2014 one standalone file that should render like the original.</p>
35442
+ <div class="shots">
35443
+ <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>
35444
+ ${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>`}
35445
+ </div>
35446
+ </section>
35447
+
35448
+ ${manifest.sections.map(renderSection3).join("\n")}
35449
+ </body>
35450
+ </html>
35451
+ `;
35452
+ }
35453
+
35454
+ // src/engine/landing-library/types.ts
35455
+ var DEFAULT_SEGMENT_OPTIONS = {
35456
+ maxSectionHeight: 2160,
35457
+ collisionMargin: 2,
35458
+ maxElements: 15e3
35459
+ };
35460
+
35461
+ // src/engine/landing-library/visualHash.ts
35462
+ import sharp5 from "sharp";
35463
+ var HASH_WIDTH = 9;
35464
+ var HASH_HEIGHT = 8;
35465
+ async function perceptualHash(image) {
35466
+ try {
35467
+ const raw = await sharp5(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
35468
+ return bitsToHex(rowGradientBits(new Uint8Array(raw)));
35469
+ } catch {
35470
+ return null;
35471
+ }
35472
+ }
35473
+ function rowGradientBits(pixels) {
35474
+ const bits = [];
35475
+ for (let y = 0; y < HASH_HEIGHT; y++) {
35476
+ for (let x = 0; x < HASH_WIDTH - 1; x++) {
35477
+ const left = pixels[y * HASH_WIDTH + x] ?? 0;
35478
+ const right = pixels[y * HASH_WIDTH + x + 1] ?? 0;
35479
+ bits.push(left > right);
35480
+ }
35481
+ }
35482
+ return bits;
35483
+ }
35484
+ function bitsToHex(bits) {
35485
+ let hex = "";
35486
+ for (let index = 0; index < bits.length; index += 4) {
35487
+ let nibble = 0;
35488
+ for (let offset = 0; offset < 4; offset++) {
35489
+ if (bits[index + offset]) nibble |= 1 << 3 - offset;
35490
+ }
35491
+ hex += nibble.toString(16);
35492
+ }
35493
+ return hex;
35494
+ }
35495
+
35496
+ // src/engine/landing-library/run.ts
35497
+ async function reproducePage(args) {
35498
+ const { browser, page, outDir, pageUrl, livePageShot } = args;
35499
+ const built = await buildSectionBundle(page, "body", pageUrl).catch(() => null);
35500
+ if (!built) return { bundle: null, fidelity: null };
35501
+ await writeFile15(path31.join(outDir, "page.html"), built.html);
35502
+ const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width, {
35503
+ wholePage: true,
35504
+ timeoutMs: 6e4
35505
+ });
35506
+ if (!rendered || !livePageShot) return { bundle: "page.html", fidelity: null };
35507
+ await writeFile15(path31.join(outDir, "page-rendered.png"), rendered);
35508
+ const { score, note } = await scoreFidelity(livePageShot, rendered);
35509
+ return { bundle: "page.html", fidelity: score, ...note ? { fidelityNote: note } : {} };
35510
+ }
35511
+ async function captureOneSection(args) {
35512
+ const { browser, page, candidate, sectionsDir, outDir, pageUrl, withCode } = args;
35513
+ const dir = path31.join(sectionsDir, String(candidate.index).padStart(2, "0"));
35514
+ await mkdir11(dir, { recursive: true });
35515
+ const desktop = await captureSection(page, candidate);
35516
+ if (desktop) await writeFile15(path31.join(dir, "desktop.png"), desktop);
35517
+ const visualHash = desktop ? await perceptualHash(desktop) : null;
35518
+ const motion = await collectMotion(page, candidate.selector);
35519
+ const built = withCode ? await buildSectionBundle(page, candidate.selector, pageUrl) : null;
35520
+ let fidelity = null;
35521
+ let fidelityNote;
35522
+ if (built) {
35523
+ await writeFile15(path31.join(dir, "section.html"), built.html);
35524
+ const rendered = await renderBundleToPng(browser, built.html, DESKTOP_VIEWPORT.width);
35525
+ if (rendered && desktop) {
35526
+ await writeFile15(path31.join(dir, "section-rendered.png"), rendered);
35527
+ const result = await scoreFidelity(desktop, rendered);
35528
+ fidelity = result.score;
35529
+ fidelityNote = result.note;
35530
+ }
35531
+ }
35532
+ return {
35533
+ ...candidate,
35534
+ desktopShot: desktop ? path31.relative(outDir, path31.join(dir, "desktop.png")) : null,
35535
+ mobileShot: null,
35536
+ bundle: built ? path31.relative(outDir, path31.join(dir, "section.html")) : null,
35537
+ fidelity,
35538
+ ...fidelityNote ? { fidelityNote } : {},
35539
+ ...built ? { cssStats: built.stats } : {},
35540
+ motion,
35541
+ motionFilmstrip: null,
35542
+ visualHash
35543
+ };
35544
+ }
35545
+ async function captureMobileShots(args) {
35546
+ const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs } = args;
35547
+ const mobile = await newPage(browser, MOBILE_VIEWPORT);
35548
+ try {
35549
+ await blockConsentManagers(mobile.page);
35550
+ await installPageRuntime(mobile.page);
35551
+ await preparePage(mobile.page, pageUrl, timeoutMs);
35552
+ for (const section of sections) {
35553
+ const shot = await captureSectionOnMobile(mobile.page, section);
35554
+ if (!shot) continue;
35555
+ const file = path31.join(sectionsDir, String(section.index).padStart(2, "0"), "mobile.png");
35556
+ await writeFile15(file, shot);
35557
+ section.mobileShot = path31.relative(outDir, file);
35558
+ }
35559
+ } finally {
35560
+ await mobile.context.close();
35561
+ }
35562
+ }
35563
+ async function captureMotionTakes(args) {
35564
+ const { browser, sections, sectionsDir, outDir, pageUrl, log } = args;
35565
+ const moving = sections.filter((section) => isWorthFilming(section.motion));
35566
+ if (moving.length === 0) return;
35567
+ log(`filming ${moving.length} moving sections`);
35568
+ for (const section of moving) {
35569
+ const take = await captureMotionTake(browser, pageUrl, section.selector);
35570
+ if (!take) continue;
35571
+ const dir = path31.join(sectionsDir, String(section.index).padStart(2, "0"));
35572
+ const file = path31.join(dir, "motion-filmstrip.png");
35573
+ await writeFile15(file, take.filmstrip);
35574
+ section.motionFilmstrip = path31.relative(outDir, file);
35575
+ log(` [${section.index}] ${section.motion.summary}`);
35576
+ }
35577
+ }
35578
+ async function scrapeLanding(options) {
35579
+ const timeoutMs = options.timeoutMs ?? 45e3;
35580
+ const log = options.onProgress ?? (() => void 0);
35581
+ const sectionsDir = path31.join(options.outDir, "sections");
35582
+ const browser = await launchBrowser();
35583
+ try {
35584
+ const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
35585
+ await blockConsentManagers(page);
35586
+ await installPageRuntime(page);
35587
+ log(`loading ${options.url}`);
35588
+ const prepared = await preparePage(page, options.url, timeoutMs);
35589
+ const blocked = detectBlockedPage({
35590
+ status: prepared.status,
35591
+ title: prepared.title,
35592
+ bodyText: prepared.bodyText,
35593
+ html: await page.content().catch(() => "")
35594
+ });
35595
+ if (blocked) throw new BlockedPageError(blocked);
35596
+ await mkdir11(sectionsDir, { recursive: true });
35597
+ log("segmenting");
35598
+ const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
35599
+ log(`found ${candidates.length} sections`);
35600
+ const sections = [];
35601
+ for (const candidate of candidates) {
35602
+ const section = await captureOneSection({
35603
+ browser,
35604
+ page,
35605
+ candidate,
35606
+ sectionsDir,
35607
+ outDir: options.outDir,
35608
+ pageUrl: prepared.finalUrl,
35609
+ withCode: options.code !== false
35610
+ });
35611
+ sections.push(section);
35612
+ log(
35613
+ ` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
35614
+ );
35615
+ }
35616
+ const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
35617
+ if (fullPage) await writeFile15(path31.join(options.outDir, "full-page.png"), fullPage);
35618
+ const reproduction = options.code === false ? { bundle: null, fidelity: null } : await reproducePage({
35619
+ browser,
35620
+ page,
35621
+ outDir: options.outDir,
35622
+ pageUrl: prepared.finalUrl,
35623
+ livePageShot: fullPage
35624
+ });
35625
+ log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
35626
+ await context.close();
35627
+ if (options.mobile !== false) {
35628
+ log("capturing mobile");
35629
+ await captureMobileShots({
35630
+ browser,
35631
+ sections,
35632
+ sectionsDir,
35633
+ outDir: options.outDir,
35634
+ pageUrl: prepared.finalUrl,
35635
+ timeoutMs
35636
+ });
35637
+ }
35638
+ if (options.motion !== false) {
35639
+ await captureMotionTakes({
35640
+ browser,
35641
+ sections,
35642
+ sectionsDir,
35643
+ outDir: options.outDir,
35644
+ pageUrl: prepared.finalUrl,
35645
+ log
35646
+ });
35647
+ }
35648
+ const manifest = {
35649
+ url: options.url,
35650
+ finalUrl: prepared.finalUrl,
35651
+ title: prepared.title,
35652
+ documentHeight: prepared.documentHeight,
35653
+ viewport: DESKTOP_VIEWPORT,
35654
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
35655
+ sections,
35656
+ page: reproduction
35657
+ };
35658
+ await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
35659
+ `);
35660
+ if (options.report !== false) {
35661
+ const reportPath = await writeCaptureReport(manifest, options.outDir);
35662
+ log(`report: ${reportPath}`);
35663
+ }
35664
+ return manifest;
35665
+ } finally {
35666
+ await browser.close();
35667
+ }
35668
+ }
35669
+
35670
+ // src/commands/landing/inspiration/scrape.ts
35671
+ async function recordCapture(manifest, outDir) {
35672
+ const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
35673
+ const references = [];
35674
+ for (const section of manifest.sections) {
35675
+ if (!section.bundle) continue;
35676
+ try {
35677
+ const markup = await readFile23(path32.join(outDir, section.bundle), "utf8");
35678
+ const copyStrings = capturedCopyStrings(markup);
35679
+ if (copyStrings.length === 0) continue;
35680
+ references.push({
35681
+ sectionId: `local:${manifest.finalUrl}#${section.index}`,
35682
+ sourceUrl: manifest.finalUrl,
35683
+ copyStrings,
35684
+ consultedAt
35685
+ });
35686
+ } catch {
35687
+ }
35688
+ }
35689
+ return await recordReferences(process.cwd(), references);
35690
+ }
35691
+ registerSchema({
35692
+ command: "landing.inspiration.scrape",
35693
+ description: "Start here when the user points at a specific page and wants it now: capture one landing page into a directory of section screenshots, standalone HTML bundles, a whole-page reproduction and motion filmstrips. Returns when the capture is done, unlike `add`. Read the screenshots. Also records every captured section for the originality check.",
35694
+ args: {
35695
+ url: { type: "string", description: "Page to capture", required: true },
35696
+ out: { type: "string", description: "Output directory", required: true },
35697
+ mobile: { type: "boolean", description: "Phone-viewport pass. `--no-mobile` to skip", required: false },
35698
+ code: {
35699
+ type: "boolean",
35700
+ description: "Standalone HTML+CSS bundles and fidelity scores. `--no-code` to segment and screenshot only",
35701
+ required: false
35702
+ },
35703
+ motion: {
35704
+ type: "boolean",
35705
+ description: "Film sections that move. `--no-motion` to skip \u2014 roughly halves runtime",
35706
+ required: false
35707
+ },
35708
+ report: { type: "boolean", description: "Write report.html. `--no-report` to skip", required: false }
35709
+ }
35710
+ });
35711
+ var scrapeCommand = defineCommand147({
35712
+ meta: {
35713
+ name: "scrape",
35714
+ description: "Capture a landing page to a directory, now. Example: baker landing inspiration scrape https://linear.app --out .baker/inspiration/linear.app"
35715
+ },
35716
+ args: {
35717
+ url: { type: "positional", description: "Page to capture", required: true },
35718
+ out: { type: "string", description: "Output directory", required: true },
35719
+ mobile: {
35720
+ type: "boolean",
35721
+ description: "Phone-viewport pass (--no-mobile to skip)",
35722
+ required: false,
35723
+ default: true
35724
+ },
35725
+ code: { type: "boolean", description: "Bundles + fidelity (--no-code to skip)", required: false, default: true },
35726
+ motion: {
35727
+ type: "boolean",
35728
+ description: "Film moving sections (--no-motion to skip)",
35729
+ required: false,
35730
+ default: true
35731
+ },
35732
+ report: { type: "boolean", description: "Write report.html (--no-report to skip)", required: false, default: true }
35733
+ },
35734
+ run: async ({ args }) => {
35735
+ try {
35736
+ const manifest = await scrapeLanding({
35737
+ url: args.url,
35738
+ outDir: args.out,
35739
+ mobile: args.mobile,
35740
+ code: args.code,
35741
+ motion: args.motion,
35742
+ report: args.report,
35743
+ // Progress goes to stderr so stdout stays a clean JSON envelope.
35744
+ onProgress: (message) => process.stderr.write(`${message}
35745
+ `)
35746
+ });
35747
+ const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
35748
+ const recorded = await recordCapture(manifest, args.out);
35749
+ writeJson({
35750
+ ok: true,
35751
+ data: {
35752
+ title: manifest.title,
35753
+ sections: manifest.sections.length,
35754
+ medianFidelity: scored.length > 0 ? scored[Math.floor(scored.length / 2)] : null,
35755
+ pageFidelity: manifest.page.fidelity,
35756
+ out: args.out,
35757
+ report: args.report ? `${args.out}/report.html` : null
35758
+ },
35759
+ hints: [
35760
+ // The command that produces the most and says the least about it. It
35761
+ // returns a count and a directory; without this the agent has no
35762
+ // instruction to open any of it, and a capture nobody looks at taught
35763
+ // nobody anything. A nudge tied to one command's result belongs here,
35764
+ // not only in the tool doc the agent may not re-read.
35765
+ args.report ? `Read ${args.out}/report.html \u2014 every section side by side with its fidelity score.` : `Read the section screenshots in ${args.out}/sections/*/desktop.png before you design.`,
35766
+ `${manifest.sections.length} sections captured to ${args.out}/sections/ \u2014 look at them, then decide which the client's page actually needs.`,
35767
+ INSPIRATION_HINTS.structureNotCopy,
35768
+ INSPIRATION_HINTS.adapt,
35769
+ recorded ? "These sections are recorded for the originality check \u2014 `baker landing critique` will block a publish that ships their copy." : "Could not record this capture, so the originality check cannot see it. Be especially careful not to reuse its copy."
35770
+ ]
35771
+ });
35772
+ } catch (error) {
35773
+ if (error instanceof BlockedPageError) {
35774
+ writeJson({ ok: false, error: { code: error.code, message: error.message } });
35775
+ process.exit(1);
35776
+ }
35777
+ reportError(error);
35778
+ }
35779
+ }
35780
+ });
35781
+
35782
+ // src/commands/landing/inspiration/search.ts
35783
+ import { mkdir as mkdir12, writeFile as writeFile16 } from "fs/promises";
35784
+ import path33 from "path";
35785
+ import { defineCommand as defineCommand148 } from "citty";
35786
+ registerSchema({
35787
+ command: "landing.inspiration.search",
35788
+ 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.",
35789
+ args: {
35790
+ query: { type: "string", description: "What you want to see, in plain English", required: false },
35791
+ type: {
35792
+ type: "string",
35793
+ description: "Comma list of section types: hero,pricing,faq,testimonials,\u2026",
35794
+ required: false
35795
+ },
35796
+ composition: {
35797
+ type: "string",
35798
+ description: "Comma list of composition patterns (references/composition.md)",
35799
+ required: false
35800
+ },
35801
+ register: {
35802
+ type: "string",
35803
+ description: "Comma list of visual registers: dev-tool-minimal,luxury-high-end,\u2026",
35804
+ required: false
35805
+ },
35806
+ interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
35807
+ motion: {
35808
+ type: "string",
35809
+ description: "Comma list of motion kinds: scroll-reveal,marquee,parallax,\u2026",
35810
+ required: false
35811
+ },
35812
+ media: { type: "string", description: "Comma list of media kinds", required: false },
35813
+ device: { type: "string", description: "Comma list of content devices", required: false },
35814
+ theme: { type: "string", description: "light | dark | mixed", required: false },
35815
+ "max-rank": {
35816
+ type: "number",
35817
+ description: "Show>Tell rank ceiling 1-7; 3 means 'rank 3 or better'",
35818
+ required: false
35819
+ },
35820
+ "min-craft": { type: "number", description: "Craft floor 0-1", required: false },
35821
+ "min-fidelity": {
35822
+ type: "number",
35823
+ description: "Only sections whose markup reproduces this well (0-1)",
35824
+ required: false
35825
+ },
35826
+ domain: { type: "string", description: "Restrict to one site", required: false },
35827
+ "similar-to": {
35828
+ type: "string",
35829
+ description: "Section id \u2014 find sections that look like this one",
35830
+ required: false
35831
+ },
35832
+ scope: { type: "string", description: "favorites (default) | all", required: false },
35833
+ limit: { type: "number", description: "Max results (default 8)", required: false },
35834
+ "no-images": { type: "boolean", description: "Skip downloading screenshots", required: false },
35835
+ full: {
35836
+ type: "boolean",
35837
+ description: "Add every classification facet (theme, density, rank, interactions, media, devices, tags, headline)",
35838
+ required: false
35839
+ }
35840
+ }
35841
+ });
35842
+ function buildSearchBody(args) {
35843
+ const body = {};
35844
+ const setList2 = (key, value) => {
35845
+ const list = splitList(value);
35846
+ if (list) Object.assign(body, { [key]: list });
35847
+ };
35848
+ if (args.query) body.query = String(args.query);
35849
+ if (args["similar-to"]) body.similarToSectionId = String(args["similar-to"]);
35850
+ setList2("sectionType", args.type);
35851
+ setList2("composition", args.composition);
35852
+ setList2("visualRegister", args.register);
35853
+ setList2("interaction", args.interaction);
35854
+ setList2("motion", args.motion);
35855
+ setList2("mediaKind", args.media);
35856
+ setList2("contentDevice", args.device);
35857
+ if (args.theme) body.theme = String(args.theme);
35858
+ if (args.domain) body.domain = String(args.domain);
35859
+ const maxRank = parseNumber(args["max-rank"]);
35860
+ if (maxRank !== void 0) body.maxShowTellRank = maxRank;
35861
+ const minCraft = parseNumber(args["min-craft"]);
35862
+ if (minCraft !== void 0) body.minCraftScore = minCraft;
35863
+ const minFidelity = parseNumber(args["min-fidelity"]);
35864
+ if (minFidelity !== void 0) body.minFidelity = minFidelity;
35865
+ const limit = parseNumber(args.limit);
35866
+ body.limit = limit ?? 8;
35867
+ body.scope = args.scope === "all" ? "all" : "favorites";
35868
+ return body;
35869
+ }
35870
+ async function downloadShots(results) {
35871
+ const dir = path33.join(process.cwd(), ".baker", "inspiration");
35872
+ await mkdir12(dir, { recursive: true });
35873
+ const saved = /* @__PURE__ */ new Map();
35874
+ await Promise.all(
35875
+ results.map(async (result) => {
35876
+ if (!result.desktopShotUrl) return;
35877
+ try {
35878
+ const response = await fetch(result.desktopShotUrl);
35879
+ if (!response.ok) return;
35880
+ const file = path33.join(dir, `${result.id}.png`);
35881
+ await writeFile16(file, Buffer.from(await response.arrayBuffer()));
35882
+ saved.set(result.id, path33.relative(process.cwd(), file));
35883
+ } catch {
35884
+ }
35885
+ })
35886
+ );
35887
+ return saved;
35888
+ }
35889
+ var searchCommand2 = defineCommand148({
35890
+ meta: {
35891
+ name: "search",
35892
+ 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"
35893
+ },
35894
+ args: {
35895
+ query: { type: "positional", description: "What you want to see, in plain English", required: false },
35896
+ type: { type: "string", description: "Comma list of section types", required: false },
35897
+ composition: { type: "string", description: "Comma list of composition patterns", required: false },
35898
+ register: { type: "string", description: "Comma list of visual registers", required: false },
35899
+ interaction: { type: "string", description: "Comma list of interaction patterns", required: false },
35900
+ motion: { type: "string", description: "Comma list of motion kinds", required: false },
35901
+ media: { type: "string", description: "Comma list of media kinds", required: false },
35902
+ device: { type: "string", description: "Comma list of content devices", required: false },
35903
+ theme: { type: "string", description: "light | dark | mixed", required: false },
35904
+ "max-rank": { type: "string", description: "Show>Tell rank ceiling 1-7", required: false },
35905
+ "min-craft": { type: "string", description: "Craft floor 0-1", required: false },
35906
+ "min-fidelity": { type: "string", description: "Reproduction-fidelity floor 0-1", required: false },
35907
+ domain: { type: "string", description: "Restrict to one site", required: false },
35908
+ "similar-to": { type: "string", description: "Section id to find lookalikes of", required: false },
35909
+ scope: { type: "string", description: "favorites (default) | all", required: false, default: "favorites" },
35910
+ limit: { type: "string", description: "Max results (default 8)", required: false },
35911
+ "no-images": { type: "boolean", description: "Skip downloading screenshots", required: false, default: false },
35912
+ full: { type: "boolean", description: "Include every classification facet", required: false, default: false }
35913
+ },
35914
+ run: async ({ args }) => {
35915
+ try {
35916
+ const body = buildSearchBody(args);
35917
+ const data = await apiPost("/api/landing-inspiration/search", body);
35918
+ const results = Array.isArray(data?.results) ? data.results : [];
35919
+ const shots = args["no-images"] ? /* @__PURE__ */ new Map() : await downloadShots(results);
35920
+ const full = args.full;
35921
+ const rows = results.map((result) => ({
35922
+ id: result.id,
35923
+ // The id `page` takes. Its own schema says "from a search result's source
35924
+ // id", and until this line no projection emitted one — so the ordering
35925
+ // view could only be reached straight after `add`, never from a search.
35926
+ page_id: result.sourceId,
35927
+ section: result.sectionType,
35928
+ composition: result.composition,
35929
+ look: result.visualRegister,
35930
+ motion: result.motionSummary,
35931
+ why_it_works: result.whyItWorks,
35932
+ craft: Number(result.craftScore?.toFixed?.(2) ?? result.craftScore),
35933
+ fidelity: result.fidelity,
35934
+ domain: result.domain,
35935
+ // Only worth saying when it means something: >1 marks a section the site
35936
+ // reuses across its pages, which is a stronger signal than a one-off.
35937
+ ...result.pageCount > 1 ? { used_on_pages: result.pageCount } : {},
35938
+ // With --no-images there is no file, so hand back the URL instead. A row
35939
+ // with neither is a section the agent is asked to judge sight unseen,
35940
+ // which is the one thing a visual library must never produce.
35941
+ ...args["no-images"] ? { screenshot_url: result.desktopShotUrl ?? null } : { screenshot: shots.get(result.id) ?? null },
35942
+ ...full ? {
35943
+ theme: result.theme,
35944
+ density: result.density,
35945
+ show_tell_rank: result.showTellRank,
35946
+ interactions: result.interactions,
35947
+ media: result.mediaKinds,
35948
+ devices: result.contentDevices,
35949
+ tags: result.tags,
35950
+ headline: result.headline,
35951
+ source_url: result.sourceUrl
35952
+ } : {}
35953
+ }));
35954
+ const hints = [INSPIRATION_HINTS.adapt];
35955
+ if (rows.length === 0) {
35956
+ hints.push(
35957
+ 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>`."
35958
+ );
35959
+ } else if (data.moreInFullLibrary > 0) {
35960
+ hints.push(`${data.moreInFullLibrary} more match in the full library \u2014 re-run with --scope all.`);
35961
+ }
35962
+ if (shots.size > 0) hints.push(`Screenshots saved to .baker/inspiration/ \u2014 Read them before deciding.`);
35963
+ writeJson({
35964
+ ok: true,
35965
+ data: { results: rows, scope: data.scope, more_in_full_library: data.moreInFullLibrary, total: data.total },
35966
+ hints
35967
+ });
35968
+ } catch (error) {
35969
+ reportError(error);
35970
+ }
35971
+ }
35972
+ });
35973
+
35974
+ // src/commands/landing/inspiration/view.ts
35975
+ import { mkdir as mkdir13, writeFile as writeFile17 } from "fs/promises";
35976
+ import path34 from "path";
35977
+ import { defineCommand as defineCommand149 } from "citty";
35978
+ registerSchema({
35979
+ command: "landing.inspiration.view",
35980
+ 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.",
35981
+ args: {
35982
+ id: { type: "string", description: "Section id from search", required: true },
35983
+ full: {
35984
+ type: "boolean",
35985
+ description: "Add the classification facets (theme, density, rank, interactions, media, devices, tags) and the reproduction notes",
35986
+ required: false
35987
+ }
35988
+ }
35989
+ });
35990
+ async function download(url, file) {
35991
+ if (!url) return null;
35992
+ try {
35993
+ const response = await fetch(url);
35994
+ if (!response.ok) return null;
35995
+ await mkdir13(path34.dirname(file), { recursive: true });
35996
+ await writeFile17(file, Buffer.from(await response.arrayBuffer()));
35997
+ return path34.relative(process.cwd(), file);
35998
+ } catch {
35999
+ return null;
36000
+ }
36001
+ }
36002
+ var viewCommand2 = defineCommand149({
36003
+ meta: {
36004
+ name: "view",
36005
+ description: "Full detail for one reference section. Example: baker landing inspiration view k57abc\u2026 \u2014 read the screenshots it saves before you build."
36006
+ },
36007
+ args: {
36008
+ id: { type: "positional", description: "Section id from search", required: true },
36009
+ full: {
36010
+ type: "boolean",
36011
+ description: "Include classification facets and reproduction notes",
36012
+ required: false,
36013
+ default: false
36014
+ }
36015
+ },
36016
+ run: async ({ args }) => {
36017
+ try {
36018
+ const id = args.id;
36019
+ const data = await apiGet("/api/landing-inspiration/section", { id });
36020
+ const section = data.section;
36021
+ const dir = path34.join(process.cwd(), ".baker", "inspiration", id);
36022
+ const [desktop, mobile, filmstrip] = await Promise.all([
36023
+ download(section.desktopShotUrl, path34.join(dir, "desktop.png")),
36024
+ download(section.mobileShotUrl, path34.join(dir, "mobile.png")),
36025
+ download(section.motionFilmstripUrl, path34.join(dir, "motion-filmstrip.png"))
36026
+ ]);
36027
+ const full = args.full;
36028
+ const hints = [INSPIRATION_HINTS.structureNotCopy, INSPIRATION_HINTS.adapt];
36029
+ if (desktop) hints.push(`Read ${desktop} before you build \u2014 the description is not the section.`);
36030
+ hints.push("Need the mechanics? `baker landing inspiration code <id>` writes the standalone HTML+CSS to disk.");
36031
+ const fidelity = fidelityHint(section.fidelity);
36032
+ if (fidelity) hints.push(fidelity);
36033
+ if (filmstrip) {
36034
+ hints.push(
36035
+ "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."
36036
+ );
36037
+ }
36038
+ writeJson({
36039
+ ok: true,
36040
+ data: {
36041
+ id: section.id,
36042
+ page_id: section.sourceId,
36043
+ section: section.sectionType,
36044
+ composition: section.composition,
36045
+ look: section.visualRegister,
36046
+ motion: section.motionSummary,
36047
+ design_tokens: section.designTokens,
36048
+ size: section.boundingBox,
36049
+ copy: {
36050
+ headline: section.headline,
36051
+ subhead: section.subhead,
36052
+ ctas: section.ctaLabels,
36053
+ proof: section.proofSignals
36054
+ },
36055
+ why_it_works: section.whyItWorks,
36056
+ adaptation_notes: section.adaptationNotes,
36057
+ craft: section.craftScore,
36058
+ fidelity: section.fidelity,
36059
+ source_url: section.sourceUrl,
36060
+ screenshots: { desktop, mobile, motion_filmstrip: filmstrip },
36061
+ // Behind --full: exactly the block `search` already gates, plus the
36062
+ // reproduction notes, which `code` returns again for the one caller
36063
+ // that acts on them. Everything above is what you design *from*;
36064
+ // these are how the section was filed and how it rebuilds.
36065
+ ...full ? {
36066
+ theme: section.theme,
36067
+ density: section.density,
36068
+ show_tell_rank: section.showTellRank,
36069
+ interactions: section.interactions,
36070
+ media: section.mediaKinds,
36071
+ devices: section.contentDevices,
36072
+ tags: section.tags,
36073
+ reproduction_notes: section.reproductionNotes
36074
+ } : {}
36075
+ },
36076
+ hints
36077
+ });
36078
+ } catch (error) {
36079
+ reportError(error);
36080
+ }
36081
+ }
36082
+ });
36083
+
36084
+ // src/commands/landing/inspiration/index.ts
36085
+ var inspirationCommand = defineCommand150({
36086
+ meta: {
36087
+ name: "inspiration",
36088
+ description: `Reference library of real landing-page sections \u2014 look at how good pages actually solve a problem before you design one.
36089
+
36090
+ Start here: \`baker landing inspiration search "<what you want to see>"\` during research, BEFORE you write the Direction Contract.
36091
+
36092
+ 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.
36093
+
36094
+ Subcommands:
36095
+ baker landing inspiration search "<query>" \u2014 search by look, section type, composition, register or motion; saves screenshots you can Read
36096
+ baker landing inspiration view <id> \u2014 one section in full: tokens, motion filmstrip, why it works, what to change
36097
+ baker landing inspiration code <id> \u2014 its standalone HTML+CSS, for structure only
36098
+ baker landing inspiration page <id> \u2014 a whole page as a sequence: how it orders its sections
36099
+ baker landing inspiration add <url> \u2014 add a page to the library and save it to this company (returns immediately)
36100
+ baker landing inspiration favorites \u2014 what this company has saved; the default search scope
36101
+ baker landing inspiration favorite <id> \u2014 save a section
36102
+ baker landing inspiration unfavorite <id> \u2014 unsave a section
36103
+ baker landing inspiration scrape <url> \u2014 internal/ops: run the capture locally
36104
+
36105
+ Examples:
36106
+ baker landing inspiration search "pricing with a monthly/annual toggle" --max-rank 3
36107
+ baker landing inspiration search "dark developer hero with a terminal" --register dev-tool-minimal --scope all
36108
+ baker landing inspiration search "testimonial wall with faces and company logos" --motion scroll-reveal
36109
+ baker landing inspiration add https://linear.app
36110
+
36111
+ Full guide: __tooling__/docs/tools/baker/landing.md`
36112
+ },
36113
+ subCommands: {
36114
+ search: searchCommand2,
36115
+ view: viewCommand2,
36116
+ code: codeCommand,
36117
+ page: pageCommand,
36118
+ add: addCommand,
36119
+ favorites: favoritesCommand,
36120
+ favorite: favoriteCommand,
36121
+ unfavorite: unfavoriteCommand,
36122
+ scrape: scrapeCommand
36123
+ }
36124
+ });
36125
+
33333
36126
  // src/commands/landing/index.ts
33334
- var landingCommand = defineCommand143({
36127
+ var landingCommand = defineCommand151({
33335
36128
  meta: {
33336
36129
  name: "landing",
33337
36130
  description: `Design-quality tools for landing pages (src/pages/<slug>/).
@@ -33339,15 +36132,17 @@ var landingCommand = defineCommand143({
33339
36132
  Start here: \`baker landing critique <slug>\` after building or editing a landing.
33340
36133
 
33341
36134
  Subcommands:
36135
+ 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.
33342
36136
  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.`
33343
36137
  },
33344
36138
  subCommands: {
36139
+ inspiration: inspirationCommand,
33345
36140
  critique: critiqueCommand2
33346
36141
  }
33347
36142
  });
33348
36143
 
33349
36144
  // src/commands/mcp/index.ts
33350
- import { defineCommand as defineCommand144 } from "citty";
36145
+ import { defineCommand as defineCommand152 } from "citty";
33351
36146
 
33352
36147
  // src/commands/mcp/platforms.ts
33353
36148
  function readsKey(label) {
@@ -33416,7 +36211,7 @@ registerSchema({
33416
36211
  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.",
33417
36212
  args: {}
33418
36213
  });
33419
- var connectedCommand = defineCommand144({
36214
+ var connectedCommand = defineCommand152({
33420
36215
  meta: {
33421
36216
  name: "connected",
33422
36217
  description: `Everything this chat can reach \u2014 managed integrations, custom MCP servers, and connected platforms.
@@ -33475,7 +36270,7 @@ registerSchema({
33475
36270
  description: "List the custom MCP servers this company's chats see (org + company + your own user scope).",
33476
36271
  args: {}
33477
36272
  });
33478
- var listCommand13 = defineCommand144({
36273
+ var listCommand13 = defineCommand152({
33479
36274
  meta: { name: "list", description: "List custom MCP servers visible to this company's chats." },
33480
36275
  run: async () => {
33481
36276
  try {
@@ -33512,7 +36307,7 @@ registerSchema({
33512
36307
  header: { type: "string", description: 'Auth header "Key: Value" (repeatable)', required: false }
33513
36308
  }
33514
36309
  });
33515
- var addCommand = defineCommand144({
36310
+ var addCommand2 = defineCommand152({
33516
36311
  meta: {
33517
36312
  name: "add",
33518
36313
  description: `Register a custom MCP server. Tools appear as mcp__<name>__* on the NEXT message.
@@ -33564,7 +36359,7 @@ registerSchema({
33564
36359
  description: "Remove a company custom MCP server by name.",
33565
36360
  args: { name: { type: "string", description: "Server name to remove", required: true } }
33566
36361
  });
33567
- var removeCommand4 = defineCommand144({
36362
+ var removeCommand4 = defineCommand152({
33568
36363
  meta: {
33569
36364
  name: "remove",
33570
36365
  description: `Remove a company custom MCP server by name.
@@ -33586,7 +36381,7 @@ Example:
33586
36381
  }
33587
36382
  }
33588
36383
  });
33589
- var mcpCommand = defineCommand144({
36384
+ var mcpCommand = defineCommand152({
33590
36385
  meta: {
33591
36386
  name: "mcp",
33592
36387
  description: `Third-party tools for this company \u2014 see what's connected, register custom HTTPS MCP endpoints.
@@ -33606,16 +36401,16 @@ Full guide: __tooling__/docs/tools/baker/mcp.md`
33606
36401
  subCommands: {
33607
36402
  connected: connectedCommand,
33608
36403
  list: listCommand13,
33609
- add: addCommand,
36404
+ add: addCommand2,
33610
36405
  remove: removeCommand4
33611
36406
  }
33612
36407
  });
33613
36408
 
33614
36409
  // src/commands/research/index.ts
33615
- import { defineCommand as defineCommand155 } from "citty";
36410
+ import { defineCommand as defineCommand163 } from "citty";
33616
36411
 
33617
36412
  // src/commands/research/advertisers.ts
33618
- import { defineCommand as defineCommand145 } from "citty";
36413
+ import { defineCommand as defineCommand153 } from "citty";
33619
36414
 
33620
36415
  // src/commands/research/output.ts
33621
36416
  var RESEARCH_DATA_NOTE = "Estimates based on third-party SERP data \u2014 not exact figures. Use for directional insights, not precise measurement.";
@@ -33728,7 +36523,7 @@ var FIELDS3 = {
33728
36523
  etv: "Estimated traffic value (USD)",
33729
36524
  visibility: "SERP visibility score (0-1)"
33730
36525
  };
33731
- var advertisersCommand = defineCommand145({
36526
+ var advertisersCommand = defineCommand153({
33732
36527
  meta: {
33733
36528
  name: "advertisers",
33734
36529
  description: `Find domains competing for a keyword in Google SERPs.
@@ -33748,15 +36543,15 @@ Examples:
33748
36543
  },
33749
36544
  run: async ({ args }) => {
33750
36545
  const keyword = args.keyword;
33751
- const location = args.location || void 0;
36546
+ const location2 = args.location || void 0;
33752
36547
  const language = args.language || void 0;
33753
36548
  const limit = args.limit ? Number(args.limit) : void 0;
33754
36549
  const skipCache = args["no-cache"] ? true : void 0;
33755
- const queryContext = buildResearchQueryContext(location, language);
36550
+ const queryContext = buildResearchQueryContext(location2, language);
33756
36551
  try {
33757
36552
  const data = await apiPost("/api/research/advertisers", {
33758
36553
  keyword,
33759
- location,
36554
+ location: location2,
33760
36555
  language,
33761
36556
  limit,
33762
36557
  skipCache
@@ -33775,7 +36570,7 @@ Examples:
33775
36570
  });
33776
36571
 
33777
36572
  // src/commands/research/autocomplete.ts
33778
- import { defineCommand as defineCommand146 } from "citty";
36573
+ import { defineCommand as defineCommand154 } from "citty";
33779
36574
  registerSchema({
33780
36575
  command: "research.autocomplete",
33781
36576
  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).",
@@ -33798,7 +36593,7 @@ registerSchema({
33798
36593
  var FIELDS4 = {
33799
36594
  suggestion: "Autocomplete suggestion from Google"
33800
36595
  };
33801
- var autocompleteCommand = defineCommand146({
36596
+ var autocompleteCommand = defineCommand154({
33802
36597
  meta: {
33803
36598
  name: "autocomplete",
33804
36599
  description: `Get Google Autocomplete suggestions for keyword expansion.
@@ -33817,15 +36612,15 @@ Examples:
33817
36612
  },
33818
36613
  run: async ({ args }) => {
33819
36614
  const keyword = args.keyword;
33820
- const location = args.location || void 0;
36615
+ const location2 = args.location || void 0;
33821
36616
  const language = args.language || void 0;
33822
36617
  const limit = args.limit ? Number(args.limit) : void 0;
33823
36618
  const skipCache = args["no-cache"] ? true : void 0;
33824
- const queryContext = buildResearchQueryContext(location, language);
36619
+ const queryContext = buildResearchQueryContext(location2, language);
33825
36620
  try {
33826
36621
  const data = await apiPost("/api/research/autocomplete", {
33827
36622
  keyword,
33828
- location,
36623
+ location: location2,
33829
36624
  language,
33830
36625
  limit,
33831
36626
  skipCache
@@ -33844,7 +36639,7 @@ Examples:
33844
36639
  });
33845
36640
 
33846
36641
  // src/commands/research/countries.ts
33847
- import { defineCommand as defineCommand147 } from "citty";
36642
+ import { defineCommand as defineCommand155 } from "citty";
33848
36643
  registerSchema({
33849
36644
  command: "research.countries",
33850
36645
  description: "List all supported country codes for --location flag in research commands.",
@@ -33901,7 +36696,7 @@ var FIELDS5 = {
33901
36696
  code: "Country code to pass as --location",
33902
36697
  name: "Country name"
33903
36698
  };
33904
- var countriesCommand = defineCommand147({
36699
+ var countriesCommand = defineCommand155({
33905
36700
  meta: {
33906
36701
  name: "countries",
33907
36702
  description: "List all supported country codes for --location flag."
@@ -33912,7 +36707,7 @@ var countriesCommand = defineCommand147({
33912
36707
  });
33913
36708
 
33914
36709
  // src/commands/research/intent.ts
33915
- import { defineCommand as defineCommand148 } from "citty";
36710
+ import { defineCommand as defineCommand156 } from "citty";
33916
36711
  registerSchema({
33917
36712
  command: "research.intent",
33918
36713
  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.",
@@ -33935,7 +36730,7 @@ var FIELDS6 = {
33935
36730
  intent: "Primary Google Search intent: informational, navigational, commercial, transactional",
33936
36731
  probability: "Confidence score 0.0-1.0"
33937
36732
  };
33938
- var intentCommand = defineCommand148({
36733
+ var intentCommand = defineCommand156({
33939
36734
  meta: {
33940
36735
  name: "intent",
33941
36736
  description: `Classify Google Search intent for keywords. Returns intent type and confidence.
@@ -33983,7 +36778,7 @@ Examples:
33983
36778
  });
33984
36779
 
33985
36780
  // src/commands/research/keyword-gap.ts
33986
- import { defineCommand as defineCommand149 } from "citty";
36781
+ import { defineCommand as defineCommand157 } from "citty";
33987
36782
  registerSchema({
33988
36783
  command: "research.keyword-gap",
33989
36784
  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.",
@@ -34012,7 +36807,7 @@ var FIELDS7 = {
34012
36807
  cpc: "Cost per click USD",
34013
36808
  their_position: "Competitor's ranking position"
34014
36809
  };
34015
- var keywordGapCommand = defineCommand149({
36810
+ var keywordGapCommand = defineCommand157({
34016
36811
  meta: {
34017
36812
  name: "keyword-gap",
34018
36813
  description: `Find keywords a competitor has that you don't. Supports pagination via --offset.
@@ -34036,18 +36831,18 @@ Examples:
34036
36831
  run: async ({ args }) => {
34037
36832
  const competitor = args.competitor;
34038
36833
  const ours = args.ours;
34039
- const location = args.location || void 0;
36834
+ const location2 = args.location || void 0;
34040
36835
  const language = args.language || void 0;
34041
36836
  const type = args.type || void 0;
34042
36837
  const limit = args.limit ? Number(args.limit) : void 0;
34043
36838
  const offset = args.offset ? Number(args.offset) : void 0;
34044
36839
  const skipCache = args["no-cache"] ? true : void 0;
34045
- const queryContext = buildResearchQueryContext(location, language);
36840
+ const queryContext = buildResearchQueryContext(location2, language);
34046
36841
  try {
34047
36842
  const result = await apiPost("/api/research/keyword-gap", {
34048
36843
  competitor,
34049
36844
  ours,
34050
- location,
36845
+ location: location2,
34051
36846
  language,
34052
36847
  type,
34053
36848
  limit,
@@ -34086,7 +36881,7 @@ Examples:
34086
36881
  });
34087
36882
 
34088
36883
  // src/commands/research/keywords-for-site.ts
34089
- import { defineCommand as defineCommand150 } from "citty";
36884
+ import { defineCommand as defineCommand158 } from "citty";
34090
36885
  registerSchema({
34091
36886
  command: "research.keywords-for-site",
34092
36887
  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.",
@@ -34119,7 +36914,7 @@ var FIELDS8 = {
34119
36914
  competition: "LOW, MEDIUM, or HIGH",
34120
36915
  competition_index: "Competition score 0-100"
34121
36916
  };
34122
- var keywordsForSiteCommand = defineCommand150({
36917
+ var keywordsForSiteCommand = defineCommand158({
34123
36918
  meta: {
34124
36919
  name: "keywords-for-site",
34125
36920
  description: `Get keywords a competitor targets in Google. Use --type to filter paid/organic.
@@ -34141,17 +36936,17 @@ Examples:
34141
36936
  },
34142
36937
  run: async ({ args }) => {
34143
36938
  const target = args.target;
34144
- const location = args.location || void 0;
36939
+ const location2 = args.location || void 0;
34145
36940
  const language = args.language || void 0;
34146
36941
  const sort = args.sort || void 0;
34147
36942
  const type = args.type || void 0;
34148
36943
  const limit = args.limit ? Number(args.limit) : void 0;
34149
36944
  const skipCache = args["no-cache"] ? true : void 0;
34150
- const queryContext = buildResearchQueryContext(location, language);
36945
+ const queryContext = buildResearchQueryContext(location2, language);
34151
36946
  try {
34152
36947
  const data = await apiPost("/api/research/keywords-for-site", {
34153
36948
  target,
34154
- location,
36949
+ location: location2,
34155
36950
  language,
34156
36951
  sort,
34157
36952
  type,
@@ -34172,7 +36967,7 @@ Examples:
34172
36967
  });
34173
36968
 
34174
36969
  // src/commands/research/languages.ts
34175
- import { defineCommand as defineCommand151 } from "citty";
36970
+ import { defineCommand as defineCommand159 } from "citty";
34176
36971
  registerSchema({
34177
36972
  command: "research.languages",
34178
36973
  description: "List all supported language codes for --language flag in research commands.",
@@ -34202,7 +36997,7 @@ var FIELDS9 = {
34202
36997
  code: "Language code to pass as --language",
34203
36998
  name: "Language name (also accepted by --language)"
34204
36999
  };
34205
- var languagesCommand2 = defineCommand151({
37000
+ var languagesCommand2 = defineCommand159({
34206
37001
  meta: {
34207
37002
  name: "languages",
34208
37003
  description: "List all supported language codes for --language flag."
@@ -34213,7 +37008,7 @@ var languagesCommand2 = defineCommand151({
34213
37008
  });
34214
37009
 
34215
37010
  // src/commands/research/lighthouse.ts
34216
- import { defineCommand as defineCommand152 } from "citty";
37011
+ import { defineCommand as defineCommand160 } from "citty";
34217
37012
  registerSchema({
34218
37013
  command: "research.lighthouse",
34219
37014
  description: "Landing page performance audit. Returns metrics that affect Google Ads Quality Score and CPC.",
@@ -34232,7 +37027,7 @@ var FIELDS10 = {
34232
37027
  speed_index_ms: "Speed Index in ms (good: < 3400)",
34233
37028
  interactive_ms: "Time to Interactive in ms (good: < 3800)"
34234
37029
  };
34235
- var lighthouseCommand = defineCommand152({
37030
+ var lighthouseCommand = defineCommand160({
34236
37031
  meta: {
34237
37032
  name: "lighthouse",
34238
37033
  description: `Landing page performance audit. Metrics affecting Google Ads Quality Score.
@@ -34270,7 +37065,7 @@ Examples:
34270
37065
  });
34271
37066
 
34272
37067
  // src/commands/research/relevant-pages.ts
34273
- import { defineCommand as defineCommand153 } from "citty";
37068
+ import { defineCommand as defineCommand161 } from "citty";
34274
37069
  registerSchema({
34275
37070
  command: "research.relevant-pages",
34276
37071
  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).",
@@ -34296,7 +37091,7 @@ var FIELDS11 = {
34296
37091
  keywords: "Total organic keywords the page ranks for",
34297
37092
  top_10: "Keywords in positions 1-10"
34298
37093
  };
34299
- var relevantPagesCommand = defineCommand153({
37094
+ var relevantPagesCommand = defineCommand161({
34300
37095
  meta: {
34301
37096
  name: "relevant-pages",
34302
37097
  description: `Get the top pages of a competitor domain with traffic data.
@@ -34315,15 +37110,15 @@ Examples:
34315
37110
  },
34316
37111
  run: async ({ args }) => {
34317
37112
  const target = args.target;
34318
- const location = args.location || void 0;
37113
+ const location2 = args.location || void 0;
34319
37114
  const language = args.language || void 0;
34320
37115
  const limit = args.limit ? Number(args.limit) : void 0;
34321
37116
  const skipCache = args["no-cache"] ? true : void 0;
34322
- const queryContext = buildResearchQueryContext(location, language);
37117
+ const queryContext = buildResearchQueryContext(location2, language);
34323
37118
  try {
34324
37119
  const data = await apiPost("/api/research/relevant-pages", {
34325
37120
  target,
34326
- location,
37121
+ location: location2,
34327
37122
  language,
34328
37123
  limit,
34329
37124
  skipCache
@@ -34342,7 +37137,7 @@ Examples:
34342
37137
  });
34343
37138
 
34344
37139
  // src/commands/research/web.ts
34345
- import { defineCommand as defineCommand154 } from "citty";
37140
+ import { defineCommand as defineCommand162 } from "citty";
34346
37141
  registerSchema({
34347
37142
  command: "research.web",
34348
37143
  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).",
@@ -34393,7 +37188,7 @@ async function runDeepResearch(question) {
34393
37188
  }
34394
37189
  throw new Error("Deep research timed out");
34395
37190
  }
34396
- var webCommand = defineCommand154({
37191
+ var webCommand = defineCommand162({
34397
37192
  meta: {
34398
37193
  name: "web",
34399
37194
  description: `Search the web with AI to answer any open-ended marketing question. Uses live internet data via Google Search.
@@ -34453,7 +37248,7 @@ Examples:
34453
37248
  });
34454
37249
 
34455
37250
  // src/commands/research/index.ts
34456
- var researchCommand = defineCommand155({
37251
+ var researchCommand = defineCommand163({
34457
37252
  meta: {
34458
37253
  name: "research",
34459
37254
  description: `Competitive intelligence and AI-powered research commands.
@@ -34494,10 +37289,10 @@ Full guide: __tooling__/docs/tools/baker/research.md`
34494
37289
  });
34495
37290
 
34496
37291
  // src/commands/scheduled-actions/index.ts
34497
- import { defineCommand as defineCommand162 } from "citty";
37292
+ import { defineCommand as defineCommand170 } from "citty";
34498
37293
 
34499
37294
  // src/commands/scheduled-actions/create.ts
34500
- import { defineCommand as defineCommand156 } from "citty";
37295
+ import { defineCommand as defineCommand164 } from "citty";
34501
37296
 
34502
37297
  // src/commands/scheduled-actions/shared.ts
34503
37298
  var TEMP_SCHEDULED_ACTION_PREFIX = "temp_sched_";
@@ -34612,7 +37407,7 @@ registerSchema({
34612
37407
  prompt: { type: "string", description: "Additional prompt instructions for the spawned agent", required: false }
34613
37408
  }
34614
37409
  });
34615
- var createCommand2 = defineCommand156({
37410
+ var createCommand2 = defineCommand164({
34616
37411
  meta: {
34617
37412
  name: "create",
34618
37413
  description: 'Stage a scheduled action. Example: baker scheduled-actions create --name "Weekly report" --description "..." --cron "0 9 * * MON"'
@@ -34661,7 +37456,7 @@ var createCommand2 = defineCommand156({
34661
37456
  });
34662
37457
 
34663
37458
  // src/commands/scheduled-actions/delete.ts
34664
- import { defineCommand as defineCommand157 } from "citty";
37459
+ import { defineCommand as defineCommand165 } from "citty";
34665
37460
  registerSchema({
34666
37461
  command: "scheduled-actions.delete",
34667
37462
  description: "Stage deletion of a published scheduled action or cancellation of a temp_sched_* draft creation.",
@@ -34669,7 +37464,7 @@ registerSchema({
34669
37464
  id: { type: "string", description: "Published scheduled action ID or temp_sched_* draft ID", required: true }
34670
37465
  }
34671
37466
  });
34672
- var deleteCommand2 = defineCommand157({
37467
+ var deleteCommand2 = defineCommand165({
34673
37468
  meta: {
34674
37469
  name: "delete",
34675
37470
  description: "Stage scheduled action deletion. Example: baker scheduled-actions delete <id-or-temp_sched_id>"
@@ -34698,7 +37493,7 @@ var deleteCommand2 = defineCommand157({
34698
37493
  });
34699
37494
 
34700
37495
  // src/commands/scheduled-actions/get.ts
34701
- import { defineCommand as defineCommand158 } from "citty";
37496
+ import { defineCommand as defineCommand166 } from "citty";
34702
37497
  registerSchema({
34703
37498
  command: "scheduled-actions.get",
34704
37499
  description: "Get a published scheduled action or a temp_sched_* draft-created scheduled action.",
@@ -34707,7 +37502,7 @@ registerSchema({
34707
37502
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
34708
37503
  }
34709
37504
  });
34710
- var getCommand3 = defineCommand158({
37505
+ var getCommand3 = defineCommand166({
34711
37506
  meta: {
34712
37507
  name: "get",
34713
37508
  description: "Get a scheduled action. Example: baker scheduled-actions get <id-or-temp_sched_id>"
@@ -34746,7 +37541,7 @@ var getCommand3 = defineCommand158({
34746
37541
  });
34747
37542
 
34748
37543
  // src/commands/scheduled-actions/list.ts
34749
- import { defineCommand as defineCommand159 } from "citty";
37544
+ import { defineCommand as defineCommand167 } from "citty";
34750
37545
  registerSchema({
34751
37546
  command: "scheduled-actions.list",
34752
37547
  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.",
@@ -34754,7 +37549,7 @@ registerSchema({
34754
37549
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
34755
37550
  }
34756
37551
  });
34757
- var listCommand14 = defineCommand159({
37552
+ var listCommand14 = defineCommand167({
34758
37553
  meta: {
34759
37554
  name: "list",
34760
37555
  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."
@@ -34777,7 +37572,7 @@ var listCommand14 = defineCommand159({
34777
37572
  });
34778
37573
 
34779
37574
  // src/commands/scheduled-actions/trigger.ts
34780
- import { defineCommand as defineCommand160 } from "citty";
37575
+ import { defineCommand as defineCommand168 } from "citty";
34781
37576
  registerSchema({
34782
37577
  command: "scheduled-actions.trigger",
34783
37578
  description: "Immediately trigger a published scheduled action. Does not require BAKER_CHAT_ID and rejects temp_sched_* IDs.",
@@ -34785,7 +37580,7 @@ registerSchema({
34785
37580
  id: { type: "string", description: "Published scheduled action ID", required: true }
34786
37581
  }
34787
37582
  });
34788
- var triggerCommand = defineCommand160({
37583
+ var triggerCommand = defineCommand168({
34789
37584
  meta: {
34790
37585
  name: "trigger",
34791
37586
  description: "Immediately trigger a published scheduled action. Example: baker scheduled-actions trigger <id>"
@@ -34822,7 +37617,7 @@ var triggerCommand = defineCommand160({
34822
37617
  });
34823
37618
 
34824
37619
  // src/commands/scheduled-actions/update.ts
34825
- import { defineCommand as defineCommand161 } from "citty";
37620
+ import { defineCommand as defineCommand169 } from "citty";
34826
37621
  registerSchema({
34827
37622
  command: "scheduled-actions.update",
34828
37623
  description: "Stage an update to a published scheduled action or temp_sched_* draft-created scheduled action.",
@@ -34847,7 +37642,7 @@ registerSchema({
34847
37642
  prompt: { type: "string", description: "Replacement additional spawned-agent instructions", required: false }
34848
37643
  }
34849
37644
  });
34850
- var updateCommand2 = defineCommand161({
37645
+ var updateCommand2 = defineCommand169({
34851
37646
  meta: {
34852
37647
  name: "update",
34853
37648
  description: "Stage a scheduled action update. Example: baker scheduled-actions update <id> --enabled false"
@@ -34918,7 +37713,7 @@ var updateCommand2 = defineCommand161({
34918
37713
  });
34919
37714
 
34920
37715
  // src/commands/scheduled-actions/index.ts
34921
- var scheduledActionsCommand = defineCommand162({
37716
+ var scheduledActionsCommand = defineCommand170({
34922
37717
  meta: {
34923
37718
  name: "scheduled-actions",
34924
37719
  description: `Manage Scheduled Actions. Subcommands: list, get, create, update, delete, trigger.
@@ -34945,14 +37740,14 @@ Full guide: __tooling__/docs/tools/baker/scheduled-actions.md`
34945
37740
  });
34946
37741
 
34947
37742
  // src/commands/schema.ts
34948
- import { defineCommand as defineCommand163 } from "citty";
37743
+ import { defineCommand as defineCommand171 } from "citty";
34949
37744
  function narrowToFamily(commandName, available) {
34950
37745
  const segments = commandName.split(".");
34951
37746
  const prefix = segments[0] === "ads" && segments[1] ? `ads.${segments[1]}.` : `${segments[0]}.`;
34952
37747
  const siblings = available.filter((name) => name.startsWith(prefix));
34953
37748
  return siblings.length > 0 ? siblings : available;
34954
37749
  }
34955
- var schemaCommand = defineCommand163({
37750
+ var schemaCommand = defineCommand171({
34956
37751
  meta: {
34957
37752
  name: "schema",
34958
37753
  description: "Inspect command argument schemas (for AI agent introspection). Lists all commands if no argument given. Example: baker schema images.search"
@@ -34996,10 +37791,10 @@ var schemaCommand = defineCommand163({
34996
37791
  });
34997
37792
 
34998
37793
  // src/commands/studio/index.ts
34999
- import { defineCommand as defineCommand172 } from "citty";
37794
+ import { defineCommand as defineCommand180 } from "citty";
35000
37795
 
35001
37796
  // src/commands/studio/animate.ts
35002
- import { defineCommand as defineCommand164 } from "citty";
37797
+ import { defineCommand as defineCommand172 } from "citty";
35003
37798
 
35004
37799
  // src/commands/studio/batch.ts
35005
37800
  function projectBatch(generation, full) {
@@ -35188,7 +37983,7 @@ function parseImageRefs(spec) {
35188
37983
  }
35189
37984
  var defaultDeps = {
35190
37985
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
35191
- upload: (path28) => uploadLocalImage({ file: path28, contentType: detectImageContentType(path28), source: "uploaded" })
37986
+ upload: (path35) => uploadLocalImage({ file: path35, contentType: detectImageContentType(path35), source: "uploaded" })
35192
37987
  };
35193
37988
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
35194
37989
  const refs = parseImageRefs(spec);
@@ -35208,10 +38003,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
35208
38003
  }
35209
38004
  return { imageIds, added };
35210
38005
  }
35211
- function uploadFailure(path28) {
38006
+ function uploadFailure(path35) {
35212
38007
  return (error) => {
35213
38008
  if (error instanceof ApiError) throw error;
35214
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path28}" as an image.`);
38009
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path35}" as an image.`);
35215
38010
  };
35216
38011
  }
35217
38012
 
@@ -35444,7 +38239,7 @@ function costHintsFor(body) {
35444
38239
  }
35445
38240
  return hints;
35446
38241
  }
35447
- var animateCommand = defineCommand164({
38242
+ var animateCommand = defineCommand172({
35448
38243
  meta: {
35449
38244
  name: "animate",
35450
38245
  description: "Render a clip. With an image the look is already fixed, so the prompt describes MOVEMENT \u2014 what the camera does, what the subject does, in what order. With --from text there is no image and the prompt is the whole shot.\n\nA rendered clip is NOT usable anywhere until you keep it: `baker studio keep <id> --slot N` is what puts it in the video library. Takes nobody keeps are never ingested, which is what makes a rejected batch cheap.\n\nFor anything longer than 15 seconds, or to build on footage that already exists, use --model bytedance/seedance-2.5: it renders 4-30s and is the only model that reads an existing clip or an existing soundtrack.\n\nExamples:\n baker studio animate 'slow push in, model turns to camera and smiles' --image j57abc123def456ghi789\n baker studio animate 'handheld drift right, steam rising from the cup' --image './out/hero.png' --duration 6 --quality 1080p\n baker studio animate 'product rotates once on a turntable' --image j57abc\u2026,j57def\u2026 --from references\n baker studio animate 'she keeps walking, camera stays with her, then she stops and looks up' --image j57abc\u2026 --from references --from-clip j57batch\u2026:0 --model bytedance/seedance-2.5 --duration 20\n baker studio animate 'hold on the product, then a slow push-in' --from references --from-video j57vid\u2026 --model bytedance/seedance-2.5\n baker studio animate 'slow drone pull-back over a solar farm at golden hour, no people' --from text --model bytedance/seedance-2.5 --duration 12"
@@ -35563,7 +38358,7 @@ var animateCommand = defineCommand164({
35563
38358
  });
35564
38359
 
35565
38360
  // src/commands/studio/generate.ts
35566
- import { defineCommand as defineCommand165 } from "citty";
38361
+ import { defineCommand as defineCommand173 } from "citty";
35567
38362
  var MODEL_LIST2 = IMAGE_MODEL_IDS;
35568
38363
  var DEFAULT_MAX_WAIT_MS2 = 24e4;
35569
38364
  registerSchema({
@@ -35693,7 +38488,7 @@ function buildGenerateBody(args, prompt) {
35693
38488
  }
35694
38489
  return body;
35695
38490
  }
35696
- var generateCommand = defineCommand165({
38491
+ var generateCommand = defineCommand173({
35697
38492
  meta: {
35698
38493
  name: "generate",
35699
38494
  description: "Start here to make an image. Renders 1-8 takes of one brief, ingests each into the media library as it lands, and shows the batch in the dashboard Studio next to the ones the client ran.\n\nModel choice: google/gemini-3.1-flash-image-preview (default \u2014 fast, best at editing a reference and at extreme ratios), google/gemini-3-pro-image-preview (highest fidelity, slower), openai/gpt-image-2 (photoreal and the cleanest in-image text \u2014 no --image-size, no 4:5 / 5:4), recraft/recraft-v4.1-pro-vector (vector/flat marks with palette control).\n\n--reference is the biggest quality lever there is: a real logo, product shot, Pinterest pin or sandbox screenshot beats any amount of adjectives.\n\nExamples:\n baker studio generate 'matte black bottle on wet marble, hard studio light, 35mm' --aspect-ratio 3:2 --count 3\n baker studio generate 'this bottle on a sunlit kitchen counter' --reference './src/brand/product.png,https://\u2026/kitchen.jpg'\n baker studio generate 'founder-style selfie, kitchen background, natural light' --skill ugc-selfie-hook\n baker studio generate 'flat geometric mascot, brand palette' --model recraft/recraft-v4.1-pro-vector --rgb-colors '[[10,10,10],[255,80,0]]'"
@@ -35771,7 +38566,7 @@ var generateCommand = defineCommand165({
35771
38566
  });
35772
38567
 
35773
38568
  // src/commands/studio/get.ts
35774
- import { defineCommand as defineCommand166 } from "citty";
38569
+ import { defineCommand as defineCommand174 } from "citty";
35775
38570
  registerSchema({
35776
38571
  command: "studio.get",
35777
38572
  description: "Read one Studio batch: every take, where it lives, and why a take is missing. This is how you pick up a batch that was still rendering when the start command returned.",
@@ -35780,7 +38575,7 @@ registerSchema({
35780
38575
  full: { type: "boolean", description: "Include settings, references and attribution", required: false }
35781
38576
  }
35782
38577
  });
35783
- var getCommand4 = defineCommand166({
38578
+ var getCommand4 = defineCommand174({
35784
38579
  meta: {
35785
38580
  name: "get",
35786
38581
  description: "Read one Studio batch \u2014 the takes, their urls, whether each is in the library, and the reason for any that failed.\n\nExample: baker studio get j57abc123def456ghi789\nExample: baker studio get j57abc123def456ghi789 --full"
@@ -35809,7 +38604,7 @@ var getCommand4 = defineCommand166({
35809
38604
  });
35810
38605
 
35811
38606
  // src/commands/studio/improve.ts
35812
- import { defineCommand as defineCommand167 } from "citty";
38607
+ import { defineCommand as defineCommand175 } from "citty";
35813
38608
  var DESCRIPTION = "Sharpen a rough brief into directed art direction \u2014 the same rewrite the client gets from the wand in the Studio prompt bar. Reach for it when you are relaying the CLIENT's own words and want them shaped without substituting your voice; when you are writing the art direction yourself, just write it, because you will do a better job than this does.";
35814
38609
  registerSchema({
35815
38610
  command: "studio.improve",
@@ -35834,7 +38629,7 @@ registerSchema({
35834
38629
  }
35835
38630
  }
35836
38631
  });
35837
- var improveCommand = defineCommand167({
38632
+ var improveCommand = defineCommand175({
35838
38633
  meta: {
35839
38634
  name: "improve",
35840
38635
  description: `${DESCRIPTION}
@@ -35878,7 +38673,7 @@ Examples:
35878
38673
  });
35879
38674
 
35880
38675
  // src/commands/studio/keep.ts
35881
- import { defineCommand as defineCommand168 } from "citty";
38676
+ import { defineCommand as defineCommand176 } from "citty";
35882
38677
  registerSchema({
35883
38678
  command: "studio.keep",
35884
38679
  description: "Mark one take as the keeper. For an image this stars it, so the client reviewing the batch sees which one you used. For a CLIP it is the step that puts it in the video library \u2014 until then the clip cannot be used in a canvas, a landing, or an ad.",
@@ -35888,7 +38683,7 @@ registerSchema({
35888
38683
  undo: { type: "boolean", description: "Un-star an image, or take a kept clip back out", required: false }
35889
38684
  }
35890
38685
  });
35891
- var keepCommand = defineCommand168({
38686
+ var keepCommand = defineCommand176({
35892
38687
  meta: {
35893
38688
  name: "keep",
35894
38689
  description: "Mark one take as the keeper. An image gets starred (it was already in the library); a clip gets INGESTED into the video library, which is what makes it usable anywhere else.\n\nExample: baker studio keep j57abc123def456ghi789 --slot 2\nExample: baker studio keep j57abc123def456ghi789 --slot 2 --undo"
@@ -35939,7 +38734,7 @@ var keepCommand = defineCommand168({
35939
38734
  });
35940
38735
 
35941
38736
  // src/commands/studio/list.ts
35942
- import { defineCommand as defineCommand169 } from "citty";
38737
+ import { defineCommand as defineCommand177 } from "citty";
35943
38738
  registerSchema({
35944
38739
  command: "studio.list",
35945
38740
  description: "Recent Studio batches for THIS conversation, newest first \u2014 what you have already generated, so you re-use a take instead of paying for it twice. `--all` widens it to everything the company generated, including what people ran themselves in the dashboard.",
@@ -35950,7 +38745,7 @@ registerSchema({
35950
38745
  full: { type: "boolean", description: "Include settings, references and attribution", required: false }
35951
38746
  }
35952
38747
  });
35953
- var listCommand15 = defineCommand169({
38748
+ var listCommand15 = defineCommand177({
35954
38749
  meta: {
35955
38750
  name: "list",
35956
38751
  description: "Recent Studio batches, newest first. Scoped to this conversation unless you pass --all.\n\nExample: baker studio list\nExample: baker studio list --kind video --limit 5\nExample: baker studio list --all # includes batches the client ran in the dashboard"
@@ -35984,7 +38779,7 @@ var listCommand15 = defineCommand169({
35984
38779
  });
35985
38780
 
35986
38781
  // src/commands/studio/models.ts
35987
- import { defineCommand as defineCommand170 } from "citty";
38782
+ import { defineCommand as defineCommand178 } from "citty";
35988
38783
  var DESCRIPTION2 = "What each Studio model actually accepts: its shapes, resolutions, clip lengths, prompt character cap, how many reference images it takes, and which knobs it has. Read this before a batch you care about \u2014 the models disagree far more than they look like they do, and a setting the chosen model does not have is REFUSED, not ignored.";
35989
38784
  registerSchema({
35990
38785
  command: "studio.models",
@@ -36071,7 +38866,7 @@ function buildModelCards(kind, model) {
36071
38866
  const selected = model ? ids.filter((id) => id === model) : ids;
36072
38867
  return selected.map((id) => build(id));
36073
38868
  }
36074
- var modelsCommand = defineCommand170({
38869
+ var modelsCommand = defineCommand178({
36075
38870
  meta: {
36076
38871
  name: "models",
36077
38872
  description: `${DESCRIPTION2}
@@ -36118,13 +38913,13 @@ Examples:
36118
38913
  });
36119
38914
 
36120
38915
  // src/commands/studio/skills.ts
36121
- import { defineCommand as defineCommand171 } from "citty";
38916
+ import { defineCommand as defineCommand179 } from "citty";
36122
38917
  registerSchema({
36123
38918
  command: "studio.skills",
36124
38919
  description: "The craft directions `studio generate --skill <id>` accepts. Each one carries directed art direction plus the model, shape and take count it wants, so you pick a look by name instead of writing the boilerplate yourself.",
36125
38920
  args: {}
36126
38921
  });
36127
- var skillsCommand = defineCommand171({
38922
+ var skillsCommand = defineCommand179({
36128
38923
  meta: {
36129
38924
  name: "skills",
36130
38925
  description: "List the craft directions available to `baker studio generate --skill <id>` \u2014 what each one is for, whether it wants a reference image, and the model/shape/count it defaults to.\n\nExample: baker studio skills"
@@ -36148,7 +38943,7 @@ var skillsCommand = defineCommand171({
36148
38943
  });
36149
38944
 
36150
38945
  // src/commands/studio/index.ts
36151
- var studioCommand = defineCommand172({
38946
+ var studioCommand = defineCommand180({
36152
38947
  meta: {
36153
38948
  name: "studio",
36154
38949
  description: `Make new imagery and clips. Every batch is recorded and shows up in the dashboard Studio for the client to review, labelled with this conversation.
@@ -36191,10 +38986,10 @@ Full guide: __tooling__/docs/tools/baker/studio.md`
36191
38986
  });
36192
38987
 
36193
38988
  // src/commands/tag-manager/index.ts
36194
- import { defineCommand as defineCommand176 } from "citty";
38989
+ import { defineCommand as defineCommand184 } from "citty";
36195
38990
 
36196
38991
  // src/commands/tag-manager/draft.ts
36197
- import { defineCommand as defineCommand173 } from "citty";
38992
+ import { defineCommand as defineCommand181 } from "citty";
36198
38993
 
36199
38994
  // src/commands/tag-manager/shared.ts
36200
38995
  import { readFileSync as readFileSync13 } from "fs";
@@ -36261,10 +39056,10 @@ async function stageOp4(op) {
36261
39056
  handleError4(err);
36262
39057
  }
36263
39058
  }
36264
- async function draftAction3(path28, body, chat) {
39059
+ async function draftAction3(path35, body, chat) {
36265
39060
  const chatId = resolveChatId(chat);
36266
39061
  try {
36267
- const data = await apiPost(path28, { chatId, ...body });
39062
+ const data = await apiPost(path35, { chatId, ...body });
36268
39063
  writeJsonEnvelope({ ok: true, data });
36269
39064
  return data;
36270
39065
  } catch (err) {
@@ -36317,13 +39112,13 @@ registerSchema({
36317
39112
  chat: { type: "string", description: CHAT_READ_ARG.description, required: false }
36318
39113
  }
36319
39114
  });
36320
- var draftCommand4 = defineCommand173({
39115
+ var draftCommand4 = defineCommand181({
36321
39116
  meta: {
36322
39117
  name: "draft",
36323
39118
  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."
36324
39119
  },
36325
39120
  subCommands: {
36326
- list: defineCommand173({
39121
+ list: defineCommand181({
36327
39122
  meta: {
36328
39123
  name: "list",
36329
39124
  description: "Review everything staged on this chat (--json for the raw envelope)"
@@ -36336,7 +39131,7 @@ var draftCommand4 = defineCommand173({
36336
39131
  await draftList2(args.json === true, args.chat);
36337
39132
  }
36338
39133
  }),
36339
- show: defineCommand173({
39134
+ show: defineCommand181({
36340
39135
  meta: {
36341
39136
  name: "show",
36342
39137
  description: "Print the full staged payload for one change \u2014 the receipt to verify it looks right before publish (never truncated)."
@@ -36353,7 +39148,7 @@ var draftCommand4 = defineCommand173({
36353
39148
  );
36354
39149
  }
36355
39150
  }),
36356
- amend: defineCommand173({
39151
+ amend: defineCommand181({
36357
39152
  meta: {
36358
39153
  name: "amend",
36359
39154
  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."
@@ -36370,7 +39165,7 @@ var draftCommand4 = defineCommand173({
36370
39165
  });
36371
39166
  }
36372
39167
  }),
36373
- remove: defineCommand173({
39168
+ remove: defineCommand181({
36374
39169
  meta: { name: "remove", description: "Remove one staged change (cascades to anything depending on it)" },
36375
39170
  args: { ref: { type: "positional", description: "Staged ref (gtm_temp_*) or target", required: false } },
36376
39171
  run: async ({ args }) => {
@@ -36379,7 +39174,7 @@ var draftCommand4 = defineCommand173({
36379
39174
  });
36380
39175
  }
36381
39176
  }),
36382
- clear: defineCommand173({
39177
+ clear: defineCommand181({
36383
39178
  meta: { name: "clear", description: "Discard all Tag Manager changes staged on this chat" },
36384
39179
  run: async () => {
36385
39180
  await draftAction3("/api/tag-manager/draft/clear", {});
@@ -36389,7 +39184,7 @@ var draftCommand4 = defineCommand173({
36389
39184
  });
36390
39185
 
36391
39186
  // src/commands/tag-manager/read.ts
36392
- import { defineCommand as defineCommand174 } from "citty";
39187
+ import { defineCommand as defineCommand182 } from "citty";
36393
39188
  registerSchema({
36394
39189
  command: "tagManager.containers",
36395
39190
  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.",
@@ -36430,7 +39225,7 @@ function containersHints(containers) {
36430
39225
  }))
36431
39226
  });
36432
39227
  }
36433
- var containersCommand = defineCommand174({
39228
+ var containersCommand = defineCommand182({
36434
39229
  meta: {
36435
39230
  name: "containers",
36436
39231
  description: `List Tag Manager containers reachable by this company's connection.
@@ -36447,7 +39242,7 @@ Start here:
36447
39242
  }
36448
39243
  }
36449
39244
  });
36450
- var readCommand = defineCommand174({
39245
+ var readCommand = defineCommand182({
36451
39246
  meta: {
36452
39247
  name: "read",
36453
39248
  description: `Read the current contents of the Tag Manager container \u2014 always do this before staging changes.
@@ -36489,7 +39284,7 @@ Examples:
36489
39284
  });
36490
39285
 
36491
39286
  // src/commands/tag-manager/write-commands.ts
36492
- import { defineCommand as defineCommand175 } from "citty";
39287
+ import { defineCommand as defineCommand183 } from "citty";
36493
39288
  var CONTAINER_ARG_DESCRIPTION = "Numeric container id (optional only when one container is connected \u2014 run `baker tag-manager containers`)";
36494
39289
  var ENTITIES = [
36495
39290
  {
@@ -36545,10 +39340,10 @@ for (const { entity, noun, createHint } of ENTITIES) {
36545
39340
  });
36546
39341
  }
36547
39342
  function entityCommand(entity, noun, example) {
36548
- return defineCommand175({
39343
+ return defineCommand183({
36549
39344
  meta: { name: entity, description: `Stage ${noun} changes on this chat's Tag Manager draft` },
36550
39345
  subCommands: {
36551
- create: defineCommand175({
39346
+ create: defineCommand183({
36552
39347
  meta: {
36553
39348
  name: "create",
36554
39349
  description: `Stage a new ${noun}
@@ -36570,7 +39365,7 @@ Examples:
36570
39365
  });
36571
39366
  }
36572
39367
  }),
36573
- update: defineCommand175({
39368
+ update: defineCommand183({
36574
39369
  meta: {
36575
39370
  name: "update",
36576
39371
  description: `Stage an update to an existing ${noun} (pass its id or path)`
@@ -36590,7 +39385,7 @@ Examples:
36590
39385
  });
36591
39386
  }
36592
39387
  }),
36593
- delete: defineCommand175({
39388
+ delete: defineCommand183({
36594
39389
  meta: { name: "delete", description: `Stage the deletion of a ${noun} (pass its id or path)` },
36595
39390
  args: {
36596
39391
  id: { type: "positional", description: `${noun} id or path`, required: false },
@@ -36631,7 +39426,7 @@ function builtinTypes(args) {
36631
39426
  }
36632
39427
  return raw.split(",").map((entry) => entry.trim());
36633
39428
  }
36634
- var builtinCommand = defineCommand175({
39429
+ var builtinCommand = defineCommand183({
36635
39430
  meta: {
36636
39431
  name: "builtin",
36637
39432
  description: `Enable or disable built-in variables
@@ -36641,7 +39436,7 @@ Examples:
36641
39436
  baker tag-manager builtin disable --types formId`
36642
39437
  },
36643
39438
  subCommands: {
36644
- enable: defineCommand175({
39439
+ enable: defineCommand183({
36645
39440
  meta: { name: "enable", description: "Stage enabling built-in variables" },
36646
39441
  args: {
36647
39442
  types: { type: "string", description: "Comma-separated types", required: false },
@@ -36655,7 +39450,7 @@ Examples:
36655
39450
  });
36656
39451
  }
36657
39452
  }),
36658
- disable: defineCommand175({
39453
+ disable: defineCommand183({
36659
39454
  meta: { name: "disable", description: "Stage disabling built-in variables" },
36660
39455
  args: {
36661
39456
  types: { type: "string", description: "Comma-separated types", required: false },
@@ -36673,7 +39468,7 @@ Examples:
36673
39468
  });
36674
39469
 
36675
39470
  // src/commands/tag-manager/index.ts
36676
- var tagManagerCommand = defineCommand176({
39471
+ var tagManagerCommand = defineCommand184({
36677
39472
  meta: {
36678
39473
  name: "tag-manager",
36679
39474
  description: `Read and change what lives inside the client's Google Tag Manager container \u2014 tags, triggers, variables, folders and built-in variables.
@@ -36710,7 +39505,7 @@ Full guide: __tooling__/docs/tools/baker/tag-manager.md`
36710
39505
  });
36711
39506
 
36712
39507
  // src/commands/tags/index.ts
36713
- import { defineCommand as defineCommand177 } from "citty";
39508
+ import { defineCommand as defineCommand185 } from "citty";
36714
39509
 
36715
39510
  // src/commands/tags/shared.ts
36716
39511
  function failApi3(err) {
@@ -36779,7 +39574,7 @@ async function listTags(json) {
36779
39574
  var listArgs9 = {
36780
39575
  json: { type: "boolean", description: "Print the raw JSON envelope instead of the readable list" }
36781
39576
  };
36782
- var listCommand16 = defineCommand177({
39577
+ var listCommand16 = defineCommand185({
36783
39578
  meta: {
36784
39579
  name: "list",
36785
39580
  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"
@@ -36798,7 +39593,7 @@ async function listDraft3(chat) {
36798
39593
  failApi3(err);
36799
39594
  }
36800
39595
  }
36801
- var draftCommand5 = defineCommand177({
39596
+ var draftCommand5 = defineCommand185({
36802
39597
  meta: {
36803
39598
  name: "draft",
36804
39599
  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."
@@ -36808,7 +39603,7 @@ var draftCommand5 = defineCommand177({
36808
39603
  await listDraft3(args.chat);
36809
39604
  }
36810
39605
  });
36811
- var tagsCommand3 = defineCommand177({
39606
+ var tagsCommand3 = defineCommand185({
36812
39607
  meta: {
36813
39608
  name: "tags",
36814
39609
  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.
@@ -36837,10 +39632,10 @@ Full guide: __tooling__/docs/tools/baker/tags.md`
36837
39632
  });
36838
39633
 
36839
39634
  // src/commands/testimonials/index.ts
36840
- import { defineCommand as defineCommand181 } from "citty";
39635
+ import { defineCommand as defineCommand189 } from "citty";
36841
39636
 
36842
39637
  // src/commands/testimonials/get.ts
36843
- import { defineCommand as defineCommand178 } from "citty";
39638
+ import { defineCommand as defineCommand186 } from "citty";
36844
39639
  registerSchema({
36845
39640
  command: "testimonials.get",
36846
39641
  description: "Get a single testimonial by ID",
@@ -36848,7 +39643,7 @@ registerSchema({
36848
39643
  id: { type: "string", description: "Testimonial ID", required: true }
36849
39644
  }
36850
39645
  });
36851
- var getCommand5 = defineCommand178({
39646
+ var getCommand5 = defineCommand186({
36852
39647
  meta: { name: "get", description: "Get a single testimonial by ID. Example: baker testimonials get j571abc123" },
36853
39648
  args: {
36854
39649
  id: { type: "positional", description: "Testimonial ID", required: false },
@@ -36885,7 +39680,7 @@ var getCommand5 = defineCommand178({
36885
39680
  });
36886
39681
 
36887
39682
  // src/commands/testimonials/list.ts
36888
- import { defineCommand as defineCommand179 } from "citty";
39683
+ import { defineCommand as defineCommand187 } from "citty";
36889
39684
  registerSchema({
36890
39685
  command: "testimonials.list",
36891
39686
  description: "List testimonials with optional filters.",
@@ -36915,7 +39710,7 @@ registerSchema({
36915
39710
  limit: { type: "number", description: "Max results (default 50)", required: false, default: 50 }
36916
39711
  }
36917
39712
  });
36918
- var listCommand17 = defineCommand179({
39713
+ var listCommand17 = defineCommand187({
36919
39714
  meta: {
36920
39715
  name: "list",
36921
39716
  description: "List testimonials with optional filters. Example: baker testimonials list --source google --sentiment positive"
@@ -36964,7 +39759,7 @@ var listCommand17 = defineCommand179({
36964
39759
  });
36965
39760
 
36966
39761
  // src/commands/testimonials/search.ts
36967
- import { defineCommand as defineCommand180 } from "citty";
39762
+ import { defineCommand as defineCommand188 } from "citty";
36968
39763
  function languageBiasHint(results, requestedLanguage) {
36969
39764
  if (requestedLanguage) {
36970
39765
  return null;
@@ -37042,7 +39837,7 @@ function buildSearchRequest(query, args) {
37042
39837
  }
37043
39838
  return body;
37044
39839
  }
37045
- var searchCommand2 = defineCommand180({
39840
+ var searchCommand3 = defineCommand188({
37046
39841
  meta: {
37047
39842
  name: "search",
37048
39843
  description: "Semantic search testimonials by text query. Uses hybrid BM25 + vector + reranking. Example: baker testimonials search 'great service' --rating-min 4"
@@ -37098,7 +39893,7 @@ var searchCommand2 = defineCommand180({
37098
39893
  var tagsCommand4 = makeTagsCommand("testimonials", "testimonial", "/api/testimonials/tags");
37099
39894
 
37100
39895
  // src/commands/testimonials/index.ts
37101
- var testimonialsCommand = defineCommand181({
39896
+ var testimonialsCommand = defineCommand189({
37102
39897
  meta: {
37103
39898
  name: "testimonials",
37104
39899
  description: `Find and browse testimonials in Baker. Subcommands: search, get, list, tags.
@@ -37113,17 +39908,17 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
37113
39908
  },
37114
39909
  subCommands: {
37115
39910
  get: getCommand5,
37116
- search: searchCommand2,
39911
+ search: searchCommand3,
37117
39912
  list: listCommand17,
37118
39913
  tags: tagsCommand4
37119
39914
  }
37120
39915
  });
37121
39916
 
37122
39917
  // src/commands/videos/index.ts
37123
- import { defineCommand as defineCommand187 } from "citty";
39918
+ import { defineCommand as defineCommand195 } from "citty";
37124
39919
 
37125
39920
  // src/commands/videos/delete.ts
37126
- import { defineCommand as defineCommand182 } from "citty";
39921
+ import { defineCommand as defineCommand190 } from "citty";
37127
39922
  registerSchema({
37128
39923
  command: "videos.delete",
37129
39924
  description: "Delete a video by ID",
@@ -37137,7 +39932,7 @@ registerSchema({
37137
39932
  }
37138
39933
  }
37139
39934
  });
37140
- var deleteCommand3 = defineCommand182({
39935
+ var deleteCommand3 = defineCommand190({
37141
39936
  meta: {
37142
39937
  name: "delete",
37143
39938
  description: "Delete a video by ID. Use --dry-run to preview. Example: baker videos delete j571abc123 --dry-run"
@@ -37178,7 +39973,7 @@ var deleteCommand3 = defineCommand182({
37178
39973
  });
37179
39974
 
37180
39975
  // src/commands/videos/get.ts
37181
- import { defineCommand as defineCommand183 } from "citty";
39976
+ import { defineCommand as defineCommand191 } from "citty";
37182
39977
  registerSchema({
37183
39978
  command: "videos.get",
37184
39979
  description: "Get a single video by ID",
@@ -37186,7 +39981,7 @@ registerSchema({
37186
39981
  id: { type: "string", description: "Video ID", required: true }
37187
39982
  }
37188
39983
  });
37189
- var getCommand6 = defineCommand183({
39984
+ var getCommand6 = defineCommand191({
37190
39985
  meta: { name: "get", description: "Get a single video by ID. Example: baker videos get j571abc123" },
37191
39986
  args: {
37192
39987
  id: { type: "positional", description: "Video ID", required: false },
@@ -37223,7 +40018,7 @@ var getCommand6 = defineCommand183({
37223
40018
  });
37224
40019
 
37225
40020
  // src/commands/videos/group.ts
37226
- import { defineCommand as defineCommand184 } from "citty";
40021
+ import { defineCommand as defineCommand192 } from "citty";
37227
40022
  registerSchema({
37228
40023
  command: "videos.group",
37229
40024
  description: "List every clip and image that arrived in the same set as this video (carousel slides, one page)",
@@ -37232,7 +40027,7 @@ registerSchema({
37232
40027
  "group-key": { type: "string", description: "The set key directly, when you already have it", required: false }
37233
40028
  }
37234
40029
  });
37235
- var groupCommand2 = defineCommand184({
40030
+ var groupCommand2 = defineCommand192({
37236
40031
  meta: {
37237
40032
  name: "group",
37238
40033
  description: "List every asset that arrived in the same set as this clip \u2014 the other slides of the Instagram post it came from, stills included. A carousel is authored to be read in order, so a clip pulled out of one is usually missing half its meaning. Example: baker videos group <videoId>"
@@ -37252,7 +40047,7 @@ var groupCommand2 = defineCommand184({
37252
40047
  });
37253
40048
 
37254
40049
  // src/commands/videos/search.ts
37255
- import { defineCommand as defineCommand185 } from "citty";
40050
+ import { defineCommand as defineCommand193 } from "citty";
37256
40051
  registerSchema({
37257
40052
  command: "videos.search",
37258
40053
  description: "Search videos by text query. Only returns ready videos.",
@@ -37262,7 +40057,7 @@ registerSchema({
37262
40057
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
37263
40058
  }
37264
40059
  });
37265
- var searchCommand3 = defineCommand185({
40060
+ var searchCommand4 = defineCommand193({
37266
40061
  meta: {
37267
40062
  name: "search",
37268
40063
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -37312,9 +40107,9 @@ var searchCommand3 = defineCommand185({
37312
40107
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
37313
40108
 
37314
40109
  // src/commands/videos/upload.ts
37315
- import { readFile as readFile22, stat as stat7 } from "fs/promises";
40110
+ import { readFile as readFile24, stat as stat7 } from "fs/promises";
37316
40111
  import { basename as basename2, extname as extname3 } from "path";
37317
- import { defineCommand as defineCommand186 } from "citty";
40112
+ import { defineCommand as defineCommand194 } from "citty";
37318
40113
  var MIME_MAP = {
37319
40114
  ".mp4": "video/mp4",
37320
40115
  ".mov": "video/quicktime",
@@ -37353,7 +40148,7 @@ function detectContentType(filePath) {
37353
40148
  }
37354
40149
  return mime;
37355
40150
  }
37356
- var uploadCommand2 = defineCommand186({
40151
+ var uploadCommand2 = defineCommand194({
37357
40152
  meta: {
37358
40153
  name: "upload",
37359
40154
  description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
@@ -37392,7 +40187,7 @@ var uploadCommand2 = defineCommand186({
37392
40187
  originalFilename,
37393
40188
  descriptionContext
37394
40189
  });
37395
- const fileBuffer = await readFile22(filePath);
40190
+ const fileBuffer = await readFile24(filePath);
37396
40191
  const uploadResponse = await fetch(uploadUrl, {
37397
40192
  method: "PUT",
37398
40193
  headers: { "Content-Type": contentType },
@@ -37423,7 +40218,7 @@ var uploadCommand2 = defineCommand186({
37423
40218
  });
37424
40219
 
37425
40220
  // src/commands/videos/index.ts
37426
- var videosCommand = defineCommand187({
40221
+ var videosCommand = defineCommand195({
37427
40222
  meta: {
37428
40223
  name: "videos",
37429
40224
  description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
@@ -37440,7 +40235,7 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
37440
40235
  subCommands: {
37441
40236
  get: getCommand6,
37442
40237
  group: groupCommand2,
37443
- search: searchCommand3,
40238
+ search: searchCommand4,
37444
40239
  upload: uploadCommand2,
37445
40240
  delete: deleteCommand3,
37446
40241
  tags: tagsCommand5
@@ -37448,19 +40243,19 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
37448
40243
  });
37449
40244
 
37450
40245
  // src/commands/winning-ads/index.ts
37451
- import { defineCommand as defineCommand200 } from "citty";
40246
+ import { defineCommand as defineCommand208 } from "citty";
37452
40247
 
37453
40248
  // src/commands/winning-ads/advertisers.ts
37454
- import { defineCommand as defineCommand188 } from "citty";
40249
+ import { defineCommand as defineCommand196 } from "citty";
37455
40250
 
37456
40251
  // src/commands/winning-ads/shared.ts
37457
- function splitList(value) {
40252
+ function splitList2(value) {
37458
40253
  if (!value) {
37459
40254
  return [];
37460
40255
  }
37461
40256
  return value.split(",").map((v) => v.trim()).filter(Boolean);
37462
40257
  }
37463
- function reportError(err) {
40258
+ function reportError2(err) {
37464
40259
  if (err instanceof ApiError) {
37465
40260
  writeJson({ ok: false, error: { code: err.code, message: err.message } });
37466
40261
  process.exit(1);
@@ -37504,7 +40299,7 @@ function advertiserNormalizer(record, full) {
37504
40299
  last_synced_at: record.last_synced_at ?? null
37505
40300
  };
37506
40301
  }
37507
- var advertisersCommand2 = defineCommand188({
40302
+ var advertisersCommand2 = defineCommand196({
37508
40303
  meta: {
37509
40304
  name: "advertisers",
37510
40305
  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'
@@ -37556,13 +40351,13 @@ var advertisersCommand2 = defineCommand188({
37556
40351
  advertiserNormalizer
37557
40352
  );
37558
40353
  } catch (err) {
37559
- reportError(err);
40354
+ reportError2(err);
37560
40355
  }
37561
40356
  }
37562
40357
  });
37563
40358
 
37564
40359
  // src/commands/winning-ads/brief.ts
37565
- import { defineCommand as defineCommand189 } from "citty";
40360
+ import { defineCommand as defineCommand197 } from "citty";
37566
40361
  registerSchema({
37567
40362
  command: "winning-ads.brief",
37568
40363
  description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
@@ -37608,7 +40403,7 @@ function parseDna(raw) {
37608
40403
  }
37609
40404
  return parsed;
37610
40405
  }
37611
- var briefCommand = defineCommand189({
40406
+ var briefCommand = defineCommand197({
37612
40407
  meta: {
37613
40408
  name: "brief",
37614
40409
  description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
@@ -37638,13 +40433,13 @@ var briefCommand = defineCommand189({
37638
40433
  const data = await apiPost("/api/ad-library/brief", body);
37639
40434
  writeJson({ ok: true, data });
37640
40435
  } catch (err) {
37641
- reportError(err);
40436
+ reportError2(err);
37642
40437
  }
37643
40438
  }
37644
40439
  });
37645
40440
 
37646
40441
  // src/commands/winning-ads/content.ts
37647
- import { defineCommand as defineCommand190 } from "citty";
40442
+ import { defineCommand as defineCommand198 } from "citty";
37648
40443
  registerSchema({
37649
40444
  command: "winning-ads.content",
37650
40445
  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.",
@@ -37657,7 +40452,7 @@ registerSchema({
37657
40452
  }
37658
40453
  }
37659
40454
  });
37660
- var contentCommand = defineCommand190({
40455
+ var contentCommand = defineCommand198({
37661
40456
  meta: {
37662
40457
  name: "content",
37663
40458
  description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
@@ -37700,16 +40495,16 @@ var contentCommand = defineCommand190({
37700
40495
  adContentNormalizer
37701
40496
  );
37702
40497
  } catch (err) {
37703
- reportError(err);
40498
+ reportError2(err);
37704
40499
  }
37705
40500
  }
37706
40501
  });
37707
40502
 
37708
40503
  // src/commands/winning-ads/feed.ts
37709
- import { defineCommand as defineCommand191 } from "citty";
40504
+ import { defineCommand as defineCommand199 } from "citty";
37710
40505
  function buildFeedParams(input) {
37711
40506
  const params = {};
37712
- const advertiser = splitList(input.advertiser);
40507
+ const advertiser = splitList2(input.advertiser);
37713
40508
  if (advertiser.length > 0) {
37714
40509
  params.advertiser = advertiser.join(",");
37715
40510
  }
@@ -37722,11 +40517,11 @@ function buildFeedParams(input) {
37722
40517
  if (input.limit !== void 0 && input.limit !== "") {
37723
40518
  params.limit = input.limit;
37724
40519
  }
37725
- const winnerCategory = splitList(input.winnerCategory);
40520
+ const winnerCategory = splitList2(input.winnerCategory);
37726
40521
  if (winnerCategory.length > 0) {
37727
40522
  params.winner_category = winnerCategory.join(",");
37728
40523
  }
37729
- const format = splitList(input.format);
40524
+ const format = splitList2(input.format);
37730
40525
  if (format.length > 0) {
37731
40526
  params.format = format.join(",");
37732
40527
  }
@@ -37758,7 +40553,7 @@ registerSchema({
37758
40553
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
37759
40554
  }
37760
40555
  });
37761
- var feedCommand = defineCommand191({
40556
+ var feedCommand = defineCommand199({
37762
40557
  meta: {
37763
40558
  name: "feed",
37764
40559
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -37837,13 +40632,13 @@ var feedCommand = defineCommand191({
37837
40632
  `);
37838
40633
  }
37839
40634
  } catch (err) {
37840
- reportError(err);
40635
+ reportError2(err);
37841
40636
  }
37842
40637
  }
37843
40638
  });
37844
40639
 
37845
40640
  // src/commands/winning-ads/follow.ts
37846
- import { defineCommand as defineCommand192 } from "citty";
40641
+ import { defineCommand as defineCommand200 } from "citty";
37847
40642
  var PLATFORMS = ["meta", "linkedin"];
37848
40643
  registerSchema({
37849
40644
  command: "winning-ads.follow",
@@ -37858,7 +40653,7 @@ registerSchema({
37858
40653
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
37859
40654
  }
37860
40655
  });
37861
- var followCommand = defineCommand192({
40656
+ var followCommand = defineCommand200({
37862
40657
  meta: {
37863
40658
  name: "follow",
37864
40659
  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'
@@ -37899,19 +40694,19 @@ var followCommand = defineCommand192({
37899
40694
  }
37900
40695
  writeJson({ ok: true, data, hints });
37901
40696
  } catch (err) {
37902
- reportError(err);
40697
+ reportError2(err);
37903
40698
  }
37904
40699
  }
37905
40700
  });
37906
40701
 
37907
40702
  // src/commands/winning-ads/follow-competitors.ts
37908
- import { defineCommand as defineCommand193 } from "citty";
40703
+ import { defineCommand as defineCommand201 } from "citty";
37909
40704
  var PLATFORMS2 = ["meta", "linkedin"];
37910
40705
  var BATCH_TIMEOUT_MS = 3e5;
37911
40706
  function buildFollowBatchBody(input) {
37912
40707
  const seen = /* @__PURE__ */ new Set();
37913
40708
  const inputs = [];
37914
- for (const domain of splitList(input.domains)) {
40709
+ for (const domain of splitList2(input.domains)) {
37915
40710
  const key = domain.toLowerCase();
37916
40711
  if (seen.has(key)) {
37917
40712
  continue;
@@ -37938,7 +40733,7 @@ registerSchema({
37938
40733
  }
37939
40734
  }
37940
40735
  });
37941
- var followCompetitorsCommand = defineCommand193({
40736
+ var followCompetitorsCommand = defineCommand201({
37942
40737
  meta: {
37943
40738
  name: "follow-competitors",
37944
40739
  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"'
@@ -38007,13 +40802,13 @@ var followCompetitorsCommand = defineCommand193({
38007
40802
  }
38008
40803
  writeJson({ ok: true, data, hints: hints.length > 0 ? hints : void 0 });
38009
40804
  } catch (err) {
38010
- reportError(err);
40805
+ reportError2(err);
38011
40806
  }
38012
40807
  }
38013
40808
  });
38014
40809
 
38015
40810
  // src/commands/winning-ads/following.ts
38016
- import { defineCommand as defineCommand194 } from "citty";
40811
+ import { defineCommand as defineCommand202 } from "citty";
38017
40812
  registerSchema({
38018
40813
  command: "winning-ads.following",
38019
40814
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -38046,7 +40841,7 @@ function followingNormalizer(record, full) {
38046
40841
  platforms: Array.isArray(record.platforms) ? record.platforms : []
38047
40842
  };
38048
40843
  }
38049
- var followingCommand = defineCommand194({
40844
+ var followingCommand = defineCommand202({
38050
40845
  meta: {
38051
40846
  name: "following",
38052
40847
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -38075,13 +40870,13 @@ var followingCommand = defineCommand194({
38075
40870
  followingNormalizer
38076
40871
  );
38077
40872
  } catch (err) {
38078
- reportError(err);
40873
+ reportError2(err);
38079
40874
  }
38080
40875
  }
38081
40876
  });
38082
40877
 
38083
40878
  // src/commands/winning-ads/patterns.ts
38084
- import { defineCommand as defineCommand195 } from "citty";
40879
+ import { defineCommand as defineCommand203 } from "citty";
38085
40880
  registerSchema({
38086
40881
  command: "winning-ads.patterns",
38087
40882
  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.",
@@ -38096,8 +40891,8 @@ registerSchema({
38096
40891
  }
38097
40892
  });
38098
40893
  function buildPatternsBody(args) {
38099
- const winners = splitList(args.winners);
38100
- const duds = splitList(args.duds);
40894
+ const winners = splitList2(args.winners);
40895
+ const duds = splitList2(args.duds);
38101
40896
  if (!winners.length || !duds.length) {
38102
40897
  throw new Error("Provide at least one ad id for both --winners and --duds");
38103
40898
  }
@@ -38120,7 +40915,7 @@ function discriminatorRow(record) {
38120
40915
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
38121
40916
  };
38122
40917
  }
38123
- var patternsCommand = defineCommand195({
40918
+ var patternsCommand = defineCommand203({
38124
40919
  meta: {
38125
40920
  name: "patterns",
38126
40921
  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"
@@ -38170,13 +40965,13 @@ var patternsCommand = defineCommand195({
38170
40965
  (record) => discriminatorRow(record)
38171
40966
  );
38172
40967
  } catch (err) {
38173
- reportError(err);
40968
+ reportError2(err);
38174
40969
  }
38175
40970
  }
38176
40971
  });
38177
40972
 
38178
40973
  // src/commands/winning-ads/search.ts
38179
- import { defineCommand as defineCommand196 } from "citty";
40974
+ import { defineCommand as defineCommand204 } from "citty";
38180
40975
  registerSchema({
38181
40976
  command: "winning-ads.search",
38182
40977
  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.",
@@ -38246,7 +41041,7 @@ function setNumber(body, key, value) {
38246
41041
  }
38247
41042
  }
38248
41043
  function setList(target, key, value) {
38249
- const list = splitList(value);
41044
+ const list = splitList2(value);
38250
41045
  if (list.length) {
38251
41046
  target[key] = list;
38252
41047
  }
@@ -38256,7 +41051,7 @@ function setString(target, key, value) {
38256
41051
  target[key] = value;
38257
41052
  }
38258
41053
  }
38259
- function buildSearchBody(args) {
41054
+ function buildSearchBody2(args) {
38260
41055
  const body = {};
38261
41056
  if (args.query) {
38262
41057
  body.free_text_query = args.query;
@@ -38284,7 +41079,7 @@ function buildSearchBody(args) {
38284
41079
  }
38285
41080
  return body;
38286
41081
  }
38287
- var searchCommand4 = defineCommand196({
41082
+ var searchCommand5 = defineCommand204({
38288
41083
  meta: {
38289
41084
  name: "search",
38290
41085
  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"
@@ -38360,7 +41155,7 @@ var searchCommand4 = defineCommand196({
38360
41155
  firstSeenBefore: args["first-seen-before"],
38361
41156
  hookArchetype: args["hook-archetype"]
38362
41157
  };
38363
- const body = buildSearchBody(searchArgs);
41158
+ const body = buildSearchBody2(searchArgs);
38364
41159
  if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
38365
41160
  writeJson({
38366
41161
  ok: false,
@@ -38393,13 +41188,13 @@ var searchCommand4 = defineCommand196({
38393
41188
  winningAdNormalizer
38394
41189
  );
38395
41190
  } catch (err) {
38396
- reportError(err);
41191
+ reportError2(err);
38397
41192
  }
38398
41193
  }
38399
41194
  });
38400
41195
 
38401
41196
  // src/commands/winning-ads/seeds.ts
38402
- import { defineCommand as defineCommand197 } from "citty";
41197
+ import { defineCommand as defineCommand205 } from "citty";
38403
41198
  function leanRow(r) {
38404
41199
  return {
38405
41200
  key: r.key,
@@ -38427,7 +41222,7 @@ function makeSeedCommand(opts) {
38427
41222
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
38428
41223
  }
38429
41224
  });
38430
- return defineCommand197({
41225
+ return defineCommand205({
38431
41226
  meta: { name: opts.name, description: opts.description },
38432
41227
  args: {
38433
41228
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -38454,7 +41249,7 @@ function makeSeedCommand(opts) {
38454
41249
  }
38455
41250
  writeOutput({ ok: true, data: projected }, output);
38456
41251
  } catch (err) {
38457
- reportError(err);
41252
+ reportError2(err);
38458
41253
  }
38459
41254
  }
38460
41255
  });
@@ -38476,7 +41271,7 @@ var formatsCommand = makeSeedCommand({
38476
41271
  });
38477
41272
 
38478
41273
  // src/commands/winning-ads/unfollow.ts
38479
- import { defineCommand as defineCommand198 } from "citty";
41274
+ import { defineCommand as defineCommand206 } from "citty";
38480
41275
  registerSchema({
38481
41276
  command: "winning-ads.unfollow",
38482
41277
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -38484,7 +41279,7 @@ registerSchema({
38484
41279
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
38485
41280
  }
38486
41281
  });
38487
- var unfollowCommand = defineCommand198({
41282
+ var unfollowCommand = defineCommand206({
38488
41283
  meta: {
38489
41284
  name: "unfollow",
38490
41285
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -38499,13 +41294,13 @@ var unfollowCommand = defineCommand198({
38499
41294
  });
38500
41295
  writeJson({ ok: true, data });
38501
41296
  } catch (err) {
38502
- reportError(err);
41297
+ reportError2(err);
38503
41298
  }
38504
41299
  }
38505
41300
  });
38506
41301
 
38507
41302
  // src/commands/winning-ads/winners.ts
38508
- import { defineCommand as defineCommand199 } from "citty";
41303
+ import { defineCommand as defineCommand207 } from "citty";
38509
41304
  registerSchema({
38510
41305
  command: "winning-ads.winners",
38511
41306
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -38515,7 +41310,7 @@ registerSchema({
38515
41310
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
38516
41311
  }
38517
41312
  });
38518
- var winnersCommand = defineCommand199({
41313
+ var winnersCommand = defineCommand207({
38519
41314
  meta: {
38520
41315
  name: "winners",
38521
41316
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -38559,13 +41354,13 @@ var winnersCommand = defineCommand199({
38559
41354
  winningAdNormalizer
38560
41355
  );
38561
41356
  } catch (err) {
38562
- reportError(err);
41357
+ reportError2(err);
38563
41358
  }
38564
41359
  }
38565
41360
  });
38566
41361
 
38567
41362
  // src/commands/winning-ads/index.ts
38568
- var winningAdsCommand = defineCommand200({
41363
+ var winningAdsCommand = defineCommand208({
38569
41364
  meta: {
38570
41365
  name: "winning-ads",
38571
41366
  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.
@@ -38603,7 +41398,7 @@ Examples:
38603
41398
  Full guide: __tooling__/docs/tools/baker/winning-ads.md`
38604
41399
  },
38605
41400
  subCommands: {
38606
- search: searchCommand4,
41401
+ search: searchCommand5,
38607
41402
  advertisers: advertisersCommand2,
38608
41403
  follow: followCommand,
38609
41404
  "follow-competitors": followCompetitorsCommand,
@@ -38637,7 +41432,7 @@ function getCliVersion() {
38637
41432
  }
38638
41433
 
38639
41434
  // src/cli.ts
38640
- var main = defineCommand201({
41435
+ var main = defineCommand209({
38641
41436
  meta: {
38642
41437
  name: "baker",
38643
41438
  version: getCliVersion(),