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

@apisurf/wire

v0.1.4

Published

wire — record a script's HTTP traffic into a local SQLite file, then query it.

Downloads

526

Readme

@apisurf/wire

Run a script, get every HTTP request, WebSocket frame and server-sent event it made in a local SQLite file, then query it.

wire execute ./scripts/sync.ts              # quiet: only the script's own output
wire execute ./scripts/sync.ts --verbose    # ...plus a run summary on stderr
wire ls requests --run 1                    # what did it call?
wire get request 42 --json                  # one request, compact
wire sql "SELECT method, host, path, status, duration_ms FROM v_requests
         ORDER BY duration_ms DESC LIMIT 10"

No server, no daemon, no account. One file (./wire.sqlite) that you can copy, delete, or open in any SQLite client.

Install

pnpm add -D @apisurf/wire        # or: npm i -D @apisurf/wire
pnpm wire execute ./scripts/sync.ts

Needs Node >= 20.12.

How it works

wire execute compiles your JS/TS entry point in memory with esbuild and runs it with globalThis.fetch, globalThis.WebSocket and globalThis.EventSource instrumented, writing every request, frame and event to the database — created and migrated if it does not exist.

Traffic that bypasses those globals for node:http — axios's default adapter, for one — is not captured.

wire execute is quiet by default: it prints only what the script prints, plus the stack trace if it throws. --verbose adds, on stderr, each failed check as it happens and a run summary:

$ wire execute ./flow.ts --verbose

  run 1  ·  3 requests  ·  1 failed  ·  5 checks passed  ·  78 B  ·  22 ms wall
        3  api.example.com

  default  ·  .env
  ./wire.sqlite
  wire sql "SELECT method, host, path, status, duration_ms
           FROM v_requests WHERE run_id = 1 ORDER BY seq"
  wire ls checks --run 1

Commands

wire execute, schema, get, ls, env and sql. Each takes -h, --help; all but env take --db <path> (default ./wire.sqlite). wire help checks is a topic page rather than a command.

$ wire --version
wire 0.1.0 (schema 4, node v22.22.2)

The schema number is the one to compare against wire schema when a database written by a newer build refuses to open. --json prints the parts.

wire execute <file>

There does not have to be a file: -e runs source directly and - reads it from stdin. Inline source is treated like a file in the working directory — bare imports resolve against the same node_modules, top-level await works, and the snippet is stored on the run (wire get run <id> --fields entry_source).

wire execute ./scripts/sync.ts
wire execute -e 'console.log((await fetch("https://example.com")).status)'
echo 'await fetch(process.env.API + "/health")' | wire execute - --env staging

| Option | | | ------------------------ | -------------------------------------------------------------------------------- | | -e, --eval <code> | Run this source instead of a file | | --env <name> | Also read .env.<name> and .env.<name>.local. Errors if neither exists | | --dotenv <path> | Read exactly these files instead of discovering any. Repeatable; later files win | | --no-dotenv | Read no .env files. The shell and --var only | | --var <key=value> | Override one variable. Repeatable. Beats the shell | | --label <text> | Name this run. Default: the entry filename, or inline for -e and stdin | | --tag <key=value> | Tag every capture in this run. Repeatable. Query them in tags | | --sample-rate <0..1> | Fraction of requests to capture. Default 1 | | --max-body-bytes <n> | Truncate payloads above this size. Default 1048576 | | --redact-header <name> | Mask this header's value. Repeatable | | --redact-body <path> | Mask a dot-path in JSON payloads, e.g. user.*.token. Repeatable | | --no-redact | Disable redaction entirely, including the defaults | | --verbose | Print failed checks as they happen and a run summary, to stderr. Off by default | | --quiet | The default. Accepted so existing commands keep working | | --ui | Open the browse UI after the run. Needs @apisurf/wireui |

authorization, cookie and set-cookie are masked unless --no-redact. The command exits with the script's own exit code. It is quiet by default; with --verbose the summary goes to stderr. Without it, the run you just made is wire ls runs --limit 1.

wire get <entity> <id...> and wire ls <entity>

Reads without SQL. Every read is projected to an explicit field list, and no default list contains a payload column — a body is reached through a bounded preview, or by naming text.

$ wire ls requests --run 1
id  seq  method  host             path         status  duration_ms
──  ───  ──────  ───────────────  ───────────  ──────  ───────────
 1    0  POST    api.example.com  /auth/login     200        6.516
 2    1  GET     api.example.com  /products       200         1.48
 3    2  GET     api.example.com  /missing        404         0.61

3 requests

| Entity | Default fields | | ---------------------- | ------------------------------------------------------------------ | | run / runs | id, uid, label, started_at, exit_status, request_count | | request / requests | id, seq, method, host, path, status, duration_ms | | check / checks | id, run_id, seq, name, status, message | | body / bodies | id, size, stored_size, truncated, is_text, preview | | message / messages | id, request_id, seq, direction, name, bytes | | header / headers | request_id, kind, name, value — keyed by request id, many per id |

