pursr 0.6.0 → 0.7.1

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 CHANGED
@@ -1,21 +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
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
21
  SOFTWARE.
package/README.md CHANGED
@@ -65,7 +65,7 @@ pursr baseline save myapp shot.png home --url https://example.com
65
65
 
66
66
  # 3. Next time you run, compare against the baseline
67
67
  pursr diff https://example.com \
68
- ~/.pursor/baselines/myapp/<id>/home.png \
68
+ ~/.pursr/baselines/myapp/<id>/home.png \
69
69
  diff.png
70
70
 
71
71
  # 4. Or: run a batched sweep + a11y audit + parallel workers
@@ -203,7 +203,7 @@ npx pursr-mcp --verbose
203
203
  | `pursr://shoot/<url|preset>` | Last screenshot PNG (image/png) |
204
204
  | `pursr://sweep/<plan-name>` | Last sweep summary JSON (application/json) |
205
205
 
206
- Resources are persisted to `~/.pursor/mcp/mcp-index.json` (override with `PURSOR_MCP_STATE`).
206
+ Resources are persisted to `~/.pursr/mcp/mcp-index.json` (override with `PURSR_MCP_STATE`).
207
207
 
208
208
  ## Visual Regression Baselines
209
209
 
@@ -214,7 +214,7 @@ pursr baseline list myapp
214
214
  pursr baseline show myapp home --url https://my.app
215
215
  ```
216
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.
217
+ Baselines live under `~/.pursr/baselines/<project>/<id>/<step>.png` + `manifest.json`. Override with `PURSR_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
218
 
219
219
  ```js
220
220
  import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
@@ -274,7 +274,7 @@ pursr auth load myapp admin --out ./round-trip.json
274
274
  pursr auth delete myapp admin
275
275
  ```
276
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 }`.
277
+ States live in `~/.pursr/auth/<project>/<name>.json` (override with `PURSR_AUTH_DIR`). The on-disk format is the standard Playwright `storageState` shape: `{ cookies, origins }`.
278
278
 
279
279
  ## Parallel Sweep
280
280
 
@@ -330,7 +330,7 @@ import {
330
330
  saveBaseline, diffKey,
331
331
  startHarCapture, stopHarCapture, writeHar,
332
332
  loadAuthState,
333
- PursorMCPServer, loadMcpConfig,
333
+ PursrMCPServer, loadMcpConfig,
334
334
  validateSweepPlan,
335
335
  listResources, readResource,
336
336
  listViewports, resolveViewport, VIEWPORTS,
@@ -355,7 +355,7 @@ import { validateSweepPlan } from "pursr/sweep-schema";
355
355
  import { startHarCapture, stopHarCapture } from "pursr/har";
356
356
  import { saveAuthState, loadAuthState } from "pursr/auth";
357
357
  import { listResources, readResource } from "pursr/mcp-resources";
358
- import { PursorMCPServer } from "pursr/mcp";
358
+ import { PursrMCPServer } from "pursr/mcp";
359
359
  ```
360
360
 
361
361
  ## Plugins
@@ -416,11 +416,11 @@ npm install --save-dev playwright-core
416
416
  npm test
417
417
  ```
418
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.
419
+ `npm test` runs 63 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
420
 
421
421
  ```
422
- src/ - 27 modules
423
- test/ - 60 tests, 0 failures
422
+ src/ - 29 modules
423
+ test/ - 63 tests, 0 failures
424
424
  plugins/ - 2 built-in plugins, auto-loaded
