diff --git a/cli/src/commands.rs b/cli/src/commands.rs index d113616..efdfd97 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -345,10 +345,8 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result().is_ok() { - Ok( - json!({ "id": id, "action": "wait", "timeout": arg.parse::().unwrap() }), - ) + if let Ok(timeout) = arg.parse::() { + Ok(json!({ "id": id, "action": "wait", "timeout": timeout })) } else { Ok(json!({ "id": id, "action": "wait", "selector": arg })) } @@ -684,7 +682,8 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result().is_ok() => { - Ok(json!({ "id": id, "action": "tab_switch", "index": n.parse::().unwrap() })) + let index = n.parse::().expect("already checked parse succeeds"); + Ok(json!({ "id": id, "action": "tab_switch", "index": index })) } _ => Ok(json!({ "id": id, "action": "tab_list" })), }, @@ -746,11 +745,11 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result Ok(json!({ "id": id, "action": "trace_start" })), Some("stop") => { - let path = rest.get(1).ok_or_else(|| ParseError::MissingArguments { - context: "trace stop".to_string(), - usage: "trace stop ", - })?; - Ok(json!({ "id": id, "action": "trace_stop", "path": path })) + let mut cmd = json!({ "id": id, "action": "trace_stop" }); + if let Some(path) = rest.get(1) { + cmd["path"] = json!(path); + } + Ok(cmd) } Some(sub) => Err(ParseError::UnknownSubcommand { subcommand: sub.to_string(), @@ -2572,4 +2571,26 @@ mod tests { assert_eq!(cmd["action"], "launch"); assert_eq!(cmd["cdpPort"], 1); } + + // === Trace Tests === + + #[test] + fn test_trace_start() { + let cmd = parse_command(&args("trace start"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "trace_start"); + } + + #[test] + fn test_trace_stop_with_path() { + let cmd = parse_command(&args("trace stop ./trace.zip"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "trace_stop"); + assert_eq!(cmd["path"], "./trace.zip"); + } + + #[test] + fn test_trace_stop_without_path() { + let cmd = parse_command(&args("trace stop"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "trace_stop"); + assert!(cmd.get("path").is_none() || cmd["path"].is_null()); + } } diff --git a/cli/src/main.rs b/cli/src/main.rs index 7be0f15..c5c1d51 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -470,7 +470,7 @@ fn main() { if flags.json { println!(r#"{{"success":false,"error":"{}"}}"#, msg); } else { - eprintln!("\x1b[31m✗\x1b[0m {}", msg); + eprintln!("{} {}", color::error_indicator(), msg); } exit(1); } diff --git a/cli/src/output.rs b/cli/src/output.rs index f9fa3c2..b401b87 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -344,6 +344,11 @@ pub fn print_response(resp: &Response, json_mode: bool, action: Option<&str>) { 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("") { diff --git a/docs/src/app/changelog/page.mdx b/docs/src/app/changelog/page.mdx index 2092651..5134fab 100644 --- a/docs/src/app/changelog/page.mdx +++ b/docs/src/app/changelog/page.mdx @@ -2,6 +2,210 @@ export const metadata = { title: "Changelog" } # Changelog +## v0.10.0 + +

February 2026

+ +### New Features + +- **Session persistence** - Automatic save/restore of cookies and localStorage across browser restarts using `--session-name` flag +- **Encrypted state** - Optional AES-256-GCM encryption for saved session state data +- **State management commands** - New commands for listing, showing, renaming, clearing, and cleaning up session state files +- **New tab on click** - Added `--new-tab` option for click commands to open links in new tabs + +```bash +# Persist session state +agent-browser --session-name myapp open https://example.com + +# Manage saved states +agent-browser state list +agent-browser state show myapp +agent-browser state clear myapp +``` + +--- + +## v0.9.4 + +

February 2026

+ +### Bug Fixes + +- Fixed all Clippy lint warnings in the Rust CLI + +--- + +## v0.9.3 + +

February 2026

+ +### Improvements + +- Added support for custom executable path in CLI browser launch options +- Documentation site UI improvements including a new chat component with sheet-based interface + +--- + +## v0.9.2 + +

February 2026

+ +### Improvements + +- Migrated documentation site to MDX for improved content authoring +- Added AI-powered docs chat feature +- Updated README with Homebrew installation instructions for macOS users + +--- + +## v0.9.1 + +

February 2026

+ +### New Features + +- **`--allow-file-access` flag** - Enable opening and interacting with local `file://` URLs (PDFs, HTML files) by passing Chromium flags that allow JavaScript access to local files +- **`-C`/`--cursor` flag for snapshots** - Include cursor-interactive elements like divs with onclick handlers or `cursor:pointer` styles + +```bash +agent-browser --allow-file-access open file:///path/to/document.pdf +agent-browser snapshot -C +``` + +--- + +## v0.9.0 + +

February 2026

+ +### New Features + +- **iOS Simulator support** - Mobile Safari testing via Appium with real device and simulator support + +```bash +# List available iOS simulators +agent-browser device list + +# Launch on iOS device +agent-browser -p ios --device "iPhone 16 Pro" open https://example.com + +# Touch interactions +agent-browser tap @e1 +agent-browser swipe up +``` + +--- + +## v0.8.10 + +

January 2026

+ +### Improvements + +- Added `--stdin` flag for eval command to read JavaScript from stdin, enabling heredoc usage for multiline scripts +- Fixed binary permission issues on macOS/Linux when postinstall scripts don't run + +--- + +## v0.8.9 + +

January 2026

+ +### Improvements + +- Added `--stdin` flag for eval command to read JavaScript from stdin + +--- + +## v0.8.8 + +

January 2026

+ +### Improvements + +- Added base64 encoding support for the eval command with `-b`/`--base64` flag to avoid shell escaping issues +- Updated documentation with AI agent setup instructions + +--- + +## v0.8.7 + +

January 2026

+ +### Bug Fixes + +- Fixed browser launch options not being passed correctly when using persistent profiles +- Added pre-flight checks for socket path length limits and directory write permissions +- Improved error handling to properly exit with failure status when browser launch fails + +--- + +## v0.8.6 + +

January 2026

+ +### Bug Fixes + +- Improved daemon connection reliability with automatic retry logic for transient errors +- CLI now cleans up stale socket and PID files before starting a new daemon + +--- + +## v0.8.5 + +

January 2026

+ +### Bug Fixes + +- Fixed version synchronization to automatically update Cargo.lock alongside Cargo.toml during releases +- Made the CLI binary executable in the npm package + +--- + +## v0.8.4 + +

January 2026

+ +### Bug Fixes + +- Fixed "Daemon not found" error when running through AI agents by resolving symlinks in the executable path + +--- + +## v0.8.3 + +

January 2026

+ +### Improvements + +- Replaced shell-based CLI wrappers with a cross-platform Node.js wrapper to enable npx support on Windows +- Added postinstall logic to patch npm bin entry on global installs for zero-overhead native binary invocation +- Added CI tests to verify global installation across all platforms + +--- + +## v0.8.2 + +

January 2026

+ +### Bug Fixes + +- Fixed the Windows CMD wrapper to use the native binary directly instead of routing through Node.js +- Added retry logic to CI install command for transient browser installation failures + +--- + +## v0.8.1 + +

January 2026

+ +### Improvements + +- Improved release workflow to validate binary file sizes and ensure binaries are executable after npm install +- Updated documentation site with a new mobile navigation system + +--- + ## v0.8.0

January 2026

diff --git a/src/actions.ts b/src/actions.ts index 22314e0..1ec9b5f 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -1442,7 +1442,10 @@ async function handleTraceStop( browser: BrowserManager ): Promise { await browser.stopTracing(command.path); - return successResponse(command.id, { path: command.path }); + return successResponse( + command.id, + command.path ? { path: command.path } : { traceStopped: true } + ); } async function handleHarStart( diff --git a/src/browser.ts b/src/browser.ts index d8c7298..365ae8e 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -678,10 +678,10 @@ export class BrowserManager { /** * Stop tracing and save */ - async stopTracing(path: string): Promise { + async stopTracing(path?: string): Promise { const context = this.contexts[0]; if (context) { - await context.tracing.stop({ path }); + await context.tracing.stop(path ? { path } : undefined); } } diff --git a/src/protocol.test.ts b/src/protocol.test.ts index 68d6e19..a652066 100644 --- a/src/protocol.test.ts +++ b/src/protocol.test.ts @@ -582,6 +582,22 @@ describe('parseCommand', () => { const result = parseCommand(cmd({ id: '1', action: 'launch', ignoreHTTPSErrors: 'true' })); expect(result.success).toBe(false); }); + + it('should parse launch with allowFileAccess true', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', allowFileAccess: true })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.allowFileAccess).toBe(true); + } + }); + + it('should parse launch with allowFileAccess false', () => { + const result = parseCommand(cmd({ id: '1', action: 'launch', allowFileAccess: false })); + expect(result.success).toBe(true); + if (result.success) { + expect(result.command.allowFileAccess).toBe(false); + } + }); }); describe('mouse actions', () => { @@ -1108,6 +1124,46 @@ describe('parseCommand', () => { }); }); + describe('addscript and addstyle', () => { + it('should parse addscript with content', () => { + const result = parseCommand( + cmd({ id: '1', action: 'addscript', content: 'console.log("hi")' }) + ); + expect(result.success).toBe(true); + }); + + it('should parse addscript with url', () => { + const result = parseCommand( + cmd({ id: '1', action: 'addscript', url: 'https://example.com/script.js' }) + ); + expect(result.success).toBe(true); + }); + + it('should reject addscript with neither content nor url', () => { + const result = parseCommand(cmd({ id: '1', action: 'addscript' })); + expect(result.success).toBe(false); + }); + + it('should parse addstyle with content', () => { + const result = parseCommand( + cmd({ id: '1', action: 'addstyle', content: 'body { color: red }' }) + ); + expect(result.success).toBe(true); + }); + + it('should parse addstyle with url', () => { + const result = parseCommand( + cmd({ id: '1', action: 'addstyle', url: 'https://example.com/style.css' }) + ); + expect(result.success).toBe(true); + }); + + it('should reject addstyle with neither content nor url', () => { + const result = parseCommand(cmd({ id: '1', action: 'addstyle' })); + expect(result.success).toBe(false); + }); + }); + describe('invalid commands', () => { it('should reject unknown action', () => { const result = parseCommand(cmd({ id: '1', action: 'unknown' })); diff --git a/src/protocol.ts b/src/protocol.ts index cb543ed..62bc9df 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -47,6 +47,7 @@ const launchSchema = baseCommandSchema.extend({ userAgent: z.string().optional(), provider: z.string().optional(), ignoreHTTPSErrors: z.boolean().optional(), + allowFileAccess: z.boolean().optional(), profile: z.string().optional(), storageState: z.string().optional(), }); @@ -369,7 +370,7 @@ const traceStartSchema = baseCommandSchema.extend({ const traceStopSchema = baseCommandSchema.extend({ action: z.literal('trace_stop'), - path: z.string().min(1), + path: z.string().min(1).optional(), }); const harStartSchema = baseCommandSchema.extend({ @@ -989,7 +990,18 @@ export function parseCommand(input: string): ParseResult { return { success: false, error: `Validation error: ${errors}`, id }; } - return { success: true, command: result.data as Command }; + const command = result.data as Command; + + // Post-parse validation for commands that need cross-field checks + if ( + (command.action === 'addscript' || command.action === 'addstyle') && + !command.content && + !command.url + ) { + return { success: false, error: 'Either content or url must be provided', id }; + } + + return { success: true, command }; } /** diff --git a/src/snapshot.ts b/src/snapshot.ts index 22caaa4..40a205f 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -132,8 +132,8 @@ const STRUCTURAL_ROLES = new Set([ */ function buildSelector(role: string, name?: string): string { if (name) { - const escapedName = name.replace(/"/g, '\\"'); - return `getByRole('${role}', { name: "${escapedName}", exact: true })`; + const escapedName = JSON.stringify(name); + return `getByRole('${role}', { name: ${escapedName}, exact: true })`; } return `getByRole('${role}')`; } @@ -307,7 +307,7 @@ export async function getEnhancedSnapshot( existingTexts.add(elTextLower); const ref = nextRef(); - const role = el.hasCursorPointer ? 'clickable' : el.hasOnClick ? 'clickable' : 'focusable'; + const role = el.hasCursorPointer || el.hasOnClick ? 'clickable' : 'focusable'; refs[ref] = { selector: el.selector, diff --git a/src/types.ts b/src/types.ts index 06a489d..004f9e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -576,7 +576,7 @@ export interface TraceStartCommand extends BaseCommand { export interface TraceStopCommand extends BaseCommand { action: 'trace_stop'; - path: string; + path?: string; } // HAR recording