Skip to main content
Glama

Why pursr?

Most teams need four separate tools to do visual QA: a screenshot CLI, a regression diff runner, an accessibility auditor, and a way to share captures with an AI assistant. pursr is all four - built as a single Node.js package with:

  • A unified CLI (pursr) for every capture, diff, sweep, and audit.

  • An MCP stdio server (pursr-mcp) so Claude Code, Cursor, and Continue can take screenshots, run sweeps, and inspect prior captures as MCP resources.

  • A library with 30+ named exports and 16 subpath modules, so you can embed it in your own tooling.

  • A plugin system for custom viewports, sweep ops, and capture hooks.

  • Zero browser bundled - drives your system Chrome via Playwright. No 200 MB Chromium download.

Related MCP server: PagePixels Screenshots MCP Server

Install

npm install pursr
npm install --save-dev playwright-core   # peer dep - bring your own Chrome

Then verify:

pursr viewports         # list 10+ registered viewport presets
pursr probe https://example.com   # health check

30 seconds

# 1. Capture a screenshot with overlays
pursr shoot https://example.com shot.png \
  --preset desktop-1280 --grid --grid-tile 64

# 2. Save it as a visual baseline
pursr baseline save myapp shot.png home --url https://example.com

# 3. Next time you run, compare against the baseline
pursr diff https://example.com \
  ~/.pursor/baselines/myapp/<id>/home.png \
  diff.png

# 4. Or: run a batched sweep + a11y audit + parallel workers
pursr sweep ./plan.json   # see plans/ for an example

Features

Feature

Description

CLI flag

Multi-viewport capture

10+ presets (mobile, tablet, desktop, ultrawide)

--preset mobile-375

Layered states

entity / terrain / hud / ui isolation

--layer entity

Animation freeze

pause CSS/JS animations for stable frames

--no-animation

Cursor overlay

pointer / grab / grabbing / crosshair

--cursor crosshair

Grid overlay

spacing guides, custom color + tile size

--grid --grid-tile 64

Camera control

zoom + pan via mouse wheel/drag

--zoom 1.5 --panX 200

Frame timeline

N captures at intervalMs for animations

pursr frames <url> 8 200

Hover capture

text=/role=/aria=/placeholder= matchers

pursr hover <url> "text=Login"

Pixel diff

pixelmatch against any reference PNG

pursr diff <url> <ref>

Visual baselines

save / approve / diff with stable IDs

pursr baseline save ...

Parallel sweep

opt-in worker pool across independent steps

{ "parallel": 4 }

Accessibility audit

axe-core WCAG 2.1 AA + highlighted screenshot

pursr audit <url>

DOM snapshot

serialized HTML + computed styles + selector map

pursr dom <url>

Sweep plans

JSON-driven batch with per-step ops

pursr sweep plan.json

HTML report

dark-themed grid of every capture + meta

auto-generated index.html

CI output

JUnit XML, GitHub Actions annotations, Markdown

written on every sweep

Auto-heal selectors

fallback chain + named matchers

["text=Login", "#login"]

HAR capture

HAR 1.2 spec, written next to your shot

--har ./req.har.json

Auth state

Playwright storageState, reuse logged-in sessions

--auth-state admin

Plugins

custom viewports, sweep ops, before/after hooks

pursr-plugin-*

MCP server

7 tools + resources/list & resources/read for Claude/Cursor

npx pursr-mcp

CLI

# Health check
pursr probe https://example.com

# Screenshot (simple)
pursr shot https://example.com ./out/shot.png

# Rich capture: viewport preset + cursor + grid
pursr shoot https://example.com \
  --preset desktop-1280 \
  --cursor crosshair \
  --grid --grid-tile 64

# Isolate a layer
pursr layer https://example.com entity

# Animation timeline
pursr frames https://example.com 8 200 ./frames/

# Hover an element
pursr hover https://example.com "text=Login"

