npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pursor

v0.3.0

Published

Visual QA & audit & MCP for the browser. Capture, sweep, and review any web target with multi-viewport, layered, animated, hover, grid, and cursor states.

Readme

pursor

Visual QA & audit CLI + library + MCP server for the browser. Capture, diff, sweep, and audit any web target — with multi-viewport, layered states, hover, grid overlays, animation freeze, camera control, axe-core accessibility audit, CI output, and auto-healing selectors.

npx pursor probe https://example.com
npx pursor shoot https://example.com --preset mobile-375 --grid
npx pursor sweep ./plan.json
npx pursor audit https://example.com --tags wcag2a,wcag2aa
npx pursor-mcp   # MCP stdio server for Claude Code / Cursor

Install

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

pursor does not bundle Chromium. It drives your system Chrome via Playwright. No extra browser downloads.


Table of Contents


CLI

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

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

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

# Isolate a layer (entity / terrain / hud / ui)
pursor layer https://example.com entity

# Animation timeline: 8 frames at 200ms
pursor frames https://example.com 8 200 ./frames/

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

# Pixel diff vs a reference screenshot
pursor diff https://example.com ./ref.png ./out/diff.png

# Batched plan (see plans/ for examples)
pursor sweep ./plan.json

# Accessibility audit (requires: npm i axe-core)
pursor audit https://example.com --tags wcag2a,wcag2aa

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

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 |


MCP Server

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

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

Exposed Tools

| Tool | Description | |---|---| | pursor_shoot | Full screenshot with viewport, grid, layer, cursor, camera, freeze | | pursor_diff | Pixel diff vs reference PNG + diff overlay | | pursor_sweep | Execute a batch plan JSON → summary | | pursor_frames | Animation frame timeline | | pursor_probe | Health-check a URL | | pursor_audit | axe-core accessibility audit | | pursor_dom_snapshot | DOM + CSSOM + selector map + bounding rects |

Config

Config via PURSOR_MCP_CONFIG env var (inline JSON or file path) or ~/.pursor/mcp-config.json:

{
  "plugins": ["./my-plugin.js"],
  "defaultOutDir": "./mcp-output",
  "verbose": true
}

MCP Host Examples

Claude Code:

{
  "mcpServers": {
    "pursor": {
      "command": "npx",
      "args": ["pursor-mcp"]
    }
  }
}

Cursor:

{
  "mcpServers": {
    "pursor": {
      "command": "npx",
      "args": ["pursor-mcp", "--verbose"]
    }
  }
}

Accessibility Audit

Run axe-core WCAG audits on any URL. Optionally captures a highlighted screenshot with violated elements outlined in red.

# Quick audit with default tags (wcag2a, wcag2aa, wcag21a, wcag21aa, best-practice)
pursor audit https://example.com

# Specific WCAG tags
pursor audit https://example.com --tags wcag2a,wcag2aa

# Custom output directory
pursor audit https://example.com ./audit-report/

Output:

  • audit.json — full axe-core results with violation summary
  • audit-summary.md — readable Markdown report with severity breakdown
  • audit-highlighted.png — screenshot with violations visibly marked

Sweep plan usage:

{
  "name": "accessibility-check",
  "base": "https://example.com",
  "steps": [
    {
      "name": "wcag-audit",
      "audit": {
        "tags": "wcag2a,wcag2aa,wcag21aa",
        "screenshot": true
      }
    }
  ]
}

DOM Snapshot

Every capture can optionally produce a .dom.json sidecar with complete page structure — useful for debugging visual regression without opening a browser.

pursor dom https://example.com

Captured data:

  • domdocument.documentElement.outerHTML
  • selectorMap[] — every visible element with:
    • tag, id, css (CSS selector), xpath
    • role, ariaLabel, ariaRole, text, placeholder, alt, href, src
    • rect — viewport-relative bounding box {x, y, w, h}
    • visible — visibility flag
  • styles — computed stylesheet rules keyed by selector
  • viewport — current viewport dimensions + DPR

