aboutsummaryrefslogtreecommitdiff
path: root/src/client.ts
AgeCommit message (Collapse)Author
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