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

apicat

v0.3.30

Published

Quick and flexible API tool for Node.js CLI and JavaScript

Readme

apicat is a tiny API caller.

Keep your API definitions in YAML, then list them, inspect them, and fire them off from the CLI or from JavaScript. It is built for quick experiments, repeatable calls, and "what was that curl again?" moments.

⚡ Quick Start

npx apicat <service.name> KEY=VALUE

Or install it locally. Use apic instead of npx apicat.

npm install -g apicat

$ apic ls
$ apic httpbin.get
🔌 apicat v0.3.25 — call APIs (apic)

Commands
  apic <service.name> [k=v …]  Call API with optional params
  apic ls|list [pattern]       List APIs (e.g. apic list "openrouter")
  apic update                  Copy latest published .apicat to ~/.apicat
  apic help [service|pattern]  Show help or search config for pattern
  apic <service.name> --help   Show help for this api call
  apic proxy -p <port> [-P <backend host:port>] [-B|--bearer <env key name>]
                       Forward HTTP requests; -P pins the backend target, -B adds a Bearer auth header from an env var

Options
  apic <service.name> --time           Show request duration
  apic <service.name> --debug          Show fetch request/response info + full response body
  apic <service.name> --response       Output raw response (skip jq filter)
  apic <service.name> --stream         Stream response events / tokens (default if not specified)
  apic <service.name> --no-stream      Disable streaming (wait for full response)
  apic --config <path> httpbin.get     Use custom config file instead of ~/.apicat
  apic -h, --help                      Show help

🤖 Model Context Protocol (MCP) Server for AI Agents

apicat includes an ultra-fast, native Rust MCP server (mcp/) that lets AI coding assistants (Antigravity, Cursor, Claude Desktop, Claude Code, Windsurf, Cline) call and execute API definitions with zero token-reading overhead, sub-5ms cold starts, and in-memory TLS connection pooling.

1. Compile the MCP Binary

cargo build --release --manifest-path mcp/Cargo.toml

The compiled binary will be at mcp/target/release/apicat-mcp.

2. Configure Your AI Agent

Add apicat to your agent's MCP configuration (mcp_config.json, .cursor/mcp.json, or claude_desktop_config.json):

{
  "mcpServers": {
    "apicat": {
      "command": "/absolute/path/to/apicat/mcp/target/release/apicat-mcp"
    }
  }
}

⚡ Empirical Performance & Benchmark Matrix

1. Real-World Autonomous Agent Task Performance

Measured on an AI billing and quota audit task across Fireworks AI and OpenRouter:

| Strategy | Total Wall Time | LLM Turns | Context Token Footprint | Discovery Overhead | | :--- | :---: | :---: | :---: | :--- | | Unassisted Agent (CLI & Search) | 21.0s | 10 turns | ~18,400 tokens | 5 exploratory tool calls | | Skill-Assisted CLI (apic) | ~4.5s | 1–2 turns | ~1,200 tokens | 0 tool calls | | In-Memory Rust MCP Server | ~2.5s | 1 turn | ~650 tokens | 0 calls (6.0x faster, 96.5% token savings) |

2. Three-Way Batch API Execution

Batch execution across four live API calls (catfact.getFact, fireworks.balance, openrouter.key, fireworks.costs):

| Endpoint | Raw curl (Subprocess) | apic CLI (Node.js) | In-Memory Rust MCP | Improvement | | :--- | :---: | :---: | :---: | :---: | | catfact.getFact | 157.8 ms | 390.5 ms | 117.6 ms | 🚀 1.34x faster (+40 ms) | | openrouter.key | 188.3 ms | 358.4 ms | 131.1 ms | 🚀 1.44x faster (+57 ms) | | fireworks.costs | 1,198.7 ms | 1,242.4 ms | 783.1 ms | 🚀 1.53x faster (+416 ms) | | fireworks.balance | 2,748.1 ms | 1,394.6 ms | 1,742.0 ms | 🚀 1.58x faster (+1,006 ms) | | Total Batch (4 Calls) | 4,292.9 ms | 3,385.8 ms | 2,773.8 ms | ⚡ 1.55x faster (+1.52s saved) |

3. MCP Runtime Architecture Comparison

Comparing MCP server implementations across languages:

