aboutsummaryrefslogtreecommitdiff
path: root/src/setup.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/setup.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/setup.ts')
-rw-r--r--src/setup.ts84
1 files changed, 45 insertions, 39 deletions
diff --git a/src/setup.ts b/src/setup.ts
index 3909603..561a343 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -1,37 +1,50 @@
/**
- * Setup command — verifies Chrome is running with remote debugging
- * and connects the pi agent to it.
+ * Setup — verifies Chrome is running with remote debugging and connects
+ * the pi agent to it. Exposed as both a slash command (/browser-setup) for
+ * users and a tool (browser_setup) for the agent to self-recover.
*/
-import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { execSync } from "node:child_process";
import type { BrowserClient } from "./client";
+import { ensureDaemon } from "./daemon/spawn";
+
+// ── Public: register the /browser-setup slash command ──────────────────────
export function registerSetupCommand(pi: ExtensionAPI, client: BrowserClient): void {
pi.registerCommand("browser-setup", {
description: "Connect pi to your Chrome browser",
handler: async (_args, ctx) => {
- await runSetup(ctx, client);
+ const result = await performSetup(client);
+ if (result.success) {
+ ctx.ui.notify(result.data, "info");
+ } else {
+ ctx.ui.notify(result.error, "error");
+ }
},
});
}
-async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<void> {
- ctx.ui.notify("browser setup: checking Chrome...", "info");
+// ── Shared setup result ────────────────────────────────────────────────────
+
+export type SetupResult = { success: true; data: string } | { success: false; error: string };
+
+// ── Shared setup logic (used by both the command and the tool) ─────────────
+export async function performSetup(client: BrowserClient): Promise<SetupResult> {
// Step 1: Check Chrome is running
const chromeRunning = checkChromeRunning();
if (!chromeRunning) {
- ctx.ui.notify(
- "No browser instance running. Please open your browser and then run /browser-setup.",
- "error",
- );
- return;
+ return { success: false, error: "No browser instance running. Please open your browser and then run /browser-setup." };
}
- ctx.ui.notify("Chrome is running ✓", "info");
- // Step 2: Try to connect to Chrome DevTools
- ctx.ui.notify("Connecting to Chrome DevTools...", "info");
+ // Step 2: Start the browser daemon (spawns if not running, silently reuses if alive)
+ const daemonReady = await ensureDaemon();
+ if (!daemonReady) {
+ return { success: false, error: "Could not start the browser daemon. Check /tmp/pi-browser-daemon.sock." };
+ }
+
+ // Step 3: Connect to Chrome DevTools
const startResult = await client.start();
if (!startResult.success) {
const msg = startResult.error.message;
@@ -43,30 +56,23 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
lower.includes("econnrefused") ||
lower.includes("cannot reach chrome devtools")
) {
- ctx.ui.notify(
- "Chrome remote debugging needs to be enabled.\n\n" +
- "Open chrome://inspect/#remote-debugging in your browser, tick the\n" +
- "\"Discover network targets\" / Allow checkbox, then run /browser-setup again.\n\n" +
+ return {
+ success: false,
+ error:
+ "Chrome remote debugging needs to be enabled.\n\n" +
+ 'Open chrome://inspect/#remote-debugging in your browser, tick the\n' +
+ '"Discover network targets" / Allow checkbox, then retry.\n\n' +
"Or set BU_CDP_WS to a remote browser WebSocket URL.",
- "warning",
- );
- return;
+ };
}
- ctx.ui.notify(`Connection failed: ${msg}`, "error");
- return;
+ return { success: false, error: `Connection failed: ${msg}` };
}
- ctx.ui.notify("Connected to Chrome ✓", "info");
- // Step 3: Verify with test navigation
- ctx.ui.notify("Testing browser control...", "info");
+ // Step 4: Verify with test navigation
const tabResult = await client.newTab("https://github.com");
if (!tabResult.success) {
- ctx.ui.notify(
- `Browser connected but test navigation failed: ${tabResult.error.message}`,
- "warning",
- );
- return;
+ return { success: false, error: `Browser connected but test navigation failed: ${tabResult.error.message}` };
}
const info = await client.pageInfo();
@@ -75,24 +81,24 @@ async function runSetup(ctx: ExtensionContext, client: BrowserClient): Promise<v
}
const pageUrl = info.success && !("dialog" in info.data) ? info.data.url : "github.com";
- ctx.ui.notify(`Browser control verified ✓\nNavigated to: ${pageUrl}`, "info");
+ return { success: true, data: `Browser connected ✓\nNavigated to: ${pageUrl}` };
}
+// ── Chrome process detection ───────────────────────────────────────────────
+
function checkChromeRunning(): boolean {
try {
if (process.platform === "darwin") {
- // Exact line matching — excludes "Google Chrome Helper" etc.
- // that linger after the user quits the browser.
+ // ps -o comm= returns the full executable path on modern macOS.
+ // We check for "Google Chrome" anywhere in the path (case-insensitive),
+ // excluding helper/renderer subprocesses.
const out = execSync("ps -A -o comm=", { timeout: 5000 }).toString().toLowerCase();
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
- const browserProcesses = ["google chrome", "chromium", "microsoft edge"];
return lines.some((l) => {
- if (l.includes("helper") || l.includes("renderer")) return false;
- return browserProcesses.includes(l);
+ if (l.includes("helper") || l.includes("renderer") || l.includes("crashpad")) return false;
+ return l.includes("google chrome") || l.includes("chromium") || l.includes("microsoft edge");
});
} else if (process.platform === "linux") {
- // Use args to distinguish the main browser process from sub-processes
- // (GPU, renderer, utility, etc.) which carry --type= flags.
const out = execSync("ps -A -o comm=,args=", { timeout: 5000 }).toString().toLowerCase();
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
const browserComms = ["chrome", "chromium", "chromium-browser", "msedge", "microsoft-edge", "google-chrome"];