chrome-devtools-axi 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1,12 +1,548 @@
1
1
  import { encode } from "@toon-format/toon";
2
- import { CdpError, callTool, ensureBridge, stopBridge } from "./client.js";
3
- import { countRefs, extractTitle } from "./snapshot.js";
2
+ import { CdpError, callTool, ensureBridge, getSessionStatus, stopBridge } from "./client.js";
3
+ import { installHooks } from "./hooks.js";
4
+ import { countRefs, extractTitle, truncateSnapshot } from "./snapshot.js";
4
5
  import { getSuggestions } from "./suggestions.js";
5
6
  const HELP = `usage: chrome-devtools-axi <command> [args]
6
- commands[12]:
7
- open <url>, snapshot, click @<uid>, fill @<uid> <text>, type <text>,
8
- press <key>, scroll <dir>, back, wait <ms|text>, eval <js>, start, stop
7
+ commands[33]:
8
+ open <url>, snapshot, screenshot <path>, click @<uid>, fill @<uid> <text>,
9
+ type <text>, press <key>, scroll <dir>, back, wait <ms|text>, eval <js>,
10
+ hover @<uid>, drag @<from> @<to>, fillform @<uid>=<val>..., dialog <action>,
11
+ upload @<uid> <path>, pages, newpage <url>, selectpage <id>, closepage <id>,
12
+ resize <w> <h>, emulate, console, console-get <id>, network,
13
+ network-get [id], lighthouse, perf-start, perf-stop,
14
+ perf-insight <set> <name>, heap <path>, start, stop
9
15
  `;