| Metric | Rust (Native Release) | Bun (TypeScript) | Python 3.14 (Stdlib) | | :--- | :---: | :---: | :---: | | Cold Start & Handshake (Spawn → Init → tools/list) | 4.36 ms | 37.24 ms | 42.79 ms | | Raw JSON-RPC IPC Latency (Stdio Ping) | 0.018 ms (18 µs) | 0.058 ms (58 µs) | 0.028 ms (28 µs) | | Memory Footprint (RSS) | 1.77 MB | 22.83 MB | 17.45 MB | | Host Runtime Dependencies | None (Standalone binary) | Requires Bun/Node | Requires Python |

4. The Impact of In-Memory Connection Pooling (Keep-Alive)

Reusing warm TCP and TLS 1.3 encryption sockets eliminates DNS and handshake latency:

  • Cold Socket (openrouter.key): 4,066 ms
  • Warm Reused Socket (openrouter.key): 468 ms (8.7x speedup)

🤖 Prompting LLMs directly

If you want an AI to learn your raw API definitions without MCP:

Learn api definitions from https://unpkg.com/apicat

🎉 API goodness

  • One command: apic
  • One bundled config file: ~/.apicat
  • HTTP and WebSocket support
  • Declarative WebSocket keep-alive, liveness checks, and reconnect
  • Simple forward proxy: apic proxy -p <port> [-P <backend host:port>]
  • Variables with $VAR and required variables with $!VAR
  • Works as a CLI, a library, and an exported CLI module

🧠 How It Thinks

On first interactive run, it can copy the bundled apicat.yaml to ~/.apicat. Edit to your liking.

When an .apicat file exists in the current directory, it automatically overrides ~/.apicat. If an apicat.yaml file exists in the current directory, those definitions get added to what is in ~/.apicat (or ./.apicat) for the client to use.

Variables can be defined in the call or will be used if named the same in env.

API IDs use <service>.<name> form, like httpbin.get, openai.chat, or echo.ws.

🧰 CLI Cheatsheet

# show the menu
apic

# list available apis
apic ls

# show help for an API
apic openai.chat --help

# use a different config
apic --config ./custom.yaml ls

# time or debug your calls
apic httpbin.get --time
apic httpbin.get --debug
# debug also prints the full raw response body to stderr,
# even when the API's jq filter would normally trim it

# output the raw response, skipping any jq filter
apic httpbin.get --response

# refresh ~/.apicat from the published apicat.yaml
apic update

# forward HTTP requests through a local proxy
# without -P it forwards to the host in each request
apic proxy -p 8080
# with -P it pins the backend: all requests go there
# a bare port (e.g. -P 3000) means localhost:3000
apic proxy -p 8080 -P api.example.com:443

# OpenAI-compatible chat
apic openai.chat \
  OPENAI_URL=https://api.openai.com \
  OPENAI_API_KEY=$OPENAI_API_KEY \
  MODEL=gpt-4o-mini \
  PROMPT="Write a haiku about logs"

# OpenRouter
apic openrouter.chat \
  API_KEY=$OPENROUTER_API_KEY \
  MODEL=openrouter/auto \
  PROMPT="Say hello"

# macOS: use clipboard text as a prompt
PROMPT="$(pbpaste)" apic <service.name>

Key Value Parameters

apic openrouter.chat MODEL="openrouter/auto" PROMPT="Reply with only: ok"

Values will automatically be used if they exist in env. e.g. API_KEY

export API_KEY=...

# In the yaml config
openrouter.chat:
  url: https://openrouter.ai/api/v1/chat/completions
  method: POST
  headers:
    Authorization: "Bearer $!API_KEY"
  body: |
    {
      "model": "$!MODEL",
      "messages": [{"role": "user", "content": "$OPTIONAL_PROMPT"}, {"role": "user", "content": "$!PROMPT"}]
      , "provider": {"order": ["$PROVIDER"]}
    }
  jq: .choices[0].message.content
  help: Create an OpenRouter chat completion. Requires API_KEY, MODEL, and PROMPT.

💻 Use It From Code

Install it locally if you want to import it:

npm install -g apicat

Then:

import { fetchApi, getApis, getRequest } from 'apicat';

const apis = getApis();
console.log(apis.map((api) => api.id));

const req = getRequest('httpbin', 'get');
console.log(req.url);

const res = await fetchApi('httpbin', 'get');
console.log(await res.json());

const chat = await fetchApi('openai', 'chat', {
  vars: {
    OPENAI_URL: 'https://api.openai.com',
    OPENAI_API_KEY: process.env.OPENAI_API_KEY,
    MODEL: 'gpt-4o-mini',
    PROMPT: 'Hello world'
  }
});

console.log(await chat.json());

