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

offbook

v0.3.0

Published

**Teach a browser task once. Replay it forever, for free.**

Readme

offbook

Teach a browser task once. Replay it forever, for free.

You (or your coding agent) walk through a web task one time. offbook leaves behind an ordinary Playwright script, checked into your repo, that does the same thing every day after that — no LLM, no tokens, no API bill. When the site changes and the script breaks, a bounded repair loop puts it back on its feet.

Inspired by Microsoft Webwright: pay the model once, then run for nothing. In the theater, an actor who is off book no longer needs the script in hand.

agent writes script.ts ──► replays free, forever
        │                        │
        │                selector breaks?
        │                        ▼
        │              classify → diagnose → one bounded
        └──────────────  AI repair → re-verify → done
                         (anything else fails closed)

Install

bun add offbook        # or: npm install offbook

Works under Bun and Node ≥ 22.6. A stealth Chromium (cloakbrowser) downloads itself the first time you launch a browser.

Check your setup any time:

offbook doctor

Quick start: your first automation

1. Create a task

offbook new price-check

That scaffolds two files. These two files are the whole model — everything else is cache you can delete.

tasks/price-check/
├── plan.md      ← what "done" means, in plain language
└── script.ts    ← the Playwright script that does it

2. Say what "done" means

Open tasks/price-check/plan.md and write the task plus a checklist of Critical Points — the things that must be true for the run to count:

# Task
Get the current price of the book on example-store.com/book/123.

# Critical Points
- [ ] CP1: Product page for book 123 is loaded
- [ ] CP2: Price is captured from the page

Critical Points aren't decoration. They're the contract the script is checked against, and they're what the repair loop reads when something breaks.

3. Write the script

tasks/price-check/script.ts starts as a working template. Fill in the middle:

import { openBrowser } from "offbook/browser";
import { act, installFailureHandler } from "offbook/act";
import { openLog, resolveRunDir, screenshot } from "offbook/trace";

installFailureHandler();

const RUN_DIR = resolveRunDir();
const log = openLog(RUN_DIR);

const { page, close } = await openBrowser();
try {
  // CP1: load the product page
  await page.goto("https://example-store.com/book/123", { waitUntil: "domcontentloaded" });
  await screenshot(page, RUN_DIR, 1, "product_page");
  log(1, "product page loaded");

  // CP2: capture the price
  const price = await act(2, page.locator(".price"), (el) => el.innerText());
  await screenshot(page, RUN_DIR, 2, "price_visible");
  log(2, `price: ${price}`);

  log.final(price);
} finally {
  await close();
}

Three things earn their keep here:

  • act(step, locator, fn) wraps any interaction that matters. If it times out, offbook re-probes the page and tells you which selector broke and how — missing, hidden, or unclickable. That classification is what makes a break automatically repairable later.
  • screenshot(...) — one per Critical Point. Proof the contract held.
  • log.final(value) — the answer the run produces.

4. Run it

offbook run price-check

Every run writes a step log and screenshots into a fresh evidence directory. Nothing overwrites anything.

offbook log price-check     # what happened last run
offbook runs price-check    # every run's evidence dir
offbook diff price-check    # what changed between two runs

5. Log in once, replay headless forever

Most useful tasks are behind a login. Do it visibly, one time:

OFFBOOK_HEADLESS=0 offbook run price-check

A real browser window opens; log in by hand. Auth state persists in ~/.offbook/profiles/, so every later run replays headless with no login step. Profiles live outside your repo — they hold cookies, and cookies never belong in git.

6. When the site changes

Sites drift. Selectors rot. Instead of debugging by hand:

offbook repair price-check

offbook runs the task, and only if it fails on a selector it recognizes, it lets an AI make exactly one repair attempt and re-runs to verify. Anything else — login walls, MFA prompts, a page that silently returns different data — fails closed and waits for you. Details in Self-repair below.


Turning a task into a reusable CLI

The task above is hardcoded to book 123. Usually you want to run the same automation with different inputs. Add a # Parameters table to plan.md:

# Parameters
| name    | type   | default | format         |
|---------|--------|---------|----------------|
| bookId  | string | "123"   | numeric string |

Then shape script.ts as one exported function plus a CLI wrapper:

import { parseArgs } from "node:util";

export async function priceCheck(args: { bookId: string }): Promise<{ price: string }> {
  /**
   * Get the current price of a book on example-store.com.
   *
   * @param bookId The product id. Default: "123".
   * @returns Object with key `price` (string).
   */
  const { page, close } = await openBrowser();
  try {
    await page.goto(`https://example-store.com/book/${args.bookId}`, { waitUntil: "domcontentloaded" });
    const price = await act(2, page.locator(".price"), (el) => el.innerText());
    return { price };
  } finally {
    await close();
  }
}

// CLI wrapper — only runs when the file is executed directly, never on import
if (import.meta.url === `file://${process.argv[1]}`) {
  const { values } = parseArgs({ options: { bookId: { type: "string", default: "123" } } });
  console.log(await priceCheck({ bookId: values.bookId! }));
}

