| Age | Commit message (Collapse) | Author |
|
|
|
|
|
Two independent Windows/production defects reported in #22.
browser_run_script rejected every absolute path on Windows. The allowed-root
check compared with a hardcoded `${root}/`, but path.resolve returns
backslash-separated paths there, so a file inside os.tmpdir() was refused even
though the rejection message listed tmpdir as allowed. Decide containment with
path.relative instead: it is separator-agnostic and carries each platform's own
case rules, so it also accepts a differing drive-letter case that a path.sep
patch would still reject. Escapes via `..` and sibling directories sharing the
root's name prefix stay rejected, as before.
The daemon could not start under `npm omit=dev`: it is spawned through tsx,
which was declared only as a dev dependency. Moving tsx to dependencies is not
enough on its own — pi installs extensions into one flat hoisted tree, so tsx
lands beside the package and the fixed <pkg>/node_modules/.bin/tsx lookup still
misses, falling through to an `npx tsx` that resolves against the wrong cwd and
dies against stdio: "ignore". Resolve the CLI entry with Node's own resolver,
which walks up to the hoisted copy. That removes the shell entirely, and with it
the Windows .cmd shim, windowsVerbatimArguments, and the manual quoting that
paths containing spaces needed.
Both fixes live in platform-branching code, so their tests are registered in the
three-OS CI matrix that already exists for that purpose — a Linux-only run
cannot exercise either one.
|
|
Breaking: the deep-research skill, command, and subagent are gone.
Also carries the 0.10.3 fixes, which were tagged but never published.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
IdMultiplexer and EventRouter were the only classes in src/; they are now
createIdMultiplexer/createEventRouter closures like every other factory
here. The multiplexer kept {clientId, localId} per daemon id while the
bridge kept {send, clientId, localId, timer, isAttach} under the same key
— two maps of overlapping data, and mux.resolve()'s return value was
discarded at three of four call sites. One map now holds the union.
That merge fixes a real leak: removeClient cleared the multiplexer but
never the callbacks map, so a disconnected client's timers stayed armed
and later fired a response at a socket that was gone. clearClient now
cancels them, and a test pins it.
handleRequest polled every 250ms for up to 15s per request when Chrome
was down; N queued requests now await one connect signal.
server.ts uses its own protocol's serialize/deserialize instead of
hand-rolled JSON, and broadcast no longer routes through `this` — the
only `this` in the codebase, and the one thing that breaks when the
server object is destructured.
cdp/event-queue.ts held two unrelated concerns; the request/timeout half
moves to cdp/pending-requests.ts. cdp/daemon-transport.ts is the client
half of the daemon protocol, so it moves to daemon/transport.ts.
test/daemon/ needs adding to the test glob to run at all — there was no
daemon test directory before because none of this was reachable without
live Chrome.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The harness ships browser tools; orchestrating a multi-agent research
workflow on top of them is the agent's job, not this extension's. Drops
the skill, the /deep-research command, and the web-search-researcher
subagent config.
browser_web_search and browser_read_page are untouched — the two tests
that covered their pure parsers move to test/domains/, where the test
glob still picks them up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A clean tsc does not prove the absence of casts. This scans src/ for
as any, ts-ignore, non-null assertions, inline object casts, and double
casts, wired into a new npm run check that CI runs as a single step.
The escape hatch is an external allowlist module rather than an inline
comment marker: comments are not used in this codebase, and an entry
carries a count, so adding a new cast to an already-exempted file still
fails until someone raises a number that shows up in the diff. Exactly
one exemption is needed today, for the dynamic sharp import.
scripts/ was gitignored wholesale, so the two new files are un-ignored
individually and everything else dropped there stays ignored.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
serialized: true was applied in registry.ts, away from the tools it
describes. Now a required concurrency field on each definition, with a
test pinning the serialized set so the classes cannot drift.
The lock check defaults to taking the mutex for any class that is not
explicitly "parallel", so a definition that bypasses defineBrowserTool at
runtime fails safe rather than racing on shared CDP state.
The test script gains --conditions=import: pi-coding-agent publishes no
"require" condition, so importing any tool definition from a CJS-loaded
test could not resolve it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
test/ was never type-checked and had no runner. Adds tsconfig.base.json
shared by a src project and a test project, an npm test script scoped to
non-manual suites, and a CI step. Ports the profile store suite as proof.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Also resyncs package-lock.json, which was left at 0.9.0 by the 0.10.0 and
0.10.1 bumps.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
When a profile is pinned and ProcessSingleton fails to delegate, the
spawned Chrome process starts a full second browser instance. The
sentinel never appeared, but the spawned process was never killed
(detached + unref'd + reference discarded), accumulating zombie
browsers that consume RAM.
Three changes:
- Store child process ref and expose kill() on ProfileWindowHandle.
Call kill() when the sentinel doesn't appear within the timeout.
- Only clear profileContextId when the browser UUID changes (Chrome
mints a fresh UUID on every launch), avoiding unnecessary re-spawns.
- Don't clear profileContextId in setProfilePin when the pin hasn't
changed, preventing gratuitous re-spawns on session_start.
|
|
* feat: pin the agent to a chosen browser profile (#15)
Harness tabs were created with a bare Target.createTarget, which places
them in Chrome's defaultBrowserContextId — and that value follows window
focus. Which profile the agent acted as therefore depended on which
browser window the user last clicked, so the same task could run as a
work account on one run and a personal account on the next.
/browser-profile lists the connected browser's profiles as "Name (email)"
and pins one to ~/.pi/agent/browser-harness.json, so the choice outlives
the session. The same picker appears on first browser_setup when nothing
is pinned; with no interactive UI, or on cancel, setup continues with a
note and the previous behavior.
Chrome offers no direct route to another profile: Target.createTarget
rejects a foreign browserContextId, and openerId does not inherit the
opener's context. The window is opened through the browser's own command
line (--profile-directory, handed to the running browser by Chromium's
ProcessSingleton) and identified by a unique file:// sentinel page, since
a bare about:blank#token argument is discarded. Further tabs come from
window.open evaluated with userGesture: true — the only CDP-reachable way
to place a tab in a non-default browser context — correlated by a unique
token so concurrent isolated tabs stay distinct. A failed handshake is
reported, never silently redirected to another profile.
All four tab-creation sites now funnel through cdp/target-factory, so
open_urls and the isolated tabs behind web_search / read_page cannot run
with a different profile's cookies than the visible tabs.
Also fixed along the way: snap Chromium is discovered, Windows uses
%LOCALAPPDATA%, browsers launched with an explicit --user-data-dir are
found, the running browser is chosen by ranking candidates against the
connected user-data-dir rather than taking the first process found, and
an installed-but-closed browser no longer reads as running.
CI runs the profile suites and a real-browser end-to-end test on ubuntu,
macOS, and Windows — profile discovery is the one part of the harness
whose behavior genuinely differs per OS.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: single-flight harness window creation
Isolated tabs run concurrently (browser_read_page is registered
unserialized), so several could each find "no harness window" and create
one — two blank windows before this feature, two LAUNCHED profile windows
after it. ensureHarnessWindow now shares one in-flight creation per
client.
Sharing introduces a second hazard: freshlyCreated is a claim on the seed
tab, and if every waiter received it, they would all drive the same tab.
Only the caller that started the creation keeps the claim; the rest open
their own tab beside it.
Covered by test/manual/profile-concurrency-test.ts (five concurrent
isolated tabs, pinned and unpinned: distinct targets, one window, correct
profile), wired into CI on Linux only — the coordination is pure JS and
identical across platforms.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: tilde expansion differs by purpose on Windows
CI on windows-latest caught this: join("~", "x") yields "~\x" there, and
expandTilde only handled "~/".
The two uses are not the same, so they no longer share a helper. The
agent dir stays byte-compatible with pi's own expandTildePath ("~/" only,
every platform) — being more lenient would put the pin file somewhere pi
never looks. $CHROME_USER_DATA_DIR is ours to interpret, so it accepts
"~\" as well, which is what a Windows user would type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
- Add forms domain with a universal field setter that auto-detects element
type and fires framework-compatible input events (React native-setter trick)
- Add window-binding helper to capture and bind the real Chrome windowId as the
session's durable window identity on every new-window code path
- Wire supporting changes across CDP session/ownership, client, navigate,
isolated-tab, prompt, and state
- Add manual tests for forms and window binding
- Bump version 0.8.3 -> 0.9.0
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
isDaemonRunning() only checked file existence of the Unix socket.
A dead daemon leaves a stale socket, causing ensureDaemon() to skip
spawning and client.start() to fail with 'Chrome not connected'.
Replace with a liveness probe: connect, register, verify ack, then
disconnect. On failure, clean up the stale socket.
Also makes bridge handleRequest async to poll for Chrome connection
for up to 15s before rejecting.
|
|
IIFE wrap (#9)
- browser_dispatch_key now sets keyCode/which on the synthesized KeyboardEvent
(both ref and selector paths) so handlers checking e.keyCode === 13 fire.
- browser_execute_js IIFE auto-wrap triggers only when the source starts with a
return statement, not on any substring "return " (which silently produced
undefined for bare expressions).
- browser_fill guidelines point at dispatch_key for tag-input submission and the
snapshot+click recipe for custom dropdowns.
Bump to 0.8.1.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
Interaction tools could only target elements by raw viewport coordinates
(browser_click) or model-guessed CSS selectors (browser_fill/focus/
select_option/upload). Neither survives a re-render: on React/SPA forms,
every save or edit reflows the page, so captured @(x,y) coordinates go
stale and guessed selectors miss — leaving the agent to fall back to
hand-rolled execute_js DOM walks and unnoticed failed saves.
Add a stable ref system, mirroring Playwright MCP / browser-use:
- snapshot assigns each interactive element a ref (e1, e2, …), shown as
[eN] in the outline, and publishes a per-tab ref → backendNodeId map
- interaction tools accept an optional `ref` that re-resolves the element
at call time (via DOM.resolveNode / getBoxModel), so it stays valid
across re-renders; coordinates/selectors remain as fallbacks
- mutating tools append a compact diff of changed / newly-appeared (*) /
removed interactive elements, so silent failed saves surface immediately
- stale refs return an actionable "re-snapshot" error instead of a miss
Ref map is replaced per snapshot (bounded memory); fill keeps the native
_valueTracker write path so React controlled inputs still update.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
|
* feat: Unix socket daemon with auto-reconnect for persistent Chrome CDP
Eliminates the 'Allow Remote Debugging' Chrome prompt that appeared on
every pi session by multiplexing pi instances through a single persistent
daemon process.
Key additions:
- Unix socket IPC (net.createServer + readline JSON framing)
- CdpBridge with ID multiplexer + event router
- DaemonTransport implementing CdpTransport interface
- Auto-spawn with ensureDaemon() (lazy, checks socket)
Fixes since initial implementation:
- Bridge auto-reconnects to Chrome with exponential backoff (1s-60s).
Previously the bridge tried once at startup and permanently failed
if Chrome wasn't ready — causing 'Chrome not connected' on every
browser tool call.
- Daemon spawn uses project-local tsx binary instead of global --import
flag, fixing silent spawn failure (ERR_MODULE_NOT_FOUND).
- Stale ctx crash: persistState in onOwnershipChange wrapped in
try/catch to prevent CDP event consumer crashes.
49 test assertions across 4 test files. Zero TypeScript errors.
* feat: on-demand browser initialization
Browser daemon no longer auto-starts on session_start. Instead:
- session_start creates the client lazily but does NOT connect or spawn
the daemon. Zero Chrome prompts for non-browser pi sessions.
- Browser tool wrapper checks for daemon socket before each call.
Socket missing → clear error: 'Run /browser-setup first'.
Socket exists → proceeds silently (daemon already running).
- /browser-setup is the explicit, one-time init point. It spawns
the daemon and connects to Chrome only when the user opts in.
- SKILL.md updated with Connection Setup instructions telling agents
to direct users to /browser-setup when socket is missing.
* fix: checkChromeRunning on macOS — match by substring not exact
ps -A -o comm= returns full executable paths on modern macOS
(e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome),
not the basename. The old exact-match check (browserProcesses.includes(l))
never matched, causing false 'No browser instance running' errors.
Changed to substring includes() with helper/renderer/crashpad filtering.
* feat: browser_setup as agent-callable tool
The agent can now self-recover from 'Browser harness not initialized'
errors by calling browser_setup directly — no user mediation needed.
- Extracted shared performSetup() from the slash command into setup.ts
- Created src/domains/setup.ts with ensureAlive:false to bypass the
socket guard (this tool IS the setup, can't require itself)
- Registered browser_setup in the tool registry
- Updated SKILL.md: agent calls browser_setup directly, not ask user
- Removed stale test/manual/spawn-test.ts
* release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init
|
|
|
|
prompts
Adds two new tools and pivots the agent's default workflow away from
screenshots toward structured page data.
browser_snapshot — CDP accessibility tree (roles, names, states, hierarchy)
with click coordinates @(x,y) attached to every interactive element via
DOM.getBoxModel (parallel, 1.5s aggregate budget). Pass directly to
browser_click — no screenshot round-trip. Optional includeScreenshot:true
for visual confirmation. format:"json" returns a slim structure with full
box per node.
browser_network_requests — replaces the deprecated browser_get_network_log
placeholder. Filters by urlPattern (substring or /regex/), method, status,
resource type, recency. Optional response-body capture under a 5s aggregate
budget with 50KB per-body cap. Buffer is page-scoped (cleared on tab switch),
500 records/tab, with a bufferOverflowed flag on drain.
CDP plumbing: in-process aggregator in src/cdp/network-buffer.ts wired into
the existing single-consumer event loop. Accessibility domain enabled
alongside Page/DOM/Runtime/Network on every attach.
Ctrl+O expand/collapse on three tools' renderResult — browser_snapshot,
browser_network_requests, browser_execute_js. Compact-by-default views with
keyHint("app.tools.expand") that respects user keybinding remaps.
Tool prompts pivoted snapshot-first: browser_screenshot is documented as
LAST RESORT for visual verification only; browser_snapshot is the default
for understanding pages; browser_execute_js is the default for surgical
reads. browser_click and browser_open_urls guidance follows.
SKILL.md collapsed from ~342 to ~53 lines — keeping only what isn't already
in tool prompts: the decision tree, connection rules, browser_run_script
daemon bindings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
- Add async mutex for auto-serialization of mutation tools
- Observation tools can run in parallel with mutations
- Fix scroll deltaY sign convention (positive=down per W3C)
- Add Page.bringToFront before scroll to prevent silent drops
- Fix screenshot TUI crash on long file paths (truncateToWidth)
|
|
matching, download auto-mkdir, session recovery, docs updates
Bug fixes:
- list_tabs: show full 32-char targetIds (was truncated to 8 chars)
- switch_tab: prefix matching fallback for hex prefixes >= 8 chars
- download: auto-create directory with mkdir -p
- ensureAlive: Runtime.evaluate probe to recover from crashed page targets
Docs:
- dispatch_key: clarify DOM-only event behavior
- navigate: warn about Google anti-bot limitation
- network_log: update deprecated promptSnippet
- SKILL.md: document actual daemon API (evaluateJs, pageInfo, session.call)
- SKILL.md: add troubleshooting for sharp/maxDim and Google navigation
- sharp: add to optionalDependencies for screenshot maxDim auto-resize
|
|
|
|
single tsconfig
|
|
|
|
screenshots, page info caching
5 performance optimizations (14-36% faster end-to-end, output quality unchanged):
- fast ensureAlive(): WS state check instead of CDP roundtrip (96% faster per-call)
- event-based waitForLoad(): CDP load events instead of 300ms polling (99.5% faster for loaded tabs)
- JPEG screenshot support: 29-49% smaller files, faster CDP transfer
- page info caching: 1s TTL eliminates redundant evaluateJS calls
- parallel domain enables: Promise.all instead of sequential await in switchTab()
Benchmark: 3622ms vs 4243ms baseline on 3-tab navigation + screenshot task.
|
|
|
|
- Add hero image for pi package gallery
- Update CHANGELOG with release date
- Update README with npm install instructions
- Add assets/ to npm package files
- Add CONTRIBUTING.md and GitHub templates
- Add CI workflow for typecheck and pack validation
|
|
Drops the Unix socket subprocess in favor of talking CDP directly over
WebSocket from the extension. Removes the protocol's send/health helpers,
simplifies setup to just verifying Chrome's remote debugging port, and
tightens types in dynamic-tools. Bumps Node engine to >=22 and adds the
ws dependency.
|
|
|
|
Browser control extension for pi — 20 browser_* tools for navigating,
clicking, typing, screenshotting, and extracting data from real Chrome
via CDP. Includes self-extending harness (register_tool/remove_tool),
guided /browser-setup, session persistence, and custom TUI renderers.
|