@koda-sl/baker-cli 0.195.0-dev.6d2f498f5 → 0.197.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 +339 -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
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,44 @@ var BlockedPageError = class extends Error {
34938
34978
  this.code = blocked.code;
34939
34979
  }
34940
34980
  };
34981
+ var CaptureFailureError = class extends Error {
34982
+ failure;
34983
+ /**
34984
+ * Which rungs were tried, and what each of them said.
34985
+ *
34986
+ * Carried out of the engine because nothing downstream could otherwise tell a
34987
+ * page refused from three different addresses apart from one refused once —
34988
+ * and those two call for opposite conclusions about whether to try again.
34989
+ *
34990
+ * It is also the only way to see the rollout fail silently. The E2B image
34991
+ * installs the CLI at build time, so until the template is rebuilt a Runtime
34992
+ * runs an older binary that ignores the credentials Convex hands it. That
34993
+ * capture returns an envelope with **no rungs at all** — indistinguishable,
34994
+ * from the outside, from a page that simply was not worth escalating.
34995
+ *
34996
+ * Deliberately thin: tier names and status codes only, so nothing here can
34997
+ * carry a credential or the raw browser text that once reached a user.
34998
+ */
34999
+ rungs;
35000
+ constructor(failure, rungs = []) {
35001
+ super(failure.message);
35002
+ this.name = "CaptureFailureError";
35003
+ this.failure = failure;
35004
+ this.rungs = rungs;
35005
+ }
35006
+ };
35007
+ 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
35008
  var NODE_LEVEL_UNREACHABLE = ["econnrefused", "enotfound", "getaddrinfo"];
34942
35009
  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
35010
  var TOO_SLOW_MESSAGE = "This page took too long to load, so we stopped waiting for it.";
34944
35011
  var CAPTURE_FAILED_MESSAGE = "We couldn't read this page.";
