@koda-sl/baker-cli 0.195.0-dev.6d2f498f5 → 0.196.0-dev.6d2f498f5

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.
Files changed (30) hide show
  1. package/README.md +11 -7
  2. package/dist/{chunk-PXXJ3HJW.js → chunk-34J26VIJ.js} +4 -4
  3. package/dist/chunk-F6OHDJIX.js +264 -0
  4. package/dist/chunk-F6OHDJIX.js.map +1 -0
  5. package/dist/{chunk-X5C6HE24.js → chunk-G3O4HRYR.js} +3 -3
  6. package/dist/{chunk-ZWYQIEBI.js → chunk-OB26WXHZ.js} +3 -3
  7. package/dist/{chunk-IDRQDUBA.js → chunk-SSDYBIZB.js} +290 -16
  8. package/dist/chunk-SSDYBIZB.js.map +1 -0
  9. package/dist/{chunk-K3PWXVF7.js → chunk-YUTDQ4PV.js} +2 -2
  10. package/dist/cli.js +307 -103
  11. package/dist/cli.js.map +1 -1
  12. package/dist/client-LTDQPQ7T.js +15 -0
  13. package/dist/engine/index.js +3 -3
  14. package/dist/env-GV4VDT5W.js +19 -0
  15. package/dist/{output-NWX3YW64.js → output-PXTEFDVX.js} +5 -5
  16. package/dist/{shared-5ZEOG664.js → shared-AR6VAAIE.js} +6 -6
  17. package/package.json +3 -1
  18. package/dist/chunk-IDRQDUBA.js.map +0 -1
  19. package/dist/chunk-YL3HDEIJ.js +0 -76
  20. package/dist/chunk-YL3HDEIJ.js.map +0 -1
  21. package/dist/client-PJ7ID35L.js +0 -15
  22. package/dist/env-6QJCMTRK.js +0 -13
  23. /package/dist/{chunk-PXXJ3HJW.js.map → chunk-34J26VIJ.js.map} +0 -0
  24. /package/dist/{chunk-X5C6HE24.js.map → chunk-G3O4HRYR.js.map} +0 -0
  25. /package/dist/{chunk-ZWYQIEBI.js.map → chunk-OB26WXHZ.js.map} +0 -0
  26. /package/dist/{chunk-K3PWXVF7.js.map → chunk-YUTDQ4PV.js.map} +0 -0
  27. /package/dist/{client-PJ7ID35L.js.map → client-LTDQPQ7T.js.map} +0 -0
  28. /package/dist/{env-6QJCMTRK.js.map → env-GV4VDT5W.js.map} +0 -0
  29. /package/dist/{output-NWX3YW64.js.map → output-PXTEFDVX.js.map} +0 -0
  30. /package/dist/{shared-5ZEOG664.js.map → shared-AR6VAAIE.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debugLogSetting
3
- } from "./chunk-YL3HDEIJ.js";
3
+ } from "./chunk-F6OHDJIX.js";
4
4
 
5
5
  // src/debugLog.ts
6
6
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
@@ -150,4 +150,4 @@ export {
150
150
  readBodyForLog,
151
151
  installStreamTaps
152
152
  };
153
- //# sourceMappingURL=chunk-K3PWXVF7.js.map
153
+ //# sourceMappingURL=chunk-YUTDQ4PV.js.map
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  describeFailureReason,
25
25
  elementMentionKeywords,
26
26
  estimateVideoCredits,
27
+ fetchExternalBytes,
27
28
  generateCatalog,
28
29
  imageProfileFor,
29
30
  isPersistedAssetRef,
@@ -47,7 +48,7 @@ import {
47
48
  toModelSafeImage,
48
49
  ulid,
49
50
  validateCanvasDeep
50
- } from "./chunk-IDRQDUBA.js";
51
+ } from "./chunk-SSDYBIZB.js";
51
52
  import {
52
53
  csvOrJson,
53
54
  daysAgoIso,
@@ -56,7 +57,7 @@ import {
56
57
  resolveAccountIdArg,
57
58
  resolveEffectiveStatus,
58
59
  todayIso
59
- } from "./chunk-PXXJ3HJW.js";
60
+ } from "./chunk-34J26VIJ.js";
60
61
  import {
61
62
  buildQueryCacheKey,
62
63
  cacheGet,
@@ -72,22 +73,28 @@ import {
72
73
  writeAdsJson,
73
74
  writeAdsOutput,
74
75
  writeJsonEnvelope
75
- } from "./chunk-X5C6HE24.js";
76
+ } from "./chunk-G3O4HRYR.js";
76
77
  import {
77
78
  ApiError,
78
79
  apiGet,
79
80
  apiPost,
80
81
  validateConvexId
81
- } from "./chunk-ZWYQIEBI.js";
82
+ } from "./chunk-OB26WXHZ.js";
82
83
  import {
83
84
  installStreamTaps,
84
85
  logInvocation
85
- } from "./chunk-K3PWXVF7.js";
86
+ } from "./chunk-YUTDQ4PV.js";
86
87
  import {
88
+ captureBudgetMs,
89
+ captureProxyCredentials,
87
90
  getEnv,
91
+ isProxyFailure,
92
+ plannedRoutes,
93
+ refuseNonPublicUrl,
88
94
  requireChatId,
89
- resolveChatId
90
- } from "./chunk-YL3HDEIJ.js";
95
+ resolveChatId,
96
+ shouldEscalate
97
+ } from "./chunk-F6OHDJIX.js";
91
98
 
