Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a0e51f4a6 | ||
|
|
6eafe50952 | ||
|
|
95675e9d55 | ||
|
|
97b17c98fb | ||
|
|
57a04385c1 | ||
|
|
1a88d7f585 |
@@ -18,6 +18,8 @@ git clone https://github.com/vercel-labs/agent-browser
|
|||||||
cd agent-browser
|
cd agent-browser
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm build
|
pnpm build
|
||||||
|
pnpm build:native # Requires Rust (https://rustup.rs)
|
||||||
|
pnpm link --global # Makes agent-browser available globally
|
||||||
agent-browser install
|
agent-browser install
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -300,6 +302,7 @@ agent-browser snapshot -i -c -d 5 # Combine options
|
|||||||
| `--name, -n` | Locator name filter |
|
| `--name, -n` | Locator name filter |
|
||||||
| `--exact` | Exact text match |
|
| `--exact` | Exact text match |
|
||||||
| `--headed` | Show browser window (not headless) |
|
| `--headed` | Show browser window (not headless) |
|
||||||
|
| `--cdp <port>` | Connect via Chrome DevTools Protocol |
|
||||||
| `--debug` | Debug output |
|
| `--debug` | Debug output |
|
||||||
|
|
||||||
## Selectors
|
## Selectors
|
||||||
@@ -457,6 +460,25 @@ export async function handler() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## CDP Mode
|
||||||
|
|
||||||
|
Connect to an existing browser via Chrome DevTools Protocol:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Connect to Electron app
|
||||||
|
agent-browser --cdp 9222 snapshot
|
||||||
|
|
||||||
|
# Connect to Chrome with remote debugging
|
||||||
|
# (Start Chrome with: google-chrome --remote-debugging-port=9222)
|
||||||
|
agent-browser --cdp 9222 open about:blank
|
||||||
|
```
|
||||||
|
|
||||||
|
This enables control of:
|
||||||
|
- Electron apps
|
||||||
|
- Chrome/Chromium instances with remote debugging
|
||||||
|
- WebView2 applications
|
||||||
|
- Any browser exposing a CDP endpoint
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
agent-browser uses a client-daemon architecture:
|
agent-browser uses a client-daemon architecture:
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-browser"
|
name = "agent-browser"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-browser"
|
name = "agent-browser"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Fast browser automation CLI for AI agents"
|
description = "Fast browser automation CLI for AI agents"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|||||||
@@ -901,6 +901,7 @@ mod tests {
|
|||||||
debug: false,
|
debug: false,
|
||||||
headers: None,
|
headers: None,
|
||||||
executable_path: None,
|
executable_path: None,
|
||||||
|
cdp: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -8,6 +8,7 @@ pub struct Flags {
|
|||||||
pub session: String,
|
pub session: String,
|
||||||
pub headers: Option<String>,
|
pub headers: Option<String>,
|
||||||
pub executable_path: Option<String>,
|
pub executable_path: Option<String>,
|
||||||
|
pub cdp: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_flags(args: &[String]) -> Flags {
|
pub fn parse_flags(args: &[String]) -> Flags {
|
||||||
@@ -19,6 +20,7 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
|
session: env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string()),
|
||||||
headers: None,
|
headers: None,
|
||||||
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
executable_path: env::var("AGENT_BROWSER_EXECUTABLE_PATH").ok(),
|
||||||
|
cdp: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -46,6 +48,12 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"--cdp" => {
|
||||||
|
if let Some(s) = args.get(i + 1) {
|
||||||
|
flags.cdp = Some(s.clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
@@ -60,7 +68,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"];
|
const GLOBAL_FLAGS_WITH_VALUE: &[&str] = &["--session", "--headers", "--executable-path", "--cdp"];
|
||||||
|
|
||||||
for arg in args.iter() {
|
for arg in args.iter() {
|
||||||
if skip_next {
|
if skip_next {
|
||||||
|
|||||||
+64
-4
@@ -168,12 +168,72 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If --headed flag is set, send launch command first to switch to headed mode
|
// Connect via CDP if --cdp flag is set
|
||||||
if flags.headed {
|
if let Some(ref port) = flags.cdp {
|
||||||
let launch_cmd = json!({ "id": gen_id(), "action": "launch", "headless": false });
|
let cdp_port: u16 = match port.parse::<u32>() {
|
||||||
|
Ok(p) if p == 0 => {
|
||||||
|
let msg = "Invalid CDP port: port must be greater than 0".to_string();
|
||||||
|
if flags.json {
|
||||||
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||||
|
} else {
|
||||||
|
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
Ok(p) if p > 65535 => {
|
||||||
|
let msg = format!("Invalid CDP port: {} is out of range (valid range: 1-65535)", p);
|
||||||
|
if flags.json {
|
||||||
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||||
|
} else {
|
||||||
|
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
Ok(p) => p as u16,
|
||||||
|
Err(_) => {
|
||||||
|
let msg = format!("Invalid CDP port: '{}' is not a valid number. Port must be a number between 1 and 65535", port);
|
||||||
|
if flags.json {
|
||||||
|
println!(r#"{{"success":false,"error":"{}"}}"#, msg);
|
||||||
|
} else {
|
||||||
|
eprintln!("\x1b[31m✗\x1b[0m {}", msg);
|
||||||
|
}
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let launch_cmd = json!({
|
||||||
|
"id": gen_id(),
|
||||||
|
"action": "launch",
|
||||||
|
"cdpPort": cdp_port
|
||||||
|
});
|
||||||
|
|
||||||
|
let err = match send_command(launch_cmd, &flags.session) {
|
||||||
|
Ok(resp) if resp.success => None,
|
||||||
|
Ok(resp) => Some(resp.error.unwrap_or_else(|| "CDP 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 if --headed flag is set (without CDP)
|
||||||
|
if flags.headed && flags.cdp.is_none() {
|
||||||
|
let launch_cmd = json!({
|
||||||
|
"id": gen_id(),
|
||||||
|
"action": "launch",
|
||||||
|
"headless": false
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
if let Err(e) = send_command(launch_cmd, &flags.session) {
|
||||||
if !flags.json {
|
if !flags.json {
|
||||||
eprintln!("\x1b[33m⚠\x1b[0m Could not switch to headed mode: {}", e);
|
eprintln!("\x1b[33m⚠\x1b[0m Could not launch headed browser: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1194,6 +1194,7 @@ Options:
|
|||||||
--json JSON output
|
--json JSON output
|
||||||
--full, -f Full page screenshot
|
--full, -f Full page screenshot
|
||||||
--headed Show browser window (not headless)
|
--headed Show browser window (not headless)
|
||||||
|
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||||
--debug Debug output
|
--debug Debug output
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -1204,6 +1205,7 @@ Examples:
|
|||||||
agent-browser find role button click --name Submit
|
agent-browser find role button click --name Submit
|
||||||
agent-browser get text @e1
|
agent-browser get text @e1
|
||||||
agent-browser screenshot --full
|
agent-browser screenshot --full
|
||||||
|
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-browser",
|
"name": "agent-browser",
|
||||||
"version": "0.4.3",
|
"version": "0.4.4",
|
||||||
"description": "Headless browser automation CLI for AI agents",
|
"description": "Headless browser automation CLI for AI agents",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/daemon.js",
|
"main": "dist/daemon.js",
|
||||||
|
|||||||
@@ -32,6 +32,25 @@ describe('BrowserManager', () => {
|
|||||||
})
|
})
|
||||||
).rejects.toThrow();
|
).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should be no-op when relaunching with same options', async () => {
|
||||||
|
const browserInstance = browser.getBrowser();
|
||||||
|
await browser.launch({ id: 'test', action: 'launch', headless: true });
|
||||||
|
expect(browser.getBrowser()).toBe(browserInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reconnect when CDP port changes', async () => {
|
||||||
|
const newBrowser = new BrowserManager();
|
||||||
|
await newBrowser.launch({ id: 'test', action: 'launch', headless: true });
|
||||||
|
expect(newBrowser.getBrowser()).not.toBeNull();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
newBrowser.launch({ id: 'test', action: 'launch', cdpPort: 59999 })
|
||||||
|
).rejects.toThrow();
|
||||||
|
|
||||||
|
expect(newBrowser.getBrowser()).toBeNull();
|
||||||
|
await newBrowser.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('navigation', () => {
|
describe('navigation', () => {
|
||||||
|
|||||||
+131
-14
@@ -39,6 +39,7 @@ interface PageError {
|
|||||||
*/
|
*/
|
||||||
export class BrowserManager {
|
export class BrowserManager {
|
||||||
private browser: Browser | null = null;
|
private browser: Browser | null = null;
|
||||||
|
private cdpPort: number | null = null;
|
||||||
private contexts: BrowserContext[] = [];
|
private contexts: BrowserContext[] = [];
|
||||||
private pages: Page[] = [];
|
private pages: Page[] = [];
|
||||||
private activePageIndex: number = 0;
|
private activePageIndex: number = 0;
|
||||||
@@ -573,13 +574,51 @@ export class BrowserManager {
|
|||||||
return this.browser;
|
return this.browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an existing CDP connection is still alive
|
||||||
|
* by verifying we can access browser contexts and that at least one has pages
|
||||||
|
*/
|
||||||
|
private isCdpConnectionAlive(): boolean {
|
||||||
|
if (!this.browser) return false;
|
||||||
|
try {
|
||||||
|
const contexts = this.browser.contexts();
|
||||||
|
if (contexts.length === 0) return false;
|
||||||
|
return contexts.some((context) => context.pages().length > 0);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if CDP connection needs to be re-established
|
||||||
|
*/
|
||||||
|
private needsCdpReconnect(cdpPort: number): boolean {
|
||||||
|
if (!this.browser?.isConnected()) return true;
|
||||||
|
if (this.cdpPort !== cdpPort) return true;
|
||||||
|
if (!this.isCdpConnectionAlive()) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
*/
|
*/
|
||||||
async launch(options: LaunchCommand): Promise<void> {
|
async launch(options: LaunchCommand): Promise<void> {
|
||||||
// If already launched, don't relaunch
|
const cdpPort = options.cdpPort;
|
||||||
|
|
||||||
if (this.browser) {
|
if (this.browser) {
|
||||||
|
const switchingFromCdpToBrowser = !cdpPort && this.cdpPort !== null;
|
||||||
|
const needsCdpReconnect = !!cdpPort && this.needsCdpReconnect(cdpPort);
|
||||||
|
|
||||||
|
if (switchingFromCdpToBrowser || needsCdpReconnect) {
|
||||||
|
await this.close();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cdpPort) {
|
||||||
|
await this.connectViaCDP(cdpPort);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,6 +632,7 @@ export class BrowserManager {
|
|||||||
headless: options.headless ?? true,
|
headless: options.headless ?? true,
|
||||||
executablePath: options.executablePath,
|
executablePath: options.executablePath,
|
||||||
});
|
});
|
||||||
|
this.cdpPort = null;
|
||||||
|
|
||||||
// Create context with viewport and optional headers
|
// Create context with viewport and optional headers
|
||||||
const context = await this.browser.newContext({
|
const context = await this.browser.newContext({
|
||||||
@@ -615,7 +655,56 @@ export class BrowserManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up console and error tracking for a page
|
* Connect to a running browser via CDP (Chrome DevTools Protocol)
|
||||||
|
*/
|
||||||
|
private async connectViaCDP(cdpPort: number | undefined): Promise<void> {
|
||||||
|
if (!cdpPort) {
|
||||||
|
throw new Error('cdpPort is required for CDP connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.connectOverCDP(`http://localhost:${cdpPort}`).catch(() => {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to connect via CDP on port ${cdpPort}. ` +
|
||||||
|
`Make sure the app is running with --remote-debugging-port=${cdpPort}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate and set up state, cleaning up browser connection if anything fails
|
||||||
|
try {
|
||||||
|
const contexts = browser.contexts();
|
||||||
|
if (contexts.length === 0) {
|
||||||
|
throw new Error('No browser context found. Make sure the app has an open window.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPages = contexts.flatMap((context) => context.pages());
|
||||||
|
if (allPages.length === 0) {
|
||||||
|
throw new Error('No page found. Make sure the app has loaded content.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// All validation passed - commit state
|
||||||
|
this.browser = browser;
|
||||||
|
this.cdpPort = cdpPort;
|
||||||
|
|
||||||
|
for (const context of contexts) {
|
||||||
|
this.contexts.push(context);
|
||||||
|
this.setupContextTracking(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const page of allPages) {
|
||||||
|
this.pages.push(page);
|
||||||
|
this.setupPageTracking(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activePageIndex = 0;
|
||||||
|
} catch (error) {
|
||||||
|
// Clean up browser connection if validation or setup failed
|
||||||
|
await browser.close().catch(() => {});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up console, error, and close tracking for a page
|
||||||
*/
|
*/
|
||||||
private setupPageTracking(page: Page): void {
|
private setupPageTracking(page: Page): void {
|
||||||
page.on('console', (msg) => {
|
page.on('console', (msg) => {
|
||||||
@@ -632,6 +721,26 @@ export class BrowserManager {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
page.on('close', () => {
|
||||||
|
const index = this.pages.indexOf(page);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.pages.splice(index, 1);
|
||||||
|
if (this.activePageIndex >= this.pages.length) {
|
||||||
|
this.activePageIndex = Math.max(0, this.pages.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up tracking for new pages in a context (for CDP connections)
|
||||||
|
*/
|
||||||
|
private setupContextTracking(context: BrowserContext): void {
|
||||||
|
context.on('page', (page) => {
|
||||||
|
this.pages.push(page);
|
||||||
|
this.setupPageTracking(page);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -745,21 +854,29 @@ export class BrowserManager {
|
|||||||
* Close the browser and clean up
|
* Close the browser and clean up
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
for (const page of this.pages) {
|
// CDP: only disconnect, don't close external app's pages
|
||||||
await page.close().catch(() => {});
|
if (this.cdpPort !== null) {
|
||||||
|
if (this.browser) {
|
||||||
|
await this.browser.close().catch(() => {});
|
||||||
|
this.browser = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular browser: close everything
|
||||||
|
for (const page of this.pages) {
|
||||||
|
await page.close().catch(() => {});
|
||||||
|
}
|
||||||
|
for (const context of this.contexts) {
|
||||||
|
await context.close().catch(() => {});
|
||||||
|
}
|
||||||
|
if (this.browser) {
|
||||||
|
await this.browser.close().catch(() => {});
|
||||||
|
this.browser = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pages = [];
|
this.pages = [];
|
||||||
|
|
||||||
for (const context of this.contexts) {
|
|
||||||
await context.close().catch(() => {});
|
|
||||||
}
|
|
||||||
this.contexts = [];
|
this.contexts = [];
|
||||||
|
this.cdpPort = null;
|
||||||
if (this.browser) {
|
|
||||||
await this.browser.close().catch(() => {});
|
|
||||||
this.browser = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activePageIndex = 0;
|
this.activePageIndex = 0;
|
||||||
this.refMap = {};
|
this.refMap = {};
|
||||||
this.lastSnapshot = '';
|
this.lastSnapshot = '';
|
||||||
|
|||||||
@@ -461,6 +461,24 @@ describe('parseCommand', () => {
|
|||||||
expect(result.command.headless).toBe(false);
|
expect(result.command.headless).toBe(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should parse launch with cdpPort', () => {
|
||||||
|
const result = parseCommand(cmd({ id: '1', action: 'launch', cdpPort: 9222 }));
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.command.cdpPort).toBe(9222);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject launch with invalid cdpPort', () => {
|
||||||
|
const result = parseCommand(cmd({ id: '1', action: 'launch', cdpPort: -1 }));
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject launch with non-numeric cdpPort', () => {
|
||||||
|
const result = parseCommand(cmd({ id: '1', action: 'launch', cdpPort: 'invalid' }));
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mouse actions', () => {
|
describe('mouse actions', () => {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const launchSchema = baseCommandSchema.extend({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
browser: z.enum(['chromium', 'firefox', 'webkit']).optional(),
|
||||||
|
cdpPort: z.number().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const navigateSchema = baseCommandSchema.extend({
|
const navigateSchema = baseCommandSchema.extend({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface LaunchCommand extends BaseCommand {
|
|||||||
browser?: 'chromium' | 'firefox' | 'webkit';
|
browser?: 'chromium' | 'firefox' | 'webkit';
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
executablePath?: string;
|
executablePath?: string;
|
||||||
|
cdpPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigateCommand extends BaseCommand {
|
export interface NavigateCommand extends BaseCommand {
|
||||||
|
|||||||
Reference in New Issue
Block a user