diff --git a/AGENTS.md b/AGENTS.md index 2553856..9f92b07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,19 @@ Instructions for AI coding agents working with this codebase. - Do not use emojis in code, output, or documentation. Unicode symbols (✓, ✗, →, ⚠) are acceptable. - CLI colored output uses `cli/src/color.rs`. This module respects the `NO_COLOR` environment variable. Never use hardcoded ANSI color codes. +- CLI flags must always use kebab-case (e.g., `--auto-connect`, `--allow-file-access`). Never use camelCase for flags (e.g., `--autoConnect` is wrong). + +## Documentation + +When adding or changing user-facing features (new flags, commands, behaviors, environment variables, etc.), update **all** of the following: + +1. `cli/src/output.rs` -- `--help` output (flags list, examples, environment variables) +2. `README.md` -- Options table, relevant feature sections, examples +3. `skills/agent-browser/SKILL.md` -- so AI agents know about the feature +4. `docs/src/app/` -- the Next.js docs site (MDX pages) +5. Inline doc comments in the relevant source files + +This applies to changes that either human users or AI agents would need to know about. Do not skip any of these locations. diff --git a/README.md b/README.md index 49bc0d9..3b0c836 100644 --- a/README.md +++ b/README.md @@ -345,6 +345,7 @@ The `-C` flag is useful for modern web apps that use custom clickable elements ( | `--exact` | Exact text match | | `--headed` | Show browser window (not headless) | | `--cdp ` | Connect via Chrome DevTools Protocol | +| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) | | `--ignore-https-errors` | Ignore HTTPS certificate errors (useful for self-signed certs) | | `--allow-file-access` | Allow file:// URLs to access local files (Chromium only) | | `--debug` | Debug output | @@ -555,6 +556,28 @@ This enables control of: - WebView2 applications - Any browser exposing a CDP endpoint +### Auto-Connect + +Use `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port: + +```bash +# Auto-discover running Chrome with remote debugging +agent-browser --auto-connect open example.com +agent-browser --auto-connect snapshot + +# Or via environment variable +AGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot +``` + +Auto-connect discovers Chrome by: +1. Reading Chrome's `DevToolsActivePort` file from the default user data directory +2. Falling back to probing common debugging ports (9222, 9229) + +This is useful when: +- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port) +- You want a zero-configuration connection to your existing browser +- You don't want to track which port Chrome is using + ## Streaming (Browser Preview) Stream the browser viewport via WebSocket for live preview or "pair browsing" where a human can watch and interact alongside an AI agent. diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 8c52752..8cee854 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1429,6 +1429,7 @@ mod tests { ignore_https_errors: false, allow_file_access: false, device: None, + auto_connect: false, cli_executable_path: false, cli_extensions: false, cli_profile: false, diff --git a/cli/src/flags.rs b/cli/src/flags.rs index 7a92ab9..ca938cb 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -20,6 +20,7 @@ pub struct Flags { pub ignore_https_errors: bool, pub allow_file_access: bool, pub device: Option, + pub auto_connect: bool, // Track which launch-time options were explicitly passed via CLI // (as opposed to being set only via environment variables) @@ -65,6 +66,7 @@ pub fn parse_flags(args: &[String]) -> Flags { ignore_https_errors: false, allow_file_access: env::var("AGENT_BROWSER_ALLOW_FILE_ACCESS").is_ok(), device: env::var("AGENT_BROWSER_IOS_DEVICE").ok(), + auto_connect: env::var("AGENT_BROWSER_AUTO_CONNECT").is_ok(), // Track CLI-passed flags (default false, set to true when flag is passed) cli_executable_path: false, cli_extensions: false, @@ -175,6 +177,7 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--auto-connect" => flags.auto_connect = true, _ => {} } i += 1; @@ -194,6 +197,7 @@ pub fn clean_args(args: &[String]) -> Vec { "--debug", "--ignore-https-errors", "--allow-file-access", + "--auto-connect", ]; // Global flags that take a value (need to skip the next arg too) const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &[ diff --git a/cli/src/main.rs b/cli/src/main.rs index eb3d301..0bb3c83 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -289,7 +289,27 @@ fn main() { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, msg); } else { - eprintln!("\x1b[31m✗\x1b[0m {}", msg); + eprintln!("{} {}", color::error_indicator(), msg); + } + exit(1); + } + + if flags.auto_connect && flags.cdp.is_some() { + let msg = "Cannot use --auto-connect and --cdp together"; + if flags.json { + println!(r#"{{"success":false,"error":"{}"}}"#, msg); + } else { + eprintln!("{} {}", color::error_indicator(), msg); + } + exit(1); + } + + if flags.auto_connect && flags.provider.is_some() { + let msg = "Cannot use --auto-connect and -p/--provider together"; + if flags.json { + println!(r#"{{"success":false,"error":"{}"}}"#, msg); + } else { + eprintln!("{} {}", color::error_indicator(), msg); } exit(1); } @@ -299,11 +319,42 @@ fn main() { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, msg); } else { - eprintln!("\x1b[31m✗\x1b[0m {}", msg); + eprintln!("{} {}", color::error_indicator(), msg); } exit(1); } + // Auto-connect to existing browser + if flags.auto_connect { + let mut launch_cmd = json!({ + "id": gen_id(), + "action": "launch", + "autoConnect": true + }); + + if flags.ignore_https_errors { + launch_cmd["ignoreHTTPSErrors"] = json!(true); + } + + let err = match send_command(launch_cmd, &flags.session) { + Ok(resp) if resp.success => None, + Ok(resp) => Some( + resp.error + .unwrap_or_else(|| "Auto-connect failed".to_string()), + ), + Err(e) => Some(e.to_string()), + }; + + if let Some(msg) = err { + if flags.json { + println!(r#"{{"success":false,"error":"{}"}}"#, msg); + } else { + eprintln!("{} {}", color::error_indicator(), msg); + } + exit(1); + } + } + // Connect via CDP if --cdp flag is set // Accepts either a port number (e.g., "9222") or a full URL (e.g., "ws://..." or "wss://...") if let Some(ref cdp_value) = flags.cdp { diff --git a/cli/src/output.rs b/cli/src/output.rs index 249c05f..21378dd 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1778,6 +1778,7 @@ Options: --full, -f Full page screenshot --headed Show browser window (not headless) --cdp Connect via CDP (Chrome DevTools Protocol) + --auto-connect Auto-discover and connect to running Chrome --debug Debug output --version, -V Show version @@ -1785,6 +1786,7 @@ Environment: AGENT_BROWSER_SESSION Session name (default: "default") AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse) + AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223) AGENT_BROWSER_IOS_DEVICE Default iOS device name AGENT_BROWSER_IOS_UDID Default iOS device UDID @@ -1798,6 +1800,7 @@ Examples: agent-browser get text @e1 agent-browser screenshot --full agent-browser --cdp 9222 snapshot # Connect via CDP port + agent-browser --auto-connect snapshot # Auto-discover running Chrome agent-browser --profile ~/.myapp open example.com # Persistent profile iOS Simulator (requires Xcode and Appium): diff --git a/docs/src/app/cdp-mode/page.mdx b/docs/src/app/cdp-mode/page.mdx index 69435ac..0310355 100644 --- a/docs/src/app/cdp-mode/page.mdx +++ b/docs/src/app/cdp-mode/page.mdx @@ -34,6 +34,30 @@ The `--cdp` flag accepts either: - A port number (e.g., `9222`) for local connections via `http://localhost:{port}` - A full WebSocket URL (e.g., `wss://...` or `ws://...`) for remote browser services +## Auto-Connect + +Use `--auto-connect` to automatically discover and connect to a running Chrome instance without specifying a port: + +```bash +# Auto-discover running Chrome with remote debugging +agent-browser --auto-connect open example.com +agent-browser --auto-connect snapshot + +# Or via environment variable +AGENT_BROWSER_AUTO_CONNECT=1 agent-browser snapshot +``` + +Auto-connect discovers Chrome by: + +1. Reading Chrome's `DevToolsActivePort` file from the default user data directory +2. Falling back to probing common debugging ports (9222, 9229) + +This is useful when: + +- Chrome 144+ has remote debugging enabled via `chrome://inspect/#remote-debugging` (which uses a dynamic port) +- You want a zero-configuration connection to your existing browser +- You don't want to track which port Chrome is using + ## Use cases This enables control of: @@ -63,6 +87,7 @@ This enables control of: | `--exact` | Exact text match | | `--headed` | Show browser window | | `--cdp ` | CDP connection (port or WebSocket URL) | +| `--auto-connect` | Auto-discover and connect to running Chrome | | `--debug` | Debug output | ## Cloud providers diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index b06f1c6..3b75fba 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -168,6 +168,7 @@ agent-browser reload # Reload page --profile # Persistent browser profile directory --headed # Show browser window (not headless) --cdp # Connect via Chrome DevTools Protocol +--auto-connect # Auto-discover and connect to running Chrome --executable-path # Custom browser executable --args # Browser launch args (comma separated) --user-agent # Custom User-Agent string diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 88e2a95..f2c9565 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -122,6 +122,17 @@ agent-browser --session site2 snapshot -i agent-browser session list ``` +### Connect to Existing Chrome + +```bash +# Auto-discover running Chrome with remote debugging enabled +agent-browser --auto-connect open https://example.com +agent-browser --auto-connect snapshot + +# Or with explicit CDP port +agent-browser --cdp 9222 snapshot +``` + ### Visual Browser (Debugging) ```bash diff --git a/src/browser.ts b/src/browser.ts index 02f4532..0ee0456 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -16,7 +16,7 @@ import { } from 'playwright-core'; import path from 'node:path'; import os from 'node:os'; -import { existsSync, mkdirSync, rmSync } from 'node:fs'; +import { existsSync, mkdirSync, rmSync, readFileSync } from 'node:fs'; import type { LaunchCommand } from './types.js'; import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js'; @@ -1082,10 +1082,14 @@ export class BrowserManager { if (this.isLaunched()) { const needsRelaunch = - (!cdpEndpoint && this.cdpEndpoint !== null) || - (!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint)); + (!cdpEndpoint && !options.autoConnect && this.cdpEndpoint !== null) || + (!!cdpEndpoint && this.needsCdpReconnect(cdpEndpoint)) || + (!!options.autoConnect && !this.isCdpConnectionAlive()); if (needsRelaunch) { await this.close(); + } else if (options.autoConnect && this.isCdpConnectionAlive()) { + // Already connected via auto-connect, no need to reconnect + return; } else { return; } @@ -1096,6 +1100,11 @@ export class BrowserManager { return; } + if (options.autoConnect) { + await this.autoConnectViaCDP(); + return; + } + // Cloud browser providers require explicit opt-in via -p flag or AGENT_BROWSER_PROVIDER env var // -p flag takes precedence over env var const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER; @@ -1283,6 +1292,141 @@ export class BrowserManager { } } + /** + * Get Chrome's default user data directory paths for the current platform. + * Returns an array of candidate paths to check (stable, then beta/canary). + */ + private getChromeUserDataDirs(): string[] { + const home = os.homedir(); + const platform = os.platform(); + + if (platform === 'darwin') { + return [ + path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), + path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), + path.join(home, 'Library', 'Application Support', 'Chromium'), + ]; + } else if (platform === 'win32') { + const localAppData = process.env.LOCALAPPDATA ?? path.join(home, 'AppData', 'Local'); + return [ + path.join(localAppData, 'Google', 'Chrome', 'User Data'), + path.join(localAppData, 'Google', 'Chrome SxS', 'User Data'), + path.join(localAppData, 'Chromium', 'User Data'), + ]; + } else { + // Linux + return [ + path.join(home, '.config', 'google-chrome'), + path.join(home, '.config', 'google-chrome-unstable'), + path.join(home, '.config', 'chromium'), + ]; + } + } + + /** + * Try to read the DevToolsActivePort file from a Chrome user data directory. + * Returns { port, wsPath } if found, or null if not available. + */ + private readDevToolsActivePort(userDataDir: string): { port: number; wsPath: string } | null { + const filePath = path.join(userDataDir, 'DevToolsActivePort'); + try { + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, 'utf-8').trim(); + const lines = content.split('\n'); + if (lines.length < 2) return null; + + const port = parseInt(lines[0].trim(), 10); + const wsPath = lines[1].trim(); + + if (isNaN(port) || port <= 0 || port > 65535) return null; + if (!wsPath) return null; + + return { port, wsPath }; + } catch { + return null; + } + } + + /** + * Try to discover a Chrome CDP endpoint by querying an HTTP debug port. + * Returns the WebSocket debugger URL if available. + */ + private async probeDebugPort(port: number): Promise { + try { + const response = await fetch(`http://127.0.0.1:${port}/json/version`, { + signal: AbortSignal.timeout(2000), + }); + if (!response.ok) return null; + const data = (await response.json()) as { webSocketDebuggerUrl?: string }; + return data.webSocketDebuggerUrl ?? null; + } catch { + return null; + } + } + + /** + * Auto-discover and connect to a running Chrome/Chromium instance. + * + * Discovery strategy: + * 1. Read DevToolsActivePort from Chrome's default user data directories + * 2. If found, connect using the port and WebSocket path from that file + * 3. If not found, probe common debugging ports (9222, 9229) + * 4. If a port responds, connect via CDP + */ + private async autoConnectViaCDP(): Promise { + // Strategy 1: Check DevToolsActivePort files + const userDataDirs = this.getChromeUserDataDirs(); + for (const dir of userDataDirs) { + const activePort = this.readDevToolsActivePort(dir); + if (activePort) { + // Verify the port is actually responding + const wsUrl = await this.probeDebugPort(activePort.port); + if (wsUrl) { + // Connect using the discovered WebSocket URL + await this.connectViaCDP(wsUrl); + return; + } + // Port from file exists but not responding; try HTTP endpoint directly + const httpUrl = `http://127.0.0.1:${activePort.port}`; + try { + await this.connectViaCDP(httpUrl); + return; + } catch { + // Port listed but not connectable, try next directory + } + } + } + + // Strategy 2: Probe common debugging ports + const commonPorts = [9222, 9229]; + for (const port of commonPorts) { + const wsUrl = await this.probeDebugPort(port); + if (wsUrl) { + await this.connectViaCDP(wsUrl); + return; + } + } + + // Nothing found + const platform = os.platform(); + let hint: string; + if (platform === 'darwin') { + hint = + 'Start Chrome with: /Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --remote-debugging-port=9222\n' + + 'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging'; + } else if (platform === 'win32') { + hint = + 'Start Chrome with: chrome.exe --remote-debugging-port=9222\n' + + 'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging'; + } else { + hint = + 'Start Chrome with: google-chrome --remote-debugging-port=9222\n' + + 'Or enable remote debugging in Chrome 144+ at chrome://inspect/#remote-debugging'; + } + + throw new Error(`No running Chrome instance with remote debugging found.\n${hint}`); + } + /** * Set up console, error, and close tracking for a page */ diff --git a/src/protocol.ts b/src/protocol.ts index d787dc5..7e9cde8 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -31,6 +31,7 @@ const launchSchema = baseCommandSchema.extend({ { message: 'CDP URL must start with ws://, wss://, http://, or https://' } ) .optional(), + autoConnect: z.boolean().optional(), executablePath: z.string().optional(), extensions: z.array(z.string()).optional(), headers: z.record(z.string()).optional(), diff --git a/src/types.ts b/src/types.ts index 5ebea3f..d987517 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface LaunchCommand extends BaseCommand { executablePath?: string; cdpPort?: number; cdpUrl?: string; + autoConnect?: boolean; // Auto-discover and connect to running Chrome via DevToolsActivePort extensions?: string[]; profile?: string; // Path to persistent browser profile directory storageState?: string; // Path to storage state JSON file