feat: add --auto-connect flag to discover and connect to running Chrome (#432)

This commit is contained in:
Chris Tate
2026-02-12 18:37:37 -06:00
committed by GitHub
parent 9c20979bfe
commit 9a01e8b3b5
12 changed files with 283 additions and 5 deletions
+13
View File
@@ -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.
<!-- opensrc:start -->
+23
View File
@@ -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 <port>` | 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.
+1
View File
@@ -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,
+4
View File
@@ -20,6 +20,7 @@ pub struct Flags {
pub ignore_https_errors: bool,
pub allow_file_access: bool,
pub device: Option<String>,
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<String> {
"--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] = &[
+53 -2
View File
@@ -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 {
+3
View File
@@ -1778,6 +1778,7 @@ Options:
--full, -f Full page screenshot
--headed Show browser window (not headless)
--cdp <port> 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):
+25
View File
@@ -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 <port\|url>` | CDP connection (port or WebSocket URL) |
| `--auto-connect` | Auto-discover and connect to running Chrome |
| `--debug` | Debug output |
## Cloud providers
+1
View File
@@ -168,6 +168,7 @@ agent-browser reload # Reload page
--profile <path> # Persistent browser profile directory
--headed # Show browser window (not headless)
--cdp <port> # Connect via Chrome DevTools Protocol
--auto-connect # Auto-discover and connect to running Chrome
--executable-path <path> # Custom browser executable
--args <args> # Browser launch args (comma separated)
--user-agent <ua> # Custom User-Agent string
+11
View File
@@ -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
+147 -3
View File
@@ -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<string | null> {
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<void> {
// 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
*/
+1
View File
@@ -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(),
+1
View File
@@ -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