feat: add batch command for multi-step workflows (#865)
Add `batch` command that reads a JSON array of commands from stdin and executes them sequentially against the daemon. This avoids per-command process startup overhead when AI agents run multi-step browser workflows. Supports --bail to stop on first error (default: continue all) and --json for structured output as an array of results. Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Matt Van Horn
Claude Opus 4.6
parent
a865dd56e0
commit
7734bb2702
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"agent-browser": minor
|
||||
---
|
||||
|
||||
Add `batch` command for executing multiple commands from stdin in a single invocation. Accepts a JSON array of string arrays and returns results sequentially. Supports `--bail` to stop on first error and `--json` for structured output.
|
||||
@@ -187,6 +187,25 @@ agent-browser wait "#spinner" --state hidden
|
||||
|
||||
**Load states:** `load`, `domcontentloaded`, `networkidle`
|
||||
|
||||
### Batch Execution
|
||||
|
||||
Execute multiple commands in a single invocation by piping a JSON array of
|
||||
string arrays to `batch`. This avoids per-command process startup overhead
|
||||
when running multi-step workflows.
|
||||
|
||||
```bash
|
||||
# Pipe commands as JSON
|
||||
echo '[
|
||||
["open", "https://example.com"],
|
||||
["snapshot", "-i"],
|
||||
["click", "@e1"],
|
||||
["screenshot", "result.png"]
|
||||
]' | agent-browser batch --json
|
||||
|
||||
# Stop on first error
|
||||
agent-browser batch --bail < commands.json
|
||||
```
|
||||
|
||||
### Clipboard
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1317,6 +1317,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
|
||||
"diff" => parse_diff(&rest, &id, flags),
|
||||
|
||||
// === Batch ===
|
||||
"batch" => {
|
||||
let bail = rest.contains(&"--bail");
|
||||
Ok(json!({ "id": id, "action": "batch", "bail": bail }))
|
||||
}
|
||||
|
||||
_ => Err(ParseError::UnknownCommand {
|
||||
command: cmd.to_string(),
|
||||
}),
|
||||
@@ -3961,4 +3967,20 @@ mod tests {
|
||||
let cmd = parse_command(&args("get cdp-url"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "cdp_url");
|
||||
}
|
||||
|
||||
// === Batch Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_batch_default() {
|
||||
let cmd = parse_command(&args("batch"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "batch");
|
||||
assert_eq!(cmd["bail"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_with_bail() {
|
||||
let cmd = parse_command(&args("batch --bail"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "batch");
|
||||
assert_eq!(cmd["bail"], true);
|
||||
}
|
||||
}
|
||||
|
||||
+152
-1
@@ -21,7 +21,7 @@ use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_I
|
||||
|
||||
use commands::{gen_id, parse_command, ParseError};
|
||||
use connection::{ensure_daemon, get_socket_dir, send_command, DaemonOptions};
|
||||
use flags::{clean_args, parse_flags};
|
||||
use flags::{clean_args, parse_flags, Flags};
|
||||
use install::run_install;
|
||||
use output::{
|
||||
print_command_help, print_help, print_response_with_opts, print_version, OutputOptions,
|
||||
@@ -729,6 +729,13 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle batch command: read commands from stdin, execute sequentially
|
||||
if cmd.get("action").and_then(|v| v.as_str()) == Some("batch") {
|
||||
let bail = cmd.get("bail").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
run_batch(&flags, bail);
|
||||
return;
|
||||
}
|
||||
|
||||
let output_opts = OutputOptions {
|
||||
json: flags.json,
|
||||
content_boundaries: flags.content_boundaries,
|
||||
@@ -809,6 +816,150 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_batch(flags: &Flags, bail: bool) {
|
||||
use std::io::Read as _;
|
||||
|
||||
let mut input = String::new();
|
||||
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
|
||||
if flags.json {
|
||||
print_json_error(format!("Failed to read stdin: {}", e));
|
||||
} else {
|
||||
eprintln!("{} Failed to read stdin: {}", color::error_indicator(), e);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let commands: Vec<Vec<String>> = match serde_json::from_str(&input) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
if flags.json {
|
||||
print_json_error(format!(
|
||||
"Invalid JSON input: {}. Expected an array of string arrays, e.g. [[\"open\", \"https://example.com\"], [\"snapshot\"]]",
|
||||
e
|
||||
));
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Invalid JSON input: {}. Expected an array of string arrays.",
|
||||
color::error_indicator(),
|
||||
e
|
||||
);
|
||||
}
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if commands.is_empty() {
|
||||
if flags.json {
|
||||
println!("[]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let output_opts = OutputOptions {
|
||||
json: flags.json,
|
||||
content_boundaries: flags.content_boundaries,
|
||||
max_output: flags.max_output,
|
||||
};
|
||||
|
||||
let mut results: Vec<serde_json::Value> = Vec::new();
|
||||
let mut had_error = false;
|
||||
|
||||
for (i, cmd_args) in commands.iter().enumerate() {
|
||||
if cmd_args.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed = match parse_command(cmd_args, flags) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
had_error = true;
|
||||
if flags.json {
|
||||
results.push(json!({
|
||||
"command": cmd_args,
|
||||
"success": false,
|
||||
"error": e.format(),
|
||||
}));
|
||||
if bail {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} Command {}: {}",
|
||||
color::error_indicator(),
|
||||
i + 1,
|
||||
e.format()
|
||||
);
|
||||
if bail {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let action = parsed
|
||||
.get("action")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
match send_command(parsed, &flags.session) {
|
||||
Ok(resp) => {
|
||||
if flags.json {
|
||||
results.push(json!({
|
||||
"command": cmd_args,
|
||||
"success": resp.success,
|
||||
"result": resp.data,
|
||||
"error": resp.error,
|
||||
}));
|
||||
} else {
|
||||
if i > 0 {
|
||||
println!();
|
||||
}
|
||||
print_response_with_opts(&resp, action.as_deref(), &output_opts);
|
||||
}
|
||||
if !resp.success {
|
||||
had_error = true;
|
||||
if bail {
|
||||
if !flags.json {
|
||||
exit(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
had_error = true;
|
||||
if flags.json {
|
||||
results.push(json!({
|
||||
"command": cmd_args,
|
||||
"success": false,
|
||||
"error": e.to_string(),
|
||||
}));
|
||||
if bail {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
eprintln!("{} Command {}: {}", color::error_indicator(), i + 1, e);
|
||||
if bail {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if flags.json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string(&results).unwrap_or_else(|_| "[]".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
if had_error {
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -2418,6 +2418,38 @@ Examples:
|
||||
"##
|
||||
}
|
||||
|
||||
"batch" => {
|
||||
r##"
|
||||
agent-browser batch - Execute multiple commands from stdin
|
||||
|
||||
Usage: echo '<json>' | agent-browser batch [options]
|
||||
|
||||
Reads a JSON array of commands from stdin and executes them sequentially.
|
||||
Each command is an array of strings matching normal CLI arguments.
|
||||
Results are printed in order, separated by blank lines (or as a JSON array
|
||||
with --json).
|
||||
|
||||
Options:
|
||||
--bail Stop on first error (default: continue all commands)
|
||||
--json Output results as a JSON array
|
||||
|
||||
Input Format:
|
||||
A JSON array of string arrays. Each inner array is one command:
|
||||
[
|
||||
["open", "https://example.com"],
|
||||
["snapshot", "-i"],
|
||||
["click", "@e1"],
|
||||
["fill", "@e2", "test@example.com"],
|
||||
["screenshot", "result.png"]
|
||||
]
|
||||
|
||||
Examples:
|
||||
echo '[["open", "https://example.com"], ["snapshot"]]' | agent-browser batch
|
||||
echo '[["open", "https://example.com"], ["get", "title"]]' | agent-browser batch --json
|
||||
agent-browser batch --bail < commands.json
|
||||
"##
|
||||
}
|
||||
|
||||
_ => return false,
|
||||
};
|
||||
println!("{}", help.trim());
|
||||
@@ -2508,6 +2540,10 @@ Debug:
|
||||
inspect Open Chrome DevTools for the active page
|
||||
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
|
||||
|
||||
Batch:
|
||||
batch [--bail] Execute commands from stdin (JSON array of string arrays)
|
||||
--bail stops on first error (default: continue all)
|
||||
|
||||
Auth Vault:
|
||||
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
||||
auth login <name> Login using saved credentials
|
||||
|
||||
Reference in New Issue
Block a user