pursr 0.5.0 → 0.6.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 CHANGED
@@ -1,471 +1,549 @@
1
- <!-- PROJECT_LOGO_START -->
2
- <p align="center">
3
- <img src="assets/social-preview.svg" alt="pursr - visual QA, audit, and MCP for the browser" width="100%">
4
- </p>
5
-
6
- <p align="center">
7
- <img src="assets/logo.svg" alt="pursr" width="320">
8
- </p>
9
-
10
- <h1 align="center">pursr</h1>
11
-
12
- <p align="center">
13
- <strong>Visual QA, audit, and MCP for the browser.</strong><br>
14
- Capture - sweep - diff - audit - repeat - from the CLI, an MCP server, or as a library.
15
- </p>
16
-
17
- <p align="center">
18
- <a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/v/pursr.svg?style=for-the-badge&color=FF2EA6" alt="npm version"></a>
19
- <a href="https://github.com/0xheycat/pursr/blob/main/LICENSE"><img src="https://img.shields.io/github/license/0xheycat/pursr.svg?style=for-the-badge" alt="license"></a>
20
- <a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/dm/pursr.svg?style=for-the-badge" alt="npm downloads"></a>
21
- <a href="https://github.com/0xheycat/pursr/actions"><img src="https://img.shields.io/github/actions/workflow/status/0xheycat/pursr/ci.yml?style=for-the-badge" alt="CI"></a>
22
- <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/pursr.svg?style=for-the-badge" alt="node"></a>
23
- </p>
24
-
25
- <p align="center">
26
- <a href="#install">Install</a> &middot; <a href="#30-seconds">30 seconds</a> &middot; <a href="#cli">CLI</a> &middot; <a href="#mcp-server">MCP</a> &middot; <a href="#library-api">Library</a> &middot; <a href="#plugins">Plugins</a> &middot; <a href="#roadmap">Roadmap</a>
27
- </p>
28
-
29
- ---
30
-
31
- ## Why pursr?
32
-
33
- Most teams need **four separate tools** to do visual QA: a screenshot CLI, a regression diff runner, an accessibility auditor, and a way to share captures with an AI assistant. **pursr is all four** - built as a single Node.js package with:
34
-
35
- - **A unified CLI** (`pursr`) for every capture, diff, sweep, and audit.
36
- - **An MCP stdio server** (`pursr-mcp`) so Claude Code, Cursor, and Continue can take screenshots, run sweeps, and inspect prior captures as MCP resources.
37
- - **A library** with 30+ named exports and 16 subpath modules, so you can embed it in your own tooling.
38
- - **A plugin system** for custom viewports, sweep ops, and capture hooks.
39
- - **Zero browser bundled** - drives your system Chrome via Playwright. No 200 MB Chromium download.
40
-
41
- ## Install
42
-
43
- ```bash
44
- npm install pursr
45
- npm install --save-dev playwright-core # peer dep - bring your own Chrome
46
- ```
47
-
48
- Then verify:
49
-
50
- ```bash
51
- pursr viewports # list 10+ registered viewport presets
52
- pursr probe https://example.com # health check
53
- ```
54
-
55
- ## 30 seconds
56
-
57
- ```bash
58
- # 1. Capture a screenshot with overlays
59
- pursr shoot https://example.com shot.png \
60
- --preset desktop-1280 --grid --grid-tile 64
61
-
62
- # 2. Save it as a visual baseline
63
- pursr baseline save myapp shot.png home --url https://example.com
64
-
65
- # 3. Next time you run, compare against the baseline
66
- pursr diff https://example.com \
67
- ~/.pursor/baselines/myapp/<id>/home.png \
68
- diff.png
69
-
70
- # 4. Or: run a batched sweep + a11y audit + parallel workers
71
- pursr sweep ./plan.json # see plans/ for an example
72
- ```
73
-
74
- ## Features
75
-
76
- | Feature | Description | CLI flag |
77
- | --- | --- | --- |
78
- | Multi-viewport capture | 10+ presets (mobile, tablet, desktop, ultrawide) | `--preset mobile-375` |
79
- | Layered states | entity / terrain / hud / ui isolation | `--layer entity` |
80
- | Animation freeze | pause CSS/JS animations for stable frames | `--no-animation` |
81
- | Cursor overlay | pointer / grab / grabbing / crosshair | `--cursor crosshair` |
82
- | Grid overlay | spacing guides, custom color + tile size | `--grid --grid-tile 64` |
83
- | Camera control | zoom + pan via mouse wheel/drag | `--zoom 1.5 --panX 200` |
84
- | Frame timeline | N captures at intervalMs for animations | `pursr frames <url> 8 200` |
85
- | Hover capture | text=/role=/aria=/placeholder= matchers | `pursr hover <url> "text=Login"` |
86
- | Pixel diff | `pixelmatch` against any reference PNG | `pursr diff <url> <ref>` |
87
- | Visual baselines | save / approve / diff with stable IDs | `pursr baseline save ...` |
88
- | Parallel sweep | opt-in worker pool across independent steps | `{ "parallel": 4 }` |
89
- | Accessibility audit | axe-core WCAG 2.1 AA + highlighted screenshot | `pursr audit <url>` |
90
- | DOM snapshot | serialized HTML + computed styles + selector map | `pursr dom <url>` |
91
- | Sweep plans | JSON-driven batch with per-step ops | `pursr sweep plan.json` |
92
- | HTML report | dark-themed grid of every capture + meta | auto-generated `index.html` |
93
- | CI output | JUnit XML, GitHub Actions annotations, Markdown | written on every sweep |
94
- | Auto-heal selectors | fallback chain + named matchers | `["text=Login", "#login"]` |
95
- | HAR capture | HAR 1.2 spec, written next to your shot | `--har ./req.har.json` |
96
- | Auth state | Playwright storageState, reuse logged-in sessions | `--auth-state admin` |
97
- | Plugins | custom viewports, sweep ops, before/after hooks | `pursr-plugin-*` |
98
- | MCP server | 7 tools + resources/list & resources/read for Claude/Cursor | `npx pursr-mcp` |
99
-
100
- ## CLI
101
-
102
- ```bash
103
- # Health check
104
- pursr probe https://example.com
105
-
106
- # Screenshot (simple)
107
- pursr shot https://example.com ./out/shot.png
108
-
109
- # Rich capture: viewport preset + cursor + grid
110
- pursr shoot https://example.com \
111
- --preset desktop-1280 \
112
- --cursor crosshair \
113
- --grid --grid-tile 64
114
-
115
- # Isolate a layer
116
- pursr layer https://example.com entity
117
-
118
- # Animation timeline
119
- pursr frames https://example.com 8 200 ./frames/
120
-
121
- # Hover an element
122
- pursr hover https://example.com "text=Login"
123
-
124
- # Pixel diff vs reference
125
- pursr diff https://example.com ./ref.png ./out/diff.png
126
-
127
- # Batched plan
128
- pursr sweep ./plan.json
129
-
130
- # Accessibility audit
131
- pursr audit https://example.com --tags wcag2a,wcag2aa
132
-
133
- # DOM + selector map snapshot
134
- pursr dom https://example.com
135
-
136
- # HAR capture during a shoot
137
- pursr shoot https://example.com shot.png --har ./req.har.json
138
-
139
- # Auth state reuse
140
- pursr shoot https://my.app/dashboard shot.png \
141
- --auth-state admin --auth-project myapp
142
-
143
- # Visual baselines
144
- pursr baseline save myapp shot.png home --url https://example.com
145
- pursr baseline list myapp
146
- pursr baseline approve myapp ./new.png home --url https://example.com
147
-
148
- # Plan validation
149
- pursr validate ./plan.json
150
- ```
151
-
152
- ### Subcommands
153
-
154
- | Subcommand | Purpose |
155
- | --- | --- |
156
- | `probe` | Health check (HTTP status, page title) |
157
- | `shot` / `full` | Viewport / full-page screenshot |
158
- | `eval` | Execute JS in the page, return result |
159
- | `click` / `type` / `wait` / `seq` | Interaction primitives |
160
- | `diff` | Pixel-level diff vs a reference PNG |
161
- | `viewports` | List all registered viewport presets |
162
- | `shoot` | Rich capture (overlays, freeze, camera, plugins) |
163
- | `layer` | Capture one isolated layer (entity/hud/ui/terrain) |
164
- | `frames` | N-frame animation timeline at interval |
165
- | `hover` | Hover state capture |
166
- | `sweep` | Batched capture plan -> HTML report + CI output |
167
- | `audit` | axe-core WCAG accessibility audit + highlighted screenshot |
168
- | `dom` / `dom-snapshot` | Serialized DOM + CSS selectors + XPath + bounding rects |
169
- | `every-viewport` | Capture once per preset in parallel (3-wide pool) |
170
- | `baseline` | save / list / approve / show visual baselines |
171
- | `auth` | save / load / list / delete Playwright storageState |
172
- | `validate` | Validate a sweep plan JSON without running it |
173
-
174
- ## MCP Server
175
-
176
- `pursr-mcp` exposes every capability as MCP tools over stdio - works with Claude Code, Cursor, Continue, and any MCP host.
177
-
178
- ```bash
179
- npx pursr-mcp
180
- # or with verbose logging:
181
- npx pursr-mcp --verbose
182
- ```
183
-
184
- ### Exposed Tools
185
-
186
- | Tool | Description |
187
- | --- | --- |
188
- | `pursr_shoot` | Rich screenshot capture (viewport, grid, layer, cursor, camera, animation freeze, HAR) |
189
- | `pursr_diff` | Pixel-diff a URL against a reference PNG |
190
- | `pursr_sweep` | Execute a batch sweep plan |
191
- | `pursr_frames` | Capture an N-frame animation timeline |
192
- | `pursr_probe` | Health-check a URL |
193
- | `pursr_audit` | axe-core WCAG audit + highlighted screenshot |
194
- | `pursr_dom_snapshot` | Full DOM + selector map snapshot |
195
-
196
- ### Exposed Resources
197
-
198
- | URI | Description |
199
- | --- | --- |
200
- | `pursr://shoot/<url|preset>` | Last screenshot PNG (image/png) |
201
- | `pursr://sweep/<plan-name>` | Last sweep summary JSON (application/json) |
202
-
203
- Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override with `PURSOR_MCP_STATE`).
204
-
205
- ## Visual Regression Baselines
206
-
207
- ```bash
208
- pursr baseline save myapp ./out/shoot.png home --url https://my.app
209
- pursr baseline approve myapp ./out/shoot.png home --url https://my.app
210
- pursr baseline list myapp
211
- pursr baseline show myapp home --url https://my.app
212
- ```
213
-
214
- Baselines live under `~/.pursor/baselines/<project>/<id>/<step>.png` + `manifest.json`. Override with `PURSOR_BASELINES_DIR`. The `id` is a 16-char SHA1 prefix of `url|viewport|flags` so re-running a sweep maps to the same slot deterministically.
215
-
216
- ```js
217
- import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
218
- const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
219
- saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });
220
- ```
221
-
222
- ## Sweep Plan Validation
223
-
224
- ```bash
225
- pursr validate ./plan.json
226
- # { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }
227
- ```
228
-
229
- Catches: empty steps, unknown ops, out-of-range numbers, duplicate names, missing required fields. `pursr sweep` runs the same validator before executing - fail-fast.
230
-
231
- ```json
232
- {
233
- "name": "homepage-matrix",
234
- "base": "https://example.com",
235
- "parallel": 4,
236
- "steps": [
237
- { "name": "baseline", "shoot": { "preset": "desktop-1280" } },
238
- { "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
239
- { "name": "tablet", "shoot": { "preset": "tablet-768" } },
240
- { "name": "mobile", "shoot": { "preset": "mobile-375" } },
241
- { "name": "hover-cta", "hover": { "selector": ["text=Get started", "a.btn-primary"] } },
242
- { "name": "audit", "audit": { "tags": "wcag2a,wcag2aa" } },
243
- { "name": "diff", "diff": { "ref": "baseline" } }
244
- ]
245
- }
246
- ```
247
-
248
- ## HAR Capture
249
-
250
- ```bash
251
- pursr shoot https://example.com shot.png --har ./out/req.har.json
252
- ```
253
-
254
- ```js
255
- import { startHarCapture, stopHarCapture, writeHar } from "pursr/har";
256
- const state = await startHarCapture(page);
257
- await page.goto(url);
258
- const har = stopHarCapture(page);
259
- await writeHar(har, "./out/req.har.json");
260
- ```
261
-
262
- Output is HAR 1.2 spec - pipe to `har-cli`, perf-tools, or any visualizer.
263
-
264
- ## Auth State
265
-
266
- ```bash
267
- pursr auth save myapp admin --from ./playwright-state.json
268
- pursr shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp
269
- pursr auth list myapp
270
- pursr auth load myapp admin --out ./round-trip.json
271
- pursr auth delete myapp admin
272
- ```
273
-
274
- States live in `~/.pursor/auth/<project>/<name>.json` (override with `PURSOR_AUTH_DIR`). The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
275
-
276
- ## Parallel Sweep
277
-
278
- Add `parallel: N` to your plan to run steps concurrently in a worker pool:
279
-
280
- ```json
281
- {
282
- "name": "matrix",
283
- "base": "https://my.app",
284
- "parallel": 4,
285
- "steps": [
286
- { "name": "home", "shoot": { "preset": "desktop-1280" } },
287
- { "name": "pricing", "shoot": { "preset": "desktop-1280" } },
288
- { "name": "docs", "shoot": { "preset": "desktop-1280" } }
289
- ]
290
- }
291
- ```
292
-
293
- Steps run in a shared browser context; results are still ordered by index in the summary. Defaults to serial (`parallel: 1`) - opt in only when steps are independent.
294
-
295
- ## Accessibility Audit
296
-
297
- ```bash
298
- pursr audit https://example.com --tags wcag2a,wcag2aa
299
- # Writes: audit.json, audit-summary.md, audit-highlighted.png
300
- ```
301
-
302
- Injects axe-core, runs a configurable tag set (`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `best-practice`), and overlays a red outline on every violating node with the rule id as a label. The summary Markdown includes per-rule failure snippets.
303
-
304
- ## DOM Snapshot
305
-
306
- ```bash
307
- pursr dom https://example.com
308
- # Writes: dom-snapshot-<ts>.dom.json
309
- ```
310
-
311
- Captures serialized HTML, computed CSS for every visible element, and a selector map (`id`, `role`, `accessible name`, `text`, `xpath`, `css selector`, viewport-relative `rect`). Great for regression diffing without re-running a browser.
312
-
313
- ## CI Output
314
-
315
- Every sweep writes three sidecar artifacts alongside `sweep.json`:
316
-
317
- - `sweep.junit.xml` - JUnit XML for Jenkins / GitLab / CircleCI
318
- - `sweep.github.json` - GitHub Actions annotation file
319
- - `sweep.md` - Human-readable Markdown summary with diffs + failures
320
-
321
- ## Library API
322
-
323
- ```js
324
- import {
325
- runProbe, runShot, runShoot, runSweep, runDiff, runAudit,
326
- captureDomSnapshot, resolveHealedSelector,
327
- saveBaseline, diffKey,
328
- startHarCapture, stopHarCapture, writeHar,
329
- loadAuthState,
330
- PursorMCPServer, loadMcpConfig,
331
- validateSweepPlan,
332
- listResources, readResource,
333
- listViewports, resolveViewport, VIEWPORTS,
334
- loadPlugins, registerPlugin, getSweepOp,
335
- VERSION,
336
- } from "pursr";
337
- ```
338
-
339
- ### Subpath exports
340
-
341
- ```js
342
- import { resolveLocator } from "pursr/selector";
343
- import { launch } from "pursr/runway";
344
- import { parseFlags, asNum } from "pursr/util";
345
- import { overlayGrid } from "pursr/overlays";
346
- import { captureDomSnapshot } from "pursr/dom-snapshot";
347
- import { runAudit } from "pursr/plugin-audit";
348
- import { resolveHealedSelector } from "pursr/selector-heal";
349
- import { writeCiOutput } from "pursr/ci-output";
350
- import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
351
- import { validateSweepPlan } from "pursr/sweep-schema";
352
- import { startHarCapture, stopHarCapture } from "pursr/har";
353
- import { saveAuthState, loadAuthState } from "pursr/auth";
354
- import { listResources, readResource } from "pursr/mcp-resources";
355
- import { PursorMCPServer } from "pursr/mcp";
356
- ```
357
-
358
- ## Plugins
359
-
360
- A plugin is a plain ES module that exports a default object:
361
-
362
- ```js
363
- // plugins/my-plugin.js
364
- export default {
365
- name: "my-plugin",
366
- viewport: { "my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" } },
367
- sweepOp: {
368
- lighthouse: async (ctx, opts) => { /* ... */ },
369
- },
370
- beforeShoot: async (ctx) => { /* mutate ctx.flags / ctx.viewport */ },
371
- afterShoot: async (ctx, meta) => { /* augment sidecar */ },
372
- flagHelp: { "my-flag": "what it does" },
373
- };
374
- ```
375
-
376
- Plugins are auto-loaded from `plugins/` (built-in) or via `--plugin <path>`.
377
-
378
- ## Architecture
379
-
380
- ```
381
- src/
382
- index.js - public library entry
383
- mcp.js - MCP stdio server (JSON-RPC 2.0)
384
- shoot.js - runShoot (overlays + camera + frame-stable)
385
- sweep.js - runSweep (validated, parallel pool)
386
- diff.js - pixelmatch wrapper
387
- plugin-audit.js - axe-core injection + highlighted screenshot
388
- dom-snapshot.js - full DOM + CSSOM + selector map
389
- selector-heal.js - auto-heal chain resolver
390
- ci-output.js - JUnit / GitHub / Markdown
391
- baseline.js - visual regression storage
392
- har.js - HAR 1.2 network capture
393
- auth.js - Playwright storageState
394
- sweep-schema.js - plan validator
395
- mcp-resources.js - MCP resources adapter
396
- overlays.js - page-side CSS overlays + camera
397
- runway.js - Playwright launcher + system-Chrome detector
398
- viewport.js - built-in viewport presets
399
- selector.js - text=/role=/aria=/placeholder= parser
400
- plugin.js - plugin registry + hook runner
401
- util.js - flags, args, hashing, HTML escape, renderSweepHtml
402
- every-viewport.js - one shot per preset in parallel
403
- frames.js, hover.js, shot.js, eval.js, probe.js, interact.js
404
- ```
405
-
406
- ## Development
407
-
408
- ```bash
409
- git clone https://github.com/0xheycat/pursr
410
- cd pursr
411
- npm install
412
- npm install --save-dev playwright-core
413
- npm test
414
- ```
415
-
416
- `npm test` runs 53 unit + integration tests (Node's built-in test runner, zero test deps). Coverage includes: viewport resolution, flag parsing, selector parsing, HTML escaping, hashing, baseline storage, sweep-plan validation, MCP resources, HAR 1.2 shape, auth state, and end-to-end CLI smoke tests.
417
-
418
- ```
419
- src/ - 25 modules
420
- test/ - 53 tests, 0 failures
421
- plugins/ - 2 built-in plugins, auto-loaded
422
- ```
423
-
424
- ## Roadmap
425
-
426
- - [x] Visual baselines (save / approve / diff)
427
- - [x] Sweep plan schema validation
428
- - [x] MCP resources (browse past captures from your AI host)
429
- - [x] HAR 1.2 capture
430
- - [x] Auth state (Playwright storageState)
431
- - [x] Parallel sweep workers
432
- - [x] Watch mode (`pursr watch <url>`)
433
- - [x] Component-level snapshot (`pursr snap <selector>`)
434
- - [ ] PDF report export
435
- - [ ] Cloud output adapters (S3 / GCS)
436
- - [ ] AI diff summary (vision model)
437
-
438
- ## Watch Mode (v0.5.0)
439
-
440
- ```bash
441
- # Re-shoot every time a CSS or HTML file changes
442
- pursr watch https://my.app --on src/**/*.css --on src/**/*.html
443
-
444
- # Re-run a sweep plan on file change
445
- pursr watch --plan ./plan.json --on src/**/*.{css,html}
446
-
447
- # Default (no --on) = watch everything in cwd
448
- pursr watch https://my.app
449
- ```
450
-
451
- Glob patterns: * (one path segment), ** (any depth), ? (one char), backslash-X (literal X). Debounce is 300ms by default.
452
-
453
- ## Component Snapshots (v0.5.0)
454
-
455
- ```bash
456
- # Capture one screenshot per matched element
457
- pursr snap https://my.app a.btn --out ./snaps --max 20
458
-
459
- # Use auto-heal selector chain
460
- pursr snap https://my.app "text=Sign up" --out ./snaps
461
-
462
- # Promote to baselines in one command
463
- pursr snap https://my.app article.product --baseline myapp
464
- ```
465
-
466
- Each capture is clipped precisely to the elements bounding box (even when scrolled offscreen), labelled with aria-label / text / tag, and written to ./snaps/<index>-<label>.png + snap.json summary.
467
-
468
- ---
469
- ## License
470
-
471
- MIT (c) 2026 - [0xheycat](https://github.com/0xheycat)
1
+ <!-- PROJECT_LOGO_START -->
2
+ <p align="center">
3
+ <img src="assets/social-preview.svg" alt="pursr - visual QA, audit, and MCP for the browser" width="100%">
4
+ </p>
5
+
6
+ <p align="center">
7
+ <img src="assets/logo.svg" alt="pursr" width="320">
8
+ </p>
9
+
10
+ <h1 align="center">pursr</h1>
11
+
12
+ <p align="center">
13
+ <strong>Visual QA, audit, and MCP for the browser.</strong><br>
14
+ Capture - sweep - diff - audit - repeat - from the CLI, an MCP server, or as a library.
15
+ </p>
16
+
17
+ <p align="center">
18
+ <a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/v/pursr.svg?style=for-the-badge&color=FF2EA6" alt="npm version"></a>
19
+ <a href="https://github.com/0xheycat/pursr/blob/main/LICENSE"><img src="https://img.shields.io/github/license/0xheycat/pursr.svg?style=for-the-badge" alt="license"></a>
20
+ <a href="https://www.npmjs.com/package/pursr"><img src="https://img.shields.io/npm/dm/pursr.svg?style=for-the-badge" alt="npm downloads"></a>
21
+ <a href="https://github.com/0xheycat/pursr/actions"><img src="https://img.shields.io/github/actions/workflow/status/0xheycat/pursr/ci.yml?style=for-the-badge" alt="CI"></a>
22
+ <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/pursr.svg?style=for-the-badge" alt="node"></a>
23
+ </p>
24
+
25
+ <p align="center">
26
+ <a href="#install">Install</a> &middot; <a href="#30-seconds">30 seconds</a> &middot; <a href="#cli">CLI</a> &middot; <a href="#mcp-server">MCP</a> &middot; <a href="#library-api">Library</a> &middot; <a href="#plugins">Plugins</a> &middot; <a href="#roadmap">Roadmap</a>
27
+ </p>
28
+
29
+ ---
30
+
31
+ ## Why pursr?
32
+
33
+ Most teams need **five separate tools** to do visual QA: a screenshot CLI, a regression diff runner, an accessibility auditor, a way to share captures with an AI assistant, and a way to **turn all of that into a PDF report** for stakeholders. **pursr is all five** - built as a single Node.js package with:
34
+
35
+ - **A unified CLI** (`pursr`) for every capture, diff, sweep, and audit.
36
+ - **An MCP stdio server** (`pursr-mcp`) so Claude Code, Cursor, and Continue can take screenshots, run sweeps, and inspect prior captures as MCP resources.
37
+ - **A library** with 34 named exports and 18 subpath modules, so you can embed it in your own tooling.
38
+ - **A plugin system** for custom viewports, sweep ops, and capture hooks.
39
+ - **PDF reports + AI diff summaries** built in - render a sweep to a styled PDF or ask a vision LLM to describe the regression in plain language.
40
+ - **Zero browser bundled** - drives your system Chrome via Playwright. No 200 MB Chromium download.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install pursr
46
+ npm install --save-dev playwright-core # peer dep - bring your own Chrome
47
+ ```
48
+
49
+ Then verify:
50
+
51
+ ```bash
52
+ pursr viewports # list 10+ registered viewport presets
53
+ pursr probe https://example.com # health check
54
+ ```
55
+
56
+ ## 30 seconds
57
+
58
+ ```bash
59
+ # 1. Capture a screenshot with overlays
60
+ pursr shoot https://example.com shot.png \
61
+ --preset desktop-1280 --grid --grid-tile 64
62
+
63
+ # 2. Save it as a visual baseline
64
+ pursr baseline save myapp shot.png home --url https://example.com
65
+
66
+ # 3. Next time you run, compare against the baseline
67
+ pursr diff https://example.com \
68
+ ~/.pursor/baselines/myapp/<id>/home.png \
69
+ diff.png
70
+
71
+ # 4. Or: run a batched sweep + a11y audit + parallel workers
72
+ pursr sweep ./plan.json # see plans/ for an example
73
+ ```
74
+
75
+ ## Features
76
+
77
+ | Feature | Description | CLI flag |
78
+ | --- | --- | --- |
79
+ | Multi-viewport capture | 10+ presets (mobile, tablet, desktop, ultrawide) | `--preset mobile-375` |
80
+ | Layered states | entity / terrain / hud / ui isolation | `--layer entity` |
81
+ | Animation freeze | pause CSS/JS animations for stable frames | `--no-animation` |
82
+ | Cursor overlay | pointer / grab / grabbing / crosshair | `--cursor crosshair` |
83
+ | Grid overlay | spacing guides, custom color + tile size | `--grid --grid-tile 64` |
84
+ | Camera control | zoom + pan via mouse wheel/drag | `--zoom 1.5 --panX 200` |
85
+ | Frame timeline | N captures at intervalMs for animations | `pursr frames <url> 8 200` |
86
+ | Hover capture | text=/role=/aria=/placeholder= matchers | `pursr hover <url> "text=Login"` |
87
+ | Pixel diff | `pixelmatch` against any reference PNG | `pursr diff <url> <ref>` |
88
+ | Visual baselines | save / approve / diff with stable IDs | `pursr baseline save ...` |
89
+ | Parallel sweep | opt-in worker pool across independent steps | `{ "parallel": 4 }` |
90
+ | Accessibility audit | axe-core WCAG 2.1 AA + highlighted screenshot | `pursr audit <url>` |
91
+ | DOM snapshot | serialized HTML + computed styles + selector map | `pursr dom <url>` |
92
+ | Sweep plans | JSON-driven batch with per-step ops | `pursr sweep plan.json` |
93
+ | HTML report | dark-themed grid of every capture + meta | auto-generated `index.html` |
94
+ | CI output | JUnit XML, GitHub Actions annotations, Markdown | written on every sweep |
95
+ | Auto-heal selectors | fallback chain + named matchers | `["text=Login", "#login"]` |
96
+ | HAR capture | HAR 1.2 spec, written next to your shot | `--har ./req.har.json` |
97
+ | Auth state | Playwright storageState, reuse logged-in sessions | `--auth-state admin` |
98
+ | Plugins | custom viewports, sweep ops, before/after hooks | `pursr-plugin-*` |
99
+ | MCP server | 7 tools + resources/list & resources/read for Claude/Cursor | `npx pursr-mcp` |
100
+ | PDF report | render sweep.json to a styled, embedded-PNG A4 PDF | `pursr report --sweep ./sweep.json` |
101
+ | AI diff summary | vision LLM describes the diff in plain language | `pursr diff ... --ai` |
102
+
103
+ ## CLI
104
+
105
+ ```bash
106
+ # Health check
107
+ pursr probe https://example.com
108
+
109
+ # Screenshot (simple)
110
+ pursr shot https://example.com ./out/shot.png
111
+
112
+ # Rich capture: viewport preset + cursor + grid
113
+ pursr shoot https://example.com \
114
+ --preset desktop-1280 \
115
+ --cursor crosshair \
116
+ --grid --grid-tile 64
117
+
118
+ # Isolate a layer
119
+ pursr layer https://example.com entity
120
+
121
+ # Animation timeline
122
+ pursr frames https://example.com 8 200 ./frames/
123
+
124
+ # Hover an element
125
+ pursr hover https://example.com "text=Login"
126
+
127
+ # Pixel diff vs reference
128
+ pursr diff https://example.com ./ref.png ./out/diff.png
129
+
130
+ # Batched plan
131
+ pursr sweep ./plan.json
132
+
133
+ # Accessibility audit
134
+ pursr audit https://example.com --tags wcag2a,wcag2aa
135
+
136
+ # DOM + selector map snapshot
137
+ pursr dom https://example.com
138
+
139
+ # HAR capture during a shoot
140
+ pursr shoot https://example.com shot.png --har ./req.har.json
141
+
142
+ # Auth state reuse
143
+ pursr shoot https://my.app/dashboard shot.png \
144
+ --auth-state admin --auth-project myapp
145
+
146
+ # Visual baselines
147
+ pursr baseline save myapp shot.png home --url https://example.com
148
+ pursr baseline list myapp
149
+ pursr baseline approve myapp ./new.png home --url https://example.com
150
+
151
+ # Plan validation
152
+ pursr validate ./plan.json
153
+ ```
154
+
155
+ ### Subcommands
156
+
157
+ | Subcommand | Purpose |
158
+ | --- | --- |
159
+ | `probe` | Health check (HTTP status, page title) |
160
+ | `shot` / `full` | Viewport / full-page screenshot |
161
+ | `eval` | Execute JS in the page, return result |
162
+ | `click` / `type` / `wait` / `seq` | Interaction primitives |
163
+ | `diff` | Pixel-level diff vs a reference PNG |
164
+ | `viewports` | List all registered viewport presets |
165
+ | `shoot` | Rich capture (overlays, freeze, camera, plugins) |
166
+ | `layer` | Capture one isolated layer (entity/hud/ui/terrain) |
167
+ | `frames` | N-frame animation timeline at interval |
168
+ | `hover` | Hover state capture |
169
+ | `sweep` | Batched capture plan -> HTML report + CI output |
170
+ | `audit` | axe-core WCAG accessibility audit + highlighted screenshot |
171
+ | `dom` / `dom-snapshot` | Serialized DOM + CSS selectors + XPath + bounding rects |
172
+ | `every-viewport` | Capture once per preset in parallel (3-wide pool) |
173
+ | `baseline` | save / list / approve / show visual baselines |
174
+ | `auth` | save / load / list / delete Playwright storageState |
175
+ | `validate` | Validate a sweep plan JSON without running it |
176
+
177
+ ## MCP Server
178
+
179
+ `pursr-mcp` exposes every capability as MCP tools over stdio - works with Claude Code, Cursor, Continue, and any MCP host.
180
+
181
+ ```bash
182
+ npx pursr-mcp
183
+ # or with verbose logging:
184
+ npx pursr-mcp --verbose
185
+ ```
186
+
187
+ ### Exposed Tools
188
+
189
+ | Tool | Description |
190
+ | --- | --- |
191
+ | `pursr_shoot` | Rich screenshot capture (viewport, grid, layer, cursor, camera, animation freeze, HAR) |
192
+ | `pursr_diff` | Pixel-diff a URL against a reference PNG |
193
+ | `pursr_sweep` | Execute a batch sweep plan |
194
+ | `pursr_frames` | Capture an N-frame animation timeline |
195
+ | `pursr_probe` | Health-check a URL |
196
+ | `pursr_audit` | axe-core WCAG audit + highlighted screenshot |
197
+ | `pursr_dom_snapshot` | Full DOM + selector map snapshot |
198
+
199
+ ### Exposed Resources
200
+
201
+ | URI | Description |
202
+ | --- | --- |
203
+ | `pursr://shoot/<url|preset>` | Last screenshot PNG (image/png) |
204
+ | `pursr://sweep/<plan-name>` | Last sweep summary JSON (application/json) |
205
+
206
+ Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override with `PURSOR_MCP_STATE`).
207
+
208
+ ## Visual Regression Baselines
209
+
210
+ ```bash
211
+ pursr baseline save myapp ./out/shoot.png home --url https://my.app
212
+ pursr baseline approve myapp ./out/shoot.png home --url https://my.app
213
+ pursr baseline list myapp
214
+ pursr baseline show myapp home --url https://my.app
215
+ ```
216
+
217
+ Baselines live under `~/.pursor/baselines/<project>/<id>/<step>.png` + `manifest.json`. Override with `PURSOR_BASELINES_DIR`. The `id` is a 16-char SHA1 prefix of `url|viewport|flags` so re-running a sweep maps to the same slot deterministically.
218
+
219
+ ```js
220
+ import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
221
+ const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
222
+ saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });
223
+ ```
224
+
225
+ ## Sweep Plan Validation
226
+
227
+ ```bash
228
+ pursr validate ./plan.json
229
+ # { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }
230
+ ```
231
+
232
+ Catches: empty steps, unknown ops, out-of-range numbers, duplicate names, missing required fields. `pursr sweep` runs the same validator before executing - fail-fast.
233
+
234
+ ```json
235
+ {
236
+ "name": "homepage-matrix",
237
+ "base": "https://example.com",
238
+ "parallel": 4,
239
+ "steps": [
240
+ { "name": "baseline", "shoot": { "preset": "desktop-1280" } },
241
+ { "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
242
+ { "name": "tablet", "shoot": { "preset": "tablet-768" } },
243
+ { "name": "mobile", "shoot": { "preset": "mobile-375" } },
244
+ { "name": "hover-cta", "hover": { "selector": ["text=Get started", "a.btn-primary"] } },
245
+ { "name": "audit", "audit": { "tags": "wcag2a,wcag2aa" } },
246
+ { "name": "diff", "diff": { "ref": "baseline" } }
247
+ ]
248
+ }
249
+ ```
250
+
251
+ ## HAR Capture
252
+
253
+ ```bash
254
+ pursr shoot https://example.com shot.png --har ./out/req.har.json
255
+ ```
256
+
257
+ ```js
258
+ import { startHarCapture, stopHarCapture, writeHar } from "pursr/har";
259
+ const state = await startHarCapture(page);
260
+ await page.goto(url);
261
+ const har = stopHarCapture(page);
262
+ await writeHar(har, "./out/req.har.json");
263
+ ```
264
+
265
+ Output is HAR 1.2 spec - pipe to `har-cli`, perf-tools, or any visualizer.
266
+
267
+ ## Auth State
268
+
269
+ ```bash
270
+ pursr auth save myapp admin --from ./playwright-state.json
271
+ pursr shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp
272
+ pursr auth list myapp
273
+ pursr auth load myapp admin --out ./round-trip.json
274
+ pursr auth delete myapp admin
275
+ ```
276
+
277
+ States live in `~/.pursor/auth/<project>/<name>.json` (override with `PURSOR_AUTH_DIR`). The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
278
+
279
+ ## Parallel Sweep
280
+
281
+ Add `parallel: N` to your plan to run steps concurrently in a worker pool:
282
+
283
+ ```json
284
+ {
285
+ "name": "matrix",
286
+ "base": "https://my.app",
287
+ "parallel": 4,
288
+ "steps": [
289
+ { "name": "home", "shoot": { "preset": "desktop-1280" } },
290
+ { "name": "pricing", "shoot": { "preset": "desktop-1280" } },
291
+ { "name": "docs", "shoot": { "preset": "desktop-1280" } }
292
+ ]
293
+ }
294
+ ```
295
+
296
+ Steps run in a shared browser context; results are still ordered by index in the summary. Defaults to serial (`parallel: 1`) - opt in only when steps are independent.
297
+
298
+ ## Accessibility Audit
299
+
300
+ ```bash
301
+ pursr audit https://example.com --tags wcag2a,wcag2aa
302
+ # Writes: audit.json, audit-summary.md, audit-highlighted.png
303
+ ```
304
+
305
+ Injects axe-core, runs a configurable tag set (`wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `best-practice`), and overlays a red outline on every violating node with the rule id as a label. The summary Markdown includes per-rule failure snippets.
306
+
307
+ ## DOM Snapshot
308
+
309
+ ```bash
310
+ pursr dom https://example.com
311
+ # Writes: dom-snapshot-<ts>.dom.json
312
+ ```
313
+
314
+ Captures serialized HTML, computed CSS for every visible element, and a selector map (`id`, `role`, `accessible name`, `text`, `xpath`, `css selector`, viewport-relative `rect`). Great for regression diffing without re-running a browser.
315
+
316
+ ## CI Output
317
+
318
+ Every sweep writes three sidecar artifacts alongside `sweep.json`:
319
+
320
+ - `sweep.junit.xml` - JUnit XML for Jenkins / GitLab / CircleCI
321
+ - `sweep.github.json` - GitHub Actions annotation file
322
+ - `sweep.md` - Human-readable Markdown summary with diffs + failures
323
+
324
+ ## Library API
325
+
326
+ ```js
327
+ import {
328
+ runProbe, runShot, runShoot, runSweep, runDiff, runAudit,
329
+ captureDomSnapshot, resolveHealedSelector,
330
+ saveBaseline, diffKey,
331
+ startHarCapture, stopHarCapture, writeHar,
332
+ loadAuthState,
333
+ PursorMCPServer, loadMcpConfig,
334
+ validateSweepPlan,
335
+ listResources, readResource,
336
+ listViewports, resolveViewport, VIEWPORTS,
337
+ loadPlugins, registerPlugin, getSweepOp,
338
+ VERSION,
339
+ } from "pursr";
340
+ ```
341
+
342
+ ### Subpath exports
343
+
344
+ ```js
345
+ import { resolveLocator } from "pursr/selector";
346
+ import { launch } from "pursr/runway";
347
+ import { parseFlags, asNum } from "pursr/util";
348
+ import { overlayGrid } from "pursr/overlays";
349
+ import { captureDomSnapshot } from "pursr/dom-snapshot";
350
+ import { runAudit } from "pursr/plugin-audit";
351
+ import { resolveHealedSelector } from "pursr/selector-heal";
352
+ import { writeCiOutput } from "pursr/ci-output";
353
+ import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
354
+ import { validateSweepPlan } from "pursr/sweep-schema";
355
+ import { startHarCapture, stopHarCapture } from "pursr/har";
356
+ import { saveAuthState, loadAuthState } from "pursr/auth";
357
+ import { listResources, readResource } from "pursr/mcp-resources";
358
+ import { PursorMCPServer } from "pursr/mcp";
359
+ ```
360
+
361
+ ## Plugins
362
+
363
+ A plugin is a plain ES module that exports a default object:
364
+
365
+ ```js
366
+ // plugins/my-plugin.js
367
+ export default {
368
+ name: "my-plugin",
369
+ viewport: { "my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" } },
370
+ sweepOp: {
371
+ lighthouse: async (ctx, opts) => { /* ... */ },
372
+ },
373
+ beforeShoot: async (ctx) => { /* mutate ctx.flags / ctx.viewport */ },
374
+ afterShoot: async (ctx, meta) => { /* augment sidecar */ },
375
+ flagHelp: { "my-flag": "what it does" },
376
+ };
377
+ ```
378
+
379
+ Plugins are auto-loaded from `plugins/` (built-in) or via `--plugin <path>`.
380
+
381
+ ## Architecture
382
+
383
+ ```
384
+ src/
385
+ index.js - public library entry
386
+ mcp.js - MCP stdio server (JSON-RPC 2.0)
387
+ shoot.js - runShoot (overlays + camera + frame-stable)
388
+ sweep.js - runSweep (validated, parallel pool)
389
+ diff.js - pixelmatch wrapper
390
+ plugin-audit.js - axe-core injection + highlighted screenshot
391
+ dom-snapshot.js - full DOM + CSSOM + selector map
392
+ selector-heal.js - auto-heal chain resolver
393
+ ci-output.js - JUnit / GitHub / Markdown
394
+ baseline.js - visual regression storage
395
+ har.js - HAR 1.2 network capture
396
+ auth.js - Playwright storageState
397
+ sweep-schema.js - plan validator
398
+ mcp-resources.js - MCP resources adapter
399
+ overlays.js - page-side CSS overlays + camera
400
+ runway.js - Playwright launcher + system-Chrome detector
401
+ viewport.js - built-in viewport presets
402
+ selector.js - text=/role=/aria=/placeholder= parser
403
+ plugin.js - plugin registry + hook runner
404
+ util.js - flags, args, hashing, HTML escape, renderSweepHtml
405
+ every-viewport.js - one shot per preset in parallel
406
+ frames.js, hover.js, shot.js, eval.js, probe.js, interact.js
407
+ ```
408
+
409
+ ## Development
410
+
411
+ ```bash
412
+ git clone https://github.com/0xheycat/pursr
413
+ cd pursr
414
+ npm install
415
+ npm install --save-dev playwright-core
416
+ npm test
417
+ ```
418
+
419
+ `npm test` runs 60 unit + integration tests (Node's built-in test runner, zero test deps). Coverage includes: viewport resolution, flag parsing, selector parsing, HTML escaping, hashing, baseline storage, sweep-plan validation, MCP resources, HAR 1.2 shape, auth state, and end-to-end CLI smoke tests.
420
+
421
+ ```
422
+ src/ - 27 modules
423
+ test/ - 60 tests, 0 failures
424
+ plugins/ - 2 built-in plugins, auto-loaded
425
+ ```
426
+
427
+ ## Roadmap
428
+
429
+ - [x] Visual baselines (save / approve / diff)
430
+ - [x] Sweep plan schema validation
431
+ - [x] MCP resources (browse past captures from your AI host)
432
+ - [x] HAR 1.2 capture
433
+ - [x] Auth state (Playwright storageState)
434
+ - [x] Parallel sweep workers
435
+ - [x] Watch mode (`pursr watch <url>`)
436
+ - [x] Component-level snapshot (`pursr snap <selector>`)
437
+ - [x] PDF report export (`pursr report --sweep`)
438
+ - [ ] Cloud output adapters (S3 / GCS)
439
+ - [x] AI diff summary (vision model, `--ai`)
440
+
441
+ ## PDF Report (v0.6.0)
442
+
443
+ Turn any sweep summary into a styled, self-contained A4 PDF you can email, attach to a PR, or hand to a designer.
444
+
445
+ ```bash
446
+ # 1. Run a sweep (writes sweep.json + index.html + per-step PNGs)
447
+ pursr sweep ./plans/marketing.json
448
+
449
+ # 2. Generate a PDF from the most recent sweep
450
+ pursr report --sweep ./out/sweep-marketing/sweep.json --out ./out/report.pdf
451
+
452
+ # Or: skip image embedding for a tiny text-only report
453
+ pursr report --sweep ./out/sweep-marketing/sweep.json --no-embed
454
+ ```
455
+
456
+ The PDF includes a colored header (pursr brand magenta), a summary stat grid (steps / passed / failed / total time), and a per-step card with: status badge, op + duration + URL, the embedded capture PNG, diff stats, audit violation count, and any error message. Page numbers in the footer.
457
+
458
+ Library:
459
+
460
+ ```js
461
+ import { renderSweepPdf } from "pursr/report";
462
+ import { readFileSync } from "node:fs";
463
+
464
+ const summary = JSON.parse(readFileSync("./sweep.json", "utf8"));
465
+ const bytes = await renderSweepPdf(summary, { out: "./report.pdf" });
466
+ console.log("wrote", bytes.length, "bytes");
467
+ ```
468
+
469
+ ## AI Diff Summary (v0.6.0)
470
+
471
+ Add `--ai` to `pursr diff` and a vision LLM describes the differences in plain language alongside the pixel-diff percentage. Perfect for triaging a regression without opening the PNG.
472
+
473
+ ```bash
474
+ # Basic
475
+ pursr diff https://my.app ./ref.png ./out/diff.png --ai
476
+
477
+ # Custom model + endpoint + key (e.g. local llama.cpp, Codex proxy, OpenAI)
478
+ pursr diff https://my.app ./ref.png ./out/diff.png \
479
+ --ai --ai-model gh/gpt-5.4 \
480
+ --ai-base-url http://127.0.0.1:20128/v1 \
481
+ --ai-api-key sk-...
482
+ ```
483
+
484
+ The AI summary is written to `<out>.ai.json` (or alongside the current PNG) and is also attached to the diff result object as `r.ai = { aiSummary, aiModel, aiElapsedMs, aiAt }`.
485
+
486
+ Auth is picked up from these env vars (in order):
487
+
488
+ ```
489
+ PURSR_AI_API_KEY (preferred)
490
+ PURSOR_AI_API_KEY (legacy alias)
491
+ ANTHROPIC_AUTH_TOKEN
492
+ OPENAI_API_KEY
493
+ ```
494
+
495
+ Base URL: `PURSR_AI_BASE_URL` (falls back to `ANTHROPIC_BASE_URL` then `https://api.openai.com/v1`).
496
+ Model: `PURSR_AI_MODEL` (falls back to `ANTHROPIC_DEFAULT_SONNET_MODEL` then `gpt-4o`).
497
+
498
+ Library:
499
+
500
+ ```js
501
+ import { aiDiffSummary, aiDiffSidecar } from "pursr/ai-diff";
502
+
503
+ const r = await aiDiffSummary({
504
+ refPath: "./ref.png",
505
+ curPath: "./out/diff-current.png",
506
+ url: "https://my.app",
507
+ model: "gpt-4o",
508
+ });
509
+ console.log(r.summary); // markdown bullet report
510
+ console.log(r.elapsedMs); // how long the LLM took
511
+
512
+ // Or attach to a sweep step:
513
+ const sidecar = await aiDiffSidecar({ refPath, curPath, url });
514
+ ```
515
+
516
+ ## Watch Mode (v0.5.0)
517
+
518
+ ```bash
519
+ # Re-shoot every time a CSS or HTML file changes
520
+ pursr watch https://my.app --on src/**/*.css --on src/**/*.html
521
+
522
+ # Re-run a sweep plan on file change
523
+ pursr watch --plan ./plan.json --on src/**/*.{css,html}
524
+
525
+ # Default (no --on) = watch everything in cwd
526
+ pursr watch https://my.app
527
+ ```
528
+
529
+ Glob patterns: * (one path segment), ** (any depth), ? (one char), backslash-X (literal X). Debounce is 300ms by default.
530
+
531
+ ## Component Snapshots (v0.5.0)
532
+
533
+ ```bash
534
+ # Capture one screenshot per matched element
535
+ pursr snap https://my.app a.btn --out ./snaps --max 20
536
+
537
+ # Use auto-heal selector chain
538
+ pursr snap https://my.app "text=Sign up" --out ./snaps
539
+
540
+ # Promote to baselines in one command
541
+ pursr snap https://my.app article.product --baseline myapp
542
+ ```
543
+
544
+ Each capture is clipped precisely to the elements bounding box (even when scrolled offscreen), labelled with aria-label / text / tag, and written to ./snaps/<index>-<label>.png + snap.json summary.
545
+
546
+ ---
547
+ ## License
548
+
549
+ MIT (c) 2026 - [0xheycat](https://github.com/0xheycat)