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 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
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Lightweight CLI client for agent-browser
4
4
  *
package/dist/cli-light.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Lightweight CLI client for agent-browser
4
4
  *
@@ -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.2.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 && bun build ./src/cli-light.ts --compile --outfile ./bin/agent-browser",
12
- "build:node": "tsc",
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}`);
@@ -1,17 +1,115 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const message = `
4
- ╔═══════════════════════════════════════════════════════════════════════════╗
5
- agent-browser was installed successfully! ║
6
- Please run the following command to download browser binaries: ║
7
- ║ ║
8
- ║ npx agent-browser install ║
9
- ║ ║
10
- On Linux, include system dependencies with: ║
11
- ║ ║
12
- ║ npx agent-browser install --with-deps ║
13
- ║ ║
14
- ╚═══════════════════════════════════════════════════════════════════════════╝
15
- `;
16
-
17
- console.log(message);
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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * Lightweight CLI client for agent-browser
4
4
  *