@zenrows/cli 1.8.0 → 1.10.0

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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The signup challenge: before it creates an account, the signup endpoint may
3
+ * answer 428 with one short natural-language question for the agent running
4
+ * the CLI to answer itself. The CLI never tries to solve it.
5
+ *
6
+ * Each CLI run is its own process, so the pending question is kept in
7
+ * `.zenrows/signup-challenge.json` (no secret: the id, the question, when it
8
+ * expires, and the signup URL that issued it, which is where the answer goes).
9
+ */
10
+ import { existsSync, unlinkSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { ToolkitError } from "./errors.js";
13
+ import { findWorkspace, readJson, workspacePaths, writeJson } from "./workspace.js";
14
+ /** The command an agent runs to answer the pending question. */
15
+ export const ANSWER_COMMAND = 'zenrows signup --answer "<your answer>"';
16
+ export function challengePath(projectRoot) {
17
+ const ws = projectRoot ? workspacePaths(projectRoot) : (findWorkspace() ?? workspacePaths());
18
+ return join(ws.dir, "signup-challenge.json");
19
+ }
20
+ export function readChallenge(projectRoot) {
21
+ try {
22
+ const c = readJson(challengePath(projectRoot));
23
+ if (!c || typeof c.id !== "string" || typeof c.prompt !== "string" || typeof c.signupUrl !== "string")
24
+ return null;
25
+ return c;
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ export function writeChallenge(c, projectRoot) {
32
+ writeJson(challengePath(projectRoot), c);
33
+ }
34
+ export function clearChallenge(projectRoot) {
35
+ const file = challengePath(projectRoot);
36
+ if (!existsSync(file))
37
+ return;
38
+ try {
39
+ unlinkSync(file);
40
+ }
41
+ catch {
42
+ // best effort
43
+ }
44
+ }
45
+ /** True when the stored question can no longer be answered. */
46
+ export function challengeExpired(c, now = Date.now()) {
47
+ const t = Date.parse(c.expiresAt);
48
+ return Number.isFinite(t) && t <= now;
49
+ }
50
+ function parseBody(body) {
51
+ try {
52
+ const json = JSON.parse(body);
53
+ return json && typeof json === "object" ? json : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Parse a 428 `challenge_required` body into a pending challenge, or null when
61
+ * the body is not one (then the caller reports a generic failure).
62
+ */
63
+ export function parseChallenge(body, signupUrl, now = Date.now()) {
64
+ const json = parseBody(body);
65
+ if (!json || json.error !== "challenge_required")
66
+ return null;
67
+ const ch = json.challenge;
68
+ if (!ch || typeof ch.id !== "string" || !ch.id || typeof ch.prompt !== "string" || !ch.prompt.trim())
69
+ return null;
70
+ const ttl = typeof ch.expires_in === "number" && ch.expires_in > 0 ? ch.expires_in : 600;
71
+ return { id: ch.id, prompt: ch.prompt.trim(), expiresAt: new Date(now + ttl * 1000).toISOString(), signupUrl };
72
+ }
73
+ /** True when a 428 says the previous answer was wrong or came too late. */
74
+ export function wasWrongAnswer(body) {
75
+ const msg = parseBody(body)?.message;
76
+ return typeof msg === "string" && /^wrong or expired/i.test(msg.trim());
77
+ }
78
+ /** True when a 429 is the per-network lock after too many questions or wrong answers. */
79
+ export function isChallengeLocked(body) {
80
+ return parseBody(body)?.error === "challenge_locked";
81
+ }
82
+ /** The error an agent sees while a question is waiting for its answer. */
83
+ export function challengeError(c, opts = {}) {
84
+ const lead = opts.wrong ? "That answer was wrong or came too late, so here is a new question. " : "";
85
+ return new ToolkitError({
86
+ code: "SIGNUP_CHALLENGE",
87
+ message: `${lead}Zenrows needs one quick answer before it creates an account: ${c.prompt}`,
88
+ likely_cause: "No API key is configured, and new accounts created by agents answer one short question first.",
89
+ next_action: `Answer the question yourself, then run: ${ANSWER_COMMAND}. When it succeeds, re-run your original command.`,
90
+ suggested_commands: [ANSWER_COMMAND, "zenrows login --api-key <your-key>"],
91
+ challenge: { id: c.id, question: c.prompt, expires_at: c.expiresAt },
92
+ });
93
+ }
94
+ export function challengeLockedError(retryAfter) {
95
+ return new ToolkitError({
96
+ code: "SIGNUP_CHALLENGE_LOCKED",
97
+ message: "Too many signup questions or wrong answers from this network, so Zenrows paused account creation here.",
98
+ likely_cause: "Too many signup questions asked, or wrong answers given, from this network within an hour.",
99
+ next_action: "Try again in an hour by re-running your original command, which asks a new question. If you already have a Zenrows API key, use it now: zenrows login --api-key <key> (or set ZENROWS_API_KEY).",
100
+ suggested_commands: ["zenrows login --api-key <your-key>"],
101
+ ...(retryAfter !== undefined ? { retry_after: retryAfter } : {}),
102
+ });
103
+ }
104
+ export function noChallengeError(expired) {
105
+ return new ToolkitError({
106
+ code: "SIGNUP_CHALLENGE_MISSING",
107
+ message: expired ? "The signup question expired before it was answered." : "There is no signup question waiting for an answer.",
108
+ likely_cause: expired
109
+ ? "Questions are valid for a few minutes."
110
+ : "No signup was attempted from this directory, or the question was already answered.",
111
+ next_action: "Re-run your original command (for example zenrows fetch <url>) to get a new question, then answer it with: " + ANSWER_COMMAND,
112
+ suggested_commands: ["zenrows fetch <url>"],
113
+ });
114
+ }
@@ -70,6 +70,7 @@ export function ensureGitignore(paths) {
70
70
  `${WORKSPACE_DIRNAME}/secrets.json`,
71
71
  `${WORKSPACE_DIRNAME}/config.json`,
72
72
  `${WORKSPACE_DIRNAME}/account.json`,
73
+ `${WORKSPACE_DIRNAME}/signup-challenge.json`,
73
74
  `${WORKSPACE_DIRNAME}/runs/`,
74
75
  `${WORKSPACE_DIRNAME}/traces/`,
75
76
  `${WORKSPACE_DIRNAME}/logs/`,
@@ -2,7 +2,7 @@
2
2
 
3
3
  Reproducible smoke test for Extract (`extract=auto`).
4
4
 
5
- - **Target:** `https://www.owler.com/company/meltwater` (enabled domain, real company page)
5
+ - **Target:** `https://www.owler.com/company/meltwater` (prepared domain, real company page)
6
6
  - **Config:** `extract=auto`
7
7
  - **Success criteria:** HTTP 2xx and a non-empty body
8
8
  - **Cost:** ~1× basic request
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenrows/cli",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
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",
@@ -12,12 +12,12 @@
12
12
  },
13
13
  "extract": {
14
14
  "key": "extract",
15
- "label": "Extract (extract=auto / Autoparse / CSS / Markdown)",
15
+ "label": "Extract (extract=auto / CSS / Markdown)",
16
16
  "status": "available",
17
17
  "command": "zenrows extract",
18
- "backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto, autoparse, css_extractor, outputs, response_type)",
18
+ "backend": "Extract — GET https://api.zenrows.com/v1/ (extract=auto, css_extractor, outputs, response_type; autoparse deprecated)",
19
19
  "requiresAuth": true,
20
- "notes": "Default is extract=auto, which works on any prepared domain with any key; CLI falls back to autoparse on REQS007 (domain not prepared yet) or AUTH010. Autoparse / CSS / outputs / markdown work on any domain."
20
+ "notes": "Default is extract=auto, which works on any prepared domain with any key. A REQS007 (legacy AUTH010) is reported, never retried, with the next step for its reason: on a domain not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED), prepare it with `zenrows extract prepare <domain> --url <page> [--wait]` (POST /v1/extract/prepared-domains: 1 to 5 example URLs, a few minutes, no credits) and check it with `zenrows extract prepare status <domain>`; on a page Extract did not recognise on a prepared domain (EXTRACT_PAGE_NOT_RECOGNISED), try a product, article or listing page, retry it once in a few minutes, or use --css, because preparing again does not change it. CSS / outputs / markdown work on any domain; autoparse is deprecated and runs only with --autoparse."
21
21
  },
22
22
  "batch": {
23
23
  "key": "batch",
@@ -25,13 +25,13 @@
25
25
  {
26
26
  "name": "extract",
27
27
  "type": "skill",
28
- "description": "Turn protected pages into structured data with Autoparse / CSS / Markdown.",
28
+ "description": "Turn protected pages into structured data with Extract / CSS / Markdown (Autoparse is deprecated).",
29
29
  "status": "available",
30
30
  "requires_backend_capabilities": ["extract"],
31
31
  "requires_auth": true,
32
32
  "version": "0.1.0",
33
33
  "path": "skills/extract",
34
- "tags": ["extract", "autoparse", "structured-data"]
34
+ "tags": ["extract", "structured-data"]
35
35
  },
36
36
  {
37
37
  "name": "batch-jobs",
@@ -14,7 +14,7 @@
14
14
  {
15
15
  "name": "extract-pricing-pages",
16
16
  "type": "template",
17
- "description": "Extract structured pricing fields from product pages (Autoparse / CSS).",
17
+ "description": "Extract structured pricing fields from product pages (extract=auto / CSS).",
18
18
  "status": "available",
19
19
  "requires_backend_capabilities": ["extract"],
20
20
  "requires_auth": true,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: extract
3
- description: Turn protected pages into structured data with Extract / Autoparse / CSS / Markdown.
3
+ description: Turn protected pages into structured data with Extract / CSS / Markdown (Autoparse is deprecated).
4
4
  version: 0.1.0
5
5
  requires_backend_capabilities: [extract]
6
6
  ---
@@ -12,8 +12,8 @@ access + extraction**, not generic LLM parsing.
12
12
 
13
13
  ## Methods
14
14
  ```
15
- zenrows extract <url> # extract=auto (default); falls back to Autoparse if the domain is not prepared yet
16
- zenrows extract <url> --autoparse # general-purpose Autoparse (any domain)
15
+ zenrows extract <url> # extract=auto (default), any prepared domain
16
+ zenrows extract <url> --autoparse # deprecated general-purpose Autoparse (any domain)
17
17
  zenrows extract <url> --css '{"title":"h1","price":".price"}' # selector map
18
18
  zenrows extract <url> --outputs emails,links # built-in output filters → JSON
19
19
  zenrows extract <url> --output markdown # Markdown conversion
@@ -21,14 +21,58 @@ zenrows extract <url> --validate # fail if not valid JSON
21
21
  ```
22
22
 
23
23
  ## When to use which
24
- - **Extract** (`extract=auto`, default): richest fields; works on any domain Zenrows has prepared.
25
- - **Autoparse**: any website; also the automatic fallback when the domain is not prepared for Extract yet.
24
+ - **Extract** (`extract=auto`, default): richest fields; works on any prepared
25
+ domain (you can prepare one yourself, see below).
26
+ - **Autoparse** (deprecated): only with `--autoparse`. Never automatic; do not
27
+ build new work on it.
26
28
  - **CSS**: you know the exact fields/selectors and want determinism.
27
29
  - **Markdown/plaintext**: feed clean content to an LLM yourself.
28
30
 
31
+ ## REQS007: prepare the domain, then extract again
32
+
33
+ `extract=auto` works on any prepared domain, and you can prepare one yourself.
34
+ When Extract refuses a page with `403 REQS007`, `zenrows extract` says which
35
+ of three cases it is and prints the next step. It never retries with Autoparse.
36
+
37
+ | Error code | Meaning | Next step |
38
+ | --- | --- | --- |
39
+ | `EXTRACT_DOMAIN_NOT_PREPARED` | The domain is not prepared yet (also the legacy `402 AUTH010`). | Prepare it (below), then extract again. |
40
+ | `EXTRACT_PAGE_NOT_RECOGNISED` | The domain is prepared, but Extract did not recognise this page as one it can extract (a page with nothing to extract, such as a home page, or a transient failure). | Try a product, article or listing page on the same site instead. If this page should work, retry it once in a few minutes; do not retry in a loop. Otherwise use `--css`. Preparing the domain again does not change this. |
41
+ | `EXTRACT_DOMAIN_NOT_ENABLED` | Extract is not enabled for the site. | Use `--css`, or contact Zenrows support to have the site enabled. Preparing does not change this. |
42
+
43
+ Preparing a domain:
44
+
45
+ ```
46
+ zenrows extract https://example.com/p/1 # 1. fails: EXTRACT_DOMAIN_NOT_PREPARED
47
+ zenrows extract prepare example.com \
48
+ --url https://example.com/p/1 --url https://example.com/list --wait
49
+ # 2. prepare it and wait until ready (a few minutes)
50
+ zenrows extract https://example.com/p/1 # 3. extract again
51
+ ```
52
+
53
+ - `--url`: 1 to 5 real pages you want to extract, one per page kind (for
54
+ example a product page and a listing page).
55
+ - Without `--wait` the command returns at once; then check about every 30
56
+ seconds with `zenrows extract prepare status example.com` until it says
57
+ `ready`. `--wait` polls at the same pace.
58
+ - The domain is the host: `shop.example.com` and `example.com` are prepared
59
+ separately, and a leading `www.` is dropped.
60
+ - Preparing takes a few minutes and costs no credits. Each new domain uses one
61
+ slot of the account's monthly allowance; `prepare status` shows `used` /
62
+ `limit` / `remaining`.
63
+ - `EXTRACT_PREPARE_OUT_OF_ALLOWANCE`: no slots left this month; use `--css`.
64
+ `EXTRACT_PREPARE_UNAVAILABLE`: retry in a few minutes; do not loop.
65
+ A `failed` status prints the next step for its failure code and says whether
66
+ the attempt used an allowance slot.
67
+ - A domain that is already prepared answers `ready` at once
68
+ (`already_prepared`) and uses no slot: run the extract. If it then fails with
69
+ `EXTRACT_PAGE_NOT_RECOGNISED`, follow that row of the table, not `prepare`.
70
+
29
71
  ## Rules
30
72
  - Validate on a single page before scaling across many URLs.
31
- - If Autoparse misses fields, switch to `--css` with explicit selectors. Auto
73
+ - Check `empty` in `--json` output: true means every field came back null or
74
+ empty, so the result is not data.
75
+ - If a field is missing, switch to `--css` with explicit selectors. Auto
32
76
  mode already renders JS when the target needs it, so reach for
33
77
  `--manual --js-render` only after auto mode has returned an incomplete page:
34
78
  it costs several times a basic call ([[cost-control]]) and it makes the
@@ -20,6 +20,13 @@ the toolkit automatically creates a **free, unclaimed Zenrows Free plan account*
20
20
  key locally — no human step required. To opt out, pass `--no-signup` or set
21
21
  `auto_signup: false` in `.zenrows/policy.json`, then use `zenrows login --api-key <key>`.
22
22
 
23
+ Before it creates the account, Zenrows may ask you one short question (error code
24
+ `SIGNUP_CHALLENGE`, with the question in the message and in `error.challenge.question`).
25
+ Answer it yourself, do not ask the human, then run `zenrows signup --answer "<your answer>"`
26
+ and re-run your original command. A wrong answer returns a new question the same way.
27
+ After too many questions or wrong answers from one network (`SIGNUP_CHALLENGE_LOCKED`), wait an hour or use
28
+ `zenrows login --api-key <key>`.
29
+
23
30
  When the Free plan nears its limit (or the user asks), tell the human they can **claim** the
24
31
  account to keep their usage and history: run `zenrows account claim` (or open the claim URL
25
32
  shown on quota errors). It prints a valid link, requesting a fresh one when the stored link is
@@ -34,7 +41,7 @@ If the user has a known URL and wants page content:
34
41
  → Use Protected Fetch. (zenrows fetch <url>)
35
42
 
36
43
  If the user has a known URL and wants structured data:
37
- → Use Extract. (zenrows extract <url> | --autoparse | --css)
44
+ → Use Extract. (zenrows extract <url> | --css)
38
45
 
39
46
  If the user has a start page (listing, index, section) but not the item URLs:
40
47
  → Use Crawl. (zenrows crawl create <url> --depth 1 --follow,
@@ -75,13 +82,16 @@ Run `zenrows status` for the live capability matrix. As of this toolkit:
75
82
  | Primitive | Command | Status |
76
83
  | --- | --- | --- |
77
84
  | Protected Fetch | `zenrows fetch` | **available**: `GET https://api.zenrows.com/v1/` |
78
- | Extract (`extract=auto`, Autoparse, CSS, Markdown) | `zenrows extract` | **available**: `GET https://api.zenrows.com/v1/?extract=auto` |
85
+ | Extract (`extract=auto`, CSS, Markdown; Autoparse deprecated) | `zenrows extract` | **available**: `GET https://api.zenrows.com/v1/?extract=auto` |
79
86
  | Batch | `zenrows batch` | **available**: `POST https://async.api.zenrows.com/v1/jobs` |
80
87
  | Crawl (new) | `zenrows crawl` | **available**: `POST https://api.zenrows.com/v1/crawls` |
81
88
  | Browser | `zenrows browser` | **available**: `POST https://mcp.zenrows.com/browser/sessions` |
82
89
  | MCP | `zenrows mcp` | **available**: `https://mcp.zenrows.com/mcp` |
83
90
 
84
- - Extract: `extract=auto` falls back to Autoparse on a domain not prepared yet.
91
+ - Extract: `extract=auto` works on any prepared domain. On one not prepared yet
92
+ (`EXTRACT_DOMAIN_NOT_PREPARED`), run `zenrows extract prepare <domain> --url
93
+ <page> --wait`, then extract again. `EXTRACT_PAGE_NOT_RECOGNISED` means the
94
+ domain is prepared but not this page: do not prepare again; see [[extract]].
85
95
  - Batch: the reliable path at scale, at Fetch's prices (create, list, status, wait, results, content, pause, resume, rerun, delete). Estimate specs locally with no key.
86
96
  - Crawl: collects the URLs behind one start page, optionally with each page's HTML.
87
97
  - Browser: escalation-only, for pages Fetch can't handle; same backend as the MCP `browser_*` tools.
@@ -3,13 +3,11 @@
3
3
  Extract structured pricing fields from product pages with Zenrows Extract.
4
4
 
5
5
  ```bash
6
- # Default: extract=auto on an enabled domain (site-tailored JSON)
6
+ # Default: extract=auto on a prepared domain (site-tailored JSON).
7
+ # Not prepared yet (EXTRACT_DOMAIN_NOT_PREPARED)? zenrows extract prepare <domain> --url <product-url> --wait
7
8
  zenrows extract https://www.owler.com/company/meltwater
8
9
 
9
- # Autoparse — general-purpose JSON on any domain
10
- zenrows extract https://www.scrapingcourse.com/ecommerce/ --autoparse
11
-
12
- # Or a precise CSS selector map
10
+ # A precise CSS selector map, on any domain
13
11
  zenrows extract <product-url> --css '{"title":"h1","price":".price"}' --validate
14
12
  ```
15
13
 
@@ -9,7 +9,7 @@ status: available
9
9
 
10
10
  A higher-level process built on Zenrows **Fetch and Extract**: retrieve
11
11
  protected pages with `zenrows fetch`, then turn them into structured data with
12
- `zenrows extract` (the same API's Autoparse / CSS / Markdown — not a separate
12
+ `zenrows extract` (the same API's `extract=auto` / CSS / Markdown — not a separate
13
13
  product). Scheduling and scaling use the tools you already have (cron/CI, a
14
14
  simple per-URL loop).
15
15
 
@@ -20,7 +20,7 @@ simple per-URL loop).
20
20
  ```
21
21
  zenrows fetch https://competitor.example/products
22
22
  ```
23
- 2. **Validate extraction** on one page (extract=auto falls back to autoparse on a domain not prepared yet).
23
+ 2. **Validate extraction** on one page (on a domain not prepared yet, `EXTRACT_DOMAIN_NOT_PREPARED`, run `zenrows extract prepare <domain> --url <page> --wait` first).
24
24
  ```
25
25
  zenrows extract https://competitor.example/p/123 --validate
26
26
  ```