From 307f970d538f1abeb9dad1b5fdfd75bcf9c8faec Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Jan 2026 08:38:53 -0600 Subject: [PATCH] fix: support WebSocket URLs in connect command (#205) * fix: support WebSocket URLs in connect command * address feedback --- cli/src/commands.rs | 147 +++++++++++++++++++++++++++++++++++++++++--- cli/src/main.rs | 1 + cli/src/output.rs | 42 ++++++++++++- 3 files changed, 182 insertions(+), 8 deletions(-) diff --git a/cli/src/commands.rs b/cli/src/commands.rs index d411b7b..3e18f60 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -17,6 +17,8 @@ pub enum ParseError { context: String, usage: &'static str, }, + /// Argument exists but has an invalid value + InvalidValue { message: String, usage: &'static str }, } impl ParseError { @@ -41,6 +43,9 @@ impl ParseError { context, usage ) } + ParseError::InvalidValue { message, usage } => { + format!("{}\nUsage: agent-browser {}", message, usage) + } } } } @@ -354,15 +359,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { - let port_str = rest.get(0).ok_or_else(|| ParseError::MissingArguments { + let endpoint = rest.first().ok_or_else(|| ParseError::MissingArguments { context: "connect".to_string(), - usage: "connect ", + usage: "connect ", })?; - let port: u16 = port_str.parse().map_err(|_| ParseError::MissingArguments { - context: format!("connect: invalid port '{}'", port_str), - usage: "connect ", - })?; - Ok(json!({ "id": id, "action": "launch", "cdpPort": port })) + // Check if it's a URL (ws://, wss://, http://, https://) + if endpoint.starts_with("ws://") + || endpoint.starts_with("wss://") + || endpoint.starts_with("http://") + || endpoint.starts_with("https://") + { + Ok(json!({ "id": id, "action": "launch", "cdpUrl": endpoint })) + } else { + // It's a port number - validate and use cdpPort field + let port: u16 = match endpoint.parse::() { + Ok(p) if p == 0 => { + return Err(ParseError::InvalidValue { + message: "Invalid port: port must be greater than 0".to_string(), + usage: "connect ", + }); + } + Ok(p) if p > 65535 => { + return Err(ParseError::InvalidValue { + message: format!( + "Invalid port: {} is out of range (valid range: 1-65535)", + p + ), + usage: "connect ", + }); + } + Ok(p) => p as u16, + Err(_) => { + return Err(ParseError::InvalidValue { + message: format!( + "Invalid value: '{}' is not a valid port number or URL", + endpoint + ), + usage: "connect ", + }); + } + }; + Ok(json!({ "id": id, "action": "launch", "cdpPort": port })) + } } // === Get === @@ -1713,4 +1751,99 @@ mod tests { assert_eq!(cmd["index"], 2); assert!(cmd.get("value").is_none()); } + + // === Connect (CDP) tests === + + #[test] + fn test_connect_with_port() { + let cmd = parse_command(&args("connect 9222"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpPort"], 9222); + assert!(cmd.get("cdpUrl").is_none()); + } + + #[test] + fn test_connect_with_ws_url() { + let input: Vec = vec![ + "connect".to_string(), + "ws://localhost:9222/devtools/browser/abc123".to_string(), + ]; + let cmd = parse_command(&input, &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpUrl"], "ws://localhost:9222/devtools/browser/abc123"); + assert!(cmd.get("cdpPort").is_none()); + } + + #[test] + fn test_connect_with_wss_url() { + let input: Vec = vec![ + "connect".to_string(), + "wss://remote-browser.example.com/cdp?token=xyz".to_string(), + ]; + let cmd = parse_command(&input, &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpUrl"], "wss://remote-browser.example.com/cdp?token=xyz"); + assert!(cmd.get("cdpPort").is_none()); + } + + #[test] + fn test_connect_with_http_url() { + let input: Vec = vec![ + "connect".to_string(), + "http://localhost:9222".to_string(), + ]; + let cmd = parse_command(&input, &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpUrl"], "http://localhost:9222"); + assert!(cmd.get("cdpPort").is_none()); + } + + #[test] + fn test_connect_missing_argument() { + let result = parse_command(&args("connect"), &default_flags()); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. })); + } + + #[test] + fn test_connect_invalid_port() { + let result = parse_command(&args("connect notanumber"), &default_flags()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, ParseError::InvalidValue { .. })); + assert!(err.format().contains("not a valid port number or URL")); + } + + #[test] + fn test_connect_port_zero() { + let result = parse_command(&args("connect 0"), &default_flags()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, ParseError::InvalidValue { .. })); + assert!(err.format().contains("port must be greater than 0")); + } + + #[test] + fn test_connect_port_out_of_range() { + let result = parse_command(&args("connect 65536"), &default_flags()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, ParseError::InvalidValue { .. })); + assert!(err.format().contains("out of range")); + assert!(err.format().contains("1-65535")); + } + + #[test] + fn test_connect_port_max_valid() { + let cmd = parse_command(&args("connect 65535"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpPort"], 65535); + } + + #[test] + fn test_connect_port_min_valid() { + let cmd = parse_command(&args("connect 1"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpPort"], 1); + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 7cf9816..7f3678b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -177,6 +177,7 @@ fn main() { ParseError::UnknownCommand { .. } => "unknown_command", ParseError::UnknownSubcommand { .. } => "unknown_subcommand", ParseError::MissingArguments { .. } => "missing_arguments", + ParseError::InvalidValue { .. } => "invalid_value", }; println!( r#"{{"success":false,"error":"{}","type":"{}"}}"#, diff --git a/cli/src/output.rs b/cli/src/output.rs index 7e0410c..714a2ae 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1317,6 +1317,46 @@ Examples: "## } + // === Connect === + "connect" => { + r##" +agent-browser connect - Connect to browser via CDP + +Usage: agent-browser connect + +Connects to a running browser instance via Chrome DevTools Protocol (CDP). +This allows controlling browsers, Electron apps, or remote browser services. + +Arguments: + Local port number (e.g., 9222) + Full WebSocket URL (ws://, wss://, http://, https://) + +Supported URL formats: + - Port number: 9222 (connects to http://localhost:9222) + - WebSocket URL: ws://localhost:9222/devtools/browser/... + - Remote service: wss://remote-browser.example.com/cdp?token=... + +Global Options: + --json Output as JSON + --session Use specific session + +Examples: + # Connect to local Chrome with remote debugging + # Start Chrome: google-chrome --remote-debugging-port=9222 + agent-browser connect 9222 + + # Connect using WebSocket URL from /json/version endpoint + agent-browser connect "ws://localhost:9222/devtools/browser/abc123" + + # Connect to remote browser service + agent-browser connect "wss://browser-service.example.com/cdp?token=xyz" + + # After connecting, run commands normally + agent-browser snapshot + agent-browser click @e1 +"## + } + _ => return false, }; println!("{}", help.trim()); @@ -1351,7 +1391,7 @@ Core Commands: pdf Save as PDF snapshot Accessibility tree with refs (for AI) eval Run JavaScript - connect Connect to browser via CDP (e.g., connect 9222) + connect Connect to browser via CDP close Close browser Navigation: