diff --git a/README.md b/README.md index 14044e5..a8c10e5 100644 --- a/README.md +++ b/README.md @@ -143,10 +143,10 @@ agent-browser open # Navigate to URL (aliases: goto, navigate agent-browser click # Click element (--new-tab to open in new tab) agent-browser dblclick # Double-click element agent-browser focus # Focus element -agent-browser type # Type into element +agent-browser type [--delay ] # Type into element agent-browser fill # Clear and fill agent-browser press # Press key (Enter, Tab, Control+a) (alias: key) -agent-browser keyboard type # Type with real keystrokes (no selector, current focus) +agent-browser keyboard type [--delay ] # Type with real keystrokes (no selector, current focus) agent-browser keyboard inserttext # Insert text without key events (no selector) agent-browser keydown # Hold key down agent-browser keyup # Release key diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 9913943..ceb91f5 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agent-browser-stealth" -version = "0.14.0-fork.2" +version = "0.14.0-fork.3" dependencies = [ "base64", "dirs", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 22dde91..0f90840 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-browser-stealth" -version = "0.14.0-fork.2" +version = "0.14.0-fork.3" edition = "2021" description = "Stealth browser automation CLI for AI agents with anti-bot evasions" license = "Apache-2.0" diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 53afaac..9527a12 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -71,6 +71,62 @@ pub fn gen_id() -> String { ) } +/// Parse free-form text arguments with optional `--delay `. +/// +/// `--` can be used to stop flag parsing if text must include `--delay` literally. +fn parse_text_with_optional_delay( + args: &[&str], + context: &str, + usage: &'static str, +) -> Result<(String, Option), ParseError> { + let mut text_parts: Vec<&str> = Vec::new(); + let mut delay_ms: Option = None; + let mut parse_flags = true; + let mut i = 0; + + while i < args.len() { + let arg = args[i]; + + if parse_flags && arg == "--" { + parse_flags = false; + i += 1; + continue; + } + + if parse_flags && arg == "--delay" { + let raw = args + .get(i + 1) + .ok_or_else(|| ParseError::MissingArguments { + context: format!("{} --delay", context), + usage, + })?; + let parsed = raw.parse::().map_err(|_| ParseError::InvalidValue { + message: format!( + "Invalid --delay value: {} (must be a non-negative integer in milliseconds)", + raw + ), + usage, + })?; + delay_ms = Some(parsed); + i += 2; + continue; + } + + text_parts.push(arg); + i += 1; + } + + let text = text_parts.join(" "); + if text.is_empty() { + return Err(ParseError::MissingArguments { + context: context.to_string(), + usage, + }); + } + + Ok((text, delay_ms)) +} + pub fn parse_command(args: &[String], flags: &Flags) -> Result { if args.is_empty() { return Err(ParseError::MissingArguments { @@ -165,9 +221,18 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { context: "type".to_string(), - usage: "type ", + usage: "type [--delay ]", })?; - Ok(json!({ "id": id, "action": "type", "selector": sel, "text": rest[1..].join(" ") })) + let (text, delay) = parse_text_with_optional_delay( + &rest[1..], + "type", + "type [--delay ]", + )?; + let mut cmd = json!({ "id": id, "action": "type", "selector": sel, "text": text }); + if let Some(ms) = delay { + cmd["delay"] = json!(ms); + } + Ok(cmd) } "hover" => { let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { @@ -272,14 +337,16 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { - let text: String = rest[1..].join(" "); - if text.is_empty() { - return Err(ParseError::MissingArguments { - context: "keyboard type".to_string(), - usage: "keyboard type ", - }); + let (text, delay) = parse_text_with_optional_delay( + &rest[1..], + "keyboard type", + "keyboard type [--delay ]", + )?; + let mut cmd = json!({ "id": id, "action": "keyboard", "subaction": "type", "text": text }); + if let Some(ms) = delay { + cmd["delay"] = json!(ms); } - Ok(json!({ "id": id, "action": "keyboard", "subaction": "type", "text": text })) + Ok(cmd) } "inserttext" | "insertText" => { let text: String = rest[1..].join(" "); @@ -2300,6 +2367,29 @@ mod tests { assert_eq!(cmd["text"], "some text"); } + #[test] + fn test_type_command_with_delay() { + let cmd = + parse_command(&args("type #input some text --delay 120"), &default_flags()).unwrap(); + assert_eq!(cmd["action"], "type"); + assert_eq!(cmd["selector"], "#input"); + assert_eq!(cmd["text"], "some text"); + assert_eq!(cmd["delay"], 120); + } + + #[test] + fn test_type_command_with_literal_delay_text() { + let cmd = parse_command( + &args("type #input -- --delay 120 should be typed"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "type"); + assert_eq!(cmd["selector"], "#input"); + assert_eq!(cmd["text"], "--delay 120 should be typed"); + assert!(cmd.get("delay").is_none()); + } + #[test] fn test_select() { let cmd = parse_command(&args("select #menu option1"), &default_flags()).unwrap(); @@ -2479,6 +2569,19 @@ mod tests { assert_eq!(cmd["selector"], "#element"); } + #[test] + fn test_keyboard_type_with_delay() { + let cmd = parse_command( + &args("keyboard type natural typing --delay 90"), + &default_flags(), + ) + .unwrap(); + assert_eq!(cmd["action"], "keyboard"); + assert_eq!(cmd["subaction"], "type"); + assert_eq!(cmd["text"], "natural typing"); + assert_eq!(cmd["delay"], 90); + } + #[test] fn test_wait_timeout() { let cmd = parse_command(&args("wait 5000"), &default_flags()).unwrap(); diff --git a/cli/src/output.rs b/cli/src/output.rs index a1810b4..0023f67 100644 --- a/cli/src/output.rs +++ b/cli/src/output.rs @@ -719,10 +719,11 @@ Examples: r##" agent-browser type - Type text into an element -Usage: agent-browser type +Usage: agent-browser type [--delay ] Types text into the specified element character by character. Unlike fill, this does not clear existing content first. +Use --delay to add per-character delay (milliseconds). Global Options: --json Output as JSON @@ -730,7 +731,9 @@ Global Options: Examples: agent-browser type "#search" "hello" + agent-browser type "#search" "iphone" --delay 120 agent-browser type @e2 "additional text" + agent-browser type @e2 -- "--delay 120 (literal text)" See Also: For typing into contenteditable editors (Lexical, ProseMirror, etc.) @@ -961,7 +964,7 @@ the current focus — essential for contenteditable editors like Lexical, ProseMirror, CodeMirror, and Monaco. Subcommands: - type Type text character-by-character with real + type [--delay ] Type text character-by-character with real key events (keydown, keypress, keyup per char) inserttext Insert text without key events (like paste) @@ -974,6 +977,7 @@ Global Options: Examples: agent-browser keyboard type "Hello, World!" + agent-browser keyboard type "human pacing" --delay 90 agent-browser keyboard type "# My Heading" agent-browser keyboard inserttext "pasted content" @@ -2016,10 +2020,10 @@ Core Commands: open Navigate to URL click Click element (or @ref) dblclick Double-click element - type Type into element + type [--delay ] Type into element fill Clear and fill press Press key (Enter, Tab, Control+a) - keyboard type Type text with real keystrokes (no selector) + keyboard type [--delay ] Type text with real keystrokes (no selector) keyboard inserttext Insert text without key events hover Hover element focus Focus element diff --git a/docs/src/app/commands/page.mdx b/docs/src/app/commands/page.mdx index b860b3f..1af885e 100644 --- a/docs/src/app/commands/page.mdx +++ b/docs/src/app/commands/page.mdx @@ -11,9 +11,9 @@ agent-browser open # Navigate (aliases: goto, navigate) agent-browser click # Click element (--new-tab to open in new tab) agent-browser dblclick # Double-click agent-browser fill # Clear and fill -agent-browser type # Type into element +agent-browser type [--delay ] # Type into element agent-browser press # Press key (Enter, Tab, Control+a) (alias: key) -agent-browser keyboard type # Type at current focus (no selector needed) +agent-browser keyboard type [--delay ] # Type at current focus (no selector needed) agent-browser keyboard inserttext # Insert text without key events agent-browser keydown # Hold key down agent-browser keyup # Release key diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md index d98d7f6..974d213 100644 --- a/skills/agent-browser/SKILL.md +++ b/skills/agent-browser/SKILL.md @@ -63,11 +63,11 @@ agent-browser snapshot -s "#selector" # Scope to CSS selector agent-browser click @e1 # Click element agent-browser click @e1 --new-tab # Click and open in new tab agent-browser fill @e2 "text" # Clear and type text -agent-browser type @e2 "text" # Type without clearing +agent-browser type @e2 "text" --delay 120 # Type without clearing (human-like pacing) agent-browser select @e1 "option" # Select dropdown option agent-browser check @e1 # Check checkbox agent-browser press Enter # Press key -agent-browser keyboard type "text" # Type at current focus (no selector) +agent-browser keyboard type "text" --delay 90 # Type at current focus (no selector) agent-browser keyboard inserttext "text" # Insert without key events agent-browser scroll down 500 # Scroll page