From fcd891e8a99269697a8e961e18b35cb880037777 Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Tue, 10 Mar 2026 12:45:50 +0900 Subject: [PATCH] =?UTF-8?q?fix(cli):=20=E5=A2=9E=E5=8A=A0=20abs=20start=20?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=20managed=209333=20=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/commands.rs | 24 ++++++++ cli/src/main.rs | 101 +++++++++++++++++---------------- cli/src/native/actions.rs | 45 ++++++++++++++- cli/src/native/browser.rs | 49 +++++++++++++--- cli/src/output.rs | 25 ++++++++ docs/src/app/cdp-mode/page.mdx | 7 +++ docs/src/app/commands/page.mdx | 1 + package.json | 4 +- scripts/copy-native.js | 58 ++++++++++++++++--- skills/agent-browser/SKILL.md | 3 + 13 files changed, 255 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 115de9f..31915dd 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ abs install ### Minimal Usage ```bash +agent-browser start agent-browser open https://example.com agent-browser snapshot -i agent-browser click @e2 @@ -176,6 +177,7 @@ flowchart TD - `--profile` / `AGENT_BROWSER_PROFILE` - `--channel` / `AGENT_BROWSER_CHANNEL` - Default CLI policy uses a dedicated automation browser on CDP `localhost:9333`. If `:9333` is unavailable, agent-browser auto-starts Chrome with the persistent profile `~/.agent-browser/chrome-bot-profile`. +- Use `agent-browser start` (or `abs start`) when you want to pre-start that managed `:9333` browser before unattended work begins. ## Principle 2: Multi-Layer Fingerprint Hardening diff --git a/cli/Cargo.lock b/cli/Cargo.lock index ebfed55..566dad7 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "agent-browser-stealth" -version = "0.17.0-fork.1" +version = "0.17.0-fork.2" dependencies = [ "aes-gcm", "async-trait", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index a523212..1b0f300 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-browser-stealth" -version = "0.17.0-fork.1" +version = "0.17.0-fork.2" edition = "2021" description = "Stealth browser automation CLI for AI agents with anti-bot evasions" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index f1226fb..1beafe0 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -215,6 +215,14 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ + "id": id, + "action": "launch", + "cdpPort": 9333, + "headless": !flags.headed + })), "back" => Ok(json!({ "id": id, "action": "back" })), "forward" => Ok(json!({ "id": id, "action": "forward" })), "reload" => Ok(json!({ "id": id, "action": "reload" })), @@ -2363,6 +2371,22 @@ mod tests { assert_eq!(cmd["url"], "https://example.com"); } + #[test] + fn test_start_command_uses_managed_cdp() { + let cmd = parse_command(&args("start"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "launch"); + assert_eq!(cmd["cdpPort"], 9333); + assert_eq!(cmd["headless"], true); + } + + #[test] + fn test_start_command_respects_headed_flag() { + let mut flags = default_flags(); + flags.headed = true; + let cmd = parse_command(&args("start"), &flags).unwrap(); + assert_eq!(cmd["headless"], false); + } + #[test] fn test_navigate_with_headers() { let mut flags = default_flags(); diff --git a/cli/src/main.rs b/cli/src/main.rs index b327842..0e83bc5 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -11,11 +11,13 @@ mod validation; use serde_json::json; use std::env; +use std::net::{SocketAddr, TcpStream}; use std::process::exit; +use std::time::Duration; use commands::{gen_id, parse_command, ParseError}; -use connection::{ensure_daemon, list_live_sessions, send_command}; -use flags::{clean_args, parse_flags}; +use connection::{ensure_daemon, list_live_sessions, send_command, Response}; +use flags::{clean_args, parse_flags, Flags}; use install::run_install; use output::{print_command_help, print_help, print_response, print_version}; @@ -49,6 +51,29 @@ fn parse_proxy(proxy_str: &str) -> serde_json::Value { }) } +fn should_try_default_cdp(flags: &Flags, command_name: Option<&str>) -> bool { + !matches!(command_name, Some("close")) + && flags.cdp.is_none() + && !flags.auto_connect + && flags.provider.is_none() + && flags.executable_path.is_none() + && flags.state.is_none() + && flags.proxy.is_none() + && flags.args.is_none() + && flags.user_agent.is_none() + && !flags.ignore_https_errors + && !flags.allow_file_access + && flags.extensions.is_empty() +} + +fn managed_cdp_port_ready() -> bool { + let addr: SocketAddr = match "127.0.0.1:9333".parse() { + Ok(addr) => addr, + Err(_) => return false, + }; + TcpStream::connect_timeout(&addr, Duration::from_millis(300)).is_ok() +} + fn run_session(args: &[String], session: &str, json_mode: bool) { let subcommand = args.get(1).map(|s| s.as_str()); @@ -113,17 +138,7 @@ fn main() { flags.native = true; } - let can_try_default_cdp = flags.cdp.is_none() - && !flags.auto_connect - && flags.provider.is_none() - && flags.executable_path.is_none() - && flags.state.is_none() - && flags.proxy.is_none() - && flags.args.is_none() - && flags.user_agent.is_none() - && !flags.ignore_https_errors - && !flags.allow_file_access - && flags.extensions.is_empty(); + let can_try_default_cdp = should_try_default_cdp(&flags, command_name); let can_force_native_for_cdp = !matches!(command_name, Some("close")); if !flags.native && can_force_native_for_cdp @@ -622,41 +637,6 @@ fn main() { } } - // Project policy: when no explicit connection mode is provided, - // commands should use the dedicated automation browser on localhost:9333. - // If 9333 is unavailable, the native daemon auto-starts a managed Chrome - // instance with a non-default profile and retries the CDP connection. - if can_try_default_cdp { - let mut launch_cmd = json!({ - "id": gen_id(), - "action": "launch", - "cdpPort": 9333 - }); - - if let Some(ref cs) = flags.color_scheme { - launch_cmd["colorScheme"] = json!(cs); - } - if let Some(ref tg) = flags.tab_group { - launch_cmd["tabGroup"] = json!(tg); - } - if let Some(ref plugin_id) = flags.tab_group_plugin_id { - launch_cmd["tabGroupPluginId"] = json!(plugin_id); - } - - if let Ok(resp) = send_command(launch_cmd, &flags.session) { - attached_to_existing_browser = resp.success; - } - } - if can_try_default_cdp && !attached_to_existing_browser { - let msg = "Project policy requires using the dedicated automation browser on localhost:9333. Could not connect to or auto-start the managed Chrome profile. Start Chrome with --remote-debugging-port=9333 and a non-default --user-data-dir, or pass --cdp / --auto-connect explicitly."; - if flags.json { - println!(r#"{{"success":false,"error":"{}"}}"#, msg); - } else { - eprintln!("{} {}", color::error_indicator(), msg); - } - exit(1); - } - // Launch headed browser or configure browser options (without CDP or provider) if (flags.headed || flags.executable_path.is_some() @@ -673,6 +653,7 @@ fn main() { && flags.cdp.is_none() && flags.provider.is_none() && !attached_to_existing_browser + && !can_try_default_cdp { let mut launch_cmd = json!({ "id": gen_id(), @@ -782,6 +763,18 @@ fn main() { } } Err(e) => { + let is_start = command_name == Some("start") + && cmd.get("action").and_then(|v| v.as_str()) == Some("launch") + && cmd.get("cdpPort").and_then(|v| v.as_u64()) == Some(9333); + if is_start && managed_cdp_port_ready() { + let resp = Response { + success: true, + data: Some(json!({ "launched": true })), + error: None, + }; + print_response(&resp, flags.json, Some("launch")); + return; + } if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, e); } else { @@ -849,4 +842,16 @@ mod tests { assert_eq!(result["username"], "user"); assert_eq!(result["password"], "p@ss:w0rd"); } + + #[test] + fn test_should_try_default_cdp_for_open() { + let flags = parse_flags(&[]); + assert!(should_try_default_cdp(&flags, Some("open"))); + } + + #[test] + fn test_should_not_try_default_cdp_for_close() { + let flags = parse_flags(&[]); + assert!(!should_try_default_cdp(&flags, Some("close"))); + } } diff --git a/cli/src/native/actions.rs b/cli/src/native/actions.rs index ccf679b..5c06da4 100644 --- a/cli/src/native/actions.rs +++ b/cli/src/native/actions.rs @@ -729,8 +729,12 @@ pub async fn execute_command(cmd: &Value, state: &mut DaemonState) -> Value { async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { let options = launch_options_from_env(); let engine = env::var("AGENT_BROWSER_ENGINE").ok(); + let debug_enabled = env::var("AGENT_BROWSER_DEBUG").as_deref() == Ok("1"); if let Ok(cdp) = env::var("AGENT_BROWSER_CDP") { + if debug_enabled { + eprintln!("[DEBUG] auto_launch: connecting via AGENT_BROWSER_CDP={}", cdp); + } let mgr = BrowserManager::connect_cdp(&cdp).await?; state.browser = Some(mgr); state.subscribe_to_browser_events(); @@ -739,6 +743,9 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { } if env::var("AGENT_BROWSER_AUTO_CONNECT").is_ok() { + if debug_enabled { + eprintln!("[DEBUG] auto_launch: connecting via AGENT_BROWSER_AUTO_CONNECT"); + } let mgr = BrowserManager::connect_auto().await?; state.browser = Some(mgr); state.subscribe_to_browser_events(); @@ -746,13 +753,36 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> { return Ok(()); } - let mgr = BrowserManager::launch(options, engine.as_deref()).await?; + let mgr = if should_auto_launch_managed_cdp(&options) { + if debug_enabled { + eprintln!("[DEBUG] auto_launch: launching managed Chrome on localhost:9333"); + } + BrowserManager::launch_managed_cdp(options.executable_path.clone(), !options.headless) + .await? + } else { + if debug_enabled { + eprintln!("[DEBUG] auto_launch: launching local browser from env options"); + } + BrowserManager::launch(options, engine.as_deref()).await? + }; state.browser = Some(mgr); state.subscribe_to_browser_events(); try_auto_restore_state(state).await; Ok(()) } +fn should_auto_launch_managed_cdp(options: &LaunchOptions) -> bool { + options.proxy.is_none() + && options.proxy_bypass.is_none() + && options.profile.is_none() + && !options.allow_file_access + && options.args.is_empty() + && options.extensions.as_ref().is_none_or(|ext| ext.is_empty()) + && options.storage_state.is_none() + && options.user_agent.is_none() + && !options.ignore_https_errors +} + fn launch_options_from_env() -> LaunchOptions { let headed = env::var("AGENT_BROWSER_HEADED") .map(|v| v == "1" || v == "true") @@ -5225,6 +5255,19 @@ mod tests { ); } + #[test] + fn test_should_auto_launch_managed_cdp_for_bare_defaults() { + let opts = launch_options_from_env(); + assert!(should_auto_launch_managed_cdp(&opts)); + } + + #[test] + fn test_should_not_auto_launch_managed_cdp_when_proxy_is_set() { + let mut opts = launch_options_from_env(); + opts.proxy = Some("http://127.0.0.1:8080".to_string()); + assert!(!should_auto_launch_managed_cdp(&opts)); + } + #[tokio::test] async fn test_execute_unknown_command() { let mut state = DaemonState::new(); diff --git a/cli/src/native/browser.rs b/cli/src/native/browser.rs index 3202103..6128f33 100644 --- a/cli/src/native/browser.rs +++ b/cli/src/native/browser.rs @@ -111,6 +111,11 @@ pub fn to_ai_friendly_error(error: &str) -> String { error.to_string() } +fn is_startup_internal_page(url: &str) -> bool { + let lower = url.to_lowercase(); + lower.starts_with("chrome://profile-picker") +} + #[derive(Debug, Clone)] pub struct PageInfo { pub target_id: String, @@ -416,8 +421,22 @@ impl BrowserManager { if attached_pages.is_empty() { self.create_and_attach_blank_page().await?; } else { + let preferred_index = attached_pages + .iter() + .position(|page| !is_startup_internal_page(&page.url)); + self.pages = attached_pages; - self.active_page_index = 0; + + if let Some(index) = preferred_index { + self.active_page_index = index; + } else { + if std::env::var("AGENT_BROWSER_DEBUG").as_deref() == Ok("1") { + eprintln!( + "[DEBUG] All discovered pages were Chrome startup pages; creating a fresh about:blank target" + ); + } + self.create_and_attach_blank_page().await?; + } } } @@ -450,6 +469,7 @@ impl BrowserManager { pub async fn navigate(&mut self, url: &str, wait_until: WaitUntil) -> Result { let session_id = self.active_session_id()?.to_string(); + let rx = self.client.subscribe(); let nav_result: PageNavigateResult = self .client @@ -467,7 +487,7 @@ impl BrowserManager { return Err(format!("Navigation failed: {}", error_text)); } - self.wait_for_lifecycle(wait_until, &session_id).await?; + self.wait_for_lifecycle(wait_until, &session_id, rx).await?; let page_url = self.get_url().await.unwrap_or_else(|_| url.to_string()); let title = self.get_title().await.unwrap_or_default(); @@ -484,14 +504,14 @@ impl BrowserManager { &self, wait_until: WaitUntil, session_id: &str, + mut rx: tokio::sync::broadcast::Receiver, ) -> Result<(), String> { let event_name = match wait_until { WaitUntil::Load => "Page.loadEventFired", WaitUntil::DomContentLoaded => "Page.domContentEventFired", - WaitUntil::NetworkIdle => return self.wait_for_network_idle(session_id).await, + WaitUntil::NetworkIdle => return self.wait_for_network_idle(session_id, rx).await, }; - let mut rx = self.client.subscribe(); let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms); tokio::time::timeout(timeout, async { @@ -506,8 +526,11 @@ impl BrowserManager { .map_err(|_| format!("Timeout waiting for {}", event_name))? } - async fn wait_for_network_idle(&self, session_id: &str) -> Result<(), String> { - let mut rx = self.client.subscribe(); + async fn wait_for_network_idle( + &self, + session_id: &str, + mut rx: tokio::sync::broadcast::Receiver, + ) -> Result<(), String> { let pending = Arc::new(Mutex::new(HashSet::::new())); let timeout = tokio::time::Duration::from_millis(self.default_timeout_ms); @@ -626,7 +649,8 @@ impl BrowserManager { wait_until: WaitUntil, session_id: &str, ) -> Result<(), String> { - self.wait_for_lifecycle(wait_until, session_id).await + self.wait_for_lifecycle(wait_until, session_id, self.client.subscribe()) + .await } pub async fn close(&mut self) -> Result<(), String> { @@ -1270,4 +1294,15 @@ mod tests { let msg = "Some custom error message"; assert_eq!(to_ai_friendly_error(msg), msg); } + + #[test] + fn test_is_startup_internal_page_detects_profile_picker() { + assert!(is_startup_internal_page("chrome://profile-picker/")); + } + + #[test] + fn test_is_startup_internal_page_ignores_normal_pages() { + assert!(!is_startup_internal_page("https://example.com/")); + assert!(!is_startup_internal_page("about:blank")); + } } diff --git a/cli/src/output.rs b/cli/src/output.rs index 3136166..ca95783 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -435,6 +435,12 @@ pub fn print_response_with_opts(resp: &Response, action: Option<&str>, opts: &Ou } return; } + // Launch/start + if data.get("launched").is_some() { + let label = "Browser ready"; + println!("{} {}", color::success_indicator(), label); + return; + } // Closed (browser or tab) if data.get("closed").is_some() { let label = match action { @@ -884,6 +890,24 @@ Examples: 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 +"## + } + "start" => { + r##" +agent-browser start - Start the managed automation browser on localhost:9333 + +Usage: agent-browser start + +Starts or reuses the dedicated automation Chrome profile on localhost:9333 +without navigating to a page. Use this to pre-warm the managed browser for +unattended runs. + +Global Options: + --json Output as JSON + +Examples: + agent-browser start + abs start "## } "back" => { @@ -2339,6 +2363,7 @@ Aliases: agent-browser, agent-browser-stealth, abs Core Commands: open Navigate to URL + start Start managed browser on localhost:9333 click Click element (or @ref) dblclick Double-click element type [--delay ] Type into element diff --git a/docs/src/app/cdp-mode/page.mdx b/docs/src/app/cdp-mode/page.mdx index a4adcd2..20e837c 100644 --- a/docs/src/app/cdp-mode/page.mdx +++ b/docs/src/app/cdp-mode/page.mdx @@ -8,6 +8,13 @@ Connect to an existing browser via Chrome DevTools Protocol: Default behavior in this fork: when `--cdp` is omitted, agent-browser targets the managed automation browser on `localhost:9333`. If `:9333` is unavailable, it auto-starts Chrome with the persistent profile `~/.agent-browser/chrome-bot-profile` and retries the CDP connection. +If you want to pre-start that managed browser explicitly, run: + +```bash +agent-browser start +abs start +``` + Project policy: - `--profile` / `AGENT_BROWSER_PROFILE` are forbidden diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index 424bbc9..fa82b11 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -9,6 +9,7 @@ Executable aliases: `agent-browser`, `agent-browser-stealth`, `abs`. ## Core ```bash +agent-browser start # Start/reuse managed browser on localhost:9333 agent-browser open # Navigate (aliases: goto, navigate) agent-browser --risk-mode block open # Block when verification/captcha interstitial is detected agent-browser click # Click element (--new-tab to open in new tab) diff --git a/package.json b/package.json index 1976b96..04ba801 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-browser-stealth", - "version": "0.17.0-fork.1", + "version": "0.17.0-fork.2", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions", "type": "module", "main": "dist/daemon.js", @@ -23,7 +23,7 @@ "build": "tsc", "build:native": "npm run version:sync && npm run native:clean && cargo build --release --manifest-path cli/Cargo.toml && node scripts/copy-native.js", "build:linux": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-linux", - "build:macos": "npm run version:sync && npm run native:clean && (cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin & wait) && cp cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && cp cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64", + "build:macos": "npm run version:sync && npm run native:clean && (cargo build --release --manifest-path cli/Cargo.toml --target aarch64-apple-darwin & cargo build --release --manifest-path cli/Cargo.toml --target x86_64-apple-darwin & wait) && node scripts/copy-native.js cli/target/aarch64-apple-darwin/release/agent-browser bin/agent-browser-darwin-arm64 && node scripts/copy-native.js cli/target/x86_64-apple-darwin/release/agent-browser bin/agent-browser-darwin-x64", "build:windows": "npm run version:sync && docker compose -f docker/docker-compose.yml run --rm build-windows", "build:all-platforms": "npm run version:sync && (npm run build:linux & npm run build:windows & wait) && npm run build:macos", "build:docker": "docker build -t agent-browser-builder -f docker/Dockerfile.build .", diff --git a/scripts/copy-native.js b/scripts/copy-native.js index 864b326..051b5d5 100644 --- a/scripts/copy-native.js +++ b/scripts/copy-native.js @@ -1,26 +1,67 @@ #!/usr/bin/env node /** - * Copies the compiled Rust binary to bin/ with platform-specific naming + * Copies the compiled Rust binary to bin/ with platform-specific naming. + * On macOS, re-apply an ad-hoc signature after copying so the binary remains + * executable from the packaged bin/ path. */ import { copyFileSync, existsSync, mkdirSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { platform, arch } from 'os'; +import { spawnSync } from 'child_process'; const __dirname = dirname(fileURLToPath(import.meta.url)); const projectRoot = join(__dirname, '..'); -const sourceExt = platform() === 'win32' ? '.exe' : ''; -const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`); const binDir = join(projectRoot, 'bin'); -// Determine platform suffix -const platformKey = `${platform()}-${arch()}`; -const ext = platform() === 'win32' ? '.exe' : ''; -const targetName = `agent-browser-${platformKey}${ext}`; -const targetPath = join(binDir, targetName); +function defaultPaths() { + const sourceExt = platform() === 'win32' ? '.exe' : ''; + const sourcePath = join(projectRoot, `cli/target/release/agent-browser${sourceExt}`); + + const platformKey = `${platform()}-${arch()}`; + const ext = platform() === 'win32' ? '.exe' : ''; + const targetName = `agent-browser-${platformKey}${ext}`; + const targetPath = join(binDir, targetName); + + return { sourcePath, targetPath }; +} + +function resolvePaths() { + const [sourceArg, targetArg] = process.argv.slice(2); + if (!sourceArg && !targetArg) { + return defaultPaths(); + } + if (!sourceArg || !targetArg) { + console.error('Usage: node scripts/copy-native.js [source-binary target-binary]'); + process.exit(1); + } + return { + sourcePath: join(projectRoot, sourceArg), + targetPath: join(projectRoot, targetArg), + }; +} + +function adHocSignIfNeeded(targetPath) { + if (platform() !== 'darwin') { + return; + } + + const result = spawnSync('codesign', ['--force', '--sign', '-', targetPath], { + stdio: 'pipe', + encoding: 'utf8', + }); + + if (result.status !== 0) { + const message = result.stderr?.trim() || result.stdout?.trim() || 'unknown codesign error'; + console.error(`Error: Failed to codesign ${targetPath}: ${message}`); + process.exit(result.status ?? 1); + } +} + +const { sourcePath, targetPath } = resolvePaths(); if (!existsSync(sourcePath)) { console.error(`Error: Native binary not found at ${sourcePath}`); @@ -33,4 +74,5 @@ if (!existsSync(binDir)) { } copyFileSync(sourcePath, targetPath); +adHocSignIfNeeded(targetPath); console.log(`✓ Copied native binary to ${targetPath}`); diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index 5127d24..352da32 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -8,6 +8,8 @@ allowed-tools: Bash(npx agent-browser-stealth:*), Bash(npx agent-browser:*), Bas Install package: `pnpm add -g agent-browser-stealth` (CLI commands: `agent-browser`, `agent-browser-stealth`, and short alias `abs`). If global install is unavailable in your environment, use `pnpm dlx agent-browser-stealth ` for one-off runs. +Use `agent-browser start` when you want to pre-warm the managed automation browser on `localhost:9333` before the actual navigation or task commands run. + ## Core Workflow Every browser automation follows this pattern: @@ -50,6 +52,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle ```bash # Navigation +agent-browser start # Start/reuse managed browser on localhost:9333 agent-browser open # Navigate (aliases: goto, navigate) agent-browser --risk-mode block open # Block if verification/captcha interstitial is detected agent-browser doctor # Diagnose CDP + sourceURL + tab-group plugin health