From d86de0e73696b4f2e935e09001996e1ea80a94bc Mon Sep 17 00:00:00 2001 From: elsigh Date: Tue, 13 Jan 2026 12:49:42 -0800 Subject: [PATCH] Add --profile flag for persistent browser profiles Adds support for persistent browser profiles that preserve cookies, localStorage, and login sessions across browser restarts. Changes: - Add --profile CLI flag (flags.rs) - Add AGENT_BROWSER_PROFILE environment variable support - Add profile field to LaunchCommand type (types.ts) - Use launchPersistentContext when profile is specified (browser.ts) - Update help text and README with documentation Usage: agent-browser --profile ~/.myapp-profile open myapp.com This enables AI agents to maintain authenticated sessions across browser restarts without re-authenticating each time. --- README.md | 25 +++++++++++++++++++++++++ cli/src/flags.rs | 10 +++++++++- cli/src/main.rs | 21 +++++++++++++++------ cli/src/output.rs | 2 ++ src/browser.ts | 16 ++++++++++++++++ src/types.ts | 1 + 6 files changed, 68 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bed0b09..7432e07 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,30 @@ Each session has its own: - Navigation history - Authentication state +## Persistent Profiles + +By default, browser state (cookies, localStorage, login sessions) is ephemeral and lost when the browser closes. Use `--profile` to persist state across browser restarts: + +```bash +# Use a persistent profile directory +agent-browser --profile ~/.myapp-profile open myapp.com + +# Login once, then reuse the authenticated session +agent-browser --profile ~/.myapp-profile open myapp.com/dashboard + +# Or via environment variable +AGENT_BROWSER_PROFILE=~/.myapp-profile agent-browser open myapp.com +``` + +The profile directory stores: +- Cookies and localStorage +- IndexedDB data +- Service workers +- Browser cache +- Login sessions + +**Tip**: Use different profile paths for different projects to keep their browser state isolated. + ## Snapshot Options The `snapshot` command supports filtering to reduce output size: @@ -295,6 +319,7 @@ agent-browser snapshot -i -c -d 5 # Combine options | Option | Description | |--------|-------------| | `--session ` | Use isolated session (or `AGENT_BROWSER_SESSION` env) | +| `--profile ` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) | | `--headers ` | Set HTTP headers scoped to the URL's origin | | `--executable-path ` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) | | `--json` | JSON output (for agents) | diff --git a/cli/src/flags.rs b/cli/src/flags.rs index df627ba..1af4080 100644 --- a/cli/src/flags.rs +++ b/cli/src/flags.rs @@ -10,6 +10,7 @@ pub struct Flags { pub executable_path: Option, pub cdp: Option, pub extensions: Vec, + pub profile: Option, } pub fn parse_flags(args: &[String]) -> Flags { @@ -28,6 +29,7 @@ pub fn parse_flags(args: &[String]) -> Flags { executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(), cdp: None, extensions: extensions_env, + profile: env::var("AGENT_BROWSER_PROFILE").ok(), }; let mut i = 0; @@ -67,6 +69,12 @@ pub fn parse_flags(args: &[String]) -> Flags { i += 1; } } + "--profile" => { + if let Some(s) = args.get(i + 1) { + flags.profile = Some(s.clone()); + i += 1; + } + } _ => {} } i += 1; @@ -81,7 +89,7 @@ pub fn clean_args(args: &[String]) -> Vec { // Global flags that should be stripped from command args const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"]; // Global flags that take a value (need to skip the next arg too) - const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension"]; + const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--profile"]; for arg in args.iter() { if skip_next { diff --git a/cli/src/main.rs b/cli/src/main.rs index 8c41015..977b900 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -161,8 +161,8 @@ fn main() { } }; - // Warn if executable_path was specified but daemon was already running - if daemon_result.already_running && (flags.executable_path.is_some() || !flags.extensions.is_empty()) { + // Warn if executable_path, profile, or extensions were specified but daemon was already running + if daemon_result.already_running && (flags.executable_path.is_some() || !flags.extensions.is_empty() || flags.profile.is_some()) { if !flags.json { if flags.executable_path.is_some() { eprintln!("\x1b[33m⚠\x1b[0m --executable-path ignored: daemon already running. Use 'agent-browser close' first to restart with new path."); @@ -170,6 +170,9 @@ fn main() { if !flags.extensions.is_empty() { eprintln!("\x1b[33m⚠\x1b[0m --extension ignored: daemon already running. Use 'agent-browser close' first to restart with extensions."); } + if flags.profile.is_some() { + eprintln!("\x1b[33m⚠\x1b[0m --profile ignored: daemon already running. Use 'agent-browser close' first to restart with profile."); + } } } @@ -229,16 +232,22 @@ fn main() { } // Launch headed browser if --headed flag is set (without CDP) - if flags.headed && flags.cdp.is_none() { - let launch_cmd = json!({ + // Also launch with profile if --profile is set + if (flags.headed || flags.profile.is_some()) && flags.cdp.is_none() { + let mut launch_cmd = json!({ "id": gen_id(), "action": "launch", - "headless": false + "headless": !flags.headed }); + // Add profile path if specified + if let Some(ref profile_path) = flags.profile { + launch_cmd["profile"] = json!(profile_path); + } + if let Err(e) = send_command(launch_cmd, &flags.session) { if !flags.json { - eprintln!("\x1b[33m⚠\x1b[0m Could not launch headed browser: {}", e); + eprintln!("\x1b[33m⚠\x1b[0m Could not launch browser: {}", e); } } } diff --git a/cli/src/output.rs b/cli/src/output.rs index 3ed8751..954244e 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -1189,6 +1189,7 @@ Snapshot Options: Options: --session Isolated session (or AGENT_BROWSER_SESSION env) + --profile Persistent browser profile (or AGENT_BROWSER_PROFILE env) --headers HTTP headers scoped to URL's origin (for auth) --executable-path Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH) --extension Load browser extensions (repeatable). @@ -1207,6 +1208,7 @@ Examples: agent-browser get text @e1 agent-browser screenshot --full agent-browser --cdp 9222 snapshot # Connect via CDP port + agent-browser --profile ~/.myapp open example.com # Persistent profile "# ); } diff --git a/src/browser.ts b/src/browser.ts index f649bd8..e0c20de 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -609,11 +609,16 @@ export class BrowserManager { async launch(options: LaunchCommand): Promise { const cdpPort = options.cdpPort; const hasExtensions = !!options.extensions?.length; + const hasProfile = !!options.profile; if (hasExtensions && cdpPort) { throw new Error('Extensions cannot be used with CDP connection'); } + if (hasProfile && cdpPort) { + throw new Error('Profile cannot be used with CDP connection'); + } + if (this.isLaunched()) { const needsRelaunch = (!cdpPort && this.cdpPort !== null) || (!!cdpPort && this.needsCdpReconnect(cdpPort)); @@ -640,6 +645,7 @@ export class BrowserManager { let context: BrowserContext; if (hasExtensions) { + // Extensions require persistent context in a temp directory const extPaths = options.extensions!.join(','); const session = process.env.AGENT_BROWSER_SESSION || 'default'; context = await launcher.launchPersistentContext( @@ -653,7 +659,17 @@ export class BrowserManager { } ); this.isPersistentContext = true; + } else if (hasProfile) { + // Profile uses persistent context for durable cookies/storage + context = await launcher.launchPersistentContext(options.profile!, { + headless: options.headless ?? true, + executablePath: options.executablePath, + viewport, + extraHTTPHeaders: options.headers, + }); + this.isPersistentContext = true; } else { + // Regular ephemeral browser this.browser = await launcher.launch({ headless: options.headless ?? true, executablePath: options.executablePath, diff --git a/src/types.ts b/src/types.ts index 8290394..f939f53 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface LaunchCommand extends BaseCommand { executablePath?: string; cdpPort?: number; extensions?: string[]; + profile?: string; // Path to persistent browser profile directory } export interface NavigateCommand extends BaseCommand {