Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30aa73e17 | ||
|
|
70ae6e34dc | ||
|
|
8b94bc35e0 | ||
|
|
574037080c | ||
|
|
278466764b |
@@ -83,3 +83,75 @@ jobs:
|
|||||||
|
|
||||||
- name: Build release binary
|
- name: Build release binary
|
||||||
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
run: cargo build --release --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Run Rust tests
|
||||||
|
run: cargo test --manifest-path cli/Cargo.toml --target ${{ matrix.target }}
|
||||||
|
|
||||||
|
windows-integration:
|
||||||
|
name: Windows Integration Test
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs: rust
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Setup Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Cache Cargo dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/
|
||||||
|
~/.cargo/registry/index/
|
||||||
|
~/.cargo/registry/cache/
|
||||||
|
~/.cargo/git/db/
|
||||||
|
cli/target/
|
||||||
|
key: windows-cargo-x86_64-pc-windows-msvc-${{ hashFiles('cli/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
windows-cargo-x86_64-pc-windows-msvc-
|
||||||
|
|
||||||
|
- name: Build Rust CLI
|
||||||
|
run: cargo build --release --manifest-path cli/Cargo.toml --target x86_64-pc-windows-msvc
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Build TypeScript
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Copy CLI binary to bin directory
|
||||||
|
run: |
|
||||||
|
Copy-Item cli/target/x86_64-pc-windows-msvc/release/agent-browser.exe bin/agent-browser-win32-x64.exe
|
||||||
|
|
||||||
|
- name: Test agent-browser install command
|
||||||
|
run: |
|
||||||
|
$env:PATH = "$pwd\bin;$env:PATH"
|
||||||
|
bin/agent-browser-win32-x64.exe install
|
||||||
|
shell: pwsh
|
||||||
|
|
||||||
|
- name: Verify Chromium was installed
|
||||||
|
run: |
|
||||||
|
$playwrightPath = "$env:LOCALAPPDATA\ms-playwright"
|
||||||
|
if (Test-Path $playwrightPath) {
|
||||||
|
Write-Host "Playwright browsers installed at: $playwrightPath"
|
||||||
|
Get-ChildItem $playwrightPath -Recurse -Depth 2 | Select-Object -First 20
|
||||||
|
} else {
|
||||||
|
Write-Error "Playwright browsers not found!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
shell: pwsh
|
||||||
|
|||||||
@@ -55,13 +55,13 @@ agent-browser find role button click --name "Submit"
|
|||||||
### Core Commands
|
### Core Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser open <url> # Navigate to URL
|
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||||
agent-browser click <sel> # Click element
|
agent-browser click <sel> # Click element
|
||||||
agent-browser dblclick <sel> # Double-click element
|
agent-browser dblclick <sel> # Double-click element
|
||||||
agent-browser focus <sel> # Focus element
|
agent-browser focus <sel> # Focus element
|
||||||
agent-browser type <sel> <text> # Type into element
|
agent-browser type <sel> <text> # Type into element
|
||||||
agent-browser fill <sel> <text> # Clear and fill
|
agent-browser fill <sel> <text> # Clear and fill
|
||||||
agent-browser press <key> # Press key (Enter, Tab, Control+a)
|
agent-browser press <key> # Press key (Enter, Tab, Control+a) (alias: key)
|
||||||
agent-browser keydown <key> # Hold key down
|
agent-browser keydown <key> # Hold key down
|
||||||
agent-browser keyup <key> # Release key
|
agent-browser keyup <key> # Release key
|
||||||
agent-browser hover <sel> # Hover element
|
agent-browser hover <sel> # Hover element
|
||||||
@@ -69,14 +69,14 @@ agent-browser select <sel> <val> # Select dropdown option
|
|||||||
agent-browser check <sel> # Check checkbox
|
agent-browser check <sel> # Check checkbox
|
||||||
agent-browser uncheck <sel> # Uncheck checkbox
|
agent-browser uncheck <sel> # Uncheck checkbox
|
||||||
agent-browser scroll <dir> [px] # Scroll (up/down/left/right)
|
agent-browser scroll <dir> [px] # Scroll (up/down/left/right)
|
||||||
agent-browser scrollintoview <sel> # Scroll element into view
|
agent-browser scrollintoview <sel> # Scroll element into view (alias: scrollinto)
|
||||||
agent-browser drag <src> <tgt> # Drag and drop
|
agent-browser drag <src> <tgt> # Drag and drop
|
||||||
agent-browser upload <sel> <files> # Upload files
|
agent-browser upload <sel> <files> # Upload files
|
||||||
agent-browser screenshot [path] # Take screenshot (--full for full page)
|
agent-browser screenshot [path] # Take screenshot (--full for full page)
|
||||||
agent-browser pdf <path> # Save as PDF
|
agent-browser pdf <path> # Save as PDF
|
||||||
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
||||||
agent-browser eval <js> # Run JavaScript
|
agent-browser eval <js> # Run JavaScript
|
||||||
agent-browser close # Close browser
|
agent-browser close # Close browser (aliases: quit, exit)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Get Info
|
### Get Info
|
||||||
@@ -129,9 +129,9 @@ agent-browser find nth 2 "a" text
|
|||||||
### Wait
|
### Wait
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
agent-browser wait <selector> # Wait for element
|
agent-browser wait <selector> # Wait for element to be visible
|
||||||
agent-browser wait <ms> # Wait for time
|
agent-browser wait <ms> # Wait for time (milliseconds)
|
||||||
agent-browser wait --text "Welcome" # Wait for text
|
agent-browser wait --text "Welcome" # Wait for text to appear
|
||||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||||
agent-browser wait --load networkidle # Wait for load state
|
agent-browser wait --load networkidle # Wait for load state
|
||||||
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
||||||
@@ -253,6 +253,10 @@ AGENT_BROWSER_SESSION=agent1 agent-browser click "#btn"
|
|||||||
|
|
||||||
# List active sessions
|
# List active sessions
|
||||||
agent-browser session list
|
agent-browser session list
|
||||||
|
# Output:
|
||||||
|
# Active sessions:
|
||||||
|
# -> default
|
||||||
|
# agent1
|
||||||
|
|
||||||
# Show current session
|
# Show current session
|
||||||
agent-browser session
|
agent-browser session
|
||||||
@@ -393,15 +397,17 @@ agent-browser uses a client-daemon architecture:
|
|||||||
|
|
||||||
The daemon starts automatically on first command and persists between commands for fast subsequent operations.
|
The daemon starts automatically on first command and persists between commands for fast subsequent operations.
|
||||||
|
|
||||||
|
**Browser Engine:** Uses Chromium by default. The daemon also supports Firefox and WebKit via the Playwright protocol.
|
||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
| Platform | Binary | Fallback |
|
| Platform | Binary | Fallback |
|
||||||
|----------|--------|----------|
|
|----------|--------|----------|
|
||||||
| macOS ARM64 | ✅ Native Rust | Node.js |
|
| macOS ARM64 | Native Rust | Node.js |
|
||||||
| macOS x64 | ✅ Native Rust | Node.js |
|
| macOS x64 | Native Rust | Node.js |
|
||||||
| Linux ARM64 | ✅ Native Rust | Node.js |
|
| Linux ARM64 | Native Rust | Node.js |
|
||||||
| Linux x64 | ✅ Native Rust | Node.js |
|
| Linux x64 | Native Rust | Node.js |
|
||||||
| Windows | - | Node.js |
|
| Windows x64 | Native Rust | Node.js |
|
||||||
|
|
||||||
## Usage with AI Agents
|
## Usage with AI Agents
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-browser"
|
name = "agent-browser"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "agent-browser"
|
name = "agent-browser"
|
||||||
version = "0.4.2"
|
version = "0.4.3"
|
||||||
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"
|
||||||
|
|||||||
+90
-1
@@ -212,6 +212,44 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
|
|
||||||
// === Wait ===
|
// === Wait ===
|
||||||
"wait" => {
|
"wait" => {
|
||||||
|
// Check for --url flag: wait --url "**/dashboard"
|
||||||
|
if let Some(idx) = rest.iter().position(|&s| s == "--url" || s == "-u") {
|
||||||
|
let url = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "wait --url".to_string(),
|
||||||
|
usage: "wait --url <pattern>",
|
||||||
|
})?;
|
||||||
|
return Ok(json!({ "id": id, "action": "waitforurl", "url": url }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for --load flag: wait --load networkidle
|
||||||
|
if let Some(idx) = rest.iter().position(|&s| s == "--load" || s == "-l") {
|
||||||
|
let state = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "wait --load".to_string(),
|
||||||
|
usage: "wait --load <state>",
|
||||||
|
})?;
|
||||||
|
return Ok(json!({ "id": id, "action": "waitforloadstate", "state": state }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for --fn flag: wait --fn "window.ready === true"
|
||||||
|
if let Some(idx) = rest.iter().position(|&s| s == "--fn" || s == "-f") {
|
||||||
|
let expr = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "wait --fn".to_string(),
|
||||||
|
usage: "wait --fn <expression>",
|
||||||
|
})?;
|
||||||
|
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for --text flag: wait --text "Welcome"
|
||||||
|
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||||
|
let text = rest.get(idx + 1).ok_or_else(|| ParseError::MissingArguments {
|
||||||
|
context: "wait --text".to_string(),
|
||||||
|
usage: "wait --text <text>",
|
||||||
|
})?;
|
||||||
|
// Use getByText locator to wait for text to appear
|
||||||
|
return Ok(json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: selector or timeout
|
||||||
if let Some(arg) = rest.get(0) {
|
if let Some(arg) = rest.get(0) {
|
||||||
if arg.parse::<u64>().is_ok() {
|
if arg.parse::<u64>().is_ok() {
|
||||||
Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::<u64>().unwrap() }))
|
Ok(json!({ "id": id, "action": "wait", "timeout": arg.parse::<u64>().unwrap() }))
|
||||||
@@ -221,7 +259,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
} else {
|
} else {
|
||||||
Err(ParseError::MissingArguments {
|
Err(ParseError::MissingArguments {
|
||||||
context: "wait".to_string(),
|
context: "wait".to_string(),
|
||||||
usage: "wait <selector|ms>",
|
usage: "wait <selector|ms|--url|--load|--fn|--text>",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1090,6 +1128,57 @@ mod tests {
|
|||||||
assert_eq!(cmd["maxDepth"], 3);
|
assert_eq!(cmd["maxDepth"], 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Wait ===
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_selector() {
|
||||||
|
let cmd = parse_command(&args("wait #element"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "wait");
|
||||||
|
assert_eq!(cmd["selector"], "#element");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_timeout() {
|
||||||
|
let cmd = parse_command(&args("wait 5000"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "wait");
|
||||||
|
assert_eq!(cmd["timeout"], 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_url() {
|
||||||
|
let cmd = parse_command(&args("wait --url **/dashboard"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "waitforurl");
|
||||||
|
assert_eq!(cmd["url"], "**/dashboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_load() {
|
||||||
|
let cmd = parse_command(&args("wait --load networkidle"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "waitforloadstate");
|
||||||
|
assert_eq!(cmd["state"], "networkidle");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_load_missing_state() {
|
||||||
|
let result = parse_command(&args("wait --load"), &default_flags());
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(result.unwrap_err(), ParseError::MissingArguments { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_fn() {
|
||||||
|
let cmd = parse_command(&args("wait --fn window.ready"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "waitforfunction");
|
||||||
|
assert_eq!(cmd["expression"], "window.ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wait_text() {
|
||||||
|
let cmd = parse_command(&args("wait --text Welcome"), &default_flags()).unwrap();
|
||||||
|
assert_eq!(cmd["action"], "wait");
|
||||||
|
assert_eq!(cmd["selector"], "text=Welcome");
|
||||||
|
}
|
||||||
|
|
||||||
// === Unknown command ===
|
// === Unknown command ===
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -206,8 +206,13 @@ pub fn ensure_daemon(session: &str, headed: bool) -> Result<(), String> {
|
|||||||
{
|
{
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
let mut cmd = Command::new("node");
|
// On Windows, use cmd.exe to run node to ensure proper PATH resolution.
|
||||||
cmd.arg(daemon_path)
|
// This handles cases where node.exe isn't directly in PATH but node.cmd is.
|
||||||
|
// Pass the entire command as a single string to /c to handle paths with spaces.
|
||||||
|
let cmd_string = format!("node \"{}\"", daemon_path.display());
|
||||||
|
let mut cmd = Command::new("cmd");
|
||||||
|
cmd.arg("/c")
|
||||||
|
.arg(&cmd_string)
|
||||||
.env("AGENT_BROWSER_DAEMON", "1")
|
.env("AGENT_BROWSER_DAEMON", "1")
|
||||||
.env("AGENT_BROWSER_SESSION", session);
|
.env("AGENT_BROWSER_SESSION", session);
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,16 @@ pub fn run_install(with_deps: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
println!("\x1b[36mInstalling Chromium browser...\x1b[0m");
|
println!("\x1b[36mInstalling Chromium browser...\x1b[0m");
|
||||||
|
|
||||||
|
// On Windows, we need to use cmd.exe to run npx because npx is actually npx.cmd
|
||||||
|
// and Command::new() doesn't resolve .cmd files the way the shell does.
|
||||||
|
// Pass the entire command as a single string to /c to handle paths with spaces.
|
||||||
|
#[cfg(windows)]
|
||||||
|
let status = Command::new("cmd")
|
||||||
|
.args(["/c", "npx playwright install chromium"])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
let status = Command::new("npx")
|
let status = Command::new("npx")
|
||||||
.args(["playwright", "install", "chromium"])
|
.args(["playwright", "install", "chromium"])
|
||||||
.status();
|
.status();
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agent-browser",
|
"name": "agent-browser",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user