Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3a302056b | ||
|
|
0eb5936f4b | ||
|
|
a533ee8aea | ||
|
|
c6d5f9bca1 | ||
|
|
400dc8b850 |
@@ -18,9 +18,9 @@ git clone https://github.com/vercel-labs/agent-browser
|
||||
cd agent-browser
|
||||
pnpm install
|
||||
pnpm build
|
||||
pnpm build:native # Requires Rust (https://rustup.rs)
|
||||
pnpm link --global # Makes agent-browser available globally
|
||||
agent-browser install
|
||||
pnpm build:native
|
||||
./bin/agent-browser install
|
||||
pnpm link --global
|
||||
```
|
||||
|
||||
### Linux Dependencies
|
||||
@@ -270,30 +270,6 @@ 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:
|
||||
@@ -319,7 +295,6 @@ agent-browser snapshot -i -c -d 5 # Combine options
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--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 |
|
||||
| `--executable-path <path>` | Custom browser executable (or `AGENT_BROWSER_EXECUTABLE_PATH` env) |
|
||||
| `--json` | JSON output (for agents) |
|
||||
|
||||
@@ -901,8 +901,6 @@ mod tests {
|
||||
debug: false,
|
||||
headers: None,
|
||||
executable_path: None,
|
||||
extensions: Vec::new(),
|
||||
cdp: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-17
@@ -159,16 +159,9 @@ pub struct DaemonResult {
|
||||
pub already_running: bool,
|
||||
}
|
||||
|
||||
pub fn ensure_daemon(
|
||||
session: &str,
|
||||
headed: bool,
|
||||
executable_path: Option<&str>,
|
||||
extensions: &[String],
|
||||
) -> Result<DaemonResult, String> {
|
||||
pub fn ensure_daemon(session: &str, headed: bool, executable_path: Option<&str>) -> Result<DaemonResult, String> {
|
||||
if is_daemon_running(session) && daemon_ready(session) {
|
||||
return Ok(DaemonResult {
|
||||
already_running: true,
|
||||
});
|
||||
return Ok(DaemonResult { already_running: true });
|
||||
}
|
||||
|
||||
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
|
||||
@@ -203,10 +196,6 @@ pub fn ensure_daemon(
|
||||
cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path);
|
||||
}
|
||||
|
||||
if !extensions.is_empty() {
|
||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||
}
|
||||
|
||||
// Create new process group and session to fully detach
|
||||
unsafe {
|
||||
cmd.pre_exec(|| {
|
||||
@@ -245,10 +234,6 @@ pub fn ensure_daemon(
|
||||
cmd.env("AGENT_BROWSER_EXECUTABLE_PATH", path);
|
||||
}
|
||||
|
||||
if !extensions.is_empty() {
|
||||
cmd.env("AGENT_BROWSER_EXTENSIONS", extensions.join(","));
|
||||
}
|
||||
|
||||
// CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const DETACHED_PROCESS: u32 = 0x00000008;
|
||||
|
||||
+2
-23
@@ -9,16 +9,9 @@ pub struct Flags {
|
||||
pub headers: Option<String>,
|
||||
pub executable_path: Option<String>,
|
||||
pub cdp: Option<String>,
|
||||
pub extensions: Vec<String>,
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
let extensions_env = env::var("AGENT_BROWSER_EXTENSIONS")
|
||||
.ok()
|
||||
.map(|s| s.split(',').map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut flags = Flags {
|
||||
json: false,
|
||||
full: false,
|
||||
@@ -28,8 +21,6 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
headers: None,
|
||||
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;
|
||||
@@ -56,25 +47,13 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
flags.executable_path = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
"--extension" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.extensions.push(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
"--cdp" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.cdp = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--profile" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.profile = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
@@ -89,7 +68,7 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
// 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", "--profile"];
|
||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp"];
|
||||
|
||||
for arg in args.iter() {
|
||||
if skip_next {
|
||||
|
||||
+8
-22
@@ -149,7 +149,7 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
let daemon_result = match ensure_daemon(&flags.session, flags.headed, flags.executable_path.as_deref(), &flags.extensions) {
|
||||
let daemon_result = match ensure_daemon(&flags.session, flags.headed, flags.executable_path.as_deref()) {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
if flags.json {
|
||||
@@ -161,18 +161,10 @@ fn main() {
|
||||
}
|
||||
};
|
||||
|
||||
// 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()) {
|
||||
// Warn if executable_path was specified but daemon was already running
|
||||
if daemon_result.already_running && flags.executable_path.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.");
|
||||
}
|
||||
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.");
|
||||
}
|
||||
eprintln!("\x1b[33m⚠\x1b[0m --executable-path ignored: daemon already running. Use 'agent-browser close' first to restart with new path.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,22 +224,16 @@ fn main() {
|
||||
}
|
||||
|
||||
// Launch headed browser if --headed flag is set (without CDP)
|
||||
// Also launch with profile if --profile is set
|
||||
if (flags.headed || flags.profile.is_some()) && flags.cdp.is_none() {
|
||||
let mut launch_cmd = json!({
|
||||
if flags.headed && flags.cdp.is_none() {
|
||||
let launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"headless": !flags.headed
|
||||
"headless": false
|
||||
});
|
||||
|
||||
// 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 browser: {}", e);
|
||||
eprintln!("\x1b[33m⚠\x1b[0m Could not launch headed browser: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1189,10 +1189,8 @@ Snapshot Options:
|
||||
|
||||
Options:
|
||||
--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)
|
||||
--executable-path <path> Custom browser executable (or AGENT_BROWSER_EXECUTABLE_PATH)
|
||||
--extension <path> Load browser extensions (repeatable).
|
||||
--json JSON output
|
||||
--full, -f Full page screenshot
|
||||
--headed Show browser window (not headless)
|
||||
@@ -1208,7 +1206,6 @@ 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
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
+24
-60
@@ -12,8 +12,6 @@ import {
|
||||
type Route,
|
||||
type Locator,
|
||||
} from 'playwright-core';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import type { LaunchCommand } from './types.js';
|
||||
import { type RefMap, type EnhancedSnapshot, getEnhancedSnapshot, parseRef } from './snapshot.js';
|
||||
|
||||
@@ -42,7 +40,6 @@ interface PageError {
|
||||
export class BrowserManager {
|
||||
private browser: Browser | null = null;
|
||||
private cdpPort: number | null = null;
|
||||
private isPersistentContext: boolean = false;
|
||||
private contexts: BrowserContext[] = [];
|
||||
private pages: Page[] = [];
|
||||
private activePageIndex: number = 0;
|
||||
@@ -61,7 +58,7 @@ export class BrowserManager {
|
||||
* Check if browser is launched
|
||||
*/
|
||||
isLaunched(): boolean {
|
||||
return this.browser !== null || this.isPersistentContext;
|
||||
return this.browser !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -608,21 +605,12 @@ export class BrowserManager {
|
||||
*/
|
||||
async launch(options: LaunchCommand): Promise<void> {
|
||||
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 (this.browser) {
|
||||
const switchingFromCdpToBrowser = !cdpPort && this.cdpPort !== null;
|
||||
const needsCdpReconnect = !!cdpPort && this.needsCdpReconnect(cdpPort);
|
||||
|
||||
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));
|
||||
if (needsRelaunch) {
|
||||
if (switchingFromCdpToBrowser || needsCdpReconnect) {
|
||||
await this.close();
|
||||
} else {
|
||||
return;
|
||||
@@ -634,58 +622,35 @@ export class BrowserManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select browser type
|
||||
const browserType = options.browser ?? 'chromium';
|
||||
if (hasExtensions && browserType !== 'chromium') {
|
||||
throw new Error('Extensions are only supported in Chromium');
|
||||
}
|
||||
|
||||
const launcher =
|
||||
browserType === 'firefox' ? firefox : browserType === 'webkit' ? webkit : chromium;
|
||||
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
||||
|
||||
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(
|
||||
path.join(os.tmpdir(), `agent-browser-ext-${session}`),
|
||||
{
|
||||
headless: false,
|
||||
executablePath: options.executablePath,
|
||||
args: [`--disable-extensions-except=${extPaths}`, `--load-extension=${extPaths}`],
|
||||
viewport,
|
||||
extraHTTPHeaders: options.headers,
|
||||
}
|
||||
);
|
||||
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 {
|
||||
// Regular ephemeral browser
|
||||
this.browser = await launcher.launch({
|
||||
headless: options.headless ?? true,
|
||||
executablePath: options.executablePath,
|
||||
});
|
||||
this.cdpPort = null;
|
||||
context = await this.browser.newContext({ viewport, extraHTTPHeaders: options.headers });
|
||||
}
|
||||
// Launch browser
|
||||
this.browser = await launcher.launch({
|
||||
headless: options.headless ?? true,
|
||||
executablePath: options.executablePath,
|
||||
});
|
||||
this.cdpPort = null;
|
||||
|
||||
// Create context with viewport and optional headers
|
||||
const context = await this.browser.newContext({
|
||||
viewport: options.viewport ?? { width: 1280, height: 720 },
|
||||
extraHTTPHeaders: options.headers,
|
||||
});
|
||||
|
||||
// Set default timeout to 10 seconds (Playwright default is 30s)
|
||||
context.setDefaultTimeout(10000);
|
||||
|
||||
this.contexts.push(context);
|
||||
|
||||
const page = context.pages()[0] ?? (await context.newPage());
|
||||
// Create initial page
|
||||
const page = await context.newPage();
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
|
||||
// Automatically start console and error tracking
|
||||
this.setupPageTracking(page);
|
||||
}
|
||||
|
||||
@@ -912,7 +877,6 @@ export class BrowserManager {
|
||||
this.pages = [];
|
||||
this.contexts = [];
|
||||
this.cdpPort = null;
|
||||
this.isPersistentContext = false;
|
||||
this.activePageIndex = 0;
|
||||
this.refMap = {};
|
||||
this.lastSnapshot = '';
|
||||
|
||||
@@ -158,17 +158,11 @@ export async function startDaemon(): Promise<void> {
|
||||
parseResult.command.action !== 'launch' &&
|
||||
parseResult.command.action !== 'close'
|
||||
) {
|
||||
const extensions = process.env.AGENT_BROWSER_EXTENSIONS
|
||||
? process.env.AGENT_BROWSER_EXTENSIONS.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
await browser.launch({
|
||||
id: 'auto',
|
||||
action: 'launch',
|
||||
headless: true,
|
||||
executablePath: process.env.AGENT_BROWSER_EXECUTABLE_PATH,
|
||||
extensions: extensions,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ export interface LaunchCommand extends BaseCommand {
|
||||
headers?: Record<string, string>;
|
||||
executablePath?: string;
|
||||
cdpPort?: number;
|
||||
extensions?: string[];
|
||||
profile?: string; // Path to persistent browser profile directory
|
||||
}
|
||||
|
||||
export interface NavigateCommand extends BaseCommand {
|
||||
|
||||
Reference in New Issue
Block a user