aboutsummaryrefslogtreecommitdiff
path: root/src/client.ts
AgeCommit message (Collapse)Author
2026-08-02refactor: merge nine files whose reason to exist was goneamankumarsingh77
src/ goes from 84 files to 75 at the same line count — every merge below had exactly one consumer. cdp/event-queue.ts and cdp/pending-requests.ts become daemon/queue.ts. I split these in the daemon-internals commit on the grounds that they held two unrelated concerns, but the transport commit had already deleted the second transport, so both halves had one consumer and the split bought nothing. They also sat in cdp/ while only daemon/ used them. cdp/transport.ts held only the CdpTransport interface after the second transport went; five of its six consumers import it type-only, so it moves to cdp/types.ts. cdp/evaluate.ts moves into cdp/session.ts rather than into its lone consumer isolated-tab.ts — it reaches session.call directly, so under src/domains/ it would trip the raw-cdp-call rule. util/keycodes.ts and util/time.ts fold into their only callers (keyboard.ts, page.ts); util/bytes.ts into util/truncate.ts, which already owns output-size formatting; readpage/capture.ts and search/extract.ts into the tools beside them; profile/picker.ts into profile/command.ts. Kept deliberately: mutex.ts and domains/target.ts each have their own test suite, and folding target.ts into click.ts would make a tool file export a helper. sharp-shim.ts is an isolation boundary carrying two allowlisted casts. result.ts, guards.ts, js-template.ts and paths.ts are small but have 44, 12, 8 and 3 consumers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: one CDP transport, not twoamankumarsingh77
createCdpTransport was a second full WebSocket-to-Chrome implementation reachable only through client.ts's `?? createCdpTransport()` fallback, which production never took — every session and every automated test passes the daemon transport. transport.ts now holds only the CdpTransport interface, and the client requires a transport instead of inventing one. The "Session with given id not found" match that both transports carried becomes classifyRemoteError in cdp/errors.ts, next to the error kind it produces. test/manual/profile-concurrency-test.ts was the sole caller of the fallback: it drives its own Chrome on a private port. It now spawns a daemon per scenario with BU_CDP_WS set, and refuses to run when a daemon is already up, since that one is bound to a different browser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix: make --browser-debug-clicks and BU_CDP_WS actually workamankumarsingh77
Both were documented and inert. --browser-debug-clicks was registered as a flag and never read; the only real switch was the undocumented BH_DEBUG_CLICKS env var. The flag now feeds util/debug.ts, which the click tool consults, and the env var keeps working as an equivalent. BU_CDP_WS was read in the client and handed to transport.connect(), but the daemon transport discards that argument and always dials the daemon socket — and the daemon's own discovery never looked at the variable, so setting it did nothing. The override now lives in discoverEndpoint, which is what both processes go through, and the client's redundant read is gone. The variable must be set before the daemon spawns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: strip comment noise from the root modulesamankumarsingh77
Removes install/command banners and comments restating the code. Keeps one-line notes for the per-session profile-pin re-read, the deliberately surviving transport on shutdown, seed-before-attach under a pin, and the starts-with-return wrap rule. src/prompt.ts is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: close the remaining unguarded type assertionsamankumarsingh77
Types the event-queue iterator's TReturn as undefined so the terminal result needs no cast; declares the isError field the pi runtime reads by duck-typing; binds socket to a local const in the daemon transport; replaces the @ts-ignore on the optional sharp import with a non-literal specifier so src/ still typechecks where sharp was not installed. Adds src/util/guards.ts as the single home for narrowing over unknown — isRecord plus scalar/array readers — and converts the two private isObject copies in cdp/types.ts and daemon/protocol.ts along with every inline `as Record<string, unknown>` in domains/js.ts and domains/render.ts. Gives Accessibility.getFullAXTree a real result schema, closing the last unvalidated CDP boundary: only nodeId is required, every object carries additionalProperties, and each AX value stays Unknown, so the tree decodes as a whole or fails as a whole rather than being filtered. Guards the five tool-result `details` readers, the daemon attach response, the persisted session state, the browser-namespace flag, and the fs errno narrowing, each feeding an already-present failure path. The daemon-transport socket binding is not a bug fix: send() runs synchronously inside sendWithTimeout's promise executor, so socket cannot be reassigned between the check and the write. It removes the assertion and makes the write immune to a later refactor that adds an await. Two assertions survive by design and are documented in the task report: the never->unknown re-widening in util/tool.ts, which the type-erased tool registry makes unavoidable, and the AsyncFunction constructor lookup in domains/js.ts, which JavaScript exposes no typed binding for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: validate JSON at every parse boundaryamankumarsingh77
Ten sites cast or implicitly typed JSON.parse output; JSON.parse("null") would have satisfied all of them. parseJson returns a Result and validates against a typebox schema. js.ts keeps unknown — user script output has no fixed shape. daemon/bridge.ts assigned JSON.parse straight to CdpRawMessage with no guard at all; it now uses isCdpRawMessage like the transport does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): extract attachTo, getWindowId, evaluateJson helpersamankumarsingh77
Target.attachToTarget was decoded at 5 sites, Browser.getWindowForTarget at 5, and Runtime.evaluate unwrapped at another. Each is now one typed helper. Deletes the dead cdp/window-binding.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26fix(client): guard page-info evaluation against a missing documentElementamankumarsingh77
The page-info probe read document.documentElement.scrollWidth/scrollHeight directly, which throws when documentElement is absent. Fall back to zeroed dimensions so evaluation returns a usable PageInfo instead of failing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26fix: kill leaked Chrome processes on profile seed failureamankumarsingh77
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.
2026-07-25feat: pin the agent to a chosen browser profile (#16)Aman Kumar
* 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>
2026-07-22feat: add forms domain and durable window binding (v0.9.0) (#14)Aman Kumar
- 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>
2026-07-05fix: make dispatch_key fire legacy keyCode handlers and tighten execute_js ↵Aman Kumar
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>
2026-06-21release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init (#6)Aman Kumar
* 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
2026-05-08feat: add per-tab isolation for multi-agent safetyamankumarsingh77
Replace singleton dialog, pageInfoDirty, networkBuffer, and consoleBuffer with per-tab TabSession objects stored in a Map<string, TabSession>. - Add TabSession type (sessionId, targetId, dialog, pageInfoDirty, networkBuffer, consoleBuffer) and tabs/sessionIdToTargetId Maps - Add ev.sessionId filter guard in consumeEvents to prevent cross-tab event contamination - Rework switchTo to create/reuse TabSession without clearing buffers, updating sessionId on re-attach - Route all CDP events (dialog, network, console) through resolveTab to the correct TabSession - takeDialog/drainPageInfoInvalidations/drainNetworkBuffer/ drainConsoleBuffer read from current TabSession - Target.targetDestroyed prunes dead TabSession entries - attachFirstPage creates initial TabSession and prunes dead entries - Per-tab pageCaches Map in client.ts replaces pageCache singleton - listTabs reconciliation prunes dead pageCaches entries - console-buffer.ts clear() resets nextSeq to 1 (per-tab seq space) - Multi-agent warning in prompt.ts Parallel Execution section
2026-05-05feat: tab ownership — isolate harness tabs from the user's browsingamankumarsingh77
Track which page targets this harness session opened so the agent can never attach to, switch to, or close a tab the user opened themselves. New tabs are spawned in a dedicated Chrome window (newWindow:true on first open, openerId chaining after) so harness activity stays visually separate. - new OwnershipRegistry (src/cdp/ownership.ts) tracks owned targetIds and the harness window seed; mutations notify a single listener that persists to BrowserState (ownedTargetIds, harnessWindowTargetId) - attachFirstPage prefers an already-owned live target; otherwise opens a fresh harness-owned tab in its own window instead of grabbing whatever page the user is on - session subscribes Target.setDiscoverTargets and reaps targetDestroyed events so the registry stays in sync with reality - listTabs returns owned:boolean per tab and reconciles dead ids - browser_list_tabs defaults to scope:"owned"; pass scope:"all" to inspect the user's other tabs (read-only — they cannot be switched to or closed) - browser_switch_tab refuses non-owned tabs, with a clear remediation hint - browser_new_tab opens inside the harness window via openerId, falling back to a fresh window when the seed has been closed - new browser_close_tab tool — owned tabs only, supports unique hex prefix Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04feat: parallel tool execution with automatic mutation serializationamankumarsingh77
- Add lightweight async mutex (src/util/mutex.ts) for FIFO serialization - Add serialized?: boolean to BrowserToolDefinition - Wire mutex acquisition into registerBrowserTool execute wrapper - Expose mutationMutex() on BrowserClient - Mark all mutation tools (click, type, scroll, navigate, switch_tab, etc.) as serialized - Leave observation tools (screenshot, page_info, execute_js, http_get, etc.) unguarded - Update prompt and skill docs to reflect parallel-safe behavior - Fix scroll tool: add Page.bringToFront, explicit button:none/buttons:0/pointerType:mouse - Update .gitignore to ignore scripts/ and data/ folders
2026-05-02release: v0.3.1 — audit fixes for list_tabs IDs, switch_tab prefix ↵amankumarsingh77
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
2026-05-02chore: address final-review minors — surface event-consumer crashes, ↵amankumarsingh77
document IIFE-wrap heuristic, log shutdown failures, fix CHANGELOG file-size claim Five tweaks from the whole-implementation review: - session.ts: log unexpected event-consumer rejections instead of silent swallow - client.ts: comment the return-substring IIFE-wrap false-positive case - transport.ts: comment that ws.onclose post-open settle is no-op-by-design - index.ts: log client.stop() failures during shutdown - CHANGELOG.md: correct largest-file claim (transport.ts and client.ts are ~220 each, not "client.ts at ~180") Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02fix(cdp): serialize event consumer; persist dialog until taken; validate ↵amankumarsingh77
page info shape
2026-05-02feat: add CDP session and BrowserClient factoryamankumarsingh77