# Pixel diff vs reference
pursr diff https://example.com ./ref.png ./out/diff.png

# Batched plan
pursr sweep ./plan.json

# Accessibility audit
pursr audit https://example.com --tags wcag2a,wcag2aa

# DOM + selector map snapshot
pursr dom https://example.com

# HAR capture during a shoot
pursr shoot https://example.com shot.png --har ./req.har.json

# Auth state reuse
pursr shoot https://my.app/dashboard shot.png \
  --auth-state admin --auth-project myapp

# Visual baselines
pursr baseline save myapp shot.png home --url https://example.com
pursr baseline list myapp
pursr baseline approve myapp ./new.png home --url https://example.com

# Plan validation
pursr validate ./plan.json

Subcommands

Subcommand

Purpose

probe

Health check (HTTP status, page title)

shot / full

Viewport / full-page screenshot

eval

Execute JS in the page, return result

click / type / wait / seq

Interaction primitives

diff

Pixel-level diff vs a reference PNG

viewports

List all registered viewport presets

shoot

Rich capture (overlays, freeze, camera, plugins)

layer

Capture one isolated layer (entity/hud/ui/terrain)

frames

N-frame animation timeline at interval

hover

Hover state capture

sweep

Batched capture plan -> HTML report + CI output

audit

axe-core WCAG accessibility audit + highlighted screenshot

dom / dom-snapshot

Serialized DOM + CSS selectors + XPath + bounding rects

every-viewport

Capture once per preset in parallel (3-wide pool)

baseline

save / list / approve / show visual baselines

auth

save / load / list / delete Playwright storageState

validate

Validate a sweep plan JSON without running it

MCP Server

pursr-mcp exposes every capability as MCP tools over stdio - works with Claude Code, Cursor, Continue, and any MCP host.

npx pursr-mcp
# or with verbose logging:
npx pursr-mcp --verbose

Exposed Tools

Tool

Description

pursr_shoot

Rich screenshot capture (viewport, grid, layer, cursor, camera, animation freeze, HAR)

pursr_diff

Pixel-diff a URL against a reference PNG

pursr_sweep

Execute a batch sweep plan

pursr_frames

Capture an N-frame animation timeline

pursr_probe

Health-check a URL

pursr_audit

axe-core WCAG audit + highlighted screenshot

pursr_dom_snapshot

Full DOM + selector map snapshot

Exposed Resources

URI

Description

`pursr://shoot/<url

preset>`

pursr://sweep/<plan-name>

Last sweep summary JSON (application/json)

Resources are persisted to ~/.pursor/mcp/mcp-index.json (override with PURSOR_MCP_STATE).

Visual Regression Baselines

pursr baseline save myapp ./out/shoot.png home --url https://my.app
pursr baseline approve myapp ./out/shoot.png home --url https://my.app
pursr baseline list myapp
pursr baseline show myapp home --url https://my.app

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.

import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
const id = diffKey({ url: "https://my.app", viewport: { width: 1280, height: 800, dpr: 1 }, flags: { preset: "desktop-1280" } });
saveBaseline({ project: "myapp", id, step: "home", png: "./shot.png", meta: { url: "https://my.app" } });

Sweep Plan Validation

pursr validate ./plan.json
# { "valid": false, "errors": ["steps[2].frames.count: must be a number between 1 and 120"] }

Catches: empty steps, unknown ops, out-of-range numbers, duplicate names, missing required fields. pursr sweep runs the same validator before executing - fail-fast.

{
  "name": "homepage-matrix",
  "base": "https://example.com",
  "parallel": 4,
  "steps": [
    { "name": "baseline",   "shoot":  { "preset": "desktop-1280" } },
    { "name": "grid-64",    "shoot":  { "preset": "desktop-1280", "grid": true, "grid-tile": 64 } },
    { "name": "tablet",     "shoot":  { "preset": "tablet-768" } },
    { "name": "mobile",     "shoot":  { "preset": "mobile-375" } },
    { "name": "hover-cta",  "hover":  { "selector": ["text=Get started", "a.btn-primary"] } },
    { "name": "audit",      "audit":  { "tags": "wcag2a,wcag2aa" } },
    { "name": "diff",       "diff":   { "ref": "baseline" } }
  ]
}