To load API definitions from a specific file, pass its path as configPath:

const res = await fetchApi('httpbin', 'get', {
  configPath: './custom.yaml'
});

Without configPath, apicat checks for .apicat in the current directory (which overrides ~/.apicat), falls back to ~/.apicat when present (or the bundled apicat.yaml), and merges in any definitions from apicat.yaml found in the current directory.

For an OpenRouter chat completion, pass the values referenced by the openrouter.chat definition in vars:

import { fetchApi } from 'apicat';

const res = await fetchApi('openrouter', 'chat', {
  vars: {
    API_KEY: process.env.OPENROUTER_API_KEY,
    MODEL: 'openrouter/auto',
    OPTIONAL_PROMPT: 'Be concise.',
    PROMPT: 'Give me one interesting cat fact.'
  }
});

if (!res.ok) throw new Error(`OpenRouter request failed: ${res.status} ${await res.text()}`);

const data = await res.json();
console.log(data.choices[0].message.content);

YAML config

Simple! See apicat.yaml.

WebSocket keep-alive and reconnect

WebSocket APIs can be finite request/response calls or long-lived listeners. Long-lived protocols can define heartbeat, liveness, and reconnect behavior declaratively in YAML:

example.listener:
  url: wss://example.com/socket
  capture:
    sequence: .s
    heartbeat_interval: .d.heartbeat_interval / 1000
  keep_alive:
    interval: $heartbeat_interval
    send:
      op: 1
      d: $sequence
    expect:
      op: 11
    timeout: 10
  reconnect:
    enabled: true
    initial: 1
    maximum: 60
    multiplier: 2
    jitter: 0.2

capture values are available to later WebSocket flow steps and heartbeat messages. expect is matched as a partial object, so op: 11 matches any incoming JSON message whose op field is 11.

When called from the CLI, WebSocket connection status is written to stderr with comment-style prefixes:

# Connected to: wss://example.com/socket
# Disconnected (closed)

File uploads

Use file to send a local file as the raw request body. FILE is a convention; any substituted variable can provide the path.

cloudflare.r2Upload:
  url: https://api.cloudflare.com/client/v4/accounts/$!CLOUDFLARE_ACCOUNT_ID/r2/buckets/$!CLOUDFLARE_BUCKET/objects/$!FILE_NAME
  method: PUT
  file: $!FILE
  headers:
    Authorization: Bearer $!CLOUDFLARE_API_KEY
    Content-Type: $CONTENT_TYPE

Use a scoped Cloudflare API token for CLOUDFLARE_API_KEY. FILE is the local path and FILE_NAME is the R2 object key.

Downloads write response bytes to FILE_PATH:

cloudflare.r2Download:
  url: https://api.cloudflare.com/client/v4/accounts/$!CLOUDFLARE_ACCOUNT_ID/r2/buckets/$!CLOUDFLARE_BUCKET/objects/$!FILE_NAME
  method: GET
  headers:
    Authorization: Bearer $!CLOUDFLARE_API_KEY
  output: $!FILE_PATH
apic cloudflare.r2Download FILE_NAME=logo.png FILE_PATH=./downloaded-logo.png

For multipart/form-data APIs, use multipart. A field containing file becomes a file part; all other fields are sent as text.

example.multipartUpload:
  url: https://example.com/upload
  method: POST
  multipart:
    document:
      file: $!FILE
      content_type: $CONTENT_TYPE
    description: $DESCRIPTION

You may set filename alongside file and content_type; otherwise apicat uses the local filename. Do not set a multipart Content-Type header manually—apicat supplies the required boundary.

Slack

slack.all is a built-in CLI command that exports accessible channels, DMs, group DMs, and thread replies as NDJSON. It uses a user token and only reads conversations available through the approved scopes and Slack retention rules. It does not download file attachments. Public channels may be accessible even when you have not joined them.

Create a Slack app at https://api.slack.com/apps using the supplied manifest, then copy its Client ID. The app must enable PKCE and register http://localhost:8765/slack/callback. Workspace policy may require administrator approval. No client secret is needed. Slack documents the desktop localhost exception in its PKCE guide.

apic slack.all CLIENT_ID=123.456 > slack.ndjson
# Subsequent calls use the saved account and automatically refresh its token:
apic slack.all SINCE=2026-10-01 > recent.ndjson
apic slack.all WORKSPACE=T123 CHANNELS=C123,D123 > selected.ndjson
apic slack.all RESUME=true >> slack.ndjson
apic slack.login CLIENT_ID=123.456
apic slack.logout WORKSPACE=T123

