原始内容
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_alltabs |
List all open tabs (id, url, title) |
browse97_switchtab |
Connect agent to an existing tab by id |
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_startcreates 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')"})
Connect to an already-open tab:
→ browse97_alltabs({})
→ browse97_switchtab({id: "ABC123"})
→ browse97_snapshot({})
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