425
425
  ```
426
426
 
package/assets/icon.svg CHANGED
@@ -1,21 +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>
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
21
  </svg>
package/assets/logo.svg CHANGED
@@ -1,29 +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>
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
29
  </svg>
@@ -1,77 +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 &amp; 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>
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">pursr.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 &amp; 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
77
  </svg>
package/bin/pursr-mcp.mjs CHANGED
@@ -1,21 +1,22 @@
1
1
  #!/usr/bin/env node
2
- // @purr/visual — MCP server binary.
2
+ // pursr — MCP server binary.
3
3
  //
4
- // Runs the pursor MCP stdio server, exposing all capture/audit/sweep
4
+ // Runs the pursr MCP stdio server, exposing all capture/audit/sweep
5
5
  // capabilities as MCP tools for Claude Code, Cursor, Continue, etc.
6
6
  //
7
- // Usage: pursor-mcp
8
- // Config via PURSOR_MCP_CONFIG env or ~/.pursor/mcp-config.json
7
+ // Usage: pursr-mcp
8
+ // Config via PURSR_MCP_CONFIG env or ~/.pursr/mcp-config.json
9
9
  //
10
- // echo '{"url":"https://example.com"}' | pursor-mcp
10
+ // echo '{"url":"https://example.com"}' | pursr-mcp
11
11
 
12
- import { PursorMCPServer, loadConfig } from "../src/mcp.js";
12
+ import { PursrMCPServer, loadConfig } from "../src/mcp.js";
13
+ import { __PURSR_GET } from "../src/util.js";
13
14
 
14
15
  const config = loadConfig();
15
16
 
16
17
  // Verbose mode: --verbose or debug env
17
- const verbose = process.argv.includes("--verbose") || !!process.env.PURSOR_DEBUG;
18
+ const verbose = process.argv.includes("--verbose") || !!__PURSR_GET("PURSR_DEBUG");
18
19
  config.verbose = verbose;
19
20
 
20
- const server = new PursorMCPServer(config);
21
- await server.start();
21
+ const server = new PursrMCPServer(config);
22
+ await server.start();
package/bin/pursr.mjs CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env node
2
- // pursor CLI. Thin wrapper around src/* that mirrors the npm bin.
1
+ #!/usr/bin/env node
2
+ // pursr CLI. Thin wrapper around src/* that mirrors the npm bin.
3
3
 
4
4
  import { VERSION } from "../src/index.js";
5
5
  import { runClick, runType, runWait, runSeq } from "../src/interact.js";
@@ -15,7 +15,7 @@ import { runEveryViewport } from "../src/every-viewport.js";
15
15
  import { runAudit } from "../src/plugin-audit.js";
16
16
  import { captureDomSnapshot } from "../src/dom-snapshot.js";
17
17
  import { listViewports } from "../src/viewport.js";
18
- import { parseFlags, asNum, readArg, makeOut, pickOutPath } from "../src/util.js";
18
+ import { parseFlags, asNum, readArg, makeOut, pickOutPath, __PURSR_GET } from "../src/util.js";
19
19
  import { writeFileSync, existsSync, mkdirSync } from "node:fs";
20
20
  import { dirname } from "node:path";
21
21
  import { readFileSync as _readFileSync } from "node:fs";
@@ -23,8 +23,8 @@ const readFile = _readFileSync;
23
23
  import { loadPlugins, listPlugins, getFlagHelp } from "../src/plugin.js";
24
24
 
25
25
  const USAGE = `usage:
26
- v1: pursor {probe|shot|full|eval|click|type|wait|diff|seq} <url> [...]
27
- v2: pursor {viewports|shoot|layer|frames|hover|sweep} <...>
26
+ v1: pursr {probe|shot|full|eval|click|type|wait|diff|seq} <url> [...]
27
+ v2: pursr {viewports|shoot|layer|frames|hover|sweep} <...>
28
28
  flags: --preset <name> --width N --height N --dpr N
29
29
  --zoom 1.5 --panX 200 --panY -100
30
30
  --cursor pointer|grab|grabbing|crosshair|none
@@ -34,7 +34,7 @@ const USAGE = `usage:
34
34
  @file prefix reads argv contents from file (UTF-8, newline trimmed).
35
35
  report: pursr report --sweep <sweep.json> [--out <report.pdf>] [--title "..."] [--no-embed]
36
36
  diff extras: --ai [--ai-model M] [--ai-base-url U] [--ai-api-key K]
37
- plugins: pursor automatically loads built-in plugins from plugins/.
37
+ plugins: pursr automatically loads built-in plugins from plugins/.
38
38
  You can also pass --plugin <path> to load custom plugins (repeatable).`;
39
39
 