HAR Capture

pursr shoot https://example.com shot.png --har ./out/req.har.json
import { startHarCapture, stopHarCapture, writeHar } from "pursr/har";
const state = await startHarCapture(page);
await page.goto(url);
const har = stopHarCapture(page);
await writeHar(har, "./out/req.har.json");

Output is HAR 1.2 spec - pipe to har-cli, perf-tools, or any visualizer.

Auth State

pursr auth save myapp admin --from ./playwright-state.json
pursr shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp
pursr auth list myapp
pursr auth load myapp admin --out ./round-trip.json
pursr auth delete myapp admin

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 }.

Parallel Sweep

Add parallel: N to your plan to run steps concurrently in a worker pool:

{
  "name": "matrix",
  "base": "https://my.app",
  "parallel": 4,
  "steps": [
    { "name": "home",    "shoot": { "preset": "desktop-1280" } },
    { "name": "pricing", "shoot": { "preset": "desktop-1280" } },
    { "name": "docs",    "shoot": { "preset": "desktop-1280" } }
  ]
}

Steps run in a shared browser context; results are still ordered by index in the summary. Defaults to serial (parallel: 1) - opt in only when steps are independent.

Accessibility Audit

pursr audit https://example.com --tags wcag2a,wcag2aa
# Writes: audit.json, audit-summary.md, audit-highlighted.png

Injects axe-core, runs a configurable tag set (wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice), and overlays a red outline on every violating node with the rule id as a label. The summary Markdown includes per-rule failure snippets.

DOM Snapshot

pursr dom https://example.com
# Writes: dom-snapshot-<ts>.dom.json

Captures serialized HTML, computed CSS for every visible element, and a selector map (id, role, accessible name, text, xpath, css selector, viewport-relative rect). Great for regression diffing without re-running a browser.

CI Output

Every sweep writes three sidecar artifacts alongside sweep.json:

  • sweep.junit.xml - JUnit XML for Jenkins / GitLab / CircleCI

  • sweep.github.json - GitHub Actions annotation file

  • sweep.md - Human-readable Markdown summary with diffs + failures

Library API

import {
  runProbe, runShot, runShoot, runSweep, runDiff, runAudit,
  captureDomSnapshot, resolveHealedSelector,
  saveBaseline, diffKey,
  startHarCapture, stopHarCapture, writeHar,
  loadAuthState,
  PursorMCPServer, loadMcpConfig,
  validateSweepPlan,
  listResources, readResource,
  listViewports, resolveViewport, VIEWPORTS,
  loadPlugins, registerPlugin, getSweepOp,
  VERSION,
} from "pursr";

Subpath exports

import { resolveLocator } from "pursr/selector";
import { launch } from "pursr/runway";
import { parseFlags, asNum } from "pursr/util";
import { overlayGrid } from "pursr/overlays";
import { captureDomSnapshot } from "pursr/dom-snapshot";
import { runAudit } from "pursr/plugin-audit";
import { resolveHealedSelector } from "pursr/selector-heal";
import { writeCiOutput } from "pursr/ci-output";
import { diffKey, saveBaseline, loadBaseline } from "pursr/baseline";
import { validateSweepPlan } from "pursr/sweep-schema";
import { startHarCapture, stopHarCapture } from "pursr/har";
import { saveAuthState, loadAuthState } from "pursr/auth";
import { listResources, readResource } from "pursr/mcp-resources";
import { PursorMCPServer } from "pursr/mcp";

Plugins

A plugin is a plain ES module that exports a default object:

