pursor 0.2.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/LICENSE +21 -0
- package/README.md +595 -0
- package/bin/pursor-mcp.mjs +21 -0
- package/bin/pursor.mjs +191 -0
- package/package.json +73 -0
- package/plans/m5.4-polish.json +22 -0
- package/plugins/plugin-audit.js +57 -0
- package/plugins/plugin-demo.js +63 -0
- package/src/baseline.js +126 -0
- package/src/ci-output.js +156 -0
- package/src/diff.js +48 -0
- package/src/dom-snapshot.js +192 -0
- package/src/eval.js +18 -0
- package/src/every-viewport.js +51 -0
- package/src/frames.js +34 -0
- package/src/hover.js +26 -0
- package/src/index.js +90 -0
- package/src/interact.js +138 -0
- package/src/mcp-resources.js +111 -0
- package/src/mcp.js +436 -0
- package/src/overlays.js +170 -0
- package/src/plugin-audit.js +260 -0
- package/src/plugin.js +121 -0
- package/src/probe.js +20 -0
- package/src/runway.js +63 -0
- package/src/selector-heal.js +85 -0
- package/src/selector.js +39 -0
- package/src/shoot.js +62 -0
- package/src/shot.js +18 -0
- package/src/sweep-schema.js +70 -0
- package/src/sweep.js +105 -0
- package/src/util.js +188 -0
- package/src/viewport.js +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PurrVisual
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,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
|
+
## License
|
|
594
|
+
|
|
595
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @purr/visual — MCP server binary.
|
|
3
|
+
//
|
|
4
|
+
// Runs the pursor MCP stdio server, exposing all capture/audit/sweep
|
|
5
|
+
// capabilities as MCP tools for Claude Code, Cursor, Continue, etc.
|
|
6
|
+
//
|
|
7
|
+
// Usage: pursor-mcp
|
|
8
|
+
// Config via PURSOR_MCP_CONFIG env or ~/.pursor/mcp-config.json
|
|
9
|
+
//
|
|
10
|
+
// echo '{"url":"https://example.com"}' | pursor-mcp
|
|
11
|
+
|
|
12
|
+
import { PursorMCPServer, loadConfig } from "../src/mcp.js";
|
|
13
|
+
|
|
14
|
+
const config = loadConfig();
|
|
15
|
+
|
|
16
|
+
// Verbose mode: --verbose or debug env
|
|
17
|
+
const verbose = process.argv.includes("--verbose") || !!process.env.PURSOR_DEBUG;
|
|
18
|
+
config.verbose = verbose;
|
|
19
|
+
|
|
20
|
+
const server = new PursorMCPServer(config);
|
|
21
|
+
await server.start();
|