@zenrows/cli 1.6.0 → 1.6.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.
- package/README.md +10 -2
- package/dist/src/cli/commands/crawl.js +51 -15
- package/dist/src/core/config.js +1 -1
- package/dist/src/core/errors.js +5 -2
- package/package.json +1 -1
- package/skills/crawl/SKILL.md +19 -5
- package/skills/trace-debug/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -187,7 +187,10 @@ to open. `--max-items` and `--max-pages` cap the crawl (API default 100 each),
|
|
|
187
187
|
and `--max-pages` bounds the cost, since each page is one fetch. The local policy `max_pages_per_run` caps `--max-pages` (default
|
|
188
188
|
1000). `--output-format html` uses up the page budget: each kept page is one
|
|
189
189
|
more fetch, and `--max-pages` counts it. `--include-patterns` /
|
|
190
|
-
`--exclude-patterns` are repeatable substring filters on the URL.
|
|
190
|
+
`--exclude-patterns` are repeatable substring filters on the URL. A crawl whose
|
|
191
|
+
pages held nothing new to follow ends `completed` with no results and exits 0.
|
|
192
|
+
When patterns were set, it prints a note (under `--json`, in `note`) to check
|
|
193
|
+
them against the URLs the start page links to. `get` and
|
|
191
194
|
`list` read one page and take `--cursor` and `--limit`. `results` follows every
|
|
192
195
|
page (`--limit` is the page size); on a running crawl it returns the URLs kept
|
|
193
196
|
so far, with `partial: true`. `download` reports the crawl's `status` (`null`
|
|
@@ -203,7 +206,12 @@ run out (default 600). When the time runs out, the command exits 0 and prints
|
|
|
203
206
|
the crawl with status `running` and the hint `zenrows crawl wait <crawl-id>`
|
|
204
207
|
(under `--json`, in `note`); the crawl keeps running. Ctrl-C stops the wait, not the crawl, and exits 130;
|
|
205
208
|
resume with `zenrows crawl wait <crawl-id>`. A crawl that ends `failed` exits 1
|
|
206
|
-
with `CRAWL_FAILED`.
|
|
209
|
+
with `CRAWL_FAILED`. A crawl that ends `stopped` because the account ran out of
|
|
210
|
+
credits or the API key hit a credit cap also exits 1, with the code a refused
|
|
211
|
+
create gets: `CRAWL_INSUFFICIENT_CREDITS` or `CRAWL_KEY_CAP_REACHED`, and the
|
|
212
|
+
API's `insufficient_credits` or `api_key_cap_reached` as `server_code`. Older
|
|
213
|
+
deployments end such a crawl `failed`; it gets the same codes. Its fetched pages
|
|
214
|
+
stay readable.
|
|
207
215
|
|
|
208
216
|
Errors carry the API's code as `server_code`, `crawl_id` on any call about one
|
|
209
217
|
crawl, and `retry_after` on a 429. When the account has reached its limit of
|
|
@@ -15,7 +15,8 @@ import { assertDomainAllowed, assertWithinLimits, loadPolicy } from "../../core/
|
|
|
15
15
|
import { newRunId, writeRun } from "../../core/artifacts.js";
|
|
16
16
|
import { contentIdOf, createCrawl, downloadCrawl, getCrawl, getCrawlContent, isTerminal, listAllResults, listCrawls, stopCrawl, waitForCrawl, } from "../../core/crawl-api.js";
|
|
17
17
|
import { asNumber, asString, parse } from "../command.js";
|
|
18
|
-
import { ToolkitError } from "../../core/errors.js";
|
|
18
|
+
import { ToolkitError, isKeyCapReached, keyCapReached, quotaExhausted } from "../../core/errors.js";
|
|
19
|
+
import { readAccount } from "../../core/agent-account.js";
|
|
19
20
|
import { printError, writeOut, writeOutStream } from "../output.js";
|
|
20
21
|
/** The API's default for `max_pages` (and `max_items`) when the flag is unset. */
|
|
21
22
|
const DEFAULT_MAX_PAGES = 100;
|
|
@@ -61,7 +62,8 @@ export const crawl = {
|
|
|
61
62
|
" a crawl still running then is printed, not stopped",
|
|
62
63
|
" --json print structured output: create/get/wait/stop print",
|
|
63
64
|
" {ok, crawl_id, status, crawl}; get adds results and next_cursor;",
|
|
64
|
-
" a wait that runs out
|
|
65
|
+
" a wait that runs out, or a completed crawl with patterns",
|
|
66
|
+
" and 0 results, adds note",
|
|
65
67
|
].join("\n"),
|
|
66
68
|
async run(argv, ctx) {
|
|
67
69
|
const [sub, ...rest] = argv;
|
|
@@ -415,7 +417,8 @@ async function stopCmd(rest, ctx) {
|
|
|
415
417
|
assertUsable("crawl");
|
|
416
418
|
const apiKey = requireApiKey();
|
|
417
419
|
const s = await stopCrawl(id, { apiKey });
|
|
418
|
-
|
|
420
|
+
// A crawl stopped on credits carries `error`: it had already ended, this call did not stop it.
|
|
421
|
+
const headline = s.status === "stopped" && !s.error ? `Stopped crawl ${id}` : `Nothing to stop: crawl ${id} had already ended`;
|
|
419
422
|
return printCrawl(s, ctx.json || values.json === true, headline);
|
|
420
423
|
}
|
|
421
424
|
/**
|
|
@@ -437,37 +440,67 @@ async function waitUntilEnd(id, opts) {
|
|
|
437
440
|
}
|
|
438
441
|
}
|
|
439
442
|
/**
|
|
440
|
-
* The error for a crawl that ended `failed`, or
|
|
441
|
-
*
|
|
443
|
+
* The error for a crawl that ended `failed`, or `stopped` with an `error` (the
|
|
444
|
+
* account ran out of credits or the API key hit a credit cap), or null for any
|
|
445
|
+
* other state. A crawl the credits ended gets the code and advice a create
|
|
446
|
+
* refused with 402 gets (`CRAWL_INSUFFICIENT_CREDITS`, `CRAWL_KEY_CAP_REACHED`),
|
|
447
|
+
* whether the API reports it `stopped` or, as older deployments do, `failed`.
|
|
448
|
+
* Any other `error.code` picks the next action of a `CRAWL_FAILED`.
|
|
442
449
|
*/
|
|
443
450
|
export function crawlFailure(c) {
|
|
444
|
-
|
|
451
|
+
const creditStop = c.status === "stopped" && !!c.error;
|
|
452
|
+
if (c.status !== "failed" && !creditStop)
|
|
445
453
|
return null;
|
|
446
454
|
const code = c.error?.code;
|
|
455
|
+
const ids = { server_code: code, crawl_id: c.crawl_id };
|
|
456
|
+
const where = `crawl ${c.crawl_id}`;
|
|
457
|
+
const detail = c.error?.detail || undefined;
|
|
458
|
+
const getCmd = `zenrows crawl get ${c.crawl_id}`;
|
|
459
|
+
if (code && isKeyCapReached(code)) {
|
|
460
|
+
const cap = keyCapReached(where, { status: null, detail }).toJSON();
|
|
461
|
+
return new ToolkitError({ ...cap, ...ids, code: "CRAWL_KEY_CAP_REACHED", suggested_commands: [...(cap.suggested_commands ?? []), getCmd] });
|
|
462
|
+
}
|
|
463
|
+
if (code === "insufficient_credits") {
|
|
464
|
+
const acct = readAccount();
|
|
465
|
+
const quota = quotaExhausted(where, acct?.unclaimed ? acct.claimUrl : undefined, { status: null, detail }).toJSON();
|
|
466
|
+
return new ToolkitError({ ...quota, ...ids, code: "CRAWL_INSUFFICIENT_CREDITS", suggested_commands: [...(quota.suggested_commands ?? []), getCmd] });
|
|
467
|
+
}
|
|
447
468
|
const why = [code, c.error?.detail].filter(Boolean).join(": ");
|
|
448
469
|
const next = {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
no_items_found: "Nothing matched. Loosen --include-patterns / --exclude-patterns, raise or drop --depth, or check the start page with `zenrows fetch <url>`.",
|
|
470
|
+
seed_unreachable: "The start URL could not be fetched or returned an empty page (the site may be blocking it). Check it with `zenrows fetch <url>`.",
|
|
471
|
+
no_items_found: "The crawl read the start page but found no data it could use, so it had nothing to follow. Start from a listing or product page.",
|
|
452
472
|
};
|
|
453
473
|
return new ToolkitError({
|
|
454
474
|
code: "CRAWL_FAILED",
|
|
455
|
-
message: `Crawl ${c.crawl_id} failed.`,
|
|
475
|
+
message: creditStop ? `Crawl ${c.crawl_id} stopped before it finished.` : `Crawl ${c.crawl_id} failed.`,
|
|
456
476
|
likely_cause: why || "The crawl ended in status failed without a reason.",
|
|
457
477
|
next_action: (code && next[code]) || "Read the error, fix the cause, then start a new crawl.",
|
|
458
|
-
suggested_commands: [
|
|
459
|
-
|
|
460
|
-
crawl_id: c.crawl_id,
|
|
478
|
+
suggested_commands: [getCmd],
|
|
479
|
+
...ids,
|
|
461
480
|
});
|
|
462
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Set only when a crawl with patterns completed and kept nothing. The API ends a
|
|
484
|
+
* crawl whose pages held nothing new to follow `completed`, and does not count
|
|
485
|
+
* the links the patterns rejected, so the patterns are the first thing to check.
|
|
486
|
+
*/
|
|
487
|
+
function noMatchNote(c) {
|
|
488
|
+
const crawl = c;
|
|
489
|
+
const patterns = [...(crawl.include_patterns ?? []), ...(crawl.exclude_patterns ?? [])];
|
|
490
|
+
if (crawl.status !== "completed" || crawl.coverage?.items_found !== 0 || !patterns.length)
|
|
491
|
+
return undefined;
|
|
492
|
+
return "The crawl completed with no results. Check --include-patterns / --exclude-patterns against the URLs the start page links to.";
|
|
493
|
+
}
|
|
463
494
|
/**
|
|
464
495
|
* Print a crawl (or a stop answer), structured under --json, and return the exit
|
|
465
|
-
* code: 1 when it ended `failed`,
|
|
466
|
-
*
|
|
496
|
+
* code: 1 when it ended `failed`, or `stopped` because credits ran out or the
|
|
497
|
+
* API key hit a credit cap, else 0. A `stopped` crawl the caller stopped is a
|
|
498
|
+
* deliberate outcome, so it exits 0 but prints as a warning. `extra` carries the results a `get` read,
|
|
467
499
|
* or the note of a wait that ran out.
|
|
468
500
|
*/
|
|
469
501
|
function printCrawl(c, json, headline, extra = {}) {
|
|
470
502
|
const err = crawlFailure(c);
|
|
503
|
+
const note = extra.note ?? noMatchNote(c);
|
|
471
504
|
const crawlFields = { ...c };
|
|
472
505
|
// A wait polls with one result to stay cheap; that page is not "the results".
|
|
473
506
|
delete crawlFields.results;
|
|
@@ -479,6 +512,7 @@ function printCrawl(c, json, headline, extra = {}) {
|
|
|
479
512
|
status: c.status,
|
|
480
513
|
crawl: crawlFields,
|
|
481
514
|
...extra,
|
|
515
|
+
note,
|
|
482
516
|
...(err ? { error: err.toJSON() } : {}),
|
|
483
517
|
}, null, 2));
|
|
484
518
|
return err ? 1 : 0;
|
|
@@ -496,6 +530,8 @@ function printCrawl(c, json, headline, extra = {}) {
|
|
|
496
530
|
}
|
|
497
531
|
if (extra.note)
|
|
498
532
|
log.dim(` next: zenrows crawl wait ${c.crawl_id}`);
|
|
533
|
+
else if (note)
|
|
534
|
+
log.warn(` ${note}`);
|
|
499
535
|
if (err)
|
|
500
536
|
printError(err, false);
|
|
501
537
|
return err ? 1 : 0;
|
package/dist/src/core/config.js
CHANGED
|
@@ -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.6.
|
|
17
|
+
export const CLI_VERSION = "1.6.2"; // x-release-please-version
|
|
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). */
|
package/dist/src/core/errors.js
CHANGED
|
@@ -45,7 +45,9 @@ export class ToolkitError extends Error {
|
|
|
45
45
|
* (so `claimUrl` is surfaced); for a real logged-in account there is nothing to
|
|
46
46
|
* claim — the step is to add credits / upgrade in the dashboard. `status`/`detail`
|
|
47
47
|
* let the caller record the actual upstream response (402 usage-limit, 429
|
|
48
|
-
* quota, …) instead of a hardcoded status.
|
|
48
|
+
* quota, …) instead of a hardcoded status. `status: null` is for a crawl the
|
|
49
|
+
* credits stopped: its status call succeeded, so there is no HTTP error to cite,
|
|
50
|
+
* only `url` (the crawl).
|
|
49
51
|
*/
|
|
50
52
|
export function quotaExhausted(url, claimUrl, opts = {}) {
|
|
51
53
|
// Say that the allowance comes back. Without it this reads as a permanent paywall,
|
|
@@ -58,10 +60,11 @@ export function quotaExhausted(url, claimUrl, opts = {}) {
|
|
|
58
60
|
? `You are on the Zenrows Free plan. Claim your account to keep your usage and add credits: ${claimUrl}. ${renewLine}`
|
|
59
61
|
: `You are out of Zenrows credits. ${renewLine} To carry on now, add a credit pack (${BILLING_TOPUP_URL} opens the purchase directly) or upgrade your plan: ${PLANS_URL}`;
|
|
60
62
|
const detail = opts.detail ? `${opts.detail.replace(/\.\s*$/, "")}. ` : "";
|
|
63
|
+
const where = opts.status === null ? `Stopped ${url}` : `HTTP ${opts.status ?? 429} for ${url}`;
|
|
61
64
|
return new ToolkitError({
|
|
62
65
|
code: "POLICY_MAX_CREDITS_EXCEEDED",
|
|
63
66
|
message: "Zenrows request quota exhausted.",
|
|
64
|
-
likely_cause: `${detail}
|
|
67
|
+
likely_cause: `${detail}${where}`,
|
|
65
68
|
next_action: claimLine,
|
|
66
69
|
suggested_commands: ["zenrows usage"],
|
|
67
70
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenrows/cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.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",
|
package/skills/crawl/SKILL.md
CHANGED
|
@@ -39,6 +39,10 @@ zenrows crawl results <id> --out urls.jsonl # same, as JSONL
|
|
|
39
39
|
- `--include-patterns` / `--exclude-patterns` are substring matches on the
|
|
40
40
|
normalized URL and repeat:
|
|
41
41
|
`--include-patterns /product/ --exclude-patterns ?add-to-cart`.
|
|
42
|
+
- A crawl whose pages held nothing new to follow (no link, only links to other
|
|
43
|
+
sites, or only links the patterns reject) ends `completed` with no results
|
|
44
|
+
and exits 0. When patterns were set, it prints a note (under `--json`, in
|
|
45
|
+
`note`) to check them against the URLs the start page links to.
|
|
42
46
|
- `create` returns at once. `--follow` (or `zenrows crawl wait <id>`) polls
|
|
43
47
|
until the crawl ends or `--timeout` seconds run out (default 600). When the
|
|
44
48
|
time runs out, the command exits 0 and prints the crawl with status
|
|
@@ -77,8 +81,18 @@ zenrows crawl stop <id> # stop it; kept URLs stay readable
|
|
|
77
81
|
```
|
|
78
82
|
|
|
79
83
|
A crawl ends `completed` (nothing left, or a cap hit: `stop_reason`
|
|
80
|
-
`max_items` / `max_pages`), `stopped`
|
|
81
|
-
|
|
84
|
+
`max_items` / `max_pages`), `stopped` or `failed` (`error.code` and
|
|
85
|
+
`error.detail` say why). `stopped` with `stop_reason` `user` means you stopped
|
|
86
|
+
it. `stopped` with no `stop_reason` and `error.code` `insufficient_credits` or
|
|
87
|
+
`api_key_cap_reached` means the account ran out of credits or the API key hit a
|
|
88
|
+
credit cap; the pages fetched before that stay readable. `failed`, and a
|
|
89
|
+
`stopped` crawl with an `error`, exit non-zero: `CRAWL_INSUFFICIENT_CREDITS` or
|
|
90
|
+
`CRAWL_KEY_CAP_REACHED` when credits ended it (the same codes a refused
|
|
91
|
+
`create` gets, also when an older deployment reports it `failed`), else
|
|
92
|
+
`CRAWL_FAILED`. `server_code` carries the API's `error.code`:
|
|
93
|
+
`seed_unreachable` when the start URL could not be fetched or returned an empty
|
|
94
|
+
page, `no_items_found` when the crawl read the start page but found no data it
|
|
95
|
+
could use.
|
|
82
96
|
`stop` on a crawl that already ended prints the status it ended with.
|
|
83
97
|
|
|
84
98
|
With `--json`, `create`, `get`, `wait` and `stop` print
|
|
@@ -112,15 +126,15 @@ The "Retry?" column is for errors that remain after these retries.
|
|
|
112
126
|
| --- | --- | --- |
|
|
113
127
|
| `CRAWL_NOT_ENABLED` | 403 `REQS008`: Crawl is not enabled for this account | no |
|
|
114
128
|
| `AUTH_INVALID` | 401: the API key was rejected | no, log in again |
|
|
115
|
-
| `CRAWL_INSUFFICIENT_CREDITS` | 402
|
|
116
|
-
| `CRAWL_KEY_CAP_REACHED` | 402 `AUTH014` or `api_key_cap_reached`: this API key reached its credit cap | after the cap resets or is raised |
|
|
129
|
+
| `CRAWL_INSUFFICIENT_CREDITS` | 402 on `create`, or a crawl that ended on `insufficient_credits`: the account is out of credits (claim the account or add credits) | after credits renew or are added |
|
|
130
|
+
| `CRAWL_KEY_CAP_REACHED` | 402 `AUTH014` or `api_key_cap_reached` on `create`, or a crawl that ended on `api_key_cap_reached`: this API key reached its credit cap | after the cap resets or is raised |
|
|
117
131
|
| `CRAWL_INVALID_REQUEST` | 400 or 422: fix the request. For `idempotency_key_reused`, send a new key or no key | no, not as is |
|
|
118
132
|
| `CRAWL_REQUEST_IN_FLIGHT` | 409: the same Idempotency-Key is still in flight | yes, once the first request ends |
|
|
119
133
|
| `CRAWL_NOT_FOUND` / `CRAWL_CONTENT_NOT_FOUND` | 404: wrong id, or the page has no content | no |
|
|
120
134
|
| `CRAWL_TOO_MANY_CRAWLS` | 429 `too_many_crawls` on `create`: the account's active-job limit | yes, after `retry_after` |
|
|
121
135
|
| `CRAWL_RATE_LIMITED` | any other 429: requests are being throttled | yes, after `retry_after` |
|
|
122
136
|
| `CRAWL_BUSY` | 503 `crawl_busy` on `stop`: the stop was not saved yet and the crawl still runs | yes, run the stop again after `retry_after` |
|
|
123
|
-
| `CRAWL_FAILED` | the crawl ended `failed`, or another error | 5xx: yes, with backoff |
|
|
137
|
+
| `CRAWL_FAILED` | the crawl ended `failed`, or `stopped` with an error other than credits, or another error | 5xx: yes, with backoff |
|
|
124
138
|
| `BACKEND_UNAVAILABLE` | the API could not be reached | yes |
|
|
125
139
|
|
|
126
140
|
Rules: start small (`--depth 1`, low caps) and check the results before a
|
|
@@ -49,12 +49,16 @@ zenrows trace export <run-id> # JSON for sharing
|
|
|
49
49
|
enabled for this account. Do not retry; ask Zenrows support for access.
|
|
50
50
|
- `CRAWL_INSUFFICIENT_CREDITS` / `CRAWL_KEY_CAP_REACHED` → Crawl's 402: the
|
|
51
51
|
account is out of credits, or this key reached its credit cap (`AUTH014`).
|
|
52
|
+
The same codes come back for a crawl that credits ended (`stopped`, or
|
|
53
|
+
`failed` on older deployments; `server_code` `insufficient_credits` /
|
|
54
|
+
`api_key_cap_reached`); its fetched pages stay readable.
|
|
52
55
|
Same advice as `POLICY_MAX_CREDITS_EXCEEDED` / `KEY_CREDIT_CAP_REACHED`.
|
|
53
56
|
- `CRAWL_TOO_MANY_CRAWLS` → 429 `too_many_crawls`: the account has reached
|
|
54
57
|
its limit of active jobs (3 by default), shared with its Batch jobs.
|
|
55
58
|
Nothing was created; retry after `retry_after` seconds (see
|
|
56
59
|
`zenrows crawl list`).
|
|
57
|
-
- `CRAWL_FAILED` → the crawl ended `failed
|
|
60
|
+
- `CRAWL_FAILED` → the crawl ended `failed`, or `stopped` with an error
|
|
61
|
+
other than credits; `error.code` in
|
|
58
62
|
`zenrows crawl get <id> --json` says why, with `error.detail`. See
|
|
59
63
|
[[crawl]].
|
|
60
64
|
- `PARAM_CONFLICT_AUTO_MANUAL` → drop the managed flags or add `--manual`.
|