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

gate-sh

v0.1.0

Published

ntfy tells you things. gate asks you things. Human approval as a curl.

Readme

The one-liner

curl -d "Deploy v2 to prod?" "https://gate.legible.sh/ops-x7k2q?wait=300"

The curl blocks. On your phone, https://gate.legible.sh/ops-x7k2q is a page with the question and two buttons. Tap one, and the curl returns:

{"id":"aB3dE9fG","status":"decided","decision":"approve","askedAt":"2026-07-06T09:14:02.113Z","decidedAt":"2026-07-06T09:14:31.870Z"}

The hosted API at gate.legible.sh is in soft launch. To run the identical API yourself: npx gate-sh serve starts it on http://127.0.0.1:4180 in one command.

Why

Your agents are smart but infrastructurally naked. A cron agent hits a destructive migration at 3am and its "interactive ask" mode hangs forever in a headless container. A CI pipeline needs a human sign-off and grows a whole Slack app. ntfy can tell you the agent wants something — but it can't carry your answer back, and anyone holding the topic name can forge an "approval." gate is the missing verb: a blocking question with per-request decide tokens, from any language, any sandbox, anything that can curl.

The whole API

| Route | What it does | |---|---| | POST /{topic} | Ask. Body is the question — plain text, or JSON {question, options?, context?}. Blocks up to ?wait= seconds (default 300, max 3600) → 200 decided or 408 pending. ?async=1201 {id, status, poll} immediately. | | GET /{topic}/{id} | Poll one ask. ?wait= to long-poll (default 0) → 200 decided / 408 pending. Browser: a single-ask decide page. | | GET /{topic} | curl: JSON {pending, recent}. Browser: phone-friendly page with Approve/Deny buttons and a note field. Accept: text/event-stream: SSE (ask / decision events, heartbeat comments every 25s). | | POST /{topic}/{id}/decide | Decide. {decision:"approve"\|"deny", note?} + the per-ask decide token (X-Decide-Token header or ?t=). Plain-text approve/deny and HTML form posts work too. | | PUT /{topic}/config | {"notify":"https://ntfy.sh/xyz"} — every new ask POSTs the question + decide link there (fire-and-forget, 2s timeout). "" clears. Write-only. | | GET /{topic}/log | Audit trail: every ask, decision, and config change with timestamps, notes, user-agents. | | GET /README.md · /llms.txt | These docs, as text, for agents. Never token-gated; GET / with Accept: text/markdown serves the README too. |

Topics match [a-zA-Z0-9_-]{1,64} and are created by first use. Errors are JSON {"error":"...","code":"...","hint":"..."} with correct status codes — hint spells out the correct next request. All limits (16 KB bodies, 100 pending per topic, 7-day retention, …) live in src/limits.mjs.

Asking

# Plain text, blocking (the default wait is 300s):
curl -d "Deploy v2 to prod?" "http://127.0.0.1:4180/ops?wait=300"

# JSON: context renders on the approval page; options relabels the two buttons
# ([approveLabel, denyLabel] — the wire decision is still approve/deny):
curl -H "Content-Type: application/json" \
  -d '{"question":"Ship the migration?","context":"+120 -3 across 4 files","options":["Ship","Hold"]}' \
  "http://127.0.0.1:4180/ops?async=1"

# Fire-and-forget returns 201 immediately:
# {"id":"aB3dE9fG","status":"pending","poll":"/ops/aB3dE9fG"}
# ...then long-poll it:
curl "http://127.0.0.1:4180/ops/aB3dE9fG?wait=60"

Deciding

Every ask mints a random decide token — approving requires it, so seeing a question is not the same as being able to answer it. Where the token comes from depends on the mode:

Convenience mode (default). The /{topic} page embeds the tokens in its buttons, and the JSON listing carries decideToken on pending asks. Knowing the topic name is the full capability — honest capability-by-obscurity, same model as ntfy. Pick unguessable topic names.

curl "http://127.0.0.1:4180/ops"                        # find id + decideToken
curl -d approve "http://127.0.0.1:4180/ops/aB3dE9fG/decide?t=TOKEN"
# or with a note:
curl -H "X-Decide-Token: TOKEN" -H "Content-Type: application/json" \
  -d '{"decision":"deny","note":"not on a friday"}' \
  "http://127.0.0.1:4180/ops/aB3dE9fG/decide"

Bound mode. Set a notify URL and tokens are only delivered to that channel — the page and JSON stop exposing them, and the asker can no longer approve its own request:

curl -X PUT -d '{"notify":"https://ntfy.sh/my-private-alerts-k3j9"}' \
  "http://127.0.0.1:4180/ops/config"

Now every ask pings your phone via ntfy with a one-tap decide link, and /{topic} says "decide link was sent to this topic's notification channel."

Watching and auditing

curl "http://127.0.0.1:4180/ops"                                  # one-shot JSON
curl -N -H "Accept: text/event-stream" "http://127.0.0.1:4180/ops" # live SSE
curl "http://127.0.0.1:4180/ops/log"                              # audit trail

Teach your agent

