@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 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. `get` and
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 adds note",
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
- const headline = s.status === "stopped" ? `Stopped crawl ${id}` : `Nothing to stop: crawl ${id} had already ended`;
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 null for any other state. The
441
- * crawl's own `error.code` picks the next action.
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
- if (c.status !== "failed")
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
- insufficient_credits: "The account ran out of credits. Check `zenrows usage`, add credits, then start a new crawl.",
450
- seed_unreachable: "The start URL could not be fetched (the site may be blocking it). Check it with `zenrows fetch <url>`.",
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: [`zenrows crawl get ${c.crawl_id}`],
459
- server_code: code,
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`, else 0. `stopped` is a deliberate outcome, so
466
- * it exits 0 but prints as a warning. `extra` carries the results a `get` read,
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;
@@ -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.0"; // x-release-please-version
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). */
@@ -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}HTTP ${opts.status ?? 429} for ${url}`,
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.0",
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",
@@ -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` (you stopped it) or `failed` (`error.code`
81
- and `error.detail` say why). Only `failed` exits non-zero (`CRAWL_FAILED`).
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: the account is out of credits (claim the account or add credits) | after credits renew or are added |
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`; `error.code` in
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`.