92
99
  // src/cli.ts
93
100
  import { defineCommand as defineCommand212, runMain } from "citty";
@@ -483,15 +490,26 @@ function writeOutput(envelope, format, fields, full, normalizer) {
483
490
  writeJson(envelope);
484
491
  return;
485
492
  }
493
+ if (format !== "files" && format !== "md") {
494
+ process.stderr.write(`Unknown --output "${String(format)}". Valid formats: json, files, md. Using json.
495
+ `);
496
+ writeJson(envelope);
497
+ return;
498
+ }
486
499
  const items = normalizeData(envelope, fields, full ?? false, normalizer ?? imageNormalizer);
487
500
  const displayFields = fields ?? Object.keys(items[0] ?? {});
488
501
  if (format === "files") {
489
502
  writeFiles(items, displayFields);
490
- return;
491
- }
492
- if (format === "md") {
503
+ } else {
493
504
  writeMd(items, displayFields);
494
505
  }
506
+ writeHints(envelope.hints);
507
+ }
508
+ function writeHints(hints) {
509
+ for (const hint of hints ?? []) {
510
+ process.stderr.write(`${hint}
511
+ `);
512
+ }
495
513
  }
496
514
 
497
515
  // src/schemas.ts
@@ -16993,10 +17011,10 @@ function duplicateCommand2(entity, label) {
16993
17011
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
16994
17012
  },
