agent-browser 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-browser +0 -0
- package/bin/agent-browser.cmd +35 -0
- package/cli/Cargo.lock +114 -0
- package/cli/Cargo.toml +17 -0
- package/cli/src/main.rs +332 -0
- package/dist/cli-light.d.ts +1 -1
- package/dist/cli-light.js +1 -1
- package/docker/Dockerfile.build +31 -0
- package/docker/docker-compose.yml +68 -0
- package/package.json +5 -3
- package/scripts/build-all-platforms.sh +68 -0
- package/scripts/copy-native.js +35 -0
- package/scripts/postinstall.js +113 -15
- package/src/cli-light.ts +1 -1
package/bin/agent-browser
CHANGED
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
:: Cross-platform launcher for agent-browser (Windows)
|
|
3
|
+
:: Detects architecture and runs the appropriate native binary
|
|
4
|
+
|
|
5
|
+
setlocal
|
|
6
|
+
|
|
7
|
+
set "SCRIPT_DIR=%~dp0"
|
|
8
|
+
|
|
9
|
+
:: Detect architecture
|
|
10
|
+
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
|
|
11
|
+
set "ARCH=x64"
|
|
12
|
+
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
|
13
|
+
set "ARCH=arm64"
|
|
14
|
+
) else (
|
|
15
|
+
set "ARCH=x64"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
set "BINARY=%SCRIPT_DIR%agent-browser-win32-%ARCH%.exe"
|
|
19
|
+
|
|
20
|
+
:: Try native binary first
|
|
21
|
+
if exist "%BINARY%" (
|
|
22
|
+
"%BINARY%" %*
|
|
23
|
+
exit /b %errorlevel%
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
:: Fallback to Node.js implementation
|
|
27
|
+
set "NODE_CLI=%SCRIPT_DIR%..\dist\cli-light.js"
|
|
28
|
+
if exist "%NODE_CLI%" (
|
|
29
|
+
node "%NODE_CLI%" %*
|
|
30
|
+
exit /b %errorlevel%
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
echo Error: No binary found for win32-%ARCH% >&2
|
|
34
|
+
echo Run 'npm run build:native' or 'npm run build:all-platforms' to build >&2
|
|
35
|
+
exit /b 1
|
package/cli/Cargo.lock
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "agent-browser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"libc",
|
|
10
|
+
"serde",
|
|
11
|
+
"serde_json",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[package]]
|
|
15
|
+
name = "itoa"
|
|
16
|
+
version = "1.0.17"
|
|
17
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
18
|
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
|
19
|
+
|
|
20
|
+
[[package]]
|
|
21
|
+
name = "libc"
|
|
22
|
+
version = "0.2.180"
|
|
23
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
+
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
|
25
|
+
|
|
26
|
+
[[package]]
|
|
27
|
+
name = "memchr"
|
|
28
|
+
version = "2.7.6"
|
|
29
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
30
|
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
|
31
|
+
|
|
32
|
+
[[package]]
|
|
33
|
+
name = "proc-macro2"
|
|
34
|
+
version = "1.0.105"
|
|
35
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
+
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
|
|
37
|
+
dependencies = [
|
|
38
|
+
"unicode-ident",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "quote"
|
|
43
|
+
version = "1.0.43"
|
|
44
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
|
+
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
|
|
46
|
+
dependencies = [
|
|
47
|
+
"proc-macro2",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "serde"
|
|
52
|
+
version = "1.0.228"
|
|
53
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
54
|
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
|
55
|
+
dependencies = [
|
|
56
|
+
"serde_core",
|
|
57
|
+
"serde_derive",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[[package]]
|
|
61
|
+
name = "serde_core"
|
|
62
|
+
version = "1.0.228"
|
|
63
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
|
65
|
+
dependencies = [
|
|
66
|
+
"serde_derive",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[[package]]
|
|
70
|
+
name = "serde_derive"
|
|
71
|
+
version = "1.0.228"
|
|
72
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
73
|
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|
74
|
+
dependencies = [
|
|
75
|
+
"proc-macro2",
|
|
76
|
+
"quote",
|
|
77
|
+
"syn",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[[package]]
|
|
81
|
+
name = "serde_json"
|
|
82
|
+
version = "1.0.149"
|
|
83
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
84
|
+
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
|
85
|
+
dependencies = [
|
|
86
|
+
"itoa",
|
|
87
|
+
"memchr",
|
|
88
|
+
"serde",
|
|
89
|
+
"serde_core",
|
|
90
|
+
"zmij",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
[[package]]
|
|
94
|
+
name = "syn"
|
|
95
|
+
version = "2.0.114"
|
|
96
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
97
|
+
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
|
98
|
+
dependencies = [
|
|
99
|
+
"proc-macro2",
|
|
100
|
+
"quote",
|
|
101
|
+
"unicode-ident",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[[package]]
|
|
105
|
+
name = "unicode-ident"
|
|
106
|
+
version = "1.0.22"
|
|
107
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
108
|
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
|
109
|
+
|
|
110
|
+
[[package]]
|
|
111
|
+
name = "zmij"
|
|
112
|
+
version = "1.0.12"
|
|
113
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
114
|
+
checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
|
package/cli/Cargo.toml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "agent-browser"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "Fast browser automation CLI for AI agents"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
serde = { version = "1.0", features = ["derive"] }
|
|
10
|
+
serde_json = "1.0"
|
|
11
|
+
libc = "0.2"
|
|
12
|
+
|
|
13
|
+
[profile.release]
|
|
14
|
+
opt-level = 3
|
|
15
|
+
lto = true
|
|
16
|
+
codegen-units = 1
|
|
17
|
+
strip = true
|
package/cli/src/main.rs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::{json, Value};
|
|
3
|
+
use std::env;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::io::{BufRead, BufReader, Write};
|
|
6
|
+
use std::os::unix::net::UnixStream;
|
|
7
|
+
use std::path::PathBuf;
|
|
8
|
+
use std::process::{exit, Command, Stdio};
|
|
9
|
+
use std::thread;
|
|
10
|
+
use std::time::Duration;
|
|
11
|
+
|
|
12
|
+
#[derive(Serialize)]
|
|
13
|
+
struct Request {
|
|
14
|
+
id: String,
|
|
15
|
+
action: String,
|
|
16
|
+
#[serde(flatten)]
|
|
17
|
+
extra: Value,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Deserialize, Serialize)]
|
|
21
|
+
struct Response {
|
|
22
|
+
success: bool,
|
|
23
|
+
data: Option<Value>,
|
|
24
|
+
error: Option<String>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn get_socket_path() -> PathBuf {
|
|
28
|
+
let session = env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string());
|
|
29
|
+
let tmp = env::temp_dir();
|
|
30
|
+
tmp.join(format!("agent-browser-{}.sock", session))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn get_pid_path() -> PathBuf {
|
|
34
|
+
let session = env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string());
|
|
35
|
+
let tmp = env::temp_dir();
|
|
36
|
+
tmp.join(format!("agent-browser-{}.pid", session))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn is_daemon_running() -> bool {
|
|
40
|
+
let pid_path = get_pid_path();
|
|
41
|
+
if !pid_path.exists() {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if let Ok(pid_str) = fs::read_to_string(&pid_path) {
|
|
45
|
+
if let Ok(pid) = pid_str.trim().parse::<i32>() {
|
|
46
|
+
// Check if process exists
|
|
47
|
+
unsafe {
|
|
48
|
+
return libc::kill(pid, 0) == 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn ensure_daemon() -> Result<(), String> {
|
|
56
|
+
let socket_path = get_socket_path();
|
|
57
|
+
|
|
58
|
+
if is_daemon_running() && socket_path.exists() {
|
|
59
|
+
return Ok(());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find daemon.js
|
|
63
|
+
let exe_path = env::current_exe().map_err(|e| e.to_string())?;
|
|
64
|
+
let exe_dir = exe_path.parent().unwrap();
|
|
65
|
+
|
|
66
|
+
let daemon_paths = [
|
|
67
|
+
exe_dir.join("daemon.js"),
|
|
68
|
+
exe_dir.join("../dist/daemon.js"),
|
|
69
|
+
PathBuf::from("dist/daemon.js"),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
let daemon_path = daemon_paths
|
|
73
|
+
.iter()
|
|
74
|
+
.find(|p| p.exists())
|
|
75
|
+
.ok_or("Daemon not found. Run from project directory or ensure daemon.js is alongside binary.")?;
|
|
76
|
+
|
|
77
|
+
// Start daemon
|
|
78
|
+
let session = env::var("AGENT_BROWSER_SESSION").unwrap_or_else(|_| "default".to_string());
|
|
79
|
+
Command::new("node")
|
|
80
|
+
.arg(daemon_path)
|
|
81
|
+
.env("AGENT_BROWSER_DAEMON", "1")
|
|
82
|
+
.env("AGENT_BROWSER_SESSION", &session)
|
|
83
|
+
.stdin(Stdio::null())
|
|
84
|
+
.stdout(Stdio::null())
|
|
85
|
+
.stderr(Stdio::null())
|
|
86
|
+
.spawn()
|
|
87
|
+
.map_err(|e| format!("Failed to start daemon: {}", e))?;
|
|
88
|
+
|
|
89
|
+
// Wait for socket
|
|
90
|
+
for _ in 0..50 {
|
|
91
|
+
if socket_path.exists() {
|
|
92
|
+
return Ok(());
|
|
93
|
+
}
|
|
94
|
+
thread::sleep(Duration::from_millis(100));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Err("Daemon failed to start".to_string())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn send_command(cmd: Value) -> Result<Response, String> {
|
|
101
|
+
let socket_path = get_socket_path();
|
|
102
|
+
let mut stream = UnixStream::connect(&socket_path)
|
|
103
|
+
.map_err(|e| format!("Failed to connect: {}", e))?;
|
|
104
|
+
|
|
105
|
+
stream.set_read_timeout(Some(Duration::from_secs(30))).ok();
|
|
106
|
+
stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
|
|
107
|
+
|
|
108
|
+
let mut json_str = serde_json::to_string(&cmd).map_err(|e| e.to_string())?;
|
|
109
|
+
json_str.push('\n');
|
|
110
|
+
|
|
111
|
+
stream.write_all(json_str.as_bytes())
|
|
112
|
+
.map_err(|e| format!("Failed to send: {}", e))?;
|
|
113
|
+
|
|
114
|
+
let mut reader = BufReader::new(stream);
|
|
115
|
+
let mut response_line = String::new();
|
|
116
|
+
reader.read_line(&mut response_line)
|
|
117
|
+
.map_err(|e| format!("Failed to read: {}", e))?;
|
|
118
|
+
|
|
119
|
+
serde_json::from_str(&response_line)
|
|
120
|
+
.map_err(|e| format!("Invalid response: {}", e))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn gen_id() -> String {
|
|
124
|
+
format!("r{}", std::time::SystemTime::now()
|
|
125
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
126
|
+
.unwrap()
|
|
127
|
+
.as_micros() % 1000000)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn parse_command(args: &[String]) -> Option<Value> {
|
|
131
|
+
if args.is_empty() {
|
|
132
|
+
return None;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let cmd = args[0].as_str();
|
|
136
|
+
let rest: Vec<&str> = args[1..].iter().map(|s| s.as_str()).collect();
|
|
137
|
+
let id = gen_id();
|
|
138
|
+
|
|
139
|
+
match cmd {
|
|
140
|
+
"open" | "goto" | "navigate" => {
|
|
141
|
+
let url = rest.get(0)?;
|
|
142
|
+
let url = if url.starts_with("http") {
|
|
143
|
+
url.to_string()
|
|
144
|
+
} else {
|
|
145
|
+
format!("https://{}", url)
|
|
146
|
+
};
|
|
147
|
+
Some(json!({ "id": id, "action": "navigate", "url": url }))
|
|
148
|
+
}
|
|
149
|
+
"click" => Some(json!({ "id": id, "action": "click", "selector": rest.get(0)? })),
|
|
150
|
+
"fill" => Some(json!({ "id": id, "action": "fill", "selector": rest.get(0)?, "value": rest[1..].join(" ") })),
|
|
151
|
+
"type" => Some(json!({ "id": id, "action": "type", "selector": rest.get(0)?, "text": rest[1..].join(" ") })),
|
|
152
|
+
"hover" => Some(json!({ "id": id, "action": "hover", "selector": rest.get(0)? })),
|
|
153
|
+
"snapshot" => {
|
|
154
|
+
let mut cmd = json!({ "id": id, "action": "snapshot" });
|
|
155
|
+
let obj = cmd.as_object_mut().unwrap();
|
|
156
|
+
for (i, arg) in rest.iter().enumerate() {
|
|
157
|
+
match *arg {
|
|
158
|
+
"-i" | "--interactive" => { obj.insert("interactive".to_string(), json!(true)); }
|
|
159
|
+
"-c" | "--compact" => { obj.insert("compact".to_string(), json!(true)); }
|
|
160
|
+
"-d" | "--depth" => {
|
|
161
|
+
if let Some(d) = rest.get(i + 1) {
|
|
162
|
+
if let Ok(n) = d.parse::<i32>() {
|
|
163
|
+
obj.insert("maxDepth".to_string(), json!(n));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
"-s" | "--selector" => {
|
|
168
|
+
if let Some(s) = rest.get(i + 1) {
|
|
169
|
+
obj.insert("selector".to_string(), json!(s));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
_ => {}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
Some(cmd)
|
|
176
|
+
}
|
|
177
|
+
"screenshot" => Some(json!({ "id": id, "action": "screenshot", "path": rest.get(0) })),
|
|
178
|
+
"close" | "quit" | "exit" => Some(json!({ "id": id, "action": "close" })),
|
|
179
|
+
"get" => match rest.get(0).map(|s| *s) {
|
|
180
|
+
Some("text") => Some(json!({ "id": id, "action": "gettext", "selector": rest.get(1)? })),
|
|
181
|
+
Some("url") => Some(json!({ "id": id, "action": "url" })),
|
|
182
|
+
Some("title") => Some(json!({ "id": id, "action": "title" })),
|
|
183
|
+
_ => None,
|
|
184
|
+
},
|
|
185
|
+
"press" => Some(json!({ "id": id, "action": "press", "key": rest.get(0)? })),
|
|
186
|
+
"wait" => {
|
|
187
|
+
if let Some(arg) = rest.get(0) {
|
|
188
|
+
if arg.parse::<u64>().is_ok() {
|
|
189
|
+
Some(json!({ "id": id, "action": "wait", "timeout": arg.parse::<u64>().unwrap() }))
|
|
190
|
+
} else {
|
|
191
|
+
Some(json!({ "id": id, "action": "wait", "selector": arg }))
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
None
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
"back" => Some(json!({ "id": id, "action": "back" })),
|
|
198
|
+
"forward" => Some(json!({ "id": id, "action": "forward" })),
|
|
199
|
+
"reload" => Some(json!({ "id": id, "action": "reload" })),
|
|
200
|
+
"eval" => Some(json!({ "id": id, "action": "evaluate", "script": rest.join(" ") })),
|
|
201
|
+
_ => None,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn print_response(resp: &Response, json_mode: bool) {
|
|
206
|
+
if json_mode {
|
|
207
|
+
println!("{}", serde_json::to_string(resp).unwrap_or_default());
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if !resp.success {
|
|
212
|
+
eprintln!("\x1b[31m✗ Error:\x1b[0m {}", resp.error.as_deref().unwrap_or("Unknown error"));
|
|
213
|
+
exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if let Some(data) = &resp.data {
|
|
217
|
+
if let Some(url) = data.get("url").and_then(|v| v.as_str()) {
|
|
218
|
+
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
|
219
|
+
println!("\x1b[32m✓\x1b[0m \x1b[1m{}\x1b[0m", title);
|
|
220
|
+
println!("\x1b[2m {}\x1b[0m", url);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
println!("{}", url);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if let Some(snapshot) = data.get("snapshot").and_then(|v| v.as_str()) {
|
|
227
|
+
println!("{}", snapshot);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if let Some(title) = data.get("title").and_then(|v| v.as_str()) {
|
|
231
|
+
println!("{}", title);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if let Some(text) = data.get("text").and_then(|v| v.as_str()) {
|
|
235
|
+
println!("{}", text);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if let Some(result) = data.get("result") {
|
|
239
|
+
println!("{}", serde_json::to_string_pretty(result).unwrap_or_default());
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if data.get("closed").is_some() {
|
|
243
|
+
println!("\x1b[32m✓\x1b[0m Browser closed");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
println!("\x1b[32m✓\x1b[0m Done");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fn print_help() {
|
|
251
|
+
println!(r#"
|
|
252
|
+
agent-browser - fast browser automation CLI (Rust)
|
|
253
|
+
|
|
254
|
+
Usage: agent-browser <command> [args] [--json]
|
|
255
|
+
|
|
256
|
+
Commands:
|
|
257
|
+
open <url> Navigate to URL
|
|
258
|
+
click <sel> Click element (@ref from snapshot)
|
|
259
|
+
fill <sel> <text> Fill input
|
|
260
|
+
type <sel> <text> Type text
|
|
261
|
+
hover <sel> Hover element
|
|
262
|
+
snapshot [opts] Get accessibility tree with refs
|
|
263
|
+
screenshot [path] Take screenshot
|
|
264
|
+
get text <sel> Get text content
|
|
265
|
+
get url Get current URL
|
|
266
|
+
get title Get page title
|
|
267
|
+
press <key> Press keyboard key
|
|
268
|
+
wait <ms|sel> Wait for time or element
|
|
269
|
+
eval <js> Evaluate JavaScript
|
|
270
|
+
close Close browser
|
|
271
|
+
|
|
272
|
+
Snapshot Options:
|
|
273
|
+
-i, --interactive Only interactive elements
|
|
274
|
+
-c, --compact Remove empty structural elements
|
|
275
|
+
-d, --depth <n> Limit tree depth
|
|
276
|
+
-s, --selector <sel> Scope to CSS selector
|
|
277
|
+
|
|
278
|
+
Options:
|
|
279
|
+
--json Output JSON
|
|
280
|
+
|
|
281
|
+
Examples:
|
|
282
|
+
agent-browser open example.com
|
|
283
|
+
agent-browser snapshot -i
|
|
284
|
+
agent-browser click @e2
|
|
285
|
+
"#);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fn main() {
|
|
289
|
+
let args: Vec<String> = env::args().skip(1).collect();
|
|
290
|
+
let json_mode = args.iter().any(|a| a == "--json");
|
|
291
|
+
let clean_args: Vec<String> = args.iter().filter(|a| !a.starts_with("--")).cloned().collect();
|
|
292
|
+
|
|
293
|
+
if clean_args.is_empty() || args.iter().any(|a| a == "--help" || a == "-h") {
|
|
294
|
+
print_help();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let cmd = match parse_command(&clean_args) {
|
|
299
|
+
Some(c) => c,
|
|
300
|
+
None => {
|
|
301
|
+
eprintln!("\x1b[31mUnknown command:\x1b[0m {}", clean_args.get(0).unwrap_or(&String::new()));
|
|
302
|
+
exit(1);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
if let Err(e) = ensure_daemon() {
|
|
307
|
+
if json_mode {
|
|
308
|
+
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
309
|
+
} else {
|
|
310
|
+
eprintln!("\x1b[31m✗ Error:\x1b[0m {}", e);
|
|
311
|
+
}
|
|
312
|
+
exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
match send_command(cmd) {
|
|
316
|
+
Ok(resp) => {
|
|
317
|
+
let success = resp.success;
|
|
318
|
+
print_response(&resp, json_mode);
|
|
319
|
+
if !success {
|
|
320
|
+
exit(1);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
Err(e) => {
|
|
324
|
+
if json_mode {
|
|
325
|
+
println!(r#"{{"success":false,"error":"{}"}}"#, e);
|
|
326
|
+
} else {
|
|
327
|
+
eprintln!("\x1b[31m✗ Error:\x1b[0m {}", e);
|
|
328
|
+
}
|
|
329
|
+
exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
package/dist/cli-light.d.ts
CHANGED
package/dist/cli-light.js
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Multi-platform Rust cross-compilation image
|
|
2
|
+
FROM rust:1.85-bookworm
|
|
3
|
+
|
|
4
|
+
# Install cross-compilation toolchains
|
|
5
|
+
RUN apt-get update && apt-get install -y \
|
|
6
|
+
gcc-aarch64-linux-gnu \
|
|
7
|
+
gcc-x86-64-linux-gnu \
|
|
8
|
+
libc6-dev-arm64-cross \
|
|
9
|
+
libc6-dev-amd64-cross \
|
|
10
|
+
mingw-w64 \
|
|
11
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
# Add Rust targets (Unix only - Windows uses Node.js fallback due to Unix socket dependency)
|
|
14
|
+
RUN rustup target add \
|
|
15
|
+
x86_64-unknown-linux-gnu \
|
|
16
|
+
aarch64-unknown-linux-gnu \
|
|
17
|
+
x86_64-apple-darwin \
|
|
18
|
+
aarch64-apple-darwin
|
|
19
|
+
|
|
20
|
+
# Install cargo-zigbuild for easier cross-compilation (especially macOS)
|
|
21
|
+
RUN curl -sSL https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | tar -xJ -C /opt \
|
|
22
|
+
&& ln -s /opt/zig-linux-x86_64-0.13.0/zig /usr/local/bin/zig
|
|
23
|
+
RUN cargo install cargo-zigbuild
|
|
24
|
+
|
|
25
|
+
# Configure linkers for cross-compilation
|
|
26
|
+
RUN mkdir -p /.cargo
|
|
27
|
+
RUN echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"\n\n[target.x86_64-pc-windows-gnu]\nlinker = "x86_64-w64-mingw32-gcc"\n' > /.cargo/config.toml
|
|
28
|
+
|
|
29
|
+
WORKDIR /build
|
|
30
|
+
|
|
31
|
+
ENTRYPOINT ["/bin/bash"]
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Docker Compose for building agent-browser
|
|
2
|
+
# Usage: docker compose -f docker/docker-compose.yml run build-all
|
|
3
|
+
#
|
|
4
|
+
# Note: Windows is skipped because the Rust CLI uses Unix sockets.
|
|
5
|
+
# Windows users will use the Node.js fallback automatically.
|
|
6
|
+
|
|
7
|
+
services:
|
|
8
|
+
# Build for all Unix platforms
|
|
9
|
+
build-all:
|
|
10
|
+
build:
|
|
11
|
+
context: ..
|
|
12
|
+
dockerfile: docker/Dockerfile.build
|
|
13
|
+
volumes:
|
|
14
|
+
- ../cli:/build
|
|
15
|
+
- ../bin:/output
|
|
16
|
+
command: |
|
|
17
|
+
-c '
|
|
18
|
+
set -e
|
|
19
|
+
echo "Building for all platforms..."
|
|
20
|
+
|
|
21
|
+
# Linux x64
|
|
22
|
+
echo "→ Linux x64"
|
|
23
|
+
cargo zigbuild --release --target x86_64-unknown-linux-gnu
|
|
24
|
+
cp /build/target/x86_64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-x64
|
|
25
|
+
chmod +x /output/agent-browser-linux-x64
|
|
26
|
+
|
|
27
|
+
# Linux ARM64
|
|
28
|
+
echo "→ Linux ARM64"
|
|
29
|
+
cargo zigbuild --release --target aarch64-unknown-linux-gnu
|
|
30
|
+
cp /build/target/aarch64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-arm64
|
|
31
|
+
chmod +x /output/agent-browser-linux-arm64
|
|
32
|
+
|
|
33
|
+
# macOS x64
|
|
34
|
+
echo "→ macOS x64"
|
|
35
|
+
cargo zigbuild --release --target x86_64-apple-darwin
|
|
36
|
+
cp /build/target/x86_64-apple-darwin/release/agent-browser /output/agent-browser-darwin-x64
|
|
37
|
+
chmod +x /output/agent-browser-darwin-x64
|
|
38
|
+
|
|
39
|
+
# macOS ARM64
|
|
40
|
+
echo "→ macOS ARM64"
|
|
41
|
+
cargo zigbuild --release --target aarch64-apple-darwin
|
|
42
|
+
cp /build/target/aarch64-apple-darwin/release/agent-browser /output/agent-browser-darwin-arm64
|
|
43
|
+
chmod +x /output/agent-browser-darwin-arm64
|
|
44
|
+
|
|
45
|
+
echo ""
|
|
46
|
+
echo "✓ All platforms built successfully!"
|
|
47
|
+
echo "(Windows uses Node.js fallback - Unix sockets not supported)"
|
|
48
|
+
ls -la /output/agent-browser-*
|
|
49
|
+
'
|
|
50
|
+
|
|
51
|
+
# Build for a single target (override with TARGET env var)
|
|
52
|
+
build-single:
|
|
53
|
+
build:
|
|
54
|
+
context: ..
|
|
55
|
+
dockerfile: docker/Dockerfile.build
|
|
56
|
+
volumes:
|
|
57
|
+
- ../cli:/build
|
|
58
|
+
- ../bin:/output
|
|
59
|
+
environment:
|
|
60
|
+
- TARGET=${TARGET:-x86_64-unknown-linux-gnu}
|
|
61
|
+
- OUTPUT_NAME=${OUTPUT_NAME:-agent-browser-linux-x64}
|
|
62
|
+
command: |
|
|
63
|
+
-c '
|
|
64
|
+
cargo zigbuild --release --target $TARGET
|
|
65
|
+
cp /build/target/$TARGET/release/agent-browser* /output/$OUTPUT_NAME
|
|
66
|
+
chmod +x /output/$OUTPUT_NAME 2>/dev/null || true
|
|
67
|
+
echo "✓ Built $OUTPUT_NAME"
|
|
68
|
+
'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Headless browser automation CLI for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
"agent-browser": "./bin/agent-browser"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc
|
|
12
|
-
"build:
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"build:native": "cargo build --release --manifest-path cli/Cargo.toml && node scripts/copy-native.js",
|
|
13
|
+
"build:all-platforms": "docker compose -f docker/docker-compose.yml run --rm build-all",
|
|
14
|
+
"build:docker": "docker build -t agent-browser-builder -f docker/Dockerfile.build .",
|
|
13
15
|
"start": "node dist/index.js",
|
|
14
16
|
"dev": "tsx src/index.ts",
|
|
15
17
|
"typecheck": "tsc --noEmit",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Build agent-browser for all platforms using Docker
|
|
5
|
+
# Usage: ./scripts/build-all-platforms.sh
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
9
|
+
OUTPUT_DIR="$PROJECT_ROOT/bin"
|
|
10
|
+
|
|
11
|
+
# Colors
|
|
12
|
+
RED='\033[0;31m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
NC='\033[0m' # No Color
|
|
16
|
+
|
|
17
|
+
echo -e "${YELLOW}Building agent-browser for all platforms...${NC}"
|
|
18
|
+
echo ""
|
|
19
|
+
|
|
20
|
+
# Ensure output directory exists
|
|
21
|
+
mkdir -p "$OUTPUT_DIR"
|
|
22
|
+
|
|
23
|
+
# Build the Docker image if needed
|
|
24
|
+
echo -e "${YELLOW}Building Docker cross-compilation image...${NC}"
|
|
25
|
+
docker build -t agent-browser-builder -f "$PROJECT_ROOT/docker/Dockerfile.build" "$PROJECT_ROOT"
|
|
26
|
+
|
|
27
|
+
# Function to build for a target
|
|
28
|
+
build_target() {
|
|
29
|
+
local target=$1
|
|
30
|
+
local output_name=$2
|
|
31
|
+
|
|
32
|
+
echo -e "${YELLOW}Building for ${target}...${NC}"
|
|
33
|
+
|
|
34
|
+
docker run --rm \
|
|
35
|
+
-v "$PROJECT_ROOT/cli:/build" \
|
|
36
|
+
-v "$OUTPUT_DIR:/output" \
|
|
37
|
+
agent-browser-builder \
|
|
38
|
+
-c "cargo zigbuild --release --target ${target} && cp /build/target/${target}/release/agent-browser* /output/${output_name} && chmod +x /output/${output_name} 2>/dev/null || true"
|
|
39
|
+
|
|
40
|
+
if [ -f "$OUTPUT_DIR/$output_name" ]; then
|
|
41
|
+
echo -e "${GREEN}✓ Built ${output_name}${NC}"
|
|
42
|
+
else
|
|
43
|
+
echo -e "${RED}✗ Failed to build ${output_name}${NC}"
|
|
44
|
+
return 1
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Build for each platform
|
|
49
|
+
# Linux x64
|
|
50
|
+
build_target "x86_64-unknown-linux-gnu" "agent-browser-linux-x64"
|
|
51
|
+
|
|
52
|
+
# Linux ARM64
|
|
53
|
+
build_target "aarch64-unknown-linux-gnu" "agent-browser-linux-arm64"
|
|
54
|
+
|
|
55
|
+
# Windows x64
|
|
56
|
+
build_target "x86_64-pc-windows-gnu" "agent-browser-win32-x64.exe"
|
|
57
|
+
|
|
58
|
+
# macOS x64 (via zig for cross-compilation)
|
|
59
|
+
build_target "x86_64-apple-darwin" "agent-browser-darwin-x64"
|
|
60
|
+
|
|
61
|
+
# macOS ARM64 (via zig for cross-compilation)
|
|
62
|
+
build_target "aarch64-apple-darwin" "agent-browser-darwin-arm64"
|
|
63
|
+
|
|
64
|
+
echo ""
|
|
65
|
+
echo -e "${GREEN}Build complete!${NC}"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Binaries are in: $OUTPUT_DIR"
|
|
68
|
+
ls -la "$OUTPUT_DIR"/agent-browser-*
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copies the compiled Rust binary to bin/ with platform-specific naming
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { platform, arch } from 'os';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const projectRoot = join(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
const sourcePath = join(projectRoot, 'cli/target/release/agent-browser');
|
|
16
|
+
const binDir = join(projectRoot, 'bin');
|
|
17
|
+
|
|
18
|
+
// Determine platform suffix
|
|
19
|
+
const platformKey = `${platform()}-${arch()}`;
|
|
20
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
21
|
+
const targetName = `agent-browser-${platformKey}${ext}`;
|
|
22
|
+
const targetPath = join(binDir, targetName);
|
|
23
|
+
|
|
24
|
+
if (!existsSync(sourcePath)) {
|
|
25
|
+
console.error(`Error: Native binary not found at ${sourcePath}`);
|
|
26
|
+
console.error('Run "cargo build --release --manifest-path cli/Cargo.toml" first');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!existsSync(binDir)) {
|
|
31
|
+
mkdirSync(binDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
copyFileSync(sourcePath, targetPath);
|
|
35
|
+
console.log(`✓ Copied native binary to ${targetPath}`);
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,17 +1,115 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall script for agent-browser
|
|
5
|
+
*
|
|
6
|
+
* Downloads the platform-specific native binary if not present.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync } from 'fs';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { platform, arch } from 'os';
|
|
13
|
+
import { get } from 'https';
|
|
14
|
+
import { execSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const projectRoot = join(__dirname, '..');
|
|
18
|
+
const binDir = join(projectRoot, 'bin');
|
|
19
|
+
|
|
20
|
+
// Platform detection
|
|
21
|
+
const platformKey = `${platform()}-${arch()}`;
|
|
22
|
+
const ext = platform() === 'win32' ? '.exe' : '';
|
|
23
|
+
const binaryName = `agent-browser-${platformKey}${ext}`;
|
|
24
|
+
const binaryPath = join(binDir, binaryName);
|
|
25
|
+
|
|
26
|
+
// Package info
|
|
27
|
+
const packageJson = JSON.parse(
|
|
28
|
+
(await import('fs')).readFileSync(join(projectRoot, 'package.json'), 'utf8')
|
|
29
|
+
);
|
|
30
|
+
const version = packageJson.version;
|
|
31
|
+
|
|
32
|
+
// GitHub release URL
|
|
33
|
+
const GITHUB_REPO = 'anthropics/agent-browser'; // Update this to your actual repo
|
|
34
|
+
const DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`;
|
|
35
|
+
|
|
36
|
+
async function downloadFile(url, dest) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const file = createWriteStream(dest);
|
|
39
|
+
|
|
40
|
+
const request = (url) => {
|
|
41
|
+
get(url, (response) => {
|
|
42
|
+
// Handle redirects
|
|
43
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
44
|
+
request(response.headers.location);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (response.statusCode !== 200) {
|
|
49
|
+
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
response.pipe(file);
|
|
54
|
+
file.on('finish', () => {
|
|
55
|
+
file.close();
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
}).on('error', (err) => {
|
|
59
|
+
unlinkSync(dest);
|
|
60
|
+
reject(err);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
request(url);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
// Check if binary already exists
|
|
70
|
+
if (existsSync(binaryPath)) {
|
|
71
|
+
console.log(`✓ Native binary already exists: ${binaryName}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure bin directory exists
|
|
76
|
+
if (!existsSync(binDir)) {
|
|
77
|
+
mkdirSync(binDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Downloading native binary for ${platformKey}...`);
|
|
81
|
+
console.log(`URL: ${DOWNLOAD_URL}`);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await downloadFile(DOWNLOAD_URL, binaryPath);
|
|
85
|
+
|
|
86
|
+
// Make executable on Unix
|
|
87
|
+
if (platform() !== 'win32') {
|
|
88
|
+
chmodSync(binaryPath, 0o755);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`✓ Downloaded native binary: ${binaryName}`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.log(`⚠ Could not download native binary: ${err.message}`);
|
|
94
|
+
console.log(` The CLI will use Node.js fallback (slightly slower startup)`);
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log('To build the native binary locally:');
|
|
97
|
+
console.log(' 1. Install Rust: https://rustup.rs');
|
|
98
|
+
console.log(' 2. Run: npm run build:native');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Reminder about Playwright browsers
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('╔═══════════════════════════════════════════════════════════════════════════╗');
|
|
104
|
+
console.log('║ To download browser binaries, run: ║');
|
|
105
|
+
console.log('║ ║');
|
|
106
|
+
console.log('║ npx playwright install chromium ║');
|
|
107
|
+
console.log('║ ║');
|
|
108
|
+
console.log('║ On Linux, include system dependencies with: ║');
|
|
109
|
+
console.log('║ ║');
|
|
110
|
+
console.log('║ npx playwright install --with-deps chromium ║');
|
|
111
|
+
console.log('║ ║');
|
|
112
|
+
console.log('╚═══════════════════════════════════════════════════════════════════════════╝');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
main().catch(console.error);
|
package/src/cli-light.ts
CHANGED