use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
const TARGET: &str =
"text:Submit (or bare text), placeholder:Email, css:#id, id:login, role:button, or a target from view";
const ADDR: &str = "Address, decimal or 0x-prefixed hex";
const MEMORY: &str = "JavaScript expression for the WebAssembly.Memory [default: auto-detect]";
const TAB_ID: &str = "Tab ID from tab list";
const OPERATION_ID: &str = "Operation ID returned by the action";
const CODE: &str = "JavaScript source; omit to use --file";
#[derive(Parser)]
#[command(name = "eoka", about = "Browser automation CLI", version)]
pub struct Cli {
#[arg(long, global = true, help = "Expected action result as typed JSON")]
pub expect: Option<String>,
#[arg(long, global = true, help = "Stable UUID for a consequential action")]
pub operation_id: Option<String>,
#[arg(long, global = true, help = "Total action deadline in milliseconds")]
pub action_timeout_ms: Option<u64>,
#[arg(
long,
default_value = "default",
global = true,
help = "Session name for isolated browser instances"
)]
pub session: String,
#[arg(long, global = true, help = "JSON output mode")]
pub json: bool,
#[arg(
long,
global = true,
help = "JSON output plus structured metadata for agent callers"
)]
pub agent: bool,
#[arg(
long,
global = true,
help = "Create a headed browser or require an existing headed session"
)]
pub headed: bool,
#[arg(
long,
global = true,
conflicts_with = "headed",
help = "Create a headless browser or require an existing headless session"
)]
pub headless: bool,
#[arg(
long,
value_name = "PORT|URL",
global = true,
env = "EOKA_CDP",
help = "Connect to Chrome by DevTools port or WebSocket URL"
)]
pub cdp: Option<String>,
#[arg(
long,
global = true,
env = "EOKA_AUTO_CONNECT",
help = "Auto-discover a running Chrome on ports 9222-9229"
)]
pub auto_connect: bool,
#[arg(
long,
value_name = "PORT|URL",
global = true,
help = "Launch fresh Chrome and pre-load cookies/storage from a running Chrome"
)]
pub clone_state_from: Option<String>,
#[arg(
long,
value_name = "auto|PATH",
global = true,
env = "EOKA_FROM_PROFILE",
help = "Launch with a copy of an existing Chrome profile directory"
)]
pub from_profile: Option<String>,
#[arg(
long,
global = true,
env = "EOKA_NO_STEALTH",
help = "Disable stealth CDP filtering and evasion script injection"
)]
pub no_stealth: bool,
#[arg(
long,
global = true,
env = "EOKA_PERSIST",
help = "Keep a durable Chrome profile for this session across restarts"
)]
pub persist: bool,
#[arg(
long,
global = true,
env = "EOKA_NO_GEO_ALIGN",
help = "Disable IP-geolocation timezone/language alignment (on by default)"
)]
pub no_geo_align: bool,
#[arg(
long,
global = true,
env = "EOKA_NO_SANDBOX",
help = "Disable Chrome's sandbox (only for containers/CI that cannot run it)"
)]
pub no_sandbox: bool,
#[arg(
long,
global = true,
env = "EOKA_AUTO_SOLVE_CLOUDFLARE",
help = "Solve full-page Cloudflare challenges in every tab automatically"
)]
pub auto_solve_cloudflare: bool,
#[arg(
long,
global = true,
value_name = "URL",
env = "EOKA_PROXY",
conflicts_with = "proxy_file",
help = "Proxy URL for launched browser"
)]
pub proxy: Option<String>,
#[arg(long, hide = true, requires = "daemon")]
pub proxy_stdin: bool,
#[arg(
long,
global = true,
value_name = "FILE",
env = "EOKA_PROXY_FILE",
conflicts_with = "proxy",
help = "Read proxy URL from a file"
)]
pub proxy_file: Option<PathBuf>,
#[arg(
long,
global = true,
env = "EOKA_NO_JS",
help = "Start with JavaScript blocked by default"
)]
pub no_js: bool,
#[arg(
long,
global = true,
value_name = "DOMAIN",
help = "Domain to always run JavaScript on"
)]
pub js_allow: Vec<String>,
#[arg(
long,
global = true,
value_name = "DOMAIN",
help = "Domain to always block JavaScript on"
)]
pub js_block: Vec<String>,
#[arg(long, hide = true)]
pub daemon: bool,
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Subcommand)]
pub enum Command {
#[command(about = "Compact agent page state with scoped control references")]
View {
#[arg(long, help = "Continue from a cursor returned by a previous view")]
cursor: Option<String>,
#[arg(long, help = "Only controls with this role")]
role: Option<String>,
#[arg(long, help = "Only controls whose accessible name contains this text")]
name: Option<String>,
#[arg(
long,
default_value_t = 40,
help = "Maximum controls to return, 1 through 200"
)]
max_controls: usize,
#[arg(
long,
default_value_t = 3000,
help = "Observation deadline, 50 through 60000 ms"
)]
timeout_ms: u64,
},
#[command(
about = "Perform one agent action and return its outcome and fresh page state",
long_about = "Perform one agent action and return its outcome and fresh page state.\n\nREQUEST is JSON. Steps verify by default: give clicks and keys an explicit expected result. Exploratory input needs \"mode\": \"explore\" and exits 4 when unverified. Fill is sequential native input unless \"input_mode\": \"bulk\"."
)]
Step {
#[arg(help = "Step request as JSON")]
request: String,
},
#[command(about = "Submit a typed action with an operation ID")]
Action {
#[arg(help = "Typed action request as JSON")]
request: String,
},
#[command(about = "Inspect durable action records")]
Operations {
#[command(subcommand)]
command: OperationCommand,
},
#[command(
about = "Navigate to URL",
long_about = "Navigate to URL. file: URLs are blocked unless EOKA_ALLOW_FILE_URLS=1 was set when the daemon started."
)]
Open {
#[arg(help = "URL, or a path relative to the current page")]
url: String,
#[arg(long, help = "Extra HTTP headers as a JSON object")]
headers: Option<String>,
#[arg(long, help = "User-Agent header for this navigation")]
user_agent: Option<String>,
#[arg(long, help = "Disable the page's Content-Security-Policy")]
bypass_csp: bool,
#[arg(
long,
value_name = "FILE",
help = "JavaScript source, or a .js file, to run on each new document before page scripts"
)]
inject_js: Option<String>,
#[arg(
long,
value_name = "FILE",
help = "Restore a save-state file before navigating"
)]
load_state: Option<PathBuf>,
},
#[command(about = "Go back in history")]
Back,
#[command(about = "Go forward in history")]
Forward,
#[command(about = "Reload current page")]
Reload,
#[command(about = "Accessibility tree with @eN refs")]
Snapshot {
#[arg(short, long, help = "Only interactive nodes")]
interactive: bool,
#[arg(long, help = "Keep decorative and empty text nodes")]
all: bool,
},
#[command(about = "Inspect page context and interactive elements with indices")]
Observe {
#[arg(long, help = "Filter elements: inputs, buttons, or all")]
filter: Option<String>,
#[arg(long, help = "Maximum elements to return")]
max: Option<usize>,
#[arg(long, help = "Return structured element objects instead of text")]
structured: bool,
},
#[command(about = "Take screenshot")]
Screenshot {
#[arg(
short,
long,
help = "PNG path [default: eoka-screenshot-<ms>.png in the temp dir]"
)]
output: Option<PathBuf>,
#[arg(long, help = "Overlay numbered labels on interactive elements")]
annotate: bool,
},
#[command(about = "Handle CAPTCHAs locally or solve/inject provider tokens")]
Captcha {
#[command(subcommand)]
action: CaptchaAction,
},
#[command(about = "Emulate a device viewport")]
Emulate {
#[arg(long, default_value = "390", help = "Viewport width in CSS pixels")]
width: u32,
#[arg(long, default_value = "844", help = "Viewport height in CSS pixels")]
height: u32,
#[arg(long, default_value = "2", help = "Device pixel ratio")]
dpr: f64,
#[arg(long, help = "Emulate a desktop instead of a touch mobile device")]
desktop: bool,
#[arg(long, help = "Clear emulation and ignore the other flags")]
reset: bool,
},
#[command(about = "Get page URL and title")]
Info,
#[command(about = "Get all visible text on page")]
Text,
#[command(about = "Find elements by text substring")]
Find {
#[arg(help = "Text to search for")]
text: String,
},
#[command(
about = "Click element",
long_about = "Click element. Requires --expect with the result that proves the click worked; without it the command fails before any input."
)]
Click {
#[arg(help = TARGET)]
target: String,
},
#[command(name = "dblclick", about = "Double-click element")]
DoubleClick {
#[arg(help = TARGET)]
target: String,
},
#[command(about = "Clear and fill input")]
Fill {
#[arg(help = TARGET)]
target: String,
#[arg(help = "Text to enter")]
text: String,
#[arg(
long,
default_value = "sequential",
value_parser = ["sequential", "bulk"],
help = "sequential types native keystrokes; bulk sets the value at once"
)]
input_mode: String,
},
#[command(about = "Select dropdown option")]
Select {
#[arg(help = TARGET)]
target: String,
#[arg(help = "Option value or visible label")]
value: String,
},
#[command(about = "Hover over element")]
Hover {
#[arg(help = TARGET)]
target: String,
},
#[command(
about = "Press keyboard key",
long_about = "Press a key on the current focus, including a focused frame.\n\n`o` sends a native printable key; `Shift+o` sends `O`. Shift uses US-keyboard ASCII mappings; Ctrl/Alt/Meta shortcuts do not insert text. Requires --expect: choose an expectation that proves the intended effect, because read-only controls and prevented events can reject edits."
)]
Key {
#[arg(help = "Key name or chord such as Enter, a, Shift+o, Control+a")]
key: String,
},
#[command(about = "Press and hold a mouse button at viewport coordinates")]
MouseDown {
#[arg(help = "Viewport X in CSS pixels")]
x: f64,
#[arg(help = "Viewport Y in CSS pixels")]
y: f64,
#[arg(
long,
default_value = "left",
help = "left, middle, right, back or forward"
)]
button: String,
},
#[command(about = "Move the mouse to viewport coordinates")]
MouseMove {
#[arg(help = "Viewport X in CSS pixels")]
x: f64,
#[arg(help = "Viewport Y in CSS pixels")]
y: f64,
},
#[command(about = "Release a held mouse button at viewport coordinates")]
MouseUp {
#[arg(help = "Viewport X in CSS pixels")]
x: f64,
#[arg(help = "Viewport Y in CSS pixels")]
y: f64,
#[arg(
long,
default_value = "left",
help = "left, middle, right, back or forward"
)]
button: String,
},
#[command(about = "Press and hold a keyboard key")]
KeyDown {
#[arg(help = "Key name such as Shift, Control or a")]
key: String,
},
#[command(about = "Release a held keyboard key")]
KeyUp {
#[arg(help = "Key name such as Shift, Control or a")]
key: String,
},
#[command(about = "Release every held mouse button and key")]
ReleaseAllInputs,
#[command(about = "Scroll page or element into view")]
Scroll {
#[arg(help = TARGET)]
target: String,
},
#[command(
about = "Execute JavaScript and return result",
long_about = "Execute a JavaScript function and return its JSON result.\n\nCODE is a function, also when loaded with --file. It runs once in a fresh scope, promises are awaited and exceptions fail the command. The result must be JSON: undefined, cycles, non-finite numbers and non-JSON objects are rejected. Source, arguments and result are each bounded to 1 MiB. --raw instead evaluates source in global scope and returns display text, not typed JSON."
)]
Eval {
#[arg(
long,
default_value = "[]",
help = "JSON array of arguments passed to the function"
)]
args: String,
#[arg(long, help = "Treat CODE as an expression, not a function")]
raw: bool,
#[arg(help = CODE)]
code: Option<String>,
#[arg(short, long, help = "Read JavaScript from a file")]
file: Option<PathBuf>,
#[arg(long, help = "Run without returning a result")]
no_return: bool,
#[arg(
long,
help = "Fail when the JSON result exceeds this many bytes (at most 1 MiB)"
)]
max_size: Option<usize>,
#[arg(long, help = "Do not await a returned promise; requires --raw")]
no_await: bool,
},
#[command(about = "List frame IDs, URLs and names in the active page")]
Frames,
#[command(
about = "Execute JavaScript in an isolated frame world",
long_about = "Execute JavaScript in an isolated frame world, including cross-origin OOPIFs.\n\nThe code has DOM access but not the page's own JavaScript globals. Requires an existing session; never launches, restarts or replays. Frame IDs change on navigation: list them again with `frames`."
)]
FrameEval {
#[arg(
help = "CSS selector matching one frame, or id:<frame-id> from frames; no bare URL/index"
)]
frame: String,
#[arg(help = CODE)]
code: Option<String>,
#[arg(short, long, help = "Read JavaScript from a file")]
file: Option<PathBuf>,
},
#[command(about = "Execute JavaScript without returning a value")]
Exec {
#[arg(
long,
default_value = "[]",
help = "JSON array of arguments passed to the function"
)]
args: String,
#[arg(long, help = "Treat CODE as an expression, not a function")]
raw: bool,
#[arg(help = CODE)]
code: Option<String>,
#[arg(short, long, help = "Read JavaScript from a file")]
file: Option<PathBuf>,
#[arg(long, help = "Do not await a returned promise; requires --raw")]
no_await: bool,
},
#[command(about = "Run Tack TypeScript against the active eoka browser session")]
Tack {
#[arg(help = "Tack TypeScript source; omit to use --file")]
code: Option<String>,
#[arg(short, long, help = "Read Tack TypeScript from a file")]
file: Option<PathBuf>,
#[arg(long, help = "Execution timeout in milliseconds")]
timeout_ms: Option<u64>,
#[arg(long, help = "Print raw Tack execution JSON")]
raw_json: bool,
#[arg(long, help = "Expose all non-lifecycle eoka tools to Tack")]
all_tools: bool,
#[arg(long = "capability", help = "Expose opt-in tools for a capability")]
capabilities: Vec<String>,
},
#[command(about = "Inspect the generated eoka tool catalog")]
Tools {
#[command(subcommand)]
action: ToolsAction,
},
#[command(
about = "Download files through the existing browser session",
long_about = "Download files through the existing eoka-owned browser session; borrowed and remote browsers are rejected.\n\nStarting returns a job ID. One file runs per session. Start actions are never replayed. Closing the session cancels unfinished jobs; published files survive. History persists across daemon restarts under EOKA_STATE_DIR; partial transfers are not resumed and clicks are never replayed. Completion means a verified regular file was atomically published without replacing anything; on failure inspect published_path and staging_path. Relative --out paths resolve in the calling shell's directory."
)]
Download(crate::download_cmd::DownloadCli),
#[command(
about = "Fetch bounded text from browser context; use download for files",
long_about = "Fetch from inside the page with its cookies and fingerprint; use download for files.\n\nJSON output reports bytesRead, truncated and bodyOmitted. Known binary bodies are skipped."
)]
Fetch {
#[arg(help = "URL to fetch from the page's context")]
url: String,
#[arg(short, long, help = "HTTP method [default: GET]")]
method: Option<String>,
#[arg(long, help = "Request headers as a JSON object")]
headers: Option<String>,
#[arg(short, long, help = "Request body")]
body: Option<String>,
#[arg(long, help = "Fetch redirect mode: follow, error or manual")]
redirect: Option<String>,
#[arg(
long,
help = "Print only the body; fails on binary or truncated bodies"
)]
body_only: bool,
#[arg(
long,
help = "Raw text bytes to retain: default 8192, maximum 16777216; 0 means headers only"
)]
max_body: Option<usize>,
},
#[command(about = "Network recording, inspection, export, and interception")]
Network {
#[command(subcommand)]
action: NetworkAction,
},
#[command(about = "Get all cookies")]
Cookies,
#[command(about = "Set a cookie")]
SetCookie {
#[arg(help = "Cookie name")]
name: String,
#[arg(help = "Cookie value")]
value: String,
#[arg(long, help = "Cookie domain [default: current page]")]
domain: Option<String>,
#[arg(long, help = "Cookie path")]
path: Option<String>,
},
#[command(about = "Delete a cookie by name")]
DeleteCookie {
#[arg(help = "Cookie name")]
name: String,
#[arg(long, help = "Cookie domain [default: current page hostname]")]
domain: Option<String>,
},
#[command(about = "Clear all cookies")]
ClearCookies,
#[command(about = "Get localStorage")]
Storage {
#[arg(help = "Key to read; omit for every key")]
key: Option<String>,
#[arg(long, help = "Use sessionStorage instead of localStorage")]
session_storage: bool,
},
#[command(about = "Set storage value")]
SetStorage {
#[arg(help = "Storage key")]
key: String,
#[arg(help = "Value to store")]
value: String,
#[arg(long, help = "Use sessionStorage instead of localStorage")]
session_storage: bool,
},
#[command(about = "Dump localStorage and sessionStorage")]
DumpStorage,
#[command(
about = "Save cookies and storage to JSON",
long_about = "Save the current page's cookies and storage to JSON.\n\nThe path must end in .json. The file is written atomically, owner-only, and can only replace an empty file or an earlier state file."
)]
SaveState {
#[arg(help = "State file to write; must end in .json")]
path: PathBuf,
},
#[command(
about = "Load cookies and storage from JSON",
long_about = "Load cookies and storage from a save-state file.\n\nCookies are replaced only on the domains the file covers, including their parent domains and subdomains; every other site's cookies are kept. Storage is restored only on the saved origin, and skipped keys are reported."
)]
LoadState {
#[arg(help = "State file written by save-state")]
path: PathBuf,
#[arg(long, help = "Do not navigate to the saved URL first")]
no_navigate: bool,
},
#[command(about = "Set persistent extra HTTP headers")]
Headers {
#[arg(help = "Headers as a JSON object")]
headers_json: String,
},
#[command(about = "Read browser console output")]
Console {
#[arg(long, help = "Clear captured output after reading")]
clear: bool,
#[arg(long, help = "Only this level: log, warn, error, info or debug")]
level: Option<String>,
},
#[command(about = "Read JavaScript errors")]
Errors {
#[arg(long, help = "Clear captured errors after reading")]
clear: bool,
},
#[command(about = "Tab management")]
Tab {
#[command(subcommand)]
action: TabAction,
},
#[command(about = "Wait for time, text, URL, or load state")]
Wait {
#[arg(help = "Fixed delay in milliseconds")]
ms: Option<u64>,
#[arg(long, help = "Wait until this text is visible")]
text: Option<String>,
#[arg(long, help = "Wait until the URL contains this string")]
url: Option<String>,
#[arg(long, help = "Wait for network idle")]
load: Option<String>,
#[arg(short, long, help = "Timeout in milliseconds")]
timeout: Option<u64>,
},
#[command(about = "Execute multiple commands in sequence")]
Batch {
#[arg(help = "JSON array of {\"cmd\": ..., \"args\": {...}} objects")]
input: Option<String>,
#[arg(short, long, help = "Read the batch from a file")]
file: Option<PathBuf>,
#[arg(long, help = "Stop at the first failed command")]
bail: bool,
},
#[command(about = "Inject fake camera from a video file")]
FakeCamera {
#[arg(help = "Video file to play as the camera")]
file: PathBuf,
#[arg(long, help = "Loop the video instead of stopping at the end")]
loop_video: bool,
},
#[command(about = "WASM linear memory operations")]
Wasm {
#[command(subcommand)]
action: WasmAction,
},
#[command(about = "Manage the per-domain JavaScript policy")]
Js {
#[command(subcommand)]
action: JsAction,
},
#[command(about = "Detect SPA router type")]
SpaInfo,
#[command(about = "Navigate SPA without page reload")]
SpaNavigate {
#[arg(help = "Route path such as /dashboard")]
path: String,
},
#[command(about = "List all sessions")]
Sessions,
#[command(about = "Show daemon status")]
Status,
#[command(about = "Diagnose local eoka runtime state")]
Doctor,
#[command(about = "Force-kill daemon")]
Kill,
#[command(about = "Close browser and daemon")]
Close,
#[command(
name = "cdp-url",
about = "Print the discovered DevTools WebSocket URL"
)]
CdpUrl {
#[arg(long, help = "DevTools port [default: discover 9222-9229]")]
port: Option<u16>,
},
#[command(
name = "clone-from",
about = "Snapshot cookies and storage from a running Chrome"
)]
CloneFrom {
#[arg(help = "DevTools port or WebSocket URL of the running Chrome")]
source: String,
#[arg(long, help = "Write the snapshot to this state file")]
to: Option<PathBuf>,
},
}
#[derive(Subcommand)]
pub enum NetworkAction {
#[command(
about = "Start, stop or inspect network recording",
long_about = "Start, stop or inspect network recording.\n\nBodies are captured unredacted by default; treat logs and exports as sensitive."
)]
Record {
#[command(subcommand)]
action: NetworkRecordAction,
},
#[command(about = "List recorded requests")]
Log {
#[arg(long, help = "Maximum entries to return")]
limit: Option<usize>,
#[arg(long, help = "URL glob such as */api/*")]
pattern: Option<String>,
#[arg(long, help = "Only this HTTP method")]
method: Option<String>,
#[arg(long, help = "Only this response status")]
status: Option<u16>,
#[arg(long, help = "Only entries after this request ID")]
since: Option<u64>,
#[arg(long, help = "One line per request")]
compact: bool,
},
#[command(about = "Show one recorded request")]
Show {
#[arg(help = "Request ID from network log")]
id: u64,
#[arg(long, help = "Include the response body")]
body: bool,
#[arg(long, help = "Maximum body bytes to print")]
max_body: Option<usize>,
},
#[command(about = "Write recorded requests as a HAR file")]
Har {
#[arg(help = "HAR file to write")]
path: PathBuf,
#[arg(long, help = "Wait this long for in-flight requests before writing")]
settle_ms: Option<u64>,
},
#[command(about = "Export recorded requests as HAR or JSON")]
Export {
#[arg(help = "File to write")]
path: PathBuf,
#[arg(long, default_value = "har", help = "har or json")]
format: String,
#[arg(long, help = "Wait this long for in-flight requests before writing")]
settle_ms: Option<u64>,
},
#[command(about = "Wait for a matching request")]
Wait {
#[arg(long, help = "URL glob such as */api/*")]
pattern: Option<String>,
#[arg(long, help = "Only this HTTP method")]
method: Option<String>,
#[arg(long, help = "Only this response status")]
status: Option<u16>,
#[arg(long, help = "Timeout in milliseconds")]
timeout: Option<u64>,
#[arg(long, help = "Only match requests after this request ID")]
since: Option<u64>,
#[arg(long, help = "Also match requests recorded before the wait started")]
include_existing: bool,
},
#[command(about = "Discard recorded requests")]
Clear,
#[command(
about = "Capture or mock requests matching a URL pattern",
long_about = "Capture or mock requests matching a URL pattern.\n\nThe log keeps the latest 1000 entries. Response and capture files must be regular files of at most 8 MiB; FIFOs and devices are rejected, and each file operation has a 500 ms deadline. A failed capture, read or fulfillment invalidates DataDome clearance. Requests that may already have been dispatched are never replayed."
)]
Intercept {
#[command(subcommand)]
action: InterceptAction,
},
}
#[derive(Subcommand)]
pub enum ToolsAction {
#[command(about = "Print the tool manifest generated from eoka-protocol")]
Manifest {
#[arg(long, help = "Include tools hidden from the default agent exposure")]
all: bool,
#[arg(long, help = "Print raw {\"tools\": [...]} JSON")]
json: bool,
},
}
#[derive(Subcommand)]
pub enum NetworkRecordAction {
#[command(about = "Start recording")]
Start {
#[arg(long = "pattern", help = "URL glob to record; repeat for several")]
patterns: Vec<String>,
#[arg(long, help = "Record metadata only")]
no_bodies: bool,
#[arg(long, default_value = "10485760", help = "Per-response body cap")]
max_body_bytes: usize,
#[arg(long, help = "Discard earlier entries first")]
clear: bool,
},
#[command(about = "Stop recording")]
Stop,
#[command(about = "Show recording state")]
Status,
}
#[derive(Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum CaptchaAction {
#[command(
about = "Attempt local DataDome clearance on the current tab; keep the session open",
long_about = "Attempt local DataDome clearance on the current tab. No API key needed.\n\nRequires an existing session; foregrounds the tab and leaves it open. challenge_cleared is not proof of login; not_present is a no-op; every other outcome exits nonzero. An old daemon is not restarted automatically."
)]
Datadome {
#[arg(long, default_value_t = eoka_protocol::DATADOME_DEFAULT_ATTEMPTS, value_parser = clap::value_parser!(u32).range(1..=i64::from(eoka_protocol::DATADOME_MAX_ATTEMPTS)), help = "Clearance attempts, 1 through 4")]
max_attempts: u32,
#[arg(long, default_value_t = eoka_protocol::DATADOME_DEFAULT_TIMEOUT_MS, value_parser = clap::value_parser!(u64).range(eoka_protocol::DATADOME_MIN_TIMEOUT_MS..=eoka_protocol::DATADOME_MAX_TIMEOUT_MS), help = "Total budget, 5000 through 120000 ms")]
timeout_ms: u64,
},
#[command(
about = "Click the Cloudflare Turnstile checkbox on the current tab until the challenge clears",
long_about = "Click the Cloudflare Turnstile checkbox on the current tab until it leaves the challenge page.\n\nReports clicks and elapsed_ms; a normal page returns zero clicks. --auto-solve-cloudflare does this for every tab of a browser eoka launches."
)]
Turnstile {
#[arg(long, default_value_t = eoka_protocol::TURNSTILE_DEFAULT_TIMEOUT_MS, value_parser = clap::value_parser!(u64).range(1..=eoka_protocol::TURNSTILE_MAX_TIMEOUT_MS), help = "Total budget, at most 120000 ms")]
timeout_ms: u64,
},
#[command(about = "Solve a CAPTCHA through the Anti-Captcha provider")]
Solve(Box<SolveArgs>),
#[command(about = "Inject a solved token into the current page")]
Inject {
#[arg(help = "Token returned by captcha solve")]
token: String,
#[arg(long, default_value = "auto", help = "auto, recaptcha or hcaptcha")]
captcha_type: String,
#[arg(long, help = "Page callback to call with the token")]
callback: Option<String>,
#[arg(long, help = "Target to click after injecting")]
click_after: Option<String>,
},
}
#[derive(Args)]
pub struct SolveArgs {
#[arg(
long,
help = "recaptcha_v2, recaptcha_v2_enterprise, recaptcha_v3, hcaptcha or amazon_waf"
)]
pub captcha_type: String,
#[arg(long, help = "URL of the page showing the CAPTCHA")]
pub website_url: String,
#[arg(
long,
help = "Site key from the page (amazon_waf: key from window.gokuProps)"
)]
pub website_key: String,
#[arg(long, env = "ANTI_CAPTCHA_KEY", help = "Anti-Captcha API key")]
pub api_key: Option<String>,
#[arg(long, help = "recaptcha_v3 page action [default: submit]")]
pub page_action: Option<String>,
#[arg(long, help = "recaptcha_v3 minimum score [default: 0.3]")]
pub min_score: Option<f32>,
#[arg(
long,
help = "recaptcha_v2_enterprise extra render parameters as JSON, such as {\"s\": ...}"
)]
pub enterprise_payload: Option<String>,
#[arg(
long,
help = "recaptcha_v2_enterprise widget domain, such as www.google.com"
)]
pub api_domain: Option<String>,
#[arg(long, help = "amazon_waf iv from window.gokuProps (required)")]
pub iv: Option<String>,
#[arg(long, help = "amazon_waf context from window.gokuProps (required)")]
pub context: Option<String>,
#[arg(long, help = "amazon_waf captcha script URL")]
pub captcha_script: Option<String>,
#[arg(long, help = "amazon_waf challenge script URL")]
pub challenge_script: Option<String>,
#[arg(long, help = "Inject the token into the current page after solving")]
pub inject: bool,
#[arg(long, help = "Page callback to call with the injected token")]
pub inject_callback: Option<String>,
#[arg(long, help = "Target to click after injecting")]
pub click_after: Option<String>,
}
#[derive(Subcommand)]
pub enum WasmAction {
#[command(about = "Read LEN bytes at ADDR as hex")]
Read {
#[arg(help = ADDR)]
addr: String,
#[arg(help = "Number of bytes")]
len: usize,
#[arg(long, help = MEMORY)]
memory: Option<String>,
},
#[command(about = "Write hex bytes at ADDR")]
Write {
#[arg(help = ADDR)]
addr: String,
#[arg(help = "Bytes as hex, such as deadbeef")]
hex: String,
#[arg(long, help = MEMORY)]
memory: Option<String>,
},
#[command(about = "Search memory for a hex byte pattern")]
Find {
#[arg(help = "Hex bytes to find; spaces allowed, such as \"ff d8 ff e0\"")]
pattern: String,
#[arg(long, help = "Start address [default: 0]")]
start: Option<String>,
#[arg(long, help = "End address [default: end of memory]")]
end: Option<String>,
#[arg(long, default_value = "20", help = "Maximum matches")]
max: usize,
#[arg(long, help = MEMORY)]
memory: Option<String>,
},
#[command(about = "List WASM memories and their sizes")]
Info,
}
#[derive(Subcommand)]
pub enum InterceptAction {
#[command(about = "Add an interception rule")]
Add {
#[arg(help = "URL glob such as */api/data*")]
url_pattern: String,
#[arg(long, help = "Write matching requests to this file")]
capture: Option<PathBuf>,
#[arg(long, help = "Serve this file's exact bytes as the response")]
respond: Option<PathBuf>,
#[arg(long, default_value = "200", help = "Status code for --respond")]
status: u16,
},
#[command(about = "List interception rules")]
List,
#[command(about = "Remove an interception rule")]
Remove {
#[arg(help = "Rule ID from intercept list")]
id: String,
},
#[command(about = "Read the interception log (latest 1000 entries)")]
Log {
#[arg(long, help = "Clear the log after reading")]
clear: bool,
},
}
#[derive(Subcommand)]
pub enum JsAction {
#[command(about = "Set the default policy")]
Mode {
#[arg(help = "block-all or allow-all")]
mode: String,
},
#[command(about = "Always run JavaScript on a domain")]
Allow {
#[arg(help = "Domain such as example.com")]
domain: String,
},
#[command(about = "Always block JavaScript on a domain")]
Block {
#[arg(help = "Domain such as example.com")]
domain: String,
},
#[command(about = "Remove a domain rule")]
Remove {
#[arg(help = "Domain such as example.com")]
domain: String,
},
#[command(about = "List the policy")]
List,
}
#[derive(Subcommand)]
pub enum TabAction {
#[command(about = "List tabs")]
List,
#[command(about = "Open a tab, optionally at a URL")]
New {
#[arg(help = "URL to open [default: blank page]")]
url: Option<String>,
},
#[command(about = "Make a tab current")]
Switch {
#[arg(help = TAB_ID)]
tab_id: String,
},
#[command(about = "Close a tab")]
Close {
#[arg(help = TAB_ID)]
tab_id: String,
},
#[command(about = "Adopt a tab eoka did not open")]
Attach {
#[arg(help = TAB_ID)]
tab_id: String,
},
}
#[cfg(test)]
pub(crate) mod test_support {
use super::{Cli, Command};
use clap::Parser;
pub(crate) fn parsed_command(args: &[&str]) -> (Cli, Command) {
let mut cli = Cli::try_parse_from(args).unwrap();
let command = cli.command.take().unwrap();
(cli, command)
}
}
#[derive(Subcommand)]
pub enum OperationCommand {
#[command(about = "Show one operation record")]
Get {
#[arg(help = OPERATION_ID)]
id: String,
},
#[command(about = "List recent operation records")]
List {
#[arg(long, default_value_t = 100, help = "Maximum records to return")]
limit: u32,
},
#[command(about = "Wait for an operation to finish")]
Wait {
#[arg(help = OPERATION_ID)]
id: String,
#[arg(
long,
default_value_t = 30000,
help = "How long to wait; the operation keeps running after it"
)]
timeout_ms: u64,
},
#[command(about = "Cancel a running operation")]
Cancel {
#[arg(help = OPERATION_ID)]
id: String,
},
}