pursr 0.5.0 → 0.7.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 +20 -20
- package/README.md +549 -471
- package/assets/icon.svg +20 -20
- package/assets/logo.svg +28 -28
- package/assets/social-preview.svg +76 -76
- package/bin/pursr-mcp.mjs +10 -9
- package/bin/pursr.mjs +44 -12
- package/package.json +95 -92
- package/plans/m5.4-polish.json +21 -21
- package/plugins/plugin-audit.js +57 -57
- package/plugins/plugin-demo.js +63 -63
- package/src/ai-diff.js +125 -0
- package/src/auth.js +92 -91
- package/src/baseline.js +126 -125
- package/src/ci-output.js +156 -156
- package/src/diff.js +76 -48
- package/src/dom-snapshot.js +192 -192
- package/src/eval.js +17 -17
- package/src/every-viewport.js +51 -51
- package/src/frames.js +33 -33
- package/src/har.js +158 -158
- package/src/hover.js +25 -25
- package/src/index.js +15 -7
- package/src/interact.js +137 -137
- package/src/mcp-resources.js +111 -110
- package/src/mcp.js +436 -435
- package/src/overlays.js +169 -169
- package/src/plugin-audit.js +260 -260
- package/src/plugin.js +120 -120
- package/src/probe.js +19 -19
- package/src/report.js +176 -0
- package/src/runway.js +65 -65
- package/src/selector-heal.js +85 -85
- package/src/selector.js +38 -38
- package/src/shoot.js +73 -73
- package/src/shot.js +17 -17
- package/src/snap.js +128 -128
- package/src/sweep-schema.js +69 -69
- package/src/sweep.js +1 -1
- package/src/util.js +204 -188
- package/src/viewport.js +38 -38
- package/src/watch.js +134 -134
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">
|
|
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>
|
|
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 & 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
|
-
//
|
|
2
|
+
// pursr — MCP server binary.
|
|
3
3
|
//
|
|
4
|
-
// Runs the
|
|
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:
|
|
8
|
-
// Config via
|
|
7
|
+
// Usage: pursr-mcp
|
|
8
|
+
// Config via PURSR_MCP_CONFIG env or ~/.pursr/mcp-config.json
|
|
9
9
|
//
|
|
10
|
-
// echo '{"url":"https://example.com"}' |
|
|
10
|
+
// echo '{"url":"https://example.com"}' | pursr-mcp
|
|
11
11
|
|
|
12
|
-
import {
|
|
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") || !!
|
|
18
|
+
const verbose = process.argv.includes("--verbose") || !!__PURSR_GET("PURSR_DEBUG");
|
|
18
19
|
config.verbose = verbose;
|
|
19
20
|
|
|
20
|
-
const server = new
|
|
21
|
-
await server.start();
|
|
21
|
+
const server = new PursrMCPServer(config);
|
|
22
|
+
await server.start();
|
package/bin/pursr.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
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";
|
|
@@ -9,13 +9,13 @@ import { runShot } from "../src/shot.js";
|
|
|
9
9
|
import { runShootWithSidecar } from "../src/shoot.js";
|
|
10
10
|
import { runHover } from "../src/hover.js";
|
|
11
11
|
import { runFrames } from "../src/frames.js";
|
|
12
|
-
import { runDiff } from "../src/diff.js";
|
|
12
|
+
import { runDiff, runDiffWithAi } from "../src/diff.js";
|
|
13
13
|
import { runSweep } from "../src/sweep.js";
|
|
14
14
|
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:
|
|
27
|
-
v2:
|
|
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
|
|
@@ -32,7 +32,9 @@ const USAGE = `usage:
|
|
|
32
32
|
--grid --grid-tile 64 --grid-color rgba(255,0,255,0.35)
|
|
33
33
|
--no-animation --wait-frame 600 --full
|
|
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
|
+
diff extras: --ai [--ai-model M] [--ai-base-url U] [--ai-api-key K]
|
|
37
|
+
plugins: pursr automatically loads built-in plugins from plugins/.
|
|
36
38
|
You can also pass --plugin <path> to load custom plugins (repeatable).`;
|
|
37
39
|
|
|
38
40
|
function die(msg, code = 2) {
|
|
@@ -42,7 +44,7 @@ function die(msg, code = 2) {
|
|
|
42
44
|
|
|
43
45
|
const argv = process.argv;
|
|
44
46
|
const [, , cmd, a, b, c, d] = argv;
|
|
45
|
-
const url =
|
|
47
|
+
const url = __PURSR_GET("PURSR_URL") || a;
|
|
46
48
|
// Top-level --plan / --out parsing for subcommands that need it before dispatch
|
|
47
49
|
function _topOpts() {
|
|
48
50
|
const o = {};
|
|
@@ -64,7 +66,7 @@ await loadPlugins(pluginPaths);
|
|
|
64
66
|
switch (cmd) {
|
|
65
67
|
case undefined: case "help": case "--help": case "-h": { console.log(JSON.stringify({ usage: USAGE }, null, 2)); break; }
|
|
66
68
|
case "version": case "--version": case "-v": {
|
|
67
|
-
console.log(JSON.stringify({ name: "
|
|
69
|
+
console.log(JSON.stringify({ name: "pursr", version: VERSION, plugins: listPlugins() }, null, 2));
|
|
68
70
|
break;
|
|
69
71
|
}
|
|
70
72
|
case "probe": { if (!url) die("missing url"); const r = await runProbe(url); console.log(JSON.stringify(r, null, 2)); break; }
|
|
@@ -74,7 +76,19 @@ await loadPlugins(pluginPaths);
|
|
|
74
76
|
case "click": { if (!url) die("missing url"); const sel = b; if (!sel) die("click: missing <selector>"); const out = c || makeOut(`click-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runClick(url, sel, out); console.log(JSON.stringify(r, null, 2)); break; }
|
|
75
77
|
case "type": { if (!url) die("missing url"); const sel = b; const text = readArg(c); if (!sel || text === undefined) die("type: missing <selector> or <text> (text can be @file)"); const out = d || makeOut(`type-${(sel||"").replace(/[^a-z0-9]+/gi, "_").slice(0, 32)}.png`); const r = await runType(url, sel, text, out); console.log(JSON.stringify(r, null, 2)); break; }
|
|
76
78
|
case "wait": { if (!url) die("missing url"); const sel = b; if (!sel) die("wait: missing <selector>"); const t = c !== undefined ? asNum(c, 30000) : 30000; const r = await runWait(url, sel, t); console.log(JSON.stringify(r, null, 2)); break; }
|
|
77
|
-
case "diff": {
|
|
79
|
+
case "diff": {
|
|
80
|
+
if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>");
|
|
81
|
+
const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1;
|
|
82
|
+
// --ai / --ai-model / --ai-base-url / --ai-api-key
|
|
83
|
+
const useAi = argv.includes("--ai");
|
|
84
|
+
const aiModel = (() => { const i = argv.indexOf("--ai-model"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
85
|
+
const aiBaseUrl = (() => { const i = argv.indexOf("--ai-base-url"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
86
|
+
const aiApiKey = (() => { const i = argv.indexOf("--ai-api-key"); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; })();
|
|
87
|
+
const r = useAi
|
|
88
|
+
? await runDiffWithAi(url, ref, out, threshold, { model: aiModel, baseUrl: aiBaseUrl, apiKey: aiApiKey })
|
|
89
|
+
: await runDiff(url, ref, out, threshold);
|
|
90
|
+
console.log(JSON.stringify(r, null, 2)); break;
|
|
91
|
+
}
|
|
78
92
|
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; }
|
|
79
93
|
case "viewports": { console.log(JSON.stringify(listViewports(), null, 2)); break; }
|
|
80
94
|
case "shoot": {
|
|
@@ -118,6 +132,24 @@ await loadPlugins(pluginPaths);
|
|
|
118
132
|
console.log(JSON.stringify(r, null, 2));
|
|
119
133
|
break;
|
|
120
134
|
}
|
|
135
|
+
case "report": {
|
|
136
|
+
// pursr report --sweep <sweep.json> [--out report.pdf] [--title "..."]
|
|
137
|
+
const sweepIdx = argv.indexOf("--sweep");
|
|
138
|
+
const sweepPath = sweepIdx >= 0 && sweepIdx + 1 < argv.length ? argv[sweepIdx + 1] : a;
|
|
139
|
+
if (!sweepPath) die("report: missing --sweep <sweep.json>");
|
|
140
|
+
if (!existsSync(sweepPath)) die("report: sweep not found: " + sweepPath);
|
|
141
|
+
const outIdx = argv.indexOf("--out");
|
|
142
|
+
const outPath = outIdx >= 0 && outIdx + 1 < argv.length ? argv[outIdx + 1] : (opts.out || makeOut("report.pdf").replace(/pursr-[^-]+-shot.png$/, "report.pdf"));
|
|
143
|
+
if (outPath && outPath !== "-") mkdirSync(dirname(outPath), { recursive: true });
|
|
144
|
+
const titleIdx = argv.indexOf("--title");
|
|
145
|
+
const title = titleIdx >= 0 && titleIdx + 1 < argv.length ? argv[titleIdx + 1] : undefined;
|
|
146
|
+
const noEmbed = argv.includes("--no-embed");
|
|
147
|
+
const summary = JSON.parse(readFile(sweepPath, "utf8"));
|
|
148
|
+
const { renderSweepPdf } = await import("../src/report.js");
|
|
149
|
+
const buf = await renderSweepPdf(summary, { out: outPath === "-" ? undefined : outPath, title, embedImages: !noEmbed });
|
|
150
|
+
console.log(JSON.stringify({ ok: true, sweep: sweepPath, out: outPath, bytes: buf.length }, null, 2));
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
121
153
|
case "every-viewport": {
|
|
122
154
|
if (!url) die("missing url");
|
|
123
155
|
const outDir = (b && !b.startsWith("--")) ? b : undefined;
|
|
@@ -154,7 +186,7 @@ await loadPlugins(pluginPaths);
|
|
|
154
186
|
break;
|
|
155
187
|
}
|
|
156
188
|
case "baseline": {
|
|
157
|
-
//
|
|
189
|
+
// baseline <sub> [...args]
|
|
158
190
|
// sub=list -> list baselines
|
|
159
191
|
// sub=save <project> <png> <step> [--id <id>] [--url <u>] [--meta-json <file>]
|
|
160
192
|
// sub=approve <project> <png> <step> [--id <id>] [--url <u>]
|
|
@@ -195,7 +227,7 @@ await loadPlugins(pluginPaths);
|
|
|
195
227
|
break;
|
|
196
228
|
}
|
|
197
229
|
case "auth": {
|
|
198
|
-
//
|
|
230
|
+
// auth <sub> [...args]
|
|
199
231
|
// save <project> <name> --from <state.json>
|
|
200
232
|
// load <project> <name> --out <state.json>
|
|
201
233
|
// list [project]
|
|
@@ -268,7 +300,7 @@ await loadPlugins(pluginPaths);
|
|
|
268
300
|
const sel = b; if (!sel) die("snap: missing <selector>");
|
|
269
301
|
const flags = parseFlags(argv.slice(4));
|
|
270
302
|
const { runSnap, approveSnapsAsBaselines } = await import("../src/snap.js");
|
|
271
|
-
const outDir = flags.out || makeOut("snaps").replace(/
|
|
303
|
+
const outDir = flags.out || makeOut("snaps").replace(/pursr-[^-]+-snap\.png$/, "snaps");
|
|
272
304
|
const snap = await runSnap({ url, selector: sel, outDir, name: flags.name, max: flags.max, flags });
|
|
273
305
|
console.log(JSON.stringify({
|
|
274
306
|
url: snap.url,
|
package/package.json
CHANGED
|
@@ -1,92 +1,95 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pursr",
|
|
3
|
-
"version": "0.
|
|
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.",
|
|
6
|
-
"homepage": "https://github.com/0xheycat/pursr",
|
|
7
|
-
"bugs": "https://github.com/0xheycat/pursr/issues",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/0xheycat/pursr.git"
|
|
11
|
-
},
|
|
12
|
-
"funding": "https://github.com/sponsors/0xheycat",
|
|
13
|
-
"type": "module",
|
|
14
|
-
"bin": {
|
|
15
|
-
"pursr": "./bin/pursr.mjs",
|
|
16
|
-
"pursr-mcp": "./bin/pursr-mcp.mjs"
|
|
17
|
-
},
|
|
18
|
-
"main": "./src/index.js",
|
|
19
|
-
"exports": {
|
|
20
|
-
".": "./src/index.js",
|
|
21
|
-
"./plugin": "./src/plugin.js",
|
|
22
|
-
"./plugins/*": "./plugins/*.js",
|
|
23
|
-
"./util": "./src/util.js",
|
|
24
|
-
"./runway": "./src/runway.js",
|
|
25
|
-
"./selector": "./src/selector.js",
|
|
26
|
-
"./overlays": "./src/overlays.js",
|
|
27
|
-
"./viewport": "./src/viewport.js",
|
|
28
|
-
"./dom-snapshot": "./src/dom-snapshot.js",
|
|
29
|
-
"./plugin-audit": "./src/plugin-audit.js",
|
|
30
|
-
"./selector-heal": "./src/selector-heal.js",
|
|
31
|
-
"./ci-output": "./src/ci-output.js",
|
|
32
|
-
"./mcp": "./src/mcp.js",
|
|
33
|
-
"./baseline": "./src/baseline.js",
|
|
34
|
-
"./sweep-schema": "./src/sweep-schema.js",
|
|
35
|
-
"./mcp-resources": "./src/mcp-resources.js",
|
|
36
|
-
"./har": "./src/har.js",
|
|
37
|
-
"./auth": "./src/auth.js",
|
|
38
|
-
"./watch": "./src/watch.js",
|
|
39
|
-
"./snap": "./src/snap.js"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
},
|
|
84
|
-
"
|
|
85
|
-
"playwright-core":
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "pursr",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"private": false,
|
|
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
|
+
"homepage": "https://github.com/0xheycat/pursr",
|
|
7
|
+
"bugs": "https://github.com/0xheycat/pursr/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/0xheycat/pursr.git"
|
|
11
|
+
},
|
|
12
|
+
"funding": "https://github.com/sponsors/0xheycat",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"pursr": "./bin/pursr.mjs",
|
|
16
|
+
"pursr-mcp": "./bin/pursr-mcp.mjs"
|
|
17
|
+
},
|
|
18
|
+
"main": "./src/index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/index.js",
|
|
21
|
+
"./plugin": "./src/plugin.js",
|
|
22
|
+
"./plugins/*": "./plugins/*.js",
|
|
23
|
+
"./util": "./src/util.js",
|
|
24
|
+
"./runway": "./src/runway.js",
|
|
25
|
+
"./selector": "./src/selector.js",
|
|
26
|
+
"./overlays": "./src/overlays.js",
|
|
27
|
+
"./viewport": "./src/viewport.js",
|
|
28
|
+
"./dom-snapshot": "./src/dom-snapshot.js",
|
|
29
|
+
"./plugin-audit": "./src/plugin-audit.js",
|
|
30
|
+
"./selector-heal": "./src/selector-heal.js",
|
|
31
|
+
"./ci-output": "./src/ci-output.js",
|
|
32
|
+
"./mcp": "./src/mcp.js",
|
|
33
|
+
"./baseline": "./src/baseline.js",
|
|
34
|
+
"./sweep-schema": "./src/sweep-schema.js",
|
|
35
|
+
"./mcp-resources": "./src/mcp-resources.js",
|
|
36
|
+
"./har": "./src/har.js",
|
|
37
|
+
"./auth": "./src/auth.js",
|
|
38
|
+
"./watch": "./src/watch.js",
|
|
39
|
+
"./snap": "./src/snap.js",
|
|
40
|
+
"./report": "./src/report.js",
|
|
41
|
+
"./ai-diff": "./src/ai-diff.js"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"bin",
|
|
45
|
+
"src",
|
|
46
|
+
"plugins",
|
|
47
|
+
"plans",
|
|
48
|
+
"assets",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"start": "node bin/pursr.mjs",
|
|
54
|
+
"test": "node --test \"test/*.test.js\"",
|
|
55
|
+
"smoke": "node bin/pursr.mjs viewports"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18"
|
|
59
|
+
},
|
|
60
|
+
"keywords": [
|
|
61
|
+
"visual-qa",
|
|
62
|
+
"visual-regression",
|
|
63
|
+
"screenshot",
|
|
64
|
+
"audit",
|
|
65
|
+
"accessibility",
|
|
66
|
+
"axe-core",
|
|
67
|
+
"playwright",
|
|
68
|
+
"mcp",
|
|
69
|
+
"model-context-protocol",
|
|
70
|
+
"baseline",
|
|
71
|
+
"diff",
|
|
72
|
+
"har",
|
|
73
|
+
"testing",
|
|
74
|
+
"devtools",
|
|
75
|
+
"pursr"
|
|
76
|
+
],
|
|
77
|
+
"license": "MIT",
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"axe-core": "^4.12.1",
|
|
80
|
+
"pdfkit": "^0.19.1",
|
|
81
|
+
"pixelmatch": "^5.3.0",
|
|
82
|
+
"pngjs": "^7.0.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"playwright-core": "*"
|
|
86
|
+
},
|
|
87
|
+
"peerDependenciesMeta": {
|
|
88
|
+
"playwright-core": {
|
|
89
|
+
"optional": true
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"devDependencies": {
|
|
93
|
+
"playwright-core": "^1.61.0"
|
|
94
|
+
}
|
|
95
|
+
}
|