@zenrows/cli 1.6.0 → 1.6.1

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
@@ -203,7 +203,12 @@ run out (default 600). When the time runs out, the command exits 0 and prints
203
203
  the crawl with status `running` and the hint `zenrows crawl wait <crawl-id>`
204
204
  (under `--json`, in `note`); the crawl keeps running. Ctrl-C stops the wait, not the crawl, and exits 130;
205
205
  resume with `zenrows crawl wait <crawl-id>`. A crawl that ends `failed` exits 1
206
- with `CRAWL_FAILED`.
206
+ with `CRAWL_FAILED`. A crawl that ends `stopped` because the account ran out of
207
+ credits or the API key hit a credit cap also exits 1, with the code a refused
208
+ create gets: `CRAWL_INSUFFICIENT_CREDITS` or `CRAWL_KEY_CAP_REACHED`, and the
209
+ API's `insufficient_credits` or `api_key_cap_reached` as `server_code`. Older
210
+ deployments end such a crawl `failed`; it gets the same codes. Its fetched pages
211
+ stay readable.
207
212
 
208
213
  Errors carry the API's code as `server_code`, `crawl_id` on any call about one
209
214
  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;
@@ -415,7 +416,8 @@ async function stopCmd(rest, ctx) {
415
416
  assertUsable("crawl");
416
417
  const apiKey = requireApiKey();
417
418
  const s = await stopCrawl(id, { apiKey });
418
- const headline = s.status === "stopped" ? `Stopped crawl ${id}` : `Nothing to stop: crawl ${id} had already ended`;
419
+ // A crawl stopped on credits carries `error`: it had already ended, this call did not stop it.
420
+ const headline = s.status === "stopped" && !s.error ? `Stopped crawl ${id}` : `Nothing to stop: crawl ${id} had already ended`;
419
421
  return printCrawl(s, ctx.json || values.json === true, headline);
420
422
  }
421
423
  /**
@@ -437,33 +439,50 @@ async function waitUntilEnd(id, opts) {
437
439
  }
438
440
  }
439
441
  /**
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.
442
+ * The error for a crawl that ended `failed`, or `stopped` with an `error` (the
443
+ * account ran out of credits or the API key hit a credit cap), or null for any
444
+ * other state. A crawl the credits ended gets the code and advice a create
445
+ * refused with 402 gets (`CRAWL_INSUFFICIENT_CREDITS`, `CRAWL_KEY_CAP_REACHED`),
446
+ * whether the API reports it `stopped` or, as older deployments do, `failed`.
447
+ * Any other `error.code` picks the next action of a `CRAWL_FAILED`.
442
448
  */
443
449
  export function crawlFailure(c) {
444
- if (c.status !== "failed")
450
+ const creditStop = c.status === "stopped" && !!c.error;
451
+ if (c.status !== "failed" && !creditStop)
445
452
  return null;
446
453
  const code = c.error?.code;
454
+ const ids = { server_code: code, crawl_id: c.crawl_id };
455
+ const where = `crawl ${c.crawl_id}`;
456
+ const detail = c.error?.detail || undefined;
457
+ const getCmd = `zenrows crawl get ${c.crawl_id}`;
458
+ if (code && isKeyCapReached(code)) {
459
+ const cap = keyCapReached(where, { status: null, detail }).toJSON();
460
+ return new ToolkitError({ ...cap, ...ids, code: "CRAWL_KEY_CAP_REACHED", suggested_commands: [...(cap.suggested_commands ?? []), getCmd] });
461
+ }
462
+ if (code === "insufficient_credits") {
463
+ const acct = readAccount();
464
+ const quota = quotaExhausted(where, acct?.unclaimed ? acct.claimUrl : undefined, { status: null, detail }).toJSON();
465
+ return new ToolkitError({ ...quota, ...ids, code: "CRAWL_INSUFFICIENT_CREDITS", suggested_commands: [...(quota.suggested_commands ?? []), getCmd] });
466
+ }
447
467
  const why = [code, c.error?.detail].filter(Boolean).join(": ");
448
468
  const next = {
449
- insufficient_credits: "The account ran out of credits. Check `zenrows usage`, add credits, then start a new crawl.",
450
469
  seed_unreachable: "The start URL could not be fetched (the site may be blocking it). Check it with `zenrows fetch <url>`.",
451
470
  no_items_found: "Nothing matched. Loosen --include-patterns / --exclude-patterns, raise or drop --depth, or check the start page with `zenrows fetch <url>`.",
452
471
  };
453
472
  return new ToolkitError({
454
473
  code: "CRAWL_FAILED",
455
- message: `Crawl ${c.crawl_id} failed.`,
474
+ message: creditStop ? `Crawl ${c.crawl_id} stopped before it finished.` : `Crawl ${c.crawl_id} failed.`,
456
475
  likely_cause: why || "The crawl ended in status failed without a reason.",
457
476
  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,
477
+ suggested_commands: [getCmd],
478
+ ...ids,
461
479
  });
462
480
  }