16995
17013
  run: async ({ args }) => {
16996
- const { apiPost: apiPost2 } = await import("./client-PJ7ID35L.js");
16997
- const { requireChatId: requireChatId2 } = await import("./env-6QJCMTRK.js");
16998
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-NWX3YW64.js");
16999
- const { handleMetaError: handleMetaError2 } = await import("./shared-5ZEOG664.js");
17014
+ const { apiPost: apiPost2 } = await import("./client-LTDQPQ7T.js");
17015
+ const { requireChatId: requireChatId2 } = await import("./env-GV4VDT5W.js");
17016
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-PXTEFDVX.js");
17017
+ const { handleMetaError: handleMetaError2 } = await import("./shared-AR6VAAIE.js");
17000
17018
  try {
17001
17019
  const accountId = bareAccountId2(args);
17002
17020
  const chatId = requireChatId2();
@@ -25345,9 +25363,11 @@ async function fileExists(target) {
25345
25363
  async function uploadSourceAsReference(source, isUrl, client) {
25346
25364
  let bytes;
25347
25365
  if (isUrl) {
25348
- const res = await fetch(source);
25349
- if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
25350
- bytes = Buffer.from(await res.arrayBuffer());
25366
+ try {
25367
+ ({ buffer: bytes } = await fetchExternalBytes(source));
25368
+ } catch (e) {
25369
+ throw new Error(`failed to download source image (${e instanceof Error ? e.message : String(e)})`);
25370
+ }
25351
25371
  } else {
25352
25372
  bytes = await readFile12(source);
25353
25373
  }
@@ -26042,18 +26062,17 @@ function videoDefinitionDescription(blueprint) {
26042
26062
  }
26043
26063
  async function materializeReferenceVideo(fileArg2) {
26044
26064
  if (!/^https?:\/\//i.test(fileArg2)) return path21.resolve(fileArg2);
26045
- let res;
26065
+ let bytes;
26066
+ let contentType;
26046
26067
  try {
26047
- res = await fetch(fileArg2);
26068
+ ({ buffer: bytes, contentType } = await fetchExternalBytes(fileArg2, { sniffChallenge: false }));
26048
26069
  } catch (e) {
26049
26070
  throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
26050
26071
  }
26051
- if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
26052
- const bytes = Buffer.from(await res.arrayBuffer());
26053
26072
  if (bytes.length === 0) throw new Error("reference video download was empty");
26054
26073
  const dest = path21.join(
26055
26074
  tmpdir2(),
26056
- `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
26075
+ `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, contentType)}`
26057
26076
  );
26058
26077
  await writeFile8(dest, bytes);
26059
26078
  return dest;
@@ -29695,11 +29714,8 @@ async function expandInputs(spec) {
29695
29714
  }
29696
29715
  async function readImageBuffer(pathOrUrl) {
29697
29716
  if (isRemoteUrl(pathOrUrl)) {
29698
- const response = await fetch(pathOrUrl);
29699
- if (!response.ok) {
29700
- throw new Error(`Failed to fetch ${pathOrUrl}: ${response.status} ${response.statusText}`);
29701
- }
29702
- return Buffer.from(await response.arrayBuffer());
29717
+ const { buffer } = await fetchExternalBytes(pathOrUrl);
29718
+ return buffer;
29703
29719
  }
29704
29720
  return readFile19(pathOrUrl);
29705
29721
  }
@@ -29988,15 +30004,19 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
29988
30004
  return out;
29989
30005
  }
29990
30006
  function disambiguate(paths) {
29991
- const seen = /* @__PURE__ */ new Map();
30007
+ const taken = /* @__PURE__ */ new Set();
29992
30008
  return paths.map((path35) => {
29993
- const count = seen.get(path35) ?? 0;
29994
- seen.set(path35, count + 1);
29995
- if (count === 0) {
30009
+ if (!taken.has(path35)) {
30010
+ taken.add(path35);
29996
30011
  return path35;
29997
30012
  }
29998
30013
  const ext = extname3(path35);
29999
- return `${path35.slice(0, path35.length - ext.length)}-${count + 1}${ext}`;
30014
+ const stem = path35.slice(0, path35.length - ext.length);
30015
+ let n = 2;
30016
+ while (taken.has(`${stem}-${n}${ext}`)) n += 1;
30017
+ const unique = `${stem}-${n}${ext}`;
30018
+ taken.add(unique);
30019
+ return unique;
30000
30020
  });
30001
30021
  }
30002
30022
 
@@ -30063,17 +30083,10 @@ async function resolveTarget(target) {
30063
30083
  return { input: target.raw, url: doc.imageUrl, baseName: stem };
30064
30084
  }
30065
30085
  async function fetchBytes(url) {
30066
- const response = await fetch(url);
30067
- if (!response.ok) {
30068
- throw new Error(`${response.status} ${response.statusText}`);
30069
- }
30070
- return {
30071
- buffer: Buffer.from(await response.arrayBuffer()),
30072
- contentType: response.headers.get("content-type")
30073
- };
30086
+ return await fetchExternalBytes(url);
30074
30087
  }
30075
30088
  var NEXT_STEP_HINT = "Downloaded files are local paths \u2014 the local transforms take them directly: `baker images normalize <file> --color '#FFFFFF' --height 40`, `baker images crop <file> --x \u2026 --y \u2026 --width \u2026 --height \u2026`, `baker images dimensions <file>`. For a landing, page-scoped images belong in `src/pages/{landing}/_images/`.";
30076
- var PARTIAL_FAILURE_HINT = "Some targets did not download. Carry on with the ones that did \u2014 a missing asset never blocks the rest of the job. Retry a failed URL only if it was a transient 5xx; a 403 means the host refuses datacenter IPs, so re-source it (`baker images extract <page>`) instead.";
30089
+ var PARTIAL_FAILURE_HINT = "Some targets did not download. Carry on with the ones that did \u2014 a missing asset never blocks the rest of the job. Retry a failed URL only if it was a transient 5xx; a 403 means the host will not serve these bytes to anyone but a browser, so re-source it (`baker images extract <page>`) instead.";
30077
30090
  var ALL_FAILED_FIX = {
30078
30091
  action: "use_different_resource",
30079
30092
  explanation: "None of the targets could be downloaded. A 403/404 means the host will not serve these bytes \u2014 re-source the asset (`baker images extract <page-url>`, `baker images stock <query>`) or, for a library image, check the row is `ready` with `baker images get <id>`. Do not retry the same URLs in a loop, and do not fall back to `curl` \u2014 continue the job with a placeholder if nothing can be fetched."
@@ -34842,6 +34855,26 @@ function capturedCopyStrings(markup) {
34842
34855
  return [...seen];
34843
34856
  }
34844
34857
 
34858
+ // src/engine/landing-library/proxyFailure.ts
34859
+ var PROXY_STATUS = 407;
34860
+ var PROXY_NET_ERRORS = [
34861
+ "ERR_TUNNEL_CONNECTION_FAILED",
34862
+ "ERR_PROXY_CONNECTION_FAILED",
34863
+ "ERR_PROXY_AUTH_REQUESTED",
34864
+ "ERR_PROXY_CERTIFICATE_INVALID",
34865
+ "ERR_UNEXPECTED_PROXY_AUTH",
34866
+ "ERR_MANDATORY_PROXY_CONFIGURATION_FAILED",
34867
+ "ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT"
34868
+ ];
34869
+ var PROXY_UNAVAILABLE_MESSAGE = "We couldn't reach this page just now. Please try adding it again.";
34870
+ function describeProxyFailure(error) {
34871
+ const raw = error instanceof Error ? error.message : typeof error === "string" ? error : "";
34872
+ if (!raw) return null;
34873
+ const netError = /net::(ERR_[A-Z0-9_]+)/.exec(raw)?.[1];
34874
+ if (!netError || !PROXY_NET_ERRORS.includes(netError)) return null;
34875
+ return { code: "PROXY_UNAVAILABLE", message: PROXY_UNAVAILABLE_MESSAGE };
34876
+ }
34877
+
34845
34878
  // src/engine/landing-library/unreachable.ts
34846
34879
  var NET_ERROR_REASONS = [
34847
34880
  {
@@ -34893,6 +34926,10 @@ function describeUnreachableSite(error) {
34893
34926
  }
34894
34927
  return null;
34895
34928
  }
34929
+ function netErrorName(error) {
34930
+ const raw = error instanceof Error ? error.message : typeof error === "string" ? error : "";
34931
+ return raw ? /net::(ERR_[A-Z0-9_]+)/.exec(raw)?.[1] ?? null : null;
34932
+ }
34896
34933
 
34897
34934
  // src/engine/landing-library/blocked.ts
34898
34935
  var CHALLENGE_PHRASES = [
@@ -34912,6 +34949,9 @@ var CHALLENGE_PHRASES = [
34912
34949
  ];
34913
34950
  var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
34914
34951
  function detectBlockedPage(page) {
34952
+ if (page.status === PROXY_STATUS) {
34953
+ return { code: "PROXY_UNAVAILABLE", message: PROXY_UNAVAILABLE_MESSAGE };
34954
+ }
34915
34955
  if (page.status !== null && page.status >= 400) {
34916
34956
  return {
34917
34957
  code: "HTTP_ERROR",
@@ -34938,14 +34978,26 @@ var BlockedPageError = class extends Error {
34938
34978
  this.code = blocked.code;
34939
34979
  }
34940
34980
  };
34981
+ var CaptureFailureError = class extends Error {
34982
+ failure;
34983
+ constructor(failure) {
34984
+ super(failure.message);
34985
+ this.name = "CaptureFailureError";
34986
+ this.failure = failure;
34987
+ }
34988
+ };
34989
+ var LOCAL_ADDRESS_REFUSAL = "That address only opens from inside your own network, so there is nothing there for us to read. A reference page has to be live on the web.";
34941
34990
  var NODE_LEVEL_UNREACHABLE = ["econnrefused", "enotfound", "getaddrinfo"];
34942
34991
  var UNREACHABLE_MESSAGE = "We couldn't reach this page \u2014 the site didn't answer, or its security certificate isn't one we can read.";
34943
34992
  var TOO_SLOW_MESSAGE = "This page took too long to load, so we stopped waiting for it.";
34944
34993
  var CAPTURE_FAILED_MESSAGE = "We couldn't read this page.";
34945
34994
  function classifyCaptureFailure(error) {
34995
+ if (error instanceof CaptureFailureError) return error.failure;
34946
34996
  if (error instanceof BlockedPageError) {
34947
34997
  return { code: error.code, message: error.message };
34948
34998
  }
34999
+ const proxyFailure = describeProxyFailure(error);
35000
+ if (proxyFailure) return proxyFailure;
34949
35001
  const unreachable = describeUnreachableSite(error);
34950
35002
  if (unreachable) return unreachable;
34951
35003
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
@@ -34969,11 +35021,12 @@ var pwSpecifier = ["play", "wright"].join("");
34969
35021
  var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
34970
35022
  var MOBILE_VIEWPORT = { width: 390, height: 844 };
34971
35023
  var DEVICE_SCALE_FACTOR = 2;
34972
- async function launchBrowser() {
35024
+ async function launchBrowser(route = { kind: "direct" }) {
34973
35025
  const playwright = require_(pwSpecifier);
34974
35026
  return await playwright.chromium.launch({
34975
35027
  headless: true,
34976
- args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
35028
+ args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"],
35029
+ ...route.kind === "proxy" ? { proxy: { server: route.server, username: route.username, password: route.password } } : {}
34977
35030
  });
34978
35031
  }
34979
35032
  async function newPage(browser, viewport, opts = { motion: false }) {
@@ -35314,6 +35367,42 @@ async function captureSectionOnMobile(page, section) {
35314
35367
  return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
35315
35368
  }
35316
35369
 
35370
+ // src/engine/landing-library/escalation.ts
35371
+ var CAPTURE_RESERVE_MS = 18e4;
35372
+ function attemptSignal(outcome) {
35373
+ const code = outcome.failure.code;
35374
+ return {
35375
+ challenge: code === "BOT_CHALLENGE",
35376
+ status: outcome.status,
35377
+ netError: outcome.netError,
35378
+ timedOut: code === "SITE_TOO_SLOW"
35379
+ };
35380
+ }
35381
+ function nextRoute(args) {
35382
+ const last = args.attempts[args.attempts.length - 1];
35383
+ if (!last) return { next: null, reason: "ladder_exhausted" };
35384
+ const brokenProxy = isProxyFailure(attemptSignal(last));
35385
+ if (!brokenProxy && !shouldEscalate(attemptSignal(last))) {
35386
+ return { next: null, reason: "site_fault_is_final" };
35387
+ }
35388
+ const tried = new Set(args.attempts.map((attempt) => routeKey(attempt.route)));
35389
+ const next = args.routes.find((route) => !tried.has(routeKey(route)));
35390
+ if (!next) return { next: null, reason: args.routes.length === 1 ? "not_configured" : "ladder_exhausted" };
35391
+ if (args.remainingMs < args.reserveMs + args.navTimeoutMs) {
35392
+ return { next: null, reason: "out_of_budget" };
35393
+ }
35394
+ return { next, reason: "escalate" };
35395
+ }
35396
+ function reportableFailure(attempts) {
35397
+ const siteVerdicts = attempts.filter((attempt) => !isProxyFailure(attemptSignal(attempt)));
35398
+ const chosen = siteVerdicts.find((attempt) => attempt.status !== null) ?? siteVerdicts[siteVerdicts.length - 1] ?? attempts[attempts.length - 1];
35399
+ if (!chosen) throw new Error("reportableFailure called with no attempts");
35400
+ return chosen.failure;
35401
+ }
35402
+ function routeKey(route) {
35403
+ return route.kind === "direct" ? "direct" : route.tier;
35404
+ }
35405
+
35317
35406
  // src/engine/landing-library/fidelity.ts
35318
35407
  import sharp3 from "sharp";
35319
35408
  var COMPARISON_SIZE = 32;
@@ -36573,17 +36662,16 @@ async function captureMotionTakes(args) {
36573
36662
  })
36574
36663
  );
36575
36664
  }
36576
- async function scrapeLanding(options) {
36577
- const timeoutMs = options.timeoutMs ?? 45e3;
36578
- const log = options.onProgress ?? (() => void 0);
36579
- const sectionsDir = path31.join(options.outDir, "sections");
36580
- const browser = await launchBrowser();
36665
+ async function openPageVia(route, url, timeoutMs) {
36666
+ const startedAt = Date.now();
36667
+ const browser = await launchBrowser(route);
36668
+ let status = null;
36581
36669
  try {
36582
36670
  const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
36583
36671
  await blockConsentManagers(page);
36584
36672
  await installPageRuntime(page);
36585
- log(`loading ${options.url}`);
36586
- const prepared = await preparePage(page, options.url, timeoutMs);
36673
+ const prepared = await preparePage(page, url, timeoutMs);
36674
+ status = prepared.status;
36587
36675
  const blocked = detectBlockedPage({
36588
36676
  status: prepared.status,
36589
36677
  title: prepared.title,
@@ -36591,58 +36679,141 @@ async function scrapeLanding(options) {
36591
36679
  html: await page.content().catch(() => "")
36592
36680
  });
36593
36681
  if (blocked) throw new BlockedPageError(blocked);
36594
- await mkdir11(sectionsDir, { recursive: true });
36595
- log("segmenting");
36596
- const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
36597
- log(`found ${candidates.length} sections`);
36598
- const sections = [];
36599
- for (const candidate of candidates) {
36600
- const section = await captureOneSection({
36601
- browser,
36602
- page,
36603
- candidate,
36604
- sectionsDir,
36605
- outDir: options.outDir,
36606
- pageUrl: prepared.finalUrl,
36607
- withCode: options.code !== false
36608
- });
36609
- sections.push(section);
36610
- log(
36611
- ` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
36612
- );
36613
- }
36614
- const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
36615
- if (fullPage) await writeFile15(path31.join(options.outDir, "full-page.png"), fullPage);
36616
- const reproduction = options.code === false ? { bundle: null, fidelity: null } : await reproducePage({
36682
+ return { ok: true, browser, context, page, prepared };
36683
+ } catch (error) {
36684
+ await browser.close().catch(() => void 0);
36685
+ return {
36686
+ ok: false,
36687
+ outcome: {
36688
+ route,
36689
+ failure: classifyCaptureFailure(error),
36690
+ status,
36691
+ netError: netErrorName(error),
36692
+ elapsedMs: Date.now() - startedAt
36693
+ }
36694
+ };
36695
+ }
36696
+ }
36697
+ async function captureAlternateViews(args) {
36698
+ const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs, withMobile, withMotion, log } = args;
36699
+ if (withMobile) {
36700
+ log("capturing mobile");
36701
+ await captureMobileShots({ browser, sections, sectionsDir, outDir, pageUrl, timeoutMs });
36702
+ }
36703
+ if (withMotion) {
36704
+ await captureMotionTakes({ browser, sections, sectionsDir, outDir, pageUrl, log });
36705
+ }
36706
+ }
36707
+ async function reproduceWholePage(args) {
36708
+ const { browser, page, outDir, pageUrl, withCode, log } = args;
36709
+ const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
36710
+ if (fullPage) await writeFile15(path31.join(outDir, "full-page.png"), fullPage);
36711
+ if (!withCode) return { bundle: null, fidelity: null };
36712
+ const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
36713
+ log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
36714
+ return reproduction;
36715
+ }
36716
+ async function captureSections(args) {
36717
+ const { browser, page, sectionsDir, outDir, pageUrl, withCode, log } = args;
36718
+ log("segmenting");
36719
+ const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
36720
+ log(`found ${candidates.length} sections`);
36721
+ const sections = [];
36722
+ for (const candidate of candidates) {
36723
+ const section = await captureOneSection({
36617
36724
  browser,
36618
36725
  page,
36726
+ candidate,
36727
+ sectionsDir,
36728
+ outDir,
36729
+ pageUrl,
36730
+ withCode
36731
+ });
36732
+ sections.push(section);
36733
+ log(
36734
+ ` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
36735
+ );
36736
+ }
36737
+ return sections;
36738
+ }
36739
+ async function openViaLadder(args) {
36740
+ const { url, routes, timeoutMs, deadline, log } = args;
36741
+ const attempts = [];
36742
+ for (let route = routes[0] ?? { kind: "direct" }; route; ) {
36743
+ log(`loading ${url}${route.kind === "proxy" ? ` (retrying via ${route.tier})` : ""}`);
36744
+ const attempt = await openPageVia(route, url, timeoutMs);
36745
+ if (attempt.ok) {
36746
+ return { opened: attempt, attempts, tier: route.kind === "proxy" ? route.tier : "direct" };
36747
+ }
36748
+ attempts.push(attempt.outcome);
36749
+ const decision = nextRoute({
36750
+ routes,
36751
+ attempts,
36752
+ remainingMs: deadline ? deadline - Date.now() : Number.POSITIVE_INFINITY,
36753
+ reserveMs: CAPTURE_RESERVE_MS,
36754
+ navTimeoutMs: timeoutMs
36755
+ });
36756
+ if (!decision.next) {
36757
+ log(`giving up after ${attempts.length} attempt(s) \u2014 ${decision.reason}`);
36758
+ break;
36759
+ }
36760
+ route = decision.next;
36761
+ }
36762
+ throw new CaptureFailureError(reportableFailure(attempts));
36763
+ }
36764
+ async function scrapeLanding(options) {
36765
+ const timeoutMs = options.timeoutMs ?? 45e3;
36766
+ const log = options.onProgress ?? (() => void 0);
36767
+ const sectionsDir = path31.join(options.outDir, "sections");
36768
+ const nonPublic = refuseNonPublicUrl(options.url);
36769
+ if (nonPublic) {
36770
+ throw new BlockedPageError({
36771
+ code: "HTTP_ERROR",
36772
+ message: nonPublic === "unparseable" ? "That doesn't look like a web address we can open." : LOCAL_ADDRESS_REFUSAL
36773
+ });
36774
+ }
36775
+ const { opened, attempts, tier } = await openViaLadder({
36776
+ url: options.url,
36777
+ routes: plannedRoutes(options.url, options.proxyCredentials ?? {}),
36778
+ timeoutMs,
36779
+ deadline: options.budgetMs ? Date.now() + options.budgetMs : null,
36780
+ log
36781
+ });
36782
+ const { browser, context, page, prepared } = opened;
36783
+ const escalated = attempts.length > 0;
36784
+ const withMotion = options.motion !== false && !escalated;
36785
+ const renderBrowser = options.code === false ? null : await launchBrowser();
36786
+ try {
36787
+ await mkdir11(sectionsDir, { recursive: true });
36788
+ const sections = await captureSections({
36789
+ browser: renderBrowser ?? browser,
36790
+ page,
36791
+ sectionsDir,
36792
+ outDir: options.outDir,
36793
+ pageUrl: prepared.finalUrl,
36794
+ withCode: options.code !== false,
36795
+ log
36796
+ });
36797
+ const reproduction = await reproduceWholePage({
36798
+ browser: renderBrowser ?? browser,
36799
+ page,
36619
36800
  outDir: options.outDir,
36620
36801
  pageUrl: prepared.finalUrl,
36621
- livePageShot: fullPage
36802
+ withCode: options.code !== false,
36803
+ log
36622
36804
  });
36623
- log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
36624
36805
  await context.close();
36625
- if (options.mobile !== false) {
36626
- log("capturing mobile");
36627
- await captureMobileShots({
36628
- browser,
36629
- sections,
36630
- sectionsDir,
36631
- outDir: options.outDir,
36632
- pageUrl: prepared.finalUrl,
36633
- timeoutMs
36634
- });
36635
- }
36636
- if (options.motion !== false) {
36637
- await captureMotionTakes({
36638
- browser,
36639
- sections,
36640
- sectionsDir,
36641
- outDir: options.outDir,
36642
- pageUrl: prepared.finalUrl,
36643
- log
36644
- });
36645
- }
36806
+ await captureAlternateViews({
36807
+ browser,
36808
+ sections,
36809
+ sectionsDir,
36810
+ outDir: options.outDir,
36811
+ pageUrl: prepared.finalUrl,
36812
+ timeoutMs,
36813
+ withMobile: options.mobile !== false,
36814
+ withMotion,
36815
+ log
36816
+ });
36646
36817
  const manifest = {
36647
36818
  url: options.url,
36648
36819
  finalUrl: prepared.finalUrl,
@@ -36652,7 +36823,8 @@ async function scrapeLanding(options) {
36652
36823
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
36653
36824
  sections,
36654
36825
  page: reproduction,
36655
- security: prepared.security
36826
+ security: prepared.security,
36827
+ captureTier: tier
36656
36828
  };
36657
36829
  await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
36658
36830
  `);
@@ -36663,11 +36835,18 @@ async function scrapeLanding(options) {
36663
36835
  return manifest;
36664
36836
  } finally {
36665
36837
  await browser.close();
36838
+ if (renderBrowser) await renderBrowser.close();
36666
36839
  }
36667
36840
  }
36668
36841
 
36669
36842
  // src/commands/landing/inspiration/scrape.ts
36670
- var RETRYABLE_FAILURES = /* @__PURE__ */ new Set(["SITE_TOO_SLOW", "CAPTURE_FAILED"]);
36843
+ var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
36844
+ "SITE_TOO_SLOW",
36845
+ "CAPTURE_FAILED",
36846
+ // Ours, not the site's. Filing it as final is exactly how a mistyped password
36847
+ // would blacklist a page that was never actually judged.
36848
+ "PROXY_UNAVAILABLE"
36849
+ ]);
36671
36850
  async function recordCapture(manifest, outDir) {
36672
36851
  const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
36673
36852
  const references = [];
@@ -36686,7 +36865,8 @@ async function recordCapture(manifest, outDir) {
36686
36865
  } catch {
36687
36866
  }
36688
36867
  }
36689
- return await recordReferences(process.cwd(), references);
36868
+ const written = await recordReferences(process.cwd(), references);
36869
+ return written ? references.length : 0;
36690
36870
  }
