browse97

Chrome browser automation for pi coding agent via CDP. Open tabs, snapshot, click, fill forms, upload files, evaluate JS.

Packages

Package details

extension

Install browse97 from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:browse97
Package
browse97
Version
1.0.2
Published
May 15, 2026
Downloads
109/mo · 109/wk
Author
nerkn
License
MIT
Types
extension
Size
27.8 KB
Dependencies
0 dependencies · 0 peers
Pi manifest JSON
{
  "extensions": [
    "./index.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

browse97

Chrome browser automation for pi coding agent. Control Chrome via Chrome DevTools Protocol (CDP) — open tabs, snapshot pages, click elements, fill forms, upload files, evaluate JavaScript.

No external dependencies. Uses Node.js built-in WebSocket.

Setup

Launch Chrome with remote debugging:

google-chrome-stable --remote-debugging-port=9222 --no-first-run --user-data-dir=data/chrome-profile &

Custom port via environment variable:

BROWSE97_PORT=9223 google-chrome-stable --remote-debugging-port=9223 ...

Install

pi install git:github.com/nerkn/browse97

Tools

Tool Description
browse97_start Open a new browser tab and connect to it. Call this first.
browse97_navigate Navigate the owned tab to a URL
browse97_snapshot List interactive elements. Scope with container, filter with filter
browse97_click Click by CSS selector or visible text
browse97_fill Fill multiple form fields by fuzzy matching name/id/placeholder/label
browse97_upload Upload file to a file input (blob injection for HTTPS)
browse97_eval Evaluate JavaScript in the browser page context
browse97_wait Wait for an element or text to appear

Ownership Model

  • browse97_start creates a new tab → your pi session owns it
  • All other tools operate only on your owned tab
  • Tab stays open in Chrome after session ends
  • Each pi session gets its own tab — safe for multi-project use

Usage Examples

Open google.com and search:
  → browse97_start({url: "https://google.com"})
  → browse97_snapshot({filter: "input,button"})
  → browse97_fill({fields: {"q": "pi coding agent"}})
  → browse97_click({target: "Google Search"})

Fill a job application form:
  → browse97_start({url: "https://apply.example.com"})
  → browse97_snapshot({container: ".application-form"})
  → browse97_fill({fields: {"first_name": "Erkin", "last_name": "Tek", "email": "me@example.com"}})
  → browse97_upload({selector: "#cv", file: "assets/CV.pdf"})
  → browse97_click({target: "Submit"})

Extract data from a page:
  → browse97_eval({expression: "[...document.querySelectorAll('.job-card')].map(c => c.textContent.trim()).join('\\n')"})

Wait for dynamic content:
  → browse97_wait({target: ".results-loaded", timeout: 5000})

snapshot Parameters

Parameter Example Description
(none) {} All interactive elements on page
container {container: ".main"} Scope to elements inside .main
filter {filter: "a,button"} Only show links and buttons
both {container: ".form", filter: "input,select"} Inputs and selects inside .form

click Auto-Detection

  • Starts with . # [ ( → CSS selector
  • Contains > + ~ * → CSS selector
  • Otherwise → matches visible text of buttons, links, submit inputs

fill Fuzzy Matching

Field keys are matched case-insensitively against: name, id, placeholder, aria-label, associated <label> text.

browse97_fill({fields: {"email": "x@y.com"}})
  → matches: name="email", id="email", placeholder="Email address", label "Email"

File Upload on HTTPS

DOM.setFileInputFiles only works on file:// pages. browse97 works around this by injecting files as blobs via DataTransfer.

Limitation: Files set via blob don't survive native form submission (browser security).

Workaround: Use browse97_eval with FormData + fetch() for form submission that includes files:

const form = document.querySelector('form');
const formData = new FormData(form);
// blob already injected by browse97_upload
const response = await fetch(form.action, { method: 'POST', body: formData });

Your Context is Safe

browse97_snapshot and browse97_eval truncate results at 5000 characters by default. When truncated, a message suggests scoping with container/filter or passing allowWhole: true.

snapshot({allowWhole: true})                          // no truncation
browse97_eval({expression: "...", allowWhole: true})  // no truncation

License

MIT