agent-browser 0.3.1 → 0.3.3

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/cli/src/main.rs DELETED
@@ -1,332 +0,0 @@
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,31 +0,0 @@
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"]
@@ -1,68 +0,0 @@
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
- '