With no token, apicat starts a temporary localhost HTTP server and opens Slack's authorization page. It checks OAuth state, exchanges the code using PKCE, closes the server, and continues the command. If the browser cannot open, use the printed URL on the same machine. Login times out after three minutes. SLACK_CLIENT_ID can supply the client ID instead. A different PORT must also be registered in the Slack app. The callback never binds to a public network interface.

Persistent settings live under ~/.apicat-settings/ (directory mode 0700):

  • slack.json: client ID, callback port, default workspace/account, and optional channels array of conversation IDs. CHANNELS overrides this local allowlist.
  • credentials.json: account metadata, plus tokens when Keychain is unavailable. On macOS tokens are stored in Keychain under service apicat.slack when possible. Fallback files are plaintext with mode 0600; all JSON writes are atomic.
  • slack-checkpoints.json: completed conversations and a fixed export cutoff.
  • slack.lock: prevents simultaneous Slack commands from racing token refresh or checkpoints. After a hard crash, remove this directory only when no Slack command is running.

RESUME=true uses the same workspace/user, SINCE, and channel selection as the interrupted export. Append to its original output file. The interrupted conversation is replayed; deduplicate by workspace, channel, and message.ts if necessary (thread broadcasts can also repeat a reply). Successful exports remove their checkpoint. Each output line contains workspace, channel, channel_name, and the original Slack message object. Progress and login URLs go to stderr.

All pages and threads are fetched, including replies to parents older than SINCE. Consequently a narrow time window can still require scanning old channel history. Slack rate limits are honored using Retry-After; large exports can take substantial time. Missing scopes, inaccessible conversations, and pagination failures produce a nonzero exit status rather than silently claiming a complete export. The output is not an atomic Slack snapshot: conversations may change during an export.

slack.logout removes the selected local account's tokens; revoke the app separately in Slack if desired. The JavaScript API supports fetchApi('slack', 'all', { vars: { SINCE: '2026-10-01' } }) and returns a streaming NDJSON Response. Consume its body to complete the export; stream errors indicate an incomplete export. The Rust MCP server delegates these entries to its existing apic subprocess fallback, so the updated Node CLI must be installed on PATH.

Listening for Slack events

apic slack.watch keeps a Socket Mode WebSocket open and writes new message events, thread replies, edits, and deletions as NDJSON. It acknowledges Slack envelopes, reconnects with backoff and fresh connection URLs, and remembers the last 10,000 event IDs across restarts to suppress retries. Ctrl+C stops it and releases its lock.

Open Slack app settings and select the apicat app you created for your workspace. In the left sidebar, enable Socket Mode, then enable Event Subscriptions and subscribe on behalf of users to message.channels, message.groups, message.im, and message.mpim. The supplied manifest includes these settings. Click Save Changes, and complete reinstallation if Slack prompts for it. Socket Mode alone does not subscribe to events. Ensure the app-level token comes from this same app, and test in a channel joined by the authorized user in the selected workspace. apic slack.watch --debug reports event types and filter decisions without logging message contents or credentials. If no events arrive after 30 seconds, the watcher prints a subscription troubleshooting reminder. Under Basic Information > App-Level Tokens, generate an xapp- token with connections:write. This app-level token is separate from the user OAuth token; no new user scopes are required beyond the existing message-history scopes.

Supply the token locally with a hidden prompt (zsh):

read -rs 'SLACK_APP_TOKEN?Slack app token: '; echo
export SLACK_APP_TOKEN
apic slack.watch
# After Ctrl+C:
unset SLACK_APP_TOKEN

The validated token is saved in Keychain when available, or in the protected credentials file. Later, run apic slack.watch with no token environment variable. slack.logout removes the user account only; app-level tokens must be revoked separately in Slack. Do not run multiple consumers for the same app: Slack may distribute events between their connections.

WORKSPACE=T123 filters events to one workspace (defaults to the saved workspace). Without either, watch listens across the app's authorized workspaces. CHANNELS=C123,D123 narrows events to a conversation allowlist. Each output record has source: "event", workspace, channel, event_id, event_time, and the original event. Status goes to stderr. fetchApi('slack', 'watch', { signal }) also supports streaming; abort the signal or cancel the response body to stop it.

