chromex-mcp 1.3.0 → 1.5.0

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.
Files changed (23) hide show
  1. package/README.md +156 -5
  2. package/package.json +7 -6
  3. package/plugins/chromex/skills/chromex/scripts/chromex.mjs +39 -9
  4. package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +1 -0
  5. package/plugins/chromex/skills/chromex/scripts/lib/commands/audit.mjs +182 -0
  6. package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +63 -5
  7. package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +2 -1
  8. package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +15 -0
  9. package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +15 -5
  10. package/plugins/chromex/skills/chromex/scripts/lib/commands/keyboard.mjs +123 -0
  11. package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +26 -1
  12. package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +98 -3
  13. package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +10 -0
  14. package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +11 -4
  15. package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +32 -5
  16. package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +101 -52
  17. package/plugins/chromex/skills/chromex/scripts/lib/commands/stats.mjs +83 -0
  18. package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +3 -2
  19. package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +179 -24
  20. package/plugins/chromex/skills/chromex/scripts/lib/hints.mjs +203 -0
  21. package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +8 -0
  22. package/plugins/chromex/skills/chromex/scripts/lib/output.mjs +74 -0
  23. package/plugins/chromex/skills/chromex/scripts/mcp-server.mjs +106 -18
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # Chromex
2
2
 
