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, } 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); } } 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()); } return; } if !resp.success { eprintln!( "{} {}", color::error_indicator(), resp.error.as_deref().unwrap_or("Unknown error") ); return; } if let Some(data) = &resp.data { // 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; } // 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; } // 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 (i, tab) in tabs.iter().enumerate() { 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() }; println!("{} [{}] {} - {}", marker, i, title, url); } 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(""); println!("{} {} ({})", method, url, resource_type); } } return; } // Cleared (cookies 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", _ => "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") => "Tab closed", _ => "Browser closed", }; println!("{} {}", color::success_indicator(), label); return; } // Recording start (has "started" field) 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()); } _ => { 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 {}", color::success_indicator(), color::green(path) ), "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 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 - Navigate to a URL Usage: agent-browser open Navigates the browser to the specified URL. If no protocol is provided, https:// is automatically prepended. Aliases: goto, navigate Global Options: --json Output as JSON --session Use specific session --headers Set HTTP headers (scoped to this origin) --headed Show browser window Examples: 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 "## } "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