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

@alook/daemon

v0.0.155

Published

Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.

Readme

@alook/daemon

A pure, host-neutral agent-runtime "driver" abstraction in TypeScript.

It documents — as compilable code — how a host adapts many different AI coding runtimes (Claude Code, Codex, Gemini, Kimi, Pi, Copilot, Cursor, OpenCode, Antigravity) behind one uniform interface, and how it delivers messages into a running agent (the "steering" mechanism).

The backend hardcodes no specific host platform. The agent always invokes a stable CLI name (alook); cliTransport.ts writes a small wrapper by that name into a per-launch state dir, prepends it to PATH, and forwards to the host's real targetCommanddecoupling the agent-facing name from the host's real binary (the host can rename/relocate its CLI without touching the agent surface). It also injects an ALOOK_* env contract. The default MOCK_CLI_CONFIG wires no targetCommand (the wrapper errors if invoked); a real deployment passes its own CliTransportConfig (CLI name, env prefix, targetCommand) and its own system-prompt communication guide. The abstraction is what's reusable — plug in whatever host you like.

Provenance. The shapes, protocols, flag strings, and control flow here were derived by studying how production agent-runtime daemons drive these CLIs, then re-expressed as a tidy, generic abstraction. It is original code, not a copy of any vendor source, and carries no platform-specific glue.


The big idea

The daemon never speaks a runtime's native protocol directly. Each runtime is wrapped by a Driver (src/types.ts) that knows how to:

  1. spawn (or createSession) — launch the runtime,
  2. encodeStdinMessage — encode an outgoing user message for the runtime's input channel,
  3. parseLine — normalize the runtime's output into a tiny shared event vocabulary (ParsedEvent: session_init, thinking, text, tool_call, tool_output, compaction_*, turn_end, error, telemetry).

A generic session host then runs the process and fans ParsedEvents out to the daemon:

  • ChildProcessRuntimeSession (src/runtime/runtimeSession.ts) for CLI runtimes.
  • SdkRuntimeSession (src/runtime/sdkRuntimeSession.ts) for in-process SDK runtimes (pi).

Because everything downstream consumes the same ParsedEvent stream and the same Driver capability flags, the rest of the daemon is transport-agnostic.


Two delivery models (the heart of it)

When a message arrives, what happens depends on the runtime's lifecycle:

Persistent runtimes (claude, codex, kimi, pi)

One long-lived process spans many turns. A new message is written onto the still-open input channel — no restart. This is "steering."

  • direct (codex, kimi, pi): write immediately; the runtime tolerates injection any time.
  • gated (claude): a raw write mid-stream could collide with an active signed thinking block, so writes are held until a safe boundary. Two state machines implement this:
    • RuntimeTurnState (src/runtime/turnState.ts) — the instantaneous gate: canSteerBusy = currentTurnId && !steeringGateActive. Tool boundary closes the gate; progress / turn-start reopens it.
    • apmStateMachine (src/runtime/apmStateMachine.ts) — the policy reducers that decide when to flush queued inbox notices and emit the concrete notify_stdin / deliver_stdin effects (clause SMR-002). Flushing is blocked while outstanding_tool_uses > 0, while compacting, while reviewing (Codex review mode), or when tool_boundary_flush_disabled. Compaction and review exit are treated as safe flush boundaries.

For Claude specifically, the input channel is stream-json: the process is launched with --input-format stream-json --output-format stream-json --include-partial-messages, and each message is one NDJSON line {"type":"user","message":{"role":"user","content":[{"type":"text","text":…}]}}.

Per-turn runtimes (gemini, copilot, cursor, opencode, antigravity)

The process handles exactly one turn and exits. supportsStdinNotification is false and encodeStdinMessage returns null. A new message means a brand-new process; the agent re-checks the inbox each wake. This lifecycle (lifecycle.kind: "per_turn") drives the ## Message Notifications section generated by buildCliSystemPrompt — no driver hand-writes this reminder. opencode is a special per-turn case: it defers spawning until a concrete message and terminates the process on turn end.


Runtime comparison

