@koda-sl/baker-cli 0.192.4 → 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 +93 -9
  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-ISZWNERZ.js → chunk-SSDYBIZB.js} +293 -19
  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 +7253 -5702
  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-ISZWNERZ.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/README.md CHANGED
@@ -90,13 +90,17 @@ All commands return a JSON envelope:
90
90
 
91
91
  Use `--output` to change format:
92
92
 
93
- | Format | Description | Best for |
94
- |---------|------------------------------------|--------------------|
95
- | `json` | Structured JSON envelope (default) | AI agents |
96
- | `csv` | RFC 4180 comma-separated values | Analysis tools |
97
- | `jsonl` | One JSON object per line | Streaming/appending |
98
- | `files` | Tab-separated, one row per result | Piping / shell |
99
- | `md` | Markdown table | Human reading |
93
+ | Format | Description | Best for | Available on |
94
+ |---------|------------------------------------|--------------------|--------------|
95
+ | `json` | Structured JSON envelope (default) | AI agents | every command |
96
+ | `files` | Tab-separated, one row per result | Piping / shell | every command |
97
+ | `md` | Markdown table | Human reading | every command |
98
+ | `csv` | RFC 4180 comma-separated values | Analysis tools | `baker research …` only |
99
+ | `jsonl` | One JSON object per line | Streaming/appending | `baker research …` only |
100
+
101
+ `csv` and `jsonl` are implemented by the research family alone. Asking any other command for them — or for a format that does not exist — falls back to the JSON envelope and says so on stderr, rather than printing nothing and exiting 0.
102
+
103
+ `files` and `md` print rows and nothing else, so any `hints` or data-quality note the envelope carries is written to **stderr** instead. Reading stdout alone still gives you clean, parseable rows.
100
104
 
101
105
  ## Commands
102
106
 
@@ -1391,13 +1395,62 @@ baker research web "What is the pricing of monday.com?" --output md
1391
1395
 
1392
1396
  ---
1393
1397
 
1398
+ ### `baker research fetch "https://url"`
1399
+
1400
+ Read one named page that an ordinary web fetch could not. Bot walls and JavaScript-rendered pages are resolved by Firecrawl on your behalf, so there is no escalation ladder to hand-roll: no retry loop, no waiting, no browser.
1401
+
1402
+ ```bash
1403
+ baker research fetch "https://competitor.com/pricing"
1404
+ baker research fetch "https://competitor.com/pricing" --full
1405
+ baker research fetch "https://app.competitor.com/blog/post" --wait-for 5000
1406
+ baker research fetch "https://competitor.com" --format html --whole-page
1407
+ ```
1408
+
1409
+ **Response:**
1410
+
1411
+ ```json
1412
+ {
1413
+ "ok": true,
1414
+ "data": {
1415
+ "url": "https://competitor.com/pricing",
1416
+ "final_url": "https://www.competitor.com/pricing",
1417
+ "title": "Pricing — Competitor",
1418
+ "content_chars": 48213,
1419
+ "content_shown": 20000,
1420
+ "truncated": false,
1421
+ "provider": "firecrawl",
1422
+ "content": "# Pricing\n\n## Starter\n..."
1423
+ },
1424
+ "hints": ["Showing the first 20,000 of 48,213 characters. Re-run the same command with `--full` ..."]
1425
+ }
1426
+ ```
1427
+
1428
+ **Flags:**
1429
+
1430
+ | Flag | Description |
1431
+ |----------------|--------------------------------------------------------------------------|
1432
+ | `--format` | `markdown` (default) or `html` |
1433
+ | `--wait-for` | Extra ms (0–30000) to let late content settle |
1434
+ | `--whole-page` | Include nav, footer and sidebars (default: main content only) |
1435
+ | `--full` | Return the whole page instead of the first 20,000 characters |
1436
+
1437
+ - `truncated: true` means the page is longer than one read returns, so its tail is missing.
1438
+ - Only Firecrawl is contacted — the target host is never re-requested directly, so a site that is rate-limiting or walling you is not hit again.
1439
+ - Each URL is cached server-side for 24 hours, so a follow-up `--full` call costs no extra credit.
1440
+ - Failures carry `error.fix` with `action: "continue_without"` for pages that will not open — finish the rest of the job and report the gap rather than retrying.
1441
+
1442
+ ---
1443
+
1394
1444
  ### `baker research advertisers "keyword"`
1395
1445
 
1396
- Find domains competing for a keyword in Google SERPs.
1446
+ Returns a ranked list of **domains** competing for one keyword in Google, with average SERP position, relevance rating, estimated traffic value and visibility — domain economics, **NOT ad copy**. Despite the name it returns no headlines, descriptions or creative of any kind; for a competitor's actual ad copy use `baker winning-ads content <adId>` (Meta/LinkedIn only — Google SERP ad copy is not available anywhere in Baker).
1447
+
1448
+ Defaults to `--location us` and `--language en` when omitted; the response carries a `query_context` showing which pair was used, so always set both for non-US or non-English markets.
1397
1449
 
1398
1450
  ```bash
1399
1451
  baker research advertisers "running shoes"
1400
1452
  baker research advertisers "crm software" --location uk --limit 10
1453
+ baker research advertisers "zapatos" --location es --language spanish
1401
1454
  ```
1402
1455
 
1403
1456
  **Response:**
@@ -1744,6 +1797,7 @@ Research data is cached server-side (shared across all callers). No local cache
1744
1797
  | keyword-gap | 6 hours | Same |
1745
1798
  | relevant-pages | 6 hours | Traffic data updates weekly |
1746
1799
  | lighthouse | 24 hours| Page performance stable day-to-day |
1800
+ | fetch | 24 hours| Page content rarely changes within a day |
1747
1801
 
1748
1802
  ---
1749
1803
 
@@ -1775,6 +1829,7 @@ Each external source is its own subcommand. Pick the verb that matches the sourc
1775
1829
  | `baker images upscale <imageId>` | Real-ESRGAN super-resolution via backend ($0.05/image, cost-tracked) | n/a (operates on library image) |
1776
1830
  | `baker images crop <file>` | Coordinate-based rectangular extract — local file or URL | n/a |
1777
1831
  | `baker images dimensions <file\|url>` | Read width / height / aspect / format without decoding | n/a |
1832
+ | `baker images download <targets>` | Remote URLs and/or library ids → local files, so the local transforms can read them | n/a (writes to disk, no upload) |
1778
1833
  | `baker images tags` | List available image tag names (defaults + company custom tags) | n/a |
1779
1834
 
1780
1835
  **Auto-ingest** runs the full `processImage` pipeline (Gemini describe + Voyage multimodal embed + OpenRouter text embed) on every hit. Override with `--auto-ingest N` (turn on) or `--no-auto-ingest` (turn off where default is on). When auto-ingest succeeds, the matching returned hit uses the Baker-owned URL and keeps the original provider URL as `sourceUrl`. After auto-ingest the next `baker images library` query for the same concept hits the local row.
@@ -2314,6 +2369,30 @@ Response:
2314
2369
  }
