feat: add CDP connection support for external browsers (#24)

* feat: add CDP connection support for external browsers

Add --cdp flag to connect to browsers via Chrome DevTools Protocol.
This enables control of Electron apps, Chrome instances, or any browser
exposing a CDP endpoint.

- Add cdpPort option to launch command schema
- Implement connectViaCDP() using chromium.connectOverCDP()
- Track browser connection type for proper reconnection handling
- Collect all pages from all contexts for CDP connections

Usage: agent-browser --cdp 9222 snapshot

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: enhance CDP connection handling and improve page tracking

* main.rs update

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>

* fix: verify CDP connection is alive before early return in launch()

Prevents misleading errors when the remote browser crashes by checking
isConnected() before reusing an existing browser reference.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: reconnect when CDP port changes instead of reusing existing browser

Ensures --cdp flag is respected even when a browser session already exists.
Adds tests for launch() reconnection behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update src/browser.ts

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>

* fix: improve CDP connection handling and validation

* feat: add CDP connection validation to ensure browser context accessibility

* Update src/browser.ts

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>

* feat: enhance CDP connection handling and add reconnect logic

* fix: improve CDP connection handling during browser closure

* fix: reset cdpPort to null during browser initialization

* feat: enhance browser launch logic to handle CDP connection switching

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
This commit is contained in:
Alan Jeon
2026-01-13 00:52:47 -06:00
committed by GitHub
co-authored by Claude Opus 4.5 vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
parent 97b17c98fb
commit 95675e9d55
9 changed files with 265 additions and 19 deletions
+9 -1
View File
@@ -8,6 +8,7 @@ pub struct Flags {
pub session: String,
pub headers: Option<String>,
pub executable_path: Option<String>,
pub cdp: Option<String>,
}
pub fn parse_flags(args: &[String]) -> Flags {
@@ -19,6 +20,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
headers: None,
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
cdp: None,
};
let mut i = 0;
@@ -46,6 +48,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
i += 1;
}
}
"--cdp" => {
if let Some(s) = args.get(i + 1) {
flags.cdp = Some(s.clone());
i += 1;
}
}
_ => {}
}
i += 1;
@@ -60,7 +68,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
// Global flags that should be stripped from command args
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
// Global flags that take a value (need to skip the next arg too)
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path"];
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp"];
for arg in args.iter() {
if skip_next {
+64 -4
View File
@@ -168,12 +168,72 @@ fn main() {
}
}
// If --headed flag is set, send launch command first to switch to headed mode
if flags.headed {
let launch_cmd = json!({ "id": gen_id(), "action": "launch", "headless": false });
// Connect via CDP if --cdp flag is set
if let Some(ref port) = flags.cdp {
let cdp_port: u16 = match port.parse::<u32>() {
Ok(p) if p == 0 => {
let msg = "Invalid CDP port: port must be greater than 0".to_string();
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
Ok(p) if p > 65535 => {
let msg = format!("Invalid CDP port: {} is out of range (valid range: 1-65535)", p);
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
Ok(p) => p as u16,
Err(_) => {
let msg = format!("Invalid CDP port: '{}' is not a valid number. Port must be a number between 1 and 65535", port);
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
};
let launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"cdpPort": cdp_port
});
let err = match send_command(launch_cmd, &flags.session) {
Ok(resp) if resp.success => None,
Ok(resp) => Some(resp.error.unwrap_or_else(|| "CDP connection 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!("\x1b[31m✗\x1b[0m {}", msg);
}
exit(1);
}
}
// Launch headed browser if --headed flag is set (without CDP)
if flags.headed && flags.cdp.is_none() {
let launch_cmd = json!({
"id": gen_id(),
"action": "launch",
"headless": false
});
if let Err(e) = send_command(launch_cmd, &flags.session) {
if !flags.json {
eprintln!("\x1b[33m⚠\x1b[0m Could not switch to headed mode: {}", e);
eprintln!("\x1b[33m⚠\x1b[0m Could not launch headed browser: {}", e);
}
}
}
+2
View File
@@ -1194,6 +1194,7 @@ Options:
--json JSON output
--full, -f Full page screenshot
--headed Show browser window (not headless)
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--debug Debug output
Examples:
@@ -1204,6 +1205,7 @@ Examples:
agent-browser find role button click --name Submit
agent-browser get text @e1
agent-browser screenshot --full
agent-browser --cdp 9222 snapshot # Connect via CDP port
"#
);
}