@agntn/browsers0.6.1
Unified browser-as-a-service provider for AI agents and CLI.
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
@agntn/browsers
🌐 Eight vendors. One scrape call. JavaScript already ran.
Docs, a page per provider and one real scrape through each of them: browsers.agntn.dev.
Why?
Steel scrapes in one HTTP call. Kernel wants a live session first. Browserbase hands you CDP. A model will mix those three up, so this is one scrape() over eight backends.
✨ Features
- 🧩 Eight backends, one contract. Steel, Browserbase, Kernel, Browserless, Hyperbrowser, Anchor, Cloudflare and Playwright. Same objects on your side. Anchor still has no scrape.
- 🖥️ Playwright is local. No key. Chromium on the machine you already have.
- ⚡ Stateless where the vendor is. Steel, Browserbase, Browserless, Hyperbrowser, Cloudflare and Playwright scrape without you opening a session. Kernel will not.
- 📸 The extras follow the backend. PDFs, crawls, links, AI extract, search. Anchor will not scrape. Hyperbrowser will search.
- 🏷️
capabilities()is the list. Navigate is true on Kernel, Browserless and Playwright. False on Steel. Read the flag. - 🤖 Five surfaces, twelve tools. CLI, library, MCP, Pi, OMP. They share the executors.
- 📏 Agent scrape and crawl have a ceiling. 20 000 characters unless you pass
maxChars, 200 000 at most. A crawl shares it across pages. - ⏳ A slow crawl keeps its job. Cloudflare and Hyperbrowser wait two minutes, then hand back the job ID. Pass it as
jobIdwith the same provider and browser, or--jobin the CLI, and you wait for that job again. No second crawl. - 🔗 Agent links come in pages. 500 per call unless you pass
limit, 5 000 at most. The answer counts the rest and names theoffsetto ask for next. The CLI still prints them all. - ♿ A form reads as its fields. The accessibility tree gives one line per field, button or heading, with its state. The httpbin pizza form comes to about a thousand characters. Cloudflare only for now.
- 🔐 Keys in a URL get scrubbed. Query params named
tokenorapi_keyland in errors as[REDACTED].
📦 Install
pnpm add @agntn/browsers
Node.js 26 or newer.
🚀 First call
npx @agntn/browsers scrape https://example.com --provider playwright
ℹ Scraping via playwright...
This domain is for use in documentation examples without needing permission. This is not a service; avoid relying on it for testing and monitoring purposes.
هذا النطاق مُخصص للاستخدام في أمثلة التوثيق دون الحاجة إلى إذن. هذه ليست خدمة، يُرجى تجنب الاعتماد عليها لأغراض الاختبار والمراقبة.
该域名仅用于文档示例,无需获得许可。这并非一项服务,请勿将其用于测试和监控目的。
L’usage de ce domaine est réservé à des exemples de documentation, sans autorisation préalable. Il ne s’agit pas d’un service ; son utilisation à des fins de test ou de surveillance est à éviter.
Данный домен предназначен для использования в примерах документации без необходимости получения предварительного разрешения. Это не сервис; не рекомендуется его использование для тестирования и мониторинга.
Este dominio está destinado al uso en ejemplos de documentación sin necesidad de permiso. Esto no es un servicio; evitar utilizarlo para realizar pruebas o monitoreos.
Learn more
Six languages now, same notice. No key, no account. Playwright runs Chromium here. After pnpm add, that command is pnpm exec browsers, or install it once with pnpm add -g @agntn/browsers. Skip --provider and the first configured backend that can run the command wins. Steel if that key is set. Playwright if nothing else is. links on a Steel setup skips Steel, which has no links endpoint.
The scrape said "Learn more" and dropped the href. This did not:
browsers links https://example.com --provider playwright
https://iana.org/help/example-domains
Same page, different door. A form wants a third one, the accessibility tree:
browsers accessibility https://httpbin.org/forms/post --root fieldset
- group "Pizza Size"
- Legend
- StaticText "Pizza Size"
- InlineTextBox
- paragraph
- none
- radio "Small" checked=false
- none
- paragraph
- none
- radio "Medium" checked=false
- none
- paragraph
- none
- radio "Large" checked=false
- none
--root keeps every node of that subtree, the boring ones too. Leave it out and you get the whole page, trimmed. Cloudflare answers this one.
Who is even configured?
browsers providers
● steel scrape screenshot(sess) sessions cdp
● browserbase scrape sessions cdp
● kernel scrape(sess) screenshot(sess) navigate evaluate sessions cdp
● browserless scrape screenshot element navigate evaluate sessions cdp pdf
● hyperbrowser scrape screenshot sessions cdp crawl search extract
● anchor screenshot(sess) sessions cdp
● cloudflare scrape screenshot element sessions cdp crawl pdf links extract accessibility
● playwright scrape screenshot(sess) element navigate evaluate sessions crawl pdf links
Filled dot means that key was in the env on this run. Playwright is always filled. A few more:
browsers scrape https://example.com --provider playwright --format html
browsers scrape https://example.com --provider playwright --waitFor p
browsers scrape https://example.com --browser kitesurf
browsers screenshot https://example.com -o page.png
browsers screenshot https://example.com -o notice.png --selector p -p browserless
browsers pdf https://example.com -o page.pdf --provider playwright
browsers crawl https://example.com --maxPages 5 --provider playwright
browsers crawl --job <job-id> --provider cloudflare
browsers search "browser automation" --provider hyperbrowser
browsers extract https://example.com --prompt "Extract the heading"
browsers session create --provider kernel
search is Hyperbrowser. extract is Cloudflare or Hyperbrowser. The rest take -p when you do not want the default. --browser kitesurf picks Cloudflare and opts into Kitesurf. Leave it out and Cloudflare stays on Chromium.
Commands
| Command | What it does | Example |
|---|---|---|
scrape |
Rendered page, markdown if it exists | browsers scrape https://example.com -p playwright |
screenshot |
Image to a file | browsers screenshot https://example.com -o page.png |
crawl |
Follow links, cap with --maxPages |
browsers crawl https://example.com --maxPages 5 |
pdf |
URL to a PDF file | browsers pdf https://example.com -o page.pdf |
links |
Unique hrefs | browsers links https://example.com -p playwright |
accessibility |
Roles, names and states, one per line | browsers accessibility https://example.com |
search |
Hyperbrowser web search | browsers search "browser automation" |
extract |
Structured extract with a prompt | browsers extract https://example.com --prompt "..." |
session |
create, list, release |
browsers session create -p kernel |
providers |
Who is configured, what they can do | browsers providers |
mcp |
MCP server on stdio | browsers mcp |
🧠 Library
import { create } from "@agntn/browsers";
const provider = await create("playwright");
const page = await provider.scrape("https://example.com");
console.log(page.markdown ?? page.text ?? page.html);
const caps = provider.capabilities();
console.log(caps.statelessScrape, caps.pdf, caps.cdp);
That's most of it, really. create("steel") if you have STEEL_API_KEY. resolveProvider() picks the first one that does. Want Kitesurf on Cloudflare? Use create("cloudflare", { browser: "kitesurf" }). Without that option it stays on Chromium. Cloudflare scrape also fills title and statusCode. Ask it for html and markdown together, or add screenshot: true, and all of them come from one render. Kernel scrape wants a session object. The agent tools and the CLI scrape command open one and close it.
create() is async because that first call is where the provider module gets imported. Importing the package loads no provider and no HTTP client, providers() and has() answer from a manifest, and @agntn/browsers/providers/steel gives you one provider's factory directly.
🗺️ Providers
| Provider | Auth | Capabilities |
|---|---|---|
| steel | STEEL_API_KEY |
scrape, screenshot (session), sessions, CDP |
| browserbase | BROWSERBASE_API_KEY |
scrape, sessions, CDP |
| kernel | KERNEL_API_KEY |
scrape (session), screenshot (session), navigate, evaluate, sessions, CDP |
| browserless | BROWSERLESS_API_KEY |
scrape, screenshot, element screenshot, navigate, evaluate, sessions, CDP, PDF |
| hyperbrowser | HYPERBROWSER_API_KEY |
scrape, screenshot, sessions, CDP, crawl, search, extract |
| anchor | ANCHOR_API_KEY |
screenshot (session), sessions, CDP |
| cloudflare | CF_API_TOKEN + CF_ACCOUNT_ID |
scrape, screenshot, element screenshot, sessions, CDP, crawl, PDF, links, extract, accessibility tree |
| playwright | none, local | scrape, screenshot (session), element screenshot, navigate, evaluate, sessions, crawl, PDF, links |
Anchor does not scrape. The row stays, because someone will look for it. Cloudflare also accepts CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID.
🤖 Agents
browsers mcp
pi install git:github.com/agntn/browsers
omp install @agntn/browsers
{
"mcpServers": {
"browsers": { "command": "browsers", "args": ["mcp"] }
}
}
Twelve tools, browsers_scrape through browsers_capabilities, the same twelve on MCP, Pi and OMP. Cloudflare calls backed by Browser Run take browser: "kitesurf". No provider needed then. They do not drive a session you already opened. browsers_screenshot hands the image back to the model. A full page too big for that goes to a file: pass path, and it refuses to overwrite one that exists. Pass selector and you get one element instead of the page. Cloudflare, Browserless and Playwright can do that. Leave provider out and one of them gets picked; name another and it says no rather than hand back the whole page. browsers_pdf always writes to path, under the same rule, since a PDF cannot go back to the model.
🚫 What this does not do
Need a fetch that is not a browser? @agntn/web. Need last year's page? @agntn/archives. Need to click through a login? Not this package. The tools never grew navigate or evaluate.
🧩 Adding a provider
Want a ninth? A class that implements BrowserProvider, an exported factory, a manifest entry in src/providers/index.ts and the name list in src/tool-contract.ts. Nonstandard env keys go in src/core/resolve.ts. Outside the package, register(name, defaultURL, factory) adds one at runtime.
🛠️ Development
pnpm install
pnpm lint # builds first, then vp lint and vp fmt --check
pnpm lint:fix
pnpm typecheck # tsc, the build config, a build, then the extensions
pnpm test:run
pnpm build # obuild
💛 Thanks
Work on this library ran through two OSS programs, Claude for Open Source and Codex for Open Source. Thank you <3