Open-source library · MIT
@nodetool-ai/browser
Drive the Chrome you are already signed in to.
A small TypeScript library for driving one Chrome page over the DevTools Protocol. Point it at a headless Chrome it launches, or at a tab in your everyday browser through a Chrome extension. In that tab your cookies, sessions and 2FA are already in place, so sites that block headless browsers still load.
$ npm install @nodetool-ai/browser- TypeScript
- Node 22+
- chrome-remote-interface
- zod
import {
browserNavigate,
browserView,
browserInput,
} from "@nodetool-ai/browser";
await browserNavigate({ url: "https://news.ycombinator.com" });
// Every interactive element comes back numbered,
// with its tag, role, text, attributes and bounding box.
const { elements, screenshot_png_b64 } = await browserView({
include_screenshot: true,
});
const search = elements.find((el) => el.attributes.name === "q");
await browserInput({ index: search!.index, text: "quickjs", press_enter: true });Two transports
One action loop, headless or in your own tab.
Only browserStatus and browserRestart know which transport is live. Element indexing, clicks, typing and screenshots run the same code against both, so a script written headless runs unchanged in your signed-in tab.
transport: "local"
your Node process ──CDP over WebSocket──▶ headless Chrome it launched
transport: "extension"
your Node process
│ CDP commands and events, NDJSON
│ unix socket, mode 0600 in a 0700 directory
▼
native host started by Chrome when the extension connects
│ Chrome native messaging, length-prefixed JSON
▼
extension service worker
│ chrome.debugger.sendCommand / onEvent
▼
the tab you attached your cookies, sessions and 2FAimport { browserRestart, browserStatus } from "@nodetool-ai/browser";
// Same calls as above, now against the tab you attached in your own Chrome.
await browserRestart({ transport: "extension" });
console.log(await browserStatus());Setting NODETOOL_BROWSER_TRANSPORT=extension before the process starts has the same effect.
Why an extension
Your real profile, without restarting Chrome.
- No debug flags on your browser
- You do not relaunch Chrome with --remote-debugging-port. Since Chrome 136 that flag is ignored for the default profile, so it cannot reach the profile you are signed in to anyway.
- Attach is a click
- The extension never attaches on page load. You press Attach to this tab in its popup, and Chrome shows its "is debugging this browser" banner until you detach or close the tab.
- One host, several clients
- The native host rewrites command ids per client and fans events out to all of them. The debugger stays attached until the last client disconnects.
- Chromium browsers
- installNativeHost() writes the host manifest for Chrome, Chromium, Brave, Edge and Arc on macOS and Linux.
API
Fourteen actions.
Each takes one plain object and returns one. Address elements by the index from the last browserView or by viewport coordinates. Lower-level access is exported as CdpPage, launchBrowser and withPage.
| Function | What it does |
|---|---|
| browserView | Index interactive elements, optionally with a PNG screenshot. |
| browserNavigate | Go to a URL and wait for load, DOMContentLoaded or network idle. |
| browserClick | Click an element by index or by viewport coordinates. |
| browserInput | Type into an element, optionally pressing Enter. |
| browserPressKey | Press a key, such as Enter or Escape. |
| browserSelectOption | Pick an option in a select element. |
| browserScroll | Scroll by pixels, or to the top or bottom. |
| browserMoveMouse | Move the pointer to viewport coordinates. |
| browserConsoleExec | Evaluate JavaScript in the page. |
| browserConsoleView | Read the page's console output. |
| browserCaptureMedia | Pull an image, video or audio file out of the page. |
| browserUploadAsset | Put bytes into a file input. |
| browserStatus | Report which transport is live. |
| browserRestart | Tear the session down, optionally switching transport. |
Design
It knows nothing about the app that calls it.
- Plain data in and out
- Inputs are plain values. A screenshot comes back as base64, not a file on disk. Your code decides what to keep.
- Zod schemas for every action
- Each action's input and output is a Zod schema exported from the package. That is the shape an LLM tool definition needs.
- Nothing loads on import
- Chrome and the CDP client are dynamic imports. Importing the package starts no process until the first action runs.
- Media capture with a fallback
- browserCaptureMedia reads the response body over CDP first, then retries with an in-page fetch when the body is gone.
Questions
Limits and trade-offs.
- Why not Playwright or Puppeteer?
- Use them for test suites and scripted scraping. They are far more complete. This package covers a narrower case: an agent or a script acting inside the browser profile you already use, through one small action loop that works the same headless or attached. Element indexing returns numbered elements so a model can say
click 14instead of writing a selector. - Who can drive my browser once the host is installed?
- Any process running as your OS user can connect to the host socket. The socket's file permissions are the only access control. If a client sends an attach frame while no tab is attached, the extension attaches the active tab. Detach when you are done, and do not install the host on a shared account.
- Can two callers use it at once?
- They share one page. The session is a process-wide singleton, so concurrent callers drive the same tab. Run separate processes if you need separate pages.
- Does the extension come from the Chrome Web Store?
- No. Build it from
chrome-extension/in the repository and load it unpacked. Its ID is pinned by the key in its manifest, so the host manifest matches it. - Windows?
- The headless transport works. The extension transport does not yet: installing the native host on Windows needs a registry entry, and installNativeHost() throws there.
- How is it tested?
npm testcovers the wire protocol, the RPC client and transport resolution without a browser.npm run test:integrationbuilds the extension, loads it in a real headless Chrome and drives the extension transport against local fixtures. It needs Chrome, so it runs by hand, not in CI.
This package came out of NodeTool, an open-source workspace for AI workflows. NodeTool's agent browser tools and its Screenshot node both call it. The library itself has no NodeTool runtime in it beyond a logger, and it is MIT licensed.