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).
|
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
|
## License
|
||||||
|
|
||||||
Apache-2.0
|
Apache-2.0
|
||||||
|
|||||||
+9
-1
@@ -11,6 +11,7 @@ pub struct Flags {
|
|||||||
pub cdp: Option<String>,
|
pub cdp: Option<String>,
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
pub proxy: Option<String>,
|
pub proxy: Option<String>,
|
||||||
|
pub provider: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_flags(args: &[String]) -> Flags {
|
pub fn parse_flags(args: &[String]) -> Flags {
|
||||||
@@ -30,6 +31,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
cdp: None,
|
cdp: None,
|
||||||
extensions: extensions_env,
|
extensions: extensions_env,
|
||||||
proxy: None,
|
proxy: None,
|
||||||
|
provider: env::var("AGENT_BROWSER_PROVIDER").ok(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -75,6 +77,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"-p" | "--provider" => {
|
||||||
|
if let Some(p) = args.get(i + 1) {
|
||||||
|
flags.provider = Some(p.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
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
|
// Global flags that should be stripped from command args
|
||||||
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
const GLOBAL_FLAGS: &[&str] = &["--json", "--full", "--headed", "--debug"];
|
||||||
// Global flags that take a value (need to skip the next arg too)
|
// 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() {
|
for arg in args.iter() {
|
||||||
if skip_next {
|
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
|
// Connect via CDP if --cdp flag is set
|
||||||
if let Some(ref port) = flags.cdp {
|
if let Some(ref port) = flags.cdp {
|
||||||
let cdp_port: u16 = match port.parse::<u32>() {
|
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)
|
// Launch with cloud provider if -p flag is set
|
||||||
if (flags.headed || flags.proxy.is_some()) && flags.cdp.is_none() {
|
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!({
|
let mut launch_cmd = json!({
|
||||||
"id": gen_id(),
|
"id": gen_id(),
|
||||||
"action": "launch",
|
"action": "launch",
|
||||||
|
|||||||
+112
-3
@@ -72,6 +72,8 @@ export class BrowserManager {
|
|||||||
private isPersistentContext: boolean = false;
|
private isPersistentContext: boolean = false;
|
||||||
private browserbaseSessionId: string | null = null;
|
private browserbaseSessionId: string | null = null;
|
||||||
private browserbaseApiKey: string | null = null;
|
private browserbaseApiKey: string | null = null;
|
||||||
|
private browserUseSessionId: string | null = null;
|
||||||
|
private browserUseApiKey: string | null = null;
|
||||||
private contexts: BrowserContext[] = [];
|
private contexts: BrowserContext[] = [];
|
||||||
private pages: Page[] = [];
|
private pages: Page[] = [];
|
||||||
private activePageIndex: number = 0;
|
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.
|
* Connect to Browserbase remote browser via CDP.
|
||||||
* Returns true if connected, false if credentials not available.
|
* Returns true if connected, false if credentials not available.
|
||||||
@@ -683,7 +703,7 @@ export class BrowserManager {
|
|||||||
throw new Error(`Failed to create Browserbase session: ${response.statusText}`);
|
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(() => {
|
const browser = await chromium.connectOverCDP(session.connectUrl).catch(() => {
|
||||||
throw new Error('Failed to connect to Browserbase session via CDP');
|
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
|
* Launch the browser with the specified options
|
||||||
* If already launched, this is a no-op (browser stays open)
|
* If already launched, this is a no-op (browser stays open)
|
||||||
@@ -744,12 +837,19 @@ export class BrowserManager {
|
|||||||
return;
|
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()) {
|
if (await this.connectToBrowserbase()) {
|
||||||
return;
|
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';
|
const browserType = options.browser ?? 'chromium';
|
||||||
if (hasExtensions && browserType !== 'chromium') {
|
if (hasExtensions && browserType !== 'chromium') {
|
||||||
throw new Error('Extensions are only supported in Chromium');
|
throw new Error('Extensions are only supported in Chromium');
|
||||||
@@ -1447,6 +1547,13 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
this.browser = null;
|
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) {
|
} else if (this.cdpPort !== null) {
|
||||||
// CDP: only disconnect, don't close external app's pages
|
// CDP: only disconnect, don't close external app's pages
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
@@ -1472,6 +1579,8 @@ export class BrowserManager {
|
|||||||
this.cdpPort = null;
|
this.cdpPort = null;
|
||||||
this.browserbaseSessionId = null;
|
this.browserbaseSessionId = null;
|
||||||
this.browserbaseApiKey = null;
|
this.browserbaseApiKey = null;
|
||||||
|
this.browserUseSessionId = null;
|
||||||
|
this.browserUseApiKey = null;
|
||||||
this.isPersistentContext = false;
|
this.isPersistentContext = false;
|
||||||
this.activePageIndex = 0;
|
this.activePageIndex = 0;
|
||||||
this.refMap = {};
|
this.refMap = {};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const launchSchema = baseCommandSchema.extend({
|
|||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
provider: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigateSchema = baseCommandSchema.extend({
|
const navigateSchema = baseCommandSchema.extend({
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
};
|
};
|
||||||
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateCommand extends BaseCommand {
|
export interface NavigateCommand extends BaseCommand {
|
||||||
|
|||||||
Reference in New Issue
Block a user