2315
2370
  ```
2316
2371
 
2372
+ ### `baker images download <targets...> [--out <dir-or-file>]`
2373
+
2374
+ Remote image URLs and/or library image ids → local files. The missing first half of `source → download → normalize → place`: `normalize` refuses URLs and takes no library ids, so without this the only route to disk was `curl`. Local-only, like `normalize` / `crop` / `dimensions` — bytes go straight to disk, never through the backend.
2375
+
2376
+ Targets are space- or comma-separated and may mix both kinds. `--out` is a directory (which must exist) or, with a single target, an exact file path; it defaults to the working directory. Names that would collide are disambiguated, and the extension is derived from the response content type.
2377
+
2378
+ ```bash
2379
+ baker images download https://media.withbaker.com/…/logo.webp
2380
+ baker images download j57abc123 j57def456 --out src/pages/pricing/_images/
2381
+ baker images download https://…/hero.png --out ./hero.png
2382
+ ```
2383
+
2384
+ Response lists what landed and what didn't — a partial failure is still `ok: true`, with the failures in `failed[]` and a hint saying so. Every target failing is `DOWNLOAD_FAILED`. Passing a path that is already on disk is a `VALIDATION_ERROR` whose `fix` points at using the local transforms directly.
2385
+
2386
+ ```json
2387
+ {
2388
+ "ok": true,
2389
+ "data": {
2390
+ "downloaded": [{ "input": "j57abc123", "output": "hero.png", "bytes": 84213, "contentType": "image/png" }],
2391
+ "failed": []
2392
+ }
2393
+ }
2394
+ ```
2395
+
2317
2396
  ### `baker videos search <query>`
2318
2397
 
2319
2398
  Semantic search videos.
@@ -2709,12 +2788,15 @@ baker actions unlink --blocker <id> --blocked <id>
2709
2788
 
2710
2789
  # Review and edit the ops staged in THIS chat before publish
2711
2790
  baker actions draft # show every staged op + footgun warnings
2791
+ baker actions draft --output md # one row per staged op (json default | md | files)
2792
+ baker actions draft --output md --fields kind,name,ref,tags # pick the columns
2793
+ baker actions draft --output md --full # add each op's payload (description, note, reason, blocker)
2712
2794
  baker actions draft remove temp_hero # drop a staged create (cascades its complete/link ops)
2713
2795
  baker actions draft remove <id> --op complete # drop a staged op on a real action (--op update|complete|discard)
2714
2796
  baker actions draft clear # drop everything staged in this chat
2715
2797
  ```
2716
2798
 
2717
- `baker actions draft` is the detailed, op-by-op view of staged (pre-publish) ops plus footgun `warnings`, and the place to edit the draft (`draft remove`/`clear`). The bucketed `list` and a chat-scoped `status` also surface this chat's draft (staged creates, `draftStatus` markers, `draft` ref status), so `draft` is mainly for the full changelog and edits. Do not stage `complete` to cancel an unwanted action — that publishes it as an already-completed item; remove the staged create (or `discard` a published action) instead.
2799
+ `baker actions draft` is the detailed, op-by-op view of staged (pre-publish) ops plus footgun `warnings`, and the place to edit the draft (`draft remove`/`clear`). It takes the same `--output json|md|files` / `--fields` / `--full` trio as the other list commands: `json` (the default) keeps the full envelope with status, count and warnings, while `md` and `files` render one row per op — `kind`, `name`, `ref`, `tags` by default, where `ref` is the handle the next command needs (the tempId for a staged create, the real action id for an op against a published one). An unrecognized `--output` is rejected rather than silently printing nothing. The bucketed `list` and a chat-scoped `status` also surface this chat's draft (staged creates, `draftStatus` markers, `draft` ref status), so `draft` is mainly for the full changelog and edits. Do not stage `complete` to cancel an unwanted action — that publishes it as an already-completed item; remove the staged create (or `discard` a published action) instead.
2718
2800
 
2719
2801
  `baker actions status <ref...>` resolves refs in one request to `/api/actions/status`. It works without `BAKER_CHAT_ID`, but when the CLI has a chat id it includes it so a temp still staged in **that** chat resolves to `status: "draft"` instead of `not_found` (only the caller's own chat draft is consulted). It preserves the backend JSON envelope:
2720
2802
 
@@ -5146,6 +5228,7 @@ baker landing inspiration search "pricing with a monthly/annual toggle" --scope
5146
5228
  baker landing inspiration view <section id> # full DNA + screenshots + motion filmstrip
5147
5229
  baker landing inspiration code <section id> # the standalone bundle
5148
5230
  baker landing inspiration page <source id> # a whole page as a section sequence
5231
+ baker landing inspiration sequences "b2b saas pricing page" --scope all # what follows what, across many pages
5149
5232
  baker landing inspiration add https://linear.app --note "client likes this density"
5150
5233
  baker landing inspiration favorites # what this company has saved
5151
5234
  baker landing inspiration scrape <url> --out <dir> # capture any page now, synchronously
@@ -5159,6 +5242,7 @@ baker landing inspiration scrape <url> --out <dir> # capture any page now, syn
5159
5242
  - **`add` refuses a page Baker already serves**, along with preview and private addresses. For our own pages the source is in the workspace, so a capture would file a screenshot and reconstructed markup next to the real thing — and because the corpus is shared, a live client page admitted here would be readable by every other company.
5160
5243
  - **`add` and `favorite` are recorded on the chat as a "Reference page" change, marked already applied.** The save and the study both happen immediately; there is nothing left for publish to apply and nothing a discard takes back.
5161
5244
  - **Compact by default, `--full` when you have chosen something.** Every list command returns what you need to *pick* a row; the 600-character "why it works" paragraph, the classification facets and the markup ride behind `--full` on `search`, `view`, `page`, `favorites` and `code`. A default `favorites` used to cost more than a default search for a question — "what does this client keep saving?" — that the facets alone answer.
5245
+ - **`sequences` answers the ordering question search structurally can't.** Search ranks individual sections and caps each site at three, so a page's top-to-bottom order never survives its results, and `page` recovers exactly one page's order. `sequences` counts adjacency across many pages at once and returns `openers`, `transitions` (`{ from, to, count }`, most frequent first) and `closers`, filtered by `--scope --domain --section-type --limit`. It returns page **ids only** — `page` stays the one way to read a single page — and always reports `pages_considered` alongside `pages_returned`, because a frequency over an unstated denominator is not evidence. `--full` counts over every matching page and prints the whole table.
5162
5246
  - **`page_id` on every search row** is what `page <id>` takes, so "how does this page sequence its sections?" is reachable from a result rather than only right after `add`. `favorites --limit` is capped at 100.
5163
5247
  - **`add` studies in the background; `scrape` returns with the page on disk.** `add` grows the shared library and takes minutes, so it can never answer "build our page like this one" within the same turn. `scrape` runs the identical capture locally and blocks until it finishes, writing section screenshots, standalone markup, a whole-page reproduction and `report.html`. `--no-motion` is the single biggest lever on runtime; `--no-mobile`, `--no-code` and `--no-report` skip further passes.
5164
5248
  - **Filming is the expensive pass, and it runs three sections at a time.** Every moving section is filmed in its own fresh page load — the only way to catch an entrance animation before it fires — so a page with ten moving sections pays for ten full loads. Measured on one heavy page, filming was 74% of the capture. The takes are independent, so they overlap; a page that cannot be captured inside the library's budget now says so instead of timing out silently.
@@ -2,13 +2,13 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-X5C6HE24.js";
5
+ } from "./chunk-G3O4HRYR.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-ZWYQIEBI.js";
8
+ } from "./chunk-OB26WXHZ.js";
9
9
  import {
10
10
  getEnv
11
- } from "./chunk-YL3HDEIJ.js";
11
+ } from "./chunk-F6OHDJIX.js";
12
12
 
13
13
  // src/commands/ads/meta/shared.ts
14
14
  var DAY_MS = 864e5;
@@ -108,4 +108,4 @@ export {
108
108
  csvOrJson,
109
109
  resolveEffectiveStatus
110
110
  };