Both take --fields <a,b,c> (or --fields '*'), --max-field-bytes <n> (default 4096, 0 disables), --max-width <n>, --json and --csv. ls adds --limit <n> (default 20, 0 for no limit), --run <id> (requests, messages, checks) and --request <id> (messages, headers). Run either with no entity for the index.

wire get request 42 43 44 --fields status,duration_ms,url
wire get body 7 --fields size,is_text,preview
wire ls messages --request 12 --limit 100

wire sql "<query>"

Any statement SQLite accepts. The file is opened without migrating it, so a query never changes the schema. Rows go to stdout, counts and errors to stderr; exits 1 if the database is missing or the query fails.

$ wire sql "SELECT host, COUNT(*) n, ROUND(AVG(duration_ms),1) avg_ms
           FROM v_requests GROUP BY host"
host             n  avg_ms
───────────────  ─  ──────
api.example.com  3     2.9

1 row (0.91 ms)

--json, --csv, --max-width <n> (default 60, 0 disables) and --ui.

Query the views, not the raw tables:

| | | | ------------ | -------------------------------------------------------------------------------------------------- | | v_runs | One per execute — label, entry, env, timings, exit status, counters | | v_requests | One per HTTP request or socket connection — method, host, path, status, phase timings, byte counts | | v_headers | request_id, kind ('request' \| 'response'), ord, name, value | | v_bodies | id, size, stored_size, truncated, is_text, text | | v_messages | WebSocket frames and SSE events, with direction and payload | | v_checks | Assertions the run made — name, mode, status, message | | tags | request_id, key, value — from execute --tag | | body_fts | FTS5 index over text payloads. MATCH it, then join on body_id |

wire schema [relation]

With no argument, the views worth querying; with a name, that relation's columns and types — read out of the file at runtime, so it describes the database in hand. --all includes base tables, --json adds the entity map get and ls use.

wire env [name]

The .env files a run reads, and what they set. --json to parse it. Exits 1 when a named environment has no .env file of its own. No value is ever printed — wire cannot tell a hostname from a token, and assumes the token.

Environments

A script reads process.env.API and the like; wire fills that in from .env files before the script is imported, so it imports nothing and is passed nothing. Files are read from one directory — the nearest above the working directory that has any of them:

.env                shared with the repo
.env.local          yours alone — gitignore it
.env.<name>         read when --env <name> is passed
.env.<name>.local   yours alone, for that environment

Precedence, lowest first:

.env  <  .env.local  <  .env.<name>  <  .env.<name>.local  <  the shell  <  --var

The shell beating every file is what makes a committed .env safe: CI exports the real token, the file holds a placeholder. An empty value from the shell counts as unset; an empty value in a file is a value.

WIRE_ENV, WIRE_RUN_UID, WIRE_DB and WIRE_ENTRY are always injected. The environment name is stored on the run: SELECT ... FROM v_runs WHERE env = 'staging'.

Checks

A recording says a request returned 200 in 40 ms. Only the script knows whether that was the right answer. @apisurf/wire/kit writes that judgement into the run:

import { assert, check } from "@apisurf/wire/kit";

const res = await fetch(`${process.env.API}/auth/login`, { method: "POST", body });
assert("login returns 200", res.ok, `expected 200, got ${res.status}`);

const session = await res.json();
check("token is a JWT", session.accessToken?.split(".").length === 3);
check("expires within an hour", session.expiresIn <= 3600, `expiresIn was ${session.expiresIn}`);

check records the outcome and returns the boolean; a failure never ends the run and never changes the exit code, so one run reports every failure it found. assert records the outcome and then throws CheckFailedError — the run ends as any uncaught error would (exit_status 'error', exit code 1) — and TypeScript narrows on it. The failure is written before the throw, so an aborted run still explains itself.

Both are inert when nothing is recording, so a script that uses them still runs under plain node. isRecording() says which it is.

A failed check prints nothing unless the run was started with --verbose, which prints each failure as it happens and again in the run summary.

wire ls checks --run 1
wire sql "SELECT run_id, status, message FROM v_checks
         WHERE name = 'login returns 200' ORDER BY ts DESC"

Full page: wire help checks.

Browse UI

The UI is a separate package — @apisurf/wireui, the wireui command — so that installing the CLI does not drag a React app and a server along with it. wire execute --ui and wire sql --ui are convenience wrappers that run wireui --db <path> and hold the terminal until Ctrl-C.

npm i -g @apisurf/wireui
wireui                        # opens ./wire.sqlite

License

ISC. Full documentation: https://github.com/apisurf/wire#readme