34945
35012
  function classifyCaptureFailure(error) {
35013
+ if (error instanceof CaptureFailureError) return error.failure;
34946
35014
  if (error instanceof BlockedPageError) {
34947
35015
  return { code: error.code, message: error.message };
34948
35016
  }
35017
+ const proxyFailure = describeProxyFailure(error);
35018
+ if (proxyFailure) return proxyFailure;
34949
35019
  const unreachable = describeUnreachableSite(error);
34950
35020
  if (unreachable) return unreachable;
34951
35021
  const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
@@ -34969,11 +35039,12 @@ var pwSpecifier = ["play", "wright"].join("");
34969
35039
  var DESKTOP_VIEWPORT = { width: 1440, height: 900 };
34970
35040
  var MOBILE_VIEWPORT = { width: 390, height: 844 };
34971
35041
  var DEVICE_SCALE_FACTOR = 2;
34972
- async function launchBrowser() {
35042
+ async function launchBrowser(route = { kind: "direct" }) {
34973
35043
  const playwright = require_(pwSpecifier);
34974
35044
  return await playwright.chromium.launch({
34975
35045
  headless: true,
34976
- args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"]
35046
+ args: ["--hide-scrollbars", "--disable-blink-features=AutomationControlled", "--mute-audio"],
35047
+ ...route.kind === "proxy" ? { proxy: { server: route.server, username: route.username, password: route.password } } : {}
34977
35048
  });
34978
35049
  }
34979
35050
  async function newPage(browser, viewport, opts = { motion: false }) {
@@ -35314,6 +35385,42 @@ async function captureSectionOnMobile(page, section) {
35314
35385
  return await locator.screenshot({ type: "png", timeout: 15e3 }).catch(() => null);
35315
35386
  }
35316
35387
 
35388
+ // src/engine/landing-library/escalation.ts
35389
+ var CAPTURE_RESERVE_MS = 18e4;
35390
+ function attemptSignal(outcome) {
35391
+ const code = outcome.failure.code;
35392
+ return {
35393
+ challenge: code === "BOT_CHALLENGE",
35394
+ status: outcome.status,
35395
+ netError: outcome.netError,
35396
+ timedOut: code === "SITE_TOO_SLOW"
35397
+ };
35398
+ }
35399
+ function nextRoute(args) {
35400
+ const last = args.attempts[args.attempts.length - 1];
35401
+ if (!last) return { next: null, reason: "ladder_exhausted" };
35402
+ const brokenProxy = isProxyFailure(attemptSignal(last));
35403
+ if (!brokenProxy && !shouldEscalate(attemptSignal(last))) {
35404
+ return { next: null, reason: "site_fault_is_final" };
35405
+ }
35406
+ const tried = new Set(args.attempts.map((attempt) => routeKey(attempt.route)));
35407
+ const next = args.routes.find((route) => !tried.has(routeKey(route)));
35408
+ if (!next) return { next: null, reason: args.routes.length === 1 ? "not_configured" : "ladder_exhausted" };
35409
+ if (args.remainingMs < args.reserveMs + args.navTimeoutMs) {
35410
+ return { next: null, reason: "out_of_budget" };
35411
+ }
35412
+ return { next, reason: "escalate" };
35413
+ }
35414
+ function reportableFailure(attempts) {
35415
+ const siteVerdicts = attempts.filter((attempt) => !isProxyFailure(attemptSignal(attempt)));
35416
+ const chosen = siteVerdicts.find((attempt) => attempt.status !== null) ?? siteVerdicts[siteVerdicts.length - 1] ?? attempts[attempts.length - 1];
35417
+ if (!chosen) throw new Error("reportableFailure called with no attempts");
35418
+ return chosen.failure;
35419
+ }
35420
+ function routeKey(route) {
35421
+ return route.kind === "direct" ? "direct" : route.tier;
35422
+ }
35423
+
35317
35424
  // src/engine/landing-library/fidelity.ts
35318
35425
  import sharp3 from "sharp";
35319
35426
  var COMPARISON_SIZE = 32;
@@ -36573,17 +36680,16 @@ async function captureMotionTakes(args) {
36573
36680
  })
36574
36681
  );
36575
36682
  }
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();
36683
+ async function openPageVia(route, url, timeoutMs) {
36684
+ const startedAt = Date.now();
36685
+ const browser = await launchBrowser(route);
36686
+ let status = null;
36581
36687
  try {
36582
36688
  const { context, page } = await newPage(browser, DESKTOP_VIEWPORT);
36583
36689
  await blockConsentManagers(page);
36584
36690
  await installPageRuntime(page);
36585
- log(`loading ${options.url}`);
36586
- const prepared = await preparePage(page, options.url, timeoutMs);
36691
+ const prepared = await preparePage(page, url, timeoutMs);
36692
+ status = prepared.status;
36587
36693
  const blocked = detectBlockedPage({
36588
36694
  status: prepared.status,
36589
36695
  title: prepared.title,
@@ -36591,58 +36697,148 @@ async function scrapeLanding(options) {
36591
36697
  html: await page.content().catch(() => "")
36592
36698
  });
36593
36699
  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({
36700
+ return { ok: true, browser, context, page, prepared };
36701
+ } catch (error) {
36702
+ await browser.close().catch(() => void 0);
36703
+ return {
36704
+ ok: false,
36705
+ outcome: {
36706
+ route,
36707
+ failure: classifyCaptureFailure(error),
36708
+ status,
36709
+ netError: netErrorName(error),
36710
+ elapsedMs: Date.now() - startedAt
36711
+ }
36712
+ };
36713
+ }
36714
+ }
36715
+ async function captureAlternateViews(args) {
36716
+ const { browser, sections, sectionsDir, outDir, pageUrl, timeoutMs, withMobile, withMotion, log } = args;
36717
+ if (withMobile) {
36718
+ log("capturing mobile");
36719
+ await captureMobileShots({ browser, sections, sectionsDir, outDir, pageUrl, timeoutMs });
36720
+ }
36721
+ if (withMotion) {
36722
+ await captureMotionTakes({ browser, sections, sectionsDir, outDir, pageUrl, log });
36723
+ }
36724
+ }
36725
+ async function reproduceWholePage(args) {
36726
+ const { browser, page, outDir, pageUrl, withCode, log } = args;
36727
+ const fullPage = await page.screenshot({ type: "png", fullPage: true }).catch(() => null);
36728
+ if (fullPage) await writeFile15(path31.join(outDir, "full-page.png"), fullPage);
36729
+ if (!withCode) return { bundle: null, fidelity: null };
36730
+ const reproduction = await reproducePage({ browser, page, outDir, pageUrl, livePageShot: fullPage });
36731
+ log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
36732
+ return reproduction;
36733
+ }
36734
+ async function captureSections(args) {
36735
+ const { browser, page, sectionsDir, outDir, pageUrl, withCode, log } = args;
36736
+ log("segmenting");
36737
+ const candidates = await segmentPage(page, DEFAULT_SEGMENT_OPTIONS);
36738
+ log(`found ${candidates.length} sections`);
36739
+ const sections = [];
36740
+ for (const candidate of candidates) {
36741
+ const section = await captureOneSection({
36617
36742
  browser,
36618
36743
  page,
36744
+ candidate,
36745
+ sectionsDir,
36746
+ outDir,
36747
+ pageUrl,
36748
+ withCode
36749
+ });
36750
+ sections.push(section);
36751
+ log(
36752
+ ` [${candidate.index}] ${candidate.rect.width}x${candidate.rect.height}${section.fidelity === null ? "" : ` fidelity=${section.fidelity.toFixed(2)}`} \u2014 ${candidate.textPreview.slice(0, 50)}`
36753
+ );
36754
+ }
36755
+ return sections;
36756
+ }
36757
+ function rungReport(outcome) {
36758
+ return {
36759
+ tier: outcome.route.kind === "proxy" ? outcome.route.tier : "direct",
36760
+ code: outcome.failure.code,
36761
+ status: outcome.status
36762
+ };
36763
+ }
36764
+ async function openViaLadder(args) {
36765
+ const { url, routes, timeoutMs, deadline, log } = args;
36766
+ const attempts = [];
36767
+ for (let route = routes[0] ?? { kind: "direct" }; route; ) {
36768
+ log(`loading ${url}${route.kind === "proxy" ? ` (retrying via ${route.tier})` : ""}`);
36769
+ const attempt = await openPageVia(route, url, timeoutMs);
36770
+ if (attempt.ok) {
36771
+ return { opened: attempt, attempts, tier: route.kind === "proxy" ? route.tier : "direct" };
36772
+ }
36773
+ attempts.push(attempt.outcome);
36774
+ const decision = nextRoute({
36775
+ routes,
36776
+ attempts,
36777
+ remainingMs: deadline ? deadline - Date.now() : Number.POSITIVE_INFINITY,
36778
+ reserveMs: CAPTURE_RESERVE_MS,
36779
+ navTimeoutMs: timeoutMs
36780
+ });
36781
+ if (!decision.next) {
36782
+ log(`giving up after ${attempts.length} attempt(s) \u2014 ${decision.reason}`);
36783
+ break;
36784
+ }
36785
+ route = decision.next;
36786
+ }
36787
+ throw new CaptureFailureError(reportableFailure(attempts), attempts.map(rungReport));
36788
+ }
36789
+ async function scrapeLanding(options) {
36790
+ const timeoutMs = options.timeoutMs ?? 45e3;
36791
+ const log = options.onProgress ?? (() => void 0);
36792
+ const sectionsDir = path31.join(options.outDir, "sections");
36793
+ const nonPublic = refuseNonPublicUrl(options.url);
36794
+ if (nonPublic) {
36795
+ throw new BlockedPageError({
36796
+ code: "HTTP_ERROR",
36797
+ message: nonPublic === "unparseable" ? "That doesn't look like a web address we can open." : LOCAL_ADDRESS_REFUSAL
36798
+ });
36799
+ }
36800
+ const { opened, attempts, tier } = await openViaLadder({
36801
+ url: options.url,
36802
+ routes: plannedRoutes(options.url, options.proxyCredentials ?? {}),
36803
+ timeoutMs,
36804
+ deadline: options.budgetMs ? Date.now() + options.budgetMs : null,
36805
+ log
36806
+ });
36807
+ const { browser, context, page, prepared } = opened;
36808
+ const escalated = attempts.length > 0;
36809
+ const withMotion = options.motion !== false && !escalated;
36810
+ const renderBrowser = options.code === false ? null : await launchBrowser();
36811
+ try {
36812
+ await mkdir11(sectionsDir, { recursive: true });
36813
+ const sections = await captureSections({
36814
+ browser: renderBrowser ?? browser,
36815
+ page,
36816
+ sectionsDir,
36817
+ outDir: options.outDir,
36818
+ pageUrl: prepared.finalUrl,
36819
+ withCode: options.code !== false,
36820
+ log
36821
+ });
36822
+ const reproduction = await reproduceWholePage({
36823
+ browser: renderBrowser ?? browser,
36824
+ page,
36619
36825
  outDir: options.outDir,
36620
36826
  pageUrl: prepared.finalUrl,
36621
- livePageShot: fullPage
36827
+ withCode: options.code !== false,
36828
+ log
36622
36829
  });
36623
- log(`page reproduction: ${reproduction.fidelity === null ? "unavailable" : reproduction.fidelity.toFixed(2)}`);
36624
36830
  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
- }
36831
+ await captureAlternateViews({
36832
+ browser,
36833
+ sections,
36834
+ sectionsDir,
36835
+ outDir: options.outDir,
36836
+ pageUrl: prepared.finalUrl,
36837
+ timeoutMs,
36838
+ withMobile: options.mobile !== false,
36839
+ withMotion,
36840
+ log
36841
+ });
36646
36842
  const manifest = {
36647
36843
  url: options.url,
36648
36844
  finalUrl: prepared.finalUrl,
@@ -36652,7 +36848,8 @@ async function scrapeLanding(options) {
36652
36848
  capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
36653
36849
  sections,
36654
36850
  page: reproduction,
36655
- security: prepared.security
36851
+ security: prepared.security,
36852
+ captureTier: tier
36656
36853
  };
36657
36854
  await writeFile15(path31.join(options.outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
36658
36855
  `);
@@ -36663,11 +36860,18 @@ async function scrapeLanding(options) {
36663
36860
  return manifest;
36664
36861
  } finally {
36665
36862
  await browser.close();
36863
+ if (renderBrowser) await renderBrowser.close();
36666
36864
  }
36667
36865
  }
36668
36866
 
36669
36867
  // src/commands/landing/inspiration/scrape.ts
36670
- var RETRYABLE_FAILURES = /* @__PURE__ */ new Set(["SITE_TOO_SLOW", "CAPTURE_FAILED"]);
36868
+ var RETRYABLE_FAILURES = /* @__PURE__ */ new Set([
36869
+ "SITE_TOO_SLOW",
36870
+ "CAPTURE_FAILED",
36871
+ // Ours, not the site's. Filing it as final is exactly how a mistyped password
36872
+ // would blacklist a page that was never actually judged.
36873
+ "PROXY_UNAVAILABLE"
36874
+ ]);
36671
36875
  async function recordCapture(manifest, outDir) {
36672
36876
  const consultedAt = (/* @__PURE__ */ new Date()).toISOString();
36673
36877
  const references = [];
@@ -36686,7 +36890,8 @@ async function recordCapture(manifest, outDir) {
36686
36890
  } catch {
36687
36891
  }
36688
36892
  }
36689
- return await recordReferences(process.cwd(), references);
36893
+ const written = await recordReferences(process.cwd(), references);
36894
+ return written ? references.length : 0;
36690
36895
  }
36691
36896
  registerSchema({
36692
36897
  command: "landing.inspiration.scrape",
@@ -36740,11 +36945,17 @@ var scrapeCommand = defineCommand148({
36740
36945
  code: args.code,
36741
36946
  motion: args.motion,
36742
36947
  report: args.report,
36948
+ // Both come from the environment, never from a flag — see their docs in
36949
+ // `src/env.ts`. Absent credentials this is the direct route only, which
36950
+ // is exactly the behaviour that shipped before the ladder existed.
36951
+ proxyCredentials: captureProxyCredentials(),
36952
+ budgetMs: captureBudgetMs() ?? void 0,
36743
36953
  // Progress goes to stderr so stdout stays a clean JSON envelope.
36744
36954
  onProgress: (message) => process.stderr.write(`${message}
36745
36955
  `)
36746
36956
  });
36747
36957
  const scored = manifest.sections.map((section) => section.fidelity).filter((value) => value !== null).sort((a, b) => a - b);
36958
+ const shotCount = manifest.sections.filter((section) => section.desktopShot !== null).length;
36748
36959
  const recorded = await recordCapture(manifest, args.out);
36749
36960
  writeJson({
36750
36961
  ok: true,
@@ -36755,6 +36966,11 @@ var scrapeCommand = defineCommand148({
36755
36966
  pageFidelity: manifest.page.fidelity,
36756
36967
  out: args.out,
36757
36968
  report: args.report ? `${args.out}/report.html` : null,
36969
+ // Never omitted, and that is the point: an envelope without it is a
36970
+ // capture that ran a build predating the ladder, which is otherwise
36971
+ // indistinguishable from one that took the direct route and is the
36972
+ // exact silence the E2B template rebuild is easy to forget about.
36973
+ capture_tier: manifest.captureTier,
36758
36974
  // Compact by default, but never omitted: a capture that could not
36759
36975
  // verify who served the page is worth less as evidence, and the
36760
36976
  // reader has to be told without going looking for it.
@@ -36770,11 +36986,22 @@ var scrapeCommand = defineCommand148({
36770
36986
  // instruction to open any of it, and a capture nobody looks at taught
36771
36987
  // nobody anything. A nudge tied to one command's result belongs here,
36772
36988
  // 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.`,
36989
+ //
36990
+ // Every one of these now reads the manifest rather than the flags the
36991
+ // caller passed. The flags say what was *asked for*; a screenshot that
36992
+ // failed still leaves `desktopShot` null, and `--no-code` leaves every
36993
+ // fidelity score null while the report renders them as "—". Telling an
36994
+ // agent to go and read a file that is not there costs it a tool call
36995
+ // and costs us its trust in the next hint.
36996
+ ...shotCount > 0 ? [
36997
+ 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.`,
36998
+ `${shotCount} of ${manifest.sections.length} sections captured to ${args.out}/sections/ \u2014 look at them, then decide which the client's page actually needs.`
36999
+ ] : [
37000
+ `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.`
37001
+ ],
36775
37002
  INSPIRATION_HINTS.structureNotCopy,
36776
37003
  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."
37004
+ 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
37005
  ]
36779
37006
  });
36780
37007
  } catch (error) {
@@ -36786,6 +37013,13 @@ var scrapeCommand = defineCommand148({
36786
37013
  error: {
36787
37014
  code: failure.code,
36788
37015
  message: failure.message,
37016
+ // Which addresses were tried and what each was told. One rung means
37017
+ // the failure was final wherever we stood; three mean the page turned
37018
+ // us away from three different places, which is a fact about the page.
37019
+ // **Absent** means this Runtime is running a CLI that predates the
37020
+ // ladder — the one way the rollout fails without anything looking
37021
+ // wrong.
37022
+ ...error instanceof CaptureFailureError && error.rungs.length > 0 ? { rungs: error.rungs } : {},
36789
37023
  // Degrade, don't abort: one unreadable reference is never a reason to
36790
37024
  // abandon the job it was research for. `retryable` is the decision
36791
37025
  // the agent would otherwise have to guess at.
@@ -38258,6 +38492,7 @@ Examples:
38258
38492
  const format = args.output || "json";
38259
38493
  if (format !== "json") {
38260
38494
  writeResearchOutput(result.data, format);
38495
+ writeResearchHints([RESEARCH_DATA_NOTE], format);
38261
38496
  return;
38262
38497
  }
38263
38498
  writeResearchJson({
@@ -38538,6 +38773,7 @@ Examples:
38538
38773
  const format = args.output || "json";
38539
38774
  if (format !== "json") {
38540
38775
  writeResearchOutput(data, format);
38776
+ writeResearchHints([RESEARCH_DATA_NOTE], format);
38541
38777
  return;
38542
38778
  }
38543
38779
  writeResearchJson({ ok: true, data, fields: FIELDS12, note: RESEARCH_DATA_NOTE, query_context: queryContext });
@@ -42917,7 +43153,7 @@ var main = defineCommand212({
42917
43153
  Auth: Set BAKER_API_KEY (starts with bk_) and BAKER_API_URL environment variables.
42918
43154
  Chat: Set BAKER_CHAT_ID for action and scheduled-action commands that stage changes against a chat.
42919
43155
  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.
43156
+ Formats: Use --output json|files|md to control output format. Default: json. (The research commands additionally accept csv and jsonl.)
42921
43157
  Introspection: Run 'baker schema <command>' to inspect argument schemas.`
42922
43158
  },
42923
43159
  subCommands: {