Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,23 @@ The daemon starts automatically on first command and persists between commands f
| Linux x64 | ✅ Native Rust | Node.js |
| Windows | - | Node.js |

## Integrations

### Browserbase

[Browserbase](https://browserbase.com) provides remote browser infrastructure to make deployment of agentic browsing agents easy. Use it when running the agent-browser CLI in an environment where a local browser isn't feasible.

To enable Browserbase, set these environment variables:

```bash
export BROWSERBASE_API_KEY="your-api-key"
export BROWSERBASE_PROJECT_ID="your-project-id"
```

When both variables are set, agent-browser automatically connects to a Browserbase session instead of launching a local browser. All commands work identically.

Get your API key and project ID from the [Browserbase Dashboard](https://browserbase.com/dashboard).

## License

Apache-2.0
2 changes: 1 addition & 1 deletion cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Option<Value> {
_ => None,
},

// Session commands are handled entirely in main.rs
"session" => None,

_ => None,
}
}
Expand Down
4 changes: 2 additions & 2 deletions cli/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn get_port_for_session(session: &str) -> u16 {
}

#[cfg(unix)]
fn is_daemon_running(session: &str) -> bool {
pub fn is_daemon_running(session: &str) -> bool {
let pid_path = get_pid_path(session);
if !pid_path.exists() {
return false;
Expand All @@ -124,7 +124,7 @@ fn is_daemon_running(session: &str) -> bool {
}

#[cfg(windows)]
fn is_daemon_running(session: &str) -> bool {
pub fn is_daemon_running(session: &str) -> bool {
let pid_path = get_pid_path(session);
if !pid_path.exists() {
return false;
Expand Down
210 changes: 210 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod connection;
mod flags;
mod install;
mod output;
mod session;

use serde_json::json;
use std::env;
Expand All @@ -13,6 +14,139 @@ use connection::{ensure_daemon, send_command};
use flags::{clean_args, parse_flags};
use install::run_install;
use output::{print_help, print_response};
use session::{find_local_sessions, is_uuid, kill_local_session, show_local_session_info};

/// Try to get cloud sessions from daemon (returns empty vec if daemon not running or API key not set)
fn try_get_cloud_sessions(session: &str, _json: bool) -> Vec<serde_json::Value> {
// Check if daemon is running first
if !connection::is_daemon_running(session) {
return Vec::new();
}

let cmd = json!({
"id": gen_id(),
"action": "bb_session_list"
});

match send_command(cmd, session) {
Ok(resp) if resp.success => {
if let Some(data) = &resp.data {
if let Some(sessions) = data.get("sessions") {
if let Some(arr) = sessions.as_array() {
return arr.clone();
}
}
}
Vec::new()
}
_ => Vec::new(),
}
}

/// Handle cloud session info query
fn handle_cloud_session_info(id: &str, flags: &flags::Flags) {
if let Err(e) = ensure_daemon(&flags.session, flags.headed) {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}

let cmd = json!({
"id": gen_id(),
"action": "bb_session_get",
"sessionId": id
});

match send_command(cmd, &flags.session) {
Ok(resp) => {
print_response(&resp, flags.json);
if !resp.success {
exit(1);
}
}
Err(e) => {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}
}
}

/// Handle cloud session stop
fn handle_cloud_session_stop(id: &str, flags: &flags::Flags) {
if let Err(e) = ensure_daemon(&flags.session, flags.headed) {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}

let cmd = json!({
"id": gen_id(),
"action": "bb_session_stop",
"sessionId": id
});

match send_command(cmd, &flags.session) {
Ok(resp) => {
print_response(&resp, flags.json);
if !resp.success {
exit(1);
}
}
Err(e) => {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}
}
}

/// Handle cloud session debug
fn handle_cloud_session_debug(id: &str, flags: &flags::Flags) {
if let Err(e) = ensure_daemon(&flags.session, flags.headed) {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}

let cmd = json!({
"id": gen_id(),
"action": "bb_session_debug",
"sessionId": id
});

match send_command(cmd, &flags.session) {
Ok(resp) => {
print_response(&resp, flags.json);
if !resp.success {
exit(1);
}
}
Err(e) => {
if flags.json {
println!(r#"{{"success":false,"error":"{}"}}"#, e);
} else {
eprintln!("\x1b[31m✗\x1b[0m {}", e);
}
exit(1);
}
}
}

fn main() {
let args: Vec<String> = env::args().skip(1).collect();
Expand All @@ -31,6 +165,82 @@ fn main() {
return;
}

// Handle unified session commands
if clean.get(0).map(|s| s.as_str()) == Some("session") {
match clean.get(1).map(|s| s.as_str()) {
Some("list") => {
// Get local sessions first
let local_sessions = find_local_sessions();

// Try to get cloud sessions from daemon (non-blocking)
let cloud_sessions = try_get_cloud_sessions(&flags.session, flags.json);

// Print combined results
output::print_session_list(&local_sessions, &cloud_sessions, flags.json);
return;
}
Some("info") => {
let id = clean.get(2).map(|s| s.as_str()).unwrap_or(&flags.session);

// If looks like UUID, query cloud; otherwise check local
if is_uuid(id) {
// Query cloud session via daemon
handle_cloud_session_info(id, &flags);
} else {
show_local_session_info(id, flags.json);
}
return;
}
Some("kill") => {
if let Some(id) = clean.get(2) {
// If looks like UUID, stop cloud session; otherwise kill local
if is_uuid(id) {
handle_cloud_session_stop(id, &flags);
} else {
kill_local_session(id, flags.json);
}
} else {
if flags.json {
println!(r#"{{"success":false,"error":"session kill requires a session name or ID"}}"#);
} else {
eprintln!("\x1b[31m✗\x1b[0m session kill requires a session name or ID");
eprintln!("\x1b[2mUsage: agent-browser session kill <name|id>\x1b[0m");
}
exit(1);
}
return;
}
Some("debug") => {
if let Some(id) = clean.get(2) {
handle_cloud_session_debug(id, &flags);
} else {
if flags.json {
println!(r#"{{"success":false,"error":"session debug requires a session ID"}}"#);
} else {
eprintln!("\x1b[31m✗\x1b[0m session debug requires a session ID");
eprintln!("\x1b[2mUsage: agent-browser session debug <id>\x1b[0m");
}
exit(1);
}
return;
}
Some(_) => {
if flags.json {
println!(r#"{{"success":false,"error":"Unknown session subcommand"}}"#);
} else {
eprintln!("\x1b[31m✗\x1b[0m Unknown session subcommand");
eprintln!("\x1b[2mUsage: agent-browser session [list|info|kill|debug]\x1b[0m");
}
exit(1);
}
None => {
// `session` with no subcommand shows current session
show_local_session_info(&flags.session, flags.json);
return;
}
}
}

let cmd = match parse_command(&clean, &flags) {
Some(c) => c,
None => {
Expand Down
Loading