aboutsummaryrefslogtreecommitdiff
path: root/src/client.ts
diff options
context:
space:
mode:
authorAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-06-21 00:46:44 +0530
committerGitHub <noreply@github.com>2026-06-21 00:46:44 +0530
commitda5d31af999b728c31ab0169373531cab4543f0c (patch)
tree5d350ad1428ff332372a9c724ac46c964d8ca477 /src/client.ts
parent8db684d43366d335760e106eae39a74376bd7640 (diff)
release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init (#6)
* 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
Diffstat (limited to 'src/client.ts')
-rw-r--r--src/client.ts18
1 files changed, 16 insertions, 2 deletions
diff --git a/src/client.ts b/src/client.ts
index f0eb016..3c79f1a 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -11,6 +11,7 @@ import type { DaemonStatus, DialogInfo, PageInfo, TabInfo } from "./cdp/types";
export type BrowserClientOptions = {
readonly namespace: string;
+ readonly transport?: CdpTransport;
readonly remote?: { readonly cdpUrl: string; readonly browserId: string };
readonly initialOwnership?: {
readonly ownedTargetIds?: ReadonlyArray<string>;
@@ -28,6 +29,9 @@ export type BrowserClient = {
status(): DaemonStatus;
start(): Promise<Result<void, CdpError>>;
stop(): Promise<void>;
+ /** Detach from the current page target (removes the "Chrome is being controlled" banner)
+ * while keeping the transport connection alive. Call on session shutdown. */
+ detach(): Promise<void>;
evaluateJs(expression: string, sessionId?: string): Promise<Result<unknown, CdpError>>;
pageInfo(): Promise<Result<PageInfo | { readonly dialog: DialogInfo }, CdpError>>;
takeDialog(): DialogInfo | null;
@@ -77,7 +81,7 @@ const parsePageInfoPayload = (v: unknown): Result<PageInfo, CdpError> => {
};
export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient => {
- const transport = createCdpTransport();
+ const transport = opts.transport ?? createCdpTransport();
const ownershipInit: { ownedTargetIds?: ReadonlyArray<string>; harnessWindowTargetId?: string } = {};
if (opts.initialOwnership?.ownedTargetIds !== undefined) {
ownershipInit.ownedTargetIds = opts.initialOwnership.ownedTargetIds;
@@ -298,9 +302,19 @@ export const createBrowserClient = (opts: BrowserClientOptions): BrowserClient =
...(remote?.browserId !== undefined ? { remoteBrowserId: remote.browserId } : {}),
});
+ const detach = async (): Promise<void> => {
+ const cur = session.current();
+ if (!cur) return;
+ // Target.detachFromTarget removes the "Chrome is being controlled" banner
+ // and releases the page session. The transport (WebSocket/Unix socket)
+ // stays alive for reuse by the next session.
+ // Reference: https://chromedevtools.github.io/devtools-protocol/tot/Target/#method-detachFromTarget
+ await transport.request("Target.detachFromTarget", { sessionId: cur.sessionId }, { sessionId: null });
+ };
+
return {
namespace: opts.namespace,
- ensureAlive, status, start, stop,
+ ensureAlive, status, start, stop, detach,
evaluateJs, pageInfo,
takeDialog: () => session.takeDialog(),
listTabs, switchTab, newTab, closeTab,