Paste this into your CLAUDE.md / AGENTS.md (set the base URL and topic for your project):

## Human approvals (gate)
GATE=https://gate.legible.sh   # or your self-hosted instance
Before destructive/irreversible actions, ask a human and block:
  curl -s -d "one-line question" "$GATE/{topic}?wait=300"
  → 200 {"decision":"approve"|"deny","note"?} — obey it
  → 408 {"id","status":"pending","poll":"/{topic}/{id}"} — nobody answered; treat as NO,
    or keep waiting: curl -s "$GATE/{topic}/{id}?wait=300"
Add detail with JSON: -H "Content-Type: application/json" -d '{"question":"...","context":"diff/plan summary"}'
Fire-and-forget: append ?async=1 → 201 {"id","poll"}.
Humans answer at $GATE/{topic} in a browser. Never decide your own asks.
Shell shortcut (if the gate CLI is installed): `gate ask {topic} "question"` exits 0=approve 1=deny 2=timeout.

Self-hosting

npx gate-sh serve
# gate listening on http://127.0.0.1:4180

Or clone and run — there are zero dependencies to install:

git clone https://github.com/legible-sh/gate.git && cd gate
npm test                 # 52 tests, no network needed
node bin/gate.mjs serve --port 4180 --data-dir ./data --base-url https://gate.internal

| Flag | Effect | |---|---| | --port / --host | Listen address (default 4180 on 0.0.0.0). | | --data-dir | Persist to <dir>/events.jsonl (JSONL, replayed and compacted on boot). Without it, state is in-memory only. | | --token | Require Authorization: Bearer <token> on every route except /, /README.md, and /llms.txt. Browsers can use ?token= — pages thread it through their forms. HTML pages are gated too, because they embed decide tokens. | | --base-url | Absolute URLs in poll fields, pages, and notify links (otherwise derived from the Host header). |

CLI

The CLI is sugar over the same HTTP API — curl remains the contract. Base URL: --url flag, then GATE_URL env, then https://gate.legible.sh. Bearer auth: --auth or GATE_TOKEN.

gate ask ops "Deploy v2 to prod?" --timeout 600    # blocks; exits 0=approve 1=deny 2=timeout
gate ask ops "Deploy?" --async                     # prints the ask id
gate list ops                                      # pending + recent [--json]
gate decide ops aB3dE9fG approve --note "go"       # fetches the decide token for you
gate log ops                                       # audit trail [--json]
gate serve --port 4180 --data-dir ./data           # the server

The exit codes make bash the whole integration:

gate ask ops "Deploy v2 to prod?" && ./deploy.sh

And examples/claude-hook.sh is a Claude Code PreToolUse hook that gates dangerous shell commands behind a phone tap. More walkthroughs in examples/.

Pro

Planned for the hosted instance at gate.legible.sh — capacity and guarantees, never the core verbs. Self-hosting stays complete:

  • Reserved topic names — own ops instead of ops-x7k2q.
  • Retention — decision audit trails beyond the free 7 days.
  • Named deciders — per-person decide tokens for teams: who approved, not just that someone did.
  • Slack / email delivery — decide links in the channels your team already watches.
  • SLA on the hosted instance.

Straight talk

  • Convenience mode is capability-by-obscurity. Anyone with the topic name can see questions and answer them (the page hands out the tokens — that is the point). Unguessable names are the access model, exactly like ntfy. Want more: bound mode, --token, or reserved names on the hosted instance.
  • Bound mode's remaining gap is topic-name config. Anyone with the topic name can repoint notify. gate warns the previous channel the moment that happens and records it in the audit log, but the real fix is --token on your own instance (config then requires auth) or a reserved topic.
  • A timeout is not a deny. 408 means nobody answered. The ask stays decidable until it's pruned. Script accordingly (&& treats timeout as NO, which is the safe default).
  • Retention is 7 days, and this is not a database. State is in-memory with an optional JSONL file. Everything — pending asks, decisions, log — is pruned after 7 days. Need the audit forever? Pull /log into your own storage.
  • The data dir contains secrets. events.jsonl holds decide tokens (mode 600, but it's on you). Same for the notify channel: anyone who can read your ntfy topic can decide your asks — use a private one.
  • No WebSocket. Long-poll and SSE only — deliberate, to stay at zero dependencies. Notify delivery is fire-and-forget with a 2s timeout; a dead webhook never delays an ask.

The family

gate is one of the legible primitives — same stack, same conventions, one verb each:

| | | |---|---| | gate (4180) | ntfy tells you things. gate asks you things. | | bigred (4181) | The big red button for your agent fleet. | | trail (4182) | The flight recorder for agent runs. | | slate (4183) | The blackboard from the multi-agent papers, as a URL. | | relay (4184) | The work queue your agents can provision themselves. | | mutex (4185) | flock(1) for agents that live on different machines. | | quorum (4186) | Coordination for agents that do not share a parent process. | | meter (4187) | The kill-brake for agent spend. 429-as-a-service. | | stash (4188) | ntfy moves signals. stash moves bytes. | | tally (4189) | StatHat reborn as ntfy. Three months too late — or right on time. |

License

MIT.