feat: add network har start/stop command for HAR 1.2 export (#874)
Expose HAR recording as a CLI subcommand under the existing `network` command so users can capture and export network traffic without a separate tool or opening the browser twice. - Parse `network har start` and `network har stop [path]` in commands.rs - Enrich HarEntry with request/response headers, timestamps, status text, resource type, HTTP version, and body sizes from CDP events - Produce HAR 1.2 output with creator/browser metadata, query strings, and proper header arrays compatible with Chrome DevTools HAR viewer - Auto-generate output path under ~/.agent-browser/tmp/har/ when omitted - Add har_stop to skip_launch list so export works without a live browser - Update help text, README, docs site, SKILL.md, and security policy docs - Add unit tests for parsing, HAR entry serialization, and stop behavior
This commit is contained in:
+52
-2
@@ -2042,8 +2042,9 @@ fn parse_set(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse network interception, request inspection, and HAR recording commands.
|
||||
fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
const VALID: &[&str] = &["route", "unroute", "requests"];
|
||||
const VALID: &[&str] = &["route", "unroute", "requests", "har"];
|
||||
|
||||
match rest.first().copied() {
|
||||
Some("route") => {
|
||||
@@ -2073,13 +2074,34 @@ fn parse_network(rest: &[&str], id: &str) -> Result<Value, ParseError> {
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some("har") => {
|
||||
const HAR_VALID: &[&str] = &["start", "stop"];
|
||||
match rest.get(1).copied() {
|
||||
Some("start") => Ok(json!({ "id": id, "action": "har_start" })),
|
||||
Some("stop") => {
|
||||
let mut cmd = json!({ "id": id, "action": "har_stop" });
|
||||
if let Some(path) = rest.get(2) {
|
||||
cmd["path"] = json!(path);
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: HAR_VALID,
|
||||
}),
|
||||
None => Err(ParseError::MissingArguments {
|
||||
context: "network har".to_string(),
|
||||
usage: "network har <start|stop> [path]",
|
||||
}),
|
||||
}
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: VALID,
|
||||
}),
|
||||
None => Err(ParseError::MissingArguments {
|
||||
context: "network".to_string(),
|
||||
usage: "network <route|unroute|requests> [args...]",
|
||||
usage: "network <route|unroute|requests|har> [args...]",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -2659,6 +2681,34 @@ mod tests {
|
||||
assert_eq!(cmd["action"], "tab_close");
|
||||
}
|
||||
|
||||
// === Network ===
|
||||
|
||||
#[test]
|
||||
fn test_network_har_start() {
|
||||
let cmd = parse_command(&args("network har start"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "har_start");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_har_stop_with_path() {
|
||||
let cmd = parse_command(&args("network har stop ./capture.har"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "har_stop");
|
||||
assert_eq!(cmd["path"], "./capture.har");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_har_stop_without_path() {
|
||||
let cmd = parse_command(&args("network har stop"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "har_stop");
|
||||
assert!(cmd.get("path").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_har_requires_subcommand() {
|
||||
let result = parse_command(&args("network har"), &default_flags());
|
||||
assert!(matches!(result, Err(ParseError::MissingArguments { .. })));
|
||||
}
|
||||
|
||||
// === Screenshot ===
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user