3
- Zero-dependency Chrome DevTools Protocol toolkit for AI agents. 52 typed MCP tools + CLI. Connects directly to Chrome, Brave, Edge, or Chromium via WebSocket. No Puppeteer, no bloat.
3
+ Zero-dependency Chrome DevTools Protocol toolkit for AI agents. 56 typed MCP tools + CLI. Connects directly to Chrome, Brave, Edge, or Chromium via WebSocket. No Puppeteer, no bloat.
4
+
5
+ Designed from the ground up for token efficiency: incremental diffs, query-filtered snapshots, ref-based selection, and a plain-text output format that consistently beats JSON and YAML-style structured alternatives by 25% to 126% in head-to-head token measurements (see [Token Efficiency](#token-efficiency) below).
4
6
 
5
7
  ## Features
6
8
 
7
- - **52 MCP tools** -- typed JSON Schema, annotations (`readOnlyHint`, `destructiveHint`), inline screenshots (base64)
9
+ - **56 MCP tools** -- typed JSON Schema, annotations (`readOnlyHint`, `destructiveHint`), inline screenshots (base64)
8
10
  - **Zero dependencies** -- uses only Node.js 22+ built-in modules (WebSocket, fs, net, crypto)
9
11
  - **Ref-based selection** -- `snap --refs` assigns `@e1`, `@e2`... to interactive elements, then `click @e5` or `fill @e3 "value"`. No fragile CSS selectors
10
12
  - **Incremental snapshots** -- second snapshot returns only changed nodes (diff), reducing output from thousands of lines to just what changed
13
+ - **Query-filtered snapshots** -- `snap --query=login` returns only matching nodes and their ancestors, cutting output by 95% to 99% on large pages like GitHub, Jira, or Gmail
11
14
  - **Auto-snapshot** -- interactive commands (click, fill, nav, etc.) automatically append an incremental snapshot with refs, so the agent sees the page state in a single round-trip
15
+ - **Contextual hints** -- after each action, chromex appends up to 3 `help[]` next-step suggestions picked from the current ref map, eliminating the "what do I do next" turn. Opt-out with `--no-hints`
16
+ - **Pre-computed aggregates** -- `net` and `console` outputs embed counters (`network[47] errors:3 pending:0 ok:44`, `console[12] errors:2 warnings:4 info:6`) so the agent never needs a follow-up count
12
17
  - **Scroll detection** -- snapshots report scrollable containers with remaining distance (`[scroll: page: down:1200px | sidebar: up:300px]`)
13
18
  - **Per-tab persistent daemons** -- each tab gets a background process connected via Unix socket. Chrome's "Allow debugging" modal fires once, not on every command
14
19
  - **Security hardened** -- domain filtering (allow/blocklist), CDP method blocklist, token-authenticated sockets, full audit log
@@ -45,7 +50,7 @@ Add to `~/.claude/settings.json`:
45
50
  }
46
51
  ```
47
52
 
48
- This approves all 52 MCP tools at once. For granular control, approve individual tools:
53
+ This approves all 56 MCP tools at once. For granular control, approve individual tools:
49
54
 
50
55
  ```json
51
56
  {
@@ -141,6 +146,10 @@ chromex close <target> # Close tab
141
146
  chromex focus <target> # Activate/focus tab
142
147
  chromex launch # Launch browser with debugging
143
148
  chromex launch --incognito --browser brave # Launch Brave in incognito
149
+ chromex launch --headless --url https://example.com # Headless mode for CI/CD
150
+ chromex launch --proxy socks5://localhost:1080 # Launch with proxy
151
+ chromex launch --insecure # Ignore certificate errors
152
+ chromex launch --chrome-arg --disable-web-security # Pass custom Chrome flag
144
153
  chromex launch --profile testing --url https://... # Isolated profile + URL
145
154
  chromex incognito https://example.com # Isolated context (no relaunch)
146
155
  chromex stop # Stop all daemons
@@ -153,12 +162,18 @@ chromex snap <target> # Accessibility tree snapshot (compa
153
162
  chromex snap <target> --refs # With interactive refs (@e1, @e2...)
154
163
  chromex snap <target> --depth=3 # Limit tree depth
155
164
  chromex snap <target> --full # Force full snapshot (skip diff)
165
+ chromex snap <target> --query=login # Filter to matching nodes + ancestors (hierarchy preserved)
156
166
  chromex html <target> "#main" # Element HTML by selector
157
167
  chromex shot <target> /tmp/page.png # Viewport screenshot
158
168
  chromex shot <target> /tmp/full.png --full # Full page screenshot
159
- chromex net <target> # Network resource timing
169
+ chromex shot <target> --format=jpeg --quality=80 # JPEG/WebP with quality control
170
+ chromex shot <target> @e5 # Screenshot of specific element by ref
171
+ chromex net <target> # List network requests (CDP tracked)
172
+ chromex net <target> <requestId> # Request detail: headers, timing, body
160
173
  chromex perf <target> # Core Web Vitals + memory + DOM stats
161
174
  chromex console <target> 5000 # Capture console.log/error for 5s
175
+ chromex console <target> list # Show stored messages since daemon start
176
+ chromex console <target> detail <id> # Message detail with stack trace
162
177
  chromex domsnapshot <target> # Structured DOM with bounding rects
163
178
  chromex domsnapshot <target> --styles # Include computed styles
164
179
  chromex highlight <target> "h1" # Highlight element with overlay
@@ -178,6 +193,10 @@ chromex evalraw <target> "Page.getLayoutMetrics" # Layout info
178
193
 
179
194
  ```bash
180
195
  chromex nav <target> "https://example.com" # Navigate + wait for load
196
+ chromex nav <target> back # Go back in history
197
+ chromex nav <target> forward # Go forward in history
198
+ chromex nav <target> reload # Reload page
199
+ chromex nav <target> reload-hard # Reload ignoring cache
181
200
  chromex waitfor <target> ".results" 10000 # Wait for CSS selector (10s)
182
201
  chromex wait <target> networkidle # Wait for network idle
183
202
  chromex wait <target> load # Wait for page load
@@ -195,7 +214,12 @@ chromex scroll <target> to "#footer" # Scroll to element
195
214
  ```bash
196
215
  chromex click <target> "button.submit" # Click by CSS selector
197
216
  chromex click <target> @e5 # Click by ref (from snap --refs)
217
+ chromex click <target> @e5 --dbl # Double-click
198
218
  chromex clickxy <target> 100 200 # Click at CSS pixel coords
219
+ chromex clickxy <target> 100 200 --dbl # Double-click at coords
220
+ chromex key <target> Enter # Press key
221
+ chromex key <target> "Control+A" # Key combination
222
+ chromex key <target> "Control+Shift+R" # Multi-modifier combo
199
223
  chromex type <target> "hello world" # Type text (works cross-origin)
200
224
  chromex hover <target> @e12 # Hover element by ref
201
225
  chromex drag <target> "#source" "#dest" # Drag & drop by selector
@@ -268,6 +292,8 @@ chromex emulate <target> macbook-air # 1440x900 @2x laptop
268
292
  chromex emulate <target> desktop-1080p # 1920x1080 @1x
269
293
  chromex emulate <target> desktop-4k # 3840x2160 @1x
270
294
  chromex emulate <target> reset # Reset to default
295
+ chromex resize <target> 1280 720 # Custom viewport dimensions
296
+ chromex resize <target> 1440 900 2 # Custom with DPR (retina)
271
297
  chromex geo <target> -23.55 -46.63 # Set geolocation (Sao Paulo)
272
298
  chromex geo <target> reset # Clear geolocation
273
299
  chromex timezone <target> "America/Sao_Paulo" # Set timezone
@@ -295,6 +321,18 @@ chromex webauthn <target> creds # List stored credentials
295
321
  chromex webauthn <target> disable # Remove authenticator
296
322
  ```
297
323
 
324
+ ### Audit & Analytics
325
+
326
+ ```bash
327
+ chromex audit <target> # Full Lighthouse audit (all categories)
328
+ chromex audit <target> performance,seo # Specific categories
329
+ chromex audit <target> accessibility desktop # Accessibility on desktop
330
+ chromex stats <target> # Session analytics (command counts, timing)
331
+ chromex stats <target> --full # Full action timeline
332
+ chromex stats <target> --reset # Reset counters
333
+ chromex stats <target> --export=/tmp/stats.json # Export as JSON
334
+ ```
335
+
298
336
  ## Ref-Based Selection
299
337
 
300
338
  The killer feature for AI agents. Instead of fragile CSS selectors, use numbered refs:
@@ -349,6 +387,30 @@ chromex snap <target> --depth=3 # Only 3 levels deep
349
387
 
350
388
  Nodes at the depth limit render as leaves (children are not expanded).
351
389
 
390
+ ### Query Filter
391
+
392
+ On large pages, a full accessibility tree can be tens of kilobytes. Use `--query` to keep only the nodes you care about, with their ancestors preserved so the hierarchy stays intact:
393
+
394
+ ```bash
395
+ chromex snap <target> --query=login # Substring match (case-insensitive)
396
+ chromex snap <target> --query=issues # role/name/value all searched
397
+ ```
398
+
399
+ Matched nodes are prefixed with `>` in the output, so the agent can spot them at a glance. Ancestor chains are included from the match up to the root, so the agent still understands the surrounding structure.
400
+
401
+ `@eN` refs stay stable across filtered and unfiltered calls because the ref map is always computed against the full tree. A filtered `snap --query=login` followed by a plain `snap --refs` returns the same ref numbering. Fingerprints for incremental diff are also computed on the full tree, so the next non-query snapshot still produces a correct diff against the previous state.
402
+
403
+ Measured reduction on a real 65 KB GitHub repo page snapshot:
404
+
405
+ | Query | Output bytes | Reduction |
406
+ |-------|-------------:|----------:|
407
+ | `snap --full` (baseline) | 65,455 | -- |
408
+ | `snap --query=issues` | 317 | **-99.5%** |
409
+ | `snap --query=star` | 1,490 | **-97.7%** |
410
+ | `snap --query=readme` | 936 | **-98.6%** |
411
+
412
+ When no node matches, chromex returns the explicit empty state `snap: no matches for query "X"` so the agent never confuses an empty filter with a silent failure.
413
+
352
414
  ### Scroll Detection
353
415
 
354
416
  Snapshots automatically detect scrollable containers and report remaining scroll distance:
@@ -379,7 +441,7 @@ chromex click <target> @e3
379
441
  # ...
380
442
  ```
381
443
 
382
- Commands that trigger auto-snapshot: `click`, `clickxy`, `type`, `fill`, `clear`, `select`, `check`, `form`, `nav`, `dialog`, `loadall`, `drag`, `touch`, `upload`.
444
+ Commands that trigger auto-snapshot: `click`, `clickxy`, `type`, `key`, `fill`, `clear`, `select`, `check`, `form`, `nav`, `dialog`, `loadall`, `drag`, `touch`, `upload`.
383
445
 
384
446
  Suppress with `--no-snap` for scripts doing rapid sequential actions:
385
447
 
@@ -389,6 +451,95 @@ chromex fill <target> @e2 "secret123" --no-snap
389
451
  chromex click <target> @e3 # Only this one triggers snapshot
390
452
  ```
391
453
 
454
+ ## Contextual Hints
455
+
456
+ After any action that produces a fresh ref map (auto-snap on interactive commands, or an explicit `snap --refs`), chromex appends a `help[N]:` block with up to 3 next-step suggestions picked heuristically from the current elements and the last command:
457
+
458
+ ```
459
+ Navigated to https://github.com/login
460
+
461
+ RootWebArea "Sign in to GitHub"
462
+ @e1 [textbox] Username or email address
463
+ @e2 [textbox] Password
464
+ @e3 [button] Sign in
465
+ @e4 [link] Forgot password?
466
+
467
+ help[3]:
468
+ chromex fill <t> @e1 "<value>" # textbox "Username or email address"
469
+ chromex click <t> @e3 # button "Sign in"
470
+ chromex click <t> @e4 # link "Forgot password?"
471
+ ```
472
+
473
+ The agent gets the most probable next commands inline, eliminating the "decide what to click" turn. Heuristic rules:
474
+
475
+ - **After `fill`** -- priority is a matching submit button (label matches `login`, `submit`, `send`, `search`, `go`, `continue`, ...) or `key Enter` fallback, then the next unfilled input.
476
+ - **After `nav`** -- first input (highest priority), then first submit button, then first link.
477
+ - **After `snap --refs`** (or any default) -- top interactive elements with non-empty names.
478
+ - **Maximum 3 hints per response.**
479
+
480
+ ### Staleness Guard
481
+
482
+ Hints are only emitted when chromex can guarantee the ref map matches the DOM that was just rendered. This prevents the agent from clicking `@eN` coordinates that no longer exist on screen:
483
+
484
+ | Command | Hints? | Why |
485
+ |---------|--------|-----|
486
+ | `click`, `fill`, `nav`, `type`, ... (default) | Yes | auto-snap just ran with refs |
487
+ | `click @e1 --no-snap`, `fill ... --no-snap` | **No** | ref map may be stale |
488
+ | `snap --refs` | Yes | ref map populated by this call |
489
+ | `snap --refs` on a page with zero interactive elements | **No** | nothing to suggest, avoids a `snap --refs` loop |
490
+ | Bare `snap` (no `--refs`) | **No** | ref map was not refreshed |
491
+
492
+ Navigation (URL, back, forward, reload) always clears the ref map before dispatching, so post-nav hints always reflect the new page.
493
+
494
+ Opt out explicitly with `--no-hints` (CLI) or `noHints: true` (MCP) for scripts that parse output strictly.
495
+
496
+ ## Token Efficiency
497
+
498
+ Chromex outputs are designed to be read by LLM agents, not humans. Every format decision -- plain text, refs over CSS selectors, incremental diffs, query filters -- was made to minimize tokens while preserving the information the agent actually needs to act.
499
+
500
+ ### Measured against common alternatives
501
+
502
+ We measured the current plain-text output against three structured format candidates often proposed for agent interfaces: minified JSON, pretty-printed JSON, and a TOON-style compact encoder (a zero-dependency 60-line implementation of the YAML-inline compact style). Five representative chromex outputs were encoded in each format and tokenized with `tiktoken` (`cl100k_base`). Lower token counts are better.
503
+
504
+ #### Case by case
505
+
506
+ | Case | text-free | json-min | json-pretty | toon-compact |
507
+ |--------------------------------|-----------:|---------------:|----------------:|----------------:|
508
+ | `snap-login-small` | **130** | 178 (+37%) | 301 (+132%) | 212 (+63%) |
509
+ | `snap-repo-large` (65 KB real) | **19,702** | 34,315 (+74%) | 63,232 (+221%) | 44,613 (+126%) |
510
+ | `net-list-50` | **454** | 574 (+26%) | 921 (+103%) | 706 (+56%) |
511
+ | `console-list-12` | **273** | 363 (+33%) | 576 (+111%) | 453 (+66%) |
512
+ | `fill-action-small` | **99** | 127 (+28%) | 221 (+123%) | 146 (+48%) |
513
+
514
+ The five cases cover the most common outputs an agent sees during a real session: a small accessibility snapshot with refs (a login form), a large accessibility snapshot of a real GitHub repository page, a network list with 50 tracked requests, a console list with a mix of `log`, `warn` and `error` entries, and a post-action result (fill + incremental diff + hints).
515
+
516
+ #### Aggregate (5 cases combined)
517
+
518
+ | Format | tokens | vs text-free |
519
+ |------------------------|----------:|-------------:|
520
+ | **text-free (current)**| **20,658**| -- |
521
+ | json-min | 35,557 | +72.1% |
522
+ | json-pretty | 65,251 | +215.9% |
523
+ | toon-compact (custom) | 46,130 | +123.3% |
524
+
525
+ The current plain-text output is the most token-efficient in every single case. Every structured alternative costs more tokens, not less, because JSON and YAML-style encodings add syntactic overhead (`{`, `}`, `"`, `:`, `,`, indentation) that the chromex plain-text format omits entirely. The chromex format is already dense: short refs (`@e5`), unquoted labels, no wrapping, no redundancy. Structured formats have nothing to optimize away.
526
+
527
+ ### Why this matters
528
+
529
+ - **No structured-output refactor is planned.** Chromex keeps the current plain-text format because the data shows it wins.
530
+ - **`@eN` refs beat CSS selectors** not just for reliability (accessibility tree is stable across SPA re-renders) but also for token count -- `@e5` is one token, `document.querySelector('#login-form > div.field input[name="email"]')` is around fifteen.
531
+ - **`--query` and incremental diff** are where the real token savings live. A filtered snapshot on a large page drops output by 95% to 99%, and an incremental diff after an action drops it by 90% or more. These are multiplicative with the already-dense base format.
532
+
533
+ ### Reproducing the benchmark
534
+
535
+ The comparison script is checked into the repo and runs standalone:
536
+
537
+ ```bash
538
+ node tests/benchmarks/token-format-comparison.mjs
539
+ ```
540
+
541
+ It uses `js-tiktoken` (devDependency) with `cl100k_base`, the GPT-4 tokenizer. Claude uses its own BPE tokenizer with a different vocabulary, so absolute numbers would shift by a few percent -- but both are byte-pair encoders over similar corpora, and the direction of the comparison (which format wins) is consistent across BPE tokenizers. Nothing in the results is a rounding error: the gaps are 25% to 220%.
542
+
392
543
  ## MCP vs CLI
393
544
 
394
545
  Both interfaces call the same core, same daemons, same commands. The difference is how they integrate with Claude Code.
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "chromex-mcp",
3
- "version": "1.3.0",
4
- "description": "Zero-dependency Chrome DevTools Protocol MCP server for AI agents. 52 typed tools, per-tab daemons, security hardened.",
3
+ "version": "1.5.0",
4
+ "description": "Zero-dependency Chrome DevTools Protocol MCP server for AI agents. 56 typed tools, per-tab daemons, security hardened, contextual hints, query-filtered snapshots.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "chromex": "./bin/chromex.mjs",
8
- "chromex-mcp": "./bin/chromex-mcp.mjs",
9
- "chromex-cli": "./bin/chromex-cli.mjs"
7
+ "chromex": "bin/chromex.mjs",
8
+ "chromex-mcp": "bin/chromex-mcp.mjs",
9
+ "chromex-cli": "bin/chromex-cli.mjs"
10
10
  },
11
11
  "files": [
12
12
  "bin/",
@@ -40,13 +40,14 @@
40
40
  "license": "MIT",
41
41
  "repository": {
42
42
  "type": "git",
43
- "url": "https://github.com/whallysson/chromex"
43
+ "url": "git+https://github.com/whallysson/chromex.git"
44
44
  },
45
45
  "homepage": "https://github.com/whallysson/chromex#mcp-server-recommended-for-claude-code",
46
46
  "engines": {
47
47
  "node": ">=22.0.0"
48
48
  },
49
49
  "devDependencies": {
50
+ "js-tiktoken": "^1.0.21",
50
51
  "vitest": "^3.0.0"
51
52
  }
52
53
  }
@@ -27,7 +27,7 @@ const NEEDS_TARGET = new Set([
27
27
  'intercept', 'har', 'coverage',
28
28
  // Tier 3
29
29
  'trace', 'heap', 'webauthn', 'drag', 'touch', 'domsnapshot', 'highlight',
30
- 'hover',
30
+ 'hover', 'key', 'resize', 'audit', 'stats',
31
31
  ]);
32
32
 
33
33
  const USAGE = `chromex - Chrome DevTools Protocol CLI for AI agents
@@ -44,15 +44,29 @@ Usage: chromex <command> [args]
44
44
  --browser chrome|brave|edge Choose browser
45
45
  --profile NAME Use named profile
46
46
  --url URL Open URL on launch
47
+ --headless Launch in headless mode (no UI)
48
+ --proxy PROXY Proxy server (e.g. socks5://localhost:1080)
49
+ --insecure Ignore certificate errors
50
+ --chrome-arg FLAG Pass custom Chrome flag (e.g. --chrome-arg --disable-web-security)
47
51
  incognito [url] Create isolated browser context (no relaunch)
48
52
 
49
53
  INSPECT
50
- snap <target> Accessibility tree snapshot (compact)
54
+ snap <target> [options] Accessibility tree snapshot (compact)
55
+ --refs Assign @eN refs to interactive elements
56
+ --full Force full snapshot (skip incremental diff)
57
+ --depth=N Limit tree depth (nodes at limit render as leaves)
58
+ --query=TEXT Filter to nodes matching substring + ancestors
51
59
  html <target> [selector] Get HTML (full page or CSS selector)
52
- shot <target> [file] [--full] Screenshot (viewport or full page)
53
- net <target> Network performance entries
60
+ shot <target> [file] [options] Screenshot (viewport, full page, or element)
61
+ --full Full page capture
62
+ --format=jpeg|webp|png Image format (default: png)
63
+ --quality=N Compression quality 0-100 (JPEG/WebP)
64
+ @eN Capture specific element by ref
65
+ net <target> [requestId] Network requests list, or detail by request ID
54
66
  perf <target> Core Web Vitals + performance metrics
55
67
  console <target> [duration_ms] Capture console output (default 5000ms)
68
+ console <target> list Show stored messages since daemon start
69
+ console <target> detail <id> Message detail with stack trace
56
70
  domsnapshot <target> [--styles] Structured DOM snapshot with bounding rects
57
71
  highlight <target> <sel|clear> Highlight element with overlay
58
72
 
@@ -61,14 +75,15 @@ Usage: chromex <command> [args]
61
75
  evalraw <target> <method> [json] Raw CDP command (some methods blocked)
62
76
 
63
77
  NAVIGATE
64
- nav <target> <url> Navigate to URL and wait for load
78
+ nav <target> <url|action> Navigate: URL, back, forward, reload, reload-hard
65
79
  waitfor <target> <selector> [ms] Wait for CSS selector to appear
66
80
  wait <target> <event> [ms] Wait for: networkidle, load, domready, fcp
67
81
  scroll <target> <dir> [amount] Scroll: up, down, top, bottom, to <selector>
68
82
 
69
83
  INTERACT
70
- click <target> <selector> Click element by CSS selector
71
- clickxy <target> <x> <y> Click at CSS pixel coordinates
84
+ click <target> <selector> [--dbl] Click element (supports double-click)
85
+ clickxy <target> <x> <y> [--dbl] Click at coordinates (supports double-click)
86
+ key <target> <combo> Press key: Enter, Tab, Escape, Control+A, Meta+C
72
87
  type <target> <text> Type text at current focus
73
88
  drag <target> <from> <to> Drag & drop (selectors or x1,y1 x2,y2)
74
89
  touch <target> <gesture> [args] Touch: tap, swipe, pinch, longpress
@@ -99,6 +114,7 @@ Usage: chromex <command> [args]
99
114
  timezone <target> <tz|reset> Set timezone (e.g. America/Sao_Paulo)
100
115
  locale <target> <locale|reset> Set locale (e.g. pt-BR)
101
116
  cpu <target> <rate|reset> CPU throttle (1=normal, 4=4x slower, 6=mobile)
117
+ resize <target> <w> <h> [dpr] Resize viewport to custom dimensions
102
118
 
103
119
  ADVANCED
104
120
  inject <target> <script|flags> Inject JS on every page load (--file, --remove, --list)
@@ -108,11 +124,22 @@ Usage: chromex <command> [args]
108
124
  heap <target> snapshot [file] Heap snapshot for memory analysis
109
125
  webauthn <target> enable|creds|dis Virtual authenticator for passkey testing
110
126
 
127
+ AUDIT
128
+ audit <target> [categories] [device] Lighthouse audit (performance, accessibility, SEO)
129
+ categories: performance,accessibility,seo,best-practices
130
+ device: mobile (default) or desktop
131
+ stats <target> [--full] [--reset] Session analytics (command counts, timing, errors)
132
+ --export=/path/to/stats.json Export as JSON
133
+
111
134
  DAEMON
112
135
  stop [target] Stop daemon(s)
113
136
 
114
137
  <target> is a unique targetId prefix from "chromex list". Ambiguous prefixes are rejected.
115
138
 
139
+ OUTPUT FLAGS
140
+ --no-snap Skip auto-snapshot after interactive commands
141
+ --no-hints Suppress contextual help[] suggestions
142
+
116
143
  SECURITY
117
144
  Config: ~/.chromex/config.json
118
145
  - blockedDomains / allowedDomains: domain filtering
@@ -166,7 +193,8 @@ async function main() {
166
193
 
167
194
  // Launch
168
195
  if (cmd === 'launch') {
169
- const options = parseFlags(args, ['incognito'], ['browser', 'profile', 'url']);
196
+ const options = parseFlags(args, ['incognito', 'headless', 'insecure'], ['browser', 'profile', 'url', 'proxy', 'chrome-arg']);
197
+ if (options['chrome-arg']) { options.chromeArgs = [options['chrome-arg']]; delete options['chrome-arg']; }
170
198
  const result = await launchBrowser(options);
171
199
  console.log(result);
172
200
  return;
@@ -245,7 +273,8 @@ async function main() {
245
273
  const conn = await getOrStartTabDaemon(targetId, config);
246
274
 
247
275
  const noSnap = args.includes('--no-snap');
248
- const cmdArgs = args.slice(1).filter(a => a !== '--no-snap');
276
+ const noHints = args.includes('--no-hints');
277
+ const cmdArgs = args.slice(1).filter(a => a !== '--no-snap' && a !== '--no-hints');
249
278
 
250
279
  // Juntar argumentos para comandos que aceitam texto livre
251
280
  if (cmd === 'eval') {
@@ -280,6 +309,7 @@ async function main() {
280
309
  }
281
310
 
282
311
  if (noSnap) cmdArgs.push('--no-snap');
312
+ if (noHints) cmdArgs.push('--no-hints');
283
313
  const response = await sendCommand(conn, { cmd, args: cmdArgs });
284
314
 
285
315
  if (response.ok) {
@@ -16,6 +16,7 @@ export class CDP {
16
16
  }
17
17
 
18
18
  async connect(wsUrl) {
19
+ this.wsUrl = wsUrl;
19
20
  return new Promise((res, rej) => {
20
21
  this.#ws = new WebSocket(wsUrl);
21
22
  this.#ws.onopen = () => res();
@@ -0,0 +1,182 @@
1
+ // Lighthouse audit via subprocess (zero deps -- invokes npx lighthouse externally)
2
+ // Chrome: connects to existing browser via --port (reuses session)
3
+ // Other browsers (Brave, Edge, etc.): Lighthouse launches its own headless Chrome
4
+
5
+ import { execSync } from 'child_process';
6
+ import { existsSync } from 'fs';
7
+ import { evalStr } from './evaluate.mjs';
8
+
9
+ const VALID_CATEGORIES = ['performance', 'accessibility', 'seo', 'best-practices'];
10
+
11
+ // Find any Chromium-based browser for CHROME_PATH env var
12
+ function findChromiumPath() {
13
+ const paths = process.platform === 'darwin' ? [
14
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
15
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
16
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
17
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
18
+ ] : [
19
+ '/usr/bin/google-chrome', '/usr/bin/google-chrome-stable',
20
+ '/usr/bin/brave-browser', '/usr/bin/chromium-browser', '/usr/bin/chromium',
21
+ '/usr/bin/microsoft-edge',
22
+ ];
23
+ for (const p of paths) {
24
+ if (existsSync(p)) return p;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ // Check if Chrome's HTTP debug endpoint is available (Brave/Edge don't expose it)
30
+ function isHttpDebugAvailable(port) {
31
+ try {
32
+ const result = execSync(`curl -sf http://127.0.0.1:${port}/json/version`, {
33
+ encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
34
+ });
35
+ return result.length > 0;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ export async function auditStr(cdp, sid, categories, device, reportPath) {
42
+ // Get current page URL
43
+ const url = await evalStr(cdp, sid, 'window.location.href');
44
+ if (!url || url === 'about:blank') {
45
+ throw new Error('Navigate to a page first before running audit.');
46
+ }
47
+
48
+ // Validate categories
49
+ const cats = categories
50
+ ? categories.split(',').map(c => c.trim().toLowerCase()).filter(c => VALID_CATEGORIES.includes(c))
51
+ : VALID_CATEGORIES;
52
+
53
+ if (cats.length === 0) {
54
+ throw new Error(`Invalid categories. Valid: ${VALID_CATEGORIES.join(', ')}`);
55
+ }
56
+
57
+ // Build base args
58
+ const args = [
59
+ '--output=json',
60
+ `--only-categories=${cats.join(',')}`,
61
+ '--quiet',
62
+ ];
63
+
64
+ if (device === 'desktop') args.push('--preset=desktop');
65
+
66
+ if (reportPath) {
67
+ args.push(`--output-path=${reportPath}`);
68
+ args.push('--output=html');
69
+ args.push('--output=json');
70
+ }
71
+
72
+ // Detect: Chrome (has /json/version) vs other browsers (Brave, Edge, etc.)
73
+ let port;
74
+ if (cdp.wsUrl) {
75
+ const m = cdp.wsUrl.match(/:(\d+)\//);
76
+ if (m) port = m[1];
77
+ }
78
+
79
+ let mode;
80
+ if (port && isHttpDebugAvailable(port)) {
81
+ // Chrome: reuse existing browser session
82
+ args.push(`--port=${port}`);
83
+ mode = 'connected (existing browser)';
84
+ } else {
85
+ // Brave/Edge/other: Lighthouse launches its own headless Chrome
86
+ args.push('--chrome-flags=--headless=new');
87
+ mode = 'standalone (headless Chrome)';
88
+ }
89
+
90
+ const cmd = `npx --yes lighthouse ${JSON.stringify(url)} ${args.join(' ')}`;
91
+
92
+ // Set CHROME_PATH for standalone mode (Lighthouse uses chrome-launcher which reads it)
93
+ const env = { ...process.env };
94
+ if (mode.startsWith('standalone')) {
95
+ const chromePath = findChromiumPath();
96
+ if (chromePath) env.CHROME_PATH = chromePath;
97
+ }
98
+
99
+ let jsonOutput;
100
+ try {
101
+ jsonOutput = execSync(cmd, {
102
+ encoding: 'utf8',
103
+ timeout: 120000,
104
+ stdio: ['pipe', 'pipe', 'pipe'],
105
+ maxBuffer: 50 * 1024 * 1024,
106
+ env,
107
+ });
108
+ } catch (e) {
109
+ const stderr = e.stderr?.toString().trim() || '';
110
+ if (stderr.includes('not found') || stderr.includes('ENOENT')) {
111
+ throw new Error('lighthouse not found. Install: npm i -g lighthouse');
112
+ }
113
+ if (stderr.includes('No Chrome installations found')) {
114
+ throw new Error('Lighthouse needs Chrome installed to run in standalone mode. Install Google Chrome or run against a Chrome instance with debug port.');
115
+ }
116
+ throw new Error(`Lighthouse failed: ${stderr || e.message}`);
117
+ }
118
+
119
+ // Parse JSON output
120
+ let report;
121
+ try {
122
+ report = JSON.parse(jsonOutput);
123
+ } catch {
124
+ const jsonStart = jsonOutput.lastIndexOf('{"');
125
+ if (jsonStart > 0) {
126
+ report = JSON.parse(jsonOutput.slice(jsonStart));
127
+ } else {
128
+ throw new Error('Failed to parse Lighthouse output.');
129
+ }
130
+ }
131
+
132
+ // Format results
133
+ const lines = [];
134
+
135
+ // Scores
136
+ const scores = {};
137
+ for (const cat of cats) {
138
+ const c = report.categories?.[cat];
139
+ if (c) scores[c.title] = Math.round((c.score || 0) * 100);
140
+ }
141
+ const scoreStr = Object.entries(scores).map(([k, v]) => `${k}: ${v}`).join(' | ');
142
+ lines.push(`Lighthouse Audit: ${scoreStr}`);
143
+ lines.push(`URL: ${url}`);
144
+ lines.push(`Device: ${device || 'mobile'} | Mode: ${mode}`);
145
+ lines.push('');
146
+
147
+ // Top opportunities
148
+ const audits = report.audits || {};
149
+ const opportunities = Object.values(audits)
150
+ .filter(a => a.details?.type === 'opportunity' && a.details?.overallSavingsMs > 0)
151
+ .sort((a, b) => (b.details.overallSavingsMs || 0) - (a.details.overallSavingsMs || 0))
152
+ .slice(0, 5);
153
+
154
+ if (opportunities.length > 0) {
155
+ lines.push('Top Opportunities:');
156
+ for (const opp of opportunities) {
157
+ const savings = (opp.details.overallSavingsMs / 1000).toFixed(1);
158
+ lines.push(` - ${opp.title} (savings: ${savings}s)`);
159
+ }
160
+ lines.push('');
161
+ }
162
+
163
+ // Critical diagnostics
164
+ const diagnostics = Object.values(audits)
165
+ .filter(a => a.score !== null && a.score < 0.5 && a.details?.type !== 'opportunity')
166
+ .sort((a, b) => (a.score || 0) - (b.score || 0))
167
+ .slice(0, 5);
168
+
169
+ if (diagnostics.length > 0) {
170
+ lines.push('Critical Issues:');
171
+ for (const diag of diagnostics) {
172
+ lines.push(` - ${diag.title}: ${diag.displayValue || 'needs improvement'}`);
173
+ }
174
+ }
175
+
176
+ if (reportPath) {
177
+ lines.push('');
178
+ lines.push(`Full report saved to: ${reportPath}`);
179
+ }
180
+
181
+ return lines.join('\n');
182
+ }