95675e9d55
* 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>
198 lines
6.1 KiB
Rust
198 lines
6.1 KiB
Rust
use std::env;
|
|
|
|
pub struct Flags {
|
|
pub json: bool,
|
|
pub full: bool,
|
|
pub headed: bool,
|
|
pub debug: bool,
|
|
pub session: String,
|
|
pub headers: Option<String>,
|
|
pub executable_path: Option<String>,
|
|
pub cdp: Option<String>,
|
|
}
|
|
|
|
pub fn parse_flags(args: &[String]) -> Flags {
|
|
let mut flags = Flags {
|
|
json: false,
|
|
full: false,
|
|
headed: false,
|
|
debug: false,
|
|
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;
|
|
while i < args.len() {
|
|
match args[i].as_str() {
|
|
"--json" => flags.json = true,
|
|
"--full" | "-f" => flags.full = true,
|
|
"--headed" => flags.headed = true,
|
|
"--debug" => flags.debug = true,
|
|
"--session" => {
|
|
if let Some(s) = args.get(i + 1) {
|
|
flags.session = s.clone();
|
|
i += 1;
|
|
}
|
|
}
|
|
"--headers" => {
|
|
if let Some(h) = args.get(i + 1) {
|
|
flags.headers = Some(h.clone());
|
|
i += 1;
|
|
}
|
|
}
|
|
"--executable-path" => {
|
|
if let Some(s) = args.get(i + 1) {
|
|
flags.executable_path = Some(s.clone());
|
|
i += 1;
|
|
}
|
|
}
|
|
"--cdp" => {
|
|
if let Some(s) = args.get(i + 1) {
|
|
flags.cdp = Some(s.clone());
|
|
i += 1;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
i += 1;
|
|
}
|
|
flags
|
|
}
|
|
|
|
pub fn clean_args(args: &[String]) -> Vec<String> {
|
|
let mut result = Vec::new();
|
|
let mut skip_next = false;
|
|
|
|
// 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", "--cdp"];
|
|
|
|
for arg in args.iter() {
|
|
if skip_next {
|
|
skip_next = false;
|
|
continue;
|
|
}
|
|
if GLOBAL_FLAGS_WITH_VALUE.contains(&arg.as_str()) {
|
|
skip_next = true;
|
|
continue;
|
|
}
|
|
// Only strip known global flags, not command-specific flags
|
|
if GLOBAL_FLAGS.contains(&arg.as_str()) || arg == "-f" {
|
|
continue;
|
|
}
|
|
result.push(arg.clone());
|
|
}
|
|
result
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn args(s: &str) -> Vec<String> {
|
|
s.split_whitespace().map(String::from).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_headers_flag() {
|
|
let flags = parse_flags(&args(r#"open example.com --headers {"Auth":"token"}"#));
|
|
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_headers_flag_with_spaces() {
|
|
// Headers JSON is passed as a single quoted argument in shell
|
|
let input: Vec<String> = vec![
|
|
"open".to_string(),
|
|
"example.com".to_string(),
|
|
"--headers".to_string(),
|
|
r#"{"Authorization": "Bearer token"}"#.to_string(),
|
|
];
|
|
let flags = parse_flags(&input);
|
|
assert_eq!(flags.headers, Some(r#"{"Authorization": "Bearer token"}"#.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_no_headers_flag() {
|
|
let flags = parse_flags(&args("open example.com"));
|
|
assert!(flags.headers.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_clean_args_removes_headers() {
|
|
let input: Vec<String> = vec![
|
|
"open".to_string(),
|
|
"example.com".to_string(),
|
|
"--headers".to_string(),
|
|
r#"{"Auth":"token"}"#.to_string(),
|
|
];
|
|
let clean = clean_args(&input);
|
|
assert_eq!(clean, vec!["open", "example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clean_args_removes_headers_at_start() {
|
|
let input: Vec<String> = vec![
|
|
"--headers".to_string(),
|
|
r#"{"Auth":"token"}"#.to_string(),
|
|
"open".to_string(),
|
|
"example.com".to_string(),
|
|
];
|
|
let clean = clean_args(&input);
|
|
assert_eq!(clean, vec!["open", "example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_headers_with_other_flags() {
|
|
let input: Vec<String> = vec![
|
|
"open".to_string(),
|
|
"example.com".to_string(),
|
|
"--headers".to_string(),
|
|
r#"{"Auth":"token"}"#.to_string(),
|
|
"--json".to_string(),
|
|
"--headed".to_string(),
|
|
];
|
|
let flags = parse_flags(&input);
|
|
assert_eq!(flags.headers, Some(r#"{"Auth":"token"}"#.to_string()));
|
|
assert!(flags.json);
|
|
assert!(flags.headed);
|
|
|
|
let clean = clean_args(&input);
|
|
assert_eq!(clean, vec!["open", "example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_executable_path_flag() {
|
|
let flags = parse_flags(&args("--executable-path /path/to/chromium open example.com"));
|
|
assert_eq!(flags.executable_path, Some("/path/to/chromium".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_executable_path_flag_no_value() {
|
|
let flags = parse_flags(&args("--executable-path"));
|
|
assert_eq!(flags.executable_path, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clean_args_removes_executable_path() {
|
|
let cleaned = clean_args(&args("--executable-path /path/to/chromium open example.com"));
|
|
assert_eq!(cleaned, vec!["open", "example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clean_args_removes_executable_path_with_other_flags() {
|
|
let cleaned = clean_args(&args("--json --executable-path /path/to/chromium --headed open example.com"));
|
|
assert_eq!(cleaned, vec!["open", "example.com"]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_flags_with_session_and_executable_path() {
|
|
let flags = parse_flags(&args("--session test --executable-path /custom/chrome open example.com"));
|
|
assert_eq!(flags.session, "test");
|
|
assert_eq!(flags.executable_path, Some("/custom/chrome".to_string()));
|
|
}
|
|
}
|