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

playwright-dsl

v0.2.0

Published

Minimal DSL + MCP server for AI agents to drive Playwright — one action per line, snapshot-first workflow

Downloads

75

Readme

playwright-dsl

Minimal DSL for AI agents to drive Playwright. Use it as a library, a CLI, an MCP server, or embed it in your own toolkit (e.g. mypry for fullstack debugging).

npm install playwright-dsl playwright

Three ways to use it: as a library (execute/Session), as a CLI (playwright-dsl run), or as an MCP server (playwright-dsl-mcp).

import { execute, connect, dispose } from 'playwright-dsl';

const handle = await connect({ cdpEndpoint: 'http://localhost:9222' });

const result = await execute(`
  fill "textbox Email" "[email protected]"
  fill "textbox Password" "s3cret"
  click "button Sign in"
  wait "#dashboard" visible
  expect url contains "/dashboard"
  extract "#handle" as handle
`, { page: handle.page, refMap: {} });

await dispose(handle);

Philosophy

  1. Snapshot-first. The agent takes a snapshot (ARIA tree), sees what's on the page, and copies selectors directly. No guessing.
  2. One action per line. No structures, no indentation, no YAML. Cheapest format in tokens for any LLM to generate and humans to read.
  3. No if/for. If the agent needs to branch, it executes a chunk, takes a snapshot, receives the new state, and emits the next chunk.
  4. Source-code selectors welcome. If the agent already knows selectors from the codebase (#email, .btn-submit), use them directly — no snapshot needed, saves tokens.

Targeting

Every verb that acts on an element receives a target as its first argument. All targets must be quoted strings.

Snapshot refs (recommended — copy from snapshot)

Take a snapshot, see what's there, copy the role + name into quotes:

| Snapshot shows | Write in script | |---|---| | button "Sign In" | click "button Sign In" | | textbox "Email" | fill "textbox Email" "alice" | | link "Dashboard" | click "link Dashboard" | | heading "Welcome" | expect "heading Welcome" visible | | checkbox "Accept" | check "checkbox Accept" | | combobox | select "combobox" "Spain" |

All ARIA roles are supported: button, textbox, link, heading, checkbox, combobox, listitem, tab, dialog, menuitem, radio, slider, switch, treeitem, etc.

Prefixed selectors

Explicit prefixes that map to Playwright's user-facing locators:

| You write | Playwright API | When to use | |---|---|---| | "role:button=Sign in" | getByRole('button', { name: 'Sign in' }) | Explicit role (same as snapshot ref) | | "label:Email" | getByLabel('Email') | Inputs by their <label> | | "placeholder:Search" | getByPlaceholder('Search') | Inputs by placeholder | | "text:Welcome" | getByText('Welcome') | Visible text | | "testid:submit" | getByTestId('submit') | If your app has data-testid | | "alt:Logo" | getByAltText('Logo') | Images by alt text | | "title:Close" | getByTitle('Close') | By title attribute |

CSS / native selectors (fallback)

If you know selectors from the source code, use them directly:

click "#submit"
fill "#email" "[email protected]"
fill "input[name=username]" "alice"
click ".btn-primary"
expect "nav >> text=Logout" visible

Priority: snapshot refs → role:label:text:testid: → CSS → xpath:. The first ones survive style and structure changes; the last ones are fragile.


Full syntax

# comment until end of line

# ── navigation ──
goto https://example.com
goto https://example.com networkidle    # optional waitUntil
back
forward
reload

# ── interactions (target = "quoted selector") ──
click "button Save"
click "role:button=Save"
dblclick ".row"
rclick "#item"                           # right-click
fill "textbox Email" "[email protected]"
clear "#email"
type "#search" "playwright"              # types char by char (fires key events)
type "Enter"                             # no target → types on the focused element
press "#search" Enter                    # key on an element
press Escape                             # global key
press "Control+A"                        # combos
select "combobox" "España"
check "checkbox Accept terms"
uncheck "#newsletter"
hover ".menu"
focus "#email"
upload "#file" "/tmp/report.pdf"

# ── scroll ──
scroll "#footer" into                    # scrollIntoView
scroll page down 800                     # wheel: down|up by N px
scroll page to 0                         # to absolute Y coordinate

# ── waits ──
wait "button Submit" visible             # states: visible|hidden|enabled|attached|detached
wait "#spinner" hidden
wait 500ms                               # absolute time (use sparingly)
waitload networkidle                     # load|domcontentloaded|networkidle
waiturl "/dashboard"                     # wait for URL to match

# ── assertions (auto-retry until timeout) ──
expect "heading Welcome" visible
expect "#status" contains "OK"
expect ".item" count 10
expect "#email" equals "[email protected]"
expect url contains "/dashboard"
expect title matches "Inbox \\(\\d+\\)"

# ── extraction to variables ──
extract "#username" as user              # textContent by default
extract "#email" value as email          # value of an input
extract ".link" href as url              # href attribute
extract "#card" html as snippet          # innerHTML
extract ".item" count as howMany         # number of matches
extract "#card" attr "data-id" as cardId # arbitrary attribute

# ── interpolation (in strings and barewords) ──
fill "#email" "${username}@example.com"
goto https://x.com/${path}

# ── dialogs (alert/confirm/prompt) ──
ondialog accept                          # accept the NEXT dialog
ondialog accept "text for prompt"
ondialog dismiss                         # cancel
ondialog default                         # revert to default behavior

# ── downloads ──
download "#export" as "data.csv"         # click and save to downloadDir
download "role:button=Download"          # uses the suggested filename

# ── multi-tab / popups ──
tab open second https://other.com        # open and switch to tab "second"
tab switch main                          # switch back
tab close second
tab list                                 # returns {tabs, active}
popup as checkout                        # capture the popup

# ── screenshots ──
screenshot "full.png" full               # full page
screenshot "#card" "card.png"            # element screenshot
screenshot                               # auto name: step-N.png

# ── control break for the agent ──
snapshot                                 # runtime returns; re-snapshot externally

# ── escape hatch ──
eval "document.title = 'patched'"
eval "window.scrollTo(0, 0)" as _        # run JS, discard result
eval "someGlobalVar" as val              # run JS, save result

Verb reference

| Verb | Form | |---|---| | goto | goto <url> [load\|domcontentloaded\|networkidle\|commit] | | back / forward / reload | history navigation | | click / dblclick / rclick | click <target> | | fill | fill <target> "<text>" | | clear | clear <target> | | type | type [<target>] "<text>" | | press | press [<target>] <Key> (e.g. Enter, Control+A) | | select | select <target> "<value>" | | check / uncheck | check <target> | | hover / focus | hover <target> | | upload | upload <target> "<path>" | | scroll | scroll <target> into / scroll page <up\|down\|to> [N] | | wait | wait <target> [state] / wait <N>ms | | waitload | waitload [load\|domcontentloaded\|networkidle] | | waiturl | waiturl "<substring-or-regex>" | | expect | see table below | | extract | extract <target> [prop\|attr "<n>"] as <name> | | ondialog | ondialog accept ["text"] / dismiss / default | | download | download <target> [as "<file>"] | | tab | tab open <id> [url] / switch <id> / close [id] / list | | popup | popup as <id> | | screenshot | screenshot [<target>] ["<file>"] [full] | | snapshot | snapshot — control break | | eval | eval "<js>" [as <var>] |

expect operators

| On | Operators | |---|---| | <target> | visible, hidden, enabled, disabled, checked, contains "x", equals "x", count N | | url | equals, contains, matches (regex) | | title | equals, contains, matches |

Variables

extract and eval ... as save variables. Use ${var} in any string:

extract "#price" as price
fill "#total" "${price}"
goto https://api.example.com/${path}

CLI

# Inline:
playwright-dsl run -e 'goto https://example.com' -e 'screenshot "out.png"' --headless

# From file:
playwright-dsl run script.as --cdp-endpoint http://localhost:9222

# From stdin:
echo 'goto https://example.com' | playwright-dsl run - --headless

Full help: playwright-dsl --help.

playwright-dsl run [<script.as>|-] [options]
playwright-dsl run -e "<line>" [-e "<line>"...] [options]
playwright-dsl parse [<script.as>|-]

Options:
  --cdp-endpoint <url>   Connect to existing Chrome via CDP
  --headless             Headless when launching (no CDP)
  --page-url <regex>     With CDP: pick page whose URL matches
  --vars <file>          JSON file with initial ${vars}
  --session <file>       Persist vars across runs
  --timeout <ms>         Per-step timeout (default 10000)
  --output <file>        Write RunResult as JSON to file
  --json                 Print RunResult as JSON to stdout
  --verbose, -v          Log each step

CDP flow (attach to your real browser)

# 1. Open Chrome with remote debugging
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/profile

# 2. Run against the page already open (preserves logins, cookies)
playwright-dsl run script.as --cdp-endpoint http://localhost:9222 -v

Persistent session (multiple scripts, one browser)

google-chrome --remote-debugging-port=9222 &

# Script 1 — login
playwright-dsl run --cdp-endpoint http://localhost:9222 --session s.json \
  -e 'fill "label:Email" "[email protected]"' \
  -e 'fill "label:Password" "s3cret"' \
  -e 'click "button Sign in"'

# Script 2 — same browser, same cookies, same ${vars}
playwright-dsl run --cdp-endpoint http://localhost:9222 --session s.json \
  -e 'goto https://site/settings' \
  -e 'fill "#token" "${token}"'

MCP server

AgentScript ships an MCP server: playwright-dsl-mcp. Your agent sees the page via browser_snapshot (ARIA tree) and acts via browser_run.

Registration

Claude Code:

claude mcp add playwright-dsl -- npx -y playwright-dsl-mcp

Antigravity / Cursor / Codex (mcp.json):

{
  "mcpServers": {
    "playwright-dsl": {
      "command": "npx",
      "args": ["-y", "playwright-dsl-mcp"],
      "env": { "AGENTSCRIPT_CDP": "http://localhost:9222" }
    }
  }
}

Tools

| Tool | Description | |---|---| | browser_connect | Attach (CDP) or launch a browser | | browser_snapshot | ARIA snapshot as YAML — how the agent "sees" the page | | browser_run | Execute a script. Returns steps, failure, and extracted vars | | browser_syntax | Full DSL reference (call once) | | browser_disconnect | Close/detach and clear state |

Agent loop

browser_connect { cdpEndpoint: "http://localhost:9222" }
browser_snapshot                          → YAML with roles + names
browser_run { script: "fill \"textbox Email\" \"alice\"\nclick \"button Sign in\"" }
browser_snapshot                          → re-snapshot after navigation
browser_run { script: "extract \"#balance\" as balance" }
  → { ok: true, vars: { balance: "€1,234" }, needsSnapshot: false }
browser_disconnect

Embeddable Toolkit (BrowserToolKit)

Import the toolkit into your own MCP server to add browser tools without running a separate process. This is how mypry adds browser interaction to its debugger:

import { BrowserToolKit } from 'playwright-dsl/toolkit';

const kit = new BrowserToolKit();

// kit.tools       → MCP tool definitions (JSON Schema)
// kit.call(name, args) → execute a tool
// kit.page        → Playwright Page (for direct page.evaluate())
// kit.dispose()   → cleanup

// Register in your MCP server:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [...yourTools, ...kit.tools],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (kit.handles(req.params.name)) {
    return kit.call(req.params.name, req.params.arguments);
  }
  // ...your tools
});