111
- //# sourceMappingURL=chunk-PXXJ3HJW.js.map
111
+ //# sourceMappingURL=chunk-34J26VIJ.js.map
@@ -0,0 +1,264 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
26
+
27
+ // ../proxy/src/challenge.ts
28
+ var CHALLENGE_PHRASES = [
29
+ "just a moment",
30
+ "attention required",
31
+ "verify you are human",
32
+ "checking your browser",
33
+ "enable javascript and cookies to continue",
34
+ "unusual traffic",
35
+ "access denied",
36
+ "you have been blocked",
37
+ "request unsuccessful",
38
+ "are you a robot",
39
+ "security check",
40
+ "ddos protection",
41
+ "captcha"
42
+ ];
43
+ var CHALLENGE_MARKERS = ["cf-browser-verification", "cf_chl_", "px-captcha", "_incapsula_", "distil_r_captcha"];
44
+ var CHALLENGE_LENGTH_CEILING = 2e3;
45
+ function isChallengeBody(content, title) {
46
+ if (content.length <= CHALLENGE_LENGTH_CEILING) {
47
+ const haystack = `${title ?? ""}
48
+ ${content}`.toLowerCase();
49
+ if (CHALLENGE_PHRASES.some((phrase) => haystack.includes(phrase))) return true;
50
+ }
51
+ const whole = content.toLowerCase();
52
+ return CHALLENGE_MARKERS.some((marker) => whole.includes(marker));
53
+ }
54
+
55
+ // ../proxy/src/tiers.ts
56
+ var PROXY_TIERS = {
57
+ datacenter: {
58
+ tier: "datacenter",
59
+ host: "dc.oxylabs.io",
60
+ port: 8e3,
61
+ usernamePrefix: "user-",
62
+ countryKey: "-country-",
63
+ sessionKey: null
64
+ },
65
+ residential: {
66
+ tier: "residential",
67
+ host: "pr.oxylabs.io",
68
+ port: 7777,
69
+ usernamePrefix: "customer-",
70
+ countryKey: "-cc-",
71
+ sessionKey: "-sessid-"
72
+ }
73
+ };
74
+ var ESCALATION_ORDER = ["datacenter", "residential"];
75
+ var KNOWN_USERNAME_PREFIXES = ["user-", "customer-"];
76
+
77
+ // ../proxy/src/credentials.ts
78
+ var PROXY_ENV_VARS = {
79
+ datacenter: { username: "OXYLABS_DATACENTER_USERNAME", password: "OXYLABS_DATACENTER_PASSWORD" },
80
+ residential: { username: "OXYLABS_RESIDENTIAL_USERNAME", password: "OXYLABS_RESIDENTIAL_PASSWORD" }
81
+ };
82
+ function readProxyCredentials(env) {
83
+ const credentials = {};
84
+ for (const tier of ESCALATION_ORDER) {
85
+ const username = env[PROXY_ENV_VARS[tier].username]?.trim();
86
+ const password = env[PROXY_ENV_VARS[tier].password]?.trim();
87
+ if (username && password) credentials[tier] = { username, password };
88
+ }
89
+ return credentials;
90
+ }
91
+ function configuredTiers(credentials) {
92
+ return ESCALATION_ORDER.filter((tier) => credentials[tier] !== void 0);
93
+ }
94
+
95
+ // ../proxy/src/escalate.ts
96
+ var ESCALATABLE_STATUSES = /* @__PURE__ */ new Set([403, 429, 451]);
97
+ var ESCALATABLE_NET_ERRORS = /* @__PURE__ */ new Set(["ERR_CONNECTION_RESET", "ERR_CONNECTION_CLOSED", "ERR_EMPTY_RESPONSE"]);
98
+ var PROXY_NET_ERRORS = /* @__PURE__ */ new Set([
99
+ "ERR_TUNNEL_CONNECTION_FAILED",
100
+ "ERR_PROXY_CONNECTION_FAILED",
101
+ "ERR_PROXY_AUTH_REQUESTED",
102
+ "ERR_PROXY_CERTIFICATE_INVALID",
103
+ "ERR_UNEXPECTED_PROXY_AUTH",
104
+ "ERR_MANDATORY_PROXY_CONFIGURATION_FAILED",
105
+ "ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT"
106
+ ]);
107
+ function isProxyFailure(signal) {
108
+ if (signal.status === 407) return true;
109
+ return signal.netError ? PROXY_NET_ERRORS.has(signal.netError) : false;
110
+ }
111
+ function shouldEscalate(signal) {
112
+ if (isProxyFailure(signal)) return false;
113
+ if (signal.timedOut) return false;
114
+ if (signal.challenge) return true;
115
+ if (signal.status != null && ESCALATABLE_STATUSES.has(signal.status)) return true;
116
+ return signal.netError ? ESCALATABLE_NET_ERRORS.has(signal.netError) : false;
117
+ }
118
+
119
+ // ../proxy/src/publicAddress.ts
120
+ var PRIVATE_HOST = /^(localhost|.*\.localhost|.*\.local|127\.\d+\.\d+\.\d+|0\.0\.0\.0|\[?::1\]?)$/;
121
+ var PRIVATE_IP = /^(10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/;
122
+ var SANDBOX_HOST = /\.e2b\.(app|dev)$/;
123
+ function isPrivateHostname(hostname) {
124
+ const host = hostname.trim().toLowerCase();
125
+ if (!host) return true;
126
+ return PRIVATE_HOST.test(host) || PRIVATE_IP.test(host) || SANDBOX_HOST.test(host);
127
+ }
128
+ var BROWSER_PROXY_BYPASS = [
129
+ "localhost",
130
+ "*.localhost",
131
+ "127.0.0.1",
132
+ "0.0.0.0",
133
+ "::1",
134
+ "*.local",
135
+ // A Session's own preview, which is served from the sandbox's public host —
136
+ // public in DNS, ours in every sense that matters here.
137
+ "*.e2b.app",
138
+ "*.e2b.dev",
139
+ "10.*",
140
+ "192.168.*",
141
+ "169.254.*",
142
+ ...Array.from({ length: 16 }, (_, i) => `172.${16 + i}.*`)
143
+ ].join(",");
144
+ function refuseNonPublicUrl(url) {
145
+ let parsed;
146
+ try {
147
+ parsed = new URL(url);
148
+ } catch {
149
+ return "unparseable";
150
+ }
151
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "not_public";
152
+ return isPrivateHostname(parsed.hostname) ? "not_public" : null;
153
+ }
154
+
155
+ // ../proxy/src/route.ts
156
+ var DIRECT_ROUTE = { kind: "direct" };
157
+ function proxyUsername(spec, rawUsername, options) {
158
+ const raw = rawUsername.trim();
159
+ const foreignPrefix = KNOWN_USERNAME_PREFIXES.find(
160
+ (prefix) => prefix !== spec.usernamePrefix && raw.startsWith(prefix)
161
+ );
162
+ if (foreignPrefix) return { ok: false, problem: "wrong_prefix" };
163
+ let username = raw.startsWith(spec.usernamePrefix) ? raw : `${spec.usernamePrefix}${raw}`;
164
+ if (options?.country) username += `${spec.countryKey}${options.country.toUpperCase()}`;
165
+ if (options?.session && spec.sessionKey) username += `${spec.sessionKey}${options.session}`;
166
+ return { ok: true, username };
167
+ }
168
+ function buildProxyRoute(tier, credentials, options) {
169
+ const held = credentials[tier];
170
+ if (!held) return { ok: false, problem: "missing" };
171
+ const spec = PROXY_TIERS[tier];
172
+ const username = proxyUsername(spec, held.username, options);
173
+ if (!username.ok) return username;
174
+ return {
175
+ ok: true,
176
+ route: {
177
+ kind: "proxy",
178
+ tier,
179
+ server: `http://${spec.host}:${spec.port}`,
180
+ username: username.username,
181
+ password: held.password
182
+ }
183
+ };
184
+ }
185
+
186
+ // ../proxy/src/ladder.ts
187
+ function plannedRoutes(url, credentials, options) {
188
+ if (refuseNonPublicUrl(url) !== null) return [DIRECT_ROUTE];
189
+ const routes = [DIRECT_ROUTE];
190
+ for (const tier of configuredTiers(credentials)) {
191
+ const built = buildProxyRoute(tier, credentials, options);
192
+ if (built.ok) routes.push(built.route);
193
+ }
194
+ return routes;
195
+ }
196
+
197
+ // src/env.ts
198
+ import { createEnv } from "@t3-oss/env-core";
199
+ import { z } from "zod";
200
+ var cached;
201
+ function getEnv() {
202
+ if (!cached) {
203
+ cached = createEnv({
204
+ server: {
205
+ BAKER_API_KEY: z.string().startsWith("bk_", "API key must start with 'bk_'"),
206
+ BAKER_API_URL: z.url("BAKER_API_URL must be a valid URL"),
207
+ BAKER_CHAT_ID: z.string().optional(),
208
+ BAKER_ACTING_USER_ID: z.string().optional(),
209
+ BAKER_GOOGLE_ADS_CUSTOMER_ID: z.string().regex(/^\d{10}$/).optional(),
210
+ BAKER_GA4_PROPERTY_ID: z.string().optional(),
211
+ BAKER_GSC_SITE_URL: z.string().optional(),
212
+ BAKER_X_ADS_ACCOUNT_ID: z.string().regex(/^[a-z0-9]+$/, "X Ads account ID must be a base36 string").optional(),
213
+ BAKER_META_AD_ACCOUNT_ID: z.string().optional(),
214
+ BAKER_LINKEDIN_AD_ACCOUNT_ID: z.string().regex(/^\d+$/, "LinkedIn ad account ID must be the numeric portion of urn:li:sponsoredAccount:N").optional()
215
+ },
216
+ runtimeEnv: process.env
217
+ });
218
+ }
219
+ return cached;
220
+ }
221
+ function debugLogSetting() {
222
+ const raw = process.env.BAKER_DEBUG_LOG?.trim();
223
+ return raw ? raw : void 0;
224
+ }
225
+ function requireChatId() {
226
+ const env = getEnv();
227
+ if (!env.BAKER_CHAT_ID) {
228
+ throw new Error(
229
+ "BAKER_CHAT_ID is not set. This command stages changes against a chat \u2014 run it from a chat-attached environment."
230
+ );
231
+ }
232
+ return env.BAKER_CHAT_ID;
233
+ }
234
+ function resolveChatId(chat) {
235
+ return typeof chat === "string" && chat.length > 0 ? chat : requireChatId();
236
+ }
237
+ function captureBudgetMs() {
238
+ const raw = Number(process.env.BAKER_CAPTURE_BUDGET_MS);
239
+ return Number.isFinite(raw) && raw > 0 ? raw : null;
240
+ }
241
+ function captureProxyCredentials() {
242
+ return readProxyCredentials(process.env);
243
+ }
244
+ function childEnvWith(extra) {
245
+ return { ...process.env, ...extra };
246
+ }
247
+
248
+ export {
249
+ __commonJS,
250
+ __toESM,
251
+ isChallengeBody,
252
+ isProxyFailure,
253
+ shouldEscalate,
254
+ refuseNonPublicUrl,
255
+ plannedRoutes,
256
+ getEnv,
257
+ debugLogSetting,
258
+ requireChatId,
259
+ resolveChatId,
260
+ captureBudgetMs,
261
+ captureProxyCredentials,
262
+ childEnvWith
263
+ };
264
+ //# sourceMappingURL=chunk-F6OHDJIX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../proxy/src/challenge.ts","../../proxy/src/tiers.ts","../../proxy/src/credentials.ts","../../proxy/src/escalate.ts","../../proxy/src/publicAddress.ts","../../proxy/src/route.ts","../../proxy/src/ladder.ts","../src/env.ts"],"sourcesContent":["/**\n * Was that a page, or the wall in front of it?\n *\n * A bot filter answers 200. The body parses, it has a title, and every check\n * downstream — `response.ok`, the status, the content type — says the read\n * succeeded. So a challenge is the one block that cannot be seen from the\n * status line, and the only place it is visible is the bytes.\n *\n * This lives in `@baker/proxy` rather than beside any one caller because the\n * escalation ladder is only coherent if every surface agrees on what \"blocked\"\n * means: the rung that gets climbed is chosen from this answer, and a detector\n * that differs per caller would mean the same wall costs money on one surface\n * and is filed as content on another.\n *\n * Pure and dependency-free, so it stays reachable from Convex's V8 runtime\n * through the package barrel.\n */\n\n/**\n * Phrases that only appear on a block or challenge page.\n *\n * Deliberately specific — \"access denied\" alone would match a page *about*\n * access control, so each phrase is one a real marketing page has no reason to\n * use as its title.\n */\nconst CHALLENGE_PHRASES = [\n \"just a moment\",\n \"attention required\",\n \"verify you are human\",\n \"checking your browser\",\n \"enable javascript and cookies to continue\",\n \"unusual traffic\",\n \"access denied\",\n \"you have been blocked\",\n \"request unsuccessful\",\n \"are you a robot\",\n \"security check\",\n \"ddos protection\",\n \"captcha\",\n];\n\n/** Vendors whose block pages carry a fingerprint even when the title does not. */\nconst CHALLENGE_MARKERS = [\"cf-browser-verification\", \"cf_chl_\", \"px-captcha\", \"_incapsula_\", \"distil_r_captcha\"];\n\n/**\n * How long a response may be and still be judged by its *wording*.\n *\n * Phrases like \"captcha\" or \"access denied\" are ordinary English that a real\n * article can legitimately contain, so matching them anywhere would refuse\n * pages we read perfectly well. Length is the precision guard: a page that is\n * mostly content is content.\n */\nexport const CHALLENGE_LENGTH_CEILING = 2_000;\n\n/**\n * Whether these bytes are a bot wall rather than the thing that was asked for.\n *\n * Reads only what the caller already paid for. Nothing here issues a request —\n * asking the host again would deepen the very block this is detecting.\n *\n * The two lists are searched differently, and that asymmetry is the point.\n * Phrases are judged only on a short response, for the precision reason above.\n * Vendor markers are fingerprints nothing but the vendor emits, so they are\n * searched at any length — which is what makes a real interstitial detectable\n * at all. Cloudflare's is 5–15 KB of inlined script wrapped around one visible\n * sentence, so gating markers behind the same length ceiling as the phrases put\n * every genuine large wall in a dead band where neither list could reach it.\n */\nexport function isChallengeBody(content: string, title?: string | null): boolean {\n if (content.length <= CHALLENGE_LENGTH_CEILING) {\n const haystack = `${title ?? \"\"}\\n${content}`.toLowerCase();\n if (CHALLENGE_PHRASES.some((phrase) => haystack.includes(phrase))) return true;\n }\n\n const whole = content.toLowerCase();\n return CHALLENGE_MARKERS.some((marker) => whole.includes(marker));\n}\n","/**\n * The two Oxylabs products, and everything that differs between them.\n *\n * They are NOT credential-swappable. Endpoint, username prefix, geo token and\n * sticky-session mechanism all differ, so \"same URL, other username/password\"\n * fails — and it fails as a 407, which Chromium reports as a plain network\n * error. Before this module existed there was no code path that could tell that\n * apart from the site refusing us, so the failure would have been recorded\n * against the page rather than against our own configuration.\n *\n * That asymmetry is the reason this table exists in exactly one place. Every\n * caller builds its route through `buildProxyRoute`; nobody concatenates an\n * Oxylabs URL by hand.\n */\n\nexport type ProxyTier = \"datacenter\" | \"residential\";\n\nexport interface ProxyTierSpec {\n readonly tier: ProxyTier;\n readonly host: string;\n readonly port: number;\n /** Oxylabs requires this in front of the account name. The two differ. */\n readonly usernamePrefix: \"user-\" | \"customer-\";\n /** Country targeting token. `-country-` on datacenter, `-cc-` on residential. */\n readonly countryKey: \"-country-\" | \"-cc-\";\n /**\n * Sticky-session token, or null when the product has none.\n *\n * Datacenter does stickiness by *port* (8001 for IP #1, 8002 for #2), not by\n * username. We do not implement that: a capture is a single page load, so a\n * reusable IP buys nothing and the knob would only add a way to get it wrong.\n */\n readonly sessionKey: \"-sessid-\" | null;\n}\n\nexport const PROXY_TIERS: Readonly<Record<ProxyTier, ProxyTierSpec>> = {\n datacenter: {\n tier: \"datacenter\",\n host: \"dc.oxylabs.io\",\n port: 8000,\n usernamePrefix: \"user-\",\n countryKey: \"-country-\",\n sessionKey: null,\n },\n residential: {\n tier: \"residential\",\n host: \"pr.oxylabs.io\",\n port: 7777,\n usernamePrefix: \"customer-\",\n countryKey: \"-cc-\",\n sessionKey: \"-sessid-\",\n },\n};\n\n/**\n * Cheapest useful route first, and the ladder never skips a rung.\n *\n * Residential costs materially more than datacenter, so it is only ever reached\n * because datacenter was tried and refused — never as a first guess and never\n * speculatively.\n */\nexport const ESCALATION_ORDER = [\"datacenter\", \"residential\"] as const satisfies readonly ProxyTier[];\n\n/** Every prefix this module knows, used to spot a credential in the wrong slot. */\nexport const KNOWN_USERNAME_PREFIXES = [\"user-\", \"customer-\"] as const;\n","import type { ProxyTier } from \"./tiers.ts\";\nimport { ESCALATION_ORDER } from \"./tiers.ts\";\n\n/**\n * The four environment variable names, in one place.\n *\n * Convex holds these and hands them to the sandbox; the CLI reads them back\n * out. A typo on either side is a *silent* no-op — the capability appears to\n * ship, every fetch quietly takes the direct route, and nothing fails. That is\n * the worst failure mode available to this feature, so the names are a shared\n * constant with a contract test rather than eight string literals spread across\n * two packages.\n */\nexport const PROXY_ENV_VARS = {\n datacenter: { username: \"OXYLABS_DATACENTER_USERNAME\", password: \"OXYLABS_DATACENTER_PASSWORD\" },\n residential: { username: \"OXYLABS_RESIDENTIAL_USERNAME\", password: \"OXYLABS_RESIDENTIAL_PASSWORD\" },\n} as const satisfies Record<ProxyTier, { username: string; password: string }>;\n\nexport interface TierCredentials {\n readonly username: string;\n readonly password: string;\n}\n\nexport type ProxyCredentials = Partial<Readonly<Record<ProxyTier, TierCredentials>>>;\n\n/**\n * Read whichever tiers are fully configured.\n *\n * Takes the env record rather than reading `process.env` itself, so the whole\n * package stays pure and the Convex and CLI sides can be tested identically.\n *\n * Half a pair is not a credential. A username with no password would build a\n * route that authenticates as nobody, and Oxylabs answers that with the same\n * 407 as a wrong password — so it would look like a broken proxy rather than an\n * unconfigured one. Dropping it here means `configuredTiers` tells the truth.\n */\nexport function readProxyCredentials(env: Record<string, string | undefined>): ProxyCredentials {\n const credentials: { -readonly [K in ProxyTier]?: TierCredentials } = {};\n for (const tier of ESCALATION_ORDER) {\n const username = env[PROXY_ENV_VARS[tier].username]?.trim();\n const password = env[PROXY_ENV_VARS[tier].password]?.trim();\n if (username && password) credentials[tier] = { username, password };\n }\n return credentials;\n}\n\n/** The highest rung a deployment permits itself, `\"none\"` meaning direct only. */\nexport type ProxyCeiling = \"none\" | ProxyTier;\n\n/**\n * Drop every credential above the ceiling.\n *\n * The incident valve, and it works by *withholding* rather than by asking\n * nicely. Residential is the expensive tier; when a bandwidth alert fires at\n * 03:00 the useful control is one that takes effect without a deploy and that\n * no downstream bug can talk its way past. A ceiling the route builder merely\n * consulted would still have handed the credential to a sandbox, where anything\n * holding it could spend it.\n *\n * So callers read credentials *through* this, never around it.\n */\nexport function cappedCredentials(credentials: ProxyCredentials, ceiling: ProxyCeiling): ProxyCredentials {\n if (ceiling === \"none\") return {};\n const allowed: { -readonly [K in ProxyTier]?: TierCredentials } = {};\n for (const tier of ESCALATION_ORDER) {\n const held = credentials[tier];\n if (held) allowed[tier] = held;\n if (tier === ceiling) break;\n }\n return allowed;\n}\n\n/** Which tiers this process could actually reach, cheapest first. */\nexport function configuredTiers(credentials: ProxyCredentials): ProxyTier[] {\n return ESCALATION_ORDER.filter((tier) => credentials[tier] !== undefined);\n}\n\n/** Whether there is any proxy to escalate to at all. */\nexport function proxyEscalationConfigured(credentials: ProxyCredentials): boolean {\n return configuredTiers(credentials).length > 0;\n}\n","/**\n * When is a failure worth spending a more expensive route on?\n *\n * The ladder only ever climbs on evidence that *who we are* was the problem. A\n * page that 404s, a domain that does not resolve, a certificate that will not\n * negotiate and a site that timed out all fail identically from every IP on\n * earth, so retrying them through a metered exit buys a second identical\n * failure and a bill. That asymmetry — cheap to be wrong in one direction,\n * expensive in the other — is why this is a closed allow-list rather than\n * \"escalate unless we recognise the error\".\n *\n * Shared deliberately: the capture engine, the image fetchers and the ad-media\n * downloaders all have to agree on what \"blocked\" means, or the corpus and the\n * bill disagree about the same event.\n */\n\n/** What one attempt observed, normalised across Playwright and `fetch`. */\nexport interface BlockSignal {\n /** Main-document / response status, when there was one. */\n readonly status?: number | null;\n /** Chromium `net::` name or a Node error code, when the attempt threw one. */\n readonly netError?: string | null;\n /** A challenge body was detected behind an otherwise successful response. */\n readonly challenge?: boolean;\n /** The attempt ran out of time rather than being refused. */\n readonly timedOut?: boolean;\n}\n\n/** \"We don't like *you*\" — a different exit IP is a different you. */\nconst ESCALATABLE_STATUSES = new Set([403, 429, 451]);\n\n/**\n * A TCP reset on ClientHello is how a WAF null-routes a datacenter range.\n *\n * Today all three of these collapse into one \"site didn't respond\" message and\n * are filed non-retryable, which is precisely why this class of block has been\n * invisible: it looks identical to a site that is genuinely down.\n */\nconst ESCALATABLE_NET_ERRORS = new Set([\"ERR_CONNECTION_RESET\", \"ERR_CONNECTION_CLOSED\", \"ERR_EMPTY_RESPONSE\"]);\n\n/**\n * Failures that belong to our route, not to the site.\n *\n * A 407 cannot come from an origin server — only something speaking proxy\n * produces one — so this needs no \"was this attempt proxied\" flag to be safe.\n */\nconst PROXY_NET_ERRORS = new Set([\n \"ERR_TUNNEL_CONNECTION_FAILED\",\n \"ERR_PROXY_CONNECTION_FAILED\",\n \"ERR_PROXY_AUTH_REQUESTED\",\n \"ERR_PROXY_CERTIFICATE_INVALID\",\n \"ERR_UNEXPECTED_PROXY_AUTH\",\n \"ERR_MANDATORY_PROXY_CONFIGURATION_FAILED\",\n \"ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT\",\n]);\n\nexport function isProxyFailure(signal: BlockSignal): boolean {\n if (signal.status === 407) return true;\n return signal.netError ? PROXY_NET_ERRORS.has(signal.netError) : false;\n}\n\nexport function shouldEscalate(signal: BlockSignal): boolean {\n // Our own broken route never advances the ladder — it degrades it. Climbing\n // here would spend a more expensive rung reproducing our misconfiguration.\n if (isProxyFailure(signal)) return false;\n if (signal.timedOut) return false;\n if (signal.challenge) return true;\n if (signal.status != null && ESCALATABLE_STATUSES.has(signal.status)) return true;\n return signal.netError ? ESCALATABLE_NET_ERRORS.has(signal.netError) : false;\n}\n","/**\n * Addresses that must never be fetched, and must never be proxied.\n *\n * Lifted from `convex/landingLibrary/utils.ts`, where these three regexes were\n * the only copy in the repo — reachable from Convex and from nowhere else. That\n * was survivable while the capture ran one hop after the check. It is not\n * survivable now: the CLI can be invoked directly, two Convex paths enqueue\n * without re-validating, and a proxy adds a rung where \"which network am I on\"\n * stops being rhetorical.\n *\n * Two distinct reasons live here, and both point the same way:\n *\n * - **There is nothing to read.** A private address only resolves from inside\n * the network asking, so a capture of one produces a screenshot of an error.\n * - **There is something to read, and we must not.** `169.254.169.254` is the\n * cloud metadata endpoint that every capture runner has a route to.\n *\n * The rule for the proxy is *refuse*, not *bypass*. Bypassing would leave a\n * request that still happens, just unproxied; refusing means there is no\n * request to route. That is a stronger guarantee and a simpler one to check.\n */\n\n/** Hostnames that only resolve from inside the machine or network asking. */\nconst PRIVATE_HOST = /^(localhost|.*\\.localhost|.*\\.local|127\\.\\d+\\.\\d+\\.\\d+|0\\.0\\.0\\.0|\\[?::1\\]?)$/;\n// `169.254.` covers link-local, and with it the cloud metadata address every\n// capture runner has a route to.\nconst PRIVATE_IP = /^(10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/;\n/** Where a Session's own preview is served while the page is being built. */\nconst SANDBOX_HOST = /\\.e2b\\.(app|dev)$/;\n\nexport function isPrivateHostname(hostname: string): boolean {\n const host = hostname.trim().toLowerCase();\n if (!host) return true;\n return PRIVATE_HOST.test(host) || PRIVATE_IP.test(host) || SANDBOX_HOST.test(host);\n}\n\n/**\n * The same rule again, in the only notation a browser's proxy settings speak.\n *\n * Chromium — and so Playwright and `agent-browser` — matches a bypass entry\n * against the hostname and understands one wildcard and no CIDR at all. The\n * ranges above therefore cannot be handed over as regexes; `172.16/12` has to\n * become sixteen entries, and there is no way to express \"any private address\".\n *\n * That makes this a second copy of one fact, which is worth stating plainly:\n * the day it disagrees with `isPrivateHostname` is the day a Session's own\n * preview at `localhost:4321` goes out through a metered exit and comes back\n * refused. Its test pins the correspondence.\n *\n * Note the direction reverses here, and deliberately. Everywhere else the rule\n * for a private address is *refuse*, because a request that still happens is\n * not a guarantee. A browser the agent drives has to reach `localhost:4321` —\n * that is its main job — so here the private address is the legitimate traffic\n * and the list says \"go direct\", not \"do not go\".\n */\nexport const BROWSER_PROXY_BYPASS = [\n \"localhost\",\n \"*.localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"::1\",\n \"*.local\",\n // A Session's own preview, which is served from the sandbox's public host —\n // public in DNS, ours in every sense that matters here.\n \"*.e2b.app\",\n \"*.e2b.dev\",\n \"10.*\",\n \"192.168.*\",\n \"169.254.*\",\n ...Array.from({ length: 16 }, (_, i) => `172.${16 + i}.*`),\n].join(\",\");\n\nexport type NonPublicReason = \"not_public\" | \"unparseable\";\n\n/**\n * `null` when the URL is safe to fetch; a reason when it must be refused.\n *\n * Anything that is not plain http(s) is refused too. A capture is handed URLs\n * from agents and from stored rows, and `file:` reaching a browser we launched\n * is a local file read wearing a URL.\n */\nexport function refuseNonPublicUrl(url: string): NonPublicReason | null {\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return \"unparseable\";\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") return \"not_public\";\n return isPrivateHostname(parsed.hostname) ? \"not_public\" : null;\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport type { ProxyTier, ProxyTierSpec } from \"./tiers.ts\";\nimport { KNOWN_USERNAME_PREFIXES, PROXY_TIERS } from \"./tiers.ts\";\n\n/**\n * How one attempt reaches the internet.\n *\n * Credentials stay in separate fields instead of being embedded in the URL:\n * Playwright's `proxy` option wants them that way, undici's `ProxyAgent` takes\n * them as a header we build once, and — the real reason — a password that never\n * exists as a substring of a URL cannot be leaked by anything that logs a URL.\n */\nexport type ProxyRoute =\n | { readonly kind: \"direct\" }\n | {\n readonly kind: \"proxy\";\n readonly tier: ProxyTier;\n /** e.g. `http://dc.oxylabs.io:8000` */\n readonly server: string;\n readonly username: string;\n readonly password: string;\n };\n\nexport const DIRECT_ROUTE: ProxyRoute = { kind: \"direct\" };\n\nexport interface RouteOptions {\n /** ISO-3166 alpha-2, upper-cased here. Omit for \"wherever\" — the default. */\n readonly country?: string;\n /** Sticky-session id. Ignored by a tier whose product has no session syntax. */\n readonly session?: string;\n}\n\nexport type RouteProblem = \"missing\" | \"wrong_prefix\";\n\nexport type RouteResult = { ok: true; route: ProxyRoute } | { ok: false; problem: RouteProblem };\n\n/**\n * Put the account name into the shape this specific product expects.\n *\n * Three cases, and the third is the one worth being strict about:\n *\n * 1. Bare (`acme`) — someone pasted the account name. Prepend the prefix.\n * 2. Already correct (`user-acme` in the datacenter slot) — someone pasted the\n * full Oxylabs username. Leave it alone.\n * 3. Carrying the OTHER product's prefix (`customer-acme` in the datacenter\n * slot) — refuse.\n *\n * Case 3 must not be \"helpfully\" rewritten. A `customer-` value in the\n * datacenter slot means the residential credentials were pasted into the wrong\n * variable, and rewriting the prefix would authenticate a residential account\n * against the datacenter endpoint. That either 407s — wasting the cheap rung\n * for no reason — or it works, and bills residential rates from the tier whose\n * entire purpose is to be the cheap one. Refusing is the only answer that\n * cannot silently cost money.\n */\nexport function proxyUsername(\n spec: ProxyTierSpec,\n rawUsername: string,\n options?: RouteOptions,\n): { ok: true; username: string } | { ok: false; problem: \"wrong_prefix\" } {\n const raw = rawUsername.trim();\n const foreignPrefix = KNOWN_USERNAME_PREFIXES.find(\n (prefix) => prefix !== spec.usernamePrefix && raw.startsWith(prefix),\n );\n if (foreignPrefix) return { ok: false, problem: \"wrong_prefix\" };\n\n let username = raw.startsWith(spec.usernamePrefix) ? raw : `${spec.usernamePrefix}${raw}`;\n // Geo before session — Oxylabs reads the username left to right and rejects\n // the pair in the other order.\n if (options?.country) username += `${spec.countryKey}${options.country.toUpperCase()}`;\n if (options?.session && spec.sessionKey) username += `${spec.sessionKey}${options.session}`;\n return { ok: true, username };\n}\n\n/** Build the route for one tier, or say why there isn't one. */\nexport function buildProxyRoute(tier: ProxyTier, credentials: ProxyCredentials, options?: RouteOptions): RouteResult {\n const held = credentials[tier];\n if (!held) return { ok: false, problem: \"missing\" };\n\n const spec = PROXY_TIERS[tier];\n const username = proxyUsername(spec, held.username, options);\n if (!username.ok) return username;\n\n return {\n ok: true,\n route: {\n kind: \"proxy\",\n tier,\n server: `http://${spec.host}:${spec.port}`,\n username: username.username,\n password: held.password,\n },\n };\n}\n","import type { ProxyCredentials } from \"./credentials.ts\";\nimport { configuredTiers } from \"./credentials.ts\";\nimport { refuseNonPublicUrl } from \"./publicAddress.ts\";\nimport type { ProxyRoute, RouteOptions } from \"./route.ts\";\nimport { buildProxyRoute, DIRECT_ROUTE } from \"./route.ts\";\n\n/**\n * Every route this URL is allowed to be attempted on, in the order to try them.\n *\n * Direct is always first and always present: an unblocked page must cost\n * nothing, and a deployment with no credentials has to behave exactly as it did\n * before any of this existed.\n *\n * A tier whose credentials are malformed is *skipped*, not fatal. One\n * mis-pasted variable should cost the use of that rung, not the whole ladder —\n * the alternative is that a typo in the cheap tier silently disables the\n * expensive one that would have worked.\n */\nexport function plannedRoutes(url: string, credentials: ProxyCredentials, options?: RouteOptions): ProxyRoute[] {\n // A private address is refused upstream; if one reaches here anyway it must\n // not become a billed request, and there is no exit node on earth from which\n // `localhost` means us.\n if (refuseNonPublicUrl(url) !== null) return [DIRECT_ROUTE];\n\n const routes: ProxyRoute[] = [DIRECT_ROUTE];\n for (const tier of configuredTiers(credentials)) {\n const built = buildProxyRoute(tier, credentials, options);\n if (built.ok) routes.push(built.route);\n }\n return routes;\n}\n","import type { ProxyCredentials } from \"@baker/proxy\";\nimport { readProxyCredentials } from \"@baker/proxy\";\nimport { createEnv } from \"@t3-oss/env-core\";\nimport { z } from \"zod\";\n\ntype Env = {\n BAKER_API_KEY: string;\n BAKER_API_URL: string;\n BAKER_CHAT_ID?: string;\n BAKER_ACTING_USER_ID?: string;\n BAKER_GOOGLE_ADS_CUSTOMER_ID?: string;\n BAKER_GA4_PROPERTY_ID?: string;\n BAKER_GSC_SITE_URL?: string;\n BAKER_X_ADS_ACCOUNT_ID?: string;\n BAKER_META_AD_ACCOUNT_ID?: string;\n BAKER_LINKEDIN_AD_ACCOUNT_ID?: string;\n};\n\nlet cached: Env | undefined;\n\nexport function getEnv(): Env {\n if (!cached) {\n cached = createEnv({\n server: {\n BAKER_API_KEY: z.string().startsWith(\"bk_\", \"API key must start with 'bk_'\"),\n BAKER_API_URL: z.url(\"BAKER_API_URL must be a valid URL\"),\n BAKER_CHAT_ID: z.string().optional(),\n BAKER_ACTING_USER_ID: z.string().optional(),\n BAKER_GOOGLE_ADS_CUSTOMER_ID: z\n .string()\n .regex(/^\\d{10}$/)\n .optional(),\n BAKER_GA4_PROPERTY_ID: z.string().optional(),\n BAKER_GSC_SITE_URL: z.string().optional(),\n BAKER_X_ADS_ACCOUNT_ID: z\n .string()\n .regex(/^[a-z0-9]+$/, \"X Ads account ID must be a base36 string\")\n .optional(),\n BAKER_META_AD_ACCOUNT_ID: z.string().optional(),\n BAKER_LINKEDIN_AD_ACCOUNT_ID: z\n .string()\n .regex(/^\\d+$/, \"LinkedIn ad account ID must be the numeric portion of urn:li:sponsoredAccount:N\")\n .optional(),\n },\n runtimeEnv: process.env,\n });\n }\n return cached;\n}\n\n// Read directly (not via the validated schema): debug logging must work even when\n// the required BAKER_API_KEY/BAKER_API_URL are missing or malformed.\nexport function debugLogSetting(): string | undefined {\n const raw = process.env.BAKER_DEBUG_LOG?.trim();\n return raw ? raw : undefined;\n}\n\nexport function requireChatId(): string {\n const env = getEnv();\n if (!env.BAKER_CHAT_ID) {\n throw new Error(\n \"BAKER_CHAT_ID is not set. This command stages changes against a chat — run it from a chat-attached environment.\",\n );\n }\n return env.BAKER_CHAT_ID;\n}\n\n/**\n * Which chat a *read* addresses. A `--chat` value names another chat in the same company — how an\n * earlier chat's staged changes are recovered verbatim instead of rebuilt from a summary — and with\n * no flag it is this session's own chat.\n *\n * Read paths only. Staging, amending and discarding keep calling `requireChatId`, so one session can\n * never edit another's draft; the backend enforces the same thing twice over, since every mutating\n * internal requires the draft be `active` and every route checks the chat belongs to the caller's\n * company.\n */\nexport function resolveChatId(chat?: unknown): string {\n return typeof chat === \"string\" && chat.length > 0 ? chat : requireChatId();\n}\n\n/**\n * Wall clock the whole capture may spend, when the caller bounded it.\n *\n * Read from the environment rather than taken as a flag, deliberately. A\n * `--budget-ms` argument would land in the command schema, and the schema is\n * what the agent reads — one question away from \"what is the budget for?\", and\n * from there the escalation ladder stops being invisible to it.\n *\n * Kept out of `getEnv()` because that validator is for a chat-attached run; the\n * capture engine also runs where none of those variables exist.\n */\nexport function captureBudgetMs(): number | null {\n const raw = Number(process.env.BAKER_CAPTURE_BUDGET_MS);\n return Number.isFinite(raw) && raw > 0 ? raw : null;\n}\n\n/**\n * Egress-proxy credentials for a capture, or none at all.\n *\n * These arrive as sandbox *command* env, never as arguments — a credential in\n * argv is readable by any `ps` sharing the sandbox. With none set,\n * `plannedRoutes` yields the direct route alone and a capture behaves exactly\n * as it did before the ladder existed.\n *\n * Deliberately **not** capped here, and that is not an omission: `OXYLABS_MAX_TIER`\n * is a deployment control and lives on the Convex side, where `proxyCredentials()`\n * applies it by withholding. A tier the deployment has closed never reaches this\n * process at all, so there is nothing left for the CLI to filter — and a second\n * ceiling read from a variable nobody sets here would only be a way to disagree.\n */\nexport function captureProxyCredentials(): ProxyCredentials {\n return readProxyCredentials(process.env);\n}\n\n/**\n * This process's environment plus `extra`, for handing to a child.\n *\n * Lives here because `process.env` reads belong in this file — but it earns its\n * place beyond the lint rule: `spawn`'s `env` *replaces* the environment rather\n * than extending it, so a caller that passes only its additions silently strips\n * `PATH` and the child fails to start for a reason that looks nothing like the\n * cause.\n */\nexport function childEnvWith(extra: Record<string, string>): NodeJS.ProcessEnv {\n return { ...process.env, ...extra };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAM,oBAAoB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,oBAAoB,CAAC,2BAA2B,WAAW,cAAc,eAAe,kBAAkB;AAUzG,IAAM,2BAA2B;AAgBjC,SAAS,gBAAgB,SAAiB,OAAgC;AAC/E,MAAI,QAAQ,UAAU,0BAA0B;AAC9C,UAAM,WAAW,GAAG,SAAS,EAAE;AAAA,EAAK,OAAO,GAAG,YAAY;AAC1D,QAAI,kBAAkB,KAAK,CAAC,WAAW,SAAS,SAAS,MAAM,CAAC,EAAG,QAAO;AAAA,EAC5E;AAEA,QAAM,QAAQ,QAAQ,YAAY;AAClC,SAAO,kBAAkB,KAAK,CAAC,WAAW,MAAM,SAAS,MAAM,CAAC;AAClE;;;ACzCO,IAAM,cAA0D;AAAA,EACrE,YAAY;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AAAA,EACA,aAAa;AAAA,IACX,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,gBAAgB;AAAA,IAChB,YAAY;AAAA,IACZ,YAAY;AAAA,EACd;AACF;AASO,IAAM,mBAAmB,CAAC,cAAc,aAAa;AAGrD,IAAM,0BAA0B,CAAC,SAAS,WAAW;;;ACnDrD,IAAM,iBAAiB;AAAA,EAC5B,YAAY,EAAE,UAAU,+BAA+B,UAAU,8BAA8B;AAAA,EAC/F,aAAa,EAAE,UAAU,gCAAgC,UAAU,+BAA+B;AACpG;AAoBO,SAAS,qBAAqB,KAA2D;AAC9F,QAAM,cAAgE,CAAC;AACvE,aAAW,QAAQ,kBAAkB;AACnC,UAAM,WAAW,IAAI,eAAe,IAAI,EAAE,QAAQ,GAAG,KAAK;AAC1D,UAAM,WAAW,IAAI,eAAe,IAAI,EAAE,QAAQ,GAAG,KAAK;AAC1D,QAAI,YAAY,SAAU,aAAY,IAAI,IAAI,EAAE,UAAU,SAAS;AAAA,EACrE;AACA,SAAO;AACT;AA6BO,SAAS,gBAAgB,aAA4C;AAC1E,SAAO,iBAAiB,OAAO,CAAC,SAAS,YAAY,IAAI,MAAM,MAAS;AAC1E;;;AC9CA,IAAM,uBAAuB,oBAAI,IAAI,CAAC,KAAK,KAAK,GAAG,CAAC;AASpD,IAAM,yBAAyB,oBAAI,IAAI,CAAC,wBAAwB,yBAAyB,oBAAoB,CAAC;AAQ9G,IAAM,mBAAmB,oBAAI,IAAI;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,eAAe,QAA8B;AAC3D,MAAI,OAAO,WAAW,IAAK,QAAO;AAClC,SAAO,OAAO,WAAW,iBAAiB,IAAI,OAAO,QAAQ,IAAI;AACnE;AAEO,SAAS,eAAe,QAA8B;AAG3D,MAAI,eAAe,MAAM,EAAG,QAAO;AACnC,MAAI,OAAO,SAAU,QAAO;AAC5B,MAAI,OAAO,UAAW,QAAO;AAC7B,MAAI,OAAO,UAAU,QAAQ,qBAAqB,IAAI,OAAO,MAAM,EAAG,QAAO;AAC7E,SAAO,OAAO,WAAW,uBAAuB,IAAI,OAAO,QAAQ,IAAI;AACzE;;;AC9CA,IAAM,eAAe;AAGrB,IAAM,aAAa;AAEnB,IAAM,eAAe;AAEd,SAAS,kBAAkB,UAA2B;AAC3D,QAAM,OAAO,SAAS,KAAK,EAAE,YAAY;AACzC,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,aAAa,KAAK,IAAI,KAAK,WAAW,KAAK,IAAI,KAAK,aAAa,KAAK,IAAI;AACnF;AAqBO,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,CAAC,GAAG,MAAM,OAAO,KAAK,CAAC,IAAI;AAC3D,EAAE,KAAK,GAAG;AAWH,SAAS,mBAAmB,KAAqC;AACtE,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,SAAU,QAAO;AACxE,SAAO,kBAAkB,OAAO,QAAQ,IAAI,eAAe;AAC7D;;;ACnEO,IAAM,eAA2B,EAAE,MAAM,SAAS;AAgClD,SAAS,cACd,MACA,aACA,SACyE;AACzE,QAAM,MAAM,YAAY,KAAK;AAC7B,QAAM,gBAAgB,wBAAwB;AAAA,IAC5C,CAAC,WAAW,WAAW,KAAK,kBAAkB,IAAI,WAAW,MAAM;AAAA,EACrE;AACA,MAAI,cAAe,QAAO,EAAE,IAAI,OAAO,SAAS,eAAe;AAE/D,MAAI,WAAW,IAAI,WAAW,KAAK,cAAc,IAAI,MAAM,GAAG,KAAK,cAAc,GAAG,GAAG;AAGvF,MAAI,SAAS,QAAS,aAAY,GAAG,KAAK,UAAU,GAAG,QAAQ,QAAQ,YAAY,CAAC;AACpF,MAAI,SAAS,WAAW,KAAK,WAAY,aAAY,GAAG,KAAK,UAAU,GAAG,QAAQ,OAAO;AACzF,SAAO,EAAE,IAAI,MAAM,SAAS;AAC9B;AAGO,SAAS,gBAAgB,MAAiB,aAA+B,SAAqC;AACnH,QAAM,OAAO,YAAY,IAAI;AAC7B,MAAI,CAAC,KAAM,QAAO,EAAE,IAAI,OAAO,SAAS,UAAU;AAElD,QAAM,OAAO,YAAY,IAAI;AAC7B,QAAM,WAAW,cAAc,MAAM,KAAK,UAAU,OAAO;AAC3D,MAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,OAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,QAAQ,UAAU,KAAK,IAAI,IAAI,KAAK,IAAI;AAAA,MACxC,UAAU,SAAS;AAAA,MACnB,UAAU,KAAK;AAAA,IACjB;AAAA,EACF;AACF;;;AC3EO,SAAS,cAAc,KAAa,aAA+B,SAAsC;AAI9G,MAAI,mBAAmB,GAAG,MAAM,KAAM,QAAO,CAAC,YAAY;AAE1D,QAAM,SAAuB,CAAC,YAAY;AAC1C,aAAW,QAAQ,gBAAgB,WAAW,GAAG;AAC/C,UAAM,QAAQ,gBAAgB,MAAM,aAAa,OAAO;AACxD,QAAI,MAAM,GAAI,QAAO,KAAK,MAAM,KAAK;AAAA,EACvC;AACA,SAAO;AACT;;;AC5BA,SAAS,iBAAiB;AAC1B,SAAS,SAAS;AAelB,IAAI;AAEG,SAAS,SAAc;AAC5B,MAAI,CAAC,QAAQ;AACX,aAAS,UAAU;AAAA,MACjB,QAAQ;AAAA,QACN,eAAe,EAAE,OAAO,EAAE,WAAW,OAAO,+BAA+B;AAAA,QAC3E,eAAe,EAAE,IAAI,mCAAmC;AAAA,QACxD,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,QACnC,sBAAsB,EAAE,OAAO,EAAE,SAAS;AAAA,QAC1C,8BAA8B,EAC3B,OAAO,EACP,MAAM,UAAU,EAChB,SAAS;AAAA,QACZ,uBAAuB,EAAE,OAAO,EAAE,SAAS;AAAA,QAC3C,oBAAoB,EAAE,OAAO,EAAE,SAAS;AAAA,QACxC,wBAAwB,EACrB,OAAO,EACP,MAAM,eAAe,0CAA0C,EAC/D,SAAS;AAAA,QACZ,0BAA0B,EAAE,OAAO,EAAE,SAAS;AAAA,QAC9C,8BAA8B,EAC3B,OAAO,EACP,MAAM,SAAS,iFAAiF,EAChG,SAAS;AAAA,MACd;AAAA,MACA,YAAY,QAAQ;AAAA,IACtB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAIO,SAAS,kBAAsC;AACpD,QAAM,MAAM,QAAQ,IAAI,iBAAiB,KAAK;AAC9C,SAAO,MAAM,MAAM;AACrB;AAEO,SAAS,gBAAwB;AACtC,QAAM,MAAM,OAAO;AACnB,MAAI,CAAC,IAAI,eAAe;AACtB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,IAAI;AACb;AAYO,SAAS,cAAc,MAAwB;AACpD,SAAO,OAAO,SAAS,YAAY,KAAK,SAAS,IAAI,OAAO,cAAc;AAC5E;AAaO,SAAS,kBAAiC;AAC/C,QAAM,MAAM,OAAO,QAAQ,IAAI,uBAAuB;AACtD,SAAO,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACjD;AAgBO,SAAS,0BAA4C;AAC1D,SAAO,qBAAqB,QAAQ,GAAG;AACzC;AAWO,SAAS,aAAa,OAAkD;AAC7E,SAAO,EAAE,GAAG,QAAQ,KAAK,GAAG,MAAM;AACpC;","names":[]}
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-ZWYQIEBI.js";
4
+ } from "./chunk-OB26WXHZ.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-YL3HDEIJ.js";
7
+ } from "./chunk-F6OHDJIX.js";
8
8
 
9
9
  // src/error-handler.ts
10
10
  var REQUEST_CONNECTION_PLATFORM = {
@@ -295,4 +295,4 @@ export {
295
295
  writeAdsOutput,
296
296
  resolveCustomerId
297
297
  };
298
- //# sourceMappingURL=chunk-X5C6HE24.js.map
298
+ //# sourceMappingURL=chunk-G3O4HRYR.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-K3PWXVF7.js";
4
+ } from "./chunk-YUTDQ4PV.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-YL3HDEIJ.js";
7
+ } from "./chunk-F6OHDJIX.js";
8
8
 
9
9
  // src/client.ts
10
10
  var MAX_RATE_LIMIT_RETRIES = 3;
@@ -198,4 +198,4 @@ export {
198
198
  apiGet,
199
199
  apiPost
200
200
  };
201
- //# sourceMappingURL=chunk-ZWYQIEBI.js.map
201
+ //# sourceMappingURL=chunk-OB26WXHZ.js.map