Repository navigation
feat(crawl): add zenrows crawl for the Crawl API - #30
StefanoGuerrini wants to merge 3 commits into
Conversation
Crawl takes one start URL, follows the links on each page up to `depth` on the start URL's domain, and returns the URLs it keeps, optionally with each page's HTML. The new `crawl` command drives https://api.zenrows.com/v1/crawls with the X-API-Key header: - start <url> --depth N [--max-items N] [--max-pages N] [--include P]... [--exclude P]... [--html] [--follow|--wait] [--timeout ms] [--idempotency-key K] - status, wait, results (all pages, one page via --cursor/--limit, or the NDJSON export via --download), content, list, stop src/core/crawl-api.ts is the client (base override ZENROWS_CRAWL_API_BASE, otherwise the configured apiBase). Error responses map to new codes: CRAWL_NOT_ENABLED (403 REQS008), CRAWL_NOT_FOUND (404), CRAWL_QUOTA_EXCEEDED (429 too_many_crawls), CRAWL_INVALID (400/409/422), CRAWL_FAILED (a failed crawl or another error) and CRAWL_TIMEOUT (wait gave up; the crawl keeps running). The guards that asserted `crawl` is an unknown command and that no crawl skill exists are narrowed to `discover` and `monitor`. Adds the `crawl` capability, a crawl skill, README and docs sections, unit tests, and an opt-in e2e test (`npm run test:e2e`) that drives the built CLI against a live API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
StefanoGuerrini
left a comment
There was a problem hiding this comment.
Crawl Beta public-surface review
This review checks the public CLI surface against what production serves to a crawl-beta customer today: Crawl 475016a (docs/openapi/api.yaml) and gateway e382348. crawl-beta is at 100%. crawl-beta-extract (output_format: json, discovery: pagination) is staff-only. The public surface is the command, subcommands, flags, --json shapes, error codes, exit codes, env vars, the registry entries and the skill text. Reviewed at 7be627f: npm run typecheck is clean, and npm test passes 320 of 320 tests. The e2e test needs a live key and did not run.
Verdict: changes needed. Items 1 to 6 become breaking changes if they ship as they are.
Fix before merge: breaking if shipped
- No Beta marking. The capability and the skill have status
available(registry/capabilities.json:34,registry/skills.json:51), and the tests pin it (tests/capabilities.test.ts:14). Batch usesbeta. The README table (README.md:42) and the master skill table (skills/zenrows/SKILL.md:77) also say "available". Set both statuses tobeta, and say "Beta" incrawl --help, the README and the crawl skill (proposed decision 1). Agents read this status, so a later downgrade fromavailabletobetais a contract change. - 429 maps to
CRAWL_QUOTA_EXCEEDED(src/core/crawl-api.ts:271). The name says quota, buttoo_many_crawlsis the account's active-job limit.Retry-Aftergoes only intolikely_causetext (:269). Rename toCRAWL_TOO_MANY_CRAWLSand add aretry_afterfield (proposed decision 4). Agents branch on the code, so a rename later breaks them. - 400, 409 and 422 all map to
CRAWL_INVALID(src/core/crawl-api.ts:289). A 409 (idempotency_request_in_flight) is safe to retry; a 422 is not. One code makes an agent unable to tell them apart. UseCRAWL_INVALID_REQUESTfor 400 and 422 (includingidempotency_key_reused) andCRAWL_REQUEST_IN_FLIGHTfor 409 (proposed decision 4). - 404
content_not_foundmaps toCRAWL_NOT_FOUND(src/core/crawl-api.ts:257). Only the message differs. UseCRAWL_CONTENT_NOT_FOUND(proposed decision 4). - Wait timeout is
CRAWL_TIMEOUT(src/core/crawl-api.ts:449,skills/crawl/SKILL.md:38). Rename toCRAWL_WAIT_TIMEOUT(proposed decision 4). Also update theErrorCodeunion (src/core/errors.ts:43). start --followwith alias--wait(src/cli/commands/crawl.ts:114, help:54). Keep--waitonly (proposed decision 10). The README (:172), the crawl skill (:25,:36,:43) and the master skill (skills/zenrows/SKILL.md:38) teach--follow, so scripts will use it. Removing it later breaks them.
Fix before merge: behaviour and docs
- The server code is not a field.
ToolkitErrorhas no field for it (src/core/errors.ts:51). The servercodeappears only inside thelikely_causetext (src/core/crawl-api.ts:233). Add it as a field, for exampleserver_code(proposed decision 4). resultsrefuses a running crawl withINVALID_USAGE(src/core/crawl-api.ts:414). Return what is kept withpartial: true, asresults --downloadalready does (src/cli/commands/crawl.ts:252) (proposed decision 2).- The 422
idempotency_key_reusedadvice is wrong. It gets "Fix the reported parameter and retry" (src/core/crawl-api.ts:297). A retry with the same key fails again. Tell the agent to send a new key, or no key. - The 429 text has the wrong meaning. The capability notes (
registry/capabilities.json:38), the README (:185), the crawl skill (:67) and the trace-debug skill (skills/trace-debug/SKILL.md:50) say "too many crawls running". The limit is the account's active Batch jobs: 3 by default, shared with its Batch jobs. Say so. The skill's "about 30s" is correct. --max-pageshelp says 1 to 100000 (src/cli/commands/crawl.ts:50). The default local policy refuses more than 1000 withPOLICY_LIMIT_EXCEEDEDbefore any request (src/cli/commands/crawl.ts:162,src/core/policy.ts:11). This is the same local cap Batch uses, not a production limit. Say in the help and the skill thatpolicy.max_pages_per_runcaps it locally.
Matches production
startsendsurlanddepth(required) and only the flags set.--htmlsetsoutput_format: "html". No flag setsjsonordiscovery.- Flag ranges match:
depth,max_items,max_pages1 to 100000;results --limitup to 10000;list --limitup to 100. The help calls the defaults of 10 API defaults. - 403
REQS008maps toCRAWL_NOT_ENABLED, 401 toAUTH_INVALID, 402 to the existingKEY_CREDIT_CAP_REACHEDandPOLICY_MAX_CREDITS_EXCEEDED. Other statuses, including a 403 withoutREQS008, map toCRAWL_FAILED. --jsonpasses the whole crawl object through, sodiscovery,extract_callsand unknown fields reach the agent. Enums are open.results --downloadreportscrawlStatusfromX-Crawl-Statusandpartial(proposed decision 3).list --jsonprintsnext_cursor: nullwhen the API leaves it out on the last page.- A
failedcrawl exits 1 withCRAWL_FAILED;stoppedexits 0. Every error exits 1, as in all other commands (src/cli/index.ts:98). ZENROWS_CRAWL_API_BASEstays (proposed decision 9). Auth is theX-API-Keyheader.startauto-creates a Free plan account throughensureApiKeyunless--no-signupis set, asfetchdoes.crawl-betais at 100%, so the new account can crawl.- The guard tests drop
crawlfrom the "removed non-GA" lists and still guarddiscoverandmonitor.
Can wait
These are additive and need no change now:
- The wait default is 600 s (
src/core/crawl-api.ts:441). All surfaces will use one value; we have not picked it yet (proposed decision 8). - No flag for
output_format: jsonordiscovery. A new flag can add them. - Errors do not expose the
RateLimitheaders. crawlFailurehas no specific advice fordomain_not_allowedorinternal_error; the generic advice applies.
Release: minor bump (1.5.0), since this adds a command.
Mark the crawl capability and skill as beta. Say Beta in the help, the README and the skills. Map Crawl errors to codes an agent can act on. 429 is now CRAWL_TOO_MANY_CRAWLS with a retry_after field. 400 and 422 are CRAWL_INVALID_REQUEST. 409 is CRAWL_REQUEST_IN_FLIGHT. A missing page is CRAWL_CONTENT_NOT_FOUND. A wait timeout is CRAWL_WAIT_TIMEOUT. Every error carries the API code as server_code, and crawl_id once the crawl exists. Keep --wait and remove --follow. Ctrl-C during a wait now says that the crawl keeps running, gives the resume command and exits 130. results returns what a running crawl kept, with partial: true. results --out --json prints the JSON envelope. stop on an ended crawl prints the status it ended with. The help and docs say that the local policy caps --max-pages and that --html uses up the page budget.
fetch and extract use --wait <ms> for a different meaning, so a boolean --wait on crawl start can confuse people. crawl start now takes --follow only. The crawl wait subcommand stays.
|
Replaced by #31: the same code, with a history that drops the AI co-author trailer. The review on this PR still applies. |
What this adds
A
zenrows crawlcommand for the Crawl API (https://api.zenrows.com/v1/crawls). Crawl is in beta. Crawl takes one start URL, follows the links on each page up todepthon the start URL's registrable domain (subdomains count), and returns the URLs it keeps, optionally with each page's HTML.Every subcommand prints JSON with
--json, in the same{ok, ..., error}shape the other commands use.results --outwith--jsonprints the envelope withfileinstead ofresults.src/core/crawl-api.ts: the client.createCrawl,getCrawl,listCrawls,stopCrawl,getCrawlContent,downloadCrawl(returns theX-Crawl-Statusstatus with the lines),listAllResults(followsnext_cursor; on a running crawl it returns what is kept so far and ends at the first empty page, withpartial: true),waitForCrawl(polls withlimit=1, 2s backoff x1.5 capped at 15s, 600 s default; on timeout the crawl keeps running). Auth is theX-API-Keyheader.ZENROWS_CRAWL_API_BASEoverrides the base; otherwise it follows the configuredapiBase. No Crawl call is retried.createsends only the fields the caller set.--htmlsetsoutput_format: "html". Without it, the crawl returns URLs only. With it, each kept page is one more fetch that--max-pagescounts.start --waitandwaitpoll until the crawl ends. Ctrl-C stops the wait, not the crawl: the CLI says the crawl is still running, printszenrows crawl wait <id>and exits 130.--followis gone.stopon a crawl that already ended prints the status it ended with, not "Stopped".CRAWL_NOT_ENABLED(403REQS008),CRAWL_INVALID_REQUEST(400 and 422; foridempotency_key_reusedthe advice is to send a new key or no key),CRAWL_REQUEST_IN_FLIGHT(409, retry once the first request ends),CRAWL_NOT_FOUNDandCRAWL_CONTENT_NOT_FOUND(404),CRAWL_TOO_MANY_CRAWLS(429: the account has reached its limit of active jobs, 3 by default, shared with its Batch jobs; structuredretry_afterin seconds),CRAWL_WAIT_TIMEOUTandCRAWL_FAILED(a crawl that endedfailed, any other 403, or another error). 401 maps toAUTH_INVALID. 402 goes through the existingKEY_CREDIT_CAP_REACHEDandPOLICY_MAX_CREDITS_EXCEEDEDerrors. Every error carries the API's code asserver_codewhen the API sent one, andcrawl_idonce the crawl exists.startchecks the domain policy andmax_pages_per_run(default 1000) before any network call, and writes a run artifact. The help says that this local policy caps--max-pages.crawlcapability (beta), acrawlskill (beta) with its registry entry, crawl lines in thezenrowsandtrace-debugskills, a README section and table row, and adocs/capabilities.mdrow. The help, README and skills say Beta.Test guards changed on purpose
tests/production-surface.test.tsasserted thatcrawlis an unknown command, andtests/registry.test.tsasserted that there is nocrawlskill. Both now check onlydiscoverandmonitor, and also check thatcrawlis registered.tests/capabilities.test.tspins thecrawlstatus tobeta.Tests
npm run typecheck: clean.npm test: 327 passed, 0 failed. New:tests/crawl-api.test.tscovers request shape, response parsing, error mapping (403REQS008and other 403, 404 both codes, 400, 409, 422 includingidempotency_key_reused, 429 withretry_afterand no retry, 401, 500, a body without a code),server_codeandcrawl_idon errors, paging on ended and running crawls, and waiter termination and timeout.tests/crawl-command.test.tscovers the command, including that undeclared flags (and--follow) are rejected before any request,partialresults, the--out --jsonenvelope,stopon an ended crawl and Ctrl-C during a wait.tests/e2e/crawl.e2e.test.ts: drives the built CLI binary. It runs only withnpm run test:e2e, and it skips unlessZENROWS_E2E=1,ZENROWS_API_KEYandZENROWS_E2E_CRAWL_URLare set.ZENROWS_E2E_CRAWL_INCLUDEandZENROWS_CRAWL_API_BASEare optional.docs/contributing.mdhas a "Running the Crawl e2e test" section.I ran it against a live ZenRows API deployment on the first commit, with the target set through
ZENROWS_E2E_CRAWL_URL: 1 passed, 0 failed. The run started a crawl and waited for it to complete. Every result matched the include pattern. It read one page's HTML, and the NDJSON export had one line per result. The crawl appeared in the list, stop on the ended crawl returned its terminal status, and an unknown id gaveCRAWL_NOT_FOUND. The e2e test now uses--waitandCRAWL_TOO_MANY_CRAWLS; it did not run again after the fix commit.