// plugins/my-plugin.js
export default {
  name: "my-plugin",
  viewport: { "my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" } },
  sweepOp: {
    lighthouse: async (ctx, opts) => { /* ... */ },
  },
  beforeShoot: async (ctx) => { /* mutate ctx.flags / ctx.viewport */ },
  afterShoot:  async (ctx, meta) => { /* augment sidecar */ },
  flagHelp:    { "my-flag": "what it does" },
};

Plugins are auto-loaded from plugins/ (built-in) or via --plugin <path>.

Architecture

src/
  index.js          - public library entry
  mcp.js            - MCP stdio server (JSON-RPC 2.0)
  shoot.js          - runShoot (overlays + camera + frame-stable)
  sweep.js          - runSweep (validated, parallel pool)
  diff.js           - pixelmatch wrapper
  plugin-audit.js   - axe-core injection + highlighted screenshot
  dom-snapshot.js   - full DOM + CSSOM + selector map
  selector-heal.js  - auto-heal chain resolver
  ci-output.js      - JUnit / GitHub / Markdown
  baseline.js       - visual regression storage
  har.js            - HAR 1.2 network capture
  auth.js           - Playwright storageState
  sweep-schema.js   - plan validator
  mcp-resources.js  - MCP resources adapter
  overlays.js       - page-side CSS overlays + camera
  runway.js         - Playwright launcher + system-Chrome detector
  viewport.js       - built-in viewport presets
  selector.js       - text=/role=/aria=/placeholder= parser
  plugin.js         - plugin registry + hook runner
  util.js           - flags, args, hashing, HTML escape, renderSweepHtml
  every-viewport.js - one shot per preset in parallel
  frames.js, hover.js, shot.js, eval.js, probe.js, interact.js

Development

git clone https://github.com/0xheycat/pursr
cd pursr
npm install
npm install --save-dev playwright-core
npm test

npm test runs 53 unit + integration tests (Node's built-in test runner, zero test deps). Coverage includes: viewport resolution, flag parsing, selector parsing, HTML escaping, hashing, baseline storage, sweep-plan validation, MCP resources, HAR 1.2 shape, auth state, and end-to-end CLI smoke tests.

src/           - 25 modules
test/          - 53 tests, 0 failures
plugins/       - 2 built-in plugins, auto-loaded

Roadmap

  • Visual baselines (save / approve / diff)

  • Sweep plan schema validation

  • MCP resources (browse past captures from your AI host)

  • HAR 1.2 capture

  • Auth state (Playwright storageState)

  • Parallel sweep workers

  • Watch mode (pursr watch <url>)

  • Component-level snapshot (pursr snap <selector>)

  • PDF report export

  • Cloud output adapters (S3 / GCS)

  • AI diff summary (vision model)

Watch Mode (v0.5.0)

# Re-shoot every time a CSS or HTML file changes
pursr watch https://my.app --on src/**/*.css --on src/**/*.html

# Re-run a sweep plan on file change
pursr watch --plan ./plan.json --on src/**/*.{css,html}

# Default (no --on) = watch everything in cwd
pursr watch https://my.app

Glob patterns: * (one path segment), ** (any depth), ? (one char), backslash-X (literal X). Debounce is 300ms by default.

Component Snapshots (v0.5.0)

# Capture one screenshot per matched element
pursr snap https://my.app a.btn --out ./snaps --max 20

# Use auto-heal selector chain
pursr snap https://my.app "text=Sign up" --out ./snaps

# Promote to baselines in one command
pursr snap https://my.app article.product --baseline myapp

Each capture is clipped precisely to the elements bounding box (even when scrolled offscreen), labelled with aria-label / text / tag, and written to ./snaps/-.png + snap.json summary.


License

MIT (c) 2026 - 0xheycat

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/0xheycat/pursr'

If you have feedback or need assistance with the MCP directory API, please join our Discord server