For optional recovery after a disconnect or restart, use CATCH_UP=true with a saved user login for the selected workspace. It reads retained history since the previous connection began while live events queue, and emits source: "history" records in the slack.all format. History and live events can overlap; deduplicate by workspace, channel, and message timestamp. History cannot reconstruct deleted messages or every intermediate edit. Delivery is best-effort, not exactly-once; events acknowledged before a crash can be lost. A failed history reconciliation stops the watcher so an incomplete catch-up is not silently ignored.

Watch state and locks use slack-watch-<selection hash>.json and .lock under ~/.apicat-settings/. A running watcher does not hold the ordinary Slack command lock, so exports and login can run alongside it. After a hard crash, remove its stale lock only when that watcher is no longer running. Full setup instructions are also available through apic slack.watch --help.

Discord

Send a message with the saved bot token:

apic discord.post CHANNEL=CHANNEL_ID POST="Hello from apicat"

DISCORD_CHANNEL_ID is also accepted instead of CHANNEL. The bot needs View Channel and Send Messages, or Send Messages in Threads for a thread. POST accepts 1–2000 characters; mentions do not trigger pings. The command prints the created message as JSON and does not require Message Content Intent. This entry replaces discord.channelpost.

discord.all exports messages the bot can read, including active threads, public archived threads/forum posts, and private archived threads the bot has joined (or can manage). discord.watch listens for message creation, edits, deletions, and bulk deletions through Discord's Gateway. Both support the Node fetchApi streaming interface as well as the CLI.

Create an app at Discord Developer Portal. On its Bot page, create the bot if needed, obtain its token, enable Privileged Gateway Intents → Message Content Intent, and Save Changes. Larger bots may need approval for that privileged intent. In OAuth2 → URL Generator, select the bot scope and View Channels / Read Message History permissions. Open the generated invite URL and authorize the bot in your server. Check channel permission overrides too; reading requires neither Administrator nor Send Messages. Invite the bot to private threads it should read.

Save the token locally using a hidden zsh prompt:

read -rs 'DISCORD_BOT_TOKEN?Discord bot token: '; echo
export DISCORD_BOT_TOKEN
apic discord.login
unset DISCORD_BOT_TOKEN
apic discord.all > discord.ndjson
apic discord.all GUILD=SERVER_ID SINCE=2026-10-01 > recent.ndjson
apic discord.all CHANNELS=CHANNEL_ID,THREAD_ID > selected.ndjson
apic discord.watch
apic discord.watch CATCH_UP=true
apic discord.logout

Enable User Settings → Advanced → Developer Mode in Discord to copy server, channel, and thread IDs from their context menus. GUILD accepts one numeric server ID; CHANNELS is an exact ID allowlist. For forum/media channels, omit CHANNELS to discover posts, or pass individual thread IDs. Settings may also specify guild and channels in ~/.apicat-settings/discord.json.

A bot cannot read your personal conversations with other people. DMs sent directly to the bot can be read with an explicit channel ID. Watch records discovered DM channel IDs so later unfiltered exports can include them; there is no bot endpoint to enumerate all historical DMs. A GUILD filter excludes DMs.

The token uses macOS Keychain when available; the file fallback is ~/.apicat-settings/discord-credentials.json (0600). All settings, known DM IDs, watch state, and export checkpoints stay under ~/.apicat-settings/ (0700). Bot tokens do not refresh: after resetting one in Discord, supply the replacement. Logout removes the local token only; stop running watchers separately.

Exports paginate history and respect rate-limit retry delays. RESUME=true resumes the same selection and SINCE; append to the original file. An interrupted page can repeat. Output records contain guild, channel, channel_name, and message. Attachments remain metadata/URLs. Permission/API failures stop the export rather than silently treating an inaccessible conversation as empty.

Watch requests the Gateway intents automatically (no separate event subscription screen). It sends heartbeats, reconnects, resumes live sessions, filters selected guilds/channels, and remembers recent event hashes to suppress replays. Its records contain source: "event", guild, channel, event_type, and event. Updates and deletions may be partial objects. --debug prints types/filter decisions without message bodies or credentials. Ctrl+C or cancellation of the JavaScript Response body stops the listener. Only one watcher per bot is supported; sharded bots are outside this command's scope.

CATCH_UP=true reads retained history when a fresh Gateway session is needed after a restart or failed resumption, tagging recovered records source: "history". History can overlap live events and cannot reconstruct deletions or all edits; deduplicate by channel and message ID. Delivery is best-effort. After a hard crash, remove stale Discord lock directories only after checking no matching process is running. Complete setup instructions ship in apic discord.all --help and apic discord.watch --help.