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.
- package/README.md +156 -5
- package/package.json +7 -6
- package/plugins/chromex/skills/chromex/scripts/chromex.mjs +39 -9
- package/plugins/chromex/skills/chromex/scripts/lib/client.mjs +1 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/audit.mjs +182 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/console.mjs +63 -5
- package/plugins/chromex/skills/chromex/scripts/lib/commands/cookies.mjs +2 -1
- package/plugins/chromex/skills/chromex/scripts/lib/commands/emulate.mjs +15 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/interact.mjs +15 -5
- package/plugins/chromex/skills/chromex/scripts/lib/commands/keyboard.mjs +123 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/navigate.mjs +26 -1
- package/plugins/chromex/skills/chromex/scripts/lib/commands/network.mjs +98 -3
- package/plugins/chromex/skills/chromex/scripts/lib/commands/perf.mjs +10 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/refs.mjs +11 -4
- package/plugins/chromex/skills/chromex/scripts/lib/commands/screenshot.mjs +32 -5
- package/plugins/chromex/skills/chromex/scripts/lib/commands/snapshot.mjs +101 -52
- package/plugins/chromex/skills/chromex/scripts/lib/commands/stats.mjs +83 -0
- package/plugins/chromex/skills/chromex/scripts/lib/commands/storage.mjs +3 -2
- package/plugins/chromex/skills/chromex/scripts/lib/daemon.mjs +179 -24
- package/plugins/chromex/skills/chromex/scripts/lib/hints.mjs +203 -0
- package/plugins/chromex/skills/chromex/scripts/lib/launcher.mjs +8 -0
- package/plugins/chromex/skills/chromex/scripts/lib/output.mjs +74 -0
- 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.
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "Zero-dependency Chrome DevTools Protocol MCP server for AI agents.
|
|
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": "
|
|
8
|
-
"chromex-mcp": "
|
|
9
|
-
"chromex-cli": "
|
|
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>
|
|
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] [
|
|
53
|
-
|
|
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>
|
|
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>
|
|
71
|
-
clickxy <target> <x> <y>
|
|
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
|
|
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) {
|
|
@@ -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
|
+
}
|