@zenrows/cli 1.3.2 → 1.4.2

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.
@@ -17,12 +17,18 @@ Use **`extract`** when you need structured JSON fields rather than a full page:
17
17
  extract({ url }) # mode=auto (default)
18
18
  extract({ url, mode: "autoparse" }) # general Autoparse (any domain)
19
19
  extract({ url, mode: "css", css_extractor: '{"title":"h1","price":".price"}' })
20
- extract({ url, js_render: true, premium_proxy: true }) # stealth flags when needed
20
+ extract({ url, proxy_country: "US" }) # geo-targeted
21
21
  ```
22
22
 
23
23
  `mode=auto` (`extract=auto`) is **open beta**: richest on enabled domains,
24
- currently free; billing may apply later. If the domain is not enabled (AUTH010),
25
- the tool retries once with Autoparse by default (`fallback_autoparse`).
24
+ currently free; billing may apply later. If the domain is not enabled (AUTH010)
25
+ or not prepared yet (REQS007), the tool retries once with Autoparse by default
26
+ (`fallback_autoparse`).
27
+
28
+ Every mode uses Adaptive Stealth Mode by default: pass just the URL, even for
29
+ protected or dynamic sites. Set `js_render` or `premium_proxy` only to force a
30
+ fixed configuration; that turns Adaptive Stealth Mode off. Check `empty` in the
31
+ result: true means no field came back with a value.
26
32
 
27
33
  ## Fallback: `scrape` extraction params
28
34
 
@@ -40,7 +46,7 @@ scrape({ url, outputs: "emails,links" }) # or "*" for all built-ins
40
46
  ## When to use which
41
47
 
42
48
  - **`mode=auto`**: site-tailored fields on beta-enabled domains.
43
- - **`mode=autoparse`**: any website; also the automatic AUTH010 fallback.
49
+ - **`mode=autoparse`**: any website; also the automatic AUTH010 / REQS007 fallback.
44
50
  - **`mode=css`**: known fields/selectors; deterministic.
45
51
  - **Markdown via `scrape`**: feed clean content to an LLM yourself.
46
52
 
@@ -69,6 +69,25 @@ export function estimateCredits(jobs) {
69
69
  });
70
70
  return { credits: perJob.reduce((a, b) => a + b, 0), perJob };
71
71
  }
72
+ /**
73
+ * Adaptive Stealth Mode (mode=auto) by default, like `fetch` and `extract`: returns
74
+ * the jobs with `mode: "auto"` on every line that doesn't force `js_render` /
75
+ * `premium_proxy` or set `mode` itself, and none at all when the job-level params
76
+ * force either flag. Applied per task rather than job-level because the API rejects
77
+ * a task that combines a job-level mode=auto with its own js_render or
78
+ * premium_proxy (REQS004).
79
+ */
80
+ export function withAdaptiveStealth(jobs, jobParams = {}) {
81
+ const forced = (p) => isTruthyFlag(p.js_render) || isTruthyFlag(p.premium_proxy);
82
+ if (forced(jobParams) || jobParams.mode != null)
83
+ return jobs;
84
+ return jobs.map((job) => {
85
+ const own = { ...(job.zenrows_params ?? {}), ...job };
86
+ if (forced(own) || own.mode != null)
87
+ return job;
88
+ return { ...job, mode: "auto" };
89
+ });
90
+ }
72
91
  /**
73
92
  * Map a validated JSONL spec + job-level flags to the Batch API request body.
74
93
  * The flat per-line options (`js_render`, `premium_proxy`, `proxy_country`,
@@ -82,7 +101,8 @@ export function estimateCredits(jobs) {
82
101
  * `protected-fetch.ts::validateAutoManual`.
83
102
  */
84
103
  export function toJobBody(jobs, jobParams = {}) {
85
- assertProxyCountryPremium(jobParams, "job-level params");
104
+ // No separate job-level check: a job-level proxy_country is valid for each task
105
+ // in Adaptive Stealth Mode, and the per-task check below sees job ⊕ task params.
86
106
  const tasks = jobs.map((job, idx) => {
87
107
  const { url, external_id, metadata, zenrows_params } = job;
88
108
  const flat = {};
@@ -9,7 +9,8 @@
9
9
  import { statSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { log, ANSI, c } from "../../core/logger.js";
12
- import { estimateCredits, toJobBody, validateJsonl } from "../../adapters/batch.js";
12
+ import { estimateCredits, toJobBody, validateJsonl, withAdaptiveStealth } from "../../adapters/batch.js";
13
+ import { loadConfig } from "../../core/config.js";
13
14
  import { requireApiKey } from "../../core/auth.js";
14
15
  import { ensureApiKey } from "../../core/ensure-key.js";
15
16
  import { assertUsable } from "../../core/capabilities.js";
@@ -25,12 +26,15 @@ export const batch = {
25
26
  usage: "zenrows batch <estimate|create <file.jsonl>|status <id>|results <id>|cancel <id>|wait <id>|retry-failed <id>>",
26
27
  help: [
27
28
  "Local (no key):",
28
- " estimate <file.jsonl> validate the spec + estimate credits",
29
+ " estimate <file.jsonl> [--manual] validate the spec + estimate credits (upper bound)",
29
30
  "Cloud (needs a key + Batch beta access):",
30
31
  " create <file.jsonl> [flags] validate, then submit the job",
31
- " --js-render job-level: render JavaScript",
32
- " --premium-proxy job-level: use residential IPs",
33
- " --proxy-country <cc> job-level: geo-target (needs --premium-proxy)",
32
+ " (default) Adaptive Stealth Mode (mode=auto) on every task that",
33
+ " doesn't force js_render / premium_proxy itself",
34
+ " --manual no Adaptive Stealth Mode; plain requests unless flags below",
35
+ " --js-render job-level: force JavaScript rendering (turns auto off)",
36
+ " --premium-proxy job-level: force residential IPs (turns auto off)",
37
+ " --proxy-country <cc> job-level: geo-target (auto mode, or with --premium-proxy)",
34
38
  " --output <fmt> job-level response_type (markdown|plaintext|pdf|html)",
35
39
  " --follow poll until the run finishes (alias: --wait)",
36
40
  " --no-signup do not auto-create a Free plan account if no key exists",
@@ -76,20 +80,24 @@ export const batch = {
76
80
  }
77
81
  },
78
82
  };
83
+ /** Jobs as they will be sent: Adaptive Stealth Mode applied unless manual or disabled in config. */
84
+ function effectiveJobs(jobs, jobParams, manual) {
85
+ return manual || loadConfig().defaultMode !== "auto" ? jobs : withAdaptiveStealth(jobs, jobParams);
86
+ }
79
87
  function estimateCmd(rest, ctx) {
80
- const { positionals } = parse(rest, {});
88
+ const { values, positionals } = parse(rest, { manual: { type: "boolean" } });
81
89
  const file = positionals[0];
82
90
  if (!file)
83
91
  throw needFile();
84
92
  const v = validateJsonl(file);
85
- const est = estimateCredits(v.jobs);
93
+ const est = estimateCredits(effectiveJobs(v.jobs, {}, values.manual === true));
86
94
  if (ctx.json) {
87
95
  log.out(JSON.stringify({ ok: v.errors.length === 0, ...v, estimatedCredits: est.credits }, null, 2));
88
96
  }
89
97
  else {
90
98
  log.info(c(ANSI.bold, `Job spec: ${file}`));
91
99
  log.info(`valid jobs: ${v.validJobs}/${v.totalLines}`);
92
- log.info(`estimated credits (1x basic / 5x js / 10x proxy / 25x both): ${est.credits}`);
100
+ log.info(`estimated credits, upper bound (1x basic / 5x js / 10x proxy / 25x both or auto): ${est.credits}`);
93
101
  if (v.errors.length) {
94
102
  log.warn(`${v.errors.length} invalid line(s):`);
95
103
  v.errors.slice(0, 10).forEach((e) => log.dim(` line ${e.line}: ${e.reason}`));
@@ -105,6 +113,7 @@ async function createCmd(rest, ctx) {
105
113
  "js-render": { type: "boolean" },
106
114
  "premium-proxy": { type: "boolean" },
107
115
  "proxy-country": { type: "string" },
116
+ manual: { type: "boolean" },
108
117
  output: { type: "string" },
109
118
  follow: { type: "boolean" },
110
119
  wait: { type: "boolean" }, // back-compat alias for --follow
@@ -136,11 +145,6 @@ async function createCmd(rest, ctx) {
136
145
  // 1. every task URL must pass the allow/deny domain policy,
137
146
  // 2. the run must fit the batch-only page/credit caps (a batch is the one
138
147
  // primitive that fans out into many requests, so the caps bind here).
139
- const policy = loadPolicy();
140
- for (const job of v.jobs)
141
- assertDomainAllowed(job.url, policy);
142
- const est = estimateCredits(v.jobs);
143
- assertWithinLimits({ pages: v.validJobs, credits: est.credits }, policy, "batch");
144
148
  const jobParams = {};
145
149
  if (values["js-render"] === true)
146
150
  jobParams.js_render = true;
@@ -152,8 +156,14 @@ async function createCmd(rest, ctx) {
152
156
  const responseType = normalizeOutput(asString(values.output));
153
157
  if (responseType)
154
158
  jobParams.response_type = responseType;
159
+ const jobs = effectiveJobs(v.jobs, jobParams, values.manual === true);
160
+ const policy = loadPolicy();
161
+ for (const job of jobs)
162
+ assertDomainAllowed(job.url, policy);
163
+ const est = estimateCredits(jobs);
164
+ assertWithinLimits({ pages: v.validJobs, credits: est.credits }, policy, "batch");
155
165
  // toJobBody validates proxy_country/premium_proxy BEFORE any HTTP call.
156
- const body = toJobBody(v.jobs, jobParams);
166
+ const body = toJobBody(jobs, jobParams);
157
167
  const apiKey = await ensureApiKey(values["no-signup"] ? { ...policy, auto_signup: false } : policy, {
158
168
  onProvision: (a) => {
159
169
  log.info("No API key found — created a Zenrows Free plan account for you.");
@@ -2,7 +2,7 @@
2
2
  * `zenrows extract <url> [flags]` → Extract (structured extraction on `/v1/`).
3
3
  *
4
4
  * Default: `extract=auto` (site-tailored Extract, open beta). If the domain is
5
- * not enabled (AUTH010), falls back once to Autoparse. Opt into Autoparse
5
+ * not enabled or prepared (AUTH010 / REQS007), falls back once to Autoparse. Opt into Autoparse
6
6
  * directly with `--autoparse`. Other methods: `--css`, `--outputs`, `--output`.
7
7
  */
8
8
  import { ensureApiKey } from "../../core/ensure-key.js";
@@ -23,7 +23,7 @@ export const extract = {
23
23
  usage: "zenrows extract <url> [--autoparse | --css <json> | --outputs <filters> | --output md|text] [flags]",
24
24
  help: [
25
25
  "Methods:",
26
- " (default) extract=auto — site-tailored Extract (open beta); falls back to Autoparse if the domain is not enabled",
26
+ " (default) extract=auto — site-tailored Extract (open beta); falls back to Autoparse if the domain is not enabled or prepared",
27
27
  " --autoparse general-purpose Autoparse (any domain, no Extract fallback)",
28
28
  " --css <json> CSS selector map, e.g. '{\"title\":\"h1\",\"price\":\".price\"}'",
29
29
  " --outputs <list> built-in output filters → JSON: emails, phone_numbers, headings,",
@@ -119,7 +119,7 @@ export const extract = {
119
119
  const safeParams = { ...outcome.params };
120
120
  delete safeParams.apikey;
121
121
  if (outcome.fellBackToAutoparse) {
122
- log.info("Extract not enabled for this domain — fell back to Autoparse.");
122
+ log.info("Extract not available for this domain yet — fell back to Autoparse.");
123
123
  }
124
124
  const runDir = writeRun({
125
125
  runId,
@@ -26,8 +26,8 @@ export const fetch_ = {
26
26
  " --js-render render JavaScript (manual mode only)",
27
27
  " --premium-proxy use residential IPs (manual mode only)",
28
28
  " --proxy-country <cc> geo-target (auto mode; in manual mode needs --premium-proxy)",
29
- " --wait <ms> fixed wait after load",
30
- " --wait-for <selector> wait for a CSS selector",
29
+ " --wait <ms> fixed wait after load (needs --manual --js-render; auto mode may ignore it)",
30
+ " --wait-for <selector> wait for a CSS selector (needs --manual --js-render; auto mode may ignore it)",
31
31
  " --js-instructions <j> JSON instructions to run on the page",
32
32
  " --session-id <n> keep the same IP across requests",
33
33
  " --original-status return the target's original HTTP status",
@@ -14,7 +14,7 @@ export const CONFIG_VERSION = "0.1.0";
14
14
  * entrypoint) so core modules — telemetry, signup provenance — can read it
15
15
  * without an import cycle. `VERSION` in `cli/index.ts` re-exports this.
16
16
  */
17
- export const CLI_VERSION = "1.3.2";
17
+ export const CLI_VERSION = "1.4.2";
18
18
  /** The User-Agent on every request to a Zenrows API. The gateway reads the version from it. */
19
19
  export const CLI_USER_AGENT = `zenrows-cli/${CLI_VERSION}`;
20
20
  /** Env var to override the Fetch and Extract API base (local/staging testing). */
@@ -98,6 +98,18 @@ export async function scrape(apiBase, apiKey, params, opts = {}) {
98
98
  concurrencyRemaining: numOrNull(res.headers.get("concurrency-remaining")),
99
99
  redactedUrl: redacted,
100
100
  };
101
+ // REQS007 on extract=auto: the domain hasn't been prepared for Extract yet.
102
+ // Same recovery as AUTH010 (the extract adapter falls back to autoparse), so it
103
+ // maps to the same code. Checked before the 403 auth branch below.
104
+ if (res.status === 403 && params.extract !== undefined && zrErrorCode(body) === "REQS007") {
105
+ throw new ToolkitError({
106
+ code: "EXTRACT_DOMAIN_NOT_ENABLED",
107
+ message: "Extract has not been prepared for this domain yet.",
108
+ likely_cause: zrErrorDetail(body) ?? "This domain is not prepared for Extract.",
109
+ next_action: "Retry with --autoparse for general-purpose extraction on any site.",
110
+ suggested_commands: [`zenrows extract ${params.url} --autoparse`],
111
+ });
112
+ }
101
113
  if (res.status === 401 || res.status === 403) {
102
114
  // Distinguish Zenrows auth errors from target-site 403s. Zenrows auth
103
115
  // failures surface AUTH00x codes in the body.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenrows/cli",
3
- "version": "1.3.2",
3
+ "version": "1.4.2",
4
4
  "description": "Open-source CLI, MCP, skills, templates, workflows, recipes, and evals layer for giving AI agents reliable access to protected web data through Zenrows cloud infrastructure.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,14 +35,15 @@ structured data instead of raw HTML.
35
35
  {"url": "https://example.com/products", "extract": "auto", "external_id": "p1"}
36
36
  ```
