@neolifehealth/cli
v0.0.2
Published
neolife developer CLI — the human-in-terminal front door to the fulfillment rail (login, keys, orders, catalog, signed webhook testing).
Downloads
337
Maintainers
Readme
@neolifehealth/cli
The neolife CLI — the human-in-terminal front door to the neolife fulfillment rail. A clinic's
developer uses it to pair a key, mint machine keys, list orders, browse the catalog, and (the headline
features) stream live events to a local receiver and fire a correctly signed test event at one —
no relay service, public URL, or tunnel needed.
It is the terminal complement to the agent MCP surface (@neolifehealth/mcp) and the SDKs
(@neolifehealth/sdk, Python). All three talk to the same HTTP API and inherit the same non-negotiable
compliance invariants:
- Default mode is SANDBOX. Sandbox keys (
nk_sandbox_…/rk_sandbox_…) resolve only to synthetic data. The CLI never operates in live by accident and prints a persistentSANDBOX/LIVEbanner on every command. - There is no
--submit. Ever.orders createproduces a draft only. An unapproved clinical order can only reach a pharmacy after a licensed provider approves it, and that gate is enforced server-side. A terminal is never a compliance backdoor. - No PHI touches the CLI. Webhook payloads are PHI-free by contract (ids, status, shipping metadata, reason codes — never a name, address, DOB, drug, or intake answer).
Contents
- Install
- Quickstart
- How it fits together
- Commands
trigger— signed local webhook testinglisten— stream live events to your receiver- Config & profiles
- Errors & exit codes
- Relationship to the MCP and SDKs
Install
# From the monorepo (recommended while unpublished):
pnpm --filter @neolifehealth/cli build
node packages/cli/dist/index.js --help
# Or, once published:
npm i -g @neolifehealth/cli
neolife --helpThe package declares a neolife bin (dist/index.js), so a global install puts neolife on your
PATH. All examples below use neolife; substitute node packages/cli/dist/index.js when running
from a local build. Requires Node 18+ (native fetch and async-iterable response bodies).
Quickstart
# 1. Log in with a sandbox key (mint one at POST /developer/keys or `neolife keys create`).
# Verifies the key against GET /developer/keys and stores it in ~/.neolife/config.json.
neolife login --key nk_sandbox_xxxxxxxxxxxxx
# …or pipe it: echo nk_sandbox_xxx | neolife login
# 2. See where you're pointed (masked key, mode, base URL, webhook secret).
neolife config
# 3. Mint another key.
neolife keys create --name "shopify-integration" --mode sandbox --type secret
neolife keys list
# 4. Look at orders and the catalog.
neolife orders list
neolife orders get ord_123 --json
neolife catalog list
# 5. Draft (never submit) an order.
neolife orders create --items '[{"productId":"prod_x","quantity":1}]'
# 6a. Stream this tenant's live events to a local receiver — like `stripe listen`.
neolife listen --forward-to http://localhost:3000/webhooks
# 6b. Or fire ONE correctly-signed test event at your receiver — no relay needed.
neolife trigger order.shipped \
--forward-to http://localhost:3000/webhooks \
--secret whsec_your_signing_secretHow it fits together
flowchart LR
subgraph terminal["your terminal"]
CLI["neolife CLI"]
end
subgraph api["neolife API (default https://api.neolife.health)"]
KEYS["/developer/keys"]
ORD["/orders"]
PROD["/products"]
SSE["/developer/events/stream (SSE)"]
end
RECV["your local webhook\nreceiver"]
CLI -- "Bearer nk_… key" --> KEYS
CLI -- "Bearer nk_… key" --> ORD
CLI -- "Bearer nk_… key" --> PROD
CLI -- "listen: GET SSE" --> SSE
SSE -- "PHI-free event frames" --> CLI
CLI -- "trigger / listen:\nStandard-Webhooks-signed POST" --> RECVlogin, keys, orders, and catalog are ordinary authenticated REST calls (Authorization:
Bearer <key>). listen opens the server's SSE stream and re-signs each event before forwarding it;
trigger skips the server entirely and signs a synthetic event locally. Both signing paths produce
byte-for-byte identical output to the server signer, so an event tested locally verifies against the
same receiver code a production event would.
Commands
| Command | What it does |
|---|---|
| neolife login [--key <k>] | Store an API key (flag, stdin, or global --key); verify it against GET /developer/keys; warn loudly on a LIVE key. --profile <name> picks the profile to store under (default default). |
| neolife config | Show the current profile — masked key, mode, base URL, webhook secret, config path, and the list of known profiles. |
| neolife keys list | List the tenant's machine keys over /developer/keys. --json. |
| neolife keys create | Mint a key. --name (required), --mode sandbox\|live (default sandbox), --type secret\|restricted (default secret), --scopes <a,b> (restricted only). Plaintext key is shown once. |
| neolife keys revoke <id> | Revoke a key by id. |
| neolife orders list | List orders. --status, cursor pagination --limit / --starting-after, --json. |
| neolife orders get <id> | Fetch one order. --json for the full object. |
| neolife orders create | Draft only — no --submit, by design. --items <json> or --file <path>. |
| neolife catalog list | List products over GET /products. --json. |
| neolife trigger <event> --forward-to <url> | Fire one signed test event from the PHI-free catalog at a local receiver. --secret whsec_…, --data <json> to override the sample payload, --json. Rejects unknown events. |
| neolife listen --forward-to <url> | Stream this tenant's live events over SSE and forward each, Standard-Webhooks-signed, to a receiver (stripe listen-style). --secret whsec_… (else generated), --events <comma,list>, --json. |
| neolife open [orders\|docs] | Open the ops console (/orders) or the docs site in your browser. |
| neolife --version / --help | Version and help. |
Global flags
-p, --profile <name>— config profile to use (default: current profile).--base-url <url>— override the API base URL for one call (defaulthttps://api.neolife.health; usehttp://localhost:4000for a local stack).--key <key>— override the API key for one call (nk_sandbox_…/nk_live_…/rk_…).
Key format
Keys are Stripe-style and self-describing — the CLI reads the mode straight from the prefix, so the
SANDBOX / LIVE banner never depends on a network round-trip:
| Prefix | Type | Mode |
|---|---|---|
| nk_sandbox_… | secret | sandbox (synthetic data only) |
| nk_live_… | secret | live (real, PHI-scoped tenant) |
| rk_sandbox_… | restricted | sandbox |
| rk_live_… | restricted | live |
trigger — signed local webhook testing
neolife trigger builds the exact event envelope the server emits — { id, type, createdAt, data } —
and signs it per Standard Webhooks:
signature = "v1," + base64( HMAC-SHA256( whsecBytes, `${id}.${timestamp}.${payload}` ) )
webhook-id: msg_… (stable across retries — receiver dedupe key)
webhook-timestamp: <unix seconds> (receivers reject outside ±5 min → replay protection)
webhook-signature: v1,<sig> (space-separated list allows key rotation)The signing secret is whsec_<base64>; the base64 part is the raw HMAC key. This byte-for-byte
matches the server signer at apps/api/src/modules/dev-webhooks/webhook-signer.ts, so an event fired
locally verifies against the same receiver code a production event would. Only events in the PHI-free
catalog are accepted:
intake.submission.created intake.certificate.issued intake.review.approved
intake.review.rejected order.routed order.submitted order.accepted
order.shipped order.delivered order.rejected refill.followup_requiredEach event ships with a representative PHI-free sample payload (ids, status, shipping metadata,
reason codes only). Override it with --data '<json>' — but keep it PHI-free. The signing secret comes
from --secret whsec_… or the profile's webhookSecret in ~/.neolife/config.json.
listen — stream live events to your receiver
neolife listen is the stripe listen of the fulfillment rail. Where trigger fires one
synthetic event, listen opens the server's live event stream and forwards every real event your
tenant produces to a local receiver — so you can exercise a real webhook handler end-to-end without a
public URL or a tunnel.
neolife listen \
--forward-to http://localhost:3000/webhooks \
--secret whsec_your_signing_secret \
--events order.shipped,order.delivered # optional filter; default: all eventsHow it works:
- Stream. It opens
GET {baseUrl}/developer/events/stream(Server-Sent Events) with your stored key asAuthorization: Bearer <key>. Eachdata:frame is one PHI-free event{ id, type, createdAt, data }. A tiny built-in SSE parser handles multi-linedata:frames and ignores:keepalive comments; the connection stays open. - Filter. If
--eventsis given, events whosetypeisn't in the list are skipped (each value is validated against the PHI-free catalog, so a typo fails loudly before the stream opens). - Sign. Each forwarded event is re-signed with the same Standard-Webhooks signer as
trigger(webhook-id/webhook-timestamp/webhook-signature: v1,<base64 HMAC>), so it verifies against the identical receiver code a production event would. - Forward. It POSTs the signed envelope to
--forward-to(defaulthttp://localhost:3000/webhooks) and prints one line per event: timestamp, event type, → the forward URL, and the receiver's HTTP status. A receiver connection error is reported without crashing the stream.--jsonprints one JSON object per event instead.
No --secret? Like stripe listen, it generates a whsec_… secret and prints it up front —
configure your receiver to verify with that value:
Signing with a generated secret: whsec_… — configure your receiver to verify with thisRobustness. If the stream drops it reconnects with capped backoff (1s → 2s → 5s → 10s → 15s); a
successful reconnect resets the counter, so a long-lived stream survives transient drops. After a run
of consecutive failures with no successful read it gives up with a clear message. 401/403 is
treated as terminal (a bad or unpermitted key won't fix itself). Ctrl-C exits cleanly. A persistent
SANDBOX / LIVE banner prints like every other command, with a loud extra warning when you listen on
a LIVE key.
Like the rest of the CLI, listen handles no PHI — the streamed events are PHI-free by contract.
Config & profiles
Config is stored at ~/.neolife/config.json (dir 0700, file 0600 — it holds a secret key and is
never world-readable), one entry per named profile:
{
"currentProfile": "default",
"profiles": {
"default": {
"apiKey": "nk_sandbox_…",
"baseUrl": "https://api.neolife.health",
"webhookSecret": "whsec_…"
}
}
}Keys and secrets are always masked when displayed (self-describing prefix + last 4). Profiles let a
dev hold a sandbox key and (rarely) a live key side by side and switch per-command with -p/--profile:
neolife login --key nk_sandbox_xxx --profile default
neolife login --key nk_live_xxx --profile prod # stored, but never the default
neolife orders list --profile prod # explicit opt-in to liveIf no config file exists yet, the CLI falls back to a default profile pointed at
https://api.neolife.health — so --base-url / --key work even before you log in.
Errors & exit codes
API failures surface the server error envelope
({ error: { type, code, message, request_id }, message }, see
docs/prd/DEVELOPER-PLATFORM.md) with the request_id for
support. Common cases:
- No key configured →
Runneolife loginfirst (or pass --key). - Unreachable API →
Could not reach the neolife API at <url> — is it running? - Unknown webhook event (
trigger/listen --events) → fails loudly, listing the valid catalog. triggerreceiver returns non-2xx → exit code1and the receiver's response body is printed.listenstream401/403→ terminal error (won't retry); other drops reconnect with backoff.
Relationship to the MCP and SDKs
The CLI is one of several clients over the same fulfillment API, each for a different consumer:
| Surface | Package | For |
|---|---|---|
| CLI (this) | @neolifehealth/cli | A human in a terminal — pair a key, inspect orders, test webhooks. |
| MCP | @neolifehealth/mcp | An AI agent — the same read/draft operations exposed as MCP tools. |
| Node SDK | @neolifehealth/sdk | Application code — typed client + webhook verification. |
| Python SDK | (packages/sdk-python) | Application code in Python. |
All four enforce the same invariants: sandbox-by-default, provider-approval-gated submission (no
client can auto-submit a clinical order), and PHI-free webhooks. The CLI is deliberately decoupled —
it ships a self-contained copy of the key-prefix parser and the Standard-Webhooks signer rather than
importing @neolifehealth/sdk, so the published package has zero workspace build coupling. The signer is
kept a byte-for-byte mirror of apps/api/src/modules/dev-webhooks/webhook-signer.ts so signatures are
interchangeable across surfaces.
See docs/prd/DEVELOPER-PLATFORM.md for the developer-platform roadmap and the API's error/webhook contracts.