40
40
  function die(msg, code = 2) {
@@ -44,7 +44,7 @@ function die(msg, code = 2) {
44
44
 
45
45
  const argv = process.argv;
46
46
  const [, , cmd, a, b, c, d] = argv;
47
- const url = process.env.PURSOR_URL || a;
47
+ const url = __PURSR_GET("PURSR_URL") || a;
48
48
  // Top-level --plan / --out parsing for subcommands that need it before dispatch
49
49
  function _topOpts() {
50
50
  const o = {};
@@ -66,7 +66,7 @@ await loadPlugins(pluginPaths);
66
66
  switch (cmd) {
67
67
  case undefined: case "help": case "--help": case "-h": { console.log(JSON.stringify({ usage: USAGE }, null, 2)); break; }
68
68
  case "version": case "--version": case "-v": {
69
- console.log(JSON.stringify({ name: "pursor", version: VERSION, plugins: listPlugins() }, null, 2));
69
+ console.log(JSON.stringify({ name: "pursr", version: VERSION, plugins: listPlugins() }, null, 2));
70
70
  break;
71
71
  }
72
72
  case "probe": { if (!url) die("missing url"); const r = await runProbe(url); console.log(JSON.stringify(r, null, 2)); break; }
@@ -79,14 +79,15 @@ await loadPlugins(pluginPaths);
79
79
  case "diff": {
80
80
  if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>");
81
81
  const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1;
82
+ const flags = parseFlags(argv.slice(5));
82
83
  // --ai / --ai-model / --ai-base-url / --ai-api-key
83
84
  const useAi = argv.includes("--ai");
84
85
  const aiModel = (() => { const i = argv.indexOf("--ai-model"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
85
86
  const aiBaseUrl = (() => { const i = argv.indexOf("--ai-base-url"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
86
87
  const aiApiKey = (() => { const i = argv.indexOf("--ai-api-key"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
87
88
  const r = useAi
88
- ? await runDiffWithAi(url, ref, out, threshold, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
89
- : await runDiff(url, ref, out, threshold);
89
+ ? await runDiffWithAi(url, ref, out, threshold, flags, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
90
+ : await runDiff(url, ref, out, threshold, flags);
90
91
  console.log(JSON.stringify(r, null, 2)); break;
91
92
  }
92
93
  case "seq": { if (!url) die("missing url"); const actions = readArg(b); if (!actions) die("seq: missing <actions.json> (or @file)"); const out = c || makeOut("seq.png"); const r = await runSeq(url, actions, out); console.log(JSON.stringify(r, null, 2)); break; }
@@ -139,7 +140,7 @@ await loadPlugins(pluginPaths);
139
140
  if (!sweepPath) die("report: missing --sweep <sweep.json>");
140
141
  if (!existsSync(sweepPath)) die("report: sweep not found: " + sweepPath);
141
142
  const outIdx = argv.indexOf("--out");
142
- const outPath = outIdx >= 0 && outIdx + 1 < argv.length ? argv[outIdx + 1] : (opts.out || makeOut("report.pdf").replace(/pursor-[^-]+-shot.png$/, "report.pdf"));
143
+ const outPath = outIdx >= 0 && outIdx + 1 < argv.length ? argv[outIdx + 1] : (opts.out || makeOut("report.pdf").replace(/pursr-[^-]+-shot.png$/, "report.pdf"));
143
144
  if (outPath && outPath !== "-") mkdirSync(dirname(outPath), { recursive: true });
144
145
  const titleIdx = argv.indexOf("--title");
145
146
  const title = titleIdx >= 0 && titleIdx + 1 < argv.length ? argv[titleIdx + 1] : undefined;
@@ -186,7 +187,7 @@ await loadPlugins(pluginPaths);
186
187
  break;
187
188
  }
188
189
  case "baseline": {
189
- // pursor baseline <sub> [...args]
190
+ // baseline <sub> [...args]
190
191
  // sub=list -> list baselines
191
192
  // sub=save <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]
192
193
  // sub=approve <project> <png> <step> [--id <id>] [--url <u>]
@@ -227,7 +228,7 @@ await loadPlugins(pluginPaths);
227
228
  break;
228
229
  }
229
230
  case "auth": {
230
- // pursor auth <sub> [...args]
231
+ // auth <sub> [...args]
231
232
  // save <project> <name> --from <state.json>
232
233
  // load <project> <name> --out <state.json>
233
234
  // list [project]
@@ -300,7 +301,7 @@ await loadPlugins(pluginPaths);
300
301
  const sel = b; if (!sel) die("snap: missing <selector>");
301
302
  const flags = parseFlags(argv.slice(4));
302
303
  const { runSnap, approveSnapsAsBaselines } = await import("../src/snap.js");
303
- const outDir = flags.out || makeOut("snaps").replace(/pursor-[^-]+-snap\.png$/, "snaps");
304
+ const outDir = flags.out || makeOut("snaps").replace(/pursr-[^-]+-snap\.png$/, "snaps");
304
305
  const snap = await runSnap({ url, selector: sel, outDir, name: flags.name, max: flags.max, flags });
305
306
  console.log(JSON.stringify({
306
307
  url: snap.url,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pursr",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
- "description": "Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
5
+ "description": "pursr — Visual QA, audit, and MCP for the browser. One CLI + one MCP server for screenshots, sweeps, baselines, diffs, axe-core a11y audits, HAR capture, and auth state — with parallel sweep workers, auto-healing selectors, and a plugin system. Zero browser bundled: drives your system Chrome via Playwright.",
6
6
  "homepage": "https://github.com/0xheycat/pursr",
7
7
  "bugs": "https://github.com/0xheycat/pursr/issues",
8
8
  "repository": {
@@ -50,9 +50,9 @@
50
50
  "LICENSE"
51
51
  ],
52
52
  "scripts": {
53
- "start": "node bin/pursor.mjs",
53
+ "start": "node bin/pursr.mjs",
54
54
  "test": "node --test \"test/*.test.js\"",
55
- "smoke": "node bin/pursor.mjs viewports"
55
+ "smoke": "node bin/pursr.mjs viewports"
56
56
  },
57
57
  "engines": {
58
58
  "node": ">=18"
@@ -1,22 +1,22 @@
1
- {
2
- "name": "m5.4-polish",
3
- "base": "http://localhost:3010",
4
- "outDir": "./out/m54-sweep",
5
- "steps": [
6
- { "name": "baseline", "shoot": { "preset": "desktop-1280" } },
7
- { "name": "cursor-pointer", "shoot": { "preset": "desktop-1280", "cursor": "pointer" } },
8
- { "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
9
- { "name": "grid-128", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 128 } },
10
- { "name": "layer-entity", "shoot": { "preset": "desktop-1280", "layer": "entity" } },
11
- { "name": "layer-terrain", "shoot": { "preset": "desktop-1280", "layer": "terrain" } },
12
- { "name": "no-hud", "shoot": { "preset": "desktop-1280", "no-hud": true } },
13
- { "name": "frozen", "shoot": { "preset": "desktop-1280", "no-animation": true } },
14
- { "name": "tablet-768", "shoot": { "preset": "tablet-768" } },
15
- { "name": "mobile-375", "shoot": { "preset": "mobile-375" } },
16
- { "name": "ultrawide-3440", "shoot": { "preset": "ultrawide-3440" } },
17
- { "name": "hover-build", "hover": { "selector": "text=Build" } },
18
- { "name": "hover-decor", "hover": { "selector": "text=Decor" } },
19
- { "name": "frames-8", "frames": { "count": 8, "intervalMs": 200 } },
20
- { "name": "diff-vs-baseline", "diff": { "ref": "baseline.png" } }
21
- ]
1
+ {
2
+ "name": "m5.4-polish",
3
+ "base": "http://localhost:3010",
4
+ "outDir": "./out/m54-sweep",
5
+ "steps": [
6
+ { "name": "baseline", "shoot": { "preset": "desktop-1280" } },
7
+ { "name": "cursor-pointer", "shoot": { "preset": "desktop-1280", "cursor": "pointer" } },
8
+ { "name": "grid-64", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
9
+ { "name": "grid-128", "shoot": { "preset": "desktop-1280", "grid": true, "grid-tile": 128 } },
10
+ { "name": "layer-entity", "shoot": { "preset": "desktop-1280", "layer": "entity" } },
11
+ { "name": "layer-terrain", "shoot": { "preset": "desktop-1280", "layer": "terrain" } },
12
+ { "name": "no-hud", "shoot": { "preset": "desktop-1280", "no-hud": true } },
13
+ { "name": "frozen", "shoot": { "preset": "desktop-1280", "no-animation": true } },
14
+ { "name": "tablet-768", "shoot": { "preset": "tablet-768" } },
15
+ { "name": "mobile-375", "shoot": { "preset": "mobile-375" } },
16
+ { "name": "ultrawide-3440", "shoot": { "preset": "ultrawide-3440" } },
17
+ { "name": "hover-build", "hover": { "selector": "text=Build" } },
18
+ { "name": "hover-decor", "hover": { "selector": "text=Decor" } },
19
+ { "name": "frames-8", "frames": { "count": 8, "intervalMs": 200 } },
20
+ { "name": "diff-vs-baseline", "diff": { "ref": "baseline.png" } }
21
+ ]
22
22
  }