The OpenWOP CLI is the local control plane for the OpenWOP demo app (apps/workflow-engine, live at app.openwop.dev) and a lightweight client for any OpenWOP-compatible host.
This is the standalone home of the @openwop/cli package; it ships independently of the openwop/openwop spec corpus.
One-line install (detects the OS, ensures Node 22+, installs the package, then onboards):
curl -fsSL https://openwop.dev/install.sh | bashOr directly: npm i -g @openwop/cli (needs Node 22+), then openwop onboard.
openwop --help
openwop doctor # check prerequisites
openwop onboard # guided setup (host + provider + key + model)
openwop demo start # boot local backend + frontend (from inside an openwop checkout)
openwop demo status
openwop catalog nodes --search ai
openwop runs create sample.demo.uppercase --input text=hello --wait
openwop packs search ads # browse the signed pack registryThe
demosubcommands operate on anopenwop/openwopcheckout discovered by walking up from your current directory; run them from inside a clone of that repo.
The onboard wizard walks you through:
- Host URL —
https://app.openwop.dev/api(shared demo),http://localhost:8080(local), or a custom URL. - AI provider —
anthropic,openai,google, orminimax(matches what the demo backend dispatches to). - API key — auto-detects
ANTHROPIC_API_KEY/OPENAI_API_KEY/GOOGLE_API_KEY/MINIMAX_API_KEY, or hidden-input via raw-mode stdin. The key is POSTed to/v1/host/sample/byok/secretson the configured host. The key is never written to your local config file — only a credential ref pointer is stored. - Model — provider-specific recommended defaults plus a custom option.
- Test the connection — verifies the credential ref appears in the host's BYOK list.
Re-running openwop onboard is safe: it detects existing config and asks Keep / Modify / Reset.
For scripted use:
openwop onboard --non-interactive \
--base-url-choice shared \
--provider anthropic \
--api-key-env ANTHROPIC_API_KEY \
--model claude-sonnet-4-6openwop providers list
openwop providers add openai --api-key-env OPENAI_API_KEY --model gpt-4o
openwop providers remove openai
openwop providers test anthropicproviders add POSTs to /v1/host/sample/byok/secrets; remove DELETEs; list and test read.
openwop chat <workflowId> opens an interactive streaming REPL. Each message you type creates a run for the workflow carrying the running conversation as a messages array, then streams that run's events to the terminal as they arrive.
openwop chat sample.chat.turn
openwop chat sample.chat.turn --inputs-json '{"credentialRef":"anthropic-default"}'
openwop chat sample.chat.turn --no-stream --json- Streaming — prefers Server-Sent Events (
GET /v1/runs/{runId}/events). When the host does not stream (it answers with JSON or a non-streamable body), the CLI falls back to pollingGET /v1/runs/{runId}/events/poll. - Turns — type a line and press Enter. Conversation history is threaded across turns; the assistant's reply is fed back as context. Use
--no-historyto send only the latest turn. - Quitting —
/exit,/quit, or Ctrl-D (EOF). --json— emits raw event records (one JSON object per event) instead of the prettyassistant>rendering.- Extra per-turn inputs (
--input k=v,--inputs-json) ride along on every run, so you can pin acredentialRef, model, or other configurable input.
openwop packs search ads # filter the catalog
openwop packs info community.openwop-team.demo # metadata + versions
openwop packs install community.openwop-team.demo@0.1.0 # download + verify
openwop packs publish ./my-pack --key ~/key.pem --key-id me-1
openwop packs yank community.openwop-team.demo@0.1.0 # local registry editThese talk to the signed node-pack registry — a separate surface from the host --base-url. The default registry is https://packs.openwop.dev; override with --registry-url or OPENWOP_REGISTRY_URL.
- search reads
/v1/index.jsonand filters the catalog client-side (the dynamic host/v1/packs/-/searchonly knows in-process nodes, not the published catalog). - info reads
/v1/packs/{name}/index.json;--version valso fetches that version's manifest. - install downloads
/v1/packs/{name}/-/{version}.tgz, checks itssha256against the manifestintegrity, and verifies the detached Ed25519.sigagainst the publisher key at/keys/{keyId}.pub(matchingsigning.method—ed25519signs the tarball,manualsigns the in-tarballpack.json). Skip verification with--no-verify. Artifacts land under~/.openwop/packs/{name}/{version}/(override with--dir). Yanked versions are refused. - publish — the reference registry has no write API (
writeApi.supported=false; publish is a GitHub pull request). This command performs the local packaging + Ed25519 signing flow (mirrorsscripts/build-pack-tarball.mjs --signed): it emits a deterministic signed.tgz, a 64-byte.sig, and a sidecar manifest intodist/packs/(override with--out), ready to commit and PR. The private key comes from--key <pem>, else~/.openwop-keys/{keyId}.private.pem; if neither exists an ephemeral key is generated and its public half printed for pre-registration. - yank edits a local registry checkout — flips
"yanked": truein the version manifest (--undoreverses), so the change is ready to commit, rerunregistry/scripts/build-index.mjs, and PR. Run it from inside the repo.
Connect chat channels (Signal / WhatsApp / iMessage) to the host so inbound messages drive workflow runs and replies are delivered back. Channels are a host-extension surface (/v1/host/sample/messaging), not part of the normative OpenWOP wire — the protocol stays channel-agnostic.
openwop relay setup --channel signal # register + activate a device, store its token
openwop relay start # bridge loop: heartbeat → poll outbound → deliver → ack
openwop relay send --conversation +15551234 --text "hi" # operator-side: queue an outbound
openwop relay status # probe the device token against the host
openwop messaging connectors list|add|enable|disable|test
openwop messaging sessions list|inspect|close
openwop messaging policy get|set <connectorId> # DM/group access + require-mention
openwop messaging routing list|add|remove # inbound match → bound workflow
openwop messaging identity list|show|create|link|unlink|delete # cross-channel peer linking
openwop messaging logs [--channel c] [--direction inbound|outbound] [--status s] [--limit n]
openwop notify email --to a@b.dev --text "hi" [--subject S] # one-off dispatch
openwop notify sms --to +15551234 --text "pong"The operator subcommands manage the gateway beyond device wiring:
- policy — per-connector access:
set <id> --dm <pairing|allowlist|open|disabled> --group <allowlist|open|disabled> --require-mention <true|false>.getreturns the host default (DM pairing, groups allowlist-only, mention required) when none is stored. - routing —
add --pattern "*" --workflow <id> [--channel c] [--priority n]binds matching inbound conversations to a workflow; higher priority wins.remove <ruleId>deletes one. - identity — links platform peers across channels into one logical person:
create --name N --peer <channel>:<peerId>,link <id> --peer …(merges, de-duped),unlink <id> --peer …,delete <id>. - logs — queries the delivery log (inbound ingested / outbound queued), filterable by channel, direction, status;
--limitis clamped to[1, 1000]. - notify — a one-off email/SMS dispatch (
POST …/messaging/notify). The reference app returns a synthetic receipt; wiring a real provider (SES / Twilio) is a host concern.
How it works: the relay device owns the platform connection and bridges it to the host. Inbound messages POST to the host, which runs the bound workflow (default sample.demo.uppercase; override with OPENWOP_MESSAGING_WORKFLOW_ID) and queues the reply; the device pulls + delivers it. doctor reports per-channel readiness:
- Signal needs
signal-clionPATH. - iMessage needs macOS (Messages signed in + Full Disk Access for
chat.db). - WhatsApp ships with the channel build (Baileys), not the core CLI.
relay start also receives: each channel plugin streams inbound platform messages and forwards them to the host's /device/inbound (which runs the bound workflow and queues a reply). Signal uses signal-cli ... receive, iMessage polls chat.db by ROWID, WhatsApp binds Baileys messages.upsert. Disable inbound with --no-receive. When a channel's tooling isn't present, inbound is skipped and outbound prints to the console instead of silently dropping it.
The relay device token is a host credential, so it is stored separately from config.json in ~/.openwop/relay-credentials.json (mode 0600), not in your main config. Revoke it any time with openwop relay revoke.
Beyond runs create / list / get / cancel, the CLI surfaces a run's full inspection + review surface:
openwop runs events <runId> [--since <seq>] [--limit n] # JSON event poll (GET .../events/poll)
openwop runs annotations <runId> # list review signals
openwop runs annotate <runId> --rating 5 [--note "great"] # attach a rating/label/correction/flag
openwop runs annotate <runId> --label triage --event-id <id>
openwop runs debug-bundle <runId> [--max-events n] [--out bundle.json]
openwop runs ancestry <runId> # RFC 0040 cross-host parent chainruns events --since N maps to the spec-canonical lastSequence query param (events with sequence > N). runs annotate posts exactly one signal kind (--rating 1-5 | --label | --correction | --flag), validated client-side. runs debug-bundle --out <file> saves the full event bundle to disk.
Beyond runs/workflows/catalog, the CLI surfaces the host's operator endpoints: agents, memory, media, webhooks, chat, plus notifications (inbox), interrupts (list/resolve human-in-the-loop pauses), and prompts (RFC 0029 library list/get/render). Every command supports --json.
These drive the demo app's protocol-enabled agent surfaces (each --help cites its RFC + endpoint):
openwop roster list # named standing agents + workflow portfolio (RFC 0086)
openwop roster create --persona "Sally" --agent-ref core.x.agent --workflow sample.demo.triage
openwop org-chart get # descriptive departments/reporting (RFC 0087)
openwop org-chart set --file ./org.json # replace the chart (departments[] + members[])
openwop kanban boards # agent task boards
openwop kanban board-create --name "Sally's work" --roster r_123
openwop kanban card-add b_1 --title "Triage" --column todo
openwop kanban watch b_1 # stream a board's card events (SSE)
openwop orgs list # organizations + RBAC (RFC 0049)
openwop orgs teams|groups|roles|members <orgId> list|create|update|delete
openwop orgs effective --subject user:jo # resolve a subject's effective access
openwop workspace list # per-tenant agent workspace files (RFC 0059)
openwop workspace put notes/todo.md --content "- ship it"
openwop byok list # BYOK credential refs (never values)
openwop byok set --ref anthropic-prod # prompts for the secret (no shell history)
openwop agents create --persona "Triage bot" --model-class fast # user-defined agent CRUDDestructive commands (roster delete, kanban board-delete/card-delete, orgs … delete, workspace delete, byok delete, agents delete) require an explicit --yes.
Two maintenance commands guard destructive operations behind a confirmation prompt (default no, bypass with --confirm):
openwop account delete # irreversibly wipe ALL data for the signed-in account
openwop admin cleanup # wipe expired ephemeral secrets for idle tenants
openwop admin cleanup --status # read-only liveness probeaccount delete requires a signed-in user (OIDC bearer). admin routes are gated by the host's OPENWOP_ADMIN_TOKEN — pass it via --api-key.
~/.openwop/config.json (or $OPENWOP_CONFIG_HOME/.openwop/) stores the host URL, default provider, default model, and credential ref. API keys are never stored locally.
openwop config file # print path
openwop config get # print full config
openwop config get host.baseUrl # dotted-path lookup
openwop config set defaultModel gpt-4o
openwop config unset credentialRef- Host URL:
OPENWOP_BASE_URLorhttp://localhost:8080. Flag (--base-url) wins over env; env wins over default. - OpenWOP host bearer (NOT the LLM provider key):
OPENWOP_API_KEY, orsample-tokenfor localhost demo URLs. Pass via global--api-keyif you need to override. - LLM provider key:
--provider-key <key>or--api-key-env <VAR>ononboard/providers add(not stored locally). - Frontend URL:
http://localhost:5173.
Input parsing for runs create:
--input k=v— each value isJSON.parsed first; on parse failure it falls back to a string. So--input n=5is the number5,--input enabled=trueis the booleantrue,--input text=hellois the string"hello", and--input list=[1,2,3]is an array. Quote shell-special characters.--inputs-json '{"a":1}'passes the wholeinputsobject as one JSON literal. Merged BEFORE--inputpairs (which override).--waitpollsGET /v1/runs/{runId}every 250ms until terminal status or--timeout-ms(default 30000) elapses. Exit 0 only oncompleted.
Exit codes:
0— success.1— server-side failure (5xx) or run terminated infailed/cancelledunder--wait.2— user-fixable error (unknown command, missing argument, 4xx response).
The CLI is dependency-light by design. It uses Node built-ins so contributors can operate the demo app without installing another command framework.
This repo is a self-contained TypeScript package (the build is esbuild; it does not depend on the spec corpus). Requires Node ≥ 20.
npm ci
npm run typecheck # tsc --noEmit (strict, noImplicitAny)
npm run build # → dist/openwop.js (esbuild bundle)
npm test # build + node --test test/*.test.mjs
node dist/openwop.js --help # run the local buildCI (.github/workflows/ci.yml) runs typecheck + tests on every push and PR.
The package publishes to npm via an OIDC trusted publisher with provenance — no tokens in CI. Pushing a vX.Y.Z tag triggers .github/workflows/publish.yml, which builds, tests, and publishes @openwop/cli@X.Y.Z (idempotent: it skips if that version is already on npm).
- Bump
versioninpackage.jsonandVERSIONinsrc/constants.ts(kept in lockstep), add aCHANGELOG.mdentry. - Commit (DCO-signed), then tag and push:
git tag vX.Y.Z && git push origin vX.Y.Z.
One-time setup (maintainer): register this repo +
publish.ymlas the trusted publisher for@openwop/clion npmjs.com (package → Settings → Trusted Publishers). The package was previously published fromopenwop/openwop; the binding must be repointed here before the first release.
Commits must be DCO signed off (git commit -s). Follow Conventional Commits. Licensed under Apache-2.0 (see LICENSE).