use std::sync::OnceLock; use crate::color; use crate::connection::Response; static BOUNDARY_NONCE: OnceLock = OnceLock::new(); /// Per-process nonce for content boundary markers. Uses a CSPRNG (getrandom) so /// that untrusted page content cannot predict or spoof the boundary delimiter. /// Process ID or timestamps would be insufficient since pages can read those. fn get_boundary_nonce() -> &'static str { BOUNDARY_NONCE.get_or_init(|| { let mut buf = [0u8; 16]; getrandom::getrandom(&mut buf).expect("failed to generate random nonce"); buf.iter().map(|b| format!("{:02x}", b)).collect() }) } #[derive(Default)] pub struct OutputOptions { pub json: bool, pub content_boundaries: bool, pub max_output: Option, } impl OutputOptions { pub fn from_flags(flags: &crate::flags::Flags) -> Self { Self { json: flags.json, content_boundaries: flags.content_boundaries, max_output: flags.max_output, } } } fn truncate_if_needed(content: &str, max: Option) -> String { let Some(limit) = max else { return content.to_string(); }; // Fast path: byte length is a lower bound on char count, so if the // byte length is within the limit the char count must be too. if content.len() <= limit { return content.to_string(); } // Find the byte offset of the limit-th character. match content.char_indices().nth(limit).map(|(i, _)| i) { Some(byte_offset) => { let total_chars = content.chars().count(); format!( "{}\n[truncated: showing {} of {} chars. Use --max-output to adjust]", &content[..byte_offset], limit, total_chars ) } // Content has fewer than `limit` chars despite more bytes None => content.to_string(), } } fn print_with_boundaries(content: &str, origin: Option<&str>, opts: &OutputOptions) { let content = truncate_if_needed(content, opts.max_output); if opts.content_boundaries { let origin_str = origin.unwrap_or("unknown"); let nonce = get_boundary_nonce(); println!( "--- AGENT_BROWSER_PAGE_CONTENT nonce={} origin={} ---", nonce, origin_str ); println!("{}", content); println!("--- END_AGENT_BROWSER_PAGE_CONTENT nonce={} ---", nonce); } else { println!("{}", content); } } fn format_storage_value(value: &serde_json::Value) -> String { value .as_str() .map(ToString::to_string) .unwrap_or_else(|| serde_json::to_string(value).unwrap_or_default()) } fn format_storage_text(data: &serde_json::Value) -> Option { if let Some(entries) = data.get("data").and_then(|v| v.as_object()) { if entries.is_empty() { return Some("No storage entries".to_string()); } let lines = entries .iter() .map(|(key, value)| format!("{}: {}", key, format_storage_value(value))) .collect::>(); return Some(lines.join("\n")); } let key = data.get("key").and_then(|v| v.as_str())?; let value = data.get("value")?; Some(format!("{}: {}", key, format_storage_value(value))) } fn format_stream_status_text(action: Option<&str>, data: &serde_json::Value) -> Option { match action { Some("stream_disable") => data .get("disabled") .and_then(|v| v.as_bool()) .filter(|disabled| *disabled) .map(|_| "Streaming disabled".to_string()), Some("stream_enable") | Some("stream_status") => { let enabled = data.get("enabled").and_then(|v| v.as_bool())?; if !enabled { return Some("Streaming disabled".to_string()); } let port = data.get("port").and_then(|v| v.as_u64())?; let connected = data .get("connected") .and_then(|v| v.as_bool()) .unwrap_or(false); let screencasting = data .get("screencasting") .and_then(|v| v.as_bool()) .unwrap_or(false); Some(format!( "Streaming enabled on ws://127.0.0.1:{port}\nConnected: {connected}\nScreencasting: {screencasting}" )) } _ => None, } } pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &OutputOptions) { if opts.json { if opts.content_boundaries { let mut json_val = serde_json::to_value(resp).unwrap_or_default(); if let Some(obj) = json_val.as_object_mut() { let nonce = get_boundary_nonce(); let origin = obj .get("data") .and_then(|d| d.get("origin")) .and_then(|v| v.as_str()) .unwrap_or("unknown"); obj.insert( "_boundary".to_string(), serde_json::json!({ "nonce": nonce, "origin": origin, }), ); } println!("{}", serde_json::to_string(&json_val).unwrap_or_default()); } else { println!("{}", serde_json::to_string(resp).unwrap_or_default()); } // JSON mode includes the warning field in the JSON payload already return; } if !resp.success { eprintln!( "{} {}", color::error_indicator(), resp.error.as_deref().unwrap_or("Unknown error") ); // Still print dialog warning after errors, since a pending dialog // is the most common cause of commands timing out if let Some(ref warning) = resp.warning { eprintln!("{} {}", color::warning_indicator(), warning); } return; } if let Some(data) = &resp.data { // Dialog status response if action == Some("dialog") { if let Some(has_dialog) = data.get("hasDialog").and_then(|v| v.as_bool()) { if has_dialog { let dtype = data .get("type") .and_then(|v| v.as_str()) .unwrap_or("unknown"); let message = data.get("message").and_then(|v| v.as_str()).unwrap_or(""); println!( "{} JavaScript {} dialog is open: \"{}\"", color::warning_indicator(), dtype, message ); if let Some(default_prompt) = data.get("defaultPrompt").and_then(|v| v.as_str()) { println!(" Default prompt text: \"{}\"", default_prompt); } println!(" Use `dialog accept [text]` or `dialog dismiss` to resolve it"); } else { println!("{} No dialog is currently open", color::success_indicator()); } print_warning(resp); return; } } if let Some(output) = format_stream_status_text(action, data) { println!("{}", output); return; } if action == Some("storage_get") { if let Some(output) = format_storage_text(data) { println!("{}", output); return; } } // Inspect response (check before generic URL handler since it also has a "url" field) if action == Some("inspect") { let opened = data .get("opened") .and_then(|v| v.as_bool()) .unwrap_or(false); if opened { if let Some(url) = data.get("url").and_then(|v| v.as_str()) { println!("{} Opened DevTools: {}", color::success_indicator(), url); } else { println!("{} Opened DevTools", color::success_indicator()); } } else if let Some(err) = data.get("error").and_then(|v| v.as_str()) { eprintln!("Could not open DevTools: {}", err); } return; } // Navigation response if let Some(url) = data.get("url").and_then(|v| v.as_str()) { if let Some(title) = data.get("title").and_then(|v| v.as_str()) { println!("{} {}", color::success_indicator(), color::bold(title)); println!(" {}", color::dim(url)); return; } println!("{}", url); return; } if let Some(cdp_url) = data.get("cdpUrl").and_then(|v| v.as_str()) { println!("{}", cdp_url); return; } // Diff responses -- route by action to avoid fragile shape probing if let Some(obj) = data.as_object() { match action { Some("diff_snapshot") => { print_snapshot_diff(obj); return; } Some("diff_screenshot") => { print_screenshot_diff(obj); return; } Some("diff_url") => { if let Some(snap_data) = obj.get("snapshot").and_then(|v| v.as_object()) { println!("{}", color::bold("Snapshot diff:")); print_snapshot_diff(snap_data); } if let Some(ss_data) = obj.get("screenshot").and_then(|v| v.as_object()) { println!("\n{}", color::bold("Screenshot diff:")); print_screenshot_diff(ss_data); } return; } _ => {} } } let origin = data.get("origin").and_then(|v| v.as_str()); // Snapshot if let Some(snapshot) = data.get("snapshot").and_then(|v| v.as_str()) { print_with_boundaries(snapshot, origin, opts); return; } // Title if let Some(title) = data.get("title").and_then(|v| v.as_str()) { println!("{}", title); return; } // Text if let Some(text) = data.get("text").and_then(|v| v.as_str()) { print_with_boundaries(text, origin, opts); return; } // HTML if let Some(html) = data.get("html").and_then(|v| v.as_str()) { print_with_boundaries(html, origin, opts); return; } // Value if let Some(value) = data.get("value").and_then(|v| v.as_str()) { println!("{}", value); return; } // Count if let Some(count) = data.get("count").and_then(|v| v.as_i64()) { println!("{}", count); return; } // Bounding box (get box) if action == Some("boundingbox") { if let Some(obj) = data.as_object() { let x = obj.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0); let y = obj.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0); let w = obj.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0); let h = obj.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0); println!("x: {}", x); println!("y: {}", y); println!("width: {}", w); println!("height: {}", h); } return; } // Computed styles (get styles) if let Some(styles) = data.get("styles").and_then(|v| v.as_object()) { for (key, val) in styles { let display = match val.as_str() { Some(s) => s.to_string(), None => val.to_string(), }; println!("{}: {}", key, display); } return; } // Boolean results if let Some(visible) = data.get("visible").and_then(|v| v.as_bool()) { println!("{}", visible); return; } if let Some(enabled) = data.get("enabled").and_then(|v| v.as_bool()) { println!("{}", enabled); return; } if let Some(checked) = data.get("checked").and_then(|v| v.as_bool()) { println!("{}", checked); return; } // Eval result if let Some(result) = data.get("result") { let formatted = serde_json::to_string_pretty(result).unwrap_or_default(); print_with_boundaries(&formatted, origin, opts); return; } // iOS Devices if let Some(devices) = data.get("devices").and_then(|v| v.as_array()) { if devices.is_empty() { println!("No iOS devices available. Open Xcode to download simulator runtimes."); return; } // Separate real devices from simulators let real_devices: Vec<_> = devices .iter() .filter(|d| { d.get("isRealDevice") .and_then(|v| v.as_bool()) .unwrap_or(false) }) .collect(); let simulators: Vec<_> = devices .iter() .filter(|d| { !d.get("isRealDevice") .and_then(|v| v.as_bool()) .unwrap_or(false) }) .collect(); if !real_devices.is_empty() { println!("Connected Devices:\n"); for device in real_devices.iter() { let name = device .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or(""); let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or(""); println!(" {} {} ({})", color::green("●"), name, runtime); println!(" {}", color::dim(udid)); } println!(); } if !simulators.is_empty() { println!("Simulators:\n"); for device in simulators.iter() { let name = device .get("name") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let runtime = device.get("runtime").and_then(|v| v.as_str()).unwrap_or(""); let state = device .get("state") .and_then(|v| v.as_str()) .unwrap_or("Unknown"); let udid = device.get("udid").and_then(|v| v.as_str()).unwrap_or(""); let state_indicator = if state == "Booted" { color::green("●") } else { color::dim("○") }; println!(" {} {} ({})", state_indicator, name, runtime); println!(" {}", color::dim(udid)); } } return; } // Tabs if let Some(tabs) = data.get("tabs").and_then(|v| v.as_array()) { for tab in tabs { let tab_id = tab.get("tabId").and_then(|v| v.as_str()).unwrap_or("?"); let tab_label = tab.get("label").and_then(|v| v.as_str()); let title = tab .get("title") .and_then(|v| v.as_str()) .unwrap_or("Untitled"); let url = tab.get("url").and_then(|v| v.as_str()).unwrap_or(""); let active = tab.get("active").and_then(|v| v.as_bool()).unwrap_or(false); let marker = if active { color::cyan("→") } else { " ".to_string() }; if let Some(label) = tab_label { println!("{} [{}] {} {} - {}", marker, tab_id, label, title, url); } else { println!("{} [{}] {} - {}", marker, tab_id, title, url); } } return; } // Tab switch if action == Some("tab_switch") { if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_str()) { if let Some(url) = data.get("url").and_then(|v| v.as_str()) { println!( "{} Switched to tab [{}] ({})", color::success_indicator(), tab_id, url ); } else { println!( "{} Switched to tab [{}]", color::success_indicator(), tab_id ); } return; } } // New tab/window if let Some(tab_id) = data.get("tabId").and_then(|v| v.as_str()) { if let Some(total) = data.get("total").and_then(|v| v.as_i64()) { let label_noun = match action { Some("window_new") => "Window opened", _ => "Tab opened", }; let tab_label = data.get("label").and_then(|v| v.as_str()); if let Some(lbl) = tab_label { println!( "{} {} [{}] {} ({} total)", color::success_indicator(), label_noun, tab_id, lbl, total ); } else { println!( "{} {} [{}] ({} total)", color::success_indicator(), label_noun, tab_id, total ); } return; } } // Console logs if let Some(logs) = data.get("messages").and_then(|v| v.as_array()) { if opts.content_boundaries { let mut console_output = String::new(); for log in logs { let level = log.get("type").and_then(|v| v.as_str()).unwrap_or("log"); let text = log.get("text").and_then(|v| v.as_str()).unwrap_or(""); console_output.push_str(&format!( "{} {}\n", color::console_level_prefix(level), text )); } if console_output.ends_with('\n') { console_output.pop(); } print_with_boundaries(&console_output, origin, opts); } else { for log in logs { let level = log.get("type").and_then(|v| v.as_str()).unwrap_or("log"); let text = log.get("text").and_then(|v| v.as_str()).unwrap_or(""); println!("{} {}", color::console_level_prefix(level), text); } } return; } // Errors if let Some(errors) = data.get("errors").and_then(|v| v.as_array()) { for err in errors { let msg = err.get("message").and_then(|v| v.as_str()).unwrap_or(""); println!("{} {}", color::error_indicator(), msg); } return; } // Cookies if let Some(cookies) = data.get("cookies").and_then(|v| v.as_array()) { for cookie in cookies { let name = cookie.get("name").and_then(|v| v.as_str()).unwrap_or(""); let value = cookie.get("value").and_then(|v| v.as_str()).unwrap_or(""); println!("{}={}", name, value); } return; } // Network requests if let Some(requests) = data.get("requests").and_then(|v| v.as_array()) { if requests.is_empty() { println!("No requests captured"); } else { for req in requests { let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("GET"); let url = req.get("url").and_then(|v| v.as_str()).unwrap_or(""); let resource_type = req .get("resourceType") .and_then(|v| v.as_str()) .unwrap_or(""); let request_id = req.get("requestId").and_then(|v| v.as_str()).unwrap_or(""); let status = req.get("status").and_then(|v| v.as_i64()); match status { Some(s) => println!( "[{}] {} {} ({}) {}", request_id, method, url, resource_type, s ), None => println!("[{}] {} {} ({})", request_id, method, url, resource_type), } } } return; } // Cleared (cookies, console, or request log) if let Some(cleared) = data.get("cleared").and_then(|v| v.as_bool()) { if cleared { let label = match action { Some("cookies_clear") => "Cookies cleared", Some("console") => "Console log cleared", _ => "Request log cleared", }; println!("{} {}", color::success_indicator(), label); return; } } // Bounding box if let Some(box_data) = data.get("box") { println!( "{}", serde_json::to_string_pretty(box_data).unwrap_or_default() ); return; } // Element styles if let Some(elements) = data.get("elements").and_then(|v| v.as_array()) { for (i, el) in elements.iter().enumerate() { let tag = el.get("tag").and_then(|v| v.as_str()).unwrap_or("?"); let text = el.get("text").and_then(|v| v.as_str()).unwrap_or(""); println!("[{}] {} \"{}\"", i, tag, text); if let Some(box_data) = el.get("box") { let w = box_data.get("width").and_then(|v| v.as_i64()).unwrap_or(0); let h = box_data.get("height").and_then(|v| v.as_i64()).unwrap_or(0); let x = box_data.get("x").and_then(|v| v.as_i64()).unwrap_or(0); let y = box_data.get("y").and_then(|v| v.as_i64()).unwrap_or(0); println!(" box: {}x{} at ({}, {})", w, h, x, y); } if let Some(styles) = el.get("styles") { let font_size = styles .get("fontSize") .and_then(|v| v.as_str()) .unwrap_or(""); let font_weight = styles .get("fontWeight") .and_then(|v| v.as_str()) .unwrap_or(""); let font_family = styles .get("fontFamily") .and_then(|v| v.as_str()) .unwrap_or(""); let color = styles.get("color").and_then(|v| v.as_str()).unwrap_or(""); let bg = styles .get("backgroundColor") .and_then(|v| v.as_str()) .unwrap_or(""); let radius = styles .get("borderRadius") .and_then(|v| v.as_str()) .unwrap_or(""); println!(" font: {} {} {}", font_size, font_weight, font_family); println!(" color: {}", color); println!(" background: {}", bg); if radius != "0px" { println!(" border-radius: {}", radius); } } println!(); } return; } // Closed (browser or tab) if data.get("closed").is_some() { let label = match action { Some("tab_close") => { if let Some(closed_id) = data.get("tabId").and_then(|v| v.as_str()) { println!("{} Tab [{}] closed", color::success_indicator(), closed_id); return; } "Tab closed" } _ => "Browser closed", }; println!("{} {}", color::success_indicator(), label); return; } // Started actions (profiling, HAR, recording) if let Some(started) = data.get("started").and_then(|v| v.as_bool()) { if started { match action { Some("profiler_start") => { println!("{} Profiling started", color::success_indicator()); } Some("har_start") => { println!("{} HAR recording started", color::success_indicator()); } _ => { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { println!("{} Recording started: {}", color::success_indicator(), path); } else { println!("{} Recording started", color::success_indicator()); } } } return; } } // Recording restart (has "stopped" field - from recording_restart action) if data.get("stopped").is_some() { let path = data .get("path") .and_then(|v| v.as_str()) .unwrap_or("unknown"); if let Some(prev_path) = data.get("previousPath").and_then(|v| v.as_str()) { println!( "{} Recording restarted: {} (previous saved to {})", color::success_indicator(), path, prev_path ); } else { println!("{} Recording started: {}", color::success_indicator(), path); } return; } // Recording stop (has "frames" field - from recording_stop action) if data.get("frames").is_some() { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { if let Some(error) = data.get("error").and_then(|v| v.as_str()) { println!( "{} Recording saved to {} - {}", color::warning_indicator(), path, error ); } else { println!("{} Recording saved to {}", color::success_indicator(), path); } } else { println!("{} Recording stopped", color::success_indicator()); } return; } // Download response (has "suggestedFilename" or "filename" field) if data.get("suggestedFilename").is_some() || data.get("filename").is_some() { if let Some(path) = data.get("path").and_then(|v| v.as_str()) { let filename = data .get("suggestedFilename") .or_else(|| data.get("filename")) .and_then(|v| v.as_str()) .unwrap_or(""); if filename.is_empty() { println!( "{} Downloaded to {}", color::success_indicator(), color::green(path) ); } else { println!( "{} Downloaded to {} ({})", color::success_indicator(), color::green(path), filename ); } return; } } // Trace stop without path if data.get("traceStopped").is_some() { println!("{} Trace stopped", color::success_indicator()); return; } // Path-based operations (screenshot/pdf/trace/har/download/state/video) if let Some(path) = data.get("path").and_then(|v| v.as_str()) { match action.unwrap_or("") { "screenshot" => { println!( "{} Screenshot saved to {}", color::success_indicator(), color::green(path) ); if let Some(annotations) = data.get("annotations").and_then(|v| v.as_array()) { for ann in annotations { let num = ann.get("number").and_then(|n| n.as_u64()).unwrap_or(0); let ref_id = ann.get("ref").and_then(|r| r.as_str()).unwrap_or(""); let role = ann.get("role").and_then(|r| r.as_str()).unwrap_or(""); let name = ann.get("name").and_then(|n| n.as_str()).unwrap_or(""); if name.is_empty() { println!( " {} @{} {}", color::dim(&format!("[{}]", num)), ref_id, role, ); } else { println!( " {} @{} {} {:?}", color::dim(&format!("[{}]", num)), ref_id, role, name, ); } } } } "pdf" => println!( "{} PDF saved to {}", color::success_indicator(), color::green(path) ), "trace_stop" => println!( "{} Trace saved to {}", color::success_indicator(), color::green(path) ), "profiler_stop" => println!( "{} Profile saved to {} ({} events)", color::success_indicator(), color::green(path), data.get("eventCount").and_then(|c| c.as_u64()).unwrap_or(0) ), "har_stop" => println!( "{} HAR saved to {} ({} requests)", color::success_indicator(), color::green(path), data.get("requestCount") .and_then(|c| c.as_u64()) .unwrap_or(0) ), "download" | "waitfordownload" => println!( "{} Download saved to {}", color::success_indicator(), color::green(path) ), "video_stop" => println!( "{} Video saved to {}", color::success_indicator(), color::green(path) ), "state_save" => println!( "{} State saved to {}", color::success_indicator(), color::green(path) ), "state_load" => { if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); } println!( "{} State path set to {}", color::success_indicator(), color::green(path) ); } // video_start and other commands that provide a path with a note "video_start" => { if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); } println!("Path: {}", path); } _ => println!( "{} Saved to {}", color::success_indicator(), color::green(path) ), } return; } // State list if let Some(files) = data.get("files").and_then(|v| v.as_array()) { if let Some(dir) = data.get("directory").and_then(|v| v.as_str()) { println!("{}", color::bold(&format!("Saved states in {}", dir))); } if files.is_empty() { println!("{}", color::dim(" No state files found")); } else { for file in files { let filename = file.get("filename").and_then(|v| v.as_str()).unwrap_or(""); let size = file.get("size").and_then(|v| v.as_i64()).unwrap_or(0); let modified = file.get("modified").and_then(|v| v.as_str()).unwrap_or(""); let encrypted = file .get("encrypted") .and_then(|v| v.as_bool()) .unwrap_or(false); let size_str = if size > 1024 { format!("{:.1}KB", size as f64 / 1024.0) } else { format!("{}B", size) }; let date_str = modified.split('T').next().unwrap_or(modified); let enc_str = if encrypted { " [encrypted]" } else { "" }; println!( " {} {}", filename, color::dim(&format!("({}, {}){}", size_str, date_str, enc_str)) ); } } return; } // State rename if let Some(true) = data.get("renamed").and_then(|v| v.as_bool()) { let old_name = data.get("oldName").and_then(|v| v.as_str()).unwrap_or(""); let new_name = data.get("newName").and_then(|v| v.as_str()).unwrap_or(""); println!( "{} Renamed {} -> {}", color::success_indicator(), old_name, new_name ); return; } // State clear if let Some(cleared) = data.get("cleared").and_then(|v| v.as_i64()) { println!( "{} Cleared {} state file(s)", color::success_indicator(), cleared ); return; } // State show summary if let Some(summary) = data.get("summary") { let cookies = summary.get("cookies").and_then(|v| v.as_i64()).unwrap_or(0); let origins = summary.get("origins").and_then(|v| v.as_i64()).unwrap_or(0); let encrypted = data .get("encrypted") .and_then(|v| v.as_bool()) .unwrap_or(false); let enc_str = if encrypted { " (encrypted)" } else { "" }; println!("State file summary{}:", enc_str); println!(" Cookies: {}", cookies); println!(" Origins with localStorage: {}", origins); return; } // State clean if let Some(cleaned) = data.get("cleaned").and_then(|v| v.as_i64()) { println!( "{} Cleaned {} old state file(s)", color::success_indicator(), cleaned ); return; } // Informational note if let Some(note) = data.get("note").and_then(|v| v.as_str()) { println!("{}", note); return; } // Auth list if let Some(profiles) = data.get("profiles").and_then(|v| v.as_array()) { if profiles.is_empty() { println!("{}", color::dim("No auth profiles saved")); } else { println!("{}", color::bold("Auth profiles:")); for p in profiles { let name = p.get("name").and_then(|v| v.as_str()).unwrap_or(""); let url = p.get("url").and_then(|v| v.as_str()).unwrap_or(""); let user = p.get("username").and_then(|v| v.as_str()).unwrap_or(""); println!( " {} {} {}", color::green(name), color::dim(user), color::dim(url) ); } } return; } // Auth show if let Some(profile) = data.get("profile").and_then(|v| v.as_object()) { let name = profile.get("name").and_then(|v| v.as_str()).unwrap_or(""); let url = profile.get("url").and_then(|v| v.as_str()).unwrap_or(""); let user = profile .get("username") .and_then(|v| v.as_str()) .unwrap_or(""); let created = profile .get("createdAt") .and_then(|v| v.as_str()) .unwrap_or(""); let last_login = profile.get("lastLoginAt").and_then(|v| v.as_str()); println!("Name: {}", name); println!("URL: {}", url); println!("Username: {}", user); println!("Created: {}", created); if let Some(ll) = last_login { println!("Last login: {}", ll); } return; } // Auth save/update/login/delete if data.get("saved").and_then(|v| v.as_bool()).unwrap_or(false) { let name = data.get("name").and_then(|v| v.as_str()).unwrap_or(""); println!( "{} Auth profile '{}' saved", color::success_indicator(), name ); return; } if data .get("updated") .and_then(|v| v.as_bool()) .unwrap_or(false) && !data.get("saved").and_then(|v| v.as_bool()).unwrap_or(false) { let name = data.get("name").and_then(|v| v.as_str()).unwrap_or(""); println!( "{} Auth profile '{}' updated", color::success_indicator(), name ); return; } if data .get("loggedIn") .and_then(|v| v.as_bool()) .unwrap_or(false) { let name = data.get("name").and_then(|v| v.as_str()).unwrap_or(""); if let Some(title) = data.get("title").and_then(|v| v.as_str()) { println!( "{} Logged in as '{}' - {}", color::success_indicator(), name, title ); } else { println!("{} Logged in as '{}'", color::success_indicator(), name); } return; } if data .get("deleted") .and_then(|v| v.as_bool()) .unwrap_or(false) { if let Some(name) = data.get("name").and_then(|v| v.as_str()) { println!( "{} Auth profile '{}' deleted", color::success_indicator(), name ); return; } } // Confirmation required (for orchestrator use) if data .get("confirmation_required") .and_then(|v| v.as_bool()) .unwrap_or(false) { let category = data.get("category").and_then(|v| v.as_str()).unwrap_or(""); let description = data .get("description") .and_then(|v| v.as_str()) .unwrap_or(""); let cid = data .get("confirmation_id") .and_then(|v| v.as_str()) .unwrap_or(""); println!("Confirmation required:"); println!(" {}: {}", category, description); println!(" Run: agent-browser confirm {}", cid); println!(" Or: agent-browser deny {}", cid); return; } if data .get("confirmed") .and_then(|v| v.as_bool()) .unwrap_or(false) { println!("{} Action confirmed", color::success_indicator()); return; } if data .get("denied") .and_then(|v| v.as_bool()) .unwrap_or(false) { println!("{} Action denied", color::success_indicator()); return; } // Default success println!("{} Done", color::success_indicator()); } print_warning(resp); } fn print_warning(resp: &Response) { if let Some(ref warning) = resp.warning { eprintln!("{} {}", color::warning_indicator(), warning); } } /// Print command-specific help. Returns true if help was printed, false if command unknown. pub fn print_command_help(command: &str) -> bool { let help = match command { // === Navigation === "open" | "goto" | "navigate" => { r##" agent-browser open - Launch the browser, optionally navigate Usage: agent-browser open [url] Without a URL, launches the browser but stays on about:blank. This lets you stage state (network routes, cookies, init scripts) before the first real navigation — useful for SSR debug, auth setup, and capturing fresh `react suspense` / `vitals` state without noise from a prior page. With a URL, launches and navigates. If no protocol is provided, https:// is automatically prepended. The `goto` and `navigate` aliases still require a URL. Global Options: --json Output as JSON --session Use specific session --headers Set HTTP headers (scoped to this origin) --headed Show browser window --enable react-devtools Inject the React DevTools hook before any page JS --init-script Register a page init script (repeatable) Examples: agent-browser open # Launch, no nav agent-browser open example.com agent-browser open https://github.com agent-browser open localhost:3000 agent-browser open api.example.com --headers '{"Authorization": "Bearer token"}' # ^ Headers only sent to api.example.com, not other domains # Pre-navigation setup in one turn: agent-browser batch \ '["open"]' \ '["network","route","*","--abort","--resource-type","script"]' \ '["navigate","http://localhost:3000/target"]' "## } "back" => { r##" agent-browser back - Navigate back in history Usage: agent-browser back Goes back one page in the browser history, equivalent to clicking the browser's back button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser back "## } "forward" => { r##" agent-browser forward - Navigate forward in history Usage: agent-browser forward Goes forward one page in the browser history, equivalent to clicking the browser's forward button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser forward "## } "reload" => { r##" agent-browser reload - Reload the current page Usage: agent-browser reload Reloads the current page, equivalent to pressing F5 or clicking the browser's reload button. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser reload "## } // === Core Actions === "click" => { r##" agent-browser click - Click an element Usage: agent-browser click [--new-tab] Clicks on the specified element. The selector can be a CSS selector, XPath, or an element reference from snapshot (e.g., @e1). Options: --new-tab Open link in a new tab instead of navigating current tab (only works on elements with href attribute) Global Options: --json Output as JSON --session Use specific session Examples: agent-browser click "#submit-button" agent-browser click @e1 agent-browser click "button.primary" agent-browser click "//button[@type='submit']" agent-browser click @e3 --new-tab "## } "dblclick" => { r##" agent-browser dblclick - Double-click an element Usage: agent-browser dblclick Double-clicks on the specified element. Useful for text selection or triggering double-click handlers. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser dblclick "#editable-text" agent-browser dblclick @e5 "## } "fill" => { r##" agent-browser fill - Clear and fill an input field Usage: agent-browser fill Clears the input field and fills it with the specified text. This replaces any existing content in the field. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser fill "#email" "user@example.com" agent-browser fill @e3 "Hello World" agent-browser fill "input[name='search']" "query" "## } "type" => { r##" agent-browser type - Type text into an element Usage: agent-browser type Types text into the specified element character by character. Unlike fill, this does not clear existing content first. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser type "#search" "hello" agent-browser type @e2 "additional text" See Also: For typing into contenteditable editors (Lexical, ProseMirror, etc.) without a selector, use 'keyboard type' instead: agent-browser keyboard type "# My Heading" "## } "hover" => { r##" agent-browser hover - Hover over an element Usage: agent-browser hover Moves the mouse to hover over the specified element. Useful for triggering hover states or dropdown menus. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser hover "#dropdown-trigger" agent-browser hover @e4 "## } "focus" => { r##" agent-browser focus - Focus an element Usage: agent-browser focus Sets keyboard focus to the specified element. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser focus "#input-field" agent-browser focus @e2 "## } "check" => { r##" agent-browser check - Check a checkbox Usage: agent-browser check Checks a checkbox element. If already checked, no action is taken. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser check "#terms-checkbox" agent-browser check @e7 "## } "uncheck" => { r##" agent-browser uncheck - Uncheck a checkbox Usage: agent-browser uncheck Unchecks a checkbox element. If already unchecked, no action is taken. Global Options: --json Output as JSON --session Use specific session Examples: agent-browser uncheck "#newsletter-opt-in" agent-browser uncheck @e8 "## } "select" => { r##" agent-browser select - Select a dropdown option Usage: agent-browser select Selects one or more options in a