Repository navigation
feat(crawl): add zenrows crawl for the Crawl API - #31
Draft
StefanoGuerrini wants to merge 7 commits into
Draft
StefanoGuerrini wants to merge 7 commits into
StefanoGuerrini wants to merge 7 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.
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.
Rename the subcommands to the shared operation names: create, get, list, results, content, download, stop and wait. start and status are gone, and results --download is now download. Rename the flags to the API names: --include-pattern, --exclude-pattern and --output-format html. get and list take --cursor and --limit. results takes --limit as the page size and follows every page. create takes --follow to wait for the end. --timeout is in seconds, default 600. A wait that runs out is not an error. It exits 0, prints the crawl with status running and the hint to wait again. CRAWL_WAIT_TIMEOUT is gone. A 402 is now CRAWL_QUOTA_EXCEEDED, or CRAWL_KEY_CAP_REACHED for AUTH014. The messages keep the claim, top-up and key-cap advice. download reports status and partial. results reports partial when the crawl still runs or a cursor remains.
Every --json key is now snake_case: crawl_id, content_id and
content_type replace crawlId, contentId and contentType. The run
artifact keeps crawlId, as Batch keeps jobId.
create, get, wait and stop print {ok, crawl_id, status, crawl}. get
adds results and next_cursor. stop_reason is no longer at the top
level; read it under crawl.
A wait that runs out with the crawl still running now adds note under
--json, with the command to wait again.
download prints status: null when the API sends no X-Crawl-Status.
The help, the README and the crawl skill describe the JSON shape.
Crawl calls now retry up to 3 times. The wait is the Retry-After seconds, or 250 ms x 2^n with +/-20% jitter, capped at 10 s. - GET requests retry on 429, 502, 503, 504 and network errors, including the per-request timeout. - create retries only with an Idempotency-Key, and only on 502, 503, 504 and network errors. It never retries 429. - stop never retries. Batch and other commands do not change. The README and the crawl skill describe the policy.
Crawl copy now says New, not Beta. The crawl capability and skill have status available. The README Crawl section and the capability notes carry one line that says Crawl is still evolving. Batch and Extract wording does not change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR replaces #30. The code is the same. The history drops the AI co-author trailer. The review of the first version is here.
What this adds
A
zenrows crawlcommand for the Crawl API (https://api.zenrows.com/v1/crawls). Crawl is new. 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.The subcommands and flags follow the shared Crawl client interface, so the CLI uses the same operation and parameter names as the Node, Python and Go SDKs and the MCP server.
Every subcommand prints JSON with
--json, in the same{ok, ..., error}shape the other commands use. All keys are snake_case (crawl_id,content_id,content_type), as in the MCP server.create,get,waitandstopprint{ok, crawl_id, status, crawl}; the crawl object, withcoverage,stop_reasonanderror, is undercrawl.getaddsresultsandnext_cursor.results --outwith--jsonprints the envelope withfileinstead ofresults. The run artifact keepscrawlId, as Batch keepsjobId.src/core/crawl-api.ts: the client.createCrawl,getCrawl,listCrawls,stopCrawl,getCrawlContent,downloadCrawl(returns theX-Crawl-Statusstatus with the NDJSON),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; when the time runs out it returns the crawl, stillrunning). Auth is theX-API-Keyheader.ZENROWS_CRAWL_API_BASEoverrides the base; otherwise it follows the configuredapiBase. Transient failures are retried as the Crawl SDKs do: up to 3 retries, waiting theRetry-Afterseconds or 250 ms x 2^n (+/-20% jitter, capped at 10 s). GET requests retry on 429, 502, 503, 504 and network errors (a per-request timeout counts).createretries only with an Idempotency-Key, and only on 502, 503, 504 and network errors, never on 429.stopnever retries. Batch does not change.createsends only the fields the caller set.--output-format htmlsetsoutput_format: "html"and is the only value it takes. Without it, the crawl returns URLs only. With it, each kept page is one more fetch that--max-pagescounts.getreads one API page: the crawl and one page of its results, withnext_cursor.listreads one page of crawls.resultsfollows every page;--limitis the page size.downloadwrites the NDJSON export to--out(default<id>.jsonl) and reportsstatus(nullwhen the API sends noX-Crawl-Status) andpartial.create --followandwaitpoll until the crawl ends or--timeoutseconds run out (default 600). When the time runs out, the command exits 0 and prints the crawl with statusrunning(ok: trueunder--json) and the hintzenrows crawl wait <id>(under--json, innote, the key the MCP server uses). The crawl keeps running. A crawl that endsfailedexits 1. Ctrl-C stops the wait, not the crawl: the CLI says the crawl is still running, printszenrows crawl wait <id>and exits 130.stopon a crawl that already ended prints the status it ended with, not "Stopped".CRAWL_NOT_ENABLED(403REQS008),AUTH_INVALID(401),CRAWL_QUOTA_EXCEEDEDandCRAWL_KEY_CAP_REACHED(402;AUTH014is the key cap; the messages keep the claim-URL, top-up and key-cap advice of the shared credit errors),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_FAILED(a crawl that endedfailed, any other 403, or another error) andBACKEND_UNAVAILABLE(network). Every error carries the API's code asserver_codewhen the API sent one, andcrawl_idon any call about one crawl.createchecks 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 (available), acrawlskill (available) 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 New. The README Crawl section and the capability notes carry one line that says Crawl is still evolving.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 toavailable.Tests
npm run typecheck: clean.npm test: 338 passed, 0 failed.tests/crawl-api.test.tscovers request shape, response parsing, error mapping (403REQS008and other 403, 402 quota and key cap, 404 both codes, 400, 409, 422 includingidempotency_key_reused, 429 withretry_after, 401, 500, a body without a code), the retry policy (a GET retried on 503 with backoff and then successful, a GET that gives up after 3 retries,Retry-Afterhonored, a transport failure retried on a GET, create without a key not retried on 503, create with a key retried on 503, create never retried on 429 even with a key, stop not retried),server_codeandcrawl_idon errors, paging with a page size on ended and running crawls, and the waiter's end and timeout.tests/crawl-command.test.tscovers the command: undeclared and removed flags (--html,--include,--exclude,--wait,results --download,results --cursor) and the removedstartandstatussubcommands are rejected before any request,--output-formatand--timeoutvalidation,getwith a cursor,partialresults,downloadwith and withoutX-Crawl-Status, the JSON keys ofcreate,get,stopandcontent, the--out --jsonenvelope,stopon an ended crawl, a wait that runs out (exit 0, statusrunning, resume hint,noteunder--json) 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 the new subcommands and flags; it did not run again after these changes.