Programmatic:

import { captureDomSnapshot } from "pursor";

const snapshot = await captureDomSnapshot({
  url: "https://example.com",
  out: "./snapshot.dom.json",
});
console.log(snapshot.selectorMap.length, "elements found");

CI Output

Sweep plans automatically generate CI-compatible output files alongside the HTML report — no extra config needed.

pursor sweep ./plan.json
# Produces in the output directory:
#   sweep.json       — raw summary
#   index.html       — visual HTML dashboard
#   sweep.junit.xml  — JUnit XML (GitLab CI, Jenkins, CircleCI)
#   sweep.github.json — GitHub Actions annotations format
#   sweep.md         — Markdown summary

GitHub Actions integration

- name: Visual QA
  run: npx pursor@latest sweep ./plan.json
- name: Annotate
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const { annotations } = JSON.parse(fs.readFileSync('sweep-output/sweep.github.json'));
      annotations.forEach(a => core.error(a.message, {file: a.filename, title: a.title}));

JUnit in GitLab CI

visual-qa:
  script: npx pursor@latest sweep ./plan.json
  artifacts:
    reports:
      junit: sweep-output/sweep.junit.xml

Auto-heal Selectors

In sweep plans, selectors can be an array of fallback strategies. pursor tries each one in order until a visible element is found:

{
  "name": "login-flow",
  "base": "https://example.com",
  "steps": [
    {
      "name": "click-login",
      "hover": {
        "selector": [
          "text=Login",
          "button[type=submit]",
          "#login-btn",
          "a[href*='login']"
        ]
      }
    }
  ]
}

Supported selector types:

  • text=Login — Playwright text locator (substring, or text==Login for exact)
  • text~regex — regex text match
  • role=button|Submit — ARIA role with accessible name
  • aria=label — accessibility label
  • placeholder=Email — placeholder text
  • CSS selectors — any valid CSS selector as fallback

Sweep Plans

Batch capture plans in JSON. Each step runs one operation.

{
  "name": "checkout-flow",
  "base": "https://example.com",
  "outDir": "./sweep-checkout",
  "steps": [
    { "name": "homepage",   "shoot": { "preset": "desktop-1280", "cursor": "default" } },
    { "name": "mobile-view","shoot": { "preset": "mobile-375", "grid": true } },
    { "name": "nav-hover",  "hover": { "selector": "text=Products", "settleMs": 400 } },
    { "name": "add-to-cart","frames": { "count": 6, "intervalMs": 200 } },
    { "name": "diff",       "diff":  { "ref": "baseline" } }
  ]
}

Step operations: shoot, hover, frames, diff, audit, or any registered plugin sweep-op.


Plugin API

Extend pursor with custom viewport presets, sweep operations, or capture hooks:

export default {
  name: "my-plugin",
  viewport: {
    "my-laptop": { width: 1440, height: 900, dpr: 2, label: "MBP 14" },
  },
  sweepOp: {
    lighthouse: async (ctx, opts) => {
      // run lighthouse audit, write result at ctx.out
      return { score: 95 };
    },
  },
  beforeShoot: async (ctx) => { /* mutate ctx.flags */ },
  afterShoot:  async (ctx, meta) => { /* augment sidecar */ },
};
pursor shoot https://example.com --plugin ./my-plugin.js

Publish as pursor-plugin-* for auto-discovery.

Built-in plugins

  • plugin-audit — adds audit sweep-op (axe-core WCAG audit) and every-viewport sweep-op (capture at every preset). Adds audit-canvas viewport preset.
  • plugin-demo — Reference implementation showing every plugin API hook: viewport presets, nav sweep-op (navbar walker), beforeShoot/ afterShoot sidecar augmentation, and flag help.

All CLI flags

