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

@gnldev/server

v0.5.0

Published

Auto-REST + OpenAPI for @gnldev/durable agents: createGnl registry → HTTP router (run/resume/runs + openapi.json).

Readme

@gnldev/server

Exposes the createGnl registry as auto-REST + OpenAPI + SSE. Every endpoint bottoms out in runDurable → the journal, replay and the at-most-once side-effect guarantee are inherited for free. (The durable counterpart to the common auto-REST pattern.)

Install: pnpm add @gnldev/server — or use it from a repo clone: pnpm install && pnpm -r build.

npm i @gnldev/server   # peer: @gnldev/durable  ·  dep: hono
import { createRestApi } from '@gnldev/server';
import { serve } from '@hono/node-server';
import { SqliteStorage } from '@gnldev/durable/sqlite';

const api = createRestApi({
  journal: new SqliteStorage('runs.db').runs,
  agents: { support: { model: 'anthropic/claude-opus-4-8', tools, guard, maxSteps: 8 } },
});
serve({ fetch: api.fetch, port: 3000 });        // on its own port
// app.mount('/api', api)                      // inside a Hono app

Binding it to a Node server

createRestApi returns a fetch handler — callable, and carrying .fetch. On Node, bind it with the bridge on this package's own subpath:

import { createServer } from 'node:http';
import { toNodeHandler } from '@gnldev/server/node';

express().use('/api', toNodeHandler(api));                            // Express
await fastify.register(middie); fastify.use('/api', toNodeHandler(api));   // Fastify + @fastify/middie
koa.use(c2k((req, res, _next) => toNodeHandler(api)(req, res)));      // Koa + koa-connect
createServer(toNodeHandler(api)).listen(3000);                        // node:http
nestApp.use('/api', toNodeHandler(api));                              // Nest (Express or Fastify)

Two rules, both measured on live servers rather than reasoned about:

Bind at the middleware layer, not as a route. On Fastify the difference is the whole story: middleware runs before body parsing, a route runs after it. fastify.all('/api/*', …) looks correct, passes every GET, and answers 400 to a perfectly good POST — because Fastify's built-in JSON parser has already drained the stream.

On Koa, declare the third parameter. koa-connect switches on fn.length: a middleware with fewer than three parameters is assumed not to terminate the response, so it calls next() straight after and Koa writes its own 404 over what was already sent — measured, ERR_HTTP_HEADERS_SENT and 404 on every route. Naming next without calling it selects the branch that waits, which is what a handler that owns the response needs.

Mount before whatever parses request bodies. A parser that runs first reads the stream to the end and hands the result to the framework, not to us. Get it wrong and the bridge says so (body_consumed_upstream) instead of blaming your request. Your own routes keep the parser: it still runs for everything mounted after.

Endpoints (per agent)

| Method | Path | Description | |---|---|---| | GET | /agents | List of registered agent metadata (client/playground selector) | | POST | /agents/:name/run | Durable run {runId, prompt\|messages, threadId?, approvals?}{ok, runId, text, interrupts} | | POST | /agents/:name/stream | SSE streaming run (schema below) | | POST | /agents/:name/resume | {runId, approvals?} — the input is read from the journal | | GET | /runs · /runs/:id | Run summaries / journal timeline | | GET | /openapi.json | Generated schema | | GET | /health | Liveness — process is alive. No storage access. Unauthenticated | | GET | /ready | Readiness — can serve traffic; 503 if the journal is unreachable. Unauthenticated |

Health checks

/health and /ready answer two different questions, and pointing the wrong probe at the wrong one causes the wrong action:

  • /health — is the process alive? It touches nothing. A failing dependency must not make an orchestrator kill and restart a healthy process: restarting it does not reconnect your database, it only discards whatever the process still had in flight. Use this for liveness probes.
  • /ready — can it serve? It reads from the journal, with a 2s budget because an unreachable database usually hangs rather than refusing. On failure the instance leaves the load balancer while staying alive to recover. Use this for readiness/traffic probes.

Both are deliberately unauthenticated — probes run before any credential exists, and usually never get one. For the same reason they report reachability and nothing else: no agent names, no counts, no configuration, and never the underlying error, which for a database failure routinely carries the host, database and user. That detail goes to your logs.

# Kubernetes
livenessProbe:  { httpGet: { path: /health, port: 3000 } }
readinessProbe: { httpGet: { path: /ready,  port: 3000 } }

SSE schema (/stream)

event + JSON data: text-delta {text} · tool-call {toolCallId,toolName,input} · tool-result {...} · error {error} · interrupt {interrupts[]} (when the stream ends) · done {runId,finishReason,usage}. The same schema is also used in the @gnldev/studio playground → @gnldev/client connects to both ends.

