feat: add --auto-connect flag to discover and connect to running Chrome (#432)
This commit is contained in:
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user