feat: add Browser Use cloud browser as available provider (#138)
* feat: add Browser Use cloud browser integration * feat: enhance Browser Use integration with provider flag support - Updated README to reflect new usage instructions for enabling Browser Use with the `-p` flag. - Modified CLI to parse and handle the `-p` flag for specifying the provider. - Implemented logic in the main application to launch with the specified cloud provider. - Adjusted BrowserManager to connect to Browser Use based on the provider flag or environment variable. - Updated types and protocol schemas to include provider information. * feat: add validation for mutually exclusive CLI options - Implemented checks to prevent the use of both --cdp and --provider flags simultaneously. - Added validation to ensure --extension cannot be used with the --provider flag. - Enhanced error handling to provide clear feedback in both JSON and console output formats.
This commit is contained in:
@@ -674,6 +674,29 @@ When both variables are set, agent-browser automatically connects to a Browserba
|
||||
|
||||
Get your API key and project ID from the [Browserbase Dashboard](https://browserbase.com/overview).
|
||||
|
||||
### Browser Use
|
||||
|
||||
[Browser Use](https://browser-use.com) provides cloud browser infrastructure for AI agents. Use it when running agent-browser in environments where a local browser isn't available (serverless, CI/CD, etc.).
|
||||
|
||||
To enable Browser Use, use the `-p` flag:
|
||||
|
||||
```bash
|
||||
export BROWSER_USE_API_KEY="your-api-key"
|
||||
agent-browser -p browseruse open https://example.com
|
||||
```
|
||||
|
||||
Or use environment variables for CI/scripts:
|
||||
|
||||
```bash
|
||||
export AGENT_BROWSER_PROVIDER=browseruse
|
||||
export BROWSER_USE_API_KEY="your-api-key"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
When enabled, agent-browser connects to a Browser Use cloud session instead of launching a local browser. All commands work identically.
|
||||
|
||||
Get your API key from the [Browser Use Cloud Dashboard](https://cloud.browser-use.com/settings?tab=api-keys). Free credits are available to get started, with pay-as-you-go pricing after.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
|
||||
+9
-1
@@ -11,6 +11,7 @@ pub struct Flags {
|
||||
pub cdp: Option<String>,
|
||||
pub extensions: Vec<String>,
|
||||
pub proxy: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_flags(args: &[String]) -> Flags {
|
||||
@@ -30,6 +31,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
cdp: None,
|
||||
extensions: extensions_env,
|
||||
proxy: None,
|
||||
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
|
||||
};
|
||||
|
||||
let mut i = 0;
|
||||
@@ -75,6 +77,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"-p" | "--provider" => {
|
||||
if let Some(p) = args.get(i + 1) {
|
||||
flags.provider = Some(p.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
@@ -89,7 +97,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", "--proxy"];
|
||||
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp", "--extension", "--proxy", "-p", "--provider"];
|
||||
|
||||
for arg in args.iter() {
|
||||
if skip_next {
|
||||
|
||||
+47
-2
@@ -216,6 +216,27 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate mutually exclusive options
|
||||
if flags.cdp.is_some() && flags.provider.is_some() {
|
||||
let msg = "Cannot use --cdp and -p/--provider together";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if flags.provider.is_some() && !flags.extensions.is_empty() {
|
||||
let msg = "Cannot use --extension with -p/--provider (extensions require local browser)";
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Connect via CDP if --cdp flag is set
|
||||
if let Some(ref port) = flags.cdp {
|
||||
let cdp_port: u16 = match port.parse::<u32>() {
|
||||
@@ -271,8 +292,32 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Launch headed browser or proxy if flags are set (without CDP)
|
||||
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() {
|
||||
// Launch with cloud provider if -p flag is set
|
||||
if let Some(ref provider) = flags.provider {
|
||||
let launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
"provider": provider
|
||||
});
|
||||
|
||||
let err = match send_command(launch_cmd, &flags.session) {
|
||||
Ok(resp) if resp.success => None,
|
||||
Ok(resp) => Some(resp.error.unwrap_or_else(|| "Provider connection failed".to_string())),
|
||||
Err(e) => Some(e.to_string()),
|
||||
};
|
||||
|
||||
if let Some(msg) = err {
|
||||
if flags.json {
|
||||
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||
} else {
|
||||
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Launch headed browser or proxy if flags are set (without CDP or provider)
|
||||
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() && flags.provider.is_none() {
|
||||
let mut launch_cmd = json!({
|
||||
"id": gen_id(),
|
||||
"action": "launch",
|
||||
|
||||
+113
-4
@@ -72,6 +72,8 @@ export class BrowserManager {
|
||||
private isPersistentContext: boolean = false;
|
||||
private browserbaseSessionId: string | null = null;
|
||||
private browserbaseApiKey: string | null = null;
|
||||
private browserUseSessionId: string | null = null;
|
||||
private browserUseApiKey: string | null = null;
|
||||
private contexts: BrowserContext[] = [];
|
||||
private pages: Page[] = [];
|
||||
private activePageIndex: number = 0;
|
||||
@@ -656,6 +658,24 @@ export class BrowserManager {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a Browser Use session via API
|
||||
*/
|
||||
private async closeBrowserUseSession(sessionId: string, apiKey: string): Promise<void> {
|
||||
const response = await fetch(`https://api.browser-use.com/api/v2/browsers/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': apiKey,
|
||||
},
|
||||
body: JSON.stringify({ action: 'stop' }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to close Browser Use session: ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Browserbase remote browser via CDP.
|
||||
* Returns true if connected, false if credentials not available.
|
||||
@@ -683,8 +703,8 @@ export class BrowserManager {
|
||||
throw new Error(`Failed to create Browserbase session: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const session = await response.json() as { id: string; connectUrl: string };
|
||||
|
||||
const session = (await response.json()) as { id: string; connectUrl: string };
|
||||
|
||||
const browser = await chromium.connectOverCDP(session.connectUrl).catch(() => {
|
||||
throw new Error('Failed to connect to Browserbase session via CDP');
|
||||
});
|
||||
@@ -717,6 +737,79 @@ export class BrowserManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Browser Use remote browser via CDP.
|
||||
* Requires BROWSER_USE_API_KEY environment variable.
|
||||
*/
|
||||
private async connectToBrowserUse(): Promise<void> {
|
||||
const browserUseApiKey = process.env.BROWSER_USE_API_KEY;
|
||||
if (!browserUseApiKey) {
|
||||
throw new Error('BROWSER_USE_API_KEY is required when using browseruse as a provider');
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.browser-use.com/api/v2/browsers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Browser-Use-API-Key': browserUseApiKey,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create Browser Use session: ${response.statusText}`);
|
||||
}
|
||||
|
||||
let session: { id: string; cdpUrl: string };
|
||||
try {
|
||||
session = (await response.json()) as { id: string; cdpUrl: string };
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse Browser Use session response: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!session.id || !session.cdpUrl) {
|
||||
throw new Error(
|
||||
`Invalid Browser Use session response: missing ${!session.id ? 'id' : 'cdpUrl'}`
|
||||
);
|
||||
}
|
||||
|
||||
const browser = await chromium.connectOverCDP(session.cdpUrl).catch(() => {
|
||||
throw new Error('Failed to connect to Browser Use session via CDP');
|
||||
});
|
||||
|
||||
try {
|
||||
const contexts = browser.contexts();
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
if (contexts.length === 0) {
|
||||
context = await browser.newContext();
|
||||
page = await context.newPage();
|
||||
} else {
|
||||
context = contexts[0];
|
||||
const pages = context.pages();
|
||||
page = pages[0] ?? (await context.newPage());
|
||||
}
|
||||
|
||||
this.browserUseSessionId = session.id;
|
||||
this.browserUseApiKey = browserUseApiKey;
|
||||
this.browser = browser;
|
||||
context.setDefaultTimeout(60000);
|
||||
this.contexts.push(context);
|
||||
this.pages.push(page);
|
||||
this.activePageIndex = 0;
|
||||
this.setupPageTracking(page);
|
||||
this.setupContextTracking(context);
|
||||
} catch (error) {
|
||||
await this.closeBrowserUseSession(session.id, browserUseApiKey).catch((sessionError) => {
|
||||
console.error('Failed to close Browser Use session during cleanup:', sessionError);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the browser with the specified options
|
||||
* If already launched, this is a no-op (browser stays open)
|
||||
@@ -744,12 +837,19 @@ export class BrowserManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try connecting to Browserbase if credentials are available
|
||||
// Try connecting to cloud browser providers if configured
|
||||
// Browserbase: auto-connects when BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID are set
|
||||
if (await this.connectToBrowserbase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select browser type
|
||||
// Browser Use: requires explicit opt-in via -p browseruse flag or AGENT_BROWSER_PROVIDER=browseruse
|
||||
const provider = options.provider ?? process.env.AGENT_BROWSER_PROVIDER;
|
||||
if (provider === 'browseruse') {
|
||||
await this.connectToBrowserUse();
|
||||
return;
|
||||
}
|
||||
|
||||
const browserType = options.browser ?? 'chromium';
|
||||
if (hasExtensions && browserType !== 'chromium') {
|
||||
throw new Error('Extensions are only supported in Chromium');
|
||||
@@ -1447,6 +1547,13 @@ export class BrowserManager {
|
||||
}
|
||||
);
|
||||
this.browser = null;
|
||||
} else if (this.browserUseSessionId && this.browserUseApiKey) {
|
||||
await this.closeBrowserUseSession(this.browserUseSessionId, this.browserUseApiKey).catch(
|
||||
(error) => {
|
||||
console.error('Failed to close Browser Use session:', error);
|
||||
}
|
||||
);
|
||||
this.browser = null;
|
||||
} else if (this.cdpPort !== null) {
|
||||
// CDP: only disconnect, don't close external app's pages
|
||||
if (this.browser) {
|
||||
@@ -1472,6 +1579,8 @@ export class BrowserManager {
|
||||
this.cdpPort = null;
|
||||
this.browserbaseSessionId = null;
|
||||
this.browserbaseApiKey = null;
|
||||
this.browserUseSessionId = null;
|
||||
this.browserUseApiKey = null;
|
||||
this.isPersistentContext = false;
|
||||
this.activePageIndex = 0;
|
||||
this.refMap = {};
|
||||
|
||||
@@ -30,6 +30,7 @@ const launchSchema = baseCommandSchema.extend({
|
||||
password: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
provider: z.string().optional(),
|
||||
});
|
||||
|
||||
const navigateSchema = baseCommandSchema.extend({
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface LaunchCommand extends BaseCommand {
|
||||
username?: string;
|
||||
password?: string;
|
||||
};
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export interface NavigateCommand extends BaseCommand {
|
||||
|
||||
Reference in New Issue
Block a user