Last updated: 2026-06-29
- Status: Reference (partial — surface defined, exact flags to be finalized as the CLI is built)
- Surface: CLI (
relavium) - Scope: Phase 1, local-first. Same
@relavium/coreengine as every other surface. - Related: home.md, chat-session.md, ../vscode/extension-api.md, ../desktop/routes-and-screens.md, ../contracts/workflow-yaml-spec.md, ../contracts/agent-yaml-spec.md, ../contracts/sse-event-schema.md, ../shared-core/built-in-tools.md, ../../tutorials/cli/run-a-workflow-in-ci.md, ../../runbooks/add-a-provider-key.md
The relavium CLI is the terminal surface of the platform and the fastest way to run a workflow non-interactively (scripts, CI/CD). It embeds the same @relavium/core engine as the desktop app and VS Code extension — there is no separate "CLI engine," so behavior is identical across surfaces (see ../../architecture/shared-core-engine.md). The CLI is built second (right after the engine) and serves as the engine's canonical integration-test harness.
- Package: published to npm as
relavium, installed globally. The artifact is an engine-inlined ESM bundle — the proprietary@relavium/*engine is bundled in; every third-party dependency (including the prebuilt native addons) installs normally (ADR-0051). A global install needs no compiler toolchain. - Build: TypeScript bundled with
tsupto a single ESMbin; released via theRelease CLIworkflow (pack → cross-OS install-smoke on macOS/Linux/Windows → npm publish with provenance), see release-a-surface.md. - Stack:
commander.jsfor argument parsing,ink(React for terminals) for the interactive TUI,@clack/promptsfor setup wizards. - API keys: stored in the OS keychain via
@napi-rs/keyring(macOS Keychain / Windows Credential Manager / Linux libsecret) — never plaintext, and never the archivedkeytar(see ADR-0019 and add-a-provider-key.md). - Workflow discovery: reads workflows from the
.relavium/directory in the project root, or from an explicit path argument.
npm install -g relavium
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.tsThe CLI auto-detects its environment and switches presentation accordingly:
| Mode | When | Behavior |
|---|---|---|
| Interactive TUI | TTY attached, no --json |
ink-rendered live view: animated per-node status, streaming token output for the active node, final cost/duration summary |
| Plain | No TTY or CI=true (and no --json) |
The TUI is disabled; a terse line-per-lifecycle-event human renderer writes to stdout |
| NDJSON | --json (anywhere on the command line) |
The machine contract: stdout is a pure NDJSON stream — RunEvents for run / gate, or SessionEvents for chat / agent run — and all diagnostics go to stderr. See The --json machine-output contract |
NDJSON is engaged only by --json (the explicit machine opt-in); a non-TTY or CI=true
environment disables the interactive TUI but does not by itself switch stdout to NDJSON
(ADR-0049). Exit codes are CI-friendly
(see Exit codes).
--no-color (and --color) do not change the mode — the interactive TUI stays active and only ANSI
color/dim are suppressed (plain output without a renderer swap). A swap to the Plain renderer happens only on
no-TTY / CI=true, and to NDJSON only on --json.
Color precedence. ANSI styling is orthogonal to the output mode above, resolved with this precedence:
an explicit --color / --no-color flag > the NO_COLOR env var (any non-empty value ⇒ off, the
no-color.org accessibility contract) > the FORCE_COLOR env var (0/false ⇒ off,
the supports-color convention) > on by default. NO_COLOR intentionally beats FORCE_COLOR — a user
who opts out of color wins over a tool/CI that opts in. (A --json/CI/no-TTY stream carries no ANSI regardless
— that is the separate output-mode selection above.)
Under relavium run --json, the CLI emits a stable machine contract a CI job can pipe and assert
on (ADR-0049). The contract covers a workflow
run; --help, --version, and a bare no-command invocation are exit-0 meta-operations that
print their human text (usage / version) to stdout as usual, --json notwithstanding. (On a genuine
interactive TTY a bare relavium instead opens the interactive Home — see home.md for
the gate; the help + exit-0 meta-op is preserved byte-for-byte on every non-interactive path: --json,
a pipe/redirect, or CI.)
- stdout is a pure NDJSON stream of RunEvents — one event
serialized verbatim per line, in
sequenceNumberorder. The line is the stable envelope (type/runId/timestamp/sequenceNumberper the schema); there is no wrapper, stream header, or version line. Every event the run emits appears, unfiltered. - The terminal
run:completedevent is the final result line — it already carriesoutputs+totalTokensUsed+totalCostMicrocents+durationMs, so there is no separate summary line. Arun:failedterminal carrieserror+partialOutputs; arun:cancelledcarries only the envelope (read run totals for those from the lastcost:updated.cumulativeCostMicrocents). - All diagnostics go to stderr, never stdout. A pre-run CLI fault (bad arguments, workflow not
found, missing key) is written to stderr as a structured
{ "type": "error", "code", "message" }envelope (distinct from the run stream'srun:failed/node:failed) and exits2; stdout stays empty. A pipe consumer reads stdout for events and uses the exit code + stderr for faults. - Secret-typed values stay masked — the engine masks them as
{ "secret": true, "ref": … }before any renderer sees them; the NDJSON carries that masked shape, never a raw secret.
Exit codes are CI-friendly (see Exit codes).
Agent-readable command surface — now realized by the command manifest. Because Relavium's own thesis is that work starts in an agent, the CLI is a natural tool surface for other agents. The two affordances once proposed here — a machine-readable help mode (
--helpemitting the command/flag surface as JSON, so an agent can discover the CLI without scraping prose) and a per-commandeffectannotation (read/write/destructive) so an agent's tool policy can gate destructive commands behind approval — are delivered by the command manifest (ADR-0056, 2.5.C). Approval enforcement of adestructiveentry is owned by ADR-0057 (2.5.E).
These flags are position-independent — they may appear anywhere on the command line, so
relavium run wf --json and relavium --json run wf are equivalent (the CLI extracts them
before parsing the subcommand).
| Flag | Effect |
|---|---|
--json |
Emit machine-readable NDJSON output (disables the TUI) — see Output modes. |
--color |
Force colored (ANSI) output on. |
--no-color |
Disable colored (ANSI) output. |
--cwd <dir> |
Run as if started in <dir> (project discovery and relative paths resolve from here). |
--config <path> |
Use an explicit global config file instead of ~/.relavium/config.toml — the project .relavium/ layers still apply (config-spec.md). |
--no-alt-screen |
Keep the byte-identical inline renderer for the bare Home + relavium chat (no full-screen alternate screen) — the screen-reader fallback. Overrides [preferences].alt_screen; a non-TTY / --json / CI path is always inline regardless (ADR-0068). |
--no-mouse |
Disable terminal mouse reporting (DECSET 1002+1006) inside the full-screen renderer. The wheel stops scrolling the transcript (PgUp/PgDn + Ctrl+Home/Ctrl+End still page), in-app selection and copy-on-select go with it, and the emulator's own click-drag selection works again without a modifier. Overrides [preferences].mouse, and forces [preferences].copy_on_select off. Ignored outside the alt screen (ADR-0068 §e, accessibility.md). |
-v, --verbose |
Print verbose diagnostics to stderr. |
-q, --quiet |
Suppress non-essential output. (--verbose and --quiet cannot be combined → exit 2.) |
-V, --version |
Print the version and exit 0. |
-h, --help |
Print help for the program or a subcommand and exit 0. |
The command set below is the confirmed surface. Commands ship per workstream: run (2.D), gate + gate list (2.G/2.I), provider (2.C), the read commands list / logs / status (2.I), the whole agent-first chat family — chat (2.M), chat-resume (2.N), chat-list (2.O), chat-export (2.P), and chat --json + agent run (2.Q) — and the YAML-lifecycle authoring commands create / import / export (2.J) are all live; budget resume is a tracked follow-up. Invoking a not-yet-shipped command exits with a clean "not available yet (lands in …)" message. Subcommands marked (planned) are intended but not yet locked.
| Command | Purpose |
|---|---|
relavium run <workflow> [--input k=v] |
Execute a workflow. Streams progress; resolves with the workflow output. |
relavium chat [--agent <ref>] |
Start an interactive agent session (the agent-first REPL). See chat-session.md. |
relavium chat-resume <sessionId> |
Reload a persisted session from history.db and continue the conversation. |
relavium chat-list |
List past agent sessions (id, agent, last activity), the way relavium list lists workflows. |
relavium chat-export <sessionId> |
Export a session to a .relavium.yaml scaffold for review (ADR-0026). |
relavium agent run <agent> [--fixture <path>] [--json] |
Run a single agent one-shot (non-interactive) on the same AgentSession infra — the prompt is read from stdin, one turn, then exit. See relavium agent run and agent-run-fixture.md. |
relavium list |
List discovered workflows (and, with a flag, agents) in the current project. |
relavium create [--force] |
Scaffold a new agent or a minimal single-agent workflow YAML via an interactive wizard (schema-validated before write). |
relavium import <path> [--force] |
Import an external .relavium.yaml / .agent.yaml into the project, validated + slug-deduplicated. |
relavium export <id> [--out <path>] [--force] |
Export a workflow/agent to a portable, canonical YAML copy (re-serialized from the validated AST — no provider key by construction, comments dropped; MCP env secrets preserved faithfully, so author them as {{secrets.*}}). |
relavium logs <runId> |
Print the persisted event/log stream for a past run. |
relavium status |
Show active runs and their per-node status. |
relavium gate <runId> |
Resolve a pending human gate (approve / reject / provide input). |
relavium gate list [<runId>] |
List pending human gates (all active runs, or one run) — the multi-gate subcommand for resolving one of several concurrently-pending gates. |
relavium budget resume <runId> [--approve|--abort] |
Resume a run suspended at a budget cap (budget:paused, on_exceed: pause_for_approval) — approve to continue or abort. The non-interactive operator path for ADR-0028. |
relavium init (planned) |
Initialize a .relavium/ directory in the current project. |
relavium agent <subcommand> (planned) |
Manage agents (list / create / test). |
relavium models |
List the cached model catalog (refreshes on first run if the cache is empty). See relavium models. |
relavium models refresh |
Force a live re-fetch of each connected provider's model list into the local cache, reporting per-provider outcomes. |
relavium models pricing <model> |
Hand-enter a user price for a model the registry does not know, so the cost cap enforces it. See relavium models. |
relavium provider <subcommand> |
Manage providers and API keys in the OS keychain (list / add / set-key / remove-key / test). |
Canonical home for the command manifest (ADR-0056, 2.5.C). The runtime form lives in
apps/cli/src/commands/manifest.ts(a CLI-only contract — no other surface consumes a CLI command list).
The command manifest is the one source the shell command surfaces derive from — the commander parser, the executeCommand dispatch table (apps/cli/src/commands/dispatch.ts), and relavium --help --json — so they can never diverge. (The in-REPL / palette + slash commands are a separate, curated registry — see In-REPL slash commands below.) The set is deliberately small and alias-free; every entry is canonical by construction (there is no per-entry alias flag). Each entry is:
{
id // stable id; a subcommand is dotted — `provider.set-key`, `agent.run`, `gate.list`
label // a short human label — "Run workflow", "Set provider key"
description // the one-line help; MUST match commander's .description() (the --help --json text)
args? // [{ name, type: 'string'|'number'|'boolean', required?, description? }]; name is the camelCase CommandInput key
effect // 'read' | 'write' | 'destructive' (see below)
modeScope? // chat modes a command is available in; omit ⇒ all modes
}
effectis a forward-looking annotation:readnever mutates,writecreates/modifies,destructiveirreversibly removes (today onlyprovider.remove-key). It is marked for agent discoverability now; approval enforcement of adestructiveentry is owned by ADR-0057 (workstream 2.5.E), not here.modeScopelists the chat modes a command appears in (omit ⇒ all). The mode values (ask/plan/accept-edits/auto) are defined in ADR-0057 (2.5.E); 2.5.C ships the field withomit = all.- A
manifest ↔ commanderdrift guard (a unit test) asserts every realcommandercommand has an entry with the same description (command + each option), socommander, theexecuteCommandtable, and--help --jsonstay byte-consistent.
| Example entry | effect |
|---|---|
run |
write |
list |
read |
provider.set-key |
write |
provider.remove-key |
destructive |
The interactive / palette + slash commands inside the Home and chat are a SEPARATE, curated surface (ADR-0056 amendment, 2.5.C) — the runtime registry is apps/cli/src/commands/repl-commands.ts (REPL_COMMANDS), the single source for the palette, the /help list, and the unknown-slash hint. It surfaces only the commands that make sense in a live REPL: lifecycle (/exit, /cancel, /export, /clear), info/discovery (/help, /workflows, /cost, /doctor), and — in a chat — /mode, /effort, /thinking (also Ctrl+T), the ADR-0062 context commands (/compact, /trim), /models, and the ADR-0068 §e copy-and-search hatches /scrollback, /edit + /copy. Their in-chat behavior is spec'd in chat-session.md and its ADRs — the /models live-reseat vs bare-Home write-default and its reasoning-effort sub-step + picker UX (ADR-0059 reseat, ADR-0064/ADR-0063 catalog+write), /effort's per-turn override (ADR-0066), and /thinking's panel toggle (2.5.H) — this reference does not restate them. The heavy, session-starting shell commands (run, chat, provider, …) are never in-REPL slashes — they stay shell-only (relavium <cmd> …). A bare / at an empty prompt opens the filterable palette (the footer hint-bar surfaces / for commands there, 2.5.C S6); an unknown slash — or an undeclared argument on a known command (/exit now) — prints a sanitized, secret-free hint. A command may declare flags (/doctor --deep) or a single positional value (/mode plan); the palette runs the bare form, so a flag/value is opt-in by typing it. There is no separate /shortcuts command — the palette's own nav hints (↑/↓ · Enter · Esc) + the footer keep keys discoverable in context.
Runs a workflow end-to-end. The argument is a path to a .relavium.yaml file (or a workflow id/slug resolvable inside .relavium/).
# interactive
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts
# CI: machine-readable event stream
relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts --json--input k=v(repeatable, one distinct key per input) supplies typed workflow inputs (see theinputsblock in ../contracts/workflow-yaml-spec.md). Each value is coerced to the input's declared type; an unknown key, a repeated key, a missing required input, or a value that does not coerce is an invalid invocation (exit2).--jsonswitches to NDJSON RunEvent output.Ctrl-C(SIGINT) requests a cooperative cancel; the run drains torun:cancelledand exits non-zero (1).- A missing API key for an inline agent's primary provider is caught pre-flight as an invalid invocation (exit
2) naming theRELAVIUM_<PROVIDER>_API_KEYto set, before the run starts. The pre-flight is a strict subset of the keys a run may touch, so it never blocks a valid run: afallback_chainprovider's key (read only if the chain fails over to it) and a$ref-resolved external agent's key (until$refresolution lands, 2.M–2.Q) are conditional and instead surface mid-run as a run failure (exit1). - On a
human_gatenode the run pauses: in interactive mode it prompts inline; in CI mode it exits with the gate-paused code (3, see Exit codes) and can be resumed withrelavium gate. The emittedhuman_gate:pausedevent carries therunId+gateIdneeded for the resume (relavium gate <runId> --gate <gateId>); with--jsonthey are on the NDJSON event line, otherwise the plain/TUI renderer prints them inline (paused at gate <gateId> (<type>), also echoed in the final summary). (relavium status,relavium logs <runId>, andrelavium gate listalso surface pendinggateIds, 2.I.)
Implementation status (as of workstream 2.G).
runis wired to the@relavium/coreengine: path/id resolution,--inputcoercion, the full lifecycle event stream, exit codes0/1/2/3, SIGINT→cancel, and the stable--jsonNDJSON machine contract (stdout = pure RunEvent stream, diagnostics → stderr; see above) are live. The interactiveinkTUI (2.E) renders the live run on a TTY — per-node status + spinners, the active node's streaming tokens, a running cost/duration footer, and a persistent final summary. Under--no-colorit keeps the TUI but suppresses ANSI color; it falls back to the plain line renderer when no TTY is attached orCI=true, and to NDJSON under--json(the three renderers are oneonEventseam over one bus). Provider keys resolve from the OS keychain →RELAVIUM_<PROVIDER>_API_KEYenv var → error (2.C; manage them withrelavium provider), and runs persist to durable history (2.H). The interactive human-gate prompt + out-of-bandrelavium gateresume are live (2.G): on a TTY ahuman_gatenode renders a@clack/promptscard inline (approve / reject + comment / input) and the run continues; under--json/CI/no-TTY there is no prompt and the run exits3, resumable later byrelavium gate <runId>. Built-in tools that need a host capability (filesystem, process, egress) are fail-closed (unavailable) pending a security-reviewed capability workstream.
Lists the workflows discovered under the project .relavium/workflows/, grouped by tag, each annotated with its last-run status from durable history (the latest run per workflow — a SQLite ROW_NUMBER() OVER (PARTITION BY workflow_id …) pick, since SQLite has no DISTINCT ON). --agents lists the agents under .relavium/agents/ instead (agents carry no tags or run history, so they list flat). Disk is the catalog source of truth, distinct from run history — a discovered file that fails to parse is listed and flagged (invalid: <reason>) rather than hidden. Outside a .relavium/ project this is reported clearly and exits 0 (an empty catalog is not a fault).
Under --json, each entry is one NDJSON record — { kind, slug, name, tags, path, valid, error?, lastRun }, where name is null when the file declares none, error is a short, secret-free parse-failure reason present only when valid is false, and lastRun is { runId, status, completedAt } or null for a never-run workflow (and is omitted entirely for agents). See Read-command --json output.
Interactive scaffolder (@clack/prompts) that asks kind (an agent → .agent.yaml, or a minimal single-agent workflow → .relavium.yaml, i.e. an input → agent → output scaffold wrapping one inline agent), then name → provider → model → system prompt → tools (comma-separated, optional). The id is the slugified name; the file lands at .relavium/agents/<id>.agent.yaml or .relavium/workflows/<id>.relavium.yaml.
The answers are assembled into a typed definition and validated against the appropriate @relavium/shared schema before any write — the agent schema for an .agent.yaml, the workflow schema for a .relavium.yaml — so a bad model/provider/name is the same clean exit-2 fault a run would raise (a name with no usable id characters is rejected up front). The id must be unique across both catalogs (see import/export below): a same-kind clash needs --force, a cross-kind clash is always rejected. The wizard needs an interactive terminal on both ends (a TTY stdout to draw the prompt and a TTY stdin to read keys), so it fails loud (exit 2) under --json or a non-TTY pipe; a cancel (Ctrl-C / ESC) writes nothing and exits 0. The result is a plain YAML file ready to commit — pure file I/O, no keychain, no run state.
Both are surface-agnostic git-native YAML operations — they read/write .relavium/ files only, never the keychain or run state. A document's kind is detected from its filename suffix (.agent.yaml / .relavium.yaml), falling back to a content sniff; a file that is neither a valid workflow nor a valid agent is a clean exit-2 fault naming both parse failures.
import <path> [--force]copies an external.relavium.yaml/.agent.yamlinto the project after validating it against the schema. It writes the re-serialized canonical form to.relavium/<workflows|agents>/<slug>.<suffix>. Ids are unique across both catalogs (a bare id resolves project-globally, soexport <id>stays unambiguous): a same-kind slug collision is exit2unless--forceoverwrites it; a cross-kind collision (the id already names the other kind) is exit2always — rename one, since--forcewould leave both files in place.export <id> [--out <path>] [--force]resolves<id>across both catalogs (an id naming neither is exit2; an id that is both a workflow and an agent is exit2, "rename one"), re-validates it, and writes a portable copy. The default destination is./<id>.<suffix>in the cwd (a copy to share, outside the catalog);--outoverrides it. Under--jsonit emits a single{ id, kind, path }record, wherepathis cwd-relative (the same shapeimport --jsonemits — no absolute filesystem path is printed in either mode).
Why it is safe to share (see ../desktop/keychain-and-secrets.md): a provider API key is never in the file by construction — there is no schema field that holds a key value (keys live in the OS keychain, referenced by account id, and are resolved only at run time). MCP-server secrets are referenced via {{secrets.*}} placeholders by convention — the env map accepts arbitrary strings, so export/import re-serialize faithfully (they preserve whatever is authored; they do not scrub): author secrets as {{secrets.*}}, never inline a literal. The re-serialize from the validated AST also drops all free-form comments (where a stray secret might otherwise hide), and the exported file re-imports cleanly. This is the "workflow file is the invite" distribution mechanism.
Replays a past run's persisted run_events in seq order (the same data the desktop run-detail drawer replays) — a terse line per event in human mode. Under --json it emits each raw RunEvent as one NDJSON line — the same RunEvent data relavium run --json streamed (this is the "raw RunEvent JSON" the run-detail replay consumes — no separate --raw flag). For a run paused at a gate, the human_gate:paused event surfaces the gateId to copy into relavium gate <runId> --gate <gateId>. An unknown runId is an invalid invocation (exit 2).
Shows the currently active/paused runs (from runs + step_executions) and each one's per-node status. Useful while a long workflow runs in another terminal or was launched detached. For any run paused at a human gate it also prints the pending gateId(s) (with gate type and node id), so a CI author can pass the right one to relavium gate <runId> --gate <gateId> — required when a run has more than one gate pending at once. It takes no argument (it lists every active run; a terminal run is not shown — inspect one with relavium logs <runId>). Under --json each active run is one NDJSON record — { runId, workflowId, status, startedAt, steps, pendingGates }, where each steps entry is { nodeId, nodeType, status, attemptNumber, startedAt, completedAt, durationMs, costMicrocents } and each pendingGates entry is { gateId, nodeId, gateType, message, expiresAt? } (the same pending-gate shape gate list emits).
The live model catalog (2.5.G, ADR-0064). The catalog is a local cache in history.db (model_catalog) that records which model ids each connected provider key can currently reach; the static registry (pricing.ts) stays the pricing authority, so the cache holds no price and no API key.
relavium models # list the cached catalog
relavium models refresh # force a live re-fetch of every connected provider
relavium models pricing my-custom-model --provider openai --input 3 --output 9 # hand-enter a pricerelavium models(no subcommand) lists the cached catalog (read-only). On the very first run — when the cache is empty — it does one minimal blocking refresh, then lists; an empty result stays a clean exit0(an empty catalog is not a fault, likerelavium list). Human output is one line per model (<modelId> <provider> ctx=<n> [<source>]).relavium models refreshforces a live re-fetch of each connected provider (a provider whose key resolves via the OS keychain →RELAVIUM_<PROVIDER>_API_KEYenv var) and prints a per-provider outcome. The refresh is per-provider isolated: one provider's failure (bad key, network, endpoint drift) or a provider without a list endpoint never fails the whole command — that provider is reportedfailed/skippedand the others still refresh. A per-provider failure is therefore not a command failure (exit0with the report). The one hard fault is an explicitrefreshwith zero providers connected (no key at all): that is a clean exit2naming how to add a key, because nothing could be fetched.relavium models pricing <model> --provider <slug> --input <usd> --output <usd> [--cached <usd>]hand-enters the per-million-token price of a model the static registry does not know — a custom-endpoint model, or a new provider model not yet in the shipped pricing.ts (2.5.G S10, ADR-0065 §1–2). Prices are USD per million tokens (--inputprompt,--outputcompletion,--cachedcache-read; stored as integer micro-cents,usd × 1e8, never a float). The row is written assource='user'and a livemodels refreshnever clobbers it. This closes the cost-cap gap (ADR-0064 §6): before, an unknown model had no price, sobudget.max_cost_microcents/[chat].max_cost_microcentsdegraded to allow for it; once user-priced, the cap is enforced (pre-egress and realized) onrun, arunresumed viarelavium gate,chat/chat-resume(incl. a/clearrebuild, re-read fresh), the Home chat, and one-shotagent run. Guards (each a clean exit2, nothing written): a canonical model id is refused (the shipped price always wins, so an override would be silently ignored); an unregistered provider is refused (register it first withrelavium provider add); a negative / non-finite / implausibly-large price is refused; and the same model id already user-priced under a different provider is refused (the overlay keys by model id, so a second provider's price could not be distinguished — use a distinct id or re-price under that provider). The static registry still wins for a known id, so a user can never misprice a shipped model.- Security. A provider key is read only to make the live request (over the bounded, abortable, secret-free
listModelsseam) and is never logged, persisted (the cache holds no key), or placed in the report /--jsonpayload / any error message. A failing provider surfaces only the seam's already-redacted message (or a genericrefresh failed), never a raw cause.models pricingwrites only a model id + provider + integer prices — no key, ever. --json(ADR-0049) emits one NDJSON record per line, stdout-pure, key-free:relavium models --json— one record per model:{ provider, modelId, displayName, contextWindowTokens, maxOutputTokens, source, lastRefreshedAt, deprecationDate }(nullfor an absent optional;source∈static | live | user;lastRefreshedAtis epoch-ms).relavium models refresh --json— one record per provider:{ provider, status, added, updated, deactivated, error }, wherestatus∈refreshed | skipped-no-key | skipped-unsupported | failed, the three counts are the model ids added / refreshed-in-place / soft-deactivated (nullunlessstatusisrefreshed), anderroris a short, secret-free reason (nullunlessfailed).relavium models pricing --json— one record:{ model, provider, source, inputCostPerMtokMicrocents, outputCostPerMtokMicrocents, cachedInputCostPerMtokMicrocents }(the stored integer micro-cents;sourceis alwaysuser).
The non-streaming read commands (list / status / gate list / chat-list / models / provider list, and logs) keep the CLI to one machine-output idiom: --json emits one result record per line (NDJSON, jq-friendly, stdout-pure with diagnostics on stderr) — the same line-oriented shape relavium run --json uses for its RunEvent stream (ADR-0049). For logs --json the records ARE raw RunEvents — the same RunEvent data the run streamed (re-serialized from the persisted log, so the field order may differ from the live run --json bytes); for the others they are the per-command result records documented above. An unknown runId (logs / gate list) is the structured pre-run fault on stderr with exit 2, stdout empty — exactly as for run. (chat-export --json is not a read command — it emits a single session:exported event, not a result record, since the export is a session-lifecycle action.)
Resolves a pending human gate from the terminal — the surface-agnostic resume path for human_gate:paused:
relavium gate <runId> --approve
relavium gate <runId> --reject --comment "Too risky"
relavium gate <runId> --input '{"region": "us-east-1"}' # for gate_type=input
relavium gate <runId> --gate <gateId> --approve # disambiguate when >1 gate is pending- Exactly one of
--approve/--reject/--inputis required and they are mutually exclusive;--comment <text>annotates an approve/reject rationale and is invalid with--input(which carries the payload). A bad combination is an invalid invocation (exit2). --input <value>is parsed as JSON when it parses ('{"k":1}'/'42'/'true'→ a structured payload), else kept as the raw string (--input some-token→"some-token"); the result becomes the gate node's output. (The interactive prompt for agate_type=inputgate takes the typed value as a raw string.)- Do not pass secrets via
--input. The value reaches the durable event log (human_gate:resumed.payload) and the--jsonstream, and argv itself leaks intops/ shell history / CI logs — exactly the exposurerelavium provider set-key's stdin-only rule avoids. Use a non-secret gate input; supply secrets through the OS keychain / env (RELAVIUM_<PROVIDER>_API_KEY), never a gate payload. --gate <gateId>selects which pending gate to resolve. The resume contract isengine.resume(runId, gateId, decision)—gateIdis mandatory on the resume path (it is carried on thehuman_gate:pausedevent; see sse-event-schema.md andresume_runin ipc-contract.md).--gateis optional on the CLI: when exactly one gate is pending the CLI fills it in automatically; when more than one gate is pending it is required, and omitting it is an invalid invocation (exit2) listing the pendinggateIds.- Read the pending
runId+gateIdfrom the run's own output: thehuman_gate:pausedevent line under--json, or thepaused at gate <gateId> (<type>)line the plain/TUI renderer prints.relavium gate list,relavium status, andrelavium logs <runId>(2.I) also surface them out-of-band. - Idempotent. A doubled decision — the run already finished, or the named gate was already resolved — is a clean exit-
0no-op, never a double-advance (it leans on the engine's checkpoint/gate-state idempotency). An unknownrunIdis exit2. Idempotency is per gate, though: on a sequential multi-gate workflow a blind repeat without--gate(after the first decision advanced the run and it re-paused at the next gate) auto-fills and resolves that gate — so an automated retry-until-exit-0loop should pin--gate <gateId>to avoid resolving later gates unattended.
Implementation status (2.G).
relavium gateruns in a fresh process from the originalrun: it reloads the run's frozenWorkflowDefinition+ inputs from the durable history snapshot (2.H), reconstructs the paused checkpoint from the persisted event log, and callsengine.resumeFromCheckpointover the same store — then drives the resumed run to its terminal (exit0complete /1failed /3paused again at a later gate). The recordeddecidedByis the constantcli(a deterministic, non-PII marker; the desktop/portal supply a real user id). Budget-cap pauses (budget:paused, ADR-0028) are not resolved here — that is the separaterelavium budget resumesurface (deferred-tasks). A run that declares asecret-typed input cannot be resumed cross-process: secrets are never persisted in plaintext (only a masked placeholder is, ADR-0006/0036), sorelavium gatefails closed (exit2) rather than resume with a value it cannot restore — re-run the workflow instead (re-providing secret inputs on resume is a tracked follow-up). Therelavium gate listmulti-gate listing is live (2.I).
Lists the pending human gates so an operator can pick the gateId to resolve — the multi-gate discovery surface the gate command's --gate requirement points at.
relavium gate list # every paused run's pending human gates
relavium gate list <runId> # just one run's- With no argument it scans every paused run; with a
<runId>it lists just that run's pending gates (an unknownrunIdexits2). Budget-cap pauses (budget:paused) are excluded — those are the separaterelavium budget resumesurface (ADR-0028). - It rests on the same persisted-event reconstruction the
gateresume path uses, so the listing and the resume can never disagree on what is pending. - Human output is one line per gate (
<runId> <gateId> <gateType> node=<nodeId> "<message>"); under--jsoneach pending gate is one NDJSON record —{ runId, gateId, nodeId, gateType, message, expiresAt? }(see Read-command--jsonoutput).
Lists past agent sessions from durable history.db, most-recently-updated first — the session counterpart of relavium list. Human output is one line per session (<id> <agentSlug> [<status>] <updatedAt> "<title>"); an empty history is reported clearly (exit 0). Under --json each session is one NDJSON record — { sessionId, agentSlug, title, status, modelId, createdAt, updatedAt, totalCostMicrocents }, where title / modelId are null when absent (see Read-command --json output). Soft-deleted sessions are excluded.
Exports a persisted session to a .relavium.yaml scaffold for review before commit (ADR-0026) — the same contract the in-REPL /export drives. Writes <sessionId>.relavium.yaml in cwd by default (the file name is keyed on the unique session id, so two sessions never collide); --out <path> overrides, --force overwrites an existing target. The session row is marked exported with the written path. Under --json it emits a single session:exported event ({ type, sessionId, timestamp, sequenceNumber, workflowPath }). An unknown sessionId or an existing target without --force exits 2; success is exit 0.
Runs a single agent one-shot (non-interactive) on the same AgentSession infra as relavium chat — a session with one turn, then exit. The agent-first headline as a scriptable, CI-friendly primitive.
echo "summarize ./README.md" | relavium agent run code-reviewer
echo "review it" | relavium agent run ./agents/coder.agent.yaml --json
echo "review it" | relavium agent run code-reviewer --fixture ./fixtures/review.cassette.json --json- The
<agent>argument is required — a.agent.yamlpath or a.relavium/-discoverable agent id (resolved by the same strict parserrelavium chat --agentuses). An unknown agent is an invalid invocation (exit2). - The prompt is read from stdin (the
echo … | relavium agent runidiom); an empty stdin is an invalid invocation (exit2). --input k=vis reserved — currently rejected (exit2): a session does not yet interpolate{{ctx.*}}into the agent's prompt (the engine passessystem_promptverbatim), so the flag is failed loud rather than exposed as an inert no-op. It re-opens when session prompt interpolation lands (a tracked engine follow-up, deferred-tasks.md).--fixture <path>replays a recorded LLM cassette so the run is deterministic and fully offline (no key, no network, no keychain) — the format is documented in agent-run-fixture.md. A malformed cassette exits2.--jsonemits theSessionEventNDJSON stream on stdout (the same shapechat --jsonproduces); otherwise the assistant reply streams in human form.- Not persisted — a stateless invoke (no
history.dbrow), unlike the REPL. The exit code is the turn's outcome:0on success,1on a turn error; an invocation fault is2. It is never4(that is the interactive REPL's session-ended code).
Registers LLM providers and manages their API keys in the OS keychain (workstream 2.C; @napi-rs/keyring,
ADR-0019). The key value never leaves the keychain:
the llm_providers row stores only the keychain account ref, display shows only a hint (last 4 chars), and a
key is read solely at LLM-call time. Known providers: anthropic, openai, gemini, deepseek.
relavium provider list # registered providers + whether a key is set
relavium provider add anthropic # register a provider (its default base URL + pricing page)
relavium provider add openai --pricing-url https://example.com/prices # override the pricing reference page
echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic # store a key (read from STDIN, never argv)
relavium provider test anthropic # verify the key with a minimal live request
relavium provider remove-key anthropic # delete the key from the keychain
relavium provider list --verify # + a live key-verification probe per providerset-keyreads the key from stdin, never a CLI argument (argv leaks intops, shell history, and CI logs); pipe it or use a heredoc. The key is stored in the OS keychain under the canonical entry-naming scheme (keychain-and-secrets.md).listshows each registered provider, its base URL, and whether a key is set — a fast, offline read (no key is read; the status is derived from the stored keychain ref).--verify(2.5.G S11, ADR-0065 §6) additionally runs a bounded, key-redacted live probe per provider — the SAMEvalidateProviderKeyseamprovider test+/doctor --deepuse — and reportsverified/failed — <redacted reason>/no keyin the status column. A provider with no resolvable key (keychain → env both empty) is reportedno keyand never probed (so--verifynever hangs on a keyless provider); the key is never echoed.listhonors--json(ADR-0049): one key-free NDJSON record per provider —{ name, baseUrl, keySet, verified, verifyDetail }, whereverifiedisnullwithout--verify(elsetrue/false) andverifyDetailis a short reason — a redacted failure message, or"no key"when a probed provider has no resolvable key, elsenull(so averified: nullrecord withverifyDetail: "no key"is a probed-keyless provider, distinct from the un-probedverifyDetail: null). The--verifyprobes run concurrently (each timeout-bounded), so verifying N providers costs one timeout, not N.add/set-keyauto-register the provider row.--base-url <url>onaddrecords a custom endpoint that is now actually used at request routing (2.5.G S9, ADR-0065 §3–4 — the earlier "dead-config" gap is closed): the resolver rebinds that provider's adapter to the custom endpoint and routes all its egress (streaminggenerate/stream+ themodels.listrefresh) through the shared SSRF-validated hop (connectValidated— HTTPS-only, no embedded credentials, every resolved IP range-blocked, connect pinned to the validated IP for DNS-rebinding safety). Custom endpoints are OpenAI-compatible only this round (openai/deepseek); a--base-urlonanthropic/geminiis refused with a clear message (exit2), as is a non-HTTPS / private-loopback / credential-bearing URL, or one carrying terminal-control / bidirectional characters (fail-fast atadd, so the stored value is terminal-safe on every surface). The provider-id set stays closed — a custom endpoint reuses theopenai/deepseekid (ADR-0065 §6).--pricing-url <url>onaddoverrides the seededpricing_reference_url— the public pricing page where you find a model's price to hand-enter viarelavium models pricing(2.5.G S10, ADR-0065 §1). Each known provider is seeded with its default pricing page; theaddconfirmation echoes it. It is a display-only pointer, never fetched (not an egress target), so — unlike--base-url— it needs no SSRF gate; it is validated as an HTTPS URL with no embedded credentials and stored normalized (control bytes percent-encoded, so it is terminal-safe). Omitting the flag on a re-addpreserves a previously-set custom pointer.testdoes a 1-tokengeneratethrough@relavium/llm;--model <id>overrides the cheap default. A bad key fails cleanly (exit2) without echoing the key.- Key resolution (used by
run+test): OS keychain →RELAVIUM_<PROVIDER>_API_KEYenv var → error. The env var is the headless/CI source; thesecrets.encencrypted-file fallback is deferred past v1.0 (keychain-and-secrets.md). An unavailable keychain (locked / no Linux Secret Service) surfaces a clean error — never a silent plaintext fallback.
CI relies on deterministic exit codes:
| Code | Meaning |
|---|---|
0 |
Workflow completed successfully |
1 |
Workflow failed (a node errored and exhausted retries/fallbacks) |
2 |
Invalid invocation (bad arguments, workflow not found, schema validation error) |
3 |
Run paused at a human gate (CI/non-interactive mode) — resume with relavium gate |
4 |
A chat session ended — via /exit, /cancel (or Ctrl-C in TTY mode), or an input-stream EOF — a user-initiated end of a relavium chat REPL — see chat-session.md |
Exit code
3lets CI distinguish a pause-for-approval (arun:pausedevent — the run's aggregate suspension, a human/approval/budget gate — in non-interactive mode) from a hard failure. This is the canonical home for the gate-paused code; other docs reference it as3.Under
--json, a pre-run fault (exit2) writes its structured{ "type": "error", … }detail to stderr while stdout stays empty (ADR-0049) — the exit code is the primary fault signal; read stderr for the detail.Exit code
4is the canonical chat-session-ended code: it marks a deliberate/exit(or its--jsonequivalent, a finalsession:cancelled/end event) from therelavium chatREPL, kept distinct from a successful workflow run (0) and a hard failure (1) so a wrapper script can tell "the user quit the chat" apart from either. Other docs reference it as4.The bare-invocation interactive Home (2.5.B, home.md) is a long-lived mode whose clean exit is
0(Ctrl-C / Ctrl-D on an empty prompt). A chat launched from inside the Home has its own exit code4, which the Home loop consumes — a chat ending returns to the Home, never leaked. An external signal to the Home runs teardown then exits the conventional128+signo(130SIGINT /143SIGTERM) so a pipeline still detects the interruption.
The CLI is designed to run inside pipelines. A typical pattern: install globally, provide the API key via the OS keychain or an environment variable, and run with --json for parseable output.
# illustrative CI step
- run: npm install -g relavium
- run: relavium run .relavium/code-review.relavium.yaml --input file=src/index.ts --jsonFor a complete walkthrough (key handling, gates, artifacts, exit-code checks), see run-a-workflow-in-ci.md.
In Phase 2,
relaviumgains cloud-mode commands (e.g.relavium auth loginvia OAuth Device Flow, and switching execution to the cloud). The engine interface is identical in both modes — the CLI requires no code changes to target cloud execution. See ../portal/api-reference.md and ../../architecture/cloud-phase-2.md.