Now it's both a library function and a command-line tool:

node tasks/price-check/script.ts --bookId 456

Two rules make this work: running with no arguments must reproduce the original task exactly, and importing the file must not launch a browser. Things genuinely fixed for the site (the base URL, the selector strategy) stay hardcoded — they're not parameters.

If you're driving a coding agent, /offbook:craft <task> produces this shape for you; /offbook:run <task> produces the one-shot version. The full authoring protocol for agents is in SKILL.md.


All CLI commands

offbook new <task>      # scaffold plan.md + script.ts
offbook run <task>      # execute; fresh evidence dir per run
offbook repair <task>   # run; on an eligible failure, one AI repair attempt
offbook log <task>      # latest run's step log
offbook runs <task>     # list run evidence dirs
offbook diff <task>     # compare two runs
offbook list            # tasks in ./tasks
offbook doctor          # validate setup (--fix downloads the browser binary)

Useful environment variables:

| Variable | What it does | |---|---| | OFFBOOK_HEADLESS=0 | Show the browser window (first login) | | OFFBOOK_PROFILE | Named profile, default "default" | | OFFBOOK_ENGINE | Browser engine (see below) | | OFFBOOK_PROXY | Proxy URL | | OFFBOOK_MODEL | Repair model, e.g. anthropic/claude-sonnet-5 |


Where things live

| Path | What | Lifecycle | |---|---|---| | tasks/<name>/ | plan.md + script.ts | committed to git | | ~/.cache/offbook/runs/<task>/run_N/ | step log + screenshots | prunable evidence | | ~/.local/share/offbook/artifacts/<task>/run_N/ | downloads, exports | yours to consume, then delete | | ~/.offbook/profiles/<name>/ | browser auth state | machine-local, never in a repo |

Only the first row belongs in version control. Everything else is regenerable, and the durable-artifacts row is where files a task downloads wait for you to pick them up.


Browser engines

offbook drives five interchangeable engines. Switch with --engine on offbook run, the OFFBOOK_ENGINE env var, or openBrowser({ engine }). Default is cloakbrowser — if you don't care, you never have to think about this section.

Why you might: to A/B whether a site's bot detection is actually triggering, or to run against a hosted browser service instead of a local one.

| Option | cloakbrowser | camoufox | playwright | obscura | cdp | |---|---|---|---|---|---| | Persistent profile | native | native | native | storageState | storageState | | humanize | ✅ | ✅ | — | — | — | | proxy | ✅ | ✅ | ✅ | ✅ | service-side | | locale / timezone | ✅ | locale | emulation | — | — | | launchArgs | ✅ | — | ✅ | — | — | | headless | ✅ | ✅ | ✅ | headless-only | remote |

Unsupported options warn once and are ignored — switching engines never errors out on you. Validate any engine with offbook doctor --engine <name> (or --engine all).

proxy accepts http://user:pass@host:port; embedded credentials are supported on every engine that supports proxy at all, but not uniformly: cloakbrowser and obscura's own HTTP client parse userinfo out of the URL themselves, while playwright's adapter splits it into separate username/password fields before handing it to Chromium — Chromium ignores credentials embedded in proxy.server and silently fails auth (407) if they aren't split out. This split happens once, centrally, in parseProxy() (lib/browser.ts), so every adapter agrees on the shape.

cloakbrowser (default) — stealth Chromium

offbook run mytask
const { page, close } = await openBrowser(); // engine: "cloakbrowser"

camoufox — stealth Firefox (optional)

bun add camoufox-js         # then fetch the browser build per camoufox-js README
offbook run mytask --engine camoufox
const { page, close } = await openBrowser({ engine: "camoufox", humanize: true });

playwright — vanilla Chromium (detection control)

offbook run mytask --engine playwright

The un-stealthed baseline. If a site blocks playwright but passes cloakbrowser/camoufox, the stealth layer is doing its job.

obscura — Rust CDP engine

# Install from github.com/h4ckf0r0day/obscura/releases (or set OFFBOOK_OBSCURA_BIN)
offbook run mytask --engine obscura

offbook spawns obscura serve on a free port and connects, then shuts it down with the browser. Point --cdp-url/OFFBOOK_CDP_URL at an already-running instance to skip the spawn. obscura has no headed mode, so headless warns once and is ignored. --proxy gets the full URL string as-is (including user:pass@ if present) — obscura's Rust HTTP client parses embedded userinfo and builds the Proxy-Authorization header itself, so credentials work without any splitting on offbook's side.

cdp — any hosted CDP endpoint

Cloudflare Browser Run (Kitesurf):

offbook run mytask --engine cdp \
  --cdp-url "wss://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/browser-run/devtools/browser?browser=kitesurf" \
  --cdp-header "Authorization: Bearer <CF_API_TOKEN>"

browserless:

offbook run mytask --engine cdp --cdp-url "wss://chrome.browserless.io?token=<TOKEN>"
const { page, close } = await openBrowser({
  engine: "cdp",
  cdpUrl: process.env.CDP_URL,
  cdpHeaders: { Authorization: `Bearer ${process.env.CF_TOKEN}` },
});

