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

@synadia-ai/nats-pi-headless

v0.5.5

Published

Headless NATS agent host that spawns, prompts, and stops PI coding-agent sessions as first-class Synadia Agent Protocol for NATS v0.3 instances. Each session registers as agents.prompt.pi-headless.<owner>.<session_id>; a small controller service adds verb

Readme

pi-headless

A headless NATS agent host for the PI coding agent, built on @synadia-ai/agents (caller-side primitives) and @synadia-ai/agent-service (host-side ReferenceAgent) and conforming to the Synadia Agent Protocol for NATS v0.3 (verb-first subjects + status endpoint).

Each spawned PI session registers as its own NATS agent instance under agents.prompt.pi-headless.<owner>.<session_id> - discoverable via $SRV.INFO.agents and promptable with any protocol-compliant client, including the @synadia-ai/agents SDK. A small controller service at agents.prompt.pi-headless.<owner>.<name> (default name = "control") adds request/reply endpoints for session lifecycle - spawn, stop, list - alongside the protocol-required prompt endpoint (which returns help text) and a status endpoint that replies with the same payload as a heartbeat.

In short: one process, many PI sessions, all first-class NATS agents.

Paired with examples/agent-web-ui/ you also get a browser-based PI Exec workspace that picks up spawned sessions automatically, surfaces lifetime/queue metadata, and includes a fan-out composer for running one prompt across many working directories in parallel.

Quickstart (run from npm)

The package ships a nats-pi-headless CLI binary, so the simplest way to try it is via npx — no clone, no build:

# Pick a NATS target via context or URL; both are picked up via env or flag.
NATS_CONTEXT=localhost npx @synadia-ai/nats-pi-headless
# or:
NATS_URL=nats://127.0.0.1:4222 npx @synadia-ai/nats-pi-headless
# or:
npx @synadia-ai/nats-pi-headless --context localhost

npx resolves the package, runs its bundled entry point under Node ≥ 20, and prints:

pi-headless: controller listening on agents.prompt.pi-headless.<you>.control
pi-headless: control endpoints — agents.spawn.pi-headless.<you>.control  …  agents.stop.…  agents.list.…

For a permanent install:

npm install -g @synadia-ai/nats-pi-headless
nats-pi-headless --context localhost

PI auth / model registry comes from ~/.pi/agent/auth.json (the same location pi uses) — independent of how you launched the host.

Quickstart (run from a local clone)

When you're working on the SDK or this example itself:

# 1. Build both SDKs (workspace siblings, referenced via file:). The
#    extra `bun install` in agent-sdk re-copies the freshly-built
#    caller dist into agent-sdk/node_modules/@synadia-ai/agents/, which
#    is the path the host SDK's compiled output resolves at runtime.
(cd ../../client-sdk/typescript && bun install && bun run build)
(cd ../../agent-sdk/typescript  && bun install && bun run build)

# 2. Run pi-headless against the local SDK source via bun.
cd ../../examples/pi-headless
bun install
bun run start                # connects via $NATS_CONTEXT or NATS_URL

# 3. Spawn a session + prompt + stop, from another shell.
bun run scripts/spawn.ts --cwd /tmp/pi-sandbox --prompt "list the files here" --stop-after

See README-DEV.md at the repo root for a fuller walk-through of the build / install dance, including how to pick up SDK edits without rebooting everything.

Configuration

Either a NATS context or an explicit URL:

NATS_CONTEXT=localhost bun run start
# or
NATS_URL=nats://127.0.0.1:4222 bun run start
# or
bun run start --context localhost

Optional defaults live in ~/.pi-headless/config.json:

{
  "context": "localhost",
  "name": "control",
  "defaultModel": "anthropic/claude-sonnet-4-5",
  "defaultThinkingLevel": "off",
  "defaultMaxLifetimeS": 1800
}

Env overrides:

| Variable | Overrides | Default | | --- | --- | --- | | PI_HEADLESS_OWNER | Owner subject token (3rd segment) | $USER | | PI_HEADLESS_NAME | Controller instance name (4th token) | control | | PI_HEADLESS_DEFAULT_MODEL | Default model spec for spawns | (none — caller must set, or PI picks) | | PI_HEADLESS_DEFAULT_THINKING_LEVEL | Default thinking level for spawns | (none) | | PI_HEADLESS_DEFAULT_MAX_LIFETIME | Default session lifetime, in seconds | 1800 |

Precedence (high → low): CLI flags → env vars → ~/.pi-headless/config.json → built-in defaults.

PI auth / model registry comes from ~/.pi/agent/auth.json (the same location pi uses).

Subject layout

