aboutsummaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
18 hourscastle-search: resolve Google links lazily, in Chrome, not eagerly over ↵HEADmasterIgor Soarez
plain HTTP The Node-side resolver sent one curl-like request per result with no cookies, JS or browser; Google answers that with its 'Verifying your request' countdown after a handful, and the testing itself triggered it. A click or direct navigation in Chrome redirects at once (measured). Search now leaves Google links wrapped and prints the breadcrumb Google shows for each. castle_cdp_resolve follows one wrapped link in a Chrome tab when the agent picks it, accepts only google.com/goto?url=<token>, and reports the verification page instead of waiting it out.
18 hourscastle-search: read Brave again, and report genuine zero-hit searches as suchIgor Soarez
Brave: #results no longer exists on the live page; fall back to #main/main. It was reported as 'no results area at all', i.e. a rate limit. DuckDuckGo: its 'No results found' notice is itself a .result, so an empty search looked like broken markup. emptyMarker lets the probe say the engine itself reported no matches, and the error tells the caller to loosen the query instead of switching engines.
18 hourscastle-search: read Google results again by resolving its /goto linksIgor Soarez
Google now wraps every result as google.com/goto?url=<opaque token>. The self-host filter discarded all of them, so every Google search failed with 'nothing could be parsed'. Accept /goto, resolve it node-side from the 302 Location, and when Google answers its 'Verifying your request' interstitial instead, keep the wrapped link and put the displayed breadcrumb in the snippet.
2026-10-17Start a local Chrome when the target is an explicit local and none answersIgor Soarez
Dedicated profile (~/.chrome-cdp-profile), macOS only, never for the castle default. BU_LOCAL_CHROME_LAUNCH=off disables it.
2026-10-15castle-search: pi-castle-cdp-search joins the fork as sz-chrome-remote-cdpIgor Soarez
2026-10-14README: install from git.soarez.net, not castleIgor Soarez
2026-08-04Write up what AI answer blocks would cost before building oneIgor Soarez
Google, Bing and Brave all put a generated answer above their results now, and none of it is being read. Investigated against castle's Chrome; nothing in src/ changed, because the cost turned out to be worth deciding on rather than absorbing. Two things make it expensive. Google's AI Overview exists only on the classic layout — udm=14 is the "Web" tab, and having no overview is part of what that tab is — so enabling it means giving back the clean page the extractor was built around. And all three stream in after the results are already parseable: Google settles ~2.3s in, Brave was still growing at 5s, against an extractor that returns at ~0.5s. The wait is often wasted, since the blocks are query-dependent — no Google overview for "how does postgres MVCC vacuum work" after 15s, no Bing answer for "who founded anthropic". Selectors, the traps behind them (#m-x-content briefly holds ~29k characters of unrelated page; Google's "Show more" hides only the disclaimer), the measured timings, three possible designs and their tradeoffs are all in the doc, so this does not have to be re-measured if the answer turns out to be yes.
2026-10-15fix(cdp): give the castle default 1.5s, not 5s, inside upstream's connect budgetIgor Soarez
2026-10-15feat(cdp): report a BU_CDP_WS pin in status, /browser-target and the promptIgor Soarez
Upstream 0.11.0 made BU_CDP_WS reach the daemon, and discoverEndpoint() returns it before the fork's target is consulted. resolveCdpTarget() did not know about it, so /browser-status and the system prompt named the BU_CDP_HTTP/stored/default target while the daemon dialled the pinned URL, and /browser-target claimed to switch browsers when it could not. resolveCdpTarget() now puts BU_CDP_WS first, as a remote target with source "ws". /browser-target warns that it takes precedence, as it already did for BU_CDP_HTTP. Only host:port is ever shown: the path carries the browser id and a proxied URL may carry a token. /browser-status does not probe a pinned URL, since /json/version answering says nothing about whether the pinned browser id is still current.
2026-10-14FORK.md: upstream 0.11 made BU_CDP_WS reach the daemonIgor Soarez
Upstream 0.11.0 moved BU_CDP_WS into discoverEndpoint(), so the claim that the daemon ignores it is no longer true. Say why BU_CDP_HTTP is still needed, where BU_CDP_WS sits in the precedence, and what to expect and run when taking upstream releases now that upstream has a boundary scanner and a daemon import-graph test.
2026-10-14FORK.md: rewrapIgor Soarez
2026-10-14FORK.md: hosted on git.soarez.net, not castleIgor Soarez
2026-10-14feat(prompt): say which Chrome browser_* drivesIgor Soarez
The prompt told the model it drives "the user's running instance". With this fork's default that is wrong: it is castle's shared Chrome, signed out of every account. It now matters which is which, because pi-chrome's chrome_* tools drive the user's signed-in Brave, and the model has to route between the two. getBrowserSystemPrompt() takes the resolved target and describes it: castle's shared browser for the built-in default, the user's own Chrome for local, the endpoint otherwise.
2026-10-14fix(profile): never prompt for a profile when driving a remote browserIgor Soarez
Profile selection works by launching the browser with --profile-directory and letting ProcessSingleton delegate to the running process, which only reaches a browser on this machine. discoverEndpoint() already withholds userDataDir for a remote target, but resolveUserDataDir() fell through to the local candidate dirs, so the picker offered this disk's Chromium profiles for a browser on another host - on castle, Brave's profiles for a Chrome on :9223. Pinning one launched a local browser and polled the remote for a sentinel that could never appear, failing after the seed deadline; seedProfileWindow()'s mismatch guard missed it because connectedDir is undefined on the remote path. Setup now returns early instead of prompting - the question had no actionable answer, and since cancelling stores nothing it returned every session. /browser-profile and seedProfileWindow() refuse with the reason, the latter catching a pin left over from a local session.
2026-10-14docs: castle restarts Chrome on failure, not hourlyIgor Soarez
2026-10-14fix(setup): skip local-browser precondition for every remote targetIgor Soarez
The pre-flight check only skipped isBrowserRunning() when BU_CDP_HTTP was set, so a machine with no local browser failed /browser-setup with 'No browser instance running' before discovery ever dialed the built-in castle default. Resolve the target the same way discovery does and skip the local-process check whenever it resolves remote — env var, /browser-target choice, or default. A resolved local target still requires a local browser, and an unreachable remote still fails loudly in step 4.
2026-10-14docs(readpage): flag SPA-shell results so models don't trust them as the pageIgor Soarez
2026-10-14docs: /browser-target, and why switching restarts the daemonIgor Soarez
2026-10-14feat(cdp): /browser-target to switch browsers without leaving piIgor Soarez
Switching between the shared browser and this machine's own previously meant leaving pi and relaunching with BU_CDP_HTTP — and, less obviously, killing the daemon first, or the switch silently did not happen. /browser-target show the current target /browser-target local this machine's browser /browser-target default the built-in default /browser-target host:port somewhere else Precedence becomes env > stored > default. The environment still wins so a one-off `BU_CDP_HTTP=… pi` is never silently overridden by a choice made days ago; when it is set, the command says so rather than pretending to obey. Clearing falls back to the default rather than to local — "use the usual browser", not "go local". The daemon is the reason this needed more than a settings write. It is machine-wide, resolves its target once per process, and holds the WebSocket to the old browser, so re-reading the store would leave tool calls going to the old browser while /browser-status reported the new one. The command therefore replaces the process: the daemon now honours an inbound control/shutdown (the action already existed in the protocol, previously only sent daemon→client), and stopDaemon() asks for it and polls until the process is really gone before ensureDaemon() respawns it against the new target. /browser-status now reads the store too, so it cannot disagree with what the tools will do. Persistence mirrors the profile pin: atomic temp-file-plus-rename, and every read failure degrades to "nothing stored" so a corrupt file falls back to the default instead of breaking browser control. It is a separate file from the pin, so damage to one cannot cost you the other. The stored value stays in BU_CDP_HTTP's vocabulary so there is exactly one resolution path. 62 tests: the full precedence table including the stored tier, that a bad value is blamed on the source that set it rather than sending you hunting through shell config, that the command only accepts values resolution can actually use, and eight ways of damaging the store file.
2026-10-14docs: castle is now the default; document the override tableIgor Soarez
BU_CDP_HTTP is an override rather than a requirement, and /browser-status explains the lazy connection instead of showing a bare red light.
2026-10-14feat(cdp): default to the shared browser, and say so in /browser-statusIgor Soarez
Two follow-ups to BU_CDP_HTTP. Default the endpoint. The remote host is now a built-in default (DEFAULT_REMOTE_CDP), so nothing has to export BU_CDP_HTTP just to get the usual setup. The variable becomes an override rather than a requirement. Precedence is deliberately asymmetric, in src/cdp/endpoint.ts: BU_CDP_HTTP=host:port that endpoint; unreachable is a hard error BU_CDP_HTTP=local|off upstream behaviour, this machine's own browser unset the default, falling back to local when unreachable An explicitly configured endpoint is an instruction, so failing to reach it must not quietly automate a different browser - that would drive the operator's own signed-in Chrome instead of the shared one. The built-in default is only a preference, so a laptop away from the network still gets the local browser the extension was originally written for. Resolution is pure, with no I/O, so the status command can report the configured intent without touching the network and the daemon can resolve the same way in its own process. /browser-status no longer reports a bare "Disconnected". The connection is lazy - nothing dials the browser until the first browser_* tool runs - so a fresh session always showed a red light that reads as a fault. It now distinguishes "not connected yet, connects on first use" from "target unreachable", and names the target with a reachability check, so the common confusion answers itself. 26 tests for the resolution table, covering the aliases, malformed values that must be rejected rather than coerced, and that the default constant is itself well-formed - if it ever stopped parsing, every machine would silently fall back to local.
2026-10-14docs: fork notes — why BU_CDP_HTTP exists, install, upstream mergesIgor Soarez
Kept out of README.md so upstream's own README continues to merge cleanly.
2026-10-14feat(cdp): BU_CDP_HTTP to attach to a browser on another hostIgor Soarez
Upstream can only drive a browser on localhost. The pi extension always goes through the daemon (src/index.ts uses createDaemonTransport), the daemon resolves Chrome with discoverWsUrl() (src/daemon/bridge.ts), and that probe is hardcoded to 127.0.0.1 (src/cdp/discovery.ts). BU_CDP_WS is read only by the in-process client (src/client.ts), which that path never touches, so it has no effect on the extension - the only workaround is relaying the remote browser onto localhost first. BU_CDP_HTTP="host:port" points at the remote CDP HTTP endpoint. It is resolved inside discoverEndpoint(), so the daemon and the client both honour it. Deliberately an HTTP endpoint rather than a WebSocket URL: it is re-resolved on every discovery call, so the browser UUID is re-read after the remote browser restarts. Chrome re-mints that UUID on every launch, so a pinned WebSocket URL goes stale as soon as the remote browser is recycled. Also skips performSetup's local-process precondition when BU_CDP_HTTP is set: isBrowserRunning() enumerates local processes and can never see a remote browser. Step 4 still fails loudly if it is unreachable. No userDataDir is returned for remote endpoints, since the profile registry lives on the remote host - profile enumeration degrades to the pre-profile behaviour rather than reading the wrong machine's Local State.
2026-10-12fix(daemon): let pi exit after a session has connected to the daemon (#40)Aman Kumar
The client's daemon socket is kept open across sessions by design; in pi -p it was the last handle keeping Node alive, so a run that had connected to the browser hung after its answer. Unref the socket; in-flight requests hold their own timers. Fixes #39 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12fix: browser_download no longer hijacks the user's own downloads (#33)Uttkarsh
Browser.setDownloadBehavior was sent without a browserContextId, so the override applied to the user's default context and outlived the session. browser_download now takes restore:true, tracks the override this process set, and a clean session shutdown restores Chrome's default with a bounded call that is skipped when the browser is already gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11refactor: remove duplicate browser prompt instructions (#35)Patrick Barattin
Remove browser instruction text repeated across tool descriptions, parameter schemas, tool guidelines and the global browser prompt. Tool names, schemas, handlers and concurrency are unchanged; shared recovery and ownership guidance is now available before and after the browser connects, and the prompt names which tools are serialized versus observations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11test: keep host-provided packages out of dependencies (#36)Uttkarsh
Adds a manifest guard (scripts/check-manifest.ts + test/util/manifest-test.ts) so npm run check fails if a host-provided package such as typebox lands in dependencies, plus a daemon-startup troubleshooting section in the skill. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11fix(daemon): stop importing typebox in the daemon process (#38)Aman Kumar
pi installs packages with dev and peer dependencies omitted and supplies typebox only to the extension host. The daemon is a separate Node process whose import graph reached typebox via src/cdp/discovery.ts, so from a pi-managed install it crashed with "Cannot find module 'typebox'" before binding its socket. Parse /json/version with the package's own guards, and add a test that keeps every package reachable from the daemon a runtime dependency. Fixes #34 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11fix: append browser state to the session only when it changes (#37)Aman Kumar
persistState appended a browser-harness-state entry on every ownership callback, session_tree and shutdown, so the append-only session file and Pi Web's "+N custom" chain grew without bound. A snapshot is now written only when it differs from the last one persisted or restored. Fixes #26 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11fix(daemon): resolve tsx from the filesystem when Node's resolver cannot see ↵PhysicistLeon
node_modules (#28) Inside a Bun-compiled pi binary (pi.exe), createRequire().resolve("tsx/cli") is rooted at the embedded virtual filesystem and cannot see the extension's real node_modules, so browser_setup failed instantly. The resolver result is now checked on disk and, when it fails, the tsx CLI is found by walking up the node_modules chain. Under Bun the daemon runs on `node` from PATH (process.execPath is the pi binary itself); PI_BROWSER_NODE overrides it. Fixes #27 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09feat: add per-tab request headers (#30)astarring
Expose explicit set and clear tools so environment-routed pages can be opened without leaking header values or carrying headers across unrelated browsing. Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com Co-authored-by: zouzhigang <zouzhigang@bytedance.com>
2026-08-13chore(release): 0.11.1 (#24)Uttkarsh
2026-08-06fix: accept Windows script paths and start the daemon from a real install (#23)Aman Kumar
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.
2026-08-06fix: reconnect to Chrome only on explicit browser demand (#18)Uttkarsh
* fix: reconnect to Chrome only on browser demand * fix(daemon): gate stale CDP bridge reconnect attempts * Harden bridge reconnect lifecycle against stale socket callbacks * fix(daemon): handle stale attempt failures without deferred settlement * test: align waitFor assertions and stale-socket case labels * fix(daemon): re-check socket after connect wait in handleRequest Restores the post-wait re-check from the original PR branch (dropped during the main rebase merge); TypeScript needs the local narrowing before send. * fix(daemon): reject in-flight requests when an open connection drops `failCurrentAttempt` delegated callback rejection to `settleAttempt`, but an attempt that reached `open` had already settled, so the close path returned early and never answered its in-flight requests. They hung until the 10s command timeout instead of failing immediately, which the on-demand reconnect change had promised to preserve. Reject the attempt's callbacks directly in `failCurrentAttempt`, before the close handler notifies clients. Move the connection-lifecycle tests out of test/manual/ — which nothing runs — into test/daemon/, where `npm test` picks them up, and convert the hand-rolled pass/fail counters to node:test. Adds a case covering the drop path above; it fails without the fix. --------- Co-authored-by: Hermes Subagent <hermes-subagent@example.com> Co-authored-by: Hermes Agent <agent@example.com> Co-authored-by: uttkarsh-26 <uttkarsh@users.noreply.github.com> Co-authored-by: amankumarsingh77 <amankumarsingh7702@gmail.com>
2026-08-02chore: remove the .rpiv folderamankumarsingh77
Four planning artifacts from the per-tab-isolation and multi-agent-safety work, all of which has since landed. Ignored going forward so local .rpiv scratch stays out of the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02docs: remove the docs/ folderamankumarsingh77
The plans, specs, and audit reports under docs/superpowers/ described work that has since landed, and ARCHITECTURE.md duplicated what the boundary scanner and the tool contract already enforce in code. CONTRIBUTING.md now points at the source that enforces each rule instead of at prose about it. .gitignore keeps docs/ ignored so local scratch docs stay out of the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02chore(release): 0.11.0amankumarsingh77
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>
2026-08-02chore: ignore .worktrees/ and .harness/ pipeline artifactsamankumarsingh77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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-02docs: record the new contract and the two knobs that now workamankumarsingh77
ARCHITECTURE.md: the layer map gains domains/cdp-call.ts as the single door and the helper-module/tool-module split; the "cdpCall vs session.call" section is replaced, since snapshot.ts no longer mixes styles — it swallows failures through cdpCall instead of around it. Rule 1 now says tool files export only tools, rule 7 names the enforced scanner rules, and the two 250-line exceptions are recorded as largely settled (snapshot 477 to 256, form 409 to 283). Also notes the one honest gap: session.attach and session.windowId perform CDP calls the rules do not match. README: BU_CDP_WS carries its daemon-restart caveat, BH_DEBUG_CLICKS is documented as the flag's equivalent, and the tool count matches the registry (38 to 40). 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-02docs: warn that the bug-audit line numbers predate the comment stripamankumarsingh77
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>