diff --git a/README.md b/README.md index 7b0dfba9..7fea985b 100644 --- a/README.md +++ b/README.md @@ -1068,6 +1068,37 @@ When enabled, agent-browser connects to a Browser Use cloud session instead of l 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. +### Hyperbrowser + +[Hyperbrowser](https://hyperbrowser.ai) is a browser-as-a-service platform for AI agents and apps. Use it to run agent-browser in the cloud without managing local browser infrastructure. + +To enable Hyperbrowser, use the `-p` flag: + +```bash +export HYPERBROWSER_API_KEY="your-api-key" +agent-browser -p hyperbrowser open https://example.com +``` + +Or use environment variables for CI/scripts: + +```bash +export AGENT_BROWSER_PROVIDER=hyperbrowser +export HYPERBROWSER_API_KEY="your-api-key" +agent-browser open https://example.com +``` + +When enabled, agent-browser connects to a Hyperbrowser cloud session instead of launching a local browser. All commands work identically. + +Optional configuration via environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `HYPERBROWSER_PROFILE_NAME` | Browser profile name for persistent cookies/logins (created if it doesn't exist) | (none) | + +**Profile Persistence:** When `HYPERBROWSER_PROFILE_NAME` is set, the profile will be created if it doesn't already exist. Cookies, logins, and session data are automatically saved back to the profile when the browser session ends, making them available for future sessions. + +Get your API key from the [Hyperbrowser Dashboard](https://app.hyperbrowser.ai). + ### Kernel [Kernel](https://www.kernel.sh) provides cloud browser infrastructure for AI agents with features like stealth mode and persistent profiles. diff --git a/cli/src/output.rs b/cli/src/output.rs index 82fced5e..be604527 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -2054,7 +2054,7 @@ Options: e.g., --proxy-bypass "localhost,*.internal.com" --ignore-https-errors Ignore HTTPS certificate errors --allow-file-access Allow file:// URLs to access local files (Chromium only) - -p, --provider Browser provider: ios, browserbase, kernel, browseruse + -p, --provider Browser provider: ios, browserbase, hyperbrowser, kernel, browseruse --device iOS device name (e.g., "iPhone 15 Pro") --json JSON output --full, -f Full page screenshot @@ -2100,7 +2100,7 @@ Environment: AGENT_BROWSER_ANNOTATE Annotated screenshot with numbered labels and legend AGENT_BROWSER_DEBUG Debug output AGENT_BROWSER_IGNORE_HTTPS_ERRORS Ignore HTTPS certificate errors - AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, kernel, browseruse) + AGENT_BROWSER_PROVIDER Browser provider (ios, browserbase, hyperbrowser, kernel, browseruse) AGENT_BROWSER_AUTO_CONNECT Auto-discover and connect to running Chrome AGENT_BROWSER_ALLOW_FILE_ACCESS Allow file:// URLs to access local files AGENT_BROWSER_STREAM_PORT Enable WebSocket streaming on port (e.g., 9223) diff --git a/docs/src/app/cdp-mode/page.mdx b/docs/src/app/cdp-mode/page.mdx index 7edec00d..93269e86 100644 --- a/docs/src/app/cdp-mode/page.mdx +++ b/docs/src/app/cdp-mode/page.mdx @@ -79,7 +79,7 @@ This enables control of: --session <name>Use isolated session --profile <path>Persistent browser profile directory - -p <provider>Cloud browser provider (browserbase, browseruse, kernel) + -p <provider>Cloud browser provider (browserbase, browseruse, hyperbrowser, kernel) --headers <json>HTTP headers scoped to origin --executable-pathCustom browser executable --args <args>Browser launch args (comma-separated) @@ -111,6 +111,10 @@ agent-browser -p browserbase open https://example.com export BROWSER_USE_API_KEY="your-api-key" agent-browser -p browseruse open https://example.com +# Hyperbrowser +export HYPERBROWSER_API_KEY="your-api-key" +agent-browser -p hyperbrowser open https://example.com + # Kernel export KERNEL_API_KEY="your-api-key" agent-browser -p kernel open https://example.com diff --git a/src/browser.ts b/src/browser.ts index f78e83d9..8cc80999 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -82,6 +82,8 @@ export class BrowserManager { private browserbaseApiKey: string | null = null; private browserUseSessionId: string | null = null; private browserUseApiKey: string | null = null; + private hyperbrowserApiKey: string | null = null; + private hyperbrowserSessionId: string | null = null; private kernelSessionId: string | null = null; private kernelApiKey: string | null = null; private contexts: BrowserContext[] = []; @@ -802,6 +804,22 @@ export class BrowserManager { } } + /** + * Close a Hyperbrowser session via API + */ + private async closeHyperbrowserSession(sessionId: string, apiKey: string): Promise { + const response = await fetch(`https://api.hyperbrowser.ai/api/session/${sessionId}/stop`, { + method: 'PUT', + headers: { + 'X-API-Key': apiKey, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to close Hyperbrowser session: ${response.statusText}`); + } + } + /** * Close a Kernel session via API */ @@ -880,6 +898,177 @@ export class BrowserManager { } } + /** + * Find or create a Hyperbrowser profile by name. + * Returns profile id + name for session attachment. + */ + private async findOrCreateHyperbrowserProfile( + profileName: string, + apiKey: string + ): Promise<{ id: string; name: string }> { + type HyperbrowserProfile = { id?: string; name?: string }; + + const query = new URLSearchParams({ + page: '1', + name: profileName, + }); + const listResponse = await fetch( + `https://api.hyperbrowser.ai/api/profiles?${query.toString()}`, + { + method: 'GET', + headers: { + 'X-API-Key': apiKey, + }, + } + ); + + if (!listResponse.ok) { + throw new Error(`Failed to list Hyperbrowser profiles: ${listResponse.statusText}`); + } + + let listResult: { profiles?: HyperbrowserProfile[] }; + try { + listResult = (await listResponse.json()) as { profiles?: HyperbrowserProfile[] }; + } catch (error) { + throw new Error( + `Failed to parse Hyperbrowser profile list response: ${error instanceof Error ? error.message : String(error)}` + ); + } + + const existingProfile = listResult.profiles?.find( + (profile) => profile.name === profileName && !!profile.id + ); + if (existingProfile?.id) { + return { id: existingProfile.id, name: existingProfile.name ?? profileName }; + } + + const createResponse = await fetch('https://api.hyperbrowser.ai/api/profile', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ name: profileName }), + }); + + if (!createResponse.ok) { + throw new Error(`Failed to create Hyperbrowser profile: ${createResponse.statusText}`); + } + + let createdProfile: HyperbrowserProfile; + try { + createdProfile = (await createResponse.json()) as HyperbrowserProfile; + } catch (error) { + throw new Error( + `Failed to parse Hyperbrowser create profile response: ${error instanceof Error ? error.message : String(error)}` + ); + } + + if (!createdProfile.id) { + throw new Error('Invalid Hyperbrowser profile response: missing id'); + } + + return { + id: createdProfile.id, + name: createdProfile.name ?? profileName, + }; + } + + /** + * Connect to Hyperbrowser remote browser via API. + * Requires HYPERBROWSER_API_KEY environment variable. + * Optional: HYPERBROWSER_PROFILE_NAME for persistent profile state. + */ + private async connectToHyperbrowser(): Promise { + const hyperbrowserApiKey = process.env.HYPERBROWSER_API_KEY; + if (!hyperbrowserApiKey) { + throw new Error('HYPERBROWSER_API_KEY is required when using hyperbrowser as a provider'); + } + + // Find or create profile if HYPERBROWSER_PROFILE_NAME is set + const profileName = process.env.HYPERBROWSER_PROFILE_NAME; + let profileConfig: + | { + profile: { + id: string; + persistChanges: boolean; + }; + } + | undefined; + + if (profileName) { + const profile = await this.findOrCreateHyperbrowserProfile(profileName, hyperbrowserApiKey); + profileConfig = { + profile: { + id: profile.id, + persistChanges: true, // Save cookies/state back to the profile when session ends + }, + }; + } + + const response = await fetch('https://api.hyperbrowser.ai/api/session', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': hyperbrowserApiKey, + }, + ...(profileConfig ? { body: JSON.stringify(profileConfig) } : {}), + }); + + if (!response.ok) { + throw new Error(`Failed to create Hyperbrowser session: ${response.statusText}`); + } + + let session: { id: string; wsEndpoint: string }; + try { + session = (await response.json()) as { id: string; wsEndpoint: string }; + } catch (error) { + throw new Error( + `Failed to parse Hyperbrowser session response: ${error instanceof Error ? error.message : String(error)}` + ); + } + + if (!session.id || !session.wsEndpoint) { + throw new Error( + `Invalid Hyperbrowser session response: missing ${!session.id ? 'id' : 'wsEndpoint'}` + ); + } + + const browser = await chromium.connectOverCDP(session.wsEndpoint).catch(() => { + throw new Error('Failed to connect to Hyperbrowser 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.hyperbrowserSessionId = session.id; + this.hyperbrowserApiKey = hyperbrowserApiKey; + 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.closeHyperbrowserSession(session.id, hyperbrowserApiKey).catch((sessionError) => { + console.error('Failed to close Hyperbrowser session during cleanup:', sessionError); + }); + throw error; + } + } + /** * Find or create a Kernel profile by name. * Returns the profile object if successful. @@ -1161,6 +1350,10 @@ export class BrowserManager { await this.connectToBrowserUse(); return; } + if (provider === 'hyperbrowser') { + await this.connectToHyperbrowser(); + return; + } // Kernel: requires explicit opt-in via -p kernel flag or AGENT_BROWSER_PROVIDER=kernel if (provider === 'kernel') { @@ -2345,6 +2538,14 @@ export class BrowserManager { } ); this.browser = null; + } else if (this.hyperbrowserSessionId && this.hyperbrowserApiKey) { + await this.closeHyperbrowserSession( + this.hyperbrowserSessionId, + this.hyperbrowserApiKey + ).catch((error) => { + console.error('Failed to close Hyperbrowser session:', error); + }); + this.browser = null; } else if (this.kernelSessionId && this.kernelApiKey) { await this.closeKernelSession(this.kernelSessionId, this.kernelApiKey).catch((error) => { console.error('Failed to close Kernel session:', error); @@ -2377,6 +2578,8 @@ export class BrowserManager { this.browserbaseApiKey = null; this.browserUseSessionId = null; this.browserUseApiKey = null; + this.hyperbrowserSessionId = null; + this.hyperbrowserApiKey = null; this.kernelSessionId = null; this.kernelApiKey = null; this.isPersistentContext = false;