37
37
 
38
- An Extract task costs the same as a regular one (1 credit at base tier), so
39
- `batch estimate` prices it correctly. The result carries two keys: `html` (the
38
+ An Extract task costs the same as a regular one, so `batch estimate` prices it
39
+ correctly. The result carries two keys: `html` (the
40
40
  raw page) and `parsed` (the structured data). Validate on a single URL with
41
41
  `zenrows extract <url>` before running the full batch.
42
42
 
43
43
  ## Cloud (needs a key + beta access)
44
44
  ```
45
- zenrows batch create jobs.jsonl [--js-render] [--premium-proxy] [--proxy-country cc] [--wait]
45
+ zenrows batch create jobs.jsonl [--proxy-country cc] [--wait]
46
+ zenrows batch create jobs.jsonl --manual [--js-render] [--premium-proxy]
46
47
  zenrows batch status <id> # run status + stats
47
48
  zenrows batch results <id> [--status successful|failed|all] [--out results.jsonl]
48
49
  zenrows batch wait <id> [--timeout ms]
@@ -50,9 +51,17 @@ zenrows batch cancel <id> # stop an in-flight run
50
51
  zenrows batch retry-failed <id> # rerun only the failed tasks (new run)
51
52
  ```
52
53
 
53
- `create` validates the spec first, then submits. `--proxy-country` needs
54
- `--premium-proxy` (or `mode=auto`) and is rejected before any request. Job-level
55
- flags apply to every task; per-task keys in the JSONL override them.
54
+ Every task runs in Adaptive Stealth Mode (`mode=auto`) by default, like `fetch`
55
+ and `extract`: Zenrows enables JS rendering and premium proxies only on the pages
56
+ that need them, and bills only the configuration that succeeds. A task that sets
57
+ `js_render`, `premium_proxy` or `mode` itself keeps its own settings, and
58
+ `--js-render` / `--premium-proxy` force that configuration on every task instead.
59
+ `--manual` turns Adaptive Stealth Mode off. `batch estimate` counts auto tasks at
60
+ the 25x upper bound, so it's the most the run can cost, not the expected cost.
61
+
62
+ `create` validates the spec first, then submits. Outside Adaptive Stealth Mode,
63
+ `--proxy-country` needs `--premium-proxy` and is rejected before any request.
64
+ Job-level flags apply to every task; per-task keys in the JSONL override them.
56
65
 
57
66
  A run ends in one of `completed`, `failed`, `stopped`, or `deleted`. `status`,
58
67
  `wait`, and `create --wait` exit non-zero (`ok: false` under `--json`) only for
@@ -20,9 +20,10 @@ zenrows trace export <run-id> # JSON for sharing
20
20
 
21
21
  ## Failure → action map
22
22
  - `FETCH_FAILED` / empty content → first retry in auto mode (`mode=auto`), which
23
- escalates for you and bills only for what succeeds. For slow pages add
24
- `--wait-for <selector>`. Only when auto mode has failed on its own, take
25
- manual control with `--manual --js-render`, then `--premium-proxy`. Each step
23
+ escalates for you and bills only for what succeeds. Only when auto mode has
24
+ failed on its own, take manual control with `--manual --js-render`, then
25
+ `--premium-proxy`. For slow pages, add `--wait-for <selector>` to a
26
+ `--manual --js-render` run; auto mode may ignore it. Each step
26
27
  multiplies the cost of the request, and both together are the most expensive
27
28
  configuration available ([[cost-control]]).
28
29
  - `REQUEST_TIMEOUT` → the CLI stopped waiting; the API was reached. Raise
@@ -29,7 +29,9 @@ zenrows batch retry-failed <job-id> # rerun only the failed
29
29
  zenrows batch cancel <job-id> # stop an in-flight run
30
30
  ```
31
31
 
32
- Job-level scrape options come from flags (`--js-render`, `--premium-proxy`,
33
- `--proxy-country`, `--output`); per-line keys override them for that URL.
32
+ Tasks use Adaptive Stealth Mode (`mode=auto`) by default. Job-level scrape
33
+ options come from flags (`--js-render`, `--premium-proxy`, `--proxy-country`,
34
+ `--output`, or `--manual` to turn Adaptive Stealth Mode off); per-line keys
35
+ override them for that URL.
34
36
  Without beta access yet, validate/estimate locally or fan out with
35
37
  `zenrows fetch` per URL on a small sample first.