Programmatic API

execute(source, opts)

import { execute } from 'playwright-dsl';
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage();

const result = await execute(script, {
  page,
  refMap: {},
  vars: { username: 'alice' },
  timeoutMs: 10_000,
  onStep: (step, status, detail) => console.log(status, step.raw),
});

// result: { completed, failed?, vars, parseErrors }

connect(opts) / dispose(handle)

import { connect, dispose } from 'playwright-dsl';

// Attach to running Chrome (preserves logins)
const handle = await connect({
  cdpEndpoint: 'http://localhost:9222',
  pageUrlMatch: 'github\\.com',
});

// Or launch fresh
const handle = await connect({ headless: true });

await dispose(handle);

parse(source) (no browser needed)

import { parse } from 'playwright-dsl/parser';
const { steps, errors } = parse(source);

Structure

src/
  parser.ts     # tokenizer + parser → Step[]
  runtime.ts    # executor on Playwright Page
  connect.ts    # CDP / launch helpers
  session.ts    # Session state management
  toolkit.ts    # BrowserToolKit — embeddable MCP tools
  cli.ts        # `playwright-dsl` binary
  mcp.ts        # `playwright-dsl-mcp` MCP server
  index.ts      # public API

playground/     # Vite test app (localhost:5199)
examples/       # login.as, search.as, etc.
test/           # parser + integration tests

License

MIT