| Runtime | Lifecycle | Transport / protocol | Steering | Initial input | Output format | |---|---|---|---|---|---| | claude | persistent | child process, stream-json NDJSON | gated | {type:"user",…} line on stdin | stream-json | | codex | persistent | child process, JSON-RPC 2.0 (app-server --listen stdio://) | direct (turn/steer) | initializethread/start/resume | JSON-RPC notifications | | kimi | persistent | child process, JSON-RPC "wire" | direct (steer) | initializeprompt | JSON-RPC events | | pi | persistent | in-process SDK (@earendil-works/pi-coding-agent), multi-provider | direct | session.prompt() | SDK event callback | | gemini | per-turn | child process, stream-json | none | prompt on stdin, then close | stream-json | | copilot | per-turn | child process, JSON | none | prompt as -p arg | JSON events | | cursor | per-turn | child process, stream-json | none | prompt as trailing arg | stream-json | | opencode | per-turn (defer-spawn, terminate-on-end) | child process, JSON | none | prompt as -- <arg> | JSON events | | antigravity | per-turn | child process, plain text | none | prompt on stdin, then close | plain text lines |

(Each driver's exact launch flags live in its file under src/drivers/.)


Layout

src/
  types.ts                       # Driver interface + ParsedEvent + lifecycle/capability types
  index.ts                       # public entry point
  logger.ts                      # tiny structured logger (ALOOK_LOG_LEVEL); wired through
                                  #   wsControlChannel.ts/agentRouter.ts/managerRuntime.ts too, not just cli/daemonStart.ts
  drivers/
    index.ts                     # getDriver(runtimeId) registry  ← start here
    cliTransport.ts              # shared: state dir, token, decoupling cliName wrapper -> targetCommand, env
    systemPrompt.ts              # shared: standing-prompt assembly
    probe.ts                     # CLI/binary detection + version
    claude.ts                    # Claude Code driver
    claudeLaunch.ts              #   args / command / spawn-spec / prompt-file
    claudeProviderIsolation.ts   #   custom-provider HOME/config isolation
    claudeEventNormalizer.ts     #   stream-json → ParsedEvent
    codex.ts / codexEventNormalizer.ts / codexTelemetrySidecar.ts
    gemini.ts  copilot.ts  cursor.ts  opencode.ts  antigravity.ts
    kimi.ts                      # child-process Kimi (JSON-RPC wire)
    pi.ts                        # in-process Pi SDK (multi-provider)
  runtime/
    runtimeSession.ts            # ChildProcessRuntimeSession + descriptor
    sdkRuntimeSession.ts         # SdkRuntimeSession (in-process)
    turnState.ts                 # instantaneous steering gate
    apmStateMachine.ts           # gated-delivery policy reducers (SMR-002)
    progressState.ts             # liveness / stall detection
    notificationState.ts         # inbox-notice de-dup + batching
    errorDiagnostics.ts          # runtime-error classification + scrubbing
  inbox/
    projection.ts                # bucket pending messages by target → notice snapshots
    stateMachine.ts              # pre-action freshness guard (forward / hold / bypass)
  manager/
    managerPolicy.ts             # pure reducer: single-flight, wake/sleep, queue+coalesce, stalled
    managerRuntime.ts            # AgentProcessManager: applies effects to real sessions
  server/
    contract.ts                  # ServerApi (data) + EnrollmentApi (machine→runner key) + HostControlChannel (control) + AdminApi
    wsControlServer.ts           # server end of the control plane over ws (machine-key authed)
    wsControlChannel.ts          # host end: WebSocket HostControlChannel (injectable socket, reconnect + heartbeat + resync)
  daemon/
    createDaemon.ts              # runtime-agnostic daemon factory (driver/sessionFactory/runtimes INJECTED; no test code)
  cli/
    index.ts                     # the `alook` agent CLI skeleton (message send / inbox pull)
    proxyServerApi.ts            # agent data-plane client: voucher → proxy → server (identity from the voucher)
  credentials/
    credentialProxy.ts           # CredentialBroker (mint/revoke/check per-voucher vouchers) + local key-swapping proxy
scripts/
  daemon.ts                      # DAEMON process: thin launcher — wraps `alook daemon start` for local dev

Host orchestration (manager + server)

Beyond driving a single runtime, the backend includes the host-side orchestration so it can run agents end-to-end:

  • manager/AgentProcessManager schedules processes. Decisions live in a pure reducer (managerPolicy.ts: single-flight one-process-per-agent, wake/sleep, message queue + coalesce, stalled detection → spawn/send/stop/terminate_stalled effects); a thin executor (managerRuntime.ts) applies them to real sessions and feeds runtime events back in. AgentRouter consumes the control plane (below).
  • server/ — the agent ⇄ server boundary, split into two planes that share one contract (contract.ts):
    • data plane (ServerApi): what the alook CLI calls — send / inboxPull / ack / read / listChannels, addressed by path-style ChannelRefs.
    • control plane (HostCommand + HostControlChannel): server → host commands (agent:wake / agent:stop; plus bot:* lifecycle frames). agent:wake carries a bodiless UnreadNotice (channel + latest seq, no message content) — the DAEMON (AgentRouter/AgentProcessManager), not the server, decides whether to spawn a process, notify an already-running one, or coalesce the notice for the next turn. There is no in-process fixture standing in for the server: wsControlServer.ts (server end) and wsControlChannel.ts (host end, injectable socket + exponential-backoff reconnect + heartbeat) carry the HostCommand frames over a real WebSocket (ws://127.0.0.1 in local dev, a real ws-do URL in production). A real server connection is the same WsControlChannel pointed at a real URL — nothing upstream changes.

tests/integration/daemon/control-plane.test.ts wires a real WsControlChannel against a running ws-do dev server into a complete loop: seed a server/bot/ machine, post a message as a human owner over real HTTP, and assert the daemon's control channel receives the resulting agent:wake HostCommand — then, acting as the agent (no CLI spawned), replies over the real credential chain (enroll → voucher → proxy → X-Agent-Id) and asserts the reply is readable back over real HTTP. See "Point a daemon at real infra locally" below for how to run the pieces this test drives by hand.

Credentials (zero-trust isolation)

A spawned runtime needs some credential to call back to the server, but handing it the real API key means the agent process (and any tool it runs) can read it, can't be scoped, and a leak forces a rotation. credentials/credentialProxy.ts implements the voucher pattern instead — the same shape a production daemon uses:

  1. The host starts one local key-swapping proxy on 127.0.0.1 (loopback only).
  2. For each launch, a CredentialBroker mints a per-launch voucher (vch_…) into a 0600 file. The child is given <PREFIX>_PROXY_URL + <PREFIX>_PROXY_TOKEN_FILE + its capability set — never the real key.
  3. The child's CLI calls the proxy with Authorization: Bearer vch_….
  4. The proxy validates the voucher, swaps in the real Bearer <hostApiKey>, stamps X-Agent-Id / X-Client / X-Agent-Active-Capabilities, and forwards to the host-supplied upstream.

This buys credential isolation (the agent only ever holds a voucher), capability scoping (the proxy rejects endpoints outside the voucher's caps), and revocability (vouchers are per-launch and individually revocable; rotating a leaked voucher never touches the real key). This is the only credential path — cliTransport requires ctx.credentialProxy (a running broker + proxy URL) and throws without it; there is no plaintext fallback that would hand the agent a real key. Everything is host-neutral: the real key, upstream URL, voucher prefix, and header names all come from the host via CredentialBrokerConfig.

src/credentials/credentialProxy.test.ts covers the swap, voucher rejection (invalid local agent proxy token), capability scoping, and revocation against a throwaway upstream.

Three key tiers (and where each lives)

A production daemon keeps three credential tiers, not two — important for understanding what hostApiKey should be:

  1. Machine master key — authenticates the daemon to the server and is used to mint per-agent credentials. Held in daemon memory only (not persisted to disk or keychain). ⚠️ In the current daemon it is passed as a process argv (--api-key …), so any local process can read its full value via ps aux. Mitigation: source it from an env var, a 0600 file, or the OS keychain instead of argv. This proxy never forwards the master key.
  2. Per-agent runner credential — the server mints one per agent (scoped to that agent, individually revocable) when the daemon asks with the master key. This is what the proxy actually swaps in and forwards upstream, and what belongs in CredentialBrokerConfig.hostApiKey. Minting it from the master key is host-side orchestration and is intentionally out of scope for this backend.
  3. vch_ voucher — minted by CredentialBroker per launch, written to a 0600 file, and the only credential the agent process ever sees.

Inbox layer (freshness)

Two pieces govern how messages reach the agent and how outgoing actions are gated:

  • inbox/projection.tsprojectAgentInboxSnapshot buckets pending messages by target (channel / DM / thread) and projects each into the metadata-only summary surfaced as an [inbox notice: …] (count, first, latest, sender, flags). No bodies — the agent pulls those with alook inbox pull.
  • inbox/stateMachine.tsplanAgentInboxSideEffect is the "don't reply on stale context" guard. Before an outward action (send / task_claim / task_update) it compares the model's seen seq boundary against pending messages and returns forward (let it through), held (hold the action, surface the latest few unseen messages as held context to reconcile first — this is the "Freshness hold → saved as a draft" you can hit when messaging a busy agent), or bypass (explicit continueAnyway). It is a pure function producing a stable producerFactId; clause ids SMR-002 (consume) and SMR-006 (held envelope) are preserved.

Build & run

This project uses pnpm (see the packageManager field).

pnpm install
pnpm run typecheck           # tsc --noEmit (passes clean)
pnpm test                    # vitest — unit tests only (drivers/manager/inbox/credentials/server)
pnpm run test:integration    # real infra: requires wrangler dev + ws-do dev + wake-worker dev (see below)

Unit behavior is covered by src/**/*.test.ts. Full control-plane and credential-chain behavior against real infra is covered by tests/integration/daemon/ — see that directory for the executable reference of how the pieces fit together over the network (real WsControlChannel over a real WebSocket, real enroll-agent/credential-proxy HTTP calls, no in-process shortcuts).

Point a daemon at real infra locally

There's no mock server to stand in for src/web/src/ws-do — a daemon always talks to the real thing. To drive one by hand in local dev, run the real servers and walk the real credential chain (cmt_ pairing token → cmk_ daemon credential → crk_ per-agent runner key):

Terminal 1 — the web app + control-plane DO:

pnpm --filter @alook/web dev      # http://localhost:3000
pnpm --filter @alook/ws-do dev    # ws://localhost:8789

Terminal 2 — pair + activate a machine, then start a daemon with the resulting cmk_... credential:

# 1. As a signed-in user, create a pairing token:
curl -s -X POST http://localhost:3000/api/community/machines/pair \
  -H "Cookie: <session-cookie>" | tee /tmp/pair.json
# → { "tokenId": "cmt_…", "expiresAt": "…" }

# 2. Exchange it for a daemon credential:
curl -s -X POST http://localhost:3000/api/community/daemon/activate \
  -H "Authorization: Bearer $(jq -r .tokenId /tmp/pair.json)" \
  -H "content-type: application/json" \
  -d '{"hostname":"my-laptop","platform":"darwin","arch":"arm64"}'
# → { "credential": "cmk_…", "machineId": "cm_…", "expiresAt": null }

# 3. Start the daemon with that credential:
ALOOK_MACHINE_KEY=cmk_… \
ALOOK_SERVER_URL=http://localhost:3000 \
ALOOK_SERVER_WS_URL=ws://localhost:8789 \
pnpm run daemon

A bot bound to this machine (via the community UI/API) can now be woken by posting a message in a channel it's a member of — the real wake-producer path (src/websrc/wake-workersrc/ws-do) delivers agent:wake over the daemon's real WsControlChannel.

Run the source, not dist/ (yet). pnpm run build emits JS, but the current Bundler-resolution emit produces extensionless relative imports that Node's native ESM rejects (Cannot find module './types'). Until that's switched to NodeNext or bundled, consume the library through a TS toolchain (tsx / ts-node / vite / esbuild / webpack).

Usage sketch

import { getDriver, createChildProcessRuntimeSession } from "@alook/daemon";

const driver = getDriver("claude");
const session = createChildProcessRuntimeSession(driver, ctx);
session.on("runtime_event", (e) => handleParsedEvent(e));
await session.start({ text: initialPrompt });

// Later, a message arrives while the agent is working:
session.send({ text: "new message from #general", mode: "busy" }); // steer