Add --profile flag for persistent browser profiles (#68)
* 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 <path> 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. * Expand tilde in profile path to home directory * fix: add missing profile field to test Flags struct --------- Co-authored-by: Chris Tate <chris@ctate.dev>
This commit is contained in:
co-authored by
Chris Tate
parent
c6a92a1472
commit
36cca10c10
@@ -271,6 +271,30 @@ Each session has its own:
|
|||||||
- Navigation history
|
- Navigation history
|
||||||
- Authentication state
|
- 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
|
## Snapshot Options
|
||||||
|
|
||||||
The `snapshot` command supports filtering to reduce output size:
|
The `snapshot` command supports filtering to reduce output size:
|
||||||
@@ -296,6 +320,7 @@ agent-browser snapshot -i -c -d 5 # Combine options
|
|||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
| `--session <name>` | Use isolated session (or `AGENT_BROWSER_SESSION` env) |
|
||||||
|
| `--profile <path>` | Persistent browser profile directory (or `AGENT_BROWSER_PROFILE` env) |
|
||||||
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
| `--headers <json>` | Set HTTP headers scoped to the URL's origin |
|
||||||
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||||
| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
|
| `--args <args>` | Browser launch args, comma or newline separated (or `AGENT_BROWSER_ARGS` env) |
|
||||||
|
|||||||
@@ -1114,6 +1114,7 @@ mod tests {
|
|||||||
executable_path: None,
|
executable_path: None,
|
||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
cdp: None,
|
cdp: None,
|
||||||
|
profile: None,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
proxy_bypass: None,
|
proxy_bypass: None,
|
||||||
args: None,
|
args: None,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct Flags {
|
|||||||
pub executable_path: Option<String>,
|
pub executable_path: Option<String>,
|
||||||
pub cdp: Option<String>,
|
pub cdp: Option<String>,
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
|
pub profile: Option<String>,
|
||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
pub proxy_bypass: Option<String>,
|
pub proxy_bypass: Option<String>,
|
||||||
pub args: Option<String>,
|
pub args: Option<String>,
|
||||||
@@ -33,6 +34,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
||||||
cdp: None,
|
cdp: None,
|
||||||
extensions: extensions_env,
|
extensions: extensions_env,
|
||||||
|
profile: env::var("AGENT_BROWSER_PROFILE").ok(),
|
||||||
proxy: env::var("AGENT_BROWSER_PROXY").ok(),
|
proxy: env::var("AGENT_BROWSER_PROXY").ok(),
|
||||||
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(),
|
proxy_bypass: env::var("AGENT_BROWSER_PROXY_BYPASS").ok(),
|
||||||
args: env::var("AGENT_BROWSER_ARGS").ok(),
|
args: env::var("AGENT_BROWSER_ARGS").ok(),
|
||||||
@@ -77,6 +79,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--profile" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.profile = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
"--proxy" => {
|
"--proxy" => {
|
||||||
if let Some(p) = args.get(i + 1) {
|
if let Some(p) = args.get(i + 1) {
|
||||||
flags.proxy = Some(p.clone());
|
flags.proxy = Some(p.clone());
|
||||||
@@ -127,6 +135,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
|||||||
"--executable-path",
|
"--executable-path",
|
||||||
"--cdp",
|
"--cdp",
|
||||||
"--extension",
|
"--extension",
|
||||||
|
"--profile",
|
||||||
"--proxy",
|
"--proxy",
|
||||||
"--proxy-bypass",
|
"--proxy-bypass",
|
||||||
"--args",
|
"--args",
|
||||||
|
|||||||
+8
-2
@@ -217,6 +217,7 @@ fn main() {
|
|||||||
let ignored_flags: Vec<&str> = [
|
let ignored_flags: Vec<&str> = [
|
||||||
flags.executable_path.as_ref().map(|_| "--executable-path"),
|
flags.executable_path.as_ref().map(|_| "--executable-path"),
|
||||||
if has_extensions { Some("--extension") } else { None },
|
if has_extensions { Some("--extension") } else { None },
|
||||||
|
flags.profile.as_ref().map(|_| "--profile"),
|
||||||
flags.args.as_ref().map(|_| "--args"),
|
flags.args.as_ref().map(|_| "--args"),
|
||||||
flags.user_agent.as_ref().map(|_| "--user-agent"),
|
flags.user_agent.as_ref().map(|_| "--user-agent"),
|
||||||
flags.proxy.as_ref().map(|_| "--proxy"),
|
flags.proxy.as_ref().map(|_| "--proxy"),
|
||||||
@@ -358,8 +359,8 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch headed browser or proxy if flags are set (without CDP or provider)
|
// Launch headed browser or configure browser options (without CDP or provider)
|
||||||
if (flags.headed || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
|
if (flags.headed || flags.profile.is_some() || flags.proxy.is_some() || flags.args.is_some() || flags.user_agent.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
|
||||||
let mut launch_cmd = json!({
|
let mut launch_cmd = json!({
|
||||||
"id": gen_id(),
|
"id": gen_id(),
|
||||||
"action": "launch",
|
"action": "launch",
|
||||||
@@ -369,6 +370,11 @@ fn main() {
|
|||||||
let cmd_obj = launch_cmd.as_object_mut()
|
let cmd_obj = launch_cmd.as_object_mut()
|
||||||
.expect("json! macro guarantees object type");
|
.expect("json! macro guarantees object type");
|
||||||
|
|
||||||
|
// Add profile path if specified
|
||||||
|
if let Some(ref profile_path) = flags.profile {
|
||||||
|
cmd_obj.insert("profile".to_string(), json!(profile_path));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref proxy_str) = flags.proxy {
|
if let Some(ref proxy_str) = flags.proxy {
|
||||||
let mut proxy_obj = parse_proxy(proxy_str);
|
let mut proxy_obj = parse_proxy(proxy_str);
|
||||||
// Add bypass if specified
|
// Add bypass if specified
|
||||||
|
|||||||
@@ -1412,6 +1412,7 @@ Snapshot Options:
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
--session <name> Isolated session (or AGENT_BROWSER_SESSION env)
|
||||||
|
--profile <path> Persistent browser profile (or AGENT_BROWSER_PROFILE env)
|
||||||
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
--headers <json> HTTP headers scoped to URL's origin (for auth)
|
||||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||||
--extension <path> Load browser extensions (repeatable)
|
--extension <path> Load browser extensions (repeatable)
|
||||||
@@ -1443,6 +1444,7 @@ Examples:
|
|||||||
agent-browser get text @e1
|
agent-browser get text @e1
|
||||||
agent-browser screenshot --full
|
agent-browser screenshot --full
|
||||||
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||||
|
agent-browser --profile ~/.myapp open example.com # Persistent profile
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -818,11 +818,16 @@ export class BrowserManager {
|
|||||||
// Determine CDP endpoint: prefer cdpUrl over cdpPort for flexibility
|
// Determine CDP endpoint: prefer cdpUrl over cdpPort for flexibility
|
||||||
const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
|
const cdpEndpoint = options.cdpUrl ?? (options.cdpPort ? String(options.cdpPort) : undefined);
|
||||||
const hasExtensions = !!options.extensions?.length;
|
const hasExtensions = !!options.extensions?.length;
|
||||||
|
const hasProfile = !!options.profile;
|
||||||
|
|
||||||
if (hasExtensions && cdpEndpoint) {
|
if (hasExtensions && cdpEndpoint) {
|
||||||
throw new Error('Extensions cannot be used with CDP connection');
|
throw new Error('Extensions cannot be used with CDP connection');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasProfile && cdpEndpoint) {
|
||||||
|
throw new Error('Profile cannot be used with CDP connection');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.isLaunched()) {
|
if (this.isLaunched()) {
|
||||||
const needsRelaunch =
|
const needsRelaunch =
|
||||||
(!cdpEndpoint && this.cdpEndpoint !== null) ||
|
(!cdpEndpoint && this.cdpEndpoint !== null) ||
|
||||||
@@ -863,6 +868,7 @@ export class BrowserManager {
|
|||||||
|
|
||||||
let context: BrowserContext;
|
let context: BrowserContext;
|
||||||
if (hasExtensions) {
|
if (hasExtensions) {
|
||||||
|
// Extensions require persistent context in a temp directory
|
||||||
const extPaths = options.extensions!.join(',');
|
const extPaths = options.extensions!.join(',');
|
||||||
const session = process.env.AGENT_BROWSER_SESSION || 'default';
|
const session = process.env.AGENT_BROWSER_SESSION || 'default';
|
||||||
// Combine extension args with custom args
|
// Combine extension args with custom args
|
||||||
@@ -881,7 +887,19 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.isPersistentContext = true;
|
this.isPersistentContext = true;
|
||||||
|
} else if (hasProfile) {
|
||||||
|
// Profile uses persistent context for durable cookies/storage
|
||||||
|
// Expand ~ to home directory since it won't be shell-expanded
|
||||||
|
const profilePath = options.profile!.replace(/^~\//, os.homedir() + '/');
|
||||||
|
context = await launcher.launchPersistentContext(profilePath, {
|
||||||
|
headless: options.headless ?? true,
|
||||||
|
executablePath: options.executablePath,
|
||||||
|
viewport,
|
||||||
|
extraHTTPHeaders: options.headers,
|
||||||
|
});
|
||||||
|
this.isPersistentContext = true;
|
||||||
} else {
|
} else {
|
||||||
|
// Regular ephemeral browser
|
||||||
this.browser = await launcher.launch({
|
this.browser = await launcher.launch({
|
||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
cdpPort?: number;
|
cdpPort?: number;
|
||||||
cdpUrl?: string;
|
cdpUrl?: string;
|
||||||
extensions?: string[];
|
extensions?: string[];
|
||||||
|
profile?: string; // Path to persistent browser profile directory
|
||||||
proxy?: {
|
proxy?: {
|
||||||
server: string;
|
server: string;
|
||||||
bypass?: string;
|
bypass?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user