Skip to main content

browser_automation_cli/commands_prd/
meta.rs

1//! Machine-readable command list and JSON Schema fragments for agents.
2
3use serde_json::{json, Value};
4
5use crate::envelope::print_success_json;
6use crate::error::{CliError, ErrorKind};
7
8/// Full CLI command surface registered for agents (`commands --json`).
9pub const COMMANDS: &[&str] = &[
10    "doctor",
11    "commands",
12    "schema",
13    "version",
14    "goto",
15    "view",
16    "press",
17    "click-at",
18    "write",
19    "keys",
20    "type",
21    "wait",
22    "hover",
23    "drag",
24    "fill-form",
25    "upload",
26    "back",
27    "forward",
28    "reload",
29    "eval",
30    "grab",
31    "run",
32    "exec",
33    "extract",
34    "text",
35    "scroll",
36    "cookie",
37    "attr",
38    "assert",
39    "console",
40    "net",
41    "page",
42    "dialog",
43    "scrape",
44    "emulate",
45    "resize",
46    "perf",
47    "lighthouse",
48    "screencast",
49    "heap",
50    "extension",
51    "devtools3p",
52    "webmcp",
53    "completions",
54];
55
56/// Default-ON DevTools parity commands that MUST appear in `COMMANDS`.
57pub const PARITY_DEFAULT_ON_REQUIRED: &[&str] = &[
58    "goto",
59    "view",
60    "press",
61    "write",
62    "keys",
63    "type",
64    "wait",
65    "hover",
66    "drag",
67    "fill-form",
68    "upload",
69    "back",
70    "forward",
71    "reload",
72    "eval",
73    "grab",
74    "console",
75    "net",
76    "page",
77    "dialog",
78    "emulate",
79    "resize",
80    "perf",
81    "lighthouse",
82    "run",
83    "exec",
84    "doctor",
85    "commands",
86    "schema",
87    "version",
88    "completions",
89];
90
91/// Official DevTools tool-ref name → CLI subcommand (agent discovery).
92pub const DEVTOOLS_TOOL_MAP: &[(&str, &str)] = &[
93    ("click", "press"),
94    ("drag", "drag"),
95    ("fill", "write"),
96    ("fill_form", "fill-form"),
97    ("handle_dialog", "dialog"),
98    ("hover", "hover"),
99    ("press_key", "keys"),
100    ("type_text", "type"),
101    ("upload_file", "upload"),
102    ("click_at", "click-at"),
103    ("navigate_page", "goto|back|forward|reload"),
104    ("new_page", "page new"),
105    ("list_pages", "page list"),
106    ("select_page", "page select"),
107    ("close_page", "page close"),
108    ("wait_for", "wait"),
109    ("emulate", "emulate"),
110    ("resize_page", "resize"),
111    ("performance_start_trace", "perf start"),
112    ("performance_stop_trace", "perf stop"),
113    ("performance_analyze_insight", "perf insight"),
114    ("list_network_requests", "net list"),
115    ("get_network_request", "net get"),
116    ("evaluate_script", "eval"),
117    ("list_console_messages", "console list"),
118    ("get_console_message", "console get"),
119    ("take_screenshot", "grab"),
120    ("take_snapshot", "view"),
121    ("lighthouse_audit", "lighthouse"),
122    ("screencast_start", "screencast start"),
123    ("screencast_stop", "screencast stop"),
124    ("take_heapsnapshot", "heap take"),
125    ("close_heapsnapshot", "heap close"),
126    ("compare_heapsnapshots", "heap compare"),
127    ("get_heapsnapshot_summary", "heap summary"),
128    ("get_heapsnapshot_details", "heap details"),
129    ("get_heapsnapshot_class_nodes", "heap class-nodes"),
130    ("get_heapsnapshot_dominators", "heap dominators"),
131    ("get_heapsnapshot_duplicate_strings", "heap dup-strings"),
132    ("get_heapsnapshot_edges", "heap edges"),
133    ("get_heapsnapshot_retainers", "heap retainers"),
134    ("get_heapsnapshot_retaining_paths", "heap paths"),
135    ("get_heapsnapshot_object_details", "heap object-details"),
136    ("install_extension", "extension install"),
137    ("list_extensions", "extension list"),
138    ("reload_extension", "extension reload"),
139    ("trigger_extension_action", "extension trigger"),
140    ("uninstall_extension", "extension uninstall"),
141    ("list_3p_developer_tools", "devtools3p list"),
142    ("execute_3p_developer_tool", "devtools3p exec"),
143    ("list_webmcp_tools", "webmcp list"),
144    ("execute_webmcp_tool", "webmcp exec"),
145];
146
147pub fn list_commands(json: bool) -> Result<(), CliError> {
148    let map: Vec<Value> = DEVTOOLS_TOOL_MAP
149        .iter()
150        .map(|(tool, cli)| json!({ "tool": tool, "cli": cli }))
151        .collect();
152    let data = json!({
153        "commands": COMMANDS,
154        "schema_version": 1,
155        "parity_default_on": PARITY_DEFAULT_ON_REQUIRED,
156        "devtools_tool_map": map,
157        "binary": "browser-automation-cli",
158    });
159    if json {
160        print_success_json(data)?;
161    } else {
162        for c in COMMANDS {
163            println!("{c}");
164        }
165    }
166    Ok(())
167}
168
169fn schema_object(description: &str, properties: Value, required: &[&str]) -> Value {
170    json!({
171        "type": "object",
172        "description": description,
173        "properties": properties,
174        "required": required,
175        "additionalProperties": false,
176    })
177}
178
179fn schema_for(cmd: &str) -> Option<Value> {
180    let props = match cmd {
181        "doctor" => schema_object(
182            "Diagnose local Chrome install and one-shot readiness",
183            json!({
184                "offline": { "type": "boolean", "description": "Skip network probes" },
185                "quick": { "type": "boolean", "description": "Skip live launch test" },
186                "fix": { "type": "boolean", "description": "Apply safe repairs when possible" },
187                "json": { "type": "boolean" }
188            }),
189            &[],
190        ),
191        "commands" => schema_object(
192            "List available commands",
193            json!({ "json": { "type": "boolean" } }),
194            &[],
195        ),
196        "schema" => schema_object(
197            "JSON Schema fragment for one command",
198            json!({
199                "cmd": { "type": "string", "description": "Command name from `commands`" }
200            }),
201            &["cmd"],
202        ),
203        "version" => schema_object("Print CLI version (JSON when --json)", json!({}), &[]),
204        "goto" => schema_object(
205            "Navigate to URL and wait for load (one-shot)",
206            json!({
207                "url": { "type": "string", "description": "Absolute URL or about:blank" }
208            }),
209            &["url"],
210        ),
211        "view" => schema_object(
212            "Accessibility snapshot with @eN refs",
213            json!({
214                "verbose": { "type": "boolean" },
215                "path": { "type": "string", "description": "Optional file to write tree text" }
216            }),
217            &[],
218        ),
219        "press" => schema_object(
220            "Click element by CSS selector or @eN",
221            json!({
222                "target": { "type": "string" },
223                "dblclick": { "type": "boolean" },
224                "include_snapshot": { "type": "boolean" }
225            }),
226            &["target"],
227        ),
228        "click-at" => schema_object(
229            "Click at page CSS coordinates (requires --experimental-vision)",
230            json!({
231                "x": { "type": "number" },
232                "y": { "type": "number" },
233                "dblclick": { "type": "boolean" },
234                "include_snapshot": { "type": "boolean" }
235            }),
236            &["x", "y"],
237        ),
238        "write" => schema_object(
239            "Smart fill: text, select option, checkbox/radio true|false",
240            json!({
241                "target": { "type": "string" },
242                "value": { "type": "string" },
243                "include_snapshot": { "type": "boolean" }
244            }),
245            &["target", "value"],
246        ),
247        "keys" => schema_object(
248            "Press a CDP key name",
249            json!({ "key": { "type": "string" } }),
250            &["key"],
251        ),
252        "type" => schema_object(
253            "Type text into a target",
254            json!({
255                "target": { "type": "string" },
256                "text": { "type": "string" },
257                "clear": { "type": "boolean" },
258                "submit": { "type": "string", "description": "Optional key after type (e.g. Enter)" }
259            }),
260            &["target", "text"],
261        ),
262        "wait" => schema_object(
263            "Wait ms and/or text and/or CSS selector and/or load state",
264            json!({
265                "ms": { "type": "integer", "minimum": 0 },
266                "text": { "type": "string" },
267                "selector": { "type": "string" },
268                "state": {
269                    "type": "string",
270                    "enum": ["load", "domcontentloaded", "networkidle", "none"]
271                },
272                "include_snapshot": { "type": "boolean" }
273            }),
274            &[],
275        ),
276        "hover" => schema_object(
277            "Hover element by CSS selector or @eN",
278            json!({ "target": { "type": "string" } }),
279            &["target"],
280        ),
281        "drag" => schema_object(
282            "Drag from one target to another",
283            json!({
284                "from": { "type": "string" },
285                "to": { "type": "string" }
286            }),
287            &["from", "to"],
288        ),
289        "fill-form" => schema_object(
290            "Fill multiple fields from JSON array",
291            json!({
292                "json": {
293                    "type": "string",
294                    "description": "JSON array of {target,value} objects"
295                }
296            }),
297            &["json"],
298        ),
299        "upload" => schema_object(
300            "Upload a regular file to a file input",
301            json!({
302                "target": { "type": "string" },
303                "path": { "type": "string" }
304            }),
305            &["target", "path"],
306        ),
307        "back" => schema_object("History back", json!({}), &[]),
308        "forward" => schema_object("History forward", json!({}), &[]),
309        "reload" => schema_object(
310            "Reload page",
311            json!({ "ignore_cache": { "type": "boolean" } }),
312            &[],
313        ),
314        "eval" => schema_object(
315            "Evaluate JavaScript expression",
316            json!({ "expression": { "type": "string" } }),
317            &["expression"],
318        ),
319        "grab" => schema_object(
320            "Screenshot (png/jpeg/webp)",
321            json!({
322                "path": { "type": "string" },
323                "format": { "type": "string", "enum": ["png", "jpeg", "webp"] },
324                "full_page": { "type": "boolean" },
325                "quality": { "type": "integer" },
326                "element": { "type": "string", "description": "CSS selector or @eN" }
327            }),
328            &[],
329        ),
330        "run" => schema_object(
331            "Execute NDJSON multi-step script in one process",
332            json!({ "script": { "type": "string", "description": "Path to .jsonl script" } }),
333            &["script"],
334        ),
335        "exec" => schema_object(
336            "Single-step inline command (same surface as run steps)",
337            json!({
338                "args": {
339                    "type": "array",
340                    "items": { "type": "string" },
341                    "description": "e.g. [\"goto\", \"about:blank\"] or [\"wait\", \"--ms\", \"100\"]"
342                }
343            }),
344            &["args"],
345        ),
346        "extract" => schema_object(
347            "Extract text or attribute from target",
348            json!({
349                "target": { "type": "string" },
350                "attr": { "type": "string" }
351            }),
352            &["target"],
353        ),
354        "text" => schema_object(
355            "Extract visible text from target (PRD §7)",
356            json!({
357                "target": { "type": "string" }
358            }),
359            &["target"],
360        ),
361        "scroll" => schema_object(
362            "Scroll window or element by pixel deltas",
363            json!({
364                "target": { "type": "string", "description": "Optional CSS/@eN" },
365                "delta_x": { "type": "number" },
366                "delta_y": { "type": "number" }
367            }),
368            &[],
369        ),
370        "cookie" => schema_object(
371            "Cookie list/set/clear for the one-shot browser process",
372            json!({
373                "action": { "type": "string", "enum": ["list", "set", "clear"] },
374                "url": { "type": "string" },
375                "json": { "type": "string", "description": "JSON array for set" }
376            }),
377            &["action"],
378        ),
379        "attr" => schema_object(
380            "Read one attribute from target",
381            json!({
382                "target": { "type": "string" },
383                "name": { "type": "string" }
384            }),
385            &["target", "name"],
386        ),
387        "assert" => schema_object(
388            "Assertion helpers (url/text/console)",
389            json!({
390                "kind": { "type": "string", "enum": ["url", "text", "console"] }
391            }),
392            &["kind"],
393        ),
394        "console" => schema_object(
395            "List/get/clear/dump captured console messages (needs --capture-console)",
396            json!({
397                "action": { "type": "string", "enum": ["list", "get", "clear", "dump"] },
398                "id": { "type": "integer", "minimum": 0 },
399                "path": { "type": "string" }
400            }),
401            &["action"],
402        ),
403        "net" => schema_object(
404            "List or get captured network requests (needs --capture-network)",
405            json!({
406                "action": { "type": "string", "enum": ["list", "get"] },
407                "id": { "type": "integer", "minimum": 0 },
408                "request_path": { "type": "string" },
409                "response_path": { "type": "string" }
410            }),
411            &["action"],
412        ),
413        "page" => schema_object(
414            "Page info or multi-tab list|new|select|close",
415            json!({
416                "action": {
417                    "type": "string",
418                    "enum": ["info", "list", "new", "select", "close"]
419                },
420                "url": { "type": "string" },
421                "index": { "type": "integer", "minimum": 0 }
422            }),
423            &[],
424        ),
425        "dialog" => schema_object(
426            "Accept or dismiss open dialog",
427            json!({
428                "action": { "type": "string", "enum": ["accept", "dismiss"] },
429                "text": { "type": "string" }
430            }),
431            &["action"],
432        ),
433        "scrape" => schema_object(
434            "Navigate and return body text",
435            json!({ "url": { "type": "string" } }),
436            &["url"],
437        ),
438        "emulate" => schema_object(
439            "Emulate UA locale timezone network geo media CPU viewport headers",
440            json!({
441                "user_agent": { "type": "string" },
442                "locale": { "type": "string" },
443                "timezone": { "type": "string" },
444                "offline": { "type": "boolean" },
445                "latitude": { "type": "number" },
446                "longitude": { "type": "number" },
447                "media": { "type": "string" },
448                "network_conditions": { "type": "string" },
449                "cpu_throttling_rate": { "type": "number" },
450                "color_scheme": { "type": "string" },
451                "extra_headers": { "type": "string" },
452                "viewport": { "type": "string" }
453            }),
454            &[],
455        ),
456        "resize" => schema_object(
457            "Resize viewport",
458            json!({
459                "width": { "type": "integer" },
460                "height": { "type": "integer" },
461                "scale": { "type": "number" },
462                "mobile": { "type": "boolean" }
463            }),
464            &["width", "height"],
465        ),
466        "perf" => schema_object(
467            "Performance start|stop|insight",
468            json!({
469                "action": { "type": "string", "enum": ["start", "stop", "insight"] },
470                "path": { "type": "string" },
471                "reload": { "type": "boolean" },
472                "name": { "type": "string" }
473            }),
474            &["action"],
475        ),
476        "lighthouse" => schema_object(
477            "External lighthouse audit with JSON scores",
478            json!({
479                "url": { "type": "string" },
480                "out_dir": { "type": "string" },
481                "device": { "type": "string" },
482                "mode": { "type": "string" },
483                "lighthouse_path": { "type": "string" }
484            }),
485            &["url"],
486        ),
487        "screencast" => schema_object(
488            "Screencast start|stop (requires --experimental-screencast)",
489            json!({
490                "action": { "type": "string", "enum": ["start", "stop"] },
491                "path": { "type": "string" }
492            }),
493            &["action"],
494        ),
495        "heap" => schema_object(
496            "Heap snapshot tools (deep ops need --category-memory)",
497            json!({
498                "action": { "type": "string" },
499                "path": { "type": "string" },
500                "base": { "type": "string" },
501                "current": { "type": "string" },
502                "id": { "type": "integer" },
503                "node": { "type": "integer" }
504            }),
505            &["action"],
506        ),
507        "extension" => schema_object(
508            "Extension tools (requires --category-extensions)",
509            json!({
510                "action": { "type": "string" },
511                "path": { "type": "string" },
512                "id": { "type": "string" }
513            }),
514            &["action"],
515        ),
516        "devtools3p" => schema_object(
517            "Third-party tools surface (requires --category-third-party)",
518            json!({
519                "action": { "type": "string", "enum": ["list", "exec"] },
520                "name": { "type": "string" },
521                "params": { "type": "string" },
522                "url": { "type": "string" }
523            }),
524            &["action"],
525        ),
526        "webmcp" => schema_object(
527            "Web surface tools (requires --category-webmcp)",
528            json!({
529                "action": { "type": "string", "enum": ["list", "exec"] },
530                "name": { "type": "string" },
531                "input": { "type": "string" },
532                "url": { "type": "string" }
533            }),
534            &["action"],
535        ),
536        "completions" => schema_object(
537            "Generate shell completions (no Chrome)",
538            json!({
539                "shell": {
540                    "type": "string",
541                    "enum": ["bash", "zsh", "fish", "elvish", "powershell"]
542                }
543            }),
544            &["shell"],
545        ),
546        _ => return None,
547    };
548    Some(props)
549}
550
551pub fn schema_for_cmd(cmd: &str, json: bool) -> Result<(), CliError> {
552    if !COMMANDS.contains(&cmd) {
553        return Err(CliError::with_suggestion(
554            ErrorKind::Usage,
555            format!("unknown command for schema: {cmd}"),
556            "use `browser-automation-cli commands --json` to list commands",
557        ));
558    }
559    let fragment = schema_for(cmd)
560        .unwrap_or_else(|| schema_object(&format!("Schema fragment for `{cmd}`"), json!({}), &[]));
561    let data = json!({
562        "command": cmd,
563        "schema_version": 1,
564        "schema": fragment,
565        "type": fragment.get("type").cloned().unwrap_or(json!("object")),
566        "description": fragment.get("description").cloned().unwrap_or(json!("")),
567        "properties": fragment.get("properties").cloned().unwrap_or(json!({})),
568        "required": fragment.get("required").cloned().unwrap_or(json!([])),
569    });
570    if json {
571        print_success_json(data)?;
572    } else {
573        println!(
574            "{}",
575            serde_json::to_string_pretty(&data).unwrap_or_default()
576        );
577    }
578    Ok(())
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584
585    #[test]
586    fn parity_default_on_subset_of_commands() {
587        for req in PARITY_DEFAULT_ON_REQUIRED {
588            assert!(
589                COMMANDS.contains(req),
590                "parity command missing from COMMANDS: {req}"
591            );
592        }
593    }
594
595    #[test]
596    fn commands_unique() {
597        let mut seen = std::collections::BTreeSet::new();
598        for c in COMMANDS {
599            assert!(seen.insert(*c), "duplicate command: {c}");
600        }
601    }
602}