feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path (#749)
* feat: add screenshot output config, clipboard CLI commands, and fix wait --text native path
## Summary
- Add `--screenshot-dir`, `--screenshot-quality`, and `--screenshot-format` CLI flags (with corresponding `AGENT_BROWSER_SCREENSHOT_DIR`, `AGENT_BROWSER_SCREENSHOT_QUALITY`, `AGENT_BROWSER_SCREENSHOT_FORMAT` env vars) so users can configure where and how screenshots are saved without specifying a full path every time
- Add `clipboard read`, `clipboard write <text>`, `clipboard copy`, and `clipboard paste` CLI commands, exposing the existing protocol-level clipboard handlers that were previously only accessible via JSON-RPC
- Fix `wait --text` in native mode: the CLI was emitting `selector: "text=..."` (a Playwright-style locator) which native's `querySelector` can't handle. Now emits a `text` field that correctly hits the native `wait_for_text` polling path
- Add native clipboard `copy` and `paste` support via CDP `Input.dispatchKeyEvent`, and a `write` operation to the Node.js handler
* fix: resolve CI failures in Rust formatting and TypeScript typecheck
Use string-based page.evaluate for clipboard writeText to avoid
referencing `navigator` in Node.js compilation context. Run cargo fmt
to fix formatting in commands.rs and screenshot.rs.
* fix: clipboard write captures full multi-word text
Use rest[1..].join(" ") instead of rest.get(1) so unquoted multi-word
input like `clipboard write hello world` sends the full string rather
than silently dropping everything after the first word.
* improvements
* fixes
* improvements
* improvements
This commit is contained in:
@@ -115,6 +115,8 @@ agent-browser drag <src> <tgt> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
agent-browser screenshot [path] # Take screenshot (--full for full page, saves to a temporary directory if no path)
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||
agent-browser pdf <path> # Save as PDF
|
||||
agent-browser snapshot # Accessibility tree with refs (best for AI)
|
||||
agent-browser eval <js> # Run JavaScript (-b for base64, --stdin for piped input)
|
||||
@@ -179,14 +181,27 @@ agent-browser find nth 2 "a" text
|
||||
```bash
|
||||
agent-browser wait <selector> # Wait for element to be visible
|
||||
agent-browser wait <ms> # Wait for time (milliseconds)
|
||||
agent-browser wait --text "Welcome" # Wait for text to appear
|
||||
agent-browser wait --text "Welcome" # Wait for text to appear (substring match)
|
||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for load state
|
||||
agent-browser wait --fn "window.ready === true" # Wait for JS condition
|
||||
|
||||
# Wait for text/element to disappear
|
||||
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
|
||||
agent-browser wait "#spinner" --state hidden
|
||||
```
|
||||
|
||||
**Load states:** `load`, `domcontentloaded`, `networkidle`
|
||||
|
||||
### Clipboard
|
||||
|
||||
```bash
|
||||
agent-browser clipboard read # Read text from clipboard
|
||||
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||
agent-browser clipboard copy # Copy current selection (Ctrl+C)
|
||||
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
|
||||
```
|
||||
|
||||
### Mouse Control
|
||||
|
||||
```bash
|
||||
@@ -532,6 +547,9 @@ This is useful for multimodal AI models that can reason about visual layout, unl
|
||||
| `--json` | JSON output (for agents) |
|
||||
| `--full, -f` | Full page screenshot |
|
||||
| `--annotate` | Annotated screenshot with numbered element labels (or `AGENT_BROWSER_ANNOTATE` env) |
|
||||
| `--screenshot-dir <path>` | Default screenshot output directory (or `AGENT_BROWSER_SCREENSHOT_DIR` env) |
|
||||
| `--screenshot-quality <n>` | JPEG quality 0-100 (or `AGENT_BROWSER_SCREENSHOT_QUALITY` env) |
|
||||
| `--screenshot-format <fmt>` | Screenshot format: `png`, `jpeg` (or `AGENT_BROWSER_SCREENSHOT_FORMAT` env) |
|
||||
| `--headed` | Show browser window (not headless) (or `AGENT_BROWSER_HEADED` env) |
|
||||
| `--cdp <port\|url>` | Connect via Chrome DevTools Protocol (port or WebSocket URL) |
|
||||
| `--auto-connect` | Auto-discover and connect to running Chrome (or `AGENT_BROWSER_AUTO_CONNECT` env) |
|
||||
|
||||
+126
-9
@@ -391,7 +391,7 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
return Ok(json!({ "id": id, "action": "waitforfunction", "expression": expr }));
|
||||
}
|
||||
|
||||
// Check for --text flag: wait --text "Welcome"
|
||||
// Check for --text flag: wait --text "Welcome" [--timeout ms]
|
||||
if let Some(idx) = rest.iter().position(|&s| s == "--text" || s == "-t") {
|
||||
let text = rest
|
||||
.get(idx + 1)
|
||||
@@ -399,10 +399,13 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
context: "wait --text".to_string(),
|
||||
usage: "wait --text <text>",
|
||||
})?;
|
||||
// Use getByText locator to wait for text to appear
|
||||
return Ok(
|
||||
json!({ "id": id, "action": "wait", "selector": format!("text={}", text) }),
|
||||
);
|
||||
let mut cmd = json!({ "id": id, "action": "wait", "text": text });
|
||||
if let Some(t_idx) = rest.iter().position(|&s| s == "--timeout") {
|
||||
if let Some(Ok(ms)) = rest.get(t_idx + 1).map(|s| s.parse::<u64>()) {
|
||||
cmd["timeout"] = json!(ms);
|
||||
}
|
||||
}
|
||||
return Ok(cmd);
|
||||
}
|
||||
|
||||
// Check for --download flag: wait --download [path] [--timeout ms]
|
||||
@@ -474,9 +477,27 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
Ok(
|
||||
json!({ "id": id, "action": "screenshot", "path": path, "selector": selector, "fullPage": flags.full, "annotate": flags.annotate }),
|
||||
)
|
||||
let mut cmd = json!({
|
||||
"id": id, "action": "screenshot",
|
||||
"path": path, "selector": selector,
|
||||
"fullPage": flags.full, "annotate": flags.annotate
|
||||
});
|
||||
if let Some(ref fmt) = flags.screenshot_format {
|
||||
cmd["format"] = json!(fmt);
|
||||
}
|
||||
if let Some(q) = flags.screenshot_quality {
|
||||
cmd["quality"] = json!(q);
|
||||
if flags.screenshot_format.as_deref() != Some("jpeg") {
|
||||
eprintln!(
|
||||
"{} --screenshot-quality is ignored for PNG; use --screenshot-format jpeg",
|
||||
color::warning_indicator()
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(ref dir) = flags.screenshot_dir {
|
||||
cmd["screenshotDir"] = json!(dir);
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
"pdf" => {
|
||||
let path = rest.first().ok_or_else(|| ParseError::MissingArguments {
|
||||
@@ -1109,6 +1130,31 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result<Value, ParseError
|
||||
Ok(json!({ "id": id, "action": "highlight", "selector": sel }))
|
||||
}
|
||||
|
||||
// === Clipboard ===
|
||||
"clipboard" => match rest.first().copied() {
|
||||
Some("read") | None => {
|
||||
Ok(json!({ "id": id, "action": "clipboard", "operation": "read" }))
|
||||
}
|
||||
Some("write") => {
|
||||
rest.get(1).ok_or_else(|| ParseError::MissingArguments {
|
||||
context: "clipboard write".to_string(),
|
||||
usage: "clipboard write <text>",
|
||||
})?;
|
||||
let text = rest[1..].join(" ");
|
||||
Ok(
|
||||
json!({ "id": id, "action": "clipboard", "operation": "write", "text": text }),
|
||||
)
|
||||
}
|
||||
Some("copy") => Ok(json!({ "id": id, "action": "clipboard", "operation": "copy" })),
|
||||
Some("paste") => {
|
||||
Ok(json!({ "id": id, "action": "clipboard", "operation": "paste" }))
|
||||
}
|
||||
Some(sub) => Err(ParseError::UnknownSubcommand {
|
||||
subcommand: sub.to_string(),
|
||||
valid_options: &["read", "write", "copy", "paste"],
|
||||
}),
|
||||
},
|
||||
|
||||
// === State ===
|
||||
"state" => {
|
||||
const VALID: &[&str] = &["save", "load", "list", "clear", "show", "clean", "rename"];
|
||||
@@ -2133,6 +2179,9 @@ mod tests {
|
||||
confirm_interactive: false,
|
||||
native: false,
|
||||
engine: None,
|
||||
screenshot_dir: None,
|
||||
screenshot_quality: None,
|
||||
screenshot_format: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2749,7 +2798,75 @@ mod tests {
|
||||
fn test_wait_text() {
|
||||
let cmd = parse_command(&args("wait --text Welcome"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "wait");
|
||||
assert_eq!(cmd["selector"], "text=Welcome");
|
||||
assert_eq!(cmd["text"], "Welcome");
|
||||
assert!(cmd.get("timeout").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wait_text_with_timeout() {
|
||||
let cmd =
|
||||
parse_command(&args("wait --text Welcome --timeout 5000"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "wait");
|
||||
assert_eq!(cmd["text"], "Welcome");
|
||||
assert_eq!(cmd["timeout"], 5000);
|
||||
}
|
||||
|
||||
// === Clipboard Tests ===
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_read_default() {
|
||||
let cmd = parse_command(&args("clipboard"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_read_explicit() {
|
||||
let cmd = parse_command(&args("clipboard read"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write() {
|
||||
let cmd = parse_command(&args("clipboard write hello"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "write");
|
||||
assert_eq!(cmd["text"], "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write_multi_word() {
|
||||
let cmd = parse_command(&args("clipboard write hello world"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "write");
|
||||
assert_eq!(cmd["text"], "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_copy() {
|
||||
let cmd = parse_command(&args("clipboard copy"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "copy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_paste() {
|
||||
let cmd = parse_command(&args("clipboard paste"), &default_flags()).unwrap();
|
||||
assert_eq!(cmd["action"], "clipboard");
|
||||
assert_eq!(cmd["operation"], "paste");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_write_missing_text() {
|
||||
let result = parse_command(&args("clipboard write"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clipboard_unknown_subcommand() {
|
||||
let result = parse_command(&args("clipboard clear"), &default_flags());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// === Unknown command ===
|
||||
|
||||
@@ -43,6 +43,9 @@ pub struct Config {
|
||||
pub confirm_interactive: Option<bool>,
|
||||
pub native: Option<bool>,
|
||||
pub engine: Option<String>,
|
||||
pub screenshot_dir: Option<String>,
|
||||
pub screenshot_quality: Option<u32>,
|
||||
pub screenshot_format: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -86,6 +89,9 @@ impl Config {
|
||||
confirm_interactive: other.confirm_interactive.or(self.confirm_interactive),
|
||||
native: other.native.or(self.native),
|
||||
engine: other.engine.or(self.engine),
|
||||
screenshot_dir: other.screenshot_dir.or(self.screenshot_dir),
|
||||
screenshot_quality: other.screenshot_quality.or(self.screenshot_quality),
|
||||
screenshot_format: other.screenshot_format.or(self.screenshot_format),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,6 +167,9 @@ fn extract_config_path(args: &[String]) -> Option<Option<String>> {
|
||||
"--action-policy",
|
||||
"--confirm-actions",
|
||||
"--engine",
|
||||
"--screenshot-dir",
|
||||
"--screenshot-quality",
|
||||
"--screenshot-format",
|
||||
];
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
@@ -240,6 +249,9 @@ pub struct Flags {
|
||||
pub confirm_interactive: bool,
|
||||
pub native: bool,
|
||||
pub engine: Option<String>,
|
||||
pub screenshot_dir: Option<String>,
|
||||
pub screenshot_quality: Option<u32>,
|
||||
pub screenshot_format: Option<String>,
|
||||
|
||||
// Track which launch-time options were explicitly passed via CLI
|
||||
// (as opposed to being set only via environment variables)
|
||||
@@ -347,6 +359,17 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
|| config.confirm_interactive.unwrap_or(false),
|
||||
native: env_var_is_truthy("AGENT_BROWSER_NATIVE") || config.native.unwrap_or(false),
|
||||
engine: env::var("AGENT_BROWSER_ENGINE").ok().or(config.engine),
|
||||
screenshot_dir: env::var("AGENT_BROWSER_SCREENSHOT_DIR")
|
||||
.ok()
|
||||
.or(config.screenshot_dir),
|
||||
screenshot_quality: env::var("AGENT_BROWSER_SCREENSHOT_QUALITY")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.or(config.screenshot_quality),
|
||||
screenshot_format: env::var("AGENT_BROWSER_SCREENSHOT_FORMAT")
|
||||
.ok()
|
||||
.or(config.screenshot_format)
|
||||
.filter(|s| s == "png" || s == "jpeg"),
|
||||
cli_executable_path: false,
|
||||
cli_extensions: false,
|
||||
cli_profile: false,
|
||||
@@ -586,6 +609,42 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--screenshot-dir" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
flags.screenshot_dir = Some(s.clone());
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--screenshot-quality" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
if let Ok(n) = s.parse::<u32>() {
|
||||
if n <= 100 {
|
||||
flags.screenshot_quality = Some(n);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} --screenshot-quality must be 0-100, got {}",
|
||||
color::warning_indicator(),
|
||||
n
|
||||
);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--screenshot-format" => {
|
||||
if let Some(s) = args.get(i + 1) {
|
||||
if s == "png" || s == "jpeg" {
|
||||
flags.screenshot_format = Some(s.clone());
|
||||
} else {
|
||||
eprintln!(
|
||||
"{} --screenshot-format must be png or jpeg, got '{}'",
|
||||
color::warning_indicator(),
|
||||
s
|
||||
);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
"--config" => {
|
||||
// Already handled by load_config(); skip the value
|
||||
i += 1;
|
||||
@@ -640,6 +699,9 @@ pub fn clean_args(args: &[String]) -> Vec<String> {
|
||||
"--confirm-actions",
|
||||
"--config",
|
||||
"--engine",
|
||||
"--screenshot-dir",
|
||||
"--screenshot-quality",
|
||||
"--screenshot-format",
|
||||
];
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
@@ -1415,6 +1415,10 @@ async fn handle_screenshot(cmd: &Value, state: &mut DaemonState) -> Result<Value
|
||||
.and_then(|v| v.as_i64())
|
||||
.map(|q| q as i32),
|
||||
annotate,
|
||||
output_dir: cmd
|
||||
.get("screenshotDir")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
};
|
||||
|
||||
if annotate {
|
||||
@@ -1641,6 +1645,11 @@ async fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
let timeout_ms = cmd.get("timeout").and_then(|v| v.as_u64()).unwrap_or(30000);
|
||||
|
||||
if let Some(text) = cmd.get("text").and_then(|v| v.as_str()) {
|
||||
wait_for_text(&mgr.client, &session_id, text, timeout_ms).await?;
|
||||
return Ok(json!({ "waited": "text", "text": text }));
|
||||
}
|
||||
|
||||
if let Some(selector) = cmd.get("selector").and_then(|v| v.as_str()) {
|
||||
let state_str = cmd
|
||||
.get("state")
|
||||
@@ -1655,11 +1664,6 @@ async fn handle_wait(cmd: &Value, state: &mut DaemonState) -> Result<Value, Stri
|
||||
return Ok(json!({ "waited": "url", "url": url_pattern }));
|
||||
}
|
||||
|
||||
if let Some(text) = cmd.get("text").and_then(|v| v.as_str()) {
|
||||
wait_for_text(&mgr.client, &session_id, text, timeout_ms).await?;
|
||||
return Ok(json!({ "waited": "text", "text": text }));
|
||||
}
|
||||
|
||||
if let Some(fn_str) = cmd.get("function").and_then(|v| v.as_str()) {
|
||||
wait_for_function(&mgr.client, &session_id, fn_str, timeout_ms).await?;
|
||||
return Ok(json!({ "waited": "function" }));
|
||||
@@ -3107,8 +3111,13 @@ async fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result<Value, Str
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("read");
|
||||
|
||||
let session_id = mgr.active_session_id()?.to_string();
|
||||
|
||||
// cfg! is compile-time; assumes the browser runs on the same OS as the CLI binary.
|
||||
let modifier: i32 = if cfg!(target_os = "macos") { 4 } else { 2 };
|
||||
|
||||
match action {
|
||||
"write" | "copy" => {
|
||||
"write" => {
|
||||
let text = cmd
|
||||
.get("text")
|
||||
.or_else(|| cmd.get("value"))
|
||||
@@ -3119,7 +3128,17 @@ async fn handle_clipboard(cmd: &Value, state: &DaemonState) -> Result<Value, Str
|
||||
serde_json::to_string(text).unwrap_or_default()
|
||||
);
|
||||
mgr.evaluate(&js, None).await?;
|
||||
Ok(json!({ "copied": text }))
|
||||
Ok(json!({ "written": text }))
|
||||
}
|
||||
"copy" => {
|
||||
interaction::press_key_with_modifiers(&mgr.client, &session_id, "c", Some(modifier))
|
||||
.await?;
|
||||
Ok(json!({ "copied": true }))
|
||||
}
|
||||
"paste" => {
|
||||
interaction::press_key_with_modifiers(&mgr.client, &session_id, "v", Some(modifier))
|
||||
.await?;
|
||||
Ok(json!({ "pasted": true }))
|
||||
}
|
||||
_ => {
|
||||
let result = mgr.evaluate("navigator.clipboard.readText()", None).await?;
|
||||
@@ -4190,6 +4209,7 @@ async fn handle_diff_screenshot(cmd: &Value, state: &DaemonState) -> Result<Valu
|
||||
format: "png".to_string(),
|
||||
quality: None,
|
||||
annotate: false,
|
||||
output_dir: None,
|
||||
};
|
||||
|
||||
let result =
|
||||
|
||||
@@ -206,6 +206,22 @@ pub async fn type_text(
|
||||
}
|
||||
|
||||
pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Result<(), String> {
|
||||
press_key_with_modifiers(client, session_id, key, None).await
|
||||
}
|
||||
|
||||
/// Dispatch a keyDown+keyUp sequence for `key` with an optional CDP modifier bitmask.
|
||||
///
|
||||
/// Modifier values follow the CDP `Input.dispatchKeyEvent` spec:
|
||||
/// 1 = Alt, 2 = Control, 4 = Meta (Cmd), 8 = Shift.
|
||||
///
|
||||
/// Callers that need a platform-appropriate modifier (e.g. Cmd on macOS,
|
||||
/// Ctrl elsewhere) must choose the value themselves -- see `cfg!(target_os)`.
|
||||
pub async fn press_key_with_modifiers(
|
||||
client: &CdpClient,
|
||||
session_id: &str,
|
||||
key: &str,
|
||||
modifiers: Option<i32>,
|
||||
) -> Result<(), String> {
|
||||
let (key_name, code, key_code) = named_key_info(key);
|
||||
|
||||
client
|
||||
@@ -219,7 +235,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
|
||||
unmodified_text: None,
|
||||
windows_virtual_key_code: Some(key_code),
|
||||
native_virtual_key_code: Some(key_code),
|
||||
modifiers: None,
|
||||
modifiers,
|
||||
},
|
||||
Some(session_id),
|
||||
)
|
||||
@@ -236,7 +252,7 @@ pub async fn press_key(client: &CdpClient, session_id: &str, key: &str) -> Resul
|
||||
unmodified_text: None,
|
||||
windows_virtual_key_code: Some(key_code),
|
||||
native_virtual_key_code: Some(key_code),
|
||||
modifiers: None,
|
||||
modifiers,
|
||||
},
|
||||
Some(session_id),
|
||||
)
|
||||
|
||||
@@ -57,6 +57,7 @@ pub struct ScreenshotOptions {
|
||||
pub format: String,
|
||||
pub quality: Option<i32>,
|
||||
pub annotate: bool,
|
||||
pub output_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ScreenshotOptions {
|
||||
@@ -68,6 +69,7 @@ impl Default for ScreenshotOptions {
|
||||
format: "png".to_string(),
|
||||
quality: None,
|
||||
annotate: false,
|
||||
output_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +147,12 @@ pub async fn take_screenshot(
|
||||
} else {
|
||||
"png"
|
||||
};
|
||||
let path = save_screenshot(&base64, options.path.as_deref(), ext)?;
|
||||
let path = save_screenshot(
|
||||
&base64,
|
||||
options.path.as_deref(),
|
||||
ext,
|
||||
options.output_dir.as_deref(),
|
||||
)?;
|
||||
|
||||
Ok(ScreenshotResult {
|
||||
path,
|
||||
@@ -479,11 +486,15 @@ fn save_screenshot(
|
||||
base64_data: &str,
|
||||
explicit_path: Option<&str>,
|
||||
ext: &str,
|
||||
output_dir: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let save_path = match explicit_path {
|
||||
Some(path) => path.to_string(),
|
||||
None => {
|
||||
let dir = get_screenshot_dir();
|
||||
let dir = match output_dir {
|
||||
Some(d) => PathBuf::from(d),
|
||||
None => get_screenshot_dir(),
|
||||
};
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
|
||||
+51
-2
@@ -1313,12 +1313,18 @@ Modes:
|
||||
--url <pattern> Wait for URL to match pattern
|
||||
--load <state> Wait for load state (load, domcontentloaded, networkidle)
|
||||
--fn <expression> Wait for JavaScript expression to be truthy
|
||||
--text <text> Wait for text to appear on page
|
||||
--text <text> Wait for text to appear on page (substring match)
|
||||
--download [path] Wait for a download to complete (optionally save to path)
|
||||
|
||||
Download Options (with --download):
|
||||
--timeout <ms> Timeout in milliseconds for download to start
|
||||
|
||||
Wait for text to disappear:
|
||||
Use --fn or --state hidden to wait for text or elements to go away:
|
||||
wait --fn "!document.body.innerText.includes('Loading...')"
|
||||
wait "#spinner" --state hidden
|
||||
wait @e5 --state detached
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
@@ -1332,6 +1338,7 @@ Examples:
|
||||
agent-browser wait --text "Welcome back"
|
||||
agent-browser wait --download ./file.pdf
|
||||
agent-browser wait --download ./report.xlsx --timeout 30000
|
||||
agent-browser wait --fn "!document.body.innerText.includes('Loading...')"
|
||||
"##
|
||||
}
|
||||
|
||||
@@ -1340,7 +1347,7 @@ Examples:
|
||||
r##"
|
||||
agent-browser screenshot - Take a screenshot
|
||||
|
||||
Usage: agent-browser screenshot [path]
|
||||
Usage: agent-browser screenshot [selector] [path]
|
||||
|
||||
Captures a screenshot of the current page. If no path is provided,
|
||||
saves to a temporary directory with a generated filename.
|
||||
@@ -1353,6 +1360,12 @@ Options:
|
||||
With --json, annotations are included in the response.
|
||||
In native mode, this is currently supported on the
|
||||
CDP-backed browser path (Chromium/Lightpanda).
|
||||
--screenshot-dir <path> Default output directory for screenshots
|
||||
(or AGENT_BROWSER_SCREENSHOT_DIR env)
|
||||
--screenshot-quality <0-100> JPEG quality (0-100, only applies to jpeg format)
|
||||
(or AGENT_BROWSER_SCREENSHOT_QUALITY env)
|
||||
--screenshot-format <fmt> Image format: png (default) or jpeg
|
||||
(or AGENT_BROWSER_SCREENSHOT_FORMAT env)
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
@@ -1365,6 +1378,8 @@ Examples:
|
||||
agent-browser screenshot --annotate # Labeled screenshot + legend
|
||||
agent-browser screenshot --annotate ./page.png # Save annotated screenshot
|
||||
agent-browser screenshot --annotate --json # JSON output with annotations
|
||||
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||
"##
|
||||
}
|
||||
"pdf" => {
|
||||
@@ -2097,6 +2112,33 @@ Examples:
|
||||
"##
|
||||
}
|
||||
|
||||
// === Clipboard ===
|
||||
"clipboard" => {
|
||||
r##"
|
||||
agent-browser clipboard - Read and write clipboard
|
||||
|
||||
Usage: agent-browser clipboard <operation> [text]
|
||||
|
||||
Read from or write to the browser clipboard.
|
||||
|
||||
Operations:
|
||||
read Read text from clipboard
|
||||
write <text> Write text to clipboard
|
||||
copy Copy current selection (simulates Ctrl+C)
|
||||
paste Paste from clipboard (simulates Ctrl+V)
|
||||
|
||||
Global Options:
|
||||
--json Output as JSON
|
||||
--session <name> Use specific session
|
||||
|
||||
Examples:
|
||||
agent-browser clipboard read
|
||||
agent-browser clipboard write "Hello, World!"
|
||||
agent-browser clipboard copy
|
||||
agent-browser clipboard paste
|
||||
"##
|
||||
}
|
||||
|
||||
// === State ===
|
||||
"state" => {
|
||||
r##"
|
||||
@@ -2434,6 +2476,7 @@ Debug:
|
||||
errors [--clear] View page errors
|
||||
highlight <sel> Highlight element
|
||||
inspect Open Chrome DevTools for the active page
|
||||
clipboard <op> [text] Read/write clipboard (read, write, copy, paste)
|
||||
|
||||
Auth Vault:
|
||||
auth save <name> [opts] Save auth profile (--url, --username, --password/--password-stdin)
|
||||
@@ -2489,6 +2532,9 @@ Options:
|
||||
--json JSON output
|
||||
--full, -f Full page screenshot
|
||||
--annotate Annotated screenshot with numbered labels and legend
|
||||
--screenshot-dir <path> Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||
--screenshot-quality <n> JPEG quality 0-100; ignored for PNG (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||
--screenshot-format <fmt> Screenshot format: png, jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||
--headed Show browser window (not headless) (or AGENT_BROWSER_HEADED env)
|
||||
--cdp <port> Connect via CDP (Chrome DevTools Protocol)
|
||||
--color-scheme <scheme> Color scheme: dark, light, no-preference (or AGENT_BROWSER_COLOR_SCHEME)
|
||||
@@ -2558,6 +2604,9 @@ Environment:
|
||||
AGENT_BROWSER_CONFIRM_INTERACTIVE Enable interactive confirmation prompts
|
||||
AGENT_BROWSER_ENGINE Browser engine: chrome (default), lightpanda
|
||||
AGENT_BROWSER_NATIVE Use native Rust daemon (experimental, no Node.js/Playwright)
|
||||
AGENT_BROWSER_SCREENSHOT_DIR Default screenshot output directory
|
||||
AGENT_BROWSER_SCREENSHOT_QUALITY JPEG quality 0-100
|
||||
AGENT_BROWSER_SCREENSHOT_FORMAT Screenshot format: png, jpeg
|
||||
|
||||
Install (recommended, fastest - native Rust CLI):
|
||||
npm install -g agent-browser
|
||||
|
||||
@@ -28,6 +28,8 @@ agent-browser drag <src> <dst> # Drag and drop
|
||||
agent-browser upload <sel> <files> # Upload files
|
||||
agent-browser screenshot [path] # Screenshot (--full for full page)
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||
agent-browser pdf <path> # Save page as PDF
|
||||
agent-browser snapshot # Accessibility tree with refs
|
||||
agent-browser eval <js> # Run JavaScript
|
||||
@@ -96,11 +98,13 @@ agent-browser find nth 2 ".card" hover
|
||||
```bash
|
||||
agent-browser wait <selector> # Wait for element
|
||||
agent-browser wait <ms> # Wait for time
|
||||
agent-browser wait --text "Welcome" # Wait for text
|
||||
agent-browser wait --text "Welcome" # Wait for text (substring match)
|
||||
agent-browser wait --url "**/dash" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for load state
|
||||
agent-browser wait --fn "condition" # Wait for JS condition
|
||||
agent-browser wait --download [path] # Wait for download
|
||||
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
|
||||
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
|
||||
```
|
||||
|
||||
## Downloads
|
||||
@@ -121,6 +125,15 @@ agent-browser mouse up [button] # Release button
|
||||
agent-browser mouse wheel <dy> [dx] # Scroll wheel
|
||||
```
|
||||
|
||||
## Clipboard
|
||||
|
||||
```bash
|
||||
agent-browser clipboard read # Read text from clipboard
|
||||
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||
agent-browser clipboard copy # Copy current selection (Ctrl+C)
|
||||
agent-browser clipboard paste # Paste from clipboard (Ctrl+V)
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
```bash
|
||||
@@ -295,6 +308,9 @@ agent-browser reload # Reload page
|
||||
--json # JSON output (for scripts)
|
||||
--full, -f # Full page screenshot
|
||||
--annotate # Annotated screenshot with numbered element labels
|
||||
--screenshot-dir <path> # Default screenshot output directory (or AGENT_BROWSER_SCREENSHOT_DIR)
|
||||
--screenshot-quality <n> # JPEG quality 0-100 (or AGENT_BROWSER_SCREENSHOT_QUALITY)
|
||||
--screenshot-format <fmt> # Format: png (default), jpeg (or AGENT_BROWSER_SCREENSHOT_FORMAT)
|
||||
--headed # Show browser window (not headless)
|
||||
--cdp <port|url> # Connect via Chrome DevTools Protocol (port or WebSocket URL)
|
||||
--auto-connect # Auto-discover and connect to running Chrome
|
||||
|
||||
@@ -136,6 +136,9 @@ agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Welcome" # Wait for text to appear (substring match)
|
||||
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
|
||||
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
|
||||
|
||||
# Downloads
|
||||
agent-browser download @e1 ./file.pdf # Click element to trigger download
|
||||
@@ -151,8 +154,16 @@ agent-browser set device "iPhone 14" # Emulate device (viewport + user
|
||||
agent-browser screenshot # Screenshot to temp dir
|
||||
agent-browser screenshot --full # Full page screenshot
|
||||
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
|
||||
agent-browser screenshot --screenshot-dir ./shots # Save to custom directory
|
||||
agent-browser screenshot --screenshot-format jpeg --screenshot-quality 80
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
|
||||
# Clipboard
|
||||
agent-browser clipboard read # Read text from clipboard
|
||||
agent-browser clipboard write "Hello, World!" # Write text to clipboard
|
||||
agent-browser clipboard copy # Copy current selection
|
||||
agent-browser clipboard paste # Paste from clipboard
|
||||
|
||||
# Diff (compare page states)
|
||||
agent-browser diff snapshot # Compare current vs last snapshot
|
||||
agent-browser diff snapshot --baseline before.txt # Compare current vs saved file
|
||||
|
||||
+19
-6
@@ -741,7 +741,7 @@ async function handleScreenshot(
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
const filename = `screenshot-${timestamp}-${random}.${ext}`;
|
||||
const screenshotDir = path.join(getAppDir(), 'tmp', 'screenshots');
|
||||
const screenshotDir = command.screenshotDir ?? path.join(getAppDir(), 'tmp', 'screenshots');
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
savePath = path.join(screenshotDir, filename);
|
||||
}
|
||||
@@ -954,7 +954,13 @@ async function handleEvaluate(
|
||||
async function handleWait(command: WaitCommand, browser: BrowserManager): Promise<Response> {
|
||||
const page = browser.getPage();
|
||||
|
||||
if (command.selector) {
|
||||
if (command.text) {
|
||||
await page.waitForFunction(
|
||||
(t: string) => (document.body.innerText || '').includes(t),
|
||||
command.text,
|
||||
{ timeout: command.timeout }
|
||||
);
|
||||
} else if (command.selector) {
|
||||
await page.waitForSelector(command.selector, {
|
||||
state: command.state ?? 'visible',
|
||||
timeout: command.timeout,
|
||||
@@ -962,7 +968,6 @@ async function handleWait(command: WaitCommand, browser: BrowserManager): Promis
|
||||
} else if (command.timeout) {
|
||||
await page.waitForTimeout(command.timeout);
|
||||
} else {
|
||||
// Default: wait for load state
|
||||
await page.waitForLoadState('load');
|
||||
}
|
||||
|
||||
@@ -2119,14 +2124,22 @@ async function handleClipboard(
|
||||
|
||||
switch (command.operation) {
|
||||
case 'copy':
|
||||
await page.keyboard.press('Control+c');
|
||||
await page.keyboard.press('ControlOrMeta+c');
|
||||
return successResponse(command.id, { copied: true });
|
||||
case 'paste':
|
||||
await page.keyboard.press('Control+v');
|
||||
await page.keyboard.press('ControlOrMeta+v');
|
||||
return successResponse(command.id, { pasted: true });
|
||||
case 'read':
|
||||
case 'read': {
|
||||
const text = await page.evaluate('navigator.clipboard.readText()');
|
||||
return successResponse(command.id, { text });
|
||||
}
|
||||
case 'write': {
|
||||
if (!command.text) {
|
||||
return errorResponse(command.id, "Missing 'text' parameter for clipboard write");
|
||||
}
|
||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(command.text)})`);
|
||||
return successResponse(command.id, { written: command.text });
|
||||
}
|
||||
default:
|
||||
return errorResponse(command.id, 'Unknown clipboard operation');
|
||||
}
|
||||
|
||||
+3
-1
@@ -468,7 +468,7 @@ const tapSchema = baseCommandSchema.extend({
|
||||
|
||||
const clipboardSchema = baseCommandSchema.extend({
|
||||
action: z.literal('clipboard'),
|
||||
operation: z.enum(['copy', 'paste', 'read']),
|
||||
operation: z.enum(['copy', 'paste', 'read', 'write']),
|
||||
text: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -794,6 +794,7 @@ const screenshotSchema = baseCommandSchema.extend({
|
||||
format: z.enum(['png', 'jpeg']).optional(),
|
||||
quality: z.number().min(0).max(100).optional(),
|
||||
annotate: z.boolean().optional(),
|
||||
screenshotDir: z.string().optional(),
|
||||
});
|
||||
|
||||
const snapshotSchema = baseCommandSchema.extend({
|
||||
@@ -814,6 +815,7 @@ const evaluateSchema = baseCommandSchema.extend({
|
||||
const waitSchema = baseCommandSchema.extend({
|
||||
action: z.literal('wait'),
|
||||
selector: z.string().min(1).optional(),
|
||||
text: z.string().min(1).optional(),
|
||||
timeout: z.number().positive().optional(),
|
||||
state: z.enum(['attached', 'detached', 'visible', 'hidden']).optional(),
|
||||
});
|
||||
|
||||
+3
-1
@@ -707,7 +707,7 @@ export interface TapCommand extends BaseCommand {
|
||||
// Clipboard
|
||||
export interface ClipboardCommand extends BaseCommand {
|
||||
action: 'clipboard';
|
||||
operation: 'copy' | 'paste' | 'read';
|
||||
operation: 'copy' | 'paste' | 'read' | 'write';
|
||||
text?: string;
|
||||
}
|
||||
|
||||
@@ -826,6 +826,7 @@ export interface ScreenshotCommand extends BaseCommand {
|
||||
format?: 'png' | 'jpeg';
|
||||
quality?: number;
|
||||
annotate?: boolean;
|
||||
screenshotDir?: string;
|
||||
}
|
||||
|
||||
export interface SnapshotCommand extends BaseCommand {
|
||||
@@ -841,6 +842,7 @@ export interface EvaluateCommand extends BaseCommand {
|
||||
export interface WaitCommand extends BaseCommand {
|
||||
action: 'wait';
|
||||
selector?: string;
|
||||
text?: string;
|
||||
timeout?: number;
|
||||
state?: 'attached' | 'detached' | 'visible' | 'hidden';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user