| Flag | Type | Default | Description | |---|---|---|---| | --preset | string | desktop-1280 | Named viewport preset | | --width / --height | number | — | Custom viewport size | | --dpr | number | 1 | Device pixel ratio | | --cursor | string | default | pointer, grab, crosshair, none | | --grid | bool | false | Overlay grid | | --grid-tile | number | 64 | Grid tile size (px) | | --grid-color | string | rgba(255,0,255,0.35) | Grid line color | | --layer | string | all | entity, terrain, hud, ui | | --no-animation | bool | false | Freeze CSS animations | | --no-hud | bool | false | Hide HUD elements | | --wait-frame | number | 600 | Wait ms for canvas stability | | --zoom | number | 1 | Zoom level | | --panX / --panY | number | 0 | Camera pan offset (px) | | --full | bool | false | Full-page (not just viewport) | | --tags | string | — | Comma-separated WCAG tags for audit | | --plugin | path | — | Load a plugin file (repeatable) | | @file | prefix | — | Read next arg from file |


Library API

All functions available as named or default import:

import { runShoot, runSweep, runAudit, captureDomSnapshot } from "pursor";
// Or:
import PurrVisual from "pursor";

Capture functions

| Function | Returns | Never throws | |---|---|---| | runShoot({url, out, flags?, prepare?, browser?}) | { url, out, ts, status, title, viewport, flags, error? } | ✅ | | runShot(url, out, opts?) | { url, out, status, title, fullPage } | — | | runProbe(url) | { url, status, title, navError, viewport } | — | | runFrames({url, count?, intervalMs?, outDir?, flags?, browser?}) | { url, files[], viewport, ... } | — | | runHover({url, selector, out, flags?}) | { url, out, selector, viewport, ... } | — | | runDiff(url, refPath, out, threshold?, browser?) | { url, refPath, numDiff, diffPct, equal, error? } | — | | runWait(url, selector, timeoutMs?) | { url, selector, found, timeoutMs } | — | | runClick(url, selector, out?) | { url, selector, clicked, out } | — | | runType(url, selector, text, out?) | { url, selector, text, typed, out } | — | | runSeq(url, actionsJson, out?) | { url, out, steps[], failed? } | — | | runEval(url, js, out?) | { url, result, out, ... } | — | | runSweep(planPath, outDir?) | { name, steps[], outDir, ... } | ✅ (per-step) | | runAudit({url, tags?, outDir?, screenshot?, flags?}) | { url, violations, violationSummary, highlightedScreenshot?, ... } | — | | captureDomSnapshot({url, out, flags?}) | { url, title, dom, selectorMap[], styles, viewport } | — |

Viewport helpers

| Export | Description | |---|---| | listViewports() | All registered presets (built-in + plugin) | | resolveViewport(flags) | Resolve --preset / --width / --height to viewport object | | VIEWPORTS | Built-in preset map | | applyCamera(page, opts) | Zoom/pan via mouse wheel + drag on canvas | | waitForStableFrame(page, ms) | Poll canvas until stable for ms |

Plugin system

| Export | Description | |---|---| | loadPlugins(paths?) | Auto-load built-in plugins + user paths | | registerPlugin(plugin) | Register a plugin manually | | listPlugins() | Names of loaded plugins | | getSweepOp(name) | Get a registered sweep operation | | getViewportPreset(name) | Get a registered viewport preset | | listViewportPresets() | All plugin-registered presets | | getFlagHelp() | All plugin-registered flag descriptions |

Selector healing

| Export | Description | |---|---| | resolveHealedSelector(page, selector, opts?) | Try selector chain, return first visible match | | healStepAction(page, action) | Mutate action.selector → resolved selector |

CI output

| Export | Description | |---|---| | writeCiOutput(summary, dir) | Write JUnit XML + GitHub annotations + Markdown |

MCP Server

