@fulldivevr/workcord-cli
v1.3.0
Published
Agent-first CLI using persistent Hermes tenant chat, with advanced API diagnostics.
Readme
@fulldivevr/workcord-cli
TypeScript CLI for talking to WorkCord agents through persistent Hermes tenant chat and using the WorkCord API. One invocation = one user action. Designed for automated agents such as Claude Code: ask the user's Assistant for the outcome you need, let that tenant manage its own instructions, skills, and flavors, then inspect its reply. Route-level commands and advanced diagnostics remain available.
Lives in the backend repo (clients/workcord-cli) so the spec it generates
from is always the spec this server serves.
Install
npm i -g @fulldivevr/workcord-cli
# or run without installing
npx @fulldivevr/workcord-cli --helpConfiguration
| Setting | How |
| --- | --- |
| Base URL | WorkCord production by default; override with saved configuration, WORKCORD_API_URL, or --base-url <url> |
| User token | Saved by auth login --save, or supplied by WORKCORD_TOKEN |
| Signing secret | WORKCORD_JWT_SECRET env var only — never a flag; privileged, enables --as impersonation |
| Internal admin key | WORKCORD_INTERNAL_API_KEY, internal commands only |
| Acting user | --as <userId>, per invocation — only with WORKCORD_JWT_SECRET |
Base URL must be a bare origin (scheme + host, no path). The /api/v1 prefix
is added by the CLI. Resolution order is --base-url, WORKCORD_API_URL,
saved configuration, then the WorkCord production default.
Your deployment's origin comes from whoever operates it. A local server is
http://localhost:3001 (WorkCord) or http://localhost:3000 (RoomCord /
JesusCord).
workcord auth login --email [email protected] --save
workcord rooms listUse another server for one command:
workcord --base-url https://your-server.example.com rooms listSave another server and login for future commands:
workcord --base-url https://your-server.example.com auth login --email [email protected] --saveReset the saved server to the built-in default by clearing baseUrl from
~/.workcord/config.json and logging in again.
Output is JSON on stdout, errors on stderr, non-zero exit on failure.
How auth works
Every request carries a bearer token. There are two ways to get one, and one extra header for internal admin routes.
1. A real user token (normal path). Log in once with --save, and every
command runs as that user with exactly that user's permissions:
workcord auth login --email [email protected] --save
# → code arrives by email, CLI prompts for it, prints
# { token, accessToken, refreshToken, user, isNewUser } on stdout
# and confirms the login was saved locally
workcord rooms listWORKCORD_TOKEN is sent as-is in Authorization: Bearer; the CLI never signs
anything in this mode and --as is not needed. This is the least-privilege
setup and the right one for agents.
Non-interactive (no TTY) auth login --email prints a sessionId and you
finish with:
workcord auth verify-email --session-id <id> --code 123456Email login returns accessToken; phone/social login returns token. The CLI
emits both fields on either path so callers can read one consistently — either
value works as WORKCORD_TOKEN.
Email auth is domain-restricted. WorkCord prod and VIA both allow
fulldive.com, via.com, via.com.tw, viatech.com.cn, *.tvbs.com,
tvbs.*.com, tvbs.com.*, and *.tvbs.com.tw (EMAIL_AUTH_ALLOWED_DOMAINS in
.sc/stacks/wize-rooms-api/client.yaml). Anything else gets
DOMAIN_NOT_ALLOWED.
auth login --phone is for local and RoomCord only. Deployed WorkCord does not
set AUTH_PHONE_OTP_DEV_BYPASS, so POST /auth/send-otp returns 503
OTP_NOT_AVAILABLE — the CLI turns that into an explicit "use --email" error.
Tokens expire. On 401, log in again with --save, or replace WORKCORD_TOKEN.
2. Local minting / impersonation (privileged, trusted CI only). With
WORKCORD_JWT_SECRET set, the global --as <userId> flag mints an HS256 JWT
locally (payload {userId, id}, 60-day expiry) instead of logging in.
WORKCORD_JWT_SECRET must equal the server's JWT_SECRET env var — the
names deliberately differ, and this is the most common setup mistake.
export WORKCORD_JWT_SECRET=<the server's JWT_SECRET>
workcord --as <userId> users get-meSame secret on both sides means the CLI can act as any user, with no
password and no server-side check. Treat it as privileged signing material:
keep it out of source control, package metadata, logs, screenshots and public
examples, and run this CLI only from trusted machines and CI jobs. Prefer
WORKCORD_TOKEN unless you actually need to switch identities.
Resolution order, in one place (resolveAuth in src/auth.ts):
| Situation | Result |
| --- | --- |
| --as <userId> passed, WORKCORD_JWT_SECRET set | mint and impersonate that user |
| --as <userId> passed, no secret | error — set the secret or drop --as |
| no --as, WORKCORD_TOKEN set | send that token verbatim |
| no --as, only WORKCORD_JWT_SECRET set | error — --as <userId> required |
| neither set | error naming both options |
--as wins over WORKCORD_TOKEN when both are available: passing it is an
explicit statement of impersonation intent.
3. Internal admin routes. WORKCORD_INTERNAL_API_KEY is sent as the
x-internal-key header; the server compares it to its own INTERNAL_API_KEY.
Required for /internal/* and the jesuscord admin routes. Treat it like the signing secret.
Usage
workcord [--as <userId>] <namespace> <command> [flags]--as is only needed when minting with WORKCORD_JWT_SECRET; with
WORKCORD_TOKEN set, omit it everywhere.
Agent-first customization
To change agent behavior, create or update a skill, or maintain a flavor, talk
to the owning agent through Hermes tenant chat. Do not edit prompts or tenant
files directly. With no --agent, the user's non-deletable Assistant is the
target and becomes the default owner of anything it creates:
workcord agent chat new --message "Create a skill that prepares a concise weekly sales report and enable it."
workcord agent chat new --message "Update my default flavor instructions to require sources in research reports."Use the returned chatId for follow-up, verification, or rollback:
workcord agent chat send --chat "$CHAT_ID" --message "Verify the change, list the versions you created, and explain how to roll it back."Both commands wait up to two minutes by default and return the durable Hermes
message document. Use --async for a queued response, then continue with its
messageId:
workcord agent chat wait --message-id "$MESSAGE_ID" --timeout 10m
workcord agent chat history --chat "$CHAT_ID"If a local wait times out, the response has timedOut: true; the remote agent
continues working and can be checked with chat wait.
To make another owned agent the explicit owner and conversation target:
workcord agent chat new --agent "$AGENT_ID" --message "Create and maintain a finance-specific flavor for yourself."The Assistant or selected agent works within its tenant's Hub permissions. It
can safely validate changes and report what it activated. agent chat uses
Hermes' native durable hub_chat transport; it is not raw A2A wire traffic.
A2A remains available for external agent peers. Direct hermes commands are
advanced diagnostics and recovery tools, not the normal customization workflow.
workcord --helplists namespaces.workcord <namespace> --helplists that namespace's commands.workcord <namespace> <command> --helpshows the command's flags.
Two layers coexist under the same namespaces.
Auto-generated (one per server route, namespaced by controller):
workcord <namespace> <command> [--<path-param> <value>] [--body '<json>'] [--query '<json>']Hand-written semantic (small set, rich typed flags):
workcord auth login --email [email protected]
workcord --as USER messages send-text --room R --text "hello"Semantic aliases cover common agent/admin actions with consistent flags: paginated listing/searching public users, internal authenticated/anonymous user filters, acting-user room lists, discoverable app-wide room lists, room create/update/delete, join/leave/read, list members, remove members, list/approve/deny join requests, rotate join codes, get the acting user's membership, and merge recent messages visible to a user across rooms. The generated route-level commands remain available for every endpoint.
users list --account-type ... and users list --authenticated use the
internal API and require WORKCORD_INTERNAL_API_KEY.
Without those flags, users list uses public search and only returns public
profile fields.
Server auto-extracts mention tokens from visible @Name text. Prefer
messages send-text --mention-user USER_ID so the CLI resolves the room
member's display name and avoids exposing raw user ids. auth login never
takes --as — it is how you obtain a WORKCORD_TOKEN in the first place.
Error handling
Every error is a single JSON line on stderr, with a non-zero exit code:
{"error":"GET /api/v1/rooms/X -> 404","status":404,"body":{"error":{"code":"NOT_FOUND","message":"Room not found"}}}Agents can branch on .status (401 → re-login, 404 → resource gone, 5xx
→ already retried, give up) and .body.error.code for app-specific codes.
Examples
workcord --as USER rooms get-room --room-id R
workcord --as USER rooms create-room --body '{"name":"hello","isPublic":true}'
workcord --as USER messages list --room-id R --query '{"cursor":"2026-04-28T00:00:00Z","limit":50}'
workcord --as USER users list --q alex --limit 10
workcord --as ADMIN users list --account-type anonymous --limit 20
workcord --as ADMIN users set-plan --user USER_ID --tier pro --platform internal
workcord --as USER rooms discover --search "ai" --tags ai,tools --limit 20
workcord --as USER rooms create-with-message --name "ops" --public --tags ai,news --text "First post."
workcord --as USER rooms members --room R
workcord --as USER rooms upload-icon-file --room R --file ./icon.png
workcord --as USER agent chat new --message "Create a room helper that can research the web."
workcord --as USER room-agents install-hosted --room R --agent system-wizeup
workcord --as USER messages react --room R --message M --emoji 👍
workcord --as USER dm send-text --to USER_B --text "hi"Common agent patterns
Send a message with a mention + image:
URL=$(workcord --as $U rooms upload-image-file --room $R --file ./img.png | jq -r .url)
workcord --as $U messages send-text --room $R --text "look @user_abc" --attachments "$URL"Poll for new messages since last check:
SINCE=2026-04-28T10:00:00Z
workcord --as $U messages list --room-id $R --query "{\"cursor\":\"$SINCE\",\"limit\":50}"Receive latest messages visible to a user across rooms:
workcord --as $U messages latest --limit 50 --per-room-limit 10
workcord --as $U messages latest --since "$SINCE" --inbox-category aiRooms --limit 100This composite command lists the acting user's rooms, fetches recent messages
from each room, merges them by sentAt descending, and includes
room/roomName context on each message.
Switch user identity per call:
Saved login is for one interactive user. Agents can use WORKCORD_TOKEN to
choose the token owner per invocation; with WORKCORD_JWT_SECRET, one process
can act as 100 users by changing --as <userId> per invocation.
Onboard a fresh agent identity:
# deployed WorkCord / VIA
USER=$(workcord auth verify-email --session-id $SID --code $CODE | jq -r .user.id)
# local / RoomCord dev, where AUTH_PHONE_OTP_DEV_BYPASS=true
USER=$(workcord auth login --phone "+14155550$RANDOM" | jq -r .user.id)Locally any phone with OTP 111111 works. On deployed WorkCord phone OTP is
disabled — start with workcord auth login --email <address>.
React to a message + reply with mention:
workcord --as $U messages react --room $R --message $M --emoji 👍
workcord --as $U messages send-text --room $R --text "agreed @user_abc" --reply-to $MDM a user once-and-done:
workcord --as $U dm send-text --to $OTHER --text "hey"
# → composite: opens (or finds) the DM room, sends the message, returns bothAdvanced Hermes diagnostics and recovery (hermes namespace)
WorkCord provisions a Hermes Hub tenant for each room-agent, Hermes custom
agent, and room-scoped marketplace flavor install. That tenant is where the
agent actually runs: its chats, runs, files, skills, flavor pin, A2A peers and
eval runs all live on the Hub, not in WorkCord. Normal changes should be
requested with workcord agent chat; the hermes namespace exists for direct
inspection, diagnostics, and recovery.
This is the one namespace that talks to a second origin. Every other command
goes to the WorkCord API; hermes uses the WorkCord API only to reveal the
tenant key, then calls the Hub's /v1/tenant-api/* routes with it.
Target selection
Every hermes command takes exactly one of:
| Flag | Meaning |
| --- | --- |
| --agent <id> | a custom agent id, used verbatim |
| --room-agent <id> | a room agent id, resolved as workcord-room-agent:<id> |
| --marketplace-install <id> | an isolated marketplace flavor install id or its room agent id |
Passing more than one, or none, is an error. Marketplace install access follows current room ownership; transferring room ownership revokes the former creator's ability to reveal the tenant credential.
Credentials
The tenant key is resolved in one place (resolveHermesKey in src/auth.ts)
and is never accepted as a flag:
| Env var | Meaning |
| --- | --- |
| WORKCORD_HERMES_KEY | tenant key, used verbatim; requires WORKCORD_HERMES_HUB_URL |
| WORKCORD_HERMES_HUB_URL | Hub origin (bare origin, no path); requires WORKCORD_HERMES_KEY |
| WORKCORD_HERMES_TENANT_ID | tenant id for the tenant-scoped routes, alongside the two above |
Setting only one of WORKCORD_HERMES_KEY / WORKCORD_HERMES_HUB_URL is an
error — the CLI will not half-guess a hub.
With none of them set, the CLI calls
POST /api/v1/agents/{id}/hermes-cli-key/reveal on WorkCord (as the acting
user, so ownership is enforced server-side) and gets back the key, tenant id and
hub origin.
Revealed credentials are cached per agent ref in ~/.workcord/config.json
under hermes, alongside the saved login. The file is written 0600 inside a
0700 directory. Two flags control it:
| Flag | Effect |
| --- | --- |
| --refresh | ignore the cached entry, re-reveal, rewrite the cache |
| --no-cache | neither read nor write the cache for this invocation |
Env credentials always win over the cache. hermes key prints the resolved
credentials on stdout — that is its whole job; no other command emits key
material.
Commands
| Group | What |
| --- | --- |
| hermes key / whoami / runtime | reveal credentials, show the tenant, show its runtime |
| hermes agent status\|start\|stop\|restart\|card | tenant agent lifecycle |
| hermes chat list\|create\|get\|messages\|send\|message\|stream\|stop\|approve | chats and chat messages (stream is SSE) |
| hermes run create\|get\|stream\|stop\|approve | runs |
| hermes files ls\|cat\|download | tenant-visible files |
| hermes share groups\|ls\|read\|stat\|grep\|find\|write\|mkdir\|move\|rm | share-group filesystem |
| hermes skills list\|enable\|disable\|files\|cat | skills installed on this tenant |
| hermes flavor current\|catalog\|get\|set\|subscribe\|checkout\|publish\|rollout | flavor marketplace and this tenant's pin |
| hermes skill catalog\|get\|checkout\|publish | skill marketplace |
| hermes eval suites\|run\|list\|get\|cancel | flavor eval suites and runs |
| hermes peers list\|get\|call\|find | A2A peers |
| hermes conversations list\|get\|messages | A2A conversations |
| hermes sessions list, hermes artifacts | agent sessions and artifacts |
workcord hermes whoami --room-agent $RA
workcord hermes whoami --marketplace-install $MARKETPLACE_INSTALL
workcord hermes chat send --room-agent $RA --chat $CHAT --text "status?"
workcord hermes flavor current --agent $AGENT
workcord hermes peers find --agent $AGENT --query "translation" --limit 5Version flags are named --flavor-version and --skill-version, not
--version: commander's program-level .version() matches --version
anywhere in argv, prints the CLI version and exits before the subcommand runs.
Direct flavor and skill recovery
For normal work, ask the Assistant or another owned agent to create, update, validate, or activate its own flavor and skills. Hermes Hub supports tenant-owned skill and flavor namespaces; ownership defaults to whichever agent tenant performs the creation.
The direct flavor publish, flavor rollout, and skill publish commands are
kept for advanced recovery and authorized publisher workflows. They operate as
the selected tenant and can only mutate namespaces that tenant owns.
The CLI does not pack or untar archives. The authoring loop is:
workcord hermes flavor checkout --agent $AGENT --flavor-version 1.4.0 --out flavor.tgz
# unpack, edit, and repack with hermes-hub's own tooling
workcord hermes flavor publish --agent $AGENT --archive flavor.tgz--archive must already be a gzipped package built beforehand; anything over
32 MiB is rejected locally before upload.
flavor set warns (and refuses without --force) on a downgrade or a
deprecated target. flavor rollout defaults to the policy-respecting automatic
rollout; --all-matching re-pins every tenant on the Hub pinned to that flavor
id regardless of their own update policy, and therefore requires --force.
Development
cd clients/workcord-cli
npm install
npm run build
npx tsx src/cli.ts --as <userId> users get-meRefreshing the spec when the server changes
npm run sync-spec builds the backend from the repo root (npm run
dump-spec) and copies the resulting cli/openapi.json into this package.
No GitHub token and no network — the spec comes from the source tree you are
sitting in.
npm run regen # sync-spec + generate
npm run typecheck # confirm cleanGenerated files live in src/generated/ and are committed, so the CLI works
without running regen if you don't need the latest server changes.
Layout
src/
├── cli.ts commander entry, global --as flag
├── auth.ts auth resolution: WORKCORD_TOKEN vs minted --as JWT,
│ plus Hermes Hub key resolution
├── http.ts shared fetch primitives (retry, errors, query)
├── client.ts WorkCord fetch wrapper (JSON + multipart)
├── hermes-client.ts Hermes Hub fetch wrapper (JSON, upload, SSE, binary)
├── hermes-guards.ts rollout/ownership/archive guards, version compare
├── commands/ hand-written semantic commands
└── generated/ auto-generated from openapi.json
├── index.ts
└── commands/<tag>.ts one file per controller namespace
scripts/
├── sync-spec.ts backend build → openapi.json
└── generate-commands.ts spec → commander commands
tests/
├── auth.test.ts node --test suite for auth resolution
└── smoke.sh end-to-end test, needs a live serverAdding a hand-written semantic command
When an auto-generated command has a use pattern that would benefit from rich
flags (e.g. typed fields instead of an opaque --body):
- Create
src/commands/<area>.ts. - Use
getOrCreateGroup(program, "<area>", "...")to extend the auto-generated namespace, then.command("<verb>")for the new subcommand. - Register in
src/cli.ts. - Add an assertion in
tests/smoke.sh.
Testing
npm test # hermetic, no network — auth resolution rules
npm run smoke # end-to-end, needs a live serverHermetic end-to-end suite (creates two fresh users per run via OTP, exercises
multi-user flows, file upload, custom agents, error paths). Requires a running
server (WORKCORD_API_URL) and a matching WORKCORD_JWT_SECRET, so it is not
part of any automated gate.
Release
Publishing is a manual maintainer action — there is no auto-publish CI.
npm run regen— refresh the spec and generated commands; commit the diff.npm run typechecknpm testthennpm run buildnpm pack --dry-run— confirm the file list is exactlydist/**/*.js,README.md,LICENSE,package.json. No sources, noopenapi.json, no.npmrc.- Smoke:
node dist/cli.js --helpandnode dist/cli.js --version. - Bump
versioninpackage.json, thennpm publish.
prepack re-runs typecheck + test + build, so steps 2–3 also gate the publish.
License
Proprietary — see LICENSE. Published to npm for installation by authorized Fulldive personnel and systems only; no reuse rights are granted.
