Files
chrome-use/cli/src/output.rs
T
2026-03-17 13:38:54 -05:00

2845 lines
94 KiB
Rust

use std::sync::OnceLock;
use crate::color;
use crate::connection::Response;
static BOUNDARY_NONCE: OnceLock<String> = 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<usize>,
}
fn truncate_if_needed(content: &str, max: Option<usize>) -> 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<String> {
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::<Vec<_>>();
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)))
}
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 {
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;
}
// 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;
}
// 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 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 <url>
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 <name> Use specific session
--headers <json> 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 <name> 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 <name> 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 <name> Use specific session
Examples:
agent-browser reload
"##
}
// === Core Actions ===
"click" => {
r##"
agent-browser click - Click an element
Usage: agent-browser click <selector> [--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 <name> 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 <selector>
Double-clicks on the specified element. Useful for text selection
or triggering double-click handlers.
Global Options:
--json Output as JSON
--session <name> 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 <selector> <text>
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 <name> 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 <selector> <text>
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 <name> 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 <selector>
Moves the mouse to hover over the specified element. Useful for
triggering hover states or dropdown menus.
Global Options:
--json Output as JSON
--session <name> 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 <selector>
Sets keyboard focus to the specified element.
Global Options:
--json Output as JSON
--session <name> 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 <selector>
Checks a checkbox element. If already checked, no action is taken.
Global Options:
--json Output as JSON
--session <name> 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 <selector>
Unchecks a checkbox element. If already unchecked, no action is taken.
Global Options:
--json Output as JSON
--session <name> 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 <selector> <value...>
Selects one or more options in a <select> dropdown by value.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser select "#country" "US"
agent-browser select @e5 "option2"
agent-browser select "#menu" "opt1" "opt2" "opt3"
"##
}
"drag" => {
r##"
agent-browser drag - Drag and drop
Usage: agent-browser drag <source> <target>
Drags an element from source to target location.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser drag "#draggable" "#drop-zone"
agent-browser drag @e1 @e2
"##
}
"upload" => {
r##"
agent-browser upload - Upload files
Usage: agent-browser upload <selector> <files...>
Uploads one or more files to a file input element.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser upload "#file-input" ./document.pdf
agent-browser upload @e3 ./image1.png ./image2.png
"##
}
"download" => {
r##"
agent-browser download - Download a file by clicking an element
Usage: agent-browser download <selector> <path>
Clicks an element that triggers a download and saves the file to the specified path.
Arguments:
selector Element to click (CSS selector or @ref)
path Path where the downloaded file will be saved
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser download "#download-btn" ./file.pdf
agent-browser download @e5 ./report.xlsx
agent-browser download "a[href$='.zip']" ./archive.zip
"##
}
// === Keyboard ===
"press" | "key" => {
r##"
agent-browser press - Press a key or key combination
Usage: agent-browser press <key>
Presses a key or key combination. Supports special keys and modifiers.
Aliases: key
Special Keys:
Enter, Tab, Escape, Backspace, Delete, Space
ArrowUp, ArrowDown, ArrowLeft, ArrowRight
Home, End, PageUp, PageDown
F1-F12
Modifiers (combine with +):
Control, Alt, Shift, Meta
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser press Enter
agent-browser press Tab
agent-browser press Control+a
agent-browser press Control+Shift+s
agent-browser press Escape
"##
}
"keydown" => {
r##"
agent-browser keydown - Press a key down (without release)
Usage: agent-browser keydown <key>
Presses a key down without releasing it. Use keyup to release.
Useful for holding modifier keys.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser keydown Shift
agent-browser keydown Control
"##
}
"keyup" => {
r##"
agent-browser keyup - Release a key
Usage: agent-browser keyup <key>
Releases a key that was pressed with keydown.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser keyup Shift
agent-browser keyup Control
"##
}
"keyboard" => {
r##"
agent-browser keyboard - Raw keyboard input (no selector needed)
Usage: agent-browser keyboard <subcommand> <text>
Sends keyboard input to whatever element currently has focus.
Unlike 'type' which requires a selector, 'keyboard' operates on
the current focus — essential for contenteditable editors like
Lexical, ProseMirror, CodeMirror, and Monaco.
Subcommands:
type <text> Type text character-by-character with real
key events (keydown, keypress, keyup per char)
inserttext <text> Insert text without key events (like paste)
Note: For key combos (Enter, Control+a), use the 'press' command
directly — it already operates on the current focus.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser keyboard type "Hello, World!"
agent-browser keyboard type "# My Heading"
agent-browser keyboard inserttext "pasted content"
Use Cases:
# Type into a Lexical/ProseMirror contenteditable editor:
agent-browser click "[contenteditable]"
agent-browser keyboard type "# My Heading"
agent-browser press Enter
agent-browser keyboard type "Some paragraph text"
"##
}
// === Scroll ===
"scroll" => {
r##"
agent-browser scroll - Scroll the page
Usage: agent-browser scroll [direction] [amount] [options]
Scrolls the page or a specific element in the specified direction.
Arguments:
direction up, down, left, right (default: down)
amount Pixels to scroll (default: 300)
Options:
-s, --selector <sel> CSS selector for a scrollable container
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser scroll
agent-browser scroll down 500
agent-browser scroll up 200
agent-browser scroll left 100
agent-browser scroll down 500 --selector "div.scroll-container"
"##
}
"scrollintoview" | "scrollinto" => {
r##"
agent-browser scrollintoview - Scroll element into view
Usage: agent-browser scrollintoview <selector>
Scrolls the page until the specified element is visible in the viewport.
Aliases: scrollinto
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser scrollintoview "#footer"
agent-browser scrollintoview @e15
"##
}
// === Wait ===
"wait" => {
r##"
agent-browser wait - Wait for condition
Usage: agent-browser wait <selector|ms|option>
Waits for an element to appear, a timeout, or other conditions.
Modes:
<selector> Wait for element to appear
<ms> Wait for specified milliseconds
--url <pattern> Wait for URL to match pattern
--load <state> Wait for load state (load, domcontentloaded, networkidle)
--fn <expression> Wait for JavaScript expression to be truthy
--text <text> Wait for text to appear on page (substring match)
--download [path] Wait for a download to complete (optionally save to path)
Download Options (with --download):
--timeout <ms> Timeout in milliseconds for download to start
Wait for text to disappear:
Use --fn or --state hidden to wait for text or elements to go away:
wait --fn "!document.body.innerText.includes('Loading...')"
wait "#spinner" --state hidden
wait @e5 --state detached
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser wait "#loading-spinner"
agent-browser wait 2000
agent-browser wait --url "**/dashboard"
agent-browser wait --load networkidle
agent-browser wait --fn "window.appReady === true"
agent-browser wait --text "Welcome back"
agent-browser wait --download ./file.pdf
agent-browser wait --download ./report.xlsx --timeout 30000
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
"##
}
// === Screenshot/PDF ===
"screenshot" => {
r##"
agent-browser screenshot - Take a screenshot
Usage: agent-browser screenshot [selector] [path]
Captures a screenshot of the current page. If no path is provided,
saves to a temporary directory with a generated filename.
Options:
--full, -f Capture full page (not just viewport)
--annotate Overlay numbered labels on interactive elements.
Each label [N] corresponds to ref @eN from snapshot.
Prints a legend mapping labels to element roles/names.
With --json, annotations are included in the response.
Supported on Chromium and Lightpanda.
--screenshot-dir <path> Default output directory for screenshots
(or AGENT_BROWSER_SCREENSHOT_DIR env)
--screenshot-quality <0-100> JPEG quality (0-100, only applies to jpeg format)
(or AGENT_BROWSER_SCREENSHOT_QUALITY env)
--screenshot-format <fmt> Image format: png (default) or jpeg
(or AGENT_BROWSER_SCREENSHOT_FORMAT env)
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser screenshot
agent-browser screenshot ./screenshot.png
agent-browser screenshot --full ./full-page.png
agent-browser screenshot --annotate # Labeled screenshot + legend
agent-browser screenshot --annotate ./page.png # Save annotated screenshot
agent-browser screenshot --annotate --json # JSON output with annotations
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
"##
}
"pdf" => {
r##"
agent-browser pdf - Save page as PDF
Usage: agent-browser pdf <path>
Saves the current page as a PDF file.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser pdf ./page.pdf
agent-browser pdf ~/Documents/report.pdf
"##
}
// === Snapshot ===
"snapshot" => {
r##"
agent-browser snapshot - Get accessibility tree snapshot
Usage: agent-browser snapshot [options]
Returns an accessibility tree representation of the page with element
references (like @e1, @e2) that can be used in subsequent commands.
Designed for AI agents to understand page structure.
Options:
-i, --interactive Only include interactive elements
-C, --cursor Include cursor-interactive elements (cursor:pointer, onclick, tabindex)
-c, --compact Remove empty structural elements
-d, --depth <n> Limit tree depth
-s, --selector <sel> Scope snapshot to CSS selector
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser snapshot
agent-browser snapshot -i
agent-browser snapshot -i -C # Interactive + cursor-interactive elements
agent-browser snapshot --compact --depth 5
agent-browser snapshot -s "#main-content"
"##
}
// === Eval ===
"eval" => {
r##"
agent-browser eval - Execute JavaScript
Usage: agent-browser eval [options] <script>
Executes JavaScript code in the browser context and returns the result.
Options:
-b, --base64 Decode script from base64 (avoids shell escaping issues)
--stdin Read script from stdin (useful for heredocs/multiline)
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser eval "document.title"
agent-browser eval "window.location.href"
agent-browser eval "document.querySelectorAll('a').length"
agent-browser eval -b "ZG9jdW1lbnQudGl0bGU="
# Read from stdin with heredoc
cat <<'EOF' | agent-browser eval --stdin
const links = document.querySelectorAll('a');
links.length;
EOF
"##
}
// === Close ===
"close" | "quit" | "exit" => {
r##"
agent-browser close - Close the browser
Usage: agent-browser close
Closes the browser instance for the current session.
Aliases: quit, exit
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser close
agent-browser close --session mysession
"##
}
// === Inspect ===
"inspect" => {
r##"
agent-browser inspect - Open Chrome DevTools for the active page
Starts a local WebSocket proxy and opens Chrome's DevTools frontend in your
default browser. The proxy routes DevTools traffic through the daemon's
existing CDP connection, so both DevTools and agent-browser commands work
simultaneously.
Usage: agent-browser inspect
Examples:
agent-browser open example.com
agent-browser inspect # opens DevTools in your browser
agent-browser click "Submit" # commands still work while DevTools is open
"##
}
// === Get ===
"get" => {
r##"
agent-browser get - Retrieve information from elements or page
Usage: agent-browser get <subcommand> [args]
Retrieves various types of information from elements or the page.
Subcommands:
text <selector> Get text content of element
html <selector> Get inner HTML of element
value <selector> Get value of input element
attr <selector> <name> Get attribute value
title Get page title
url Get current URL
count <selector> Count matching elements
box <selector> Get bounding box (x, y, width, height)
styles <selector> Get computed styles of elements
cdp-url Get Chrome DevTools Protocol WebSocket URL
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser get text @e1
agent-browser get html "#content"
agent-browser get value "#email-input"
agent-browser get attr "#link" href
agent-browser get title
agent-browser get url
agent-browser get count "li.item"
agent-browser get box "#header"
agent-browser get styles "button"
agent-browser get styles @e1
"##
}
// === Is ===
"is" => {
r##"
agent-browser is - Check element state
Usage: agent-browser is <subcommand> <selector>
Checks the state of an element and returns true/false.
Subcommands:
visible <selector> Check if element is visible
enabled <selector> Check if element is enabled (not disabled)
checked <selector> Check if checkbox/radio is checked
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser is visible "#modal"
agent-browser is enabled "#submit-btn"
agent-browser is checked "#agree-checkbox"
"##
}
// === Find ===
"find" => {
r##"
agent-browser find - Find and interact with elements by locator
Usage: agent-browser find <locator> <value> [action] [text]
Finds elements using semantic locators and optionally performs an action.
Locators:
role <role> Find by ARIA role (--name <n>, --exact)
text <text> Find by text content (--exact)
label <label> Find by associated label (--exact)
placeholder <text> Find by placeholder text (--exact)
alt <text> Find by alt text (--exact)
title <text> Find by title attribute (--exact)
testid <id> Find by data-testid attribute
first <selector> First matching element
last <selector> Last matching element
nth <index> <selector> Nth matching element (0-based)
Actions (default: click):
click, fill, type, hover, focus, check, uncheck
Options:
--name <name> Filter role by accessible name
--exact Require exact text match
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser find role button click --name Submit
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@example.com"
agent-browser find placeholder "Search..." type "query"
agent-browser find testid "login-form" click
agent-browser find first "li.item" click
agent-browser find nth 2 ".card" hover
"##
}
// === Mouse ===
"mouse" => {
r##"
agent-browser mouse - Low-level mouse operations
Usage: agent-browser mouse <subcommand> [args]
Performs low-level mouse operations for precise control.
Subcommands:
move <x> <y> Move mouse to coordinates
down [button] Press mouse button (left, right, middle)
up [button] Release mouse button
wheel <dy> [dx] Scroll mouse wheel
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser mouse move 100 200
agent-browser mouse down
agent-browser mouse up
agent-browser mouse down right
agent-browser mouse wheel 100
agent-browser mouse wheel -50 0
"##
}
// === Set ===
"set" => {
r##"
agent-browser set - Configure browser settings
Usage: agent-browser set <setting> [args]
Configures various browser settings and emulation options.
Settings:
viewport <w> <h> [scale] Set viewport size (scale = deviceScaleFactor, e.g. 2 for retina)
device <name> Emulate device (e.g., "iPhone 12")
geo <lat> <lng> Set geolocation
offline [on|off] Toggle offline mode
headers <json> Set extra HTTP headers
credentials <user> <pass> Set HTTP authentication
media [dark|light] Set color scheme preference
[reduced-motion] Enable reduced motion
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser set viewport 1920 1080
agent-browser set viewport 1920 1080 2 # 2x retina
agent-browser set device "iPhone 12"
agent-browser set geo 37.7749 -122.4194
agent-browser set offline on
agent-browser set headers '{"X-Custom": "value"}'
agent-browser set credentials admin secret123
agent-browser set media dark
agent-browser set media light reduced-motion
"##
}
// === Network ===
"network" => {
r##"
agent-browser network - Network interception and monitoring
Usage: agent-browser network <subcommand> [args]
Intercept, mock, or monitor network requests.
Subcommands:
route <url> [options] Intercept requests matching URL pattern
--abort Abort matching requests
--body <json> Respond with custom body
unroute [url] Remove route (all if no URL)
requests [options] List captured requests
--clear Clear request log
--filter <pattern> Filter by URL pattern
har <start|stop> [path] Record and export a HAR file
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser network route "**/api/*" --abort
agent-browser network route "**/data.json" --body '{"mock": true}'
agent-browser network unroute
agent-browser network requests
agent-browser network requests --filter "api"
agent-browser network requests --clear
agent-browser network har start
agent-browser network har stop ./capture.har
"##
}
// === Storage ===
"storage" => {
r##"
agent-browser storage - Manage web storage
Usage: agent-browser storage <type> [operation] [key] [value]
Manage localStorage and sessionStorage.
Types:
local localStorage
session sessionStorage
Operations:
get [key] Get all storage or specific key
set <key> <value> Set a key-value pair
clear Clear all storage
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser storage local
agent-browser storage local get authToken
agent-browser storage local set theme "dark"
agent-browser storage local clear
agent-browser storage session get userId
"##
}
// === Cookies ===
"cookies" => {
r##"
agent-browser cookies - Manage browser cookies
Usage: agent-browser cookies [operation] [args]
Manage browser cookies for the current context.
Operations:
get Get all cookies (default)
set <name> <value> [options] Set a cookie with optional properties
clear Clear all cookies
Cookie Set Options:
--url <url> URL for the cookie (allows setting before page load)
--domain <domain> Cookie domain (e.g., ".example.com")
--path <path> Cookie path (e.g., "/api")
--httpOnly Set HttpOnly flag (prevents JavaScript access)
--secure Set Secure flag (HTTPS only)
--sameSite <Strict|Lax|None> SameSite policy
--expires <timestamp> Expiration time (Unix timestamp in seconds)
Note: If --url, --domain, and --path are all omitted, the cookie will be set
for the current page URL.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
# Simple cookie for current page
agent-browser cookies set session_id "abc123"
# Set cookie for a URL before loading it (useful for authentication)
agent-browser cookies set session_id "abc123" --url https://app.example.com
# Set secure, httpOnly cookie with domain and path
agent-browser cookies set auth_token "xyz789" --domain example.com --path /api --httpOnly --secure
# Set cookie with SameSite policy
agent-browser cookies set tracking_consent "yes" --sameSite Strict
# Set cookie with expiration (Unix timestamp)
agent-browser cookies set temp_token "temp123" --expires 1735689600
# Get all cookies
agent-browser cookies
# Clear all cookies
agent-browser cookies clear
"##
}
// === Tabs ===
"tab" => {
r##"
agent-browser tab - Manage browser tabs
Usage: agent-browser tab [operation] [args]
Manage browser tabs in the current window.
Operations:
list List all tabs (default)
new [url] Open new tab
close [index] Close tab (current if no index)
<index> Switch to tab by index
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser tab
agent-browser tab list
agent-browser tab new
agent-browser tab new https://example.com
agent-browser tab 2
agent-browser tab close
agent-browser tab close 1
"##
}
// === Window ===
"window" => {
r##"
agent-browser window - Manage browser windows
Usage: agent-browser window <operation>
Manage browser windows.
Operations:
new Open new browser window
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser window new
"##
}
// === Frame ===
"frame" => {
r##"
agent-browser frame - Switch frame context
Usage: agent-browser frame <selector|main>
Switch to an iframe or back to the main frame.
Arguments:
<selector> CSS selector for iframe
main Switch back to main frame
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser frame "#embed-iframe"
agent-browser frame "iframe[name='content']"
agent-browser frame main
"##
}
// === Auth ===
"auth" => {
r##"
agent-browser auth - Manage authentication profiles
Usage: agent-browser auth <subcommand> [args]
Subcommands:
save <name> Save credentials for a login profile
login <name> Login using saved credentials
list List saved profiles (names and URLs only)
show <name> Show profile metadata (no passwords)
delete <name> Delete a saved profile
Save Options:
--url <url> Login page URL (required)
--username <user> Username (required)
--password <pass> Password (required unless --password-stdin)
--password-stdin Read password from stdin (recommended)
--username-selector <s> Custom CSS selector for username field
--password-selector <s> Custom CSS selector for password field
--submit-selector <s> Custom CSS selector for submit button
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
echo "pass" | agent-browser auth save github --url https://github.com/login --username user --password-stdin
agent-browser auth save github --url https://github.com/login --username user --password pass
agent-browser auth login github
agent-browser auth list
agent-browser auth show github
agent-browser auth delete github
"##
}
// === Confirm/Deny ===
"confirm" | "deny" => {
r##"
agent-browser confirm/deny - Approve or deny pending actions
Usage:
agent-browser confirm <confirmation-id>
agent-browser deny <confirmation-id>
When --confirm-actions is set, certain action categories return a
confirmation_required response with a confirmation ID. Use confirm/deny
to approve or reject the action.
Pending confirmations auto-deny after 60 seconds.
Examples:
agent-browser confirm c_8f3a1234
agent-browser deny c_8f3a1234
"##
}
// === Dialog ===
"dialog" => {
r##"
agent-browser dialog - Handle browser dialogs
Usage: agent-browser dialog <response> [text]
Respond to browser dialogs (alert, confirm, prompt).
Operations:
accept [text] Accept dialog, optionally with prompt text
dismiss Dismiss/cancel dialog
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser dialog accept
agent-browser dialog accept "my input"
agent-browser dialog dismiss
"##
}
// === Trace ===
"trace" => {
r##"
agent-browser trace - Record execution trace
Usage: agent-browser trace <operation> [path]
Record a Chrome DevTools trace for debugging.
Operations:
start [path] Start recording trace
stop [path] Stop recording and save trace
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser trace start
agent-browser trace start ./my-trace
agent-browser trace stop
agent-browser trace stop ./debug-trace.zip
"##
}
// === Profile (CDP Tracing) ===
"profiler" => {
r##"
agent-browser profiler - Record Chrome DevTools performance profile
Usage: agent-browser profiler <operation> [options]
Record a performance profile using Chrome DevTools Protocol (CDP) Tracing.
The output JSON file can be loaded into Chrome DevTools Performance panel,
Perfetto UI (https://ui.perfetto.dev/), or other trace analysis tools.
Operations:
start Start profiling
stop [path] Stop profiling and save to file
Start Options:
--categories <list> Comma-separated trace categories (default includes
devtools.timeline, v8.execute, blink, and others)
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
# Basic profiling
agent-browser profiler start
agent-browser navigate https://example.com
agent-browser click "#button"
agent-browser profiler stop ./trace.json
# With custom categories
agent-browser profiler start --categories "devtools.timeline,v8.execute,blink.user_timing"
agent-browser profiler stop ./custom-trace.json
The output file can be viewed in:
- Chrome DevTools: Performance panel > Load profile
- Perfetto: https://ui.perfetto.dev/
"##
}
// === Record (video) ===
"record" => {
r##"
agent-browser record - Record browser session to video
Usage: agent-browser record start <path.webm> [url]
agent-browser record stop
agent-browser record restart <path.webm> [url]
Record the browser to a WebM video file.
Creates a fresh browser context but preserves cookies and localStorage.
If no URL is provided, automatically navigates to your current page.
Operations:
start <path> [url] Start recording (defaults to current URL if omitted)
stop Stop recording and save video
restart <path> [url] Stop current recording (if any) and start a new one
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
# Record from current page (preserves login state)
agent-browser open https://app.example.com/dashboard
agent-browser snapshot -i # Explore and plan
agent-browser record start ./demo.webm
agent-browser click @e3 # Execute planned actions
agent-browser record stop
# Or specify a different URL
agent-browser record start ./demo.webm https://example.com
# Restart recording with a new file (stops previous, starts new)
agent-browser record restart ./take2.webm
"##
}
// === Console/Errors ===
"console" => {
r##"
agent-browser console - View console logs
Usage: agent-browser console [--clear]
View browser console output (log, warn, error, info).
Options:
--clear Clear console log buffer
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser console
agent-browser console --clear
"##
}
"errors" => {
r##"
agent-browser errors - View page errors
Usage: agent-browser errors [--clear]
View JavaScript errors and uncaught exceptions.
Options:
--clear Clear error buffer
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser errors
agent-browser errors --clear
"##
}
// === Highlight ===
"highlight" => {
r##"
agent-browser highlight - Highlight an element
Usage: agent-browser highlight <selector>
Visually highlights an element on the page for debugging.
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser highlight "#target-element"
agent-browser highlight @e5
"##
}
// === Clipboard ===
"clipboard" => {
r##"
agent-browser clipboard - Read and write clipboard
Usage: agent-browser clipboard <operation> [text]
Read from or write to the browser clipboard.
Operations:
read Read text from clipboard
write <text> Write text to clipboard
copy Copy current selection (simulates Ctrl+C)
paste Paste from clipboard (simulates Ctrl+V)
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser clipboard read
agent-browser clipboard write "Hello, World!"
agent-browser clipboard copy
agent-browser clipboard paste
"##
}
// === State ===
"state" => {
r##"
agent-browser state - Manage browser state
Usage: agent-browser state <operation> [args]
Save, restore, list, and manage browser state (cookies, localStorage, sessionStorage).
Operations:
save <path> Save current state to file
load <path> Load state from file
list List saved state files
show <filename> Show state summary
rename <old-name> <new-name> Rename state file
clear [session-name] [--all] Clear saved states
clean --older-than <days> Delete expired state files
Automatic State Persistence:
Use --session-name to auto-save/restore state across restarts:
agent-browser --session-name myapp open https://example.com
Or set AGENT_BROWSER_SESSION_NAME environment variable.
State Encryption:
Set AGENT_BROWSER_ENCRYPTION_KEY (64-char hex) for AES-256-GCM encryption.
Generate a key: openssl rand -hex 32
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser state save ./auth-state.json
agent-browser state load ./auth-state.json
agent-browser state list
agent-browser state show myapp-default.json
agent-browser state rename old-name new-name
agent-browser state clear --all
agent-browser state clean --older-than 7
"##
}
// === Session ===
"session" => {
r##"
agent-browser session - Manage sessions
Usage: agent-browser session [operation]
Manage isolated browser sessions. Each session has its own browser
instance with separate cookies, storage, and state.
Operations:
(none) Show current session name
list List all active sessions
Environment:
AGENT_BROWSER_SESSION Default session name
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser session
agent-browser session list
agent-browser --session test open example.com
"##
}
// === Install ===
"install" => {
r##"
agent-browser install - Install browser binaries
Usage: agent-browser install [--with-deps]
Downloads and installs browser binaries required for automation.
Options:
-d, --with-deps Also install system dependencies (Linux only)
Examples:
agent-browser install
agent-browser install --with-deps
"##
}
// === 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
"##
}
// === iOS Commands ===
"tap" => {
r##"
agent-browser tap - Tap an element (touch gesture)
Usage: agent-browser tap <selector>
Taps an element. This is an alias for 'click' that provides semantic clarity
for touch-based interfaces like iOS Safari.
Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser tap "#submit-button"
agent-browser tap @e1
agent-browser -p ios tap "button:has-text('Sign In')"
"##
}
"swipe" => {
r##"
agent-browser swipe - Swipe gesture (iOS)
Usage: agent-browser swipe <direction> [distance]
Performs a swipe gesture on iOS Safari. The direction determines
which way the content moves (swipe up scrolls down, etc.).
Arguments:
direction up, down, left, or right
distance Optional distance in pixels (default: 300)
Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser -p ios swipe up
agent-browser -p ios swipe down 500
agent-browser -p ios swipe left
"##
}
"device" => {
r##"
agent-browser device - Manage iOS simulators
Usage: agent-browser device <subcommand>
Subcommands:
list List available iOS simulators
Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser device list
agent-browser -p ios device list
"##
}
"diff" => {
r##"
agent-browser diff - Compare page states
Subcommands:
diff snapshot Compare current snapshot to last snapshot in session
diff screenshot --baseline <f> Visual pixel diff against a baseline image
diff url <url1> <url2> Compare two pages
Snapshot Diff:
Usage: agent-browser diff snapshot [options]
Options:
-b, --baseline <file> Compare against a saved snapshot file
-s, --selector <sel> Scope snapshot to a CSS selector or @ref
-c, --compact Use compact snapshot format
-d, --depth <n> Limit snapshot tree depth
Without --baseline, compares against the last snapshot taken in this session.
Screenshot Diff:
Usage: agent-browser diff screenshot --baseline <file> [options]
Options:
-b, --baseline <file> Baseline image to compare against (required)
-o, --output <file> Path for the diff image (default: temp dir)
-t, --threshold <0-1> Color distance threshold (default: 0.1)
-s, --selector <sel> Scope screenshot to element
--full Full page screenshot
URL Diff:
Usage: agent-browser diff url <url1> <url2> [options]
Options:
--screenshot Also compare screenshots (default: snapshot only)
--full Full page screenshots
--wait-until <strategy> Navigation wait strategy: load, domcontentloaded, networkidle (default: load)
-s, --selector <sel> Scope snapshots to a CSS selector or @ref
-c, --compact Use compact snapshot format
-d, --depth <n> Limit snapshot tree depth
Global Options:
--json Output as JSON
--session <name> Use specific session
Examples:
agent-browser diff snapshot
agent-browser diff snapshot --baseline before.txt
agent-browser diff screenshot --baseline before.png
agent-browser diff screenshot --baseline before.png --output diff.png --threshold 0.2
agent-browser diff url https://staging.example.com https://prod.example.com
agent-browser diff url https://v1.example.com https://v2.example.com --screenshot
"##
}
"batch" => {
r##"
agent-browser batch - Execute multiple commands from stdin
Usage: echo '<json>' | agent-browser batch [options]
Reads a JSON array of commands from stdin and executes them sequentially.
Each command is an array of strings matching normal CLI arguments.
Results are printed in order, separated by blank lines (or as a JSON array
with --json).
Options:
--bail Stop on first error (default: continue all commands)
--json Output results as a JSON array
Input Format:
A JSON array of string arrays. Each inner array is one command:
[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["fill", "@e2", "test@example.com"],
["screenshot", "result.png"]
]
Examples:
echo '[["open", "https://example.com"], ["snapshot"]]' | agent-browser batch
echo '[["open", "https://example.com"], ["get", "title"]]' | agent-browser batch --json
agent-browser batch --bail < commands.json
"##
}
_ => return false,
};
println!("{}", help.trim());
true
}
pub fn print_help() {
println!(
r#"
agent-browser - fast browser automation CLI for AI agents
Usage: agent-browser <command> [args] [options]
Core Commands:
open <url> Navigate to URL
click <sel> Click element (or @ref)
dblclick <sel> Double-click element
type <sel> <text> Type into element
fill <sel> <text> Clear and fill
press <key> Press key (Enter, Tab, Control+a)
keyboard type <text> Type text with real keystrokes (no selector)
keyboard inserttext <text> Insert text without key events
hover <sel> Hover element
focus <sel> Focus element
check <sel> Check checkbox
uncheck <sel> Uncheck checkbox
select <sel> <val...> Select dropdown option
drag <src> <dst> Drag and drop
upload <sel> <files...> Upload files
download <sel> <path> Download file by clicking element
scroll <dir> [px] Scroll (up/down/left/right)
scrollintoview <sel> Scroll element into view
wait <sel|ms> Wait for element or time
screenshot [path] Take screenshot
pdf <path> Save as PDF
snapshot Accessibility tree with refs (for AI)
eval <js> Run JavaScript
connect <port|url> Connect to browser via CDP
close Close browser
Navigation:
back Go back
forward Go forward
reload Reload page
Get Info: agent-browser get <what> [selector]
text, html, value, attr <name>, title, url, count, box, styles, cdp-url
Check State: agent-browser is <what> <selector>
visible, enabled, checked
Find Elements: agent-browser find <locator> <value> <action> [text]
role, text, label, placeholder, alt, title, testid, first, last, nth
Mouse: agent-browser mouse <action> [args]
move <x> <y>, down [btn], up [btn], wheel <dy> [dx]
Browser Settings: agent-browser set <setting> [value]
viewport <w> <h>, device <name>, geo <lat> <lng>
offline [on|off], headers <json>, credentials <user> <pass>
media [dark|light] [reduced-motion]
Network: agent-browser network <action>
route <url> [--abort|--body <json>]
unroute [url]
requests [--clear] [--filter <pattern>]
har <start|stop> [path]
Storage:
cookies [get|set|clear] Manage cookies (set supports --url, --domain, --path, --httpOnly, --secure, --sameSite, --expires)
storage <local|session> Manage web storage
Tabs:
tab [new|list|close|<n>] Manage tabs
Diff:
diff snapshot Compare current vs last snapshot
diff screenshot --baseline Compare current vs baseline image
diff url <u1> <u2> Compare two pages
Debug:
trace start|stop [path] Record Chrome DevTools trace
profiler start|stop [path] Record Chrome DevTools profile
record start <path> [url] Start video recording (WebM)
record stop Stop and save video
console [--clear] View console logs
errors [--clear] View page errors
highlight <sel> Highlight element
inspect Open Chrome DevTools for the active page
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
Batch:
batch [--bail] Execute commands from stdin (JSON array of string arrays)
--bail stops on first error (default: continue all)
Auth Vault:
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
auth login <name> Login using saved credentials
auth list List saved auth profiles
auth show <name> Show auth profile metadata
auth delete <name> Delete auth profile
Confirmation:
confirm <id> Approve a pending action
deny <id> Deny a pending action
Sessions:
session Show current session name
session list List active sessions
Setup:
install Install browser binaries
install --with-deps Also install system dependencies (Linux)
Snapshot Options:
-i, --interactive Only interactive elements
-c, --compact Remove empty structural elements
-d, --depth <n> Limit tree depth
-s, --selector <sel> Scope to CSS selector
Authentication:
--profile <path> Persist login sessions across restarts (cookies, IndexedDB, cache)
(or AGENT_BROWSER_PROFILE env)
--session-name <name> Auto-save/restore cookies and localStorage by name
(or AGENT_BROWSER_SESSION_NAME env)
--state <path> Load saved auth state (cookies + storage) from JSON file
(or AGENT_BROWSER_STATE env)
--auto-connect Connect to a running Chrome to reuse its auth state
Tip: agent-browser --auto-connect state save ./auth.json
--headers <json> HTTP headers scoped to URL's origin (e.g., Authorization bearer token)
Options:
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
--extension <path> Load browser extensions (repeatable)
--args <args> Browser launch args, comma or newline separated (or AGENT_BROWSER_ARGS)
e.g., --args "--no-sandbox,--disable-blink-features=AutomationControlled"
--user-agent <ua> Custom User-Agent (or AGENT_BROWSER_USER_AGENT)
--proxy <server> Proxy server URL (or AGENT_BROWSER_PROXY)
e.g., --proxy "http://user:pass@127.0.0.1:7890"
--proxy-bypass <hosts> Bypass proxy for these hosts (or AGENT_BROWSER_PROXY_BYPASS)
e.g., --proxy-bypass "localhost,*.internal.com"
--ignore-https-errors Ignore HTTPS certificate errors
--allow-file-access Allow file:// URLs to access local files (Chromium only)
-p, --provider <name> Browser provider: ios, browserbase, kernel, browseruse, browserless
--device <name> iOS device name (e.g., "iPhone 15 Pro")
--json JSON output
--annotate Annotated screenshot with numbered labels and legend
--screenshot-dir <path> Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
--screenshot-quality <n> JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)
--screenshot-format <fmt> Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
--download-path <path> Default download directory (or AGENT_BROWSER_DOWNLOAD_PATH)
--content-boundaries Wrap page output in boundary markers (or AGENT_BROWSER_CONTENT_BOUNDARIES)
--max-output <chars> Truncate page output to N chars (or AGENT_BROWSER_MAX_OUTPUT)
--allowed-domains <list> Restrict navigation domains (or AGENT_BROWSER_ALLOWED_DOMAINS)
--action-policy <path> Action policy JSON file (or AGENT_BROWSER_ACTION_POLICY)
--confirm-actions <list> Categories requiring confirmation (or AGENT_BROWSER_CONFIRM_ACTIONS)
--confirm-interactive Interactive confirmation prompts; auto-denies if stdin is not a TTY (or AGENT_BROWSER_CONFIRM_INTERACTIVE)
--engine <name> Browser engine: chrome (default), lightpanda (or AGENT_BROWSER_ENGINE)
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
--debug Debug output
--version, -V Show version
Configuration:
agent-browser looks for agent-browser.json in these locations (lowest to highest priority):
1. ~/.agent-browser/config.json User-level defaults
2. ./agent-browser.json Project-level overrides
3. Environment variables Override config file values
4. CLI flags Override everything
Use --config <path> to load a specific config file instead of the defaults.
If --config points to a missing or invalid file, agent-browser exits with an error.
Boolean flags accept an optional true/false value to override config:
--headed (same as --headed true)
--headed false (disables "headed": true from config)
Extensions from user and project configs are merged (not replaced).
Example agent-browser.json:
{{"headed": true, "proxy": "http://localhost:8080", "profile": "./browser-data"}}
Environment:
AGENT_BROWSER_CONFIG Path to config file (or use --config)
AGENT_BROWSER_SESSION Session name (default: "default")
AGENT_BROWSER_SESSION_NAME Auto-save/restore state persistence name
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM state encryption
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete states older than N days (default: 30)
AGENT_BROWSER_EXECUTABLE_PATH Custom browser executable path
AGENT_BROWSER_EXTENSIONS Comma-separated browser extension paths
AGENT_BROWSER_HEADED Show browser window (not headless)
AGENT_BROWSER_JSON JSON output
AGENT_BROWSER_ANNOTATE Annotated screenshot with numbered labels and legend
AGENT_BROWSER_DEBUG Debug output
AGENT_BROWSER_IGNORE_HTTPS_ERRORS Ignore HTTPS certificate errors
AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse, browserless)
AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome
AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files
AGENT_BROWSER_COLOR_SCHEME Color scheme preference (dark, light, no-preference)
AGENT_BROWSER_DOWNLOAD_PATH Default download directory for browser downloads
AGENT_BROWSER_DEFAULT_TIMEOUT Default action timeout in ms (default: 25000)
AGENT_BROWSER_SESSION_NAME Auto-save/load state persistence name
AGENT_BROWSER_STATE_EXPIRE_DAYS Auto-delete saved states older than N days (default: 30)
AGENT_BROWSER_ENCRYPTION_KEY 64-char hex key for AES-256-GCM session encryption
AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223)
AGENT_BROWSER_IDLE_TIMEOUT_MS Auto-shutdown daemon after N ms of inactivity (disabled by default)
AGENT_BROWSER_IOS_DEVICE Default iOS device name
AGENT_BROWSER_IOS_UDID Default iOS device UDID
AGENT_BROWSER_CONTENT_BOUNDARIES Wrap page output in boundary markers
AGENT_BROWSER_MAX_OUTPUT Max characters for page output
AGENT_BROWSER_ALLOWED_DOMAINS Comma-separated allowed domain patterns
AGENT_BROWSER_ACTION_POLICY Path to action policy JSON file
AGENT_BROWSER_CONFIRM_ACTIONS Action categories requiring confirmation
AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts
AGENT_BROWSER_ENGINE Browser engine: chrome (default), lightpanda
AGENT_BROWSER_SCREENSHOT_DIR Default screenshot output directory
AGENT_BROWSER_SCREENSHOT_QUALITY JPEG quality 0-100
AGENT_BROWSER_SCREENSHOT_FORMAT Screenshot format: png, jpeg
Install:
npm install -g agent-browser # npm
brew install agent-browser # Homebrew
cargo install agent-browser # Cargo
agent-browser install # Download Chrome (first time)
Examples:
agent-browser open example.com
agent-browser snapshot -i # Interactive elements only
agent-browser click @e2 # Click by ref from snapshot
agent-browser fill @e3 "test@example.com"
agent-browser find role button click --name Submit
agent-browser get text @e1
agent-browser screenshot --full
agent-browser screenshot --annotate # Labeled screenshot for vision models
agent-browser wait --load networkidle # Wait for slow pages to load
agent-browser --cdp 9222 snapshot # Connect via CDP port
agent-browser --auto-connect snapshot # Auto-discover running Chrome
agent-browser --color-scheme dark open example.com # Dark mode
agent-browser --profile ~/.myapp open example.com # Persistent profile
agent-browser --session-name myapp open example.com # Auto-save/restore state
Command Chaining:
Chain commands with && in a single shell call (browser persists via daemon):
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
agent-browser fill @e1 "user@example.com" && agent-browser fill @e2 "pass" && agent-browser click @e3
agent-browser open example.com && agent-browser wait --load networkidle && agent-browser screenshot page.png
iOS Simulator (requires Xcode and Appium):
agent-browser -p ios open example.com # Use default iPhone
agent-browser -p ios --device "iPhone 15 Pro" open url # Specific device
agent-browser -p ios device list # List simulators
agent-browser -p ios swipe up # Swipe gesture
agent-browser -p ios tap @e1 # Touch element
"#
);
}
fn print_snapshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
let changed = data
.get("changed")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if !changed {
println!("{} No changes detected", color::success_indicator());
return;
}
if let Some(diff) = data.get("diff").and_then(|v| v.as_str()) {
for line in diff.lines() {
if line.starts_with("+ ") {
println!("{}", color::green(line));
} else if line.starts_with("- ") {
println!("{}", color::red(line));
} else {
println!("{}", color::dim(line));
}
}
let additions = data.get("additions").and_then(|v| v.as_i64()).unwrap_or(0);
let removals = data.get("removals").and_then(|v| v.as_i64()).unwrap_or(0);
let unchanged = data.get("unchanged").and_then(|v| v.as_i64()).unwrap_or(0);
println!(
"\n{} additions, {} removals, {} unchanged",
color::green(&additions.to_string()),
color::red(&removals.to_string()),
unchanged
);
}
}
fn print_screenshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
let mismatch = data
.get("mismatchPercentage")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let is_match = data.get("match").and_then(|v| v.as_bool()).unwrap_or(false);
let dim_mismatch = data
.get("dimensionMismatch")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if dim_mismatch {
println!(
"{} Images have different dimensions",
color::error_indicator()
);
} else if is_match {
println!(
"{} Images match (0% difference)",
color::success_indicator()
);
} else {
println!(
"{} {:.2}% pixels differ",
color::error_indicator(),
mismatch
);
}
if let Some(diff_path) = data.get("diffPath").and_then(|v| v.as_str()) {
println!(" Diff image: {}", color::green(diff_path));
}
let total = data
.get("totalPixels")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let different = data
.get("differentPixels")
.and_then(|v| v.as_i64())
.unwrap_or(0);
println!(
" {} different / {} total pixels",
color::red(&different.to_string()),
total
);
}
pub fn print_version() {
println!("agent-browser {}", env!("CARGO_PKG_VERSION"));
}
#[cfg(test)]
mod tests {
use super::format_storage_text;
use serde_json::json;
#[test]
fn test_format_storage_text_for_all_entries() {
let data = json!({
"data": {
"token": "abc123",
"user": "alice"
}
});
let rendered = format_storage_text(&data).unwrap();
assert_eq!(rendered, "token: abc123\nuser: alice");
}
#[test]
fn test_format_storage_text_for_key_lookup() {
let data = json!({
"key": "token",
"value": "abc123"
});
let rendered = format_storage_text(&data).unwrap();
assert_eq!(rendered, "token: abc123");
}
#[test]
fn test_format_storage_text_for_empty_store() {
let data = json!({
"data": {}
});
let rendered = format_storage_text(&data).unwrap();
assert_eq!(rendered, "No storage entries");
}
}