Reconnect recovery (resumable SSE, opt-in)

Every SSE event carries a deterministic, monotonically increasing id: field starting at 0. Calling the stream again with the same runId is deterministic journal replay (the model/tools do NOT actually run again) → fullStream produces the same chunk sequence, so the same event sequence comes out with the SAME ids.

The browser's EventSource automatically sends the last id: it saw via the Last-Event-ID header when reconnecting after a dropped connection — the server reads this (or lastEventId in the body) and for events with id <= lastEventId it produces them but does not write them to the client (production is cheap: replay does not call the model/tools). Only events with id > lastEventId are sent. If lastEventId is not given, behavior is identical to before (aside from the added id).

Frontend

For type-safe calls use @gnldev/client (core) + @gnldev/client/react (hooks). For REST + Studio Playground in one command use @gnldev/cli gnl dev.

How it works

Every call to /run and /stream names one of two things, and exactly one:

  • workKey — your name for a unit of work, not for a conversation. The engine derives the run's id from it and returns that id in the X-Gnl-Run-Id header. Calling again with the same workKey is routed to the same run: durable replay, side effects once. A workKey is a business name (the invoice being issued, tonight's reconciliation batch) — keep sensitive data out of it, because it is reflected in error details and shown on Studio screens. Coming from thread_id? There the same key means continue this conversation; here it means this is the same job. A conversation is threadId, a separate field you can send at the same time.
  • runId — a raw id you already hold (a resume, a fork, an id you stored).

Sending both is a 400: two identities for one call is a question with no honest answer. Sending neither is also a 400. Idempotency-Key is accepted as a workKey alias — it names the work, which is what that header has always meant — and it is read only when the body named nothing itself.

A workKey in the default resource scope needs a subject to be unique within: name the end user (resourceId, or an authenticated principal), or the call is refused rather than run for nobody.

How long a workKey stays unique: as long as the run record lives — not a minute longer. Your retention window should not be shorter than the longest retry your clients can produce.

Was this call new work, or an answer you already had? Every response to /run and /stream carries X-Gnl-Idempotency-Status: new when this call started the run, replay when the run had already been driven and the answer came from the journal. Nothing re-ran in the replay case — no model call, no tool, no charge. It is the header to log if you want to know how much of your traffic is retries, and the one to assert on if you are verifying that your client's retry logic is actually deduplicating.

run_busy (409) is a narrower condition than it looks. It does not mean "this id has been used"; it means the run is executing right now, in this process or another one. A per-run lock is taken before execution and renewed by a heartbeat while it lasts, and a concurrent second call — a double-click, two tabs, a retry racing its original — is declined instead of executing a second time. Three consequences:

  • The response carries Retry-After. The same request is correct; it is only early. Honour the delay and send it again — it will replay.
  • A process that dies without releasing holds the run only until the lock's TTL expires (default 300 000 ms), after which a retry is admitted and replays from the journal.
  • A long run does not time itself out of its own lock: the heartbeat keeps it alive, so the TTL bounds crash recovery, not run length.

A run that has already finished does not produce this at all — it replays (see the header above).

Auth (opt-in, but no silent openness in production): createRestApi(config, { auth }) accepts an AuthProvider (the free @gnldev/auth roleAuth, or the paid @gnldev/auth-ee) or a backward-compatible {read, write} pair. If auth is not given, endpoints are open; but under NODE_ENV=production this is only possible DELIBERATELY, via allowOpenAccess: true — without the flag, setup throws a clear error ("auth required in production"). Outside production, a setup without auth works, with a one-time console.warn on the first request, so a silent fail-open cannot survive unnoticed.

This server is not for browsers. It is a backend service your own backend calls; your users reach your application, and your application reaches this. Nothing here authenticates an end user, and no credential below is safe to ship to a client device.

Which credential your application carries: client, not admin — see @gnldev/auth for the four classes. admin cancels runs, reads the whole organization's history and reads /usage; an application that runs agents needs none of that. A client credential is a whitelist (agents:run, workflow:run, run:cancel, plus reads), so routes added in later versions are refused rather than silently granted.

Every client request names the end user it acts for. One application credential serves many users, so resourceId in the body (or ?resourceId= on a read) says which one:

POST /agents/:name/run   { runId, prompt, threadId, resourceId }   → 400 without resourceId
GET  /runs?resourceId=u-ayse                                       → only that user's runs
GET  /threads?resourceId=u-ayse                                    → only that user's conversations
GET  /runs/:id?resourceId=u-mehmet                                 → 403 if the run is someone else's

That id is what keeps two of your users' conversations, memories and runs apart — the memory layer scopes on it. Operator credentials may omit it; they work across the organization by design.

License

Apache-2.0 — see LICENSE.