36691
36871
  registerSchema({
36692
36872
  command: "landing.inspiration.scrape",
@@ -36740,11 +36920,17 @@ var scrapeCommand = defineCommand148({
36740
36920
  code: args.code,
36741
36921
  motion: args.motion,
36742
36922
  report: args.report,
36923
+ // Both come from the environment, never from a flag — see their docs in
36924
+ // `src/env.ts`. Absent credentials this is the direct route only, which
36925
+ // is exactly the behaviour that shipped before the ladder existed.
36926
+ proxyCredentials: captureProxyCredentials(),
36927
+ budgetMs: captureBudgetMs() ?? void 0,
36743
36928
  // Progress goes to stderr so stdout stays a clean JSON envelope.
36744
36929
  onProgress: (message) => process.stderr.write(`${message}
36745
36930
  `)
36746
36931
  });
36747
36932
  const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
36933
+ const shotCount = manifest.sections.filter((section) => section.desktopShot !== null).length;
36748
36934
  const recorded = await recordCapture(manifest, args.out);
36749
36935
  writeJson({
36750
36936
  ok: true,
@@ -36755,6 +36941,11 @@ var scrapeCommand = defineCommand148({
36755
36941
  pageFidelity: manifest.page.fidelity,
36756
36942
  out: args.out,
36757
36943
  report: args.report ? `${args.out}/report.html` : null,
36944
+ // Never omitted, and that is the point: an envelope without it is a
36945
+ // capture that ran a build predating the ladder, which is otherwise
36946
+ // indistinguishable from one that took the direct route and is the
36947
+ // exact silence the E2B template rebuild is easy to forget about.
36948
+ capture_tier: manifest.captureTier,
36758
36949
  // Compact by default, but never omitted: a capture that could not
36759
36950
  // verify who served the page is worth less as evidence, and the
36760
36951
  // reader has to be told without going looking for it.
@@ -36770,11 +36961,22 @@ var scrapeCommand = defineCommand148({
36770
36961
  // instruction to open any of it, and a capture nobody looks at taught
36771
36962
  // nobody anything. A nudge tied to one command's result belongs here,
36772
36963
  // not only in the tool doc the agent may not re-read.
36773
- 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.`,
36774
- `${manifest.sections.length} sections captured to ${args.out}/sections/ \u2014 look at them, then decide which the client's page actually needs.`,
36964
+ //
36965
+ // Every one of these now reads the manifest rather than the flags the
36966
+ // caller passed. The flags say what was *asked for*; a screenshot that
36967
+ // failed still leaves `desktopShot` null, and `--no-code` leaves every
36968
+ // fidelity score null while the report renders them as "—". Telling an
36969
+ // agent to go and read a file that is not there costs it a tool call
36970
+ // and costs us its trust in the next hint.
36971
+ ...shotCount > 0 ? [
36972
+ args.report ? `Read ${args.out}/report.html \u2014 every section side by side${scored.length > 0 ? " with its fidelity score" : ""}.` : `Read the section screenshots in ${args.out}/sections/*/desktop.png before you design.`,
36973
+ `${shotCount} of ${manifest.sections.length} sections captured to ${args.out}/sections/ \u2014 look at them, then decide which the client's page actually needs.`
36974
+ ] : [
36975
+ `No section screenshots were produced, so there is nothing to look at in ${args.out}/sections/. Treat this capture as unusable rather than designing from the counts.`
36976
+ ],
36775
36977
  INSPIRATION_HINTS.structureNotCopy,
36776
36978
  INSPIRATION_HINTS.adapt,
36777
- 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."
36979
+ recorded > 0 ? `${recorded} sections are recorded for the originality check \u2014 \`baker landing critique\` will block a publish that ships their copy.` : "Nothing from this capture is recorded for the originality check, so it cannot see this page. Be especially careful not to reuse its copy."
36778
36980
  ]
36779
36981
  });
36780
36982
  } catch (error) {
@@ -38258,6 +38460,7 @@ Examples:
38258
38460
  const format = args.output || "json";
38259
38461
  if (format !== "json") {
38260
38462
  writeResearchOutput(result.data, format);
38463
+ writeResearchHints([RESEARCH_DATA_NOTE], format);
38261
38464
  return;
38262
38465
  }
38263
38466
  writeResearchJson({
@@ -38538,6 +38741,7 @@ Examples:
38538
38741
  const format = args.output || "json";
38539
38742
  if (format !== "json") {
38540
38743
  writeResearchOutput(data, format);
38744
+ writeResearchHints([RESEARCH_DATA_NOTE], format);
38541
38745
  return;
38542
38746
  }
38543
38747
  writeResearchJson({ ok: true, data, fields: FIELDS12, note: RESEARCH_DATA_NOTE, query_context: queryContext });
@@ -42917,7 +43121,7 @@ var main = defineCommand212({
42917
43121
  Auth: Set BAKER_API_KEY (starts with bk_) and BAKER_API_URL environment variables.
42918
43122
  Chat: Set BAKER_CHAT_ID for action and scheduled-action commands that stage changes against a chat.
42919
43123
  Output: All commands return JSON envelopes: { ok: true, data: ... } or { ok: false, error: { code, message } }.
42920
- Formats: Use --output json|csv|jsonl|files|md to control output format. Default: json.
43124
+ Formats: Use --output json|files|md to control output format. Default: json. (The research commands additionally accept csv and jsonl.)
42921
43125
  Introspection: Run 'baker schema <command>' to inspect argument schemas.`
42922
43126
  },
42923
43127
  subCommands: {