Verb-first throughout — protocol verbs and pi-headless extension verbs share the same agents.<verb>.pi-headless.<owner>.<token> shape, so a tracer or audit layer can subscribe to agents.<verb>.> and parse identity positionally.

agents.prompt.pi-headless.<owner>.<name>      ← controller prompt endpoint (help text)
agents.status.pi-headless.<owner>.<name>      ← controller status (replies with heartbeat-shaped payload)
agents.hb.pi-headless.<owner>.<name>          ← controller heartbeat (30 s)
agents.spawn.pi-headless.<owner>.<name>       ← POST JSON → session descriptor
agents.stop.pi-headless.<owner>.<name>        ← POST { session_id } → { ok: true }
agents.list.pi-headless.<owner>.<name>        ← (empty) → { sessions: [...] }

agents.prompt.pi-headless.<owner>.<session_id>  ← spawned session prompt
agents.status.pi-headless.<owner>.<session_id>  ← spawned session status
agents.hb.pi-headless.<owner>.<session_id>      ← spawned session heartbeat (30 s)

Wire examples

Spawn

nats req agents.spawn.pi-headless.$USER.control \
  '{"cwd":"/tmp/pi-sandbox","model":"anthropic/claude-sonnet-4-5","max_lifetime_s":900}' \
  --timeout=10s
# → { "session_id":"sess-a1b2c3d4", "subject":"agents.prompt.pi-headless.$USER.sess-a1b2c3d4", "status_subject":"agents.status.pi-headless.$USER.sess-a1b2c3d4", ... }

Prompt (protocol-standard - no custom format)

nats req agents.prompt.pi-headless.$USER.sess-a1b2c3d4 \
  'summarise the files in this directory' \
  --replies=0 --reply-timeout=30s --timeout=60s
# → {"type":"status","data":"ack"}
# → {"type":"response","data":"There are three files: …"}
# → (empty terminator)

--reply-timeout=30s is important: the default 300 ms is shorter than the gap between the immediate ack chunk and the LLM's first response, so nats req exits after the ack alone. SDK callers (requestMany with strategy:"sentinel") wait the full maxWait regardless of inter-arrival gaps and don't need this flag.

Programmatically with the SDK:

import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia-ai/agents";

const nc = await connect({ servers: "nats://localhost:4222" });
const agents = new Agents({ nc });

const all = await agents.discover();
const session = all.find(a => a.name === "sess-a1b2c3d4")!;
for await (const ev of await session.prompt("summarise the files in this directory")) {
  if (ev.type === "response") process.stdout.write(ev.text);
}

await agents.close();
await nc.close();

Stop

nats req agents.stop.pi-headless.$USER.control '{"session_id":"sess-a1b2c3d4"}'
# → { "ok": true, "session_id":"sess-a1b2c3d4" }

List

nats req agents.list.pi-headless.$USER.control ''
# → { "sessions": [ { "session_id":"sess-a1b2c3d4", "cwd":"/tmp/pi-sandbox", "remaining_lifetime_s": 867, ... } ] }

Errors

Custom endpoints respond with NATS micro-service error headers (Nats-Service-Error-Code / Nats-Service-Error):

| Code | When | |------|----------------------------------------------------------------------| | 400 | Bad JSON, missing cwd, unknown model, invalid thinking level, bad base64 | | 404 | stop for an unknown session | | 500 | PI SDK threw during prompt execution |

Session prompt endpoints follow protocol §9.

CLI helpers

  • bun run scripts/spawn.ts --cwd /path [--prompt …] [--stop-after] - end-to-end smoke test.
  • bun run scripts/list.ts - print active sessions from every reachable controller.
  • bun run scripts/stop.ts SESSION_ID - dispose a session.

Notes

  • Session identity. The 4th subject token is the session id; metadata.session echoes it. Controllers use name = "control" by default and sessions carry metadata.role = "session".
  • Metadata marker. The controller carries metadata.role = "controller" so clients can tell it apart from sessions. The shared agent: "pi-headless" token already disambiguates this from the regular agent: "pi" runtime.
  • Multiple controllers per host. On startup the controller probes $SRV.INFO.agents and, if its target prompt subject is already claimed, picks the next free <name>-2, <name>-3, … suffix automatically. So booting a second pi-headless with default settings leaves the first as control and the second as control-2 without explicit --name flags. (For deterministic naming or two stable controllers side-by-side, still pass --name explicitly.)
  • Serial drain. Per session, prompts are queued and processed one at a time.
  • Lifetime & pruning. max_lifetime_s bounds a session's wall-clock life; pending requests older than 30 min are evicted (active requests are never evicted).
  • Attachments. Base64 attachments are decoded to ~/.pi-headless/attachments/<session_id>/<uuid>/ and their absolute paths are prepended to the prompt text, matching the agents/pi/ staging pattern.