| Export | Description | |---|---| | PurrVisualMCPServer | MCP stdio server class | | loadMcpConfig() | Load config from env or ~/.pursor/mcp-config.json | | MCP_VERSION | MCP protocol version string |

Low-level (plugin authors)

| Export | Source | |---|---| | launch() / newPage(browser, viewport) | runway.js | | resolveLocator(page, selector) / parseTextSelector(s) | selector.js | | parseFlags(argv) / asNum(v, dflt) / asBool(v, dflt) | util.js | | nowIso() / shortHash(buf) / escapeHtml(s) | util.js | | readArg(arg) / makeOut(name) / findStepPng(dir, name) | util.js | | renderSweepHtml(summary) | util.js |

Subpath exports

import { resolveLocator } from "pursor/selector";
import { launch } from "pursor/runway";
import { parseFlags } from "pursor/util";
import { overlayGrid } from "pursor/overlays";
import { captureDomSnapshot } from "pursor/dom-snapshot";
import { runAudit } from "pursor/plugin-audit";
import { resolveHealedSelector } from "pursor/selector-heal";
import { writeCiOutput } from "pursor/ci-output";
import { PurrVisualMCPServer } from "pursor/mcp";

Sidecar JSON

Every capture writes a .json sidecar next to its PNG with metadata (url, viewport, flags, timestamp, file size, SHA1 hash). DOM snapshots write .dom.json with full element map. Audit reports write full axe-core results to audit.json.


Development

git clone <this repo>
cd pursor
npm install
npm install --save-dev playwright-core
npm test

All 32 tests use Node's built-in test runner. Coverage: unit tests for viewport resolution, flag parsing, selector parsing, HTML escaping, hashing, and end-to-end smoke tests for the full CLI pipeline.

src/           — 22 modules
test/          — 32 tests, 0 failures
plugins/       — 2 built-in plugins, auto-loaded

Visual Regression Baselines

# Save a baseline (computed id from url+viewport+flags)
pursor baseline save myapp ./out/shoot.png home --url https://my.app

# Approve a new baseline from a current capture
pursor baseline approve myapp ./out/shoot.png home --url https://my.app

# List all baselines
pursor baseline list myapp

# Show a specific baseline
pursor baseline show myapp home --url https://my.app

Baselines are stored under ~/.pursor/baselines/<project>/<id>/. 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. Use in code:

import { diffKey, saveBaseline, loadBaseline } from "pursor/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

pursor 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. pursor sweep runs the same validator before executing — fail-fast.

MCP Resources

The MCP server now exposes resources/list and resources/read so Claude Code / Cursor can browse past captures:

uri: pursor://shoot/<url|preset>
uri: pursor://sweep/<plan-name>

Resources are persisted to ~/.pursor/mcp/mcp-index.json (override with PURSOR_MCP_STATE) and re-listed on every server start.

Library additions (v0.2.0)

| Export | Description | |---|---| | diffKey, saveBaseline, loadBaseline, listBaselines, approveBaseline | Visual regression baseline storage | | validateSweepPlan, registerSweepOp | Sweep plan schema validation | | listResources, readResource, recordResource | MCP resource adapter |

Subpath exports: pursor/baseline, pursor/sweep-schema, pursor/mcp-resources.


HAR Capture (v0.3.0)

# Capture network traffic during a shoot
pursor shoot https://example.com shot.png --har ./out/req.har.json

In code:

import { startHarCapture, stopHarCapture, writeHar } from "pursor/har";
const state = await startHarCapture(page);
await page.goto(url);
// ... your capture logic ...
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 (v0.3.0)

# Save a state file you exported from Playwright/DevTools
pursor auth save myapp admin --from ./playwright-state.json

# Use it in a shoot
pursor shoot https://my.app/dashboard shot.png --auth-state admin --auth-project myapp

# Inspect / delete
pursor auth list myapp
pursor auth load myapp admin --out ./round-trip.json
pursor 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 (v0.3.0)

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.


License

MIT