aboutsummaryrefslogtreecommitdiff
path: root/src
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: act on the fallow auditamankumarsingh77
Unused exports it found that my own sweep missed, because I had grepped by name and these names occur as local variables elsewhere: deadline and the Deadline type in util/time.ts were genuinely unreferenced and are gone; isWireMessage, resolveUserDataDir, CLEAR_SELECTION_LABEL and the ExpandableText type are used only inside their own file and no longer export. isWireMessage became internal when server.ts moved to the shared serialize/deserialize codec. Duplication: console-buffer and network-buffer were the same capacity-bounded map with an overflow flag, sharing eviction, the drain tail and clear() across three clone groups. That is now cdp/record-store.ts, with tests — neither buffer had any. The three Network follow-up events also repeated the same decode-and-find preamble; one `update` helper covers them, and drops the redundant optional chaining that survived a `!id` guard. formatBytes existed verbatim in js.ts and network.ts; it is now util/bytes.ts. Duplication overall: 2.7% to 2.0%, and none of the remainder is in src. .fallowrc.json records the two entry points the tool cannot infer — the pi extension (declared under pi.extensions) and the daemon (spawned by path) — without which five reachable files read as dead. sharp is ignored because it is loaded through a variable specifier in a dynamic import so it can stay optional. agentDir is exported solely for a test's dynamic import, so it is suppressed by name rather than globally enabling ignoreExportsUsedInFile, which would have hidden four of the five findings above. Not acted on: the "split high-impact file" suggestions for session.ts, client.ts and ref-resolve.ts (119 LOC) are size and fan-in heuristics with no defect behind them, and splitting them would add the indirection this refactor set out to remove. The claim that state.ts is 100% dead is an artifact of the missing entry point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: daemon internals, and fix a leak the merge exposedamankumarsingh77
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>
2026-08-02refactor: attach and windowId become session methodsamankumarsingh77
cdp/attach.ts and cdp/window.ts were a module each for a single `map(await session.callBrowser(...))` one-liner that took the session as its first argument — two files, two imports and two names for what the session can expose itself. They are now session.attach(targetId) and session.windowId(targetId). session.ts also repeated `targetId ? tabs.get(targetId) : undefined` eight times; that is now currentTab(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: helpers live in helper modules, not in tool filesamankumarsingh77
form.ts exported resolveAndCall and detailsOf to form-batch.ts, snapshot.ts exported buildTree and collectInteractiveTargets to ref-resolve.ts, and screenshot.ts exported captureWithCrosshair to click.ts — so importing one tool pulled in another tool's whole module. The shared halves move out: ax-tree.ts (the AX tree model, its construction and the queries over it), element-call.ts (calling a function on a resolved element, and the PageResult it returns), screenshot-capture.ts (capture, resize, crosshair overlay). Each tool file now holds its tools and its own presentation, nothing else. NavOutcome stops being exported; nothing outside navigate.ts read it. target.ts stays a module rather than folding into click.ts as planned: it is already helper-only and has its own test suite, and moving a tested pure function into a tool file is the thing this commit removes. Having one consumer is not the defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor: one door from a tool to the browseramankumarsingh77
Four routes coexisted: cdpCall, a raw client.session().call, a session held in a local and called directly, and client.evaluateJs — and five files mixed two or three of them. The CdpError-to-ToolErr mapping was hand-copied at nine sites, plus a private copy in isolated-tab.ts. domains/cdp-call.ts now owns all of it: cdpErrToToolErr, cdpCall, cdpCallOnTarget, cdpCallBrowser, evalJs. Every other file under src/domains goes through them, which also removes a pile of `if (!r.success) return err({ kind: "cdp_error", ... })` boilerplate in favour of returning the failed Result as-is. Two scanner rules keep it that way, scoped to src/domains/ so the layer that legitimately owns the session — cdp/ and client.ts — is unaffected. Scoping is new to the rule model; without it the rules fired on the transport itself. One deliberate behaviour change: a CDP timeout now surfaces as kind "timeout" rather than "cdp_error". isolated-tab.ts already did this and the other 13 sites flattened it; the shared mapper uses the honest label rather than preserving the mislabel. 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: delete exports and knobs that nothing callsamankumarsingh77
isInternalUrl, SPECIAL_KEYS, andThen and mapErr had no callers in src or test. BrowserState.remoteBrowserId was parsed on restore and never written — the field that is actually read lives on DaemonStatus. IpcServer.disconnectClient and IpcServer.clients() were unreachable, as was EventRouter.allClients and a write-only lineCount counter. DAEMON_STALE_SOCKET_CLEANUP was a hardcoded true with an if around it, so the unlink now runs unconditionally. CdpBridge.isAlive and getSessionOwner look equally dead from src/ alone, but test/manual/bridge-test.ts drives both — they are the only seams that suite has, so they stay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02feat!: remove the deep-research featureamankumarsingh77
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>
2026-08-02refactor: tidy the whitespace the comment removal left behindamankumarsingh77
Drops leading blank lines where a file header was removed, collapses double blank lines, and closes the empty catch/handler blocks whose only content had been a comment. Whitespace only. 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(domains): strip comment noiseamankumarsingh77
Removes module banners, step markers, and comments restating the code. Keeps one-line notes for safeJs's trust boundary, the select-match null sentinels, the TUI width clamp, the single-window tab rule, the input synthesis quirks (char events, keyCode, mousemove, focus, compositor), the stale AX value, ignored-wrapper recursion, and isolated-tab routing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(profile): strip comment noiseamankumarsingh77
Removes module banners and comments restating the code. Keeps one-line notes for the ProcessSingleton user-data-dir rule, the sentinel-URL and async-spawn-error quirks, the replicated getAgentDir, pi's tilde-expansion mirroring, the Windows wmic removal, the macOS/Linux process-identification quirks, the picker's undefined-means-cancel contract, and the deliberately unpersisted browserContextId. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(daemon): strip comment noiseamankumarsingh77
Removes module banners, section dividers, reference lists, and comments restating the code. Keeps one-line notes for the Windows named-pipe endpoint, the Windows unlink no-op, the do-not-unlink-on-rejection rule, the `.cmd` shell requirement, shell-quoting of spaced paths, and the in-place client promotion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): strip comment noiseamankumarsingh77
Removes module banners, section dividers, and comments restating the code. Keeps one-line notes for the window.open/browserContextId constraint, the seed-tab claim, the unverifiable-pin reuse rule, the SeedTargetProvider rationale, the deliberate dialog retention, the discovery-time browser quirks, and the guarded CDP decoder assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(util): strip comment noiseamankumarsingh77
Removes file-header banners and comments that restate the code. Keeps one-line notes at the optional-sharp dynamic import, the sharp .default probe, the `never` contravariance trick, the sanctioned renderCall cast, and safeJs's interpolation rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02style: drop the comments added by the bug-audit fixesamankumarsingh77
The audit fixes were required to add no comments; two doc blocks slipped through review. Removed, leaving only comments that predate this task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): report a page-side throw instead of blaming the refamankumarsingh77
runOnElement checked only whether Runtime.callFunctionOn reached Chrome, not whether the function it ran threw. A page-side exception left result.value undefined, the result guard rejected it, and browser_fill, browser_focus and browser_select_option all reported "No element matched: <ref>" for an element that was there — sending the agent to re-snapshot a ref that was fine and discarding the real error. The throw is now surfaced as cdp_error carrying the exception text, which is distinguishable from both the no-match and stale-ref paths; those still fire for a genuinely missing or stale element. The selector path already reported throws correctly via evaluateJs. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): one fill engine, so browser_fill stops lying about checkboxesamankumarsingh77
browser_fill and browser_fill_form carried separate in-page implementations that disagreed on the same element. The worst case was silent: browser_fill on a checkbox fired the HTMLInputElement value setter, which writes the submit value and leaves `checked` alone, then reported status ok — a no-op the agent had no reason to doubt. Both tools now share one engine, with browser_fill's documented <select> rejection kept as an explicit pre-check and per-tool focus behaviour kept as a parameter. Deliberate, user-approved behaviour changes: - browser_fill on a DISABLED input now returns an error instead of success. The old success was the bug; browser_fill_form already refused it. - browser_fill on a checkbox/radio now toggles `checked` instead of the value. - browser_fill_form on a contenteditable now uses execCommand("insertText"), so it drives ProseMirror/Slack-style editors as browser_fill already did. browser_fill keeps appending interactiveDiff and republishing refs; fill_form still does not. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f5 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): keep element refs stable across a mutationamankumarsingh77
interactiveDiff re-assigned refs by tree position after every fill, click and select, so any element appearing or disappearing shifted the numbering and a ref the agent was still holding silently resolved to a different element — while the tools kept promising that refs survive re-renders. Refs are now carried over by backendNodeId and only genuinely new elements draw a new number. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(tool): stop the socket gate rejecting every tool call on Windowsamankumarsingh77
The tool gate ran access() on DAEMON_SOCKET_PATH, which on Windows is the named pipe \\.\pipe\pi-browser-daemon. Named pipes are not filesystem entries, so access() always fails there and every browser_* call returned "Browser harness not initialized" no matter how healthy the daemon was. spawn.ts already skipped the identical check for this reason; that knowledge is now one shared daemonSocketMayExist() used by both call sites, with platform and path as parameters so the branch is testable off-platform. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): route background-tab events to the tab they belong toamankumarsingh77
The event loop dropped every event whose sessionId was not the active tab's, so the per-tab console and network buffers could only ever hold data captured while their tab was in the foreground. The filter now drops only sessions this instance never attached, and the dialog, frameNavigated and loadEventFired arms resolve their tab from the event's sessionId instead of assuming the active tab — without that, a background tab's dialog would have been recorded against the foreground one. Tabs are also registered before the domain-enable round trips, so events arriving during attach are no longer dropped as unknown. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f4 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): re-enable domains when re-attaching to a known tabamankumarsingh77
Every Target.attachToTarget mints a new sessionId and CDP domain enablement is per-session, but switchTo only enabled domains on a tab's first visit. Switching away and back left the refreshed session with Page, Network, Runtime and Log disabled, so that tab silently stopped collecting console and network records and stopped reporting dialogs and load events for the rest of the session. Report: docs/superpowers/reports/2026-07-26-bug-audit.md#f3 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-02fix(domains): stop fill_form misreporting select-by-label, let it failamankumarsingh77
Three review fixes on the restored fill_form: - FILL_FN matches a <select> option by value, label or text but returns el.value, so filling by visible label ("Canada" -> value "ca") tripped the read-back mismatch check and reported a correctly-set field as "Filled 0/1 fields; issues: ref e4 (value mismatch)". The mismatch test now also accepts res.text, which FILL_FN sets only for selects, so mismatch detection is preserved for every other field kind. - fill_form always returned ok(), so total failure surfaced as a successful result reading "Filled 0/4 fields". It now returns invalid_state when okCount is 0, carrying the per-field issue list as the error message. Partial success stays ok(). - Removed the two comment blocks this task had added to test/manual/forms-test.ts. Neither behaviour fix has automated coverage; the manual suite that would exercise them still needs a live Chrome and was not run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): restore fill_form and set_checked, drop the stale forkamankumarsingh77
domains/forms.ts was a stale fork of form.ts imported only by a manual test. It held two working tools the registry never registered: fill_form and set_checked. Ported both onto the current pattern and deleted the fork; form.ts versions of the shared tools win. The fork's tools took ref as Type.Integer (a raw CDP backendNodeId). browser_snapshot has since moved to string refs ('e7') resolved through the session ref map, so as written the tools were not callable at all. Ported them onto resolveRefToObjectId and Type.String refs; the only user-facing strings that changed are the four that documented the old [ref=N] syntax. All page-side JS is carried byte-for-byte. form.ts exceeded 250 lines, so fillForm moved to form-batch.ts per the single-field/batch split. form.ts is still 425 lines — the pre-existing three tools account for that, so the split does not reach the target. test/manual/forms-test.ts is repointed and adapted to publish an 'eN' ref map, but NOT executed — it needs a live Chrome. Type-checked only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(domains): share target resolution and CDP error mappingamankumarsingh77
The ref-vs-x/y branch turned out to exist in one tool, not four; it is now one helper with tests covering the half-specified-coordinate and stale-ref cases. cdpCall folds the repeated cdp_error mapping at 24 active-session call sites; target-scoped callOnTarget/callBrowser sites are left alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(tools): declare concurrency in the tool definitionamankumarsingh77
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>
2026-08-02refactor(cdp): validate event params against a typed tableamankumarsingh77
Malformed or unknown events are now dropped rather than hand-narrowed; consumers get typed params. Unknown methods are ignored, since the transport delivers every event Chrome emits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(domains): restore fail-closed focus check in browser_typeamankumarsingh77
Adopting isFocusProbe inverted the safety default: the guard was ANDed into the condition gating the check, so a probe result the guard rejected skipped the check and fell through to Input.insertText — text typed into nothing, silently, which is the exact failure the block exists to prevent. The guard now feeds a value that defaults to { ok: false }, so a rejected shape produces the error instead of bypassing it. The other four guard adoptions (isFillResult, isFocusResult, isSelectResult, isPageResult) were already fail-closed: each feeds a value whose rejection lands on a pre-existing error branch. Also assert x/y in the box centre test — the fields snapshot's off-viewport filter reads — and wrap the zero-size message to the surrounding width. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(domains): drop CDP casts, share the box-model unwrapamankumarsingh77
ref-resolve.ts and snapshot.ts had the same box-model unwrap, both using data.model! to work around an untyped response. Extracted to domains/box.ts; the typed schema makes the assertions unnecessary. Both derived the centre identically (quad corner + half the model width/height), so centreOf keeps that derivation; the shared zero-size rejection moved into boxOf, and snapshot keeps its own off-viewport (x/y < 0) filter. evalInIsolatedTab now goes through evaluateJson with the tab's own sessionId and a caller-supplied guard; both callers evaluate expressions ending in JSON.stringify, so they pass isJsonText rather than a check that checks nothing. Runtime.evaluate/callFunctionOn result casts in form.ts, forms.ts and keyboard.ts are replaced by named guards over the in-page result shapes. Runtime.callFunctionOn's params schema gained arguments/returnByValue/ awaitPromise/userGesture: four domain call sites legitimately pass them and typebox's additionalProperties:true does not widen the static type, so the params type was rejecting valid CDP fields. Params schemas are type-only (only results are compiled), so this is a compile-time widening with no runtime effect. tsc is green again for both tsconfigs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(profile): drop CDP casts in favour of the typed boundaryamankumarsingh77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02fix(cdp): let evaluateJson target a specific sessionamankumarsingh77
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-08-02test(cdp): cover call/callOnTarget/callBrowser through a stub transport; ↵amankumarsingh77
collapse decode into one path session-call-test.ts previously only exercised decodeResult (Task 3's surface, and mostly duplicating commands-test.ts) — call, callOnTarget, callBrowser, and the session's internal req helper had zero coverage. Add a stub CdpTransport (no socket, no browser) and test: a valid payload decodes to typed data, a garbage payload becomes invalid_response without throwing, a transport-level error passes through with its original kind (not relabeled), call/callOnTarget/callBrowser route the session id correctly, and opts.timeoutMs reaches the transport only when provided. Also collapse the four near-identical transport-then-decode bodies (req, call, callOnTarget, callBrowser) into one: the three public methods now delegate to req, which is the sole place unknown ends for every CDP call the session makes, internal or public. Public signatures are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02refactor(cdp): make session.call generic over the command tableamankumarsingh77
call/callOnTarget/callBrowser now take a known method and return its decoded result type. The session is where unknown ends; transport keeps returning unknown. Removes the casts in session.ts and target-factory.ts, including the session's own internal Target.*/Browser.* calls, which now decode through the same path as the public call methods. Also completes two params-schema gaps in commands.ts surfaced by this migration: Target.createTarget was missing openerId and Runtime.evaluate was missing userGesture, both real CDP fields src/cdp/ already sends. tsc is red project-wide (6 errors, all in src/domains/) — the sanctioned state between Tasks 4 and 7. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02feat(cdp): add typed command table with validated decodersamankumarsingh77
One typebox schema per CDP command is the source of both the TS types and the runtime validator, so they cannot drift. Malformed responses become invalid_response errors instead of TypeErrors several frames later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26fix(profile): resolve replaced binaries and surface real launch failuresamankumarsingh77
Two failures made a profile launch report the wrong cause. Linux appends " (deleted)" to /proc/<pid>/exe once the running binary is unlinked, which is what a google-chrome package upgrade does while the browser stays open. detectRunningBrowser passed that literal string on as the executable path, so every launch tried to spawn "/opt/google/chrome/chrome (deleted)" and hit ENOENT. Executable paths are now validated against disk, with the marker stripped and argv[0] used as a fallback; a binary genuinely named "… (deleted)" still wins. spawn() reports a missing or non-executable binary asynchronously via an 'error' event rather than throwing, and that event was discarded — so openProfileWindow returned success, no window opened, and the caller blamed the browser 15s later with a sentinel timeout. The launch now awaits 'spawn'/'error', returns the real cause, and removes its handshake page on the failure path. 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-13feat: support Brave + Chrome channels, fix stale DevToolsActivePort ↵Aman Kumar
discovery (#13) * feat: support Brave + Chrome channels, fix stale DevToolsActivePort discovery Broadens browser detection and hardens CDP endpoint discovery, based on the investigation and approach in #4 (Brave Beta on macOS with a leftover Chrome DevToolsActivePort file). Browser detection (src/setup.ts, src/cdp/discovery.ts): - checkChromeRunning() now recognizes Brave (macOS "brave browser", Linux brave/brave-browser, Windows brave.exe), alongside the existing Chrome/ Chromium/Edge detection. Also excludes gpu/updater sub-processes that can linger after a browser quits. - profileDirs() gains Brave (Stable/Beta/Nightly/Dev) and Chrome Beta/Dev/ Canary/Chromium profile paths across macOS, Linux, and Windows. Stale DevToolsActivePort handling (src/cdp/discovery.ts): Previously discovery trusted the first readable DevToolsActivePort file. A browser that has quit leaves its file behind, so if another browser later binds the same port (e.g. 9222), discovery returned a WS URL carrying the dead browser's UUID against the live browser's server, and the connection failed. discoverWsUrl() now: - collects all readable candidates instead of committing to the first; - when candidates share a port, keeps the most-recently-written file (mtime); - skips candidates whose port isn't live via a fast single-shot probe (no 30s waitForPort block on stale files); - asks each live browser for its canonical webSocketDebuggerUrl via /json/version, falling back to the file's WS path when that endpoint is disabled (some browsers only expose it via --remote-debugging-port); - falls back to probing well-known ports (9222, plus BU_CDP_PORTS) when no profile file is readable — covering sandboxed harnesses (EPERM/EACCES) and non-default install locations. Setup guidance updated to mention brave://inspect / edge://inspect and the --remote-debugging-port launch flag. Verified: typecheck clean; queryLiveWsUrl / mtime-dedup / fallback-port logic exercised against a mock /json/version server; checkChromeRunning() matches the single main process against live macOS ps output, excluding all helpers. Closes #4 Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com> * fix: harden discovery against malformed port files + stale-then-fallback Addresses two issues raised in review of the discovery rewrite: - Validate the port parsed from DevToolsActivePort before building a candidate. A truncated/corrupt file (non-numeric or out-of-range first line) yielded NaN, and net.connect throws ERR_SOCKET_BAD_PORT synchronously for NaN / <=0 / >=65536 — which escaped discoverWsUrl as an unhandled rejection instead of surfacing as a Result error. Such entries are now skipped. - Fall back to well-known ports (9222 / BU_CDP_PORTS) after all discovered candidates prove stale. Previously the fallback ran only when no port file was readable, so a leftover file for a quit browser caused a ~30s waitForPort block and failure without ever trying a live fallback port. Ports already covered by a candidate are skipped to avoid re-probing. --------- Co-authored-by: Trevor Whitney <trevorjwhitney@gmail.com>
2026-07-12fix: make browser daemon setup work on Windows (#7) (#12)Aman Kumar
The Unix socket daemon introduced in v0.6.0 made several POSIX-only assumptions that break setup on Windows: - protocol.ts hardcoded a Unix socket path (/tmp/pi-browser-daemon.sock). Node IPC on Windows requires a named pipe (\\.\pipe\<name>); a /tmp path is not a valid net listen/connect target there. - spawn.ts spawned the bare tsx binary / `npx` and probed the non-Windows .bin/tsx shim. On Windows npm binaries are .cmd shims that must run via a shell, and spawning them directly fails with EINVAL / ENOENT. - isDaemonRunning() pre-checked the socket with fs.access(), which always fails for a named pipe and would report a live daemon as dead. - stale-socket unlink() would throw on a named pipe path. Changes: - DAEMON_SOCKET_PATH is now platform-aware: named pipe on win32, Unix socket elsewhere. - spawnDaemon() resolves tsx.cmd / npx.cmd on Windows, runs via shell with per-token quoting (paths with spaces) and windowsVerbatimArguments. - isDaemonRunning() skips the fs.access pre-check on Windows and probes the pipe directly. - stale-socket cleanup is a no-op on Windows (named pipes self-clean). - setup.ts daemon-failure message uses the actual DAEMON_SOCKET_PATH. Closes #7
2026-07-12fix: remove browser footer status indicator (#10) (#11)Aman Kumar
The footer status chip set during session_start could go stale: after a successful /browser-setup, the setup command only called ctx.ui.notify() and never updated ctx.ui.setStatus("browser", ...), so the footer kept showing the red "Browser — run /browser-setup" nudge even though the browser was connected. Rather than syncing the status from the setup handler, remove the footer status indicator entirely. Browser control is on-demand and the setup command already reports its own outcome via notify(), so the persistent footer chip added no value and only introduced a stale-state bug. Closes #10
2026-07-08feat: add deep-research skill, /deep-research command, fix web-search-researcheramankumarsingh77
deep-research skill fans out to isolated web-search-researcher subagents (now using browser_web_search + browser_read_page instead of the non-existent web_search/web_fetch), coverage-driven loop with a hard ceiling, synthesizes a cited Markdown report. /deep-research command as explicit entry point. Docs updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08feat: add expand/collapse (Ctrl+O) rendering to web_search and read_pageamankumarsingh77
Both tools dumped full output into the TUI. Add a shared renderExpandableText helper (src/domains/render.ts) mirroring browser_execute_js: a compact preview by default, full body on expand via keyHint("app.tools.expand"). Tools stash a {summary, body, fullOutputPath} render payload in details; the full text still goes to the LLM via ToolOk.text. Also surfaces engine + result count in the web_search summary line (closes the S4 observability gap). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08feat: add browser_read_page reader-mode toolamankumarsingh77
URL (or owned targetId) → clean main-article text with nav/ads/footer boilerplate stripped, via a dependency-free readability heuristic. DOM walk lives in an in-page capture expression; scoring/selection is a pure, fixture- tested function (link-density + boilerplate-ancestor filtering, body-text fallback for structure-less pages). Reuses the isolated-attach lifecycle; registered unserialized (safe concurrent read). 10 extractor unit scenarios. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>