Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> Browser provider: ios, browserbase, kernel, browseruse
-p, --provider <name> Browser provider: ios, browserbase, hyperbrowser, kernel, browseruse
--device <name> iOS device name (e.g., "iPhone 15 Pro")
--json JSON output
--full, -f Full page screenshot
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion docs/src/app/cdp-mode/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ This enables control of:
<tbody>
<tr><td><code>--session &lt;name&gt;</code></td><td>Use isolated session</td></tr>
<tr><td><code>--profile &lt;path&gt;</code></td><td>Persistent browser profile directory</td></tr>
<tr><td><code>-p &lt;provider&gt;</code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>kernel</code>)</td></tr>
<tr><td><code>-p &lt;provider&gt;</code></td><td>Cloud browser provider (<code>browserbase</code>, <code>browseruse</code>, <code>hyperbrowser</code>, <code>kernel</code>)</td></tr>
<tr><td><code>--headers &lt;json&gt;</code></td><td>HTTP headers scoped to origin</td></tr>
<tr><td><code>--executable-path</code></td><td>Custom browser executable</td></tr>
<tr><td><code>--args &lt;args&gt;</code></td><td>Browser launch args (comma-separated)</td></tr>
Expand Down Expand Up @@ -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
Expand Down
203 changes: 203 additions & 0 deletions src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -802,6 +804,22 @@ export class BrowserManager {
}
}

/**
* Close a Hyperbrowser session via API
*/
private async closeHyperbrowserSession(sessionId: string, apiKey: string): Promise<void> {
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
*/
Expand Down Expand Up @@ -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<void> {
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.
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down