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/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
1
  #!/usr/bin/env node
2
- // pursor CLI. Thin wrapper around src/* that mirrors the npm bin.
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: 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
@@ -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
- plugins: pursor automatically loads built-in plugins from plugins/.
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 = process.env.PURSOR_URL || a;
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: "pursor", version: VERSION, plugins: listPlugins() }, null, 2));
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": { if (!url) die("missing url"); const ref = b; if (!ref) die("diff: missing <ref.png>"); const out = c || makeOut("diff.png"); const threshold = d !== undefined ? Number(d) : 0.1; const r = await runDiff(url, ref, out, threshold); console.log(JSON.stringify(r, null, 2)); break; }
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
- // pursor baseline <sub> [...args]
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
- // pursor auth <sub> [...args]
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(/pursor-[^-]+-snap\.png$/, "snaps");
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.5.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
- "files": [
42
- "bin",
43
- "src",
44
- "plugins",
45
- "plans",
46
- "assets",
47
- "README.md",
48
- "LICENSE"
49
- ],
50
- "scripts": {
51
- "start": "node bin/pursor.mjs",
52
- "test": "node --test \"test/*.test.js\"",
53
- "smoke": "node bin/pursor.mjs viewports"
54
- },
55
- "engines": {
56
- "node": ">=18"
57
- },
58
- "keywords": [
59
- "visual-qa",
60
- "visual-regression",
61
- "screenshot",
62
- "audit",
63
- "accessibility",
64
- "axe-core",
65
- "playwright",
66
- "mcp",
67
- "model-context-protocol",
68
- "baseline",
69
- "diff",
70
- "har",
71
- "testing",
72
- "devtools",
73
- "pursr"
74
- ],
75
- "license": "MIT",
76
- "dependencies": {
77
- "axe-core": "^4.12.1",
78
- "pixelmatch": "^5.3.0",
79
- "pngjs": "^7.0.0"
80
- },
81
- "peerDependencies": {
82
- "playwright-core": "*"
83
- },
84
- "peerDependenciesMeta": {
85
- "playwright-core": {
86
- "optional": true
87
- }
88
- },
89
- "devDependencies": {
90
- "playwright-core": "^1.61.0"
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
+ }