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`
|
**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
|
### Clipboard
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1317,6 +1317,12 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
|||||||
|
|
||||||
"diff" => parse_diff(&rest, &id, flags),
|
"diff" => parse_diff(&rest, &id, flags),
|
||||||
|
|
||||||
|
// === Batch ===
|
||||||
|
"batch" => {
|
||||||
|
let bail = rest.contains(&"--bail");
|
||||||
|
Ok(json!({ "id": id, "action": "batch", "bail": bail }))
|
||||||
|
}
|
||||||
|
|
||||||
_ => Err(ParseError::UnknownCommand {
|
_ => Err(ParseError::UnknownCommand {
|
||||||
command: cmd.to_string(),
|
command: cmd.to_string(),
|
||||||
}),
|
}),
|
||||||
@@ -3961,4 +3967,20 @@ mod tests {
|
|||||||
let cmd = parse_command(&args("get cdp-url"), &default_flags()).unwrap();
|
let cmd = parse_command(&args("get cdp-url"), &default_flags()).unwrap();
|
||||||
assert_eq!(cmd["action"], "cdp_url");
|
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 commands::{gen_id, parse_command, ParseError};
|
||||||
use connection::{ensure_daemon, get_socket_dir, send_command, DaemonOptions};
|
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 install::run_install;
|
||||||
use output::{
|
use output::{
|
||||||
print_command_help, print_help, print_response_with_opts, print_version, OutputOptions,
|
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 {
|
let output_opts = OutputOptions {
|
||||||
json: flags.json,
|
json: flags.json,
|
||||||
content_boundaries: flags.content_boundaries,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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,
|
_ => return false,
|
||||||
};
|
};
|
||||||
println!("{}", help.trim());
|
println!("{}", help.trim());
|
||||||
@@ -2508,6 +2540,10 @@ Debug:
|
|||||||
inspect Open Chrome DevTools for the active page
|
inspect Open Chrome DevTools for the active page
|
||||||
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
|
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 Vault:
|
||||||
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
||||||
auth login <name> Login using saved credentials
|
auth login <name> Login using saved credentials
|
||||||
|
|||||||
Reference in New Issue
Block a user