pursr 0.4.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 +440 -0
- package/assets/icon.svg +21 -0
- package/assets/logo.svg +29 -0
- package/assets/social-preview.svg +77 -0
- package/bin/pursr-mcp.mjs +21 -0
- package/bin/pursr.mjs +227 -0
- package/package.json +90 -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/auth.js +92 -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/har.js +159 -0
- package/src/hover.js +26 -0
- package/src/index.js +95 -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 +66 -0
- package/src/selector-heal.js +85 -0
- package/src/selector.js +39 -0
- package/src/shoot.js +74 -0
- package/src/shot.js +18 -0
- package/src/sweep-schema.js +70 -0
- package/src/sweep.js +119 -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 pursr
|
|
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,440 @@
|
|
|
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> · <a href="#30-seconds">30 seconds</a> · <a href="#cli">CLI</a> · <a href="#mcp-server">MCP</a> · <a href="#library-api">Library</a> · <a href="#plugins">Plugins</a> · <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 47 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/ - 47 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
|
+
- [ ] Watch mode (`pursr watch <url>`)
|
|
433
|
+
- [ ] Component-level snapshot (`pursr snap <selector>`)
|
|
434
|
+
- [ ] PDF report export
|
|
435
|
+
- [ ] Cloud output adapters (S3 / GCS)
|
|
436
|
+
- [ ] AI diff summary (vision model)
|
|
437
|
+
|
|
438
|
+
## License
|
|
439
|
+
|
|
440
|
+
MIT (c) 2026 - [0xheycat](https://github.com/0xheycat)
|
package/assets/icon.svg
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="pursr icon">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="128" height="128" rx="24" fill="#0B0B0F"/>
|
|
9
|
+
<g transform="translate(8,8)">
|
|
10
|
+
<circle cx="56" cy="56" r="42" fill="none" stroke="url(#g)" stroke-width="7"/>
|
|
11
|
+
<g fill="none" stroke="url(#g)" stroke-width="7" stroke-linecap="round">
|
|
12
|
+
<path d="M56 22 L72 44"/>
|
|
13
|
+
<path d="M87 41 L74 59"/>
|
|
14
|
+
<path d="M87 75 L66 67"/>
|
|
15
|
+
<path d="M56 90 L46 69"/>
|
|
16
|
+
<path d="M25 75 L38 66"/>
|
|
17
|
+
<path d="M25 41 L39 51"/>
|
|
18
|
+
</g>
|
|
19
|
+
<path d="M56 28 L72 70 L60 66 L52 80 L48 70 L56 28 Z" fill="url(#g)"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
package/assets/logo.svg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 96" role="img" aria-label="pursr">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="pursr-mark-grad" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
5
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<!-- Mark: cursor arrowhead fused with camera aperture / lens iris -->
|
|
9
|
+
<g transform="translate(8,8)">
|
|
10
|
+
<!-- Outer iris ring -->
|
|
11
|
+
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6"/>
|
|
12
|
+
<!-- Aperture blades (6) -->
|
|
13
|
+
<g fill="none" stroke="url(#pursr-mark-grad)" stroke-width="6" stroke-linecap="round">
|
|
14
|
+
<path d="M40 12 L52 30"/>
|
|
15
|
+
<path d="M64.4 25.6 L54.5 40"/>
|
|
16
|
+
<path d="M64.4 54.4 L48 48"/>
|
|
17
|
+
<path d="M40 68 L32 50"/>
|
|
18
|
+
<path d="M15.6 54.4 L26 47"/>
|
|
19
|
+
<path d="M15.6 25.6 L27 34"/>
|
|
20
|
+
</g>
|
|
21
|
+
<!-- Cursor chevron pointer on top of iris -->
|
|
22
|
+
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#pursr-mark-grad)"/>
|
|
23
|
+
</g>
|
|
24
|
+
<!-- Wordmark: pursr -->
|
|
25
|
+
<g transform="translate(108,62)" font-family="-apple-system,BlinkMacSystemFont,Inter,Segoe UI,Roboto,sans-serif" font-weight="700" font-size="56" letter-spacing="-2">
|
|
26
|
+
<text fill="#0B0B0F" x="0" y="0">pursr</text>
|
|
27
|
+
<rect x="170" y="-12" width="10" height="10" fill="#FF2EA6"/>
|
|
28
|
+
</g>
|
|
29
|
+
</svg>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 640" role="img" aria-label="pursr — visual QA, audit, and MCP for the browser">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#0B0B0F"/>
|
|
5
|
+
<stop offset="0.6" stop-color="#15101A"/>
|
|
6
|
+
<stop offset="1" stop-color="#1F0A1A"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="mark" x1="0" y1="0" x2="1" y2="1">
|
|
9
|
+
<stop offset="0" stop-color="#FF2EA6"/>
|
|
10
|
+
<stop offset="1" stop-color="#FF5CC1"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<pattern id="grid" width="48" height="48" patternUnits="userSpaceOnUse">
|
|
13
|
+
<path d="M48 0 L0 0 0 48" fill="none" stroke="#FF2EA6" stroke-opacity="0.08" stroke-width="1"/>
|
|
14
|
+
</pattern>
|
|
15
|
+
<radialGradient id="glow" cx="0.85" cy="0.15" r="0.6">
|
|
16
|
+
<stop offset="0" stop-color="#FF2EA6" stop-opacity="0.35"/>
|
|
17
|
+
<stop offset="1" stop-color="#FF2EA6" stop-opacity="0"/>
|
|
18
|
+
</radialGradient>
|
|
19
|
+
</defs>
|
|
20
|
+
<rect width="1280" height="640" fill="url(#bg)"/>
|
|
21
|
+
<rect width="1280" height="640" fill="url(#grid)"/>
|
|
22
|
+
<rect width="1280" height="640" fill="url(#glow)"/>
|
|
23
|
+
|
|
24
|
+
<!-- Decorative screenshot card on the right -->
|
|
25
|
+
<g transform="translate(720,160)">
|
|
26
|
+
<rect width="440" height="320" rx="16" fill="#101015" stroke="#2A2A30"/>
|
|
27
|
+
<rect x="0" y="0" width="440" height="40" rx="16" fill="#1B1B22"/>
|
|
28
|
+
<circle cx="20" cy="20" r="6" fill="#FF5F56"/>
|
|
29
|
+
<circle cx="40" cy="20" r="6" fill="#FFBD2E"/>
|
|
30
|
+
<circle cx="60" cy="20" r="6" fill="#27C93F"/>
|
|
31
|
+
<text x="220" y="25" fill="#666" font-family="monospace" font-size="12" text-anchor="middle">pursor.mjs shoot</text>
|
|
32
|
+
<line x1="20" y1="80" x2="200" y2="80" stroke="#FF2EA6" stroke-width="14" stroke-linecap="round"/>
|
|
33
|
+
<line x1="20" y1="120" x2="380" y2="120" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
34
|
+
<line x1="20" y1="160" x2="320" y2="160" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
35
|
+
<line x1="20" y1="200" x2="260" y2="200" stroke="#3A3A45" stroke-width="8" stroke-linecap="round"/>
|
|
36
|
+
<rect x="20" y="240" width="120" height="48" rx="6" fill="#FF2EA6"/>
|
|
37
|
+
<line x1="160" y1="264" x2="220" y2="264" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
38
|
+
<line x1="240" y1="252" x2="380" y2="252" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
39
|
+
<line x1="240" y1="276" x2="340" y2="276" stroke="#3A3A45" stroke-width="6" stroke-linecap="round"/>
|
|
40
|
+
</g>
|
|
41
|
+
|
|
42
|
+
<!-- Brand mark + wordmark on the left -->
|
|
43
|
+
<g transform="translate(80,200)">
|
|
44
|
+
<g transform="scale(1.5)">
|
|
45
|
+
<circle cx="40" cy="40" r="34" fill="none" stroke="url(#mark)" stroke-width="6"/>
|
|
46
|
+
<g fill="none" stroke="url(#mark)" stroke-width="6" stroke-linecap="round">
|
|
47
|
+
<path d="M40 12 L52 30"/>
|
|
48
|
+
<path d="M64.4 25.6 L54.5 40"/>
|
|
49
|
+
<path d="M64.4 54.4 L48 48"/>
|
|
50
|
+
<path d="M40 68 L32 50"/>
|
|
51
|
+
<path d="M15.6 54.4 L26 47"/>
|
|
52
|
+
<path d="M15.6 25.6 L27 34"/>
|
|
53
|
+
</g>
|
|
54
|
+
<path d="M40 18 L52 50 L42 47 L36 58 L33 50 L40 18 Z" fill="url(#mark)"/>
|
|
55
|
+
</g>
|
|
56
|
+
<text x="140" y="56" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="800" font-size="76" letter-spacing="-3">pursr</text>
|
|
57
|
+
<rect x="438" y="0" width="14" height="14" fill="#FF2EA6"/>
|
|
58
|
+
</g>
|
|
59
|
+
|
|
60
|
+
<!-- Tagline -->
|
|
61
|
+
<text x="80" y="380" fill="#FFFFFF" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="600" font-size="34" letter-spacing="-1">Visual QA, audit & MCP for the browser.</text>
|
|
62
|
+
<text x="80" y="430" fill="#A0A0AA" font-family="-apple-system,BlinkMacSystemFont,Inter,sans-serif" font-weight="400" font-size="22" letter-spacing="-0.5">Capture · sweep · diff · audit · repeat.</text>
|
|
63
|
+
|
|
64
|
+
<!-- Footer chip line -->
|
|
65
|
+
<g transform="translate(80,520)">
|
|
66
|
+
<rect width="180" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
67
|
+
<text x="90" y="23" fill="#FF5CC1" font-family="monospace" font-size="14" text-anchor="middle">npm i pursr</text>
|
|
68
|
+
</g>
|
|
69
|
+
<g transform="translate(280,520)">
|
|
70
|
+
<rect width="200" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
71
|
+
<text x="100" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">7 MCP tools · 32 tests</text>
|
|
72
|
+
</g>
|
|
73
|
+
<g transform="translate(500,520)">
|
|
74
|
+
<rect width="220" height="36" rx="18" fill="#1A1A22" stroke="#2A2A30"/>
|
|
75
|
+
<text x="110" y="23" fill="#A0A0AA" font-family="monospace" font-size="14" text-anchor="middle">HAR · baselines · parallel</text>
|
|
76
|
+
</g>
|
|
77
|
+
</svg>
|
|
@@ -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();
|