463
481
  /**
464
482
  * 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,
483
+ * code: 1 when it ended `failed`, or `stopped` because credits ran out or the
484
+ * API key hit a credit cap, else 0. A `stopped` crawl the caller stopped is a
485
+ * deliberate outcome, so it exits 0 but prints as a warning. `extra` carries the results a `get` read,
467
486
  * or the note of a wait that ran out.
468
487
  */
469
488
  function printCrawl(c, json, headline, extra = {}) {
@@ -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.1"; // 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.1",
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",
@@ -77,8 +77,15 @@ zenrows crawl stop <id> # stop it; kept URLs stay readable
77
77
  ```
78
78
 
79
79
  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`).
80
+ `max_items` / `max_pages`), `stopped` or `failed` (`error.code` and
81
+ `error.detail` say why). `stopped` with `stop_reason` `user` means you stopped
82
+ it. `stopped` with no `stop_reason` and `error.code` `insufficient_credits` or
83
+ `api_key_cap_reached` means the account ran out of credits or the API key hit a
84
+ credit cap; the pages fetched before that stay readable. `failed`, and a
85
+ `stopped` crawl with an `error`, exit non-zero: `CRAWL_INSUFFICIENT_CREDITS` or
86
+ `CRAWL_KEY_CAP_REACHED` when credits ended it (the same codes a refused
87
+ `create` gets, also when an older deployment reports it `failed`), else
88
+ `CRAWL_FAILED`. `server_code` carries the API's `error.code`.
82
89
  `stop` on a crawl that already ended prints the status it ended with.
83
90
 
84
91
  With `--json`, `create`, `get`, `wait` and `stop` print
@@ -112,15 +119,15 @@ The "Retry?" column is for errors that remain after these retries.
112
119
  | --- | --- | --- |
113
120
  | `CRAWL_NOT_ENABLED` | 403 `REQS008`: Crawl is not enabled for this account | no |
114
121
  | `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 |
122
+ | `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 |
123
+ | `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
124
  | `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
125
  | `CRAWL_REQUEST_IN_FLIGHT` | 409: the same Idempotency-Key is still in flight | yes, once the first request ends |
119
126
  | `CRAWL_NOT_FOUND` / `CRAWL_CONTENT_NOT_FOUND` | 404: wrong id, or the page has no content | no |
120
127
  | `CRAWL_TOO_MANY_CRAWLS` | 429 `too_many_crawls` on `create`: the account's active-job limit | yes, after `retry_after` |
121
128
  | `CRAWL_RATE_LIMITED` | any other 429: requests are being throttled | yes, after `retry_after` |
122
129
  | `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 |
130
+ | `CRAWL_FAILED` | the crawl ended `failed`, or `stopped` with an error other than credits, or another error | 5xx: yes, with backoff |
124
131
  | `BACKEND_UNAVAILABLE` | the API could not be reached | yes |
125
132
 
126
133
  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`.