Prefer OFFBOOK_CDP_HEADERS (a JSON object) over --cdp-header for real secrets — command-line arguments are visible to other users via ps. Header values are never logged. offbook doctor and connection-failure errors redact userinfo (user:pass@) and sensitive query params (token, apiKey, access_token, key, auth) out of any CDP URL before printing it.

One exception offbook can't close: the obscura engine accepts a proxy only as a --proxy command-line argument, so a proxy URL with embedded credentials is visible to other local users via ps for the life of that process. Use an unauthenticated or IP-allowlisted proxy with obscura, or run it yourself and connect with --cdp-url.

Two things to know about the CDP-based engines (obscura, cdp): they persist cookies and localStorage per profile but not service workers or IndexedDB, so some sites re-challenge you across runs. And on affected Bun versions they run their script under node automatically — Playwright's connectOverCDP handshake hangs under Bun (oven-sh/bun#32526, fixed in Bun 1.4.0; every release so far is older, so the fallback is currently always on). The check is version-gated, so a Bun ≥ 1.4.0 keeps these engines on Bun with no offbook change. offbook run/offbook repair switch runtimes for you; if you execute a script by hand on an affected Bun, use node script.ts. Escape hatch: OFFBOOK_ALLOW_BUN_CDP=1.


Developer reference

Everything the CLI does is an exported module. Skip this section unless you're embedding offbook in another system.

Run a task programmatically

import { allocateRunWorkspace, runTask } from "offbook/runner";

const workspace = allocateRunWorkspace({
  task: "cibc-export",
  artifactRoot: "/my/app/intake",     // durable outputs land here, run_N per run
});
const result = await runTask({ taskDir: "tasks/cibc-export", workspace });
// { status: "ok" | "failed", exitCode, workspace, failure?, logTail? }

No process.exit, no globals: structured results, and run numbers are never reused while evidence or durable artifacts survive. Library callers pass artifactRoot explicitly, so downloaded bytes land where your system already looks instead of in a directory nobody watches.

Instrument scripts for repairability

import { act, installFailureHandler } from "offbook/act";

installFailureHandler();
await act(3, page.locator("button.submit"), (b) => b.click());

When a wrapped interaction times out, act re-probes the locator and throws a classified failure — selector-not-found, selector-not-visible, or selector-not-actionable — with the selector and step attached. That classification is what makes a failure eligible for repair.

Self-repair: how the safety comes from structure

import { runWithRepair } from "offbook/repair";
import { aiRepairProvider } from "offbook/repair-ai";

const report = await runWithRepair({
  taskDir: "tasks/cibc-export",
  artifactRoot: "/my/app/intake",
  provider: aiRepairProvider(),       // model from OFFBOOK_MODEL / API keys
});
// outcome: "ok" | "repaired" | "failed-closed"

The bound is structural, not prompted: only the three selector failure kinds ever reach a provider, the provider is called exactly once, and a second failing run fails closed. Auth changes, MFA, semantic drift, and anything unclassified fail closed immediately. The orchestrator never runs git and never applies patches itself — your code decides whether a proposal lands.

The built-in provider is a minimal agent whose tool whitelist is the safety boundary: read the plan, the script, and sanitized diagnostics; probe live pages only on origins already present in the script; propose exact-match edits; finish. Edits revert on every refusal path.

OFFBOOK_MODEL="anthropic/claude-sonnet-5"          # or openai/... or openrouter/...
# defaults: ANTHROPIC_API_KEY → claude-sonnet-5, OPENROUTER_API_KEY → gpt-5.6-luna

Bring your own provider by implementing one method:

import type { RepairProvider } from "offbook/repair";
const provider: RepairProvider = { repair: async (request) => ({ status: "refused", changedFiles: [], summary: "..." }) };

Safe forensics

import { collectDiagnostics, defaultScrubbers } from "offbook/sanitize";

Forensic capture is bounded by default — full-DOM dumps are explicit opt-in. Repair requests carry only SanitizedDiagnostics: the classified failure, a bounded log tail, a sanitized ARIA snapshot, and screenshot names. Never cookies, storage, input values, or downloaded artifacts. Opt-in scrubbers mask digit runs, emails, and token blobs.

All exports

offbook/browser (persistent-profile browser launch across all five engines; locale, timezone, downloads, humanization presets) · offbook/runner · offbook/act · offbook/errors · offbook/trace (step log, screenshots, forensics) · offbook/plan (parse plan.md, correlate evidence to Critical Points) · offbook/sanitize · offbook/repair · offbook/repair-ai · offbook/workspace · offbook/paths · offbook/diff

Agent workflow

SKILL.md documents the full authoring protocol for coding agents: plan the task as Critical Points, explore with scratch scripts, author with act() instrumentation, execute, then self-verify each Critical Point against screenshot evidence before calling it done. references/engines.md covers engine selection in the same detail as above.

Requirements

  • Bun ≥ 1.1 or Node ≥ 22.6
  • macOS or Linux (Chromium binary via cloakbrowser)