pursor 0.2.0 → 0.3.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 +654 -595
- package/bin/pursor.mjs +226 -190
- package/package.json +75 -73
- package/src/auth.js +92 -0
- package/src/har.js +159 -0
- package/src/index.js +94 -89
- package/src/runway.js +5 -2
- package/src/shoot.js +13 -1
- package/src/sweep.js +118 -104
package/README.md
CHANGED
|
@@ -1,595 +1,654 @@
|
|
|
1
|
-
# pursor
|
|
2
|
-
|
|
3
|
-
> **Visual QA & audit CLI + library + MCP server for the browser.**
|
|
4
|
-
> Capture, diff, sweep, and audit any web target — with multi-viewport,
|
|
5
|
-
> layered states, hover, grid overlays, animation freeze, camera control,
|
|
6
|
-
> axe-core accessibility audit, CI output, and auto-healing selectors.
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npx pursor probe https://example.com
|
|
10
|
-
npx pursor shoot https://example.com --preset mobile-375 --grid
|
|
11
|
-
npx pursor sweep ./plan.json
|
|
12
|
-
npx pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
13
|
-
npx pursor-mcp # MCP stdio server for Claude Code / Cursor
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm install pursor
|
|
20
|
-
npm install --save-dev playwright-core # peer dep — bring your own Chrome
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
`pursor` does **not** bundle Chromium. It drives your system Chrome via
|
|
24
|
-
Playwright. No extra browser downloads.
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Table of Contents
|
|
29
|
-
|
|
30
|
-
- [CLI](#cli)
|
|
31
|
-
- [MCP Server](#mcp-server)
|
|
32
|
-
- [Accessibility Audit](#accessibility-audit)
|
|
33
|
-
- [DOM Snapshot](#dom-snapshot)
|
|
34
|
-
- [CI Output](#ci-output)
|
|
35
|
-
- [Auto-heal Selectors](#auto-heal-selectors)
|
|
36
|
-
- [Sweep Plans](#sweep-plans)
|
|
37
|
-
- [Plugin API](#plugin-api)
|
|
38
|
-
- [Library API](#library-api)
|
|
39
|
-
- [Development](#development)
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## CLI
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
# Health check
|
|
47
|
-
pursor probe https://example.com
|
|
48
|
-
|
|
49
|
-
# Screenshot (simple)
|
|
50
|
-
pursor shot https://example.com ./out/shot.png
|
|
51
|
-
|
|
52
|
-
# Rich capture: viewport preset + cursor + grid
|
|
53
|
-
pursor shoot https://example.com \
|
|
54
|
-
--preset desktop-1280 \
|
|
55
|
-
--cursor crosshair \
|
|
56
|
-
--grid --grid-tile 64
|
|
57
|
-
|
|
58
|
-
# Isolate a layer (entity / terrain / hud / ui)
|
|
59
|
-
pursor layer https://example.com entity
|
|
60
|
-
|
|
61
|
-
# Animation timeline: 8 frames at 200ms
|
|
62
|
-
pursor frames https://example.com 8 200 ./frames/
|
|
63
|
-
|
|
64
|
-
# Hover an element
|
|
65
|
-
pursor hover https://example.com "text=Login"
|
|
66
|
-
|
|
67
|
-
# Pixel diff vs a reference screenshot
|
|
68
|
-
pursor diff https://example.com ./ref.png ./out/diff.png
|
|
69
|
-
|
|
70
|
-
# Batched plan (see plans/ for examples)
|
|
71
|
-
pursor sweep ./plan.json
|
|
72
|
-
|
|
73
|
-
# Accessibility audit (requires: npm i axe-core)
|
|
74
|
-
pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
75
|
-
|
|
76
|
-
# DOM + selector map snapshot
|
|
77
|
-
pursor dom https://example.com
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Subcommands
|
|
81
|
-
|
|
82
|
-
| Subcommand | Purpose |
|
|
83
|
-
|---|---|
|
|
84
|
-
| `probe` | Health check (HTTP status, page title) |
|
|
85
|
-
| `shot` / `full` | Viewport / full-page screenshot |
|
|
86
|
-
| `eval` | Execute JS in the page, return result |
|
|
87
|
-
| `click` / `type` / `wait` / `seq` | Interaction primitives |
|
|
88
|
-
| `diff` | Pixel-level diff vs a reference PNG |
|
|
89
|
-
| `viewports` | List all registered viewport presets |
|
|
90
|
-
| `shoot` | Rich capture (overlays, freeze, camera, plugins) |
|
|
91
|
-
| `layer` | Capture one isolated layer (entity/hud/ui/terrain) |
|
|
92
|
-
| `frames` | N-frame animation timeline at interval |
|
|
93
|
-
| `hover` | Hover state capture |
|
|
94
|
-
| `sweep` | Batched capture plan → HTML report + CI output |
|
|
95
|
-
| `audit` | ⭐ axe-core WCAG accessibility audit + highlighted screenshot |
|
|
96
|
-
| `dom` / `dom-snapshot` | ⭐ Serialized DOM + CSS selectors + XPath + bounding rects |
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
## MCP Server
|
|
101
|
-
|
|
102
|
-
`pursor-mcp` exposes every capability as MCP tools over stdio —
|
|
103
|
-
works with Claude Code, Cursor, Continue, and any MCP host.
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
npx pursor-mcp
|
|
107
|
-
# or with verbose logging:
|
|
108
|
-
npx pursor-mcp --verbose
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Exposed Tools
|
|
112
|
-
|
|
113
|
-
| Tool | Description |
|
|
114
|
-
|---|---|
|
|
115
|
-
| `pursor_shoot` | Full screenshot with viewport, grid, layer, cursor, camera, freeze |
|
|
116
|
-
| `pursor_diff` | Pixel diff vs reference PNG + diff overlay |
|
|
117
|
-
| `pursor_sweep` | Execute a batch plan JSON → summary |
|
|
118
|
-
| `pursor_frames` | Animation frame timeline |
|
|
119
|
-
| `pursor_probe` | Health-check a URL |
|
|
120
|
-
| `pursor_audit` | axe-core accessibility audit |
|
|
121
|
-
| `pursor_dom_snapshot` | DOM + CSSOM + selector map + bounding rects |
|
|
122
|
-
|
|
123
|
-
### Config
|
|
124
|
-
|
|
125
|
-
Config via `PURSOR_MCP_CONFIG` env var (inline JSON or file path)
|
|
126
|
-
or `~/.pursor/mcp-config.json`:
|
|
127
|
-
|
|
128
|
-
```json
|
|
129
|
-
{
|
|
130
|
-
"plugins": ["./my-plugin.js"],
|
|
131
|
-
"defaultOutDir": "./mcp-output",
|
|
132
|
-
"verbose": true
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### MCP Host Examples
|
|
137
|
-
|
|
138
|
-
**Claude Code:**
|
|
139
|
-
```json
|
|
140
|
-
{
|
|
141
|
-
"mcpServers": {
|
|
142
|
-
"pursor": {
|
|
143
|
-
"command": "npx",
|
|
144
|
-
"args": ["pursor-mcp"]
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
**Cursor:**
|
|
151
|
-
```json
|
|
152
|
-
{
|
|
153
|
-
"mcpServers": {
|
|
154
|
-
"pursor": {
|
|
155
|
-
"command": "npx",
|
|
156
|
-
"args": ["pursor-mcp", "--verbose"]
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
---
|
|
163
|
-
|
|
164
|
-
## Accessibility Audit
|
|
165
|
-
|
|
166
|
-
Run axe-core WCAG audits on any URL. Optionally captures a highlighted
|
|
167
|
-
screenshot with violated elements outlined in red.
|
|
168
|
-
|
|
169
|
-
```bash
|
|
170
|
-
# Quick audit with default tags (wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice)
|
|
171
|
-
pursor audit https://example.com
|
|
172
|
-
|
|
173
|
-
# Specific WCAG tags
|
|
174
|
-
pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
175
|
-
|
|
176
|
-
# Custom output directory
|
|
177
|
-
pursor audit https://example.com ./audit-report/
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
**Output:**
|
|
181
|
-
- `audit.json` — full axe-core results with violation summary
|
|
182
|
-
- `audit-summary.md` — readable Markdown report with severity breakdown
|
|
183
|
-
- `audit-highlighted.png` — screenshot with violations visibly marked
|
|
184
|
-
|
|
185
|
-
**Sweep plan usage:**
|
|
186
|
-
```json
|
|
187
|
-
{
|
|
188
|
-
"name": "accessibility-check",
|
|
189
|
-
"base": "https://example.com",
|
|
190
|
-
"steps": [
|
|
191
|
-
{
|
|
192
|
-
"name": "wcag-audit",
|
|
193
|
-
"audit": {
|
|
194
|
-
"tags": "wcag2a,wcag2aa,wcag21aa",
|
|
195
|
-
"screenshot": true
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
]
|
|
199
|
-
}
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## DOM Snapshot
|
|
205
|
-
|
|
206
|
-
Every capture can optionally produce a `.dom.json` sidecar with complete
|
|
207
|
-
page structure — useful for debugging visual regression without opening
|
|
208
|
-
a browser.
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
pursor dom https://example.com
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
**Captured data:**
|
|
215
|
-
- `dom` — `document.documentElement.outerHTML`
|
|
216
|
-
- `selectorMap[]` — every visible element with:
|
|
217
|
-
- `tag`, `id`, `css` (CSS selector), `xpath`
|
|
218
|
-
- `role`, `ariaLabel`, `ariaRole`, `text`, `placeholder`, `alt`, `href`, `src`
|
|
219
|
-
- `rect` — viewport-relative bounding box `{x, y, w, h}`
|
|
220
|
-
- `visible` — visibility flag
|
|
221
|
-
- `styles` — computed stylesheet rules keyed by selector
|
|
222
|
-
- `viewport` — current viewport dimensions + DPR
|
|
223
|
-
|
|
224
|
-
**Programmatic:**
|
|
225
|
-
```js
|
|
226
|
-
import { captureDomSnapshot } from "pursor";
|
|
227
|
-
|
|
228
|
-
const snapshot = await captureDomSnapshot({
|
|
229
|
-
url: "https://example.com",
|
|
230
|
-
out: "./snapshot.dom.json",
|
|
231
|
-
});
|
|
232
|
-
console.log(snapshot.selectorMap.length, "elements found");
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## CI Output
|
|
238
|
-
|
|
239
|
-
Sweep plans automatically generate CI-compatible output files alongside
|
|
240
|
-
the HTML report — no extra config needed.
|
|
241
|
-
|
|
242
|
-
```bash
|
|
243
|
-
pursor sweep ./plan.json
|
|
244
|
-
# Produces in the output directory:
|
|
245
|
-
# sweep.json — raw summary
|
|
246
|
-
# index.html — visual HTML dashboard
|
|
247
|
-
# sweep.junit.xml — JUnit XML (GitLab CI, Jenkins, CircleCI)
|
|
248
|
-
# sweep.github.json — GitHub Actions annotations format
|
|
249
|
-
# sweep.md — Markdown summary
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### GitHub Actions integration
|
|
253
|
-
|
|
254
|
-
```yaml
|
|
255
|
-
- name: Visual QA
|
|
256
|
-
run: npx pursor@latest sweep ./plan.json
|
|
257
|
-
- name: Annotate
|
|
258
|
-
uses: actions/github-script@v7
|
|
259
|
-
with:
|
|
260
|
-
script: |
|
|
261
|
-
const fs = require('fs');
|
|
262
|
-
const { annotations } = JSON.parse(fs.readFileSync('sweep-output/sweep.github.json'));
|
|
263
|
-
annotations.forEach(a => core.error(a.message, {file: a.filename, title: a.title}));
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### JUnit in GitLab CI
|
|
267
|
-
|
|
268
|
-
```yaml
|
|
269
|
-
visual-qa:
|
|
270
|
-
script: npx pursor@latest sweep ./plan.json
|
|
271
|
-
artifacts:
|
|
272
|
-
reports:
|
|
273
|
-
junit: sweep-output/sweep.junit.xml
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
---
|
|
277
|
-
|
|
278
|
-
## Auto-heal Selectors
|
|
279
|
-
|
|
280
|
-
In sweep plans, selectors can be an array of fallback strategies.
|
|
281
|
-
pursor tries each one in order until a visible element is found:
|
|
282
|
-
|
|
283
|
-
```json
|
|
284
|
-
{
|
|
285
|
-
"name": "login-flow",
|
|
286
|
-
"base": "https://example.com",
|
|
287
|
-
"steps": [
|
|
288
|
-
{
|
|
289
|
-
"name": "click-login",
|
|
290
|
-
"hover": {
|
|
291
|
-
"selector": [
|
|
292
|
-
"text=Login",
|
|
293
|
-
"button[type=submit]",
|
|
294
|
-
"#login-btn",
|
|
295
|
-
"a[href*='login']"
|
|
296
|
-
]
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
]
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
**Supported selector types:**
|
|
304
|
-
- `text=Login` — Playwright text locator (substring, or `text==Login` for exact)
|
|
305
|
-
- `text~regex` — regex text match
|
|
306
|
-
- `role=button|Submit` — ARIA role with accessible name
|
|
307
|
-
- `aria=label` — accessibility label
|
|
308
|
-
- `placeholder=Email` — placeholder text
|
|
309
|
-
- CSS selectors — any valid CSS selector as fallback
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
## Sweep Plans
|
|
314
|
-
|
|
315
|
-
Batch capture plans in JSON. Each step runs one operation.
|
|
316
|
-
|
|
317
|
-
```json
|
|
318
|
-
{
|
|
319
|
-
"name": "checkout-flow",
|
|
320
|
-
"base": "https://example.com",
|
|
321
|
-
"outDir": "./sweep-checkout",
|
|
322
|
-
"steps": [
|
|
323
|
-
{ "name": "homepage", "shoot": { "preset": "desktop-1280", "cursor": "default" } },
|
|
324
|
-
{ "name": "mobile-view","shoot": { "preset": "mobile-375", "grid": true } },
|
|
325
|
-
{ "name": "nav-hover", "hover": { "selector": "text=Products", "settleMs": 400 } },
|
|
326
|
-
{ "name": "add-to-cart","frames": { "count": 6, "intervalMs": 200 } },
|
|
327
|
-
{ "name": "diff", "diff": { "ref": "baseline" } }
|
|
328
|
-
]
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Step operations:** `shoot`, `hover`, `frames`, `diff`, `audit`, or any
|
|
333
|
-
registered plugin sweep-op.
|
|
334
|
-
|
|
335
|
-
---
|
|
336
|
-
|
|
337
|
-
## Plugin API
|
|
338
|
-
|
|
339
|
-
Extend `pursor` with custom viewport presets, sweep operations, or
|
|
340
|
-
capture hooks:
|
|
341
|
-
|
|
342
|
-
```js
|
|
343
|
-
export default {
|
|
344
|
-
name: "my-plugin",
|
|
345
|
-
viewport: {
|
|
346
|
-
"my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" },
|
|
347
|
-
},
|
|
348
|
-
sweepOp: {
|
|
349
|
-
lighthouse: async (ctx, opts) => {
|
|
350
|
-
// run lighthouse audit, write result at ctx.out
|
|
351
|
-
return { score: 95 };
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
beforeShoot: async (ctx) => { /* mutate ctx.flags */ },
|
|
355
|
-
afterShoot: async (ctx, meta) => { /* augment sidecar */ },
|
|
356
|
-
};
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
```bash
|
|
360
|
-
pursor shoot https://example.com --plugin ./my-plugin.js
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
Publish as `pursor-plugin-*` for auto-discovery.
|
|
364
|
-
|
|
365
|
-
### Built-in plugins
|
|
366
|
-
|
|
367
|
-
- **`plugin-audit`** — adds `audit` sweep-op (axe-core WCAG audit) and
|
|
368
|
-
`every-viewport` sweep-op (capture at every preset). Adds `audit-canvas`
|
|
369
|
-
viewport preset.
|
|
370
|
-
- **`plugin-demo`** — Reference implementation showing every plugin API
|
|
371
|
-
hook: viewport presets, `nav` sweep-op (navbar walker), `beforeShoot`/
|
|
372
|
-
`afterShoot` sidecar augmentation, and flag help.
|
|
373
|
-
|
|
374
|
-
---
|
|
375
|
-
|
|
376
|
-
## All CLI flags
|
|
377
|
-
|
|
378
|
-
| Flag | Type | Default | Description |
|
|
379
|
-
|---|---|---|---|
|
|
380
|
-
| `--preset` | string | `desktop-1280` | Named viewport preset |
|
|
381
|
-
| `--width` / `--height` | number | — | Custom viewport size |
|
|
382
|
-
| `--dpr` | number | `1` | Device pixel ratio |
|
|
383
|
-
| `--cursor` | string | `default` | `pointer`, `grab`, `crosshair`, `none` |
|
|
384
|
-
| `--grid` | bool | `false` | Overlay grid |
|
|
385
|
-
| `--grid-tile` | number | `64` | Grid tile size (px) |
|
|
386
|
-
| `--grid-color` | string | `rgba(255,0,255,0.35)` | Grid line color |
|
|
387
|
-
| `--layer` | string | `all` | `entity`, `terrain`, `hud`, `ui` |
|
|
388
|
-
| `--no-animation` | bool | `false` | Freeze CSS animations |
|
|
389
|
-
| `--no-hud` | bool | `false` | Hide HUD elements |
|
|
390
|
-
| `--wait-frame` | number | `600` | Wait ms for canvas stability |
|
|
391
|
-
| `--zoom` | number | `1` | Zoom level |
|
|
392
|
-
| `--panX` / `--panY` | number | `0` | Camera pan offset (px) |
|
|
393
|
-
| `--full` | bool | `false` | Full-page (not just viewport) |
|
|
394
|
-
| `--tags` | string | — | Comma-separated WCAG tags for audit |
|
|
395
|
-
| `--plugin` | path | — | Load a plugin file (repeatable) |
|
|
396
|
-
| `@file` | prefix | — | Read next arg from file |
|
|
397
|
-
|
|
398
|
-
---
|
|
399
|
-
|
|
400
|
-
## Library API
|
|
401
|
-
|
|
402
|
-
All functions available as named or default import:
|
|
403
|
-
|
|
404
|
-
```js
|
|
405
|
-
import { runShoot, runSweep, runAudit, captureDomSnapshot } from "pursor";
|
|
406
|
-
// Or:
|
|
407
|
-
import PurrVisual from "pursor";
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Capture functions
|
|
411
|
-
|
|
412
|
-
| Function | Returns | Never throws |
|
|
413
|
-
|---|---|---|
|
|
414
|
-
| `runShoot({url, out, flags?, prepare?, browser?})` | `{ url, out, ts, status, title, viewport, flags, error? }` | ✅ |
|
|
415
|
-
| `runShot(url, out, opts?)` | `{ url, out, status, title, fullPage }` | — |
|
|
416
|
-
| `runProbe(url)` | `{ url, status, title, navError, viewport }` | — |
|
|
417
|
-
| `runFrames({url, count?, intervalMs?, outDir?, flags?, browser?})` | `{ url, files[], viewport, ... }` | — |
|
|
418
|
-
| `runHover({url, selector, out, flags?})` | `{ url, out, selector, viewport, ... }` | — |
|
|
419
|
-
| `runDiff(url, refPath, out, threshold?, browser?)` | `{ url, refPath, numDiff, diffPct, equal, error? }` | — |
|
|
420
|
-
| `runWait(url, selector, timeoutMs?)` | `{ url, selector, found, timeoutMs }` | — |
|
|
421
|
-
| `runClick(url, selector, out?)` | `{ url, selector, clicked, out }` | — |
|
|
422
|
-
| `runType(url, selector, text, out?)` | `{ url, selector, text, typed, out }` | — |
|
|
423
|
-
| `runSeq(url, actionsJson, out?)` | `{ url, out, steps[], failed? }` | — |
|
|
424
|
-
| `runEval(url, js, out?)` | `{ url, result, out, ... }` | — |
|
|
425
|
-
| `runSweep(planPath, outDir?)` | `{ name, steps[], outDir, ... }` | ✅ (per-step) |
|
|
426
|
-
| `runAudit({url, tags?, outDir?, screenshot?, flags?})` | `{ url, violations, violationSummary, highlightedScreenshot?, ... }` | — |
|
|
427
|
-
| `captureDomSnapshot({url, out, flags?})` | `{ url, title, dom, selectorMap[], styles, viewport }` | — |
|
|
428
|
-
|
|
429
|
-
### Viewport helpers
|
|
430
|
-
|
|
431
|
-
| Export | Description |
|
|
432
|
-
|---|---|
|
|
433
|
-
| `listViewports()` | All registered presets (built-in + plugin) |
|
|
434
|
-
| `resolveViewport(flags)` | Resolve `--preset` / `--width` / `--height` to viewport object |
|
|
435
|
-
| `VIEWPORTS` | Built-in preset map |
|
|
436
|
-
| `applyCamera(page, opts)` | Zoom/pan via mouse wheel + drag on canvas |
|
|
437
|
-
| `waitForStableFrame(page, ms)` | Poll canvas until stable for `ms` |
|
|
438
|
-
|
|
439
|
-
### Plugin system
|
|
440
|
-
|
|
441
|
-
| Export | Description |
|
|
442
|
-
|---|---|
|
|
443
|
-
| `loadPlugins(paths?)` | Auto-load built-in plugins + user paths |
|
|
444
|
-
| `registerPlugin(plugin)` | Register a plugin manually |
|
|
445
|
-
| `listPlugins()` | Names of loaded plugins |
|
|
446
|
-
| `getSweepOp(name)` | Get a registered sweep operation |
|
|
447
|
-
| `getViewportPreset(name)` | Get a registered viewport preset |
|
|
448
|
-
| `listViewportPresets()` | All plugin-registered presets |
|
|
449
|
-
| `getFlagHelp()` | All plugin-registered flag descriptions |
|
|
450
|
-
|
|
451
|
-
### Selector healing
|
|
452
|
-
|
|
453
|
-
| Export | Description |
|
|
454
|
-
|---|---|
|
|
455
|
-
| `resolveHealedSelector(page, selector, opts?)` | Try selector chain, return first visible match |
|
|
456
|
-
| `healStepAction(page, action)` | Mutate action.selector → resolved selector |
|
|
457
|
-
|
|
458
|
-
### CI output
|
|
459
|
-
|
|
460
|
-
| Export | Description |
|
|
461
|
-
|---|---|
|
|
462
|
-
| `writeCiOutput(summary, dir)` | Write JUnit XML + GitHub annotations + Markdown |
|
|
463
|
-
|
|
464
|
-
### MCP Server
|
|
465
|
-
|
|
466
|
-
| Export | Description |
|
|
467
|
-
|---|---|
|
|
468
|
-
| `PurrVisualMCPServer` | MCP stdio server class |
|
|
469
|
-
| `loadMcpConfig()` | Load config from env or `~/.pursor/mcp-config.json` |
|
|
470
|
-
| `MCP_VERSION` | MCP protocol version string |
|
|
471
|
-
|
|
472
|
-
### Low-level (plugin authors)
|
|
473
|
-
|
|
474
|
-
| Export | Source |
|
|
475
|
-
|---|---|
|
|
476
|
-
| `launch()` / `newPage(browser, viewport)` | `runway.js` |
|
|
477
|
-
| `resolveLocator(page, selector)` / `parseTextSelector(s)` | `selector.js` |
|
|
478
|
-
| `parseFlags(argv)` / `asNum(v, dflt)` / `asBool(v, dflt)` | `util.js` |
|
|
479
|
-
| `nowIso()` / `shortHash(buf)` / `escapeHtml(s)` | `util.js` |
|
|
480
|
-
| `readArg(arg)` / `makeOut(name)` / `findStepPng(dir, name)` | `util.js` |
|
|
481
|
-
| `renderSweepHtml(summary)` | `util.js` |
|
|
482
|
-
|
|
483
|
-
### Subpath exports
|
|
484
|
-
|
|
485
|
-
```js
|
|
486
|
-
import { resolveLocator } from "pursor/selector";
|
|
487
|
-
import { launch } from "pursor/runway";
|
|
488
|
-
import { parseFlags } from "pursor/util";
|
|
489
|
-
import { overlayGrid } from "pursor/overlays";
|
|
490
|
-
import { captureDomSnapshot } from "pursor/dom-snapshot";
|
|
491
|
-
import { runAudit } from "pursor/plugin-audit";
|
|
492
|
-
import { resolveHealedSelector } from "pursor/selector-heal";
|
|
493
|
-
import { writeCiOutput } from "pursor/ci-output";
|
|
494
|
-
import { PurrVisualMCPServer } from "pursor/mcp";
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## Sidecar JSON
|
|
500
|
-
|
|
501
|
-
Every capture writes a `.json` sidecar next to its PNG with metadata
|
|
502
|
-
(url, viewport, flags, timestamp, file size, SHA1 hash). DOM snapshots
|
|
503
|
-
write `.dom.json` with full element map. Audit reports write full
|
|
504
|
-
axe-core results to `audit.json`.
|
|
505
|
-
|
|
506
|
-
---
|
|
507
|
-
|
|
508
|
-
## Development
|
|
509
|
-
|
|
510
|
-
```bash
|
|
511
|
-
git clone <this repo>
|
|
512
|
-
cd pursor
|
|
513
|
-
npm install
|
|
514
|
-
npm install --save-dev playwright-core
|
|
515
|
-
npm test
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
All 32 tests use Node's built-in test runner. Coverage: unit tests for
|
|
519
|
-
viewport resolution, flag parsing, selector parsing, HTML escaping, hashing,
|
|
520
|
-
and end-to-end smoke tests for the full CLI pipeline.
|
|
521
|
-
|
|
522
|
-
```
|
|
523
|
-
src/ — 22 modules
|
|
524
|
-
test/ — 32 tests, 0 failures
|
|
525
|
-
plugins/ — 2 built-in plugins, auto-loaded
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
---
|
|
529
|
-
|
|
530
|
-
## Visual Regression Baselines
|
|
531
|
-
|
|
532
|
-
```bash
|
|
533
|
-
# Save a baseline (computed id from url+viewport+flags)
|
|
534
|
-
pursor baseline save myapp ./out/shoot.png home --url https://my.app
|
|
535
|
-
|
|
536
|
-
# Approve a new baseline from a current capture
|
|
537
|
-
pursor baseline approve myapp ./out/shoot.png home --url https://my.app
|
|
538
|
-
|
|
539
|
-
# List all baselines
|
|
540
|
-
pursor baseline list myapp
|
|
541
|
-
|
|
542
|
-
# Show a specific baseline
|
|
543
|
-
pursor baseline show myapp home --url https://my.app
|
|
544
|
-
```
|
|
545
|
-
|
|
546
|
-
Baselines are stored under `~/.pursor/baselines/<project>/<id>/`. Override
|
|
547
|
-
with `PURSOR_BASELINES_DIR`. The `id` is a 16-char SHA1 prefix of
|
|
548
|
-
`url|viewport|flags` so re-running a sweep maps to the same slot
|
|
549
|
-
deterministically. Use in code:
|
|
550
|
-
|
|
551
|
-
```js
|
|
552
|
-
import { diffKey, saveBaseline, loadBaseline } from "pursor/baseline";
|
|
553
|
-
const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
|
|
554
|
-
saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
## Sweep Plan Validation
|
|
558
|
-
|
|
559
|
-
```bash
|
|
560
|
-
pursor validate ./plan.json
|
|
561
|
-
# { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
Catches: empty steps, unknown ops, out-of-range numbers, duplicate names,
|
|
565
|
-
missing required fields. `pursor sweep` runs the same validator before
|
|
566
|
-
executing — fail-fast.
|
|
567
|
-
|
|
568
|
-
## MCP Resources
|
|
569
|
-
|
|
570
|
-
The MCP server now exposes `resources/list` and `resources/read` so
|
|
571
|
-
Claude Code / Cursor can browse past captures:
|
|
572
|
-
|
|
573
|
-
```
|
|
574
|
-
uri: pursor://shoot/<url|preset>
|
|
575
|
-
uri: pursor://sweep/<plan-name>
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override
|
|
579
|
-
with `PURSOR_MCP_STATE`) and re-listed on every server start.
|
|
580
|
-
|
|
581
|
-
## Library additions (v0.2.0)
|
|
582
|
-
|
|
583
|
-
| Export | Description |
|
|
584
|
-
|---|---|
|
|
585
|
-
| `diffKey`, `saveBaseline`, `loadBaseline`, `listBaselines`, `approveBaseline` | Visual regression baseline storage |
|
|
586
|
-
| `validateSweepPlan`, `registerSweepOp` | Sweep plan schema validation |
|
|
587
|
-
| `listResources`, `readResource`, `recordResource` | MCP resource adapter |
|
|
588
|
-
|
|
589
|
-
Subpath exports: `pursor/baseline`, `pursor/sweep-schema`, `pursor/mcp-resources`.
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
##
|
|
594
|
-
|
|
595
|
-
|
|
1
|
+
# pursor
|
|
2
|
+
|
|
3
|
+
> **Visual QA & audit CLI + library + MCP server for the browser.**
|
|
4
|
+
> Capture, diff, sweep, and audit any web target — with multi-viewport,
|
|
5
|
+
> layered states, hover, grid overlays, animation freeze, camera control,
|
|
6
|
+
> axe-core accessibility audit, CI output, and auto-healing selectors.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx pursor probe https://example.com
|
|
10
|
+
npx pursor shoot https://example.com --preset mobile-375 --grid
|
|
11
|
+
npx pursor sweep ./plan.json
|
|
12
|
+
npx pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
13
|
+
npx pursor-mcp # MCP stdio server for Claude Code / Cursor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install pursor
|
|
20
|
+
npm install --save-dev playwright-core # peer dep — bring your own Chrome
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`pursor` does **not** bundle Chromium. It drives your system Chrome via
|
|
24
|
+
Playwright. No extra browser downloads.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Table of Contents
|
|
29
|
+
|
|
30
|
+
- [CLI](#cli)
|
|
31
|
+
- [MCP Server](#mcp-server)
|
|
32
|
+
- [Accessibility Audit](#accessibility-audit)
|
|
33
|
+
- [DOM Snapshot](#dom-snapshot)
|
|
34
|
+
- [CI Output](#ci-output)
|
|
35
|
+
- [Auto-heal Selectors](#auto-heal-selectors)
|
|
36
|
+
- [Sweep Plans](#sweep-plans)
|
|
37
|
+
- [Plugin API](#plugin-api)
|
|
38
|
+
- [Library API](#library-api)
|
|
39
|
+
- [Development](#development)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## CLI
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Health check
|
|
47
|
+
pursor probe https://example.com
|
|
48
|
+
|
|
49
|
+
# Screenshot (simple)
|
|
50
|
+
pursor shot https://example.com ./out/shot.png
|
|
51
|
+
|
|
52
|
+
# Rich capture: viewport preset + cursor + grid
|
|
53
|
+
pursor shoot https://example.com \
|
|
54
|
+
--preset desktop-1280 \
|
|
55
|
+
--cursor crosshair \
|
|
56
|
+
--grid --grid-tile 64
|
|
57
|
+
|
|
58
|
+
# Isolate a layer (entity / terrain / hud / ui)
|
|
59
|
+
pursor layer https://example.com entity
|
|
60
|
+
|
|
61
|
+
# Animation timeline: 8 frames at 200ms
|
|
62
|
+
pursor frames https://example.com 8 200 ./frames/
|
|
63
|
+
|
|
64
|
+
# Hover an element
|
|
65
|
+
pursor hover https://example.com "text=Login"
|
|
66
|
+
|
|
67
|
+
# Pixel diff vs a reference screenshot
|
|
68
|
+
pursor diff https://example.com ./ref.png ./out/diff.png
|
|
69
|
+
|
|
70
|
+
# Batched plan (see plans/ for examples)
|
|
71
|
+
pursor sweep ./plan.json
|
|
72
|
+
|
|
73
|
+
# Accessibility audit (requires: npm i axe-core)
|
|
74
|
+
pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
75
|
+
|
|
76
|
+
# DOM + selector map snapshot
|
|
77
|
+
pursor dom https://example.com
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Subcommands
|
|
81
|
+
|
|
82
|
+
| Subcommand | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `probe` | Health check (HTTP status, page title) |
|
|
85
|
+
| `shot` / `full` | Viewport / full-page screenshot |
|
|
86
|
+
| `eval` | Execute JS in the page, return result |
|
|
87
|
+
| `click` / `type` / `wait` / `seq` | Interaction primitives |
|
|
88
|
+
| `diff` | Pixel-level diff vs a reference PNG |
|
|
89
|
+
| `viewports` | List all registered viewport presets |
|
|
90
|
+
| `shoot` | Rich capture (overlays, freeze, camera, plugins) |
|
|
91
|
+
| `layer` | Capture one isolated layer (entity/hud/ui/terrain) |
|
|
92
|
+
| `frames` | N-frame animation timeline at interval |
|
|
93
|
+
| `hover` | Hover state capture |
|
|
94
|
+
| `sweep` | Batched capture plan → HTML report + CI output |
|
|
95
|
+
| `audit` | ⭐ axe-core WCAG accessibility audit + highlighted screenshot |
|
|
96
|
+
| `dom` / `dom-snapshot` | ⭐ Serialized DOM + CSS selectors + XPath + bounding rects |
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## MCP Server
|
|
101
|
+
|
|
102
|
+
`pursor-mcp` exposes every capability as MCP tools over stdio —
|
|
103
|
+
works with Claude Code, Cursor, Continue, and any MCP host.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx pursor-mcp
|
|
107
|
+
# or with verbose logging:
|
|
108
|
+
npx pursor-mcp --verbose
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Exposed Tools
|
|
112
|
+
|
|
113
|
+
| Tool | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `pursor_shoot` | Full screenshot with viewport, grid, layer, cursor, camera, freeze |
|
|
116
|
+
| `pursor_diff` | Pixel diff vs reference PNG + diff overlay |
|
|
117
|
+
| `pursor_sweep` | Execute a batch plan JSON → summary |
|
|
118
|
+
| `pursor_frames` | Animation frame timeline |
|
|
119
|
+
| `pursor_probe` | Health-check a URL |
|
|
120
|
+
| `pursor_audit` | axe-core accessibility audit |
|
|
121
|
+
| `pursor_dom_snapshot` | DOM + CSSOM + selector map + bounding rects |
|
|
122
|
+
|
|
123
|
+
### Config
|
|
124
|
+
|
|
125
|
+
Config via `PURSOR_MCP_CONFIG` env var (inline JSON or file path)
|
|
126
|
+
or `~/.pursor/mcp-config.json`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"plugins": ["./my-plugin.js"],
|
|
131
|
+
"defaultOutDir": "./mcp-output",
|
|
132
|
+
"verbose": true
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### MCP Host Examples
|
|
137
|
+
|
|
138
|
+
**Claude Code:**
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"mcpServers": {
|
|
142
|
+
"pursor": {
|
|
143
|
+
"command": "npx",
|
|
144
|
+
"args": ["pursor-mcp"]
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Cursor:**
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"pursor": {
|
|
155
|
+
"command": "npx",
|
|
156
|
+
"args": ["pursor-mcp", "--verbose"]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Accessibility Audit
|
|
165
|
+
|
|
166
|
+
Run axe-core WCAG audits on any URL. Optionally captures a highlighted
|
|
167
|
+
screenshot with violated elements outlined in red.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Quick audit with default tags (wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice)
|
|
171
|
+
pursor audit https://example.com
|
|
172
|
+
|
|
173
|
+
# Specific WCAG tags
|
|
174
|
+
pursor audit https://example.com --tags wcag2a,wcag2aa
|
|
175
|
+
|
|
176
|
+
# Custom output directory
|
|
177
|
+
pursor audit https://example.com ./audit-report/
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Output:**
|
|
181
|
+
- `audit.json` — full axe-core results with violation summary
|
|
182
|
+
- `audit-summary.md` — readable Markdown report with severity breakdown
|
|
183
|
+
- `audit-highlighted.png` — screenshot with violations visibly marked
|
|
184
|
+
|
|
185
|
+
**Sweep plan usage:**
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"name": "accessibility-check",
|
|
189
|
+
"base": "https://example.com",
|
|
190
|
+
"steps": [
|
|
191
|
+
{
|
|
192
|
+
"name": "wcag-audit",
|
|
193
|
+
"audit": {
|
|
194
|
+
"tags": "wcag2a,wcag2aa,wcag21aa",
|
|
195
|
+
"screenshot": true
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## DOM Snapshot
|
|
205
|
+
|
|
206
|
+
Every capture can optionally produce a `.dom.json` sidecar with complete
|
|
207
|
+
page structure — useful for debugging visual regression without opening
|
|
208
|
+
a browser.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
pursor dom https://example.com
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Captured data:**
|
|
215
|
+
- `dom` — `document.documentElement.outerHTML`
|
|
216
|
+
- `selectorMap[]` — every visible element with:
|
|
217
|
+
- `tag`, `id`, `css` (CSS selector), `xpath`
|
|
218
|
+
- `role`, `ariaLabel`, `ariaRole`, `text`, `placeholder`, `alt`, `href`, `src`
|
|
219
|
+
- `rect` — viewport-relative bounding box `{x, y, w, h}`
|
|
220
|
+
- `visible` — visibility flag
|
|
221
|
+
- `styles` — computed stylesheet rules keyed by selector
|
|
222
|
+
- `viewport` — current viewport dimensions + DPR
|
|
223
|
+
|
|
224
|
+
**Programmatic:**
|
|
225
|
+
```js
|
|
226
|
+
import { captureDomSnapshot } from "pursor";
|
|
227
|
+
|
|
228
|
+
const snapshot = await captureDomSnapshot({
|
|
229
|
+
url: "https://example.com",
|
|
230
|
+
out: "./snapshot.dom.json",
|
|
231
|
+
});
|
|
232
|
+
console.log(snapshot.selectorMap.length, "elements found");
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## CI Output
|
|
238
|
+
|
|
239
|
+
Sweep plans automatically generate CI-compatible output files alongside
|
|
240
|
+
the HTML report — no extra config needed.
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
pursor sweep ./plan.json
|
|
244
|
+
# Produces in the output directory:
|
|
245
|
+
# sweep.json — raw summary
|
|
246
|
+
# index.html — visual HTML dashboard
|
|
247
|
+
# sweep.junit.xml — JUnit XML (GitLab CI, Jenkins, CircleCI)
|
|
248
|
+
# sweep.github.json — GitHub Actions annotations format
|
|
249
|
+
# sweep.md — Markdown summary
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### GitHub Actions integration
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
- name: Visual QA
|
|
256
|
+
run: npx pursor@latest sweep ./plan.json
|
|
257
|
+
- name: Annotate
|
|
258
|
+
uses: actions/github-script@v7
|
|
259
|
+
with:
|
|
260
|
+
script: |
|
|
261
|
+
const fs = require('fs');
|
|
262
|
+
const { annotations } = JSON.parse(fs.readFileSync('sweep-output/sweep.github.json'));
|
|
263
|
+
annotations.forEach(a => core.error(a.message, {file: a.filename, title: a.title}));
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### JUnit in GitLab CI
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
visual-qa:
|
|
270
|
+
script: npx pursor@latest sweep ./plan.json
|
|
271
|
+
artifacts:
|
|
272
|
+
reports:
|
|
273
|
+
junit: sweep-output/sweep.junit.xml
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Auto-heal Selectors
|
|
279
|
+
|
|
280
|
+
In sweep plans, selectors can be an array of fallback strategies.
|
|
281
|
+
pursor tries each one in order until a visible element is found:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"name": "login-flow",
|
|
286
|
+
"base": "https://example.com",
|
|
287
|
+
"steps": [
|
|
288
|
+
{
|
|
289
|
+
"name": "click-login",
|
|
290
|
+
"hover": {
|
|
291
|
+
"selector": [
|
|
292
|
+
"text=Login",
|
|
293
|
+
"button[type=submit]",
|
|
294
|
+
"#login-btn",
|
|
295
|
+
"a[href*='login']"
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Supported selector types:**
|
|
304
|
+
- `text=Login` — Playwright text locator (substring, or `text==Login` for exact)
|
|
305
|
+
- `text~regex` — regex text match
|
|
306
|
+
- `role=button|Submit` — ARIA role with accessible name
|
|
307
|
+
- `aria=label` — accessibility label
|
|
308
|
+
- `placeholder=Email` — placeholder text
|
|
309
|
+
- CSS selectors — any valid CSS selector as fallback
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Sweep Plans
|
|
314
|
+
|
|
315
|
+
Batch capture plans in JSON. Each step runs one operation.
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"name": "checkout-flow",
|
|
320
|
+
"base": "https://example.com",
|
|
321
|
+
"outDir": "./sweep-checkout",
|
|
322
|
+
"steps": [
|
|
323
|
+
{ "name": "homepage", "shoot": { "preset": "desktop-1280", "cursor": "default" } },
|
|
324
|
+
{ "name": "mobile-view","shoot": { "preset": "mobile-375", "grid": true } },
|
|
325
|
+
{ "name": "nav-hover", "hover": { "selector": "text=Products", "settleMs": 400 } },
|
|
326
|
+
{ "name": "add-to-cart","frames": { "count": 6, "intervalMs": 200 } },
|
|
327
|
+
{ "name": "diff", "diff": { "ref": "baseline" } }
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Step operations:** `shoot`, `hover`, `frames`, `diff`, `audit`, or any
|
|
333
|
+
registered plugin sweep-op.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Plugin API
|
|
338
|
+
|
|
339
|
+
Extend `pursor` with custom viewport presets, sweep operations, or
|
|
340
|
+
capture hooks:
|
|
341
|
+
|
|
342
|
+
```js
|
|
343
|
+
export default {
|
|
344
|
+
name: "my-plugin",
|
|
345
|
+
viewport: {
|
|
346
|
+
"my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" },
|
|
347
|
+
},
|
|
348
|
+
sweepOp: {
|
|
349
|
+
lighthouse: async (ctx, opts) => {
|
|
350
|
+
// run lighthouse audit, write result at ctx.out
|
|
351
|
+
return { score: 95 };
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
beforeShoot: async (ctx) => { /* mutate ctx.flags */ },
|
|
355
|
+
afterShoot: async (ctx, meta) => { /* augment sidecar */ },
|
|
356
|
+
};
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
pursor shoot https://example.com --plugin ./my-plugin.js
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Publish as `pursor-plugin-*` for auto-discovery.
|
|
364
|
+
|
|
365
|
+
### Built-in plugins
|
|
366
|
+
|
|
367
|
+
- **`plugin-audit`** — adds `audit` sweep-op (axe-core WCAG audit) and
|
|
368
|
+
`every-viewport` sweep-op (capture at every preset). Adds `audit-canvas`
|
|
369
|
+
viewport preset.
|
|
370
|
+
- **`plugin-demo`** — Reference implementation showing every plugin API
|
|
371
|
+
hook: viewport presets, `nav` sweep-op (navbar walker), `beforeShoot`/
|
|
372
|
+
`afterShoot` sidecar augmentation, and flag help.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## All CLI flags
|
|
377
|
+
|
|
378
|
+
| Flag | Type | Default | Description |
|
|
379
|
+
|---|---|---|---|
|
|
380
|
+
| `--preset` | string | `desktop-1280` | Named viewport preset |
|
|
381
|
+
| `--width` / `--height` | number | — | Custom viewport size |
|
|
382
|
+
| `--dpr` | number | `1` | Device pixel ratio |
|
|
383
|
+
| `--cursor` | string | `default` | `pointer`, `grab`, `crosshair`, `none` |
|
|
384
|
+
| `--grid` | bool | `false` | Overlay grid |
|
|
385
|
+
| `--grid-tile` | number | `64` | Grid tile size (px) |
|
|
386
|
+
| `--grid-color` | string | `rgba(255,0,255,0.35)` | Grid line color |
|
|
387
|
+
| `--layer` | string | `all` | `entity`, `terrain`, `hud`, `ui` |
|
|
388
|
+
| `--no-animation` | bool | `false` | Freeze CSS animations |
|
|
389
|
+
| `--no-hud` | bool | `false` | Hide HUD elements |
|
|
390
|
+
| `--wait-frame` | number | `600` | Wait ms for canvas stability |
|
|
391
|
+
| `--zoom` | number | `1` | Zoom level |
|
|
392
|
+
| `--panX` / `--panY` | number | `0` | Camera pan offset (px) |
|
|
393
|
+
| `--full` | bool | `false` | Full-page (not just viewport) |
|
|
394
|
+
| `--tags` | string | — | Comma-separated WCAG tags for audit |
|
|
395
|
+
| `--plugin` | path | — | Load a plugin file (repeatable) |
|
|
396
|
+
| `@file` | prefix | — | Read next arg from file |
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Library API
|
|
401
|
+
|
|
402
|
+
All functions available as named or default import:
|
|
403
|
+
|
|
404
|
+
```js
|
|
405
|
+
import { runShoot, runSweep, runAudit, captureDomSnapshot } from "pursor";
|
|
406
|
+
// Or:
|
|
407
|
+
import PurrVisual from "pursor";
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Capture functions
|
|
411
|
+
|
|
412
|
+
| Function | Returns | Never throws |
|
|
413
|
+
|---|---|---|
|
|
414
|
+
| `runShoot({url, out, flags?, prepare?, browser?})` | `{ url, out, ts, status, title, viewport, flags, error? }` | ✅ |
|
|
415
|
+
| `runShot(url, out, opts?)` | `{ url, out, status, title, fullPage }` | — |
|
|
416
|
+
| `runProbe(url)` | `{ url, status, title, navError, viewport }` | — |
|
|
417
|
+
| `runFrames({url, count?, intervalMs?, outDir?, flags?, browser?})` | `{ url, files[], viewport, ... }` | — |
|
|
418
|
+
| `runHover({url, selector, out, flags?})` | `{ url, out, selector, viewport, ... }` | — |
|
|
419
|
+
| `runDiff(url, refPath, out, threshold?, browser?)` | `{ url, refPath, numDiff, diffPct, equal, error? }` | — |
|
|
420
|
+
| `runWait(url, selector, timeoutMs?)` | `{ url, selector, found, timeoutMs }` | — |
|
|
421
|
+
| `runClick(url, selector, out?)` | `{ url, selector, clicked, out }` | — |
|
|
422
|
+
| `runType(url, selector, text, out?)` | `{ url, selector, text, typed, out }` | — |
|
|
423
|
+
| `runSeq(url, actionsJson, out?)` | `{ url, out, steps[], failed? }` | — |
|
|
424
|
+
| `runEval(url, js, out?)` | `{ url, result, out, ... }` | — |
|
|
425
|
+
| `runSweep(planPath, outDir?)` | `{ name, steps[], outDir, ... }` | ✅ (per-step) |
|
|
426
|
+
| `runAudit({url, tags?, outDir?, screenshot?, flags?})` | `{ url, violations, violationSummary, highlightedScreenshot?, ... }` | — |
|
|
427
|
+
| `captureDomSnapshot({url, out, flags?})` | `{ url, title, dom, selectorMap[], styles, viewport }` | — |
|
|
428
|
+
|
|
429
|
+
### Viewport helpers
|
|
430
|
+
|
|
431
|
+
| Export | Description |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `listViewports()` | All registered presets (built-in + plugin) |
|
|
434
|
+
| `resolveViewport(flags)` | Resolve `--preset` / `--width` / `--height` to viewport object |
|
|
435
|
+
| `VIEWPORTS` | Built-in preset map |
|
|
436
|
+
| `applyCamera(page, opts)` | Zoom/pan via mouse wheel + drag on canvas |
|
|
437
|
+
| `waitForStableFrame(page, ms)` | Poll canvas until stable for `ms` |
|
|
438
|
+
|
|
439
|
+
### Plugin system
|
|
440
|
+
|
|
441
|
+
| Export | Description |
|
|
442
|
+
|---|---|
|
|
443
|
+
| `loadPlugins(paths?)` | Auto-load built-in plugins + user paths |
|
|
444
|
+
| `registerPlugin(plugin)` | Register a plugin manually |
|
|
445
|
+
| `listPlugins()` | Names of loaded plugins |
|
|
446
|
+
| `getSweepOp(name)` | Get a registered sweep operation |
|
|
447
|
+
| `getViewportPreset(name)` | Get a registered viewport preset |
|
|
448
|
+
| `listViewportPresets()` | All plugin-registered presets |
|
|
449
|
+
| `getFlagHelp()` | All plugin-registered flag descriptions |
|
|
450
|
+
|
|
451
|
+
### Selector healing
|
|
452
|
+
|
|
453
|
+
| Export | Description |
|
|
454
|
+
|---|---|
|
|
455
|
+
| `resolveHealedSelector(page, selector, opts?)` | Try selector chain, return first visible match |
|
|
456
|
+
| `healStepAction(page, action)` | Mutate action.selector → resolved selector |
|
|
457
|
+
|
|
458
|
+
### CI output
|
|
459
|
+
|
|
460
|
+
| Export | Description |
|
|
461
|
+
|---|---|
|
|
462
|
+
| `writeCiOutput(summary, dir)` | Write JUnit XML + GitHub annotations + Markdown |
|
|
463
|
+
|
|
464
|
+
### MCP Server
|
|
465
|
+
|
|
466
|
+
| Export | Description |
|
|
467
|
+
|---|---|
|
|
468
|
+
| `PurrVisualMCPServer` | MCP stdio server class |
|
|
469
|
+
| `loadMcpConfig()` | Load config from env or `~/.pursor/mcp-config.json` |
|
|
470
|
+
| `MCP_VERSION` | MCP protocol version string |
|
|
471
|
+
|
|
472
|
+
### Low-level (plugin authors)
|
|
473
|
+
|
|
474
|
+
| Export | Source |
|
|
475
|
+
|---|---|
|
|
476
|
+
| `launch()` / `newPage(browser, viewport)` | `runway.js` |
|
|
477
|
+
| `resolveLocator(page, selector)` / `parseTextSelector(s)` | `selector.js` |
|
|
478
|
+
| `parseFlags(argv)` / `asNum(v, dflt)` / `asBool(v, dflt)` | `util.js` |
|
|
479
|
+
| `nowIso()` / `shortHash(buf)` / `escapeHtml(s)` | `util.js` |
|
|
480
|
+
| `readArg(arg)` / `makeOut(name)` / `findStepPng(dir, name)` | `util.js` |
|
|
481
|
+
| `renderSweepHtml(summary)` | `util.js` |
|
|
482
|
+
|
|
483
|
+
### Subpath exports
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
import { resolveLocator } from "pursor/selector";
|
|
487
|
+
import { launch } from "pursor/runway";
|
|
488
|
+
import { parseFlags } from "pursor/util";
|
|
489
|
+
import { overlayGrid } from "pursor/overlays";
|
|
490
|
+
import { captureDomSnapshot } from "pursor/dom-snapshot";
|
|
491
|
+
import { runAudit } from "pursor/plugin-audit";
|
|
492
|
+
import { resolveHealedSelector } from "pursor/selector-heal";
|
|
493
|
+
import { writeCiOutput } from "pursor/ci-output";
|
|
494
|
+
import { PurrVisualMCPServer } from "pursor/mcp";
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Sidecar JSON
|
|
500
|
+
|
|
501
|
+
Every capture writes a `.json` sidecar next to its PNG with metadata
|
|
502
|
+
(url, viewport, flags, timestamp, file size, SHA1 hash). DOM snapshots
|
|
503
|
+
write `.dom.json` with full element map. Audit reports write full
|
|
504
|
+
axe-core results to `audit.json`.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Development
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
git clone <this repo>
|
|
512
|
+
cd pursor
|
|
513
|
+
npm install
|
|
514
|
+
npm install --save-dev playwright-core
|
|
515
|
+
npm test
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
All 32 tests use Node's built-in test runner. Coverage: unit tests for
|
|
519
|
+
viewport resolution, flag parsing, selector parsing, HTML escaping, hashing,
|
|
520
|
+
and end-to-end smoke tests for the full CLI pipeline.
|
|
521
|
+
|
|
522
|
+
```
|
|
523
|
+
src/ — 22 modules
|
|
524
|
+
test/ — 32 tests, 0 failures
|
|
525
|
+
plugins/ — 2 built-in plugins, auto-loaded
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
## Visual Regression Baselines
|
|
531
|
+
|
|
532
|
+
```bash
|
|
533
|
+
# Save a baseline (computed id from url+viewport+flags)
|
|
534
|
+
pursor baseline save myapp ./out/shoot.png home --url https://my.app
|
|
535
|
+
|
|
536
|
+
# Approve a new baseline from a current capture
|
|
537
|
+
pursor baseline approve myapp ./out/shoot.png home --url https://my.app
|
|
538
|
+
|
|
539
|
+
# List all baselines
|
|
540
|
+
pursor baseline list myapp
|
|
541
|
+
|
|
542
|
+
# Show a specific baseline
|
|
543
|
+
pursor baseline show myapp home --url https://my.app
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Baselines are stored under `~/.pursor/baselines/<project>/<id>/`. Override
|
|
547
|
+
with `PURSOR_BASELINES_DIR`. The `id` is a 16-char SHA1 prefix of
|
|
548
|
+
`url|viewport|flags` so re-running a sweep maps to the same slot
|
|
549
|
+
deterministically. Use in code:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
import { diffKey, saveBaseline, loadBaseline } from "pursor/baseline";
|
|
553
|
+
const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
|
|
554
|
+
saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Sweep Plan Validation
|
|
558
|
+
|
|
559
|
+
```bash
|
|
560
|
+
pursor validate ./plan.json
|
|
561
|
+
# { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
Catches: empty steps, unknown ops, out-of-range numbers, duplicate names,
|
|
565
|
+
missing required fields. `pursor sweep` runs the same validator before
|
|
566
|
+
executing — fail-fast.
|
|
567
|
+
|
|
568
|
+
## MCP Resources
|
|
569
|
+
|
|
570
|
+
The MCP server now exposes `resources/list` and `resources/read` so
|
|
571
|
+
Claude Code / Cursor can browse past captures:
|
|
572
|
+
|
|
573
|
+
```
|
|
574
|
+
uri: pursor://shoot/<url|preset>
|
|
575
|
+
uri: pursor://sweep/<plan-name>
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override
|
|
579
|
+
with `PURSOR_MCP_STATE`) and re-listed on every server start.
|
|
580
|
+
|
|
581
|
+
## Library additions (v0.2.0)
|
|
582
|
+
|
|
583
|
+
| Export | Description |
|
|
584
|
+
|---|---|
|
|
585
|
+
| `diffKey`, `saveBaseline`, `loadBaseline`, `listBaselines`, `approveBaseline` | Visual regression baseline storage |
|
|
586
|
+
| `validateSweepPlan`, `registerSweepOp` | Sweep plan schema validation |
|
|
587
|
+
| `listResources`, `readResource`, `recordResource` | MCP resource adapter |
|
|
588
|
+
|
|
589
|
+
Subpath exports: `pursor/baseline`, `pursor/sweep-schema`, `pursor/mcp-resources`.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## HAR Capture (v0.3.0)
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
# Capture network traffic during a shoot
|
|
597
|
+
pursor shoot https://example.com shot.png --har ./out/req.har.json
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
In code:
|
|
601
|
+
|
|
602
|
+
```js
|
|
603
|
+
import { startHarCapture, stopHarCapture, writeHar } from "pursor/har";
|
|
604
|
+
const state = await startHarCapture(page);
|
|
605
|
+
await page.goto(url);
|
|
606
|
+
// ... your capture logic ...
|
|
607
|
+
const har = stopHarCapture(page);
|
|
608
|
+
await writeHar(har, "./out/req.har.json");
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
Output is HAR 1.2 spec — pipe to har-cli, perf-tools, or any visualizer.
|
|
612
|
+
|
|
613
|
+
## Auth State (v0.3.0)
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
# Save a state file you exported from Playwright/DevTools
|
|
617
|
+
pursor auth save myapp admin --from ./playwright-state.json
|
|
618
|
+
|
|
619
|
+
# Use it in a shoot
|
|
620
|
+
pursor shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp
|
|
621
|
+
|
|
622
|
+
# Inspect / delete
|
|
623
|
+
pursor auth list myapp
|
|
624
|
+
pursor auth load myapp admin --out ./round-trip.json
|
|
625
|
+
pursor auth delete myapp admin
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
States live in `~/.pursor/auth/<project>/<name>.json` (override with `PURSOR_AUTH_DIR`).
|
|
629
|
+
The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
|
|
630
|
+
|
|
631
|
+
## Parallel Sweep (v0.3.0)
|
|
632
|
+
|
|
633
|
+
Add `parallel: N` to your plan to run steps concurrently in a worker pool:
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"name": "matrix",
|
|
638
|
+
"base": "https://my.app",
|
|
639
|
+
"parallel": 4,
|
|
640
|
+
"steps": [
|
|
641
|
+
{ "name": "home", "shoot": { "preset": "desktop-1280" } },
|
|
642
|
+
{ "name": "pricing", "shoot": { "preset": "desktop-1280" } },
|
|
643
|
+
{ "name": "docs", "shoot": { "preset": "desktop-1280" } }
|
|
644
|
+
]
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
Steps run in a shared browser context; results are still ordered by index in the summary.
|
|
649
|
+
Defaults to serial (`parallel: 1`) — opt in only when steps are independent.
|
|
650
|
+
|
|
651
|
+
---
|
|
652
|
+
## License
|
|
653
|
+
|
|
654
|
+
MIT
|