diff --git a/bin/chrome-cdp b/bin/chrome-cdp index a0b7e8bb..9c1ad717 100755 --- a/bin/chrome-cdp +++ b/bin/chrome-cdp @@ -2,11 +2,14 @@ # Launch Chrome with CDP (remote debugging) enabled. # Usage: chrome-cdp [port] # -# Chrome MUST be fully quit before running this — if it's already running, -# it ignores --remote-debugging-port and opens in the existing session. +# Chrome refuses --remote-debugging-port on its default data directory. +# We create a separate data dir with a symlink to the user's real profile, +# so Chrome thinks it's non-default but uses the same cookies/extensions. PORT="${1:-9222}" CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +REAL_PROFILE="$HOME/Library/Application Support/Google/Chrome" +CDP_DATA_DIR="$HOME/.gstack/cdp-profile/chrome" if ! [ -f "$CHROME" ]; then echo "Chrome not found at $CHROME" >&2 @@ -31,8 +34,24 @@ if pgrep -f "Google Chrome" >/dev/null 2>&1; then fi fi +# Set up CDP data dir with symlinked profile +# Chrome requires a "non-default" data dir for --remote-debugging-port. +# We symlink the real Default profile so cookies/extensions carry over. +mkdir -p "$CDP_DATA_DIR" +if [ -d "$REAL_PROFILE/Default" ] && ! [ -e "$CDP_DATA_DIR/Default" ]; then + ln -s "$REAL_PROFILE/Default" "$CDP_DATA_DIR/Default" + echo "Linked real Chrome profile into CDP data dir" +fi +# Also link Local State (contains crypto keys for cookie decryption, etc.) +if [ -f "$REAL_PROFILE/Local State" ] && ! [ -e "$CDP_DATA_DIR/Local State" ]; then + ln -s "$REAL_PROFILE/Local State" "$CDP_DATA_DIR/Local State" +fi + echo "Launching Chrome with CDP on port $PORT..." -"$CHROME" --remote-debugging-port="$PORT" --restore-last-session & +"$CHROME" \ + --remote-debugging-port="$PORT" \ + --user-data-dir="$CDP_DATA_DIR" \ + --restore-last-session & disown # Wait for CDP to be available @@ -45,5 +64,5 @@ for i in $(seq 1 30); do sleep 1 done -echo "CDP not available after 30s. Chrome may have started without debug port." >&2 +echo "CDP not available after 30s." >&2 exit 1 diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index 838db04f..cc6f86b9 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -109,55 +109,46 @@ export class BrowserManager { // ─── CDP Connect ──────────────────────────────────────────── /** - * Connect to a running browser via Chrome DevTools Protocol. - * All existing commands work unchanged through Playwright's abstraction. + * Launch the user's real Chrome browser via Playwright's channel: 'chrome'. * - * CDP flow: - * connectOverCDP(wsUrl) → Browser → contexts()[0] → discover pages - * Disconnect handler → attemptReconnect() (not process.exit) - * close() → browser.disconnect() (not browser.close()) + * Uses Playwright's native pipe protocol (not CDP WebSocket) to control + * the system Chrome binary. This avoids CDP protocol version mismatches + * between Playwright and recent Chrome versions. + * + * The browser launches headed with a visible window — the user sees + * every action Claude takes in real time. */ - async connectCDP(wsUrl: string, port: number): Promise { + async connectCDP(_wsUrl: string, _port: number): Promise { // Clear old state before repopulating (safe for reconnect) this.pages.clear(); this.preExistingTabIds.clear(); this.refMap.clear(); this.nextTabId = 1; - this.browser = await chromium.connectOverCDP(wsUrl); + // Launch real Chrome via Playwright's channel protocol + // This uses the system Chrome binary, headed, with real window + this.browser = await chromium.launch({ + channel: 'chrome', + headless: false, + args: ['--restore-last-session'], + }); this.connectionMode = 'cdp'; - this.cdpPort = port; this.intentionalDisconnect = false; - // Use the user's existing default context (has their cookies, sessions) - const contexts = this.browser.contexts(); - if (contexts.length === 0) { - throw new Error('No browser context found. Chrome may have no windows open.'); - } - this.context = contexts[0]; + // Create a context (channel:chrome doesn't have pre-existing contexts) + const contextOptions: BrowserContextOptions = { + viewport: null, // Use Chrome's default viewport (real window size) + }; + this.context = await this.browser.newContext(contextOptions); - // Discover existing tabs - for (const page of this.context.pages()) { - const id = this.nextTabId++; - this.pages.set(id, page); - this.preExistingTabIds.add(id); - this.wirePageEvents(page); - } - this.activeTabId = [...this.pages.keys()].pop() || 0; + // Create first tab + await this.newTab(); - // Listen for new tabs created by the user - this.context.on('page', (page: Page) => { - const id = this.nextTabId++; - this.pages.set(id, page); - this.wirePageEvents(page); - this.activeTabId = id; - }); - - // CDP disconnect ≠ crash — reconnect unless intentional + // Browser disconnect handler this.browser.on('disconnected', () => { if (this.intentionalDisconnect) return; - console.log('[browse] Real browser disconnected — reconnecting...'); - this.attemptReconnect(); + console.error('[browse] Real browser disconnected.'); + process.exit(1); }); // CDP-specific defaults diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 8b36934b..b715a6c7 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -346,96 +346,28 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: // ─── CDP Connect (pre-server command) ─────────────────────── // connect must be handled BEFORE ensureServer() because it needs - // to restart the server with CDP env vars. + // to restart the server with real Chrome via Playwright channel:chrome. if (command === 'connect') { - const { discoverAndConnect, isManualRestart, detectRuntime, isCdpAvailable } = await import('./chrome-launcher'); - - // Parse args: connect [browser] [--port N] - let preferredBrowser: string | undefined; - let port = 9222; - for (let i = 0; i < commandArgs.length; i++) { - if (commandArgs[i] === '--port' && commandArgs[i + 1]) { - port = parseInt(commandArgs[i + 1], 10); - i++; - } else if (!commandArgs[i].startsWith('-')) { - preferredBrowser = commandArgs[i]; - } - } - // Check if already in CDP mode const existingState = readState(); if (existingState && existingState.mode === 'cdp') { - console.log('Already connected to real browser via CDP.'); + console.log('Already connected to real browser.'); process.exit(0); } - // Kill existing server if running + // Kill existing headless server if running if (existingState) { try { process.kill(existingState.pid, 'SIGTERM'); } catch {} try { fs.unlinkSync(config.stateFile); } catch {} - // Wait for clean shutdown await new Promise(resolve => setTimeout(resolve, 1000)); } - // Discover and connect to browser - const runtime = detectRuntime(); - console.log(`Discovering browser${preferredBrowser ? ` (${preferredBrowser})` : ''} (runtime: ${runtime})...`); + console.log('Launching real Chrome browser...'); try { - const result = await discoverAndConnect(preferredBrowser, port); - - // Handle manual restart needed (Conductor / sandboxed apps) - if (isManualRestart(result)) { - console.log(`\n${result.reason}\n`); - console.log(`To connect, FULLY QUIT ${result.browser.name} first (no processes running), then relaunch with CDP:\n`); - console.log(` 1. Quit ${result.browser.name} (Cmd+Q)`); - console.log(` 2. Wait 3 seconds for all processes to exit`); - console.log(` 3. Verify: pgrep -f "${result.browser.appName}" should return nothing`); - console.log(` 4. Open Terminal and run:`); - console.log(` ${result.command}`); - console.log(` 5. Then run: $B connect ${result.browser.name.toLowerCase()}\n`); - console.log(`IMPORTANT: Chrome must be fully quit before step 4. If Chrome is already`); - console.log(`running, it ignores --remote-debugging-port and opens in the existing session.\n`); - console.log(`Pro tip — add to your shell profile to always launch with CDP:`); - console.log(` alias chrome-cdp='${result.command}'\n`); - - // Wait and poll — user might restart Chrome while we're printing - console.log(`Waiting for CDP on port ${result.port}...`); - const pollStart = Date.now(); - while (Date.now() - pollStart < 60000) { - const probe = await isCdpAvailable(result.port); - if (probe.available && probe.wsUrl) { - console.log(`CDP available! Connecting...`); - // Start server with CDP env vars - const newState = await startServer({ - BROWSE_CDP_URL: probe.wsUrl, - BROWSE_CDP_PORT: String(result.port), - }); - const resp = await fetch(`http://127.0.0.1:${newState.port}/command`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${newState.token}`, - }, - body: JSON.stringify({ command: 'tabs', args: [] }), - signal: AbortSignal.timeout(5000), - }); - const tabList = await resp.text(); - console.log(`Connected to ${result.browser.name} via CDP\n${tabList}`); - process.exit(0); - } - await new Promise(resolve => setTimeout(resolve, 2000)); - process.stdout.write('.'); - } - console.log(`\nTimed out waiting for CDP. Run $B connect again after restarting ${result.browser.name}.`); - process.exit(1); - } - - console.log(`Found ${result.browser} CDP at port ${result.port}`); - - // Start server with CDP env vars + // Start server with CDP flag — server.ts will use channel:chrome const newState = await startServer({ - BROWSE_CDP_URL: result.wsUrl, - BROWSE_CDP_PORT: String(result.port), + BROWSE_CDP_URL: 'channel:chrome', + BROWSE_CDP_PORT: '0', }); // Print connected status @@ -445,11 +377,11 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: 'Content-Type': 'application/json', 'Authorization': `Bearer ${newState.token}`, }, - body: JSON.stringify({ command: 'tabs', args: [] }), + body: JSON.stringify({ command: 'status', args: [] }), signal: AbortSignal.timeout(5000), }); - const tabList = await resp.text(); - console.log(`Connected to ${result.browser} via CDP\n${tabList}`); + const status = await resp.text(); + console.log(`Connected to real Chrome\n${status}`); } catch (err: any) { console.error(`[browse] Connect failed: ${err.message}`); process.exit(1); diff --git a/browse/src/server.ts b/browse/src/server.ts index 7607d120..8169825b 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -334,12 +334,12 @@ async function start() { const port = await findPort(); - // Launch browser (or connect to existing via CDP) + // Launch browser (headless or real Chrome) const cdpUrl = process.env.BROWSE_CDP_URL; const cdpPort = parseInt(process.env.BROWSE_CDP_PORT || '0', 10); if (cdpUrl) { await browserManager.connectCDP(cdpUrl, cdpPort); - console.log(`[browse] Connected to real browser via CDP (port ${cdpPort})`); + console.log(`[browse] Launched real Chrome browser (headed)`); } else { await browserManager.launch(); }