16
+ const COMMAND_HELP = {
17
+ open: `usage: chrome-devtools-axi open <url> [--full]
18
+ Navigate to a URL and capture an accessibility snapshot.
19
+
20
+ args:
21
+ <url> URL to navigate to (required)
22
+
23
+ flags:
24
+ --full Show complete snapshot without truncation
25
+
26
+ examples:
27
+ chrome-devtools-axi open https://example.com
28
+ chrome-devtools-axi open https://example.com --full`,
29
+ screenshot: `usage: chrome-devtools-axi screenshot <path> [--uid @<uid>] [--full-page] [--format png|jpeg|webp]
30
+ Save a screenshot to a file.
31
+
32
+ args:
33
+ <path> File path to save the screenshot (required)
34
+
35
+ flags:
36
+ --uid @<uid> Capture a specific element instead of the full viewport
37
+ --full-page Capture the entire scrollable page
38
+ --format <fmt> Image format: png (default), jpeg, or webp
39
+
40
+ examples:
41
+ chrome-devtools-axi screenshot ./page.png
42
+ chrome-devtools-axi screenshot ./element.png --uid @3
43
+ chrome-devtools-axi screenshot ./full.png --full-page --format jpeg`,
44
+ snapshot: `usage: chrome-devtools-axi snapshot [--full]
45
+ Capture the current page accessibility snapshot.
46
+
47
+ flags:
48
+ --full Show complete snapshot without truncation
49
+
50
+ examples:
51
+ chrome-devtools-axi snapshot
52
+ chrome-devtools-axi snapshot --full`,
53
+ click: `usage: chrome-devtools-axi click @<uid> [--full]
54
+ Click an interactive element by its ref from the snapshot.
55
+
56
+ args:
57
+ @<uid> Element ref from snapshot (required)
58
+
59
+ flags:
60
+ --full Show complete snapshot without truncation
61
+
62
+ examples:
63
+ chrome-devtools-axi click @1
64
+ chrome-devtools-axi click @12 --full`,
65
+ fill: `usage: chrome-devtools-axi fill @<uid> <text> [--full]
66
+ Fill a form field with text.
67
+
68
+ args:
69
+ @<uid> Element ref from snapshot (required)
70
+ <text> Text to fill (required)
71
+
72
+ flags:
73
+ --full Show complete snapshot without truncation
74
+
75
+ examples:
76
+ chrome-devtools-axi fill @3 "hello world"
77
+ chrome-devtools-axi fill @3 "search query" --full`,
78
+ type: `usage: chrome-devtools-axi type <text> [--full]
79
+ Type text at the currently focused element.
80
+
81
+ args:
82
+ <text> Text to type (required)
83
+
84
+ flags:
85
+ --full Show complete snapshot without truncation
86
+
87
+ examples:
88
+ chrome-devtools-axi type "hello"
89
+ chrome-devtools-axi type "search query" --full`,
90
+ press: `usage: chrome-devtools-axi press <key> [--full]
91
+ Press a keyboard key.
92
+
93
+ args:
94
+ <key> Key name, e.g. Enter, Tab, Escape, ArrowDown (required)
95
+
96
+ flags:
97
+ --full Show complete snapshot without truncation
98
+
99
+ examples:
100
+ chrome-devtools-axi press Enter
101
+ chrome-devtools-axi press Tab --full`,
102
+ scroll: `usage: chrome-devtools-axi scroll <direction> [--full]
103
+ Scroll the page in a direction.
104
+
105
+ args:
106
+ <direction> up, down, top, or bottom (default: down)
107
+
108
+ flags:
109
+ --full Show complete snapshot without truncation
110
+
111
+ examples:
112
+ chrome-devtools-axi scroll down
113
+ chrome-devtools-axi scroll top --full`,
114
+ back: `usage: chrome-devtools-axi back [--full]
115
+ Navigate back in browser history.
116
+
117
+ flags:
118
+ --full Show complete snapshot without truncation
119
+
120
+ examples:
121
+ chrome-devtools-axi back
122
+ chrome-devtools-axi back --full`,
123
+ wait: `usage: chrome-devtools-axi wait <ms|text>
124
+ Wait for a duration or for text to appear on the page.
125
+
126
+ args:
127
+ <ms> Milliseconds to wait (numeric)
128
+ <text> Text to wait for (string)
129
+
130
+ examples:
131
+ chrome-devtools-axi wait 2000
132
+ chrome-devtools-axi wait "Submit"`,
133
+ eval: `usage: chrome-devtools-axi eval <js>
134
+ Evaluate JavaScript in the page context.
135
+
136
+ args:
137
+ <js> JavaScript expression (required)
138
+
139
+ examples:
140
+ chrome-devtools-axi eval "document.title"
141
+ chrome-devtools-axi eval "document.querySelectorAll('a').length"`,
142
+ start: `usage: chrome-devtools-axi start
143
+ Start the bridge server (launches headless Chrome).
144
+
145
+ examples:
146
+ chrome-devtools-axi start`,
147
+ stop: `usage: chrome-devtools-axi stop
148
+ Stop the bridge server and close the browser.
149
+
150
+ examples:
151
+ chrome-devtools-axi stop`,
152
+ // Page management
153
+ pages: `usage: chrome-devtools-axi pages
154
+ List all open pages/tabs in the browser.
155
+
156
+ examples:
157
+ chrome-devtools-axi pages`,
158
+ newpage: `usage: chrome-devtools-axi newpage <url> [--background] [--full]
159
+ Open a new tab and navigate to a URL.
160
+
161
+ args:
162
+ <url> URL to open (required)
163
+
164
+ flags:
165
+ --background Open in background without bringing to front
166
+ --full Show complete snapshot without truncation
167
+
168
+ examples:
169
+ chrome-devtools-axi newpage https://example.com
170
+ chrome-devtools-axi newpage https://example.com --background`,
171
+ selectpage: `usage: chrome-devtools-axi selectpage <id> [--full]
172
+ Switch to a tab by page ID.
173
+
174
+ args:
175
+ <id> Page ID from the pages command (required)
176
+
177
+ flags:
178
+ --full Show complete snapshot without truncation
179
+
180
+ examples:
181
+ chrome-devtools-axi selectpage 1`,
182
+ closepage: `usage: chrome-devtools-axi closepage <id>
183
+ Close a tab by page ID. The last open page cannot be closed.
184
+
185
+ args:
186
+ <id> Page ID from the pages command (required)
187
+
188
+ examples:
189
+ chrome-devtools-axi closepage 2`,
190
+ resize: `usage: chrome-devtools-axi resize <width> <height>
191
+ Resize the browser viewport.
192
+
193
+ args:
194
+ <width> Width in pixels (required)
195
+ <height> Height in pixels (required)
196
+
197
+ examples:
198
+ chrome-devtools-axi resize 1280 720
199
+ chrome-devtools-axi resize 390 844`,
200
+ // Interaction
201
+ hover: `usage: chrome-devtools-axi hover @<uid> [--full]
202
+ Hover over an element to trigger hover states.
203
+
204
+ args:
205
+ @<uid> Element ref from snapshot (required)
206
+
207
+ flags:
208
+ --full Show complete snapshot without truncation
209
+
210
+ examples:
211
+ chrome-devtools-axi hover @5`,
212
+ drag: `usage: chrome-devtools-axi drag @<from> @<to> [--full]
213
+ Drag an element onto another element.
214
+
215
+ args:
216
+ @<from> Element to drag (required)
217
+ @<to> Element to drop onto (required)
218
+
219
+ flags:
220
+ --full Show complete snapshot without truncation
221
+
222
+ examples:
223
+ chrome-devtools-axi drag @3 @7`,
224
+ fillform: `usage: chrome-devtools-axi fillform @<uid>=<value>... [--full]
225
+ Fill multiple form fields at once.
226
+
227
+ args:
228
+ @<uid>=<value> One or more field entries (required)
229
+
230
+ flags:
231
+ --full Show complete snapshot without truncation
232
+
233
+ examples:
234
+ chrome-devtools-axi fillform @1="hello" @2="world"
235
+ chrome-devtools-axi fillform @3="user@email.com" @4="password123"`,
236
+ dialog: `usage: chrome-devtools-axi dialog <accept|dismiss> [text]
237
+ Handle a browser dialog (alert, confirm, prompt).
238
+
239
+ args:
240
+ <action> accept or dismiss (required)
241
+ [text] Optional text to enter into a prompt dialog
242
+
243
+ examples:
244
+ chrome-devtools-axi dialog accept
245
+ chrome-devtools-axi dialog dismiss
246
+ chrome-devtools-axi dialog accept "confirmed"`,
247
+ upload: `usage: chrome-devtools-axi upload @<uid> <path> [--full]
248
+ Upload a file through a file input element.
249
+
250
+ args:
251
+ @<uid> File input element ref from snapshot (required)
252
+ <path> Local file path to upload (required)
253
+
254
+ flags:
255
+ --full Show complete snapshot without truncation
256
+
257
+ examples:
258
+ chrome-devtools-axi upload @5 ./photo.jpg`,
259
+ // Emulation
260
+ emulate: `usage: chrome-devtools-axi emulate [flags]
261
+ Emulate device features on the selected page.
262
+
263
+ flags:
264
+ --viewport <spec> Viewport like "390x844x3,mobile,touch"
265
+ --color-scheme <value> dark | light | auto
266
+ --network <condition> Offline | Slow 3G | Fast 3G | Slow 4G | Fast 4G
267
+ --cpu <rate> CPU throttling rate 1-20
268
+ --geolocation <lat>x<lon> Geolocation like "37.7749x-122.4194"
269
+ --user-agent <string> Custom user agent string
270
+
271
+ examples:
272
+ chrome-devtools-axi emulate --viewport "390x844x3,mobile" --color-scheme dark
273
+ chrome-devtools-axi emulate --network "Slow 3G" --cpu 4`,
274
+ // DevTools debugging
275
+ console: `usage: chrome-devtools-axi console [--type <type>] [--limit <n>] [--page <n>]
276
+ List console messages for the current page.
277
+
278
+ flags:
279
+ --type <type> Filter by message type (error, warn, log, etc.)
280
+ --limit <n> Maximum messages to return
281
+ --page <n> Page number (0-based)
282
+
283
+ examples:
284
+ chrome-devtools-axi console
285
+ chrome-devtools-axi console --type error --limit 50`,
286
+ "console-get": `usage: chrome-devtools-axi console-get <id>
287
+ Get a specific console message by ID.
288
+
289
+ args:
290
+ <id> Message ID from the console command (required)
291
+
292
+ examples:
293
+ chrome-devtools-axi console-get 3`,
294
+ network: `usage: chrome-devtools-axi network [--type <type>] [--limit <n>] [--page <n>]
295
+ List network requests for the current page.
296
+
297
+ flags:
298
+ --type <type> Filter by resource type (fetch, xhr, document, etc.)
299
+ --limit <n> Maximum requests to return
300
+ --page <n> Page number (0-based)
301
+
302
+ examples:
303
+ chrome-devtools-axi network
304
+ chrome-devtools-axi network --type fetch --limit 50`,
305
+ "network-get": `usage: chrome-devtools-axi network-get [id] [--response-file <path>] [--request-file <path>]
306
+ Get a specific network request. If id is omitted, gets the selected request.
307
+
308
+ args:
309
+ [id] Request ID from the network command (optional)
310
+
311
+ flags:
312
+ --response-file <path> Save response body to file
313
+ --request-file <path> Save request body to file
314
+
315
+ examples:
316
+ chrome-devtools-axi network-get 42
317
+ chrome-devtools-axi network-get 42 --response-file ./response.json`,
318
+ // Performance
319
+ lighthouse: `usage: chrome-devtools-axi lighthouse [--device <device>] [--mode <mode>] [--output-dir <path>]
320
+ Run a Lighthouse audit for accessibility, SEO, and best practices.
321
+
322
+ flags:
323
+ --device <device> desktop (default) or mobile
324
+ --mode <mode> navigation (default) or snapshot
325
+ --output-dir <path> Directory for reports
326
+
327
+ examples:
328
+ chrome-devtools-axi lighthouse
329
+ chrome-devtools-axi lighthouse --device mobile --output-dir ./reports`,
330
+ "perf-start": `usage: chrome-devtools-axi perf-start [--no-reload] [--no-auto-stop] [--file <path>]
331
+ Start a performance trace recording.
332
+
333
+ flags:
334
+ --no-reload Don't reload the page when starting
335
+ --no-auto-stop Don't automatically stop the trace
336
+ --file <path> Save raw trace data to file
337
+
338
+ examples:
339
+ chrome-devtools-axi perf-start
340
+ chrome-devtools-axi perf-start --no-reload --file trace.json.gz`,
341
+ "perf-stop": `usage: chrome-devtools-axi perf-stop [--file <path>]
342
+ Stop the active performance trace recording.
343
+
344
+ flags:
345
+ --file <path> Save raw trace data to file
346
+
347
+ examples:
348
+ chrome-devtools-axi perf-stop
349
+ chrome-devtools-axi perf-stop --file trace.json.gz`,
350
+ "perf-insight": `usage: chrome-devtools-axi perf-insight <set-id> <insight-name>
351
+ Analyze a specific performance insight from a trace.
352
+
353
+ args:
354
+ <set-id> Insight set ID from trace results (required)
355
+ <insight-name> Insight name, e.g. "DocumentLatency" (required)
356
+
357
+ examples:
358
+ chrome-devtools-axi perf-insight set1 DocumentLatency
359
+ chrome-devtools-axi perf-insight set1 LCPBreakdown`,
360
+ heap: `usage: chrome-devtools-axi heap <path>
361
+ Capture a heap snapshot for memory leak debugging.
362
+
363
+ args:
364
+ <path> File path to save the .heapsnapshot file (required)
365
+
366
+ examples:
367
+ chrome-devtools-axi heap ./snapshot.heapsnapshot`,
368
+ };
369
+ export function getCommandHelp(command) {
370
+ return COMMAND_HELP[command] ?? null;
371
+ }
372
+ export function parseScreenshotArgs(args) {
373
+ let filePath = null;
374
+ let uid;
375
+ let fullPage = false;
376
+ let format;
377
+ for (let i = 0; i < args.length; i++) {
378
+ const a = args[i];
379
+ if (a === "--uid" && i + 1 < args.length) {
380
+ const raw = args[++i];
381
+ uid = raw.startsWith("@") ? raw.slice(1) : raw;
382
+ }
383
+ else if (a === "--full-page") {
384
+ fullPage = true;
385
+ }
386
+ else if (a === "--format" && i + 1 < args.length) {
387
+ format = args[++i];
388
+ }
389
+ else if (!a.startsWith("--")) {
390
+ filePath = a;
391
+ }
392
+ }
393
+ return { filePath, uid, fullPage, format };
394
+ }
395
+ export function formatScreenshotOutput(filePath) {
396
+ return encode({ screenshot: filePath });
397
+ }
398
+ /** Parse MCP list_pages markdown into structured data. */
399
+ export function parsePagesList(text) {
400
+ const pages = [];
401
+ for (const line of text.split("\n")) {
402
+ const m = line.match(/^(\d+):\s+(\S+)(\s+\[selected\])?/);
403
+ if (m) {
404
+ pages.push({ id: parseInt(m[1], 10), url: m[2], selected: !!m[3] });
405
+ }
406
+ }
407
+ return pages;
408
+ }
409
+ /** Format raw MCP text result as AXI output: labeled block + truncation + suggestions. */
410
+ export function formatMcpResult(label, text, suggestions) {
411
+ const blocks = [];
412
+ const tr = truncateSnapshot(text, false, 2000);
413
+ blocks.push(`${label}:\n${tr.text.trimEnd()}`);
414
+ if (tr.truncated) {
415
+ blocks[0] += `\n ... (truncated, ${tr.totalLength} chars total)`;
416
+ }
417
+ if (suggestions.length > 0) {
418
+ blocks.push(renderHelp(suggestions));
419
+ }
420
+ return renderOutput(blocks);
421
+ }
422
+ export function parseFillFormArgs(args) {
423
+ const entries = [];
424
+ for (const arg of args) {
425
+ if (arg === "--full")
426
+ continue;
427
+ const match = arg.match(/^@([^=]+)=(.+)$/);
428
+ if (!match)
429
+ continue;
430
+ const uid = match[1];
431
+ let value = match[2];
432
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
433
+ value = value.slice(1, -1);
434
+ }
435
+ entries.push({ uid, value });
436
+ }
437
+ return { entries };
438
+ }
439
+ export function parseEmulateArgs(args) {
440
+ const result = {};
441
+ let i = 0;
442
+ while (i < args.length) {
443
+ switch (args[i]) {
444
+ case "--viewport":
445
+ result.viewport = args[++i];
446
+ break;
447
+ case "--color-scheme":
448
+ result.colorScheme = args[++i];
449
+ break;
450
+ case "--network":
451
+ result.networkConditions = args[++i];
452
+ break;
453
+ case "--cpu":
454
+ result.cpuThrottlingRate = parseInt(args[++i], 10);
455
+ break;
456
+ case "--geolocation":
457
+ result.geolocation = args[++i];
458
+ break;
459
+ case "--user-agent":
460
+ result.userAgent = args[++i];
461
+ break;
462
+ }
463
+ i++;
464
+ }
465
+ return JSON.parse(JSON.stringify(result));
466
+ }
467
+ export function parseConsoleArgs(args) {
468
+ const result = {};
469
+ for (let i = 0; i < args.length; i++) {
470
+ if (args[i] === "--type" && i + 1 < args.length) {
471
+ result.types = [args[++i]];
472
+ }
473
+ else if (args[i] === "--limit" && i + 1 < args.length) {
474
+ result.pageSize = parseInt(args[++i], 10);
475
+ }
476
+ else if (args[i] === "--page" && i + 1 < args.length) {
477
+ result.pageIdx = parseInt(args[++i], 10);
478
+ }
479
+ }
480
+ return result;
481
+ }
482
+ export function parseNetworkArgs(args) {
483
+ const result = {};
484
+ for (let i = 0; i < args.length; i++) {
485
+ if (args[i] === "--type" && i + 1 < args.length) {
486
+ result.resourceTypes = [args[++i]];
487
+ }
488
+ else if (args[i] === "--limit" && i + 1 < args.length) {
489
+ result.pageSize = parseInt(args[++i], 10);
490
+ }
491
+ else if (args[i] === "--page" && i + 1 < args.length) {
492
+ result.pageIdx = parseInt(args[++i], 10);
493
+ }
494
+ }
495
+ return result;
496
+ }
497
+ export function parseNetworkGetArgs(args) {
498
+ const result = {};
499
+ for (let i = 0; i < args.length; i++) {
500
+ if (args[i] === "--response-file" && i + 1 < args.length) {
501
+ result.responseFilePath = args[++i];
502
+ }
503
+ else if (args[i] === "--request-file" && i + 1 < args.length) {
504
+ result.requestFilePath = args[++i];
505
+ }
506
+ else if (!args[i].startsWith("--")) {
507
+ result.reqid = parseInt(args[i], 10);
508
+ }
509
+ }
510
+ return result;
511
+ }
512
+ export function parseLighthouseArgs(args) {
513
+ const result = {};
514
+ for (let i = 0; i < args.length; i++) {
515
+ switch (args[i]) {
516
+ case "--device":
517
+ result.device = args[++i];
518
+ break;
519
+ case "--mode":
520
+ result.mode = args[++i];
521
+ break;
522
+ case "--output-dir":
523
+ result.outputDirPath = args[++i];
524
+ break;
525
+ }
526
+ }
527
+ return result;
528
+ }
529
+ export function parsePerfStartArgs(args) {
530
+ const result = {};
531
+ for (let i = 0; i < args.length; i++) {
532
+ switch (args[i]) {
533
+ case "--no-reload":
534
+ result.reload = false;
535
+ break;
536
+ case "--no-auto-stop":
537
+ result.autoStop = false;
538
+ break;
539
+ case "--file":
540
+ result.filePath = args[++i];
541
+ break;
542
+ }
543
+ }
544
+ return result;
545
+ }
10
546
  function renderHelp(lines) {
11
547
  if (lines.length === 0)
12
548
  return "";
@@ -40,7 +576,7 @@ function parseSnapshotFromResponse(response) {
40
576
  return nextHeading === -1 ? trimmed.trimEnd() : trimmed.slice(0, nextHeading).trimEnd();
41
577
  }
42
578
  /** Format page metadata (TOON) + raw snapshot + suggestions. */
43
- function formatPageOutput(snapshot, command, url) {
579
+ function formatPageOutput(snapshot, command, url, full = false) {
44
580
  const title = extractTitle(snapshot);
45
581
  const refs = countRefs(snapshot);
46
582
  const blocks = [];
@@ -53,17 +589,31 @@ function formatPageOutput(snapshot, command, url) {
53
589
  page.refs = refs;
54
590
  blocks.push(encode({ page }));
55
591
  // Raw snapshot (not TOON-encoded — already token-efficient tree format)
56
- blocks.push(`snapshot:\n${snapshot.trimEnd()}`);
592
+ const tr = truncateSnapshot(snapshot, full);
593
+ let snapshotBlock = `snapshot:\n${tr.text.trimEnd()}`;
594
+ if (tr.truncated) {
595
+ snapshotBlock += `\n ... (truncated, ${tr.totalLength} chars total)`;
596
+ }
597
+ blocks.push(snapshotBlock);
57
598
  // Contextual suggestions
58
599
  const suggestions = getSuggestions({ command, url, snapshot });
600
+ if (tr.truncated) {
601
+ suggestions.push(`Run \`chrome-devtools-axi ${command}${url ? " " + url : ""} --full\` to see complete snapshot`);
602
+ }
59
603
  if (suggestions.length > 0) {
60
604
  blocks.push(renderHelp(suggestions));
61
605
  }
62
606
  return renderOutput(blocks);
63
607
  }
64
- /** Strip the `## Latest page snapshot` header that chrome-devtools-mcp prepends. */
608
+ /** Strip everything before the actual accessibility tree (MCP may prepend status lines and headers). */
65
609
  function stripSnapshotHeader(text) {
66
- return text.replace(/^##\s+Latest page snapshot\s*\n/, "");
610
+ // Find the first line that looks like a tree node (uid= or RootWebArea)
611
+ const lines = text.split("\n");
612
+ const treeStart = lines.findIndex((l) => /\bRootWebArea\b|\buid=/.test(l));
613
+ if (treeStart > 0)
614
+ return lines.slice(treeStart).join("\n");
615
+ // Fallback: strip known headers
616
+ return text.replace(/^[\s\S]*?##\s+Latest page snapshot\s*\n/, "");
67
617
  }
68
618
  /** Strip leading @ from uid ref. */
69
619
  function parseUid(arg) {
@@ -87,7 +637,7 @@ const SCROLL_FUNCTIONS = {
87
637
  top: "window.scrollTo(0, 0)",
88
638
  bottom: "window.scrollTo(0, document.body.scrollHeight)",
89
639
  };
90
- async function handleOpen(args) {
640
+ async function handleOpen(args, full) {
91
641
  const url = args[0];
92
642
  if (!url) {
93
643
  throw new CdpError("Missing URL", "VALIDATION_ERROR", [
@@ -96,13 +646,30 @@ async function handleOpen(args) {
96
646
  }
97
647
  await callTool("navigate_page", { type: "url", url });
98
648
  const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
99
- return formatPageOutput(snapshot, "open", url);
649
+ return formatPageOutput(snapshot, "open", url, full);
100
650
  }
101
- async function handleSnapshot() {
651
+ async function handleSnapshot(full) {
102
652
  const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
103
- return formatPageOutput(snapshot, "snapshot");
653
+ return formatPageOutput(snapshot, "snapshot", undefined, full);
104
654
  }
105
- async function handleClick(args) {
655
+ async function handleScreenshot(args) {
656
+ const parsed = parseScreenshotArgs(args);
657
+ if (!parsed.filePath) {
658
+ throw new CdpError("Missing file path", "VALIDATION_ERROR", [
659
+ "Run `chrome-devtools-axi screenshot ./page.png` to save a screenshot",
660
+ ]);
661
+ }
662
+ const toolArgs = { filePath: parsed.filePath };
663
+ if (parsed.uid)
664
+ toolArgs.uid = parsed.uid;
665
+ if (parsed.fullPage)
666
+ toolArgs.fullPage = true;
667
+ if (parsed.format)
668
+ toolArgs.format = parsed.format;
669
+ await callTool("take_screenshot", toolArgs);
670
+ return formatScreenshotOutput(parsed.filePath);
671
+ }
672
+ async function handleClick(args, full) {
106
673
  const uid = args[0];
107
674
  if (!uid) {
108
675
  throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
@@ -110,9 +677,9 @@ async function handleClick(args) {
110
677
  ]);
111
678
  }
112
679
  const snapshot = await callWithSnapshot("click", { uid: parseUid(uid) });
113
- return formatPageOutput(snapshot, "click");
680
+ return formatPageOutput(snapshot, "click", undefined, full);
114
681
  }
115
- async function handleFill(args) {
682
+ async function handleFill(args, full) {
116
683
  const uid = args[0];
117
684
  const value = args.slice(1).join(" ");
118
685
  if (!uid) {
@@ -126,9 +693,9 @@ async function handleFill(args) {
126
693
  ]);
127
694
  }
128
695
  const snapshot = await callWithSnapshot("fill", { uid: parseUid(uid), value });
129
- return formatPageOutput(snapshot, "fill");
696
+ return formatPageOutput(snapshot, "fill", undefined, full);
130
697
  }
131
- async function handlePress(args) {
698
+ async function handlePress(args, full) {
132
699
  const key = args[0];
133
700
  if (!key) {
134
701
  throw new CdpError("Missing key name", "VALIDATION_ERROR", [
@@ -136,9 +703,9 @@ async function handlePress(args) {
136
703
  ]);
137
704
  }
138
705
  const snapshot = await callWithSnapshot("press_key", { key });
139
- return formatPageOutput(snapshot, "press");
706
+ return formatPageOutput(snapshot, "press", undefined, full);
140
707
  }
141
- async function handleType(args) {
708
+ async function handleType(args, full) {
142
709
  const text = args.join(" ");
143
710
  if (!text) {
144
711
  throw new CdpError("Missing text", "VALIDATION_ERROR", [
@@ -147,9 +714,9 @@ async function handleType(args) {
147
714
  }
148
715
  await callTool("type_text", { text });
149
716
  const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
150
- return formatPageOutput(snapshot, "type");
717
+ return formatPageOutput(snapshot, "type", undefined, full);
151
718
  }
152
- async function handleScroll(args) {
719
+ async function handleScroll(args, full) {
153
720
  const dir = (args[0] ?? "down").toLowerCase();
154
721
  const fn = SCROLL_FUNCTIONS[dir];
155
722
  if (!fn) {
@@ -159,12 +726,12 @@ async function handleScroll(args) {
159
726
  }
160
727
  await callTool("evaluate_script", { function: fn });
161
728
  const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
162
- return formatPageOutput(snapshot, "scroll");
729
+ return formatPageOutput(snapshot, "scroll", undefined, full);
163
730
  }
164
- async function handleBack() {
731
+ async function handleBack(full) {
165
732
  await callTool("navigate_page", { type: "back" });
166
733
  const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
167
- return formatPageOutput(snapshot, "back");
734
+ return formatPageOutput(snapshot, "back", undefined, full);
168
735
  }
169
736
  async function handleWait(args) {
170
737
  const target = args[0];
@@ -190,6 +757,22 @@ async function handleWait(args) {
190
757
  blocks.push(renderHelp(suggestions));
191
758
  return renderOutput(blocks);
192
759
  }
760
+ /** Wrap JS input in an arrow function for MCP evaluate_script. */
761
+ function wrapJsExpression(js) {
762
+ return `() => (${js.trim()})`;
763
+ }
764
+ /** Extract the actual value from MCP evaluate_script response. */
765
+ function parseEvalResult(output) {
766
+ // MCP wraps results in: "Script ran on page and returned:\n```json\n<value>\n```"
767
+ const jsonBlock = output.match(/```json\n([\s\S]*?)\n```/);
768
+ if (jsonBlock)
769
+ return jsonBlock[1].trim();
770
+ // Fallback: strip the preamble if present
771
+ const preamble = "Script ran on page and returned:";
772
+ if (output.includes(preamble))
773
+ return output.slice(output.indexOf(preamble) + preamble.length).trim();
774
+ return output.trim();
775
+ }
193
776
  async function handleEval(args) {
194
777
  const js = args.join(" ");
195
778
  if (!js) {
@@ -197,9 +780,9 @@ async function handleEval(args) {
197
780
  'Run `chrome-devtools-axi eval "document.title"` to evaluate JavaScript',
198
781
  ]);
199
782
  }
200
- const output = await callTool("evaluate_script", { function: js });
783
+ const output = await callTool("evaluate_script", { function: wrapJsExpression(js) });
201
784
  const blocks = [];
202
- blocks.push(encode({ result: output.trim() }));
785
+ blocks.push(encode({ result: parseEvalResult(output) }));
203
786
  const suggestions = getSuggestions({ command: "eval" });
204
787
  if (suggestions.length > 0)
205
788
  blocks.push(renderHelp(suggestions));
@@ -209,44 +792,352 @@ async function handleStart() {
209
792
  const port = await ensureBridge();
210
793
  return encode({ status: "ready", port });
211
794
  }
795
+ export function formatStopOutput(wasStopped) {
796
+ return encode({ status: wasStopped ? "stopped" : "stopped (no-op)" });
797
+ }
212
798
  async function handleStop() {
213
- await stopBridge();
214
- return encode({ status: "stopped" });
799
+ const wasStopped = await stopBridge();
800
+ return formatStopOutput(wasStopped);
801
+ }
802
+ // --- Page management handlers ---
803
+ async function handlePages() {
804
+ const result = await callTool("list_pages");
805
+ const pages = parsePagesList(result);
806
+ if (pages.length === 0) {
807
+ return "pages: 0 pages open";
808
+ }
809
+ const blocks = [];
810
+ const header = `pages[${pages.length}]{id,url,selected}:`;
811
+ const rows = pages.map((p) => ` ${p.id},${p.url},${p.selected}`);
812
+ blocks.push(`${header}\n${rows.join("\n")}`);
813
+ blocks.push(renderHelp([
814
+ "Run `chrome-devtools-axi selectpage <id>` to switch tabs",
815
+ "Run `chrome-devtools-axi newpage <url>` to open a new tab",
816
+ ]));
817
+ return renderOutput(blocks);
818
+ }
819
+ async function handleNewPage(args, full) {
820
+ const url = args.filter((a) => !a.startsWith("--"))[0];
821
+ if (!url) {
822
+ throw new CdpError("Missing URL", "VALIDATION_ERROR", [
823
+ "Run `chrome-devtools-axi newpage https://example.com` to open a new tab",
824
+ ]);
825
+ }
826
+ const background = args.includes("--background");
827
+ const toolArgs = { url };
828
+ if (background)
829
+ toolArgs.background = true;
830
+ await callTool("new_page", toolArgs);
831
+ const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
832
+ return formatPageOutput(snapshot, "newpage", url, full);
833
+ }
834
+ async function handleSelectPage(args, full) {
835
+ const id = args[0];
836
+ if (!id) {
837
+ throw new CdpError("Missing page ID", "VALIDATION_ERROR", [
838
+ "Run `chrome-devtools-axi selectpage <id>` — get ID from `pages` command",
839
+ ]);
840
+ }
841
+ const pageId = parseInt(id, 10);
842
+ if (isNaN(pageId)) {
843
+ throw new CdpError(`Invalid page ID: ${id}`, "VALIDATION_ERROR", [
844
+ "Run `chrome-devtools-axi pages` to list available page IDs",
845
+ ]);
846
+ }
847
+ await callTool("select_page", { pageId });
848
+ const snapshot = stripSnapshotHeader(await callTool("take_snapshot"));
849
+ return formatPageOutput(snapshot, "selectpage", undefined, full);
850
+ }
851
+ async function handleClosePage(args) {
852
+ const id = args[0];
853
+ if (!id) {
854
+ throw new CdpError("Missing page ID", "VALIDATION_ERROR", [
855
+ "Run `chrome-devtools-axi closepage <id>` — get ID from `pages` command",
856
+ ]);
857
+ }
858
+ const pageId = parseInt(id, 10);
859
+ if (isNaN(pageId)) {
860
+ throw new CdpError(`Invalid page ID: ${id}`, "VALIDATION_ERROR", [
861
+ "Run `chrome-devtools-axi pages` to list available page IDs",
862
+ ]);
863
+ }
864
+ // Check page count before closing — last page can't be closed
865
+ const beforeResult = await callTool("list_pages");
866
+ const pagesBefore = parsePagesList(beforeResult);
867
+ if (pagesBefore.length <= 1) {
868
+ const blocks = [encode({ status: "cannot close the last open page (no-op)" })];
869
+ blocks.push(renderHelp([
870
+ "Run `chrome-devtools-axi newpage <url>` to open another tab first",
871
+ "Run `chrome-devtools-axi stop` to shut down the browser entirely",
872
+ ]));
873
+ return renderOutput(blocks);
874
+ }
875
+ await callTool("close_page", { pageId });
876
+ return encode({ status: "closed", pageId });
877
+ }
878
+ async function handleResize(args) {
879
+ const [widthStr, heightStr] = args;
880
+ if (!widthStr || !heightStr) {
881
+ throw new CdpError("Missing width and/or height", "VALIDATION_ERROR", [
882
+ "Run `chrome-devtools-axi resize 1280 720` to resize the viewport",
883
+ ]);
884
+ }
885
+ const width = parseInt(widthStr, 10);
886
+ const height = parseInt(heightStr, 10);
887
+ if (isNaN(width) || isNaN(height)) {
888
+ throw new CdpError("Width and height must be numbers", "VALIDATION_ERROR", [
889
+ "Run `chrome-devtools-axi resize 1280 720` to resize the viewport",
890
+ ]);
891
+ }
892
+ await callTool("resize_page", { width, height });
893
+ return encode({ resized: { width, height } });
894
+ }
895
+ // --- Interaction handlers ---
896
+ async function handleHover(args, full) {
897
+ const uid = args[0];
898
+ if (!uid) {
899
+ throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
900
+ "Run `chrome-devtools-axi hover @<uid>` — get uid from snapshot",
901
+ ]);
902
+ }
903
+ const snapshot = await callWithSnapshot("hover", { uid: parseUid(uid) });
904
+ return formatPageOutput(snapshot, "hover", undefined, full);
905
+ }
906
+ async function handleDrag(args, full) {
907
+ const from = args[0];
908
+ const to = args[1];
909
+ if (!from || !to) {
910
+ throw new CdpError("Missing element refs", "VALIDATION_ERROR", [
911
+ "Run `chrome-devtools-axi drag @<from> @<to>` — get uids from snapshot",
912
+ ]);
913
+ }
914
+ const snapshot = await callWithSnapshot("drag", { from_uid: parseUid(from), to_uid: parseUid(to) });
915
+ return formatPageOutput(snapshot, "drag", undefined, full);
916
+ }
917
+ async function handleFillForm(args, full) {
918
+ const { entries } = parseFillFormArgs(args);
919
+ if (entries.length === 0) {
920
+ throw new CdpError("No valid field entries", "VALIDATION_ERROR", [
921
+ 'Run `chrome-devtools-axi fillform @1="hello" @2="world"` to fill multiple fields',
922
+ ]);
923
+ }
924
+ const snapshot = await callWithSnapshot("fill_form", { elements: entries });
925
+ return formatPageOutput(snapshot, "fillform", undefined, full);
926
+ }
927
+ async function handleDialog(args) {
928
+ const action = args[0];
929
+ if (!action || (action !== "accept" && action !== "dismiss")) {
930
+ throw new CdpError("Missing or invalid action", "VALIDATION_ERROR", [
931
+ "Run `chrome-devtools-axi dialog accept` or `chrome-devtools-axi dialog dismiss`",
932
+ ]);
933
+ }
934
+ const params = { action };
935
+ const promptText = args.slice(1).join(" ");
936
+ if (promptText)
937
+ params.promptText = promptText;
938
+ await callTool("handle_dialog", params);
939
+ return encode({ dialog: action });
940
+ }
941
+ async function handleUpload(args, full) {
942
+ const uid = args[0];
943
+ const filePath = args[1];
944
+ if (!uid) {
945
+ throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
946
+ "Run `chrome-devtools-axi upload @<uid> <path>` — get uid from snapshot",
947
+ ]);
948
+ }
949
+ if (!filePath) {
950
+ throw new CdpError("Missing file path", "VALIDATION_ERROR", [
951
+ "Run `chrome-devtools-axi upload @<uid> /path/to/file` to upload a file",
952
+ ]);
953
+ }
954
+ const snapshot = await callWithSnapshot("upload_file", { uid: parseUid(uid), filePath });
955
+ return formatPageOutput(snapshot, "upload", undefined, full);
956
+ }
957
+ // --- Emulation handler ---
958
+ async function handleEmulate(args) {
959
+ const parsed = parseEmulateArgs(args);
960
+ const mcpArgs = {};
961
+ if (parsed.viewport !== undefined)
962
+ mcpArgs.viewport = parsed.viewport;
963
+ if (parsed.colorScheme !== undefined)
964
+ mcpArgs.colorScheme = parsed.colorScheme;
965
+ if (parsed.networkConditions !== undefined)
966
+ mcpArgs.networkConditions = parsed.networkConditions;
967
+ if (parsed.cpuThrottlingRate !== undefined)
968
+ mcpArgs.cpuThrottlingRate = parsed.cpuThrottlingRate;
969
+ if (parsed.geolocation !== undefined)
970
+ mcpArgs.geolocation = parsed.geolocation;
971
+ if (parsed.userAgent !== undefined)
972
+ mcpArgs.userAgent = parsed.userAgent;
973
+ await callTool("emulate", mcpArgs);
974
+ return encode({ emulated: parsed });
975
+ }
976
+ // --- DevTools debugging handlers ---
977
+ async function handleConsole(args) {
978
+ const parsed = parseConsoleArgs(args);
979
+ const mcpArgs = {};
980
+ if (parsed.types)
981
+ mcpArgs.types = parsed.types;
982
+ if (parsed.pageSize !== undefined)
983
+ mcpArgs.pageSize = parsed.pageSize;
984
+ if (parsed.pageIdx !== undefined)
985
+ mcpArgs.pageIdx = parsed.pageIdx;
986
+ const result = await callTool("list_console_messages", mcpArgs);
987
+ return formatMcpResult("console", result, [
988
+ "Run `chrome-devtools-axi console-get <id>` to see a specific message",
989
+ "Run `chrome-devtools-axi console --type error` to filter by type",
990
+ ]);
991
+ }
992
+ async function handleConsoleGet(args) {
993
+ const id = args[0];
994
+ if (!id) {
995
+ throw new CdpError("Missing console message id", "VALIDATION_ERROR", [
996
+ "Run `chrome-devtools-axi console-get <id>` — get id from `chrome-devtools-axi console`",
997
+ ]);
998
+ }
999
+ const result = await callTool("get_console_message", { msgid: parseInt(id, 10) });
1000
+ return formatMcpResult("message", result, []);
1001
+ }
1002
+ async function handleNetwork(args) {
1003
+ const parsed = parseNetworkArgs(args);
1004
+ const mcpArgs = {};
1005
+ if (parsed.resourceTypes)
1006
+ mcpArgs.resourceTypes = parsed.resourceTypes;
1007
+ if (parsed.pageSize !== undefined)
1008
+ mcpArgs.pageSize = parsed.pageSize;
1009
+ if (parsed.pageIdx !== undefined)
1010
+ mcpArgs.pageIdx = parsed.pageIdx;
1011
+ const result = await callTool("list_network_requests", mcpArgs);
1012
+ return formatMcpResult("network", result, [
1013
+ "Run `chrome-devtools-axi network-get <id>` to see request details",
1014
+ "Run `chrome-devtools-axi network --type fetch` to filter by type",
1015
+ ]);
1016
+ }
1017
+ async function handleNetworkGet(args) {
1018
+ const parsed = parseNetworkGetArgs(args);
1019
+ const mcpArgs = {};
1020
+ if (parsed.reqid !== undefined)
1021
+ mcpArgs.reqid = parsed.reqid;
1022
+ if (parsed.responseFilePath)
1023
+ mcpArgs.responseFilePath = parsed.responseFilePath;
1024
+ if (parsed.requestFilePath)
1025
+ mcpArgs.requestFilePath = parsed.requestFilePath;
1026
+ const result = await callTool("get_network_request", mcpArgs);
1027
+ return formatMcpResult("request", result, []);
1028
+ }
1029
+ // --- Performance handlers ---
1030
+ async function handleLighthouse(args) {
1031
+ const opts = parseLighthouseArgs(args);
1032
+ const result = await callTool("lighthouse_audit", opts);
1033
+ return formatMcpResult("lighthouse", result, []);
1034
+ }
1035
+ async function handlePerfStart(args) {
1036
+ const opts = parsePerfStartArgs(args);
1037
+ const toolArgs = {};
1038
+ if (opts.reload !== undefined)
1039
+ toolArgs.reload = opts.reload;
1040
+ if (opts.autoStop !== undefined)
1041
+ toolArgs.autoStop = opts.autoStop;
1042
+ if (opts.filePath !== undefined)
1043
+ toolArgs.filePath = opts.filePath;
1044
+ await callTool("performance_start_trace", toolArgs);
1045
+ return encode({ trace: "started", ...opts });
1046
+ }
1047
+ async function handlePerfStop(args) {
1048
+ const toolArgs = {};
1049
+ for (let i = 0; i < args.length; i++) {
1050
+ if (args[i] === "--file")
1051
+ toolArgs.filePath = args[++i];
1052
+ }
1053
+ const result = await callTool("performance_stop_trace", toolArgs);
1054
+ return formatMcpResult("trace", result, [
1055
+ "Run `chrome-devtools-axi perf-insight <set-id> <insight-name>` to analyze insights",
1056
+ ]);
1057
+ }
1058
+ async function handlePerfInsight(args) {
1059
+ const [setId, insightName] = args;
1060
+ if (!setId || !insightName) {
1061
+ throw new CdpError("Missing required arguments", "VALIDATION_ERROR", [
1062
+ "Run `chrome-devtools-axi perf-insight <set-id> <insight-name>` to analyze an insight",
1063
+ ]);
1064
+ }
1065
+ const result = await callTool("performance_analyze_insight", { insightSetId: setId, insightName });
1066
+ return formatMcpResult("insight", result, []);
1067
+ }
1068
+ async function handleHeap(args) {
1069
+ const filePath = args[0];
1070
+ if (!filePath) {
1071
+ throw new CdpError("Missing file path", "VALIDATION_ERROR", [
1072
+ "Run `chrome-devtools-axi heap ./snapshot.heapsnapshot` to take a heap snapshot",
1073
+ ]);
1074
+ }
1075
+ await callTool("take_memory_snapshot", { filePath });
1076
+ return encode({ heap: filePath });
1077
+ }
1078
+ async function handleHome(full) {
1079
+ const result = await getSessionStatus();
1080
+ if (!result) {
1081
+ const blocks = [encode({ browser: "no active session" })];
1082
+ blocks.push(renderHelp(["Run `chrome-devtools-axi open <url>` to start browsing"]));
1083
+ return renderOutput(blocks);
1084
+ }
1085
+ const snapshot = stripSnapshotHeader(result);
1086
+ return formatPageOutput(snapshot, "snapshot", undefined, full);
215
1087
  }
216
1088
  export async function main(argv) {
1089
+ // Best-effort hook installation on every invocation
1090
+ try {
1091
+ installHooks();
1092
+ }
1093
+ catch { /* silent */ }
217
1094
  const args = [...argv];
1095
+ const full = args.includes("--full");
1096
+ const filteredArgs = args.filter((a) => a !== "--full");
1097
+ const command = filteredArgs[0] ?? "";
1098
+ const commandArgs = filteredArgs.slice(1);
1099
+ // Per-subcommand help: `chrome-devtools-axi open --help`
1100
+ if (command && (commandArgs.includes("--help") || commandArgs.includes("-h"))) {
1101
+ const help = getCommandHelp(command);
1102
+ if (help) {
1103
+ process.stdout.write(help + "\n");
1104
+ return;
1105
+ }
1106
+ }
1107
+ // Global help: `chrome-devtools-axi --help`
218
1108
  if (args.includes("--help") || args.includes("-h")) {
219
1109
  process.stdout.write(HELP);
220
1110
  return;
221
1111
  }
222
- const command = args[0] ?? "";
223
- const commandArgs = args.slice(1);
224
1112
  try {
225
1113
  let output;
226
1114
  switch (command) {
227
1115
  case "open":
228
- output = await handleOpen(commandArgs);
1116
+ output = await handleOpen(commandArgs, full);
229
1117
  break;
230
1118
  case "snapshot":
231
- output = await handleSnapshot();
1119
+ output = await handleSnapshot(full);
1120
+ break;
1121
+ case "screenshot":
1122
+ output = await handleScreenshot(commandArgs);
232
1123
  break;
233
1124
  case "click":
234
- output = await handleClick(commandArgs);
1125
+ output = await handleClick(commandArgs, full);
235
1126
  break;
236
1127
  case "fill":
237
- output = await handleFill(commandArgs);
1128
+ output = await handleFill(commandArgs, full);
238
1129
  break;
239
1130
  case "type":
240
- output = await handleType(commandArgs);
1131
+ output = await handleType(commandArgs, full);
241
1132
  break;
242
1133
  case "press":
243
- output = await handlePress(commandArgs);
1134
+ output = await handlePress(commandArgs, full);
244
1135
  break;
245
1136
  case "scroll":
246
- output = await handleScroll(commandArgs);
1137
+ output = await handleScroll(commandArgs, full);
247
1138
  break;
248
1139
  case "back":
249
- output = await handleBack();
1140
+ output = await handleBack(full);
250
1141
  break;
251
1142
  case "wait":
252
1143
  output = await handleWait(commandArgs);
@@ -254,6 +1145,66 @@ export async function main(argv) {
254
1145
  case "eval":
255
1146
  output = await handleEval(commandArgs);
256
1147
  break;
1148
+ case "hover":
1149
+ output = await handleHover(commandArgs, full);
1150
+ break;
1151
+ case "drag":
1152
+ output = await handleDrag(commandArgs, full);
1153
+ break;
1154
+ case "fillform":
1155
+ output = await handleFillForm(commandArgs, full);
1156
+ break;
1157
+ case "dialog":
1158
+ output = await handleDialog(commandArgs);
1159
+ break;
1160
+ case "upload":
1161
+ output = await handleUpload(commandArgs, full);
1162
+ break;
1163
+ case "pages":
1164
+ output = await handlePages();
1165
+ break;
1166
+ case "newpage":
1167
+ output = await handleNewPage(commandArgs, full);
1168
+ break;
1169
+ case "selectpage":
1170
+ output = await handleSelectPage(commandArgs, full);
1171
+ break;
1172
+ case "closepage":
1173
+ output = await handleClosePage(commandArgs);
1174
+ break;
1175
+ case "resize":
1176
+ output = await handleResize(commandArgs);
1177
+ break;
1178
+ case "emulate":
1179
+ output = await handleEmulate(commandArgs);
1180
+ break;
1181
+ case "console":
1182
+ output = await handleConsole(commandArgs);
1183
+ break;
1184
+ case "console-get":
1185
+ output = await handleConsoleGet(commandArgs);
1186
+ break;
1187
+ case "network":
1188
+ output = await handleNetwork(commandArgs);
1189
+ break;
1190
+ case "network-get":
1191
+ output = await handleNetworkGet(commandArgs);
1192
+ break;
1193
+ case "lighthouse":
1194
+ output = await handleLighthouse(commandArgs);
1195
+ break;
1196
+ case "perf-start":
1197
+ output = await handlePerfStart(commandArgs);
1198
+ break;
1199
+ case "perf-stop":
1200
+ output = await handlePerfStop(commandArgs);
1201
+ break;
1202
+ case "perf-insight":
1203
+ output = await handlePerfInsight(commandArgs);
1204
+ break;
1205
+ case "heap":
1206
+ output = await handleHeap(commandArgs);
1207
+ break;
257
1208
  case "start":
258
1209
  output = await handleStart();
259
1210
  break;
@@ -261,8 +1212,8 @@ export async function main(argv) {
261
1212
  output = await handleStop();
262
1213
  break;
263
1214
  case "":
264
- // No command = show current page state
265
- output = await handleSnapshot();
1215
+ // No command = home view (status if running, hint if not)
1216
+ output = await handleHome(full);
266
1217
  break;
267
1218
  default:
268
1219
  process.stdout.write(renderError(`Unknown command: ${command}`, "UNKNOWN", [