fix: support WebSocket URLs in connect command (#205)

* fix: support WebSocket URLs in connect command

* address feedback
This commit is contained in:
Chris Tate
2026-01-22 08:38:53 -06:00
committed by GitHub
parent 36cca10c10
commit 307f970d53
3 changed files with 182 additions and 8 deletions
+140 -7
View File
@@ -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<Value, ParseError
// === Connect (CDP) ===
"connect" => {
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 <port>",
usage: "connect <port|url>",
})?;
let port: u16 = port_str.parse().map_err(|_| ParseError::MissingArguments {
context: format!("connect: invalid port '{}'", port_str),
usage: "connect <port>",
})?;
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::<u32>() {
Ok(p) if p == 0 => {
return Err(ParseError::InvalidValue {
message: "Invalid port: port must be greater than 0".to_string(),
usage: "connect <port|url>",
});
}
Ok(p) if p > 65535 => {
return Err(ParseError::InvalidValue {
message: format!(
"Invalid port: {} is out of range (valid range: 1-65535)",
p
),
usage: "connect <port|url>",
});
}
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 <port|url>",
});
}
};
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<String> = 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<String> = 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<String> = 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);
}
}
+1
View File
@@ -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":"{}"}}"#,
+41 -1
View File
@@ -1317,6 +1317,46 @@ Examples:
"##
}
// === Connect ===
"connect" => {
r##"
agent-browser connect - Connect to browser via CDP
Usage: agent-browser connect <port|url>
Connects to a running browser instance via Chrome DevTools Protocol (CDP).
This allows controlling browsers, Electron apps, or remote browser services.
Arguments:
<port> Local port number (e.g., 9222)
<url> 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 <name> 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 <path> Save as PDF
snapshot Accessibility tree with refs (for AI)
eval <js> Run JavaScript
connect <port> Connect to browser via CDP (e.g., connect 9222)
connect <port|url> Connect to browser via CDP
close Close browser
Navigation: