Skip to content

Commit 307f970

Browse files
authored
fix: support WebSocket URLs in connect command (#205)
* fix: support WebSocket URLs in connect command * address feedback
1 parent 36cca10 commit 307f970

3 files changed

Lines changed: 182 additions & 8 deletions

File tree

cli/src/commands.rs

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ pub enum ParseError {
1717
context: String,
1818
usage: &'static str,
1919
},
20+
/// Argument exists but has an invalid value
21+
InvalidValue { message: String, usage: &'static str },
2022
}
2123

2224
impl ParseError {
@@ -41,6 +43,9 @@ impl ParseError {
4143
context, usage
4244
)
4345
}
46+
ParseError::InvalidValue { message, usage } => {
47+
format!("{}\nUsage: agent-browser {}", message, usage)
48+
}
4449
}
4550
}
4651
}
@@ -354,15 +359,48 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
354359

355360
// === Connect (CDP) ===
356361
"connect" => {
357-
let port_str = rest.get(0).ok_or_else(|| ParseError::MissingArguments {
362+
let endpoint = rest.first().ok_or_else(|| ParseError::MissingArguments {
358363
context: "connect".to_string(),
359-
usage: "connect <port>",
360-
})?;
361-
let port: u16 = port_str.parse().map_err(|_| ParseError::MissingArguments {
362-
context: format!("connect: invalid port '{}'", port_str),
363-
usage: "connect <port>",
364+
usage: "connect <port|url>",
364365
})?;
365-
Ok(json!({ "id": id, "action": "launch", "cdpPort": port }))
366+
// Check if it's a URL (ws://, wss://, http://, https://)
367+
if endpoint.starts_with("ws://")
368+
|| endpoint.starts_with("wss://")
369+
|| endpoint.starts_with("http://")
370+
|| endpoint.starts_with("https://")
371+
{
372+
Ok(json!({ "id": id, "action": "launch", "cdpUrl": endpoint }))
373+
} else {
374+
// It's a port number - validate and use cdpPort field
375+
let port: u16 = match endpoint.parse::<u32>() {
376+
Ok(p) if p == 0 => {
377+
return Err(ParseError::InvalidValue {
378+
message: "Invalid port: port must be greater than 0".to_string(),
379+
usage: "connect <port|url>",
380+
});
381+
}
382+
Ok(p) if p > 65535 => {
383+
return Err(ParseError::InvalidValue {
384+
message: format!(
385+
"Invalid port: {} is out of range (valid range: 1-65535)",
386+
p
387+
),
388+
usage: "connect <port|url>",
389+
});
390+
}
391+
Ok(p) => p as u16,
392+
Err(_) => {
393+
return Err(ParseError::InvalidValue {
394+
message: format!(
395+
"Invalid value: '{}' is not a valid port number or URL",
396+
endpoint
397+
),
398+
usage: "connect <port|url>",
399+
});
400+
}
401+
};
402+
Ok(json!({ "id": id, "action": "launch", "cdpPort": port }))
403+
}
366404
}
367405

368406
// === Get ===
@@ -1713,4 +1751,99 @@ mod tests {
17131751
assert_eq!(cmd["index"], 2);
17141752
assert!(cmd.get("value").is_none());
17151753
}
1754+
1755+
// === Connect (CDP) tests ===
1756+
1757+
#[test]
1758+
fn test_connect_with_port() {
1759+
let cmd = parse_command(&args("connect 9222"), &default_flags()).unwrap();
1760+
assert_eq!(cmd["action"], "launch");
1761+
assert_eq!(cmd["cdpPort"], 9222);
1762+
assert!(cmd.get("cdpUrl").is_none());
1763+
}
1764+
1765+
#[test]
1766+
fn test_connect_with_ws_url() {
1767+
let input: Vec<String> = vec![
1768+
"connect".to_string(),
1769+
"ws://localhost:9222/devtools/browser/abc123".to_string(),
1770+
];
1771+
let cmd = parse_command(&input, &default_flags()).unwrap();
1772+
assert_eq!(cmd["action"], "launch");
1773+
assert_eq!(cmd["cdpUrl"], "ws://localhost:9222/devtools/browser/abc123");
1774+
assert!(cmd.get("cdpPort").is_none());
1775+
}
1776+
1777+
#[test]
1778+
fn test_connect_with_wss_url() {
1779+
let input: Vec<String> = vec![
1780+
"connect".to_string(),
1781+
"wss://remote-browser.example.com/cdp?token=xyz".to_string(),
1782+
];
1783+
let cmd = parse_command(&input, &default_flags()).unwrap();
1784+
assert_eq!(cmd["action"], "launch");
1785+
assert_eq!(cmd["cdpUrl"], "wss://remote-browser.example.com/cdp?token=xyz");
1786+
assert!(cmd.get("cdpPort").is_none());
1787+
}
1788+
1789+
#[test]
1790+
fn test_connect_with_http_url() {
1791+
let input: Vec<String> = vec![
1792+
"connect".to_string(),
1793+
"http://localhost:9222".to_string(),
1794+
];
1795+
let cmd = parse_command(&input, &default_flags()).unwrap();
1796+
assert_eq!(cmd["action"], "launch");
1797+
assert_eq!(cmd["cdpUrl"], "http://localhost:9222");
1798+
assert!(cmd.get("cdpPort").is_none());
1799+
}
1800+
1801+
#[test]
1802+
fn test_connect_missing_argument() {
1803+
let result = parse_command(&args("connect"), &default_flags());
1804+
assert!(result.is_err());
1805+
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
1806+
}
1807+
1808+
#[test]
1809+
fn test_connect_invalid_port() {
1810+
let result = parse_command(&args("connect notanumber"), &default_flags());
1811+
assert!(result.is_err());
1812+
let err = result.unwrap_err();
1813+
assert!(matches!(err, ParseError::InvalidValue { .. }));
1814+
assert!(err.format().contains("not a valid port number or URL"));
1815+
}
1816+
1817+
#[test]
1818+
fn test_connect_port_zero() {
1819+
let result = parse_command(&args("connect 0"), &default_flags());
1820+
assert!(result.is_err());
1821+
let err = result.unwrap_err();
1822+
assert!(matches!(err, ParseError::InvalidValue { .. }));
1823+
assert!(err.format().contains("port must be greater than 0"));
1824+
}
1825+
1826+
#[test]
1827+
fn test_connect_port_out_of_range() {
1828+
let result = parse_command(&args("connect 65536"), &default_flags());
1829+
assert!(result.is_err());
1830+
let err = result.unwrap_err();
1831+
assert!(matches!(err, ParseError::InvalidValue { .. }));
1832+
assert!(err.format().contains("out of range"));
1833+
assert!(err.format().contains("1-65535"));
1834+
}
1835+
1836+
#[test]
1837+
fn test_connect_port_max_valid() {
1838+
let cmd = parse_command(&args("connect 65535"), &default_flags()).unwrap();
1839+
assert_eq!(cmd["action"], "launch");
1840+
assert_eq!(cmd["cdpPort"], 65535);
1841+
}
1842+
1843+
#[test]
1844+
fn test_connect_port_min_valid() {
1845+
let cmd = parse_command(&args("connect 1"), &default_flags()).unwrap();
1846+
assert_eq!(cmd["action"], "launch");
1847+
assert_eq!(cmd["cdpPort"], 1);
1848+
}
17161849
}

cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ fn main() {
177177
ParseError::UnknownCommand { .. } => "unknown_command",
178178
ParseError::UnknownSubcommand { .. } => "unknown_subcommand",
179179
ParseError::MissingArguments { .. } => "missing_arguments",
180+
ParseError::InvalidValue { .. } => "invalid_value",
180181
};
181182
println!(
182183
r#"{{"success":false,"error":"{}","type":"{}"}}"#,

cli/src/output.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,46 @@ Examples:
13171317
"##
13181318
}
13191319

1320+
// === Connect ===
1321+
"connect" => {
1322+
r##"
1323+
agent-browser connect - Connect to browser via CDP
1324+
1325+
Usage: agent-browser connect <port|url>
1326+
1327+
Connects to a running browser instance via Chrome DevTools Protocol (CDP).
1328+
This allows controlling browsers, Electron apps, or remote browser services.
1329+
1330+
Arguments:
1331+
<port> Local port number (e.g., 9222)
1332+
<url> Full WebSocket URL (ws://, wss://, http://, https://)
1333+
1334+
Supported URL formats:
1335+
- Port number: 9222 (connects to http://localhost:9222)
1336+
- WebSocket URL: ws://localhost:9222/devtools/browser/...
1337+
- Remote service: wss://remote-browser.example.com/cdp?token=...
1338+
1339+
Global Options:
1340+
--json Output as JSON
1341+
--session <name> Use specific session
1342+
1343+
Examples:
1344+
# Connect to local Chrome with remote debugging
1345+
# Start Chrome: google-chrome --remote-debugging-port=9222
1346+
agent-browser connect 9222
1347+
1348+
# Connect using WebSocket URL from /json/version endpoint
1349+
agent-browser connect "ws://localhost:9222/devtools/browser/abc123"
1350+
1351+
# Connect to remote browser service
1352+
agent-browser connect "wss://browser-service.example.com/cdp?token=xyz"
1353+
1354+
# After connecting, run commands normally
1355+
agent-browser snapshot
1356+
agent-browser click @e1
1357+
"##
1358+
}
1359+
13201360
_ => return false,
13211361
};
13221362
println!("{}", help.trim());
@@ -1351,7 +1391,7 @@ Core Commands:
13511391
pdf <path> Save as PDF
13521392
snapshot Accessibility tree with refs (for AI)
13531393
eval <js> Run JavaScript
1354-
connect <port> Connect to browser via CDP (e.g., connect 9222)
1394+
connect <port|url> Connect to browser via CDP
13551395
close Close browser
13561396
13571397
Navigation:

0 commit comments

Comments
 (0)