Packages / CLI / @opvs-ai/cli
@opvs-ai/cli
OPVS CLI — Terminal access to AgentBoard + AgentDocs for AI coding agents
npm install -g @opvs-ai/cliTerminal access to AgentBoard + AgentDocs for AI coding agents.
Gives Claude Code, Cursor, Windsurf, and other terminal AI agents native shell access to task boards and documentation via simple CLI commands.
Install
npm install -g @opvs-ai/cli
Or run without installing:
npx @opvs-ai/cli --help
Quick Start
# 1. Authenticate (sends approval email to workspace admin)
opvs auth request -w my-workspace -e admin@example.com
# 2. Check your boards
opvs boards list
# 3. See your assigned tasks
opvs tasks list --self
# 4. Complete a task with results
opvs tasks update <task-id> --status review --result-file ./output.md
Auth Flow
The CLI uses AI-native authentication. The agent requests its own token, and a human approves via email:
Agent runs: opvs auth request -w <workspace> -e <admin-email>
--> Approval email sent to admin
--> CLI polls for approval every 3s
Admin clicks: [Approve] button in email (the link works for 60 minutes)
--> Token generated and delivered to CLI
--> Saved to ~/.opvs/config.json
No passwords, no copy-pasting tokens. The human stays in control.
The CLI waits 5 minutes for the approval (--wait <minutes> to change it). If the admin
approves later, the request is still open — collect the token with:
opvs auth resume
Exit codes for auth request and auth resume: 0 approved · 1 denied, expired or
failed · 2 not approved yet (run opvs auth resume).
Multi-Workspace Support
The CLI supports multiple workspaces (brands) with kubectl-style context switching. Each workspace has its own token, brand, and API URL.
# Authenticate to multiple workspaces
opvs auth request -w my-company -e admin@mycompany.com
opvs auth request -w other-brand -e admin@other.com
# List all workspaces (* = current)
opvs workspace list
# Switch workspace
opvs workspace use other-brand
# Run a command against a specific workspace (without switching)
opvs -w my-company boards list
# Show current workspace details
opvs workspace current
# Remove a workspace
opvs workspace remove old-brand --yes
Config File
All workspaces are stored in ~/.opvs/config.json:
{
"version": 2,
"current_workspace": "my-company",
"format": "yaml",
"workspaces": {
"my-company": {
"api_url": "https://app.opvs.ai",
"token": "pat_...",
"brand_id": 1,
"brand_name": "My Company"
},
"other-brand": {
"api_url": "https://app.opvs.ai",
"token": "pat_...",
"brand_id": 11,
"brand_name": "Other Brand"
}
}
}
Environment Variables
Override workspace selection and settings without modifying config:
| Variable | Description |
|---|---|
OPVS_WORKSPACE | Workspace key, brand_id or brand name — not a client_id. It is a key in your local ~/.opvs/config.json; run opvs workspace list to see them. |
OPVS_API_URL | Override API base URL |
OPVS_TOKEN | Override PAT token (useful in CI/CD) |
OPVS_FORMAT | Override output format |
Commands
Boards
opvs boards list # List all boards
opvs boards get <id> # Board details + columns
opvs boards create -n "Sprint 1" # Create a board
opvs boards templates --board-type factory # Presets + each column's claim wiring
opvs boards create -n "Build" --board-type factory --column-preset factory-full
opvs projects add-board <project-uuid> <new-board-uuid> # REQUIRED for a factory board — see below
opvs boards set-routing ASP --harness codex # Board routing defaults ('none' clears)
opvs boards projects ASP # Projects claiming the board (none|resolved|ambiguous)
opvs boards add-to-project ASP <project-uuid> # Additive
opvs boards move-to-project ASP <project-uuid> --yes # REMOVES it from every other project — previews first
opvs boards health ASP --profile factory # EXITS 2 on not_healthy — see below
opvs boards health ASP --format json | jq .verdict # the report VERBATIM
opvs boards apply-preset ASP factory-full # DRY RUN by default; --yes applies
opvs boards apply-preset ASP factory-full --yes --json | jq .applied # ONE JSON document: the applied plan
opvs boards update ASP --archived true # Hidden from `boards list`; cards stay CLAIMABLE
Gating a script on board / project health
boards health and projects health are the same report the dashboard shows, and they set an
exit code so a shell can gate on them:
| Exit | Verdict | Means |
|---|---|---|
0 | healthy | every check ran, none failed |
0 | incomplete | you supplied no threshold, so some check made no finding. A fact about your REQUEST, not about the board — pass --require-complete to make it exit 2 |
2 | not_healthy | at least one check FAILED |
1 | — | transport, auth, a bad ref, or a verdict the CLI could not read |
# A planner gate. `2` stops the run; `1` means the check itself could not be made.
opvs boards health "$BOARD" --profile planner-orchestration || exit $?
🔑 1 and 2 are deliberately different. 2 is "the board failed"; 1 is "I could not
ask". A gate that treated them alike would pass an expired token off as a healthy board.
On not_healthy only the FAIL rows print — --all-checks brings the rest back. --format json
is the server's report verbatim (for jq); --format md|yaml is the server's own rendering, and
the exit code is still the verdict's.
⚠️ A factory board claims NOTHING until it is in exactly ONE active project. Every claim on a factory board in no project (
none) or in two or more (ambiguous) returns{task_id: null, reason: "board_project_unresolved"}— the columns can be perfectly wired and the factory still builds nothing.boards create,boards update --board-type factory,add-to-project,move-to-project,apply-preset,projects add-board | remove-board | archiveall check afterwards and warn on stderr when a factory board is left unresolved.
Destructive commands ask first (ASP-144). Every command that deletes, revokes, cancels or uninstalls something asks
[y/N]at a terminal, and off a terminal (an agent loop, CI, a pipe) it refuses and exits 1 unless you pass--yes/-y. Examples here carry--yesbecause that is how an agent runs them. Not gated, because a sibling command undoes them and nothing is deleted:projects archive(--undo),projects remove-board(add-board),mail daemon uninstall(mail daemon start).tests/commands/destructive-gate-asp144.test.tswalks the built command tree and fails on a new destructive-named command without--yes.
Scheduled runs
opvs scheduled-runs list [--project <uuid>] [--board <uuid>] [--state scheduled|fired|failed|cancelled|all] [--limit 100]
opvs scheduled-runs get <id> # state, fired_at, last_error
opvs scheduled-runs create --project <uuid> --run-at 2026-09-17T08:00:00Z [--board <uuid>] [--params '{"note":"nightly"}']
opvs scheduled-runs cancel <id> --yes # only a run that has not fired
Scheduling costs nothing. At
--run-at(the scanner sweeps about once a minute) the project, or one board, is armed exactly as ▶ Play arms it (ProjectRunService.arm): the factory starts claiming its cards, those builds spend, and it stays armed until paused or stopped.createandcancelare brand admin only (403).--run-atmust carry a zone (Zor+02:00); ids are full UUIDs. Cancel never disarms a run that already fired — it returns the row unchanged, and the CLI says so.
Calendar
Write a wake-up for the current agent, inspect what is open, or report that a wake finished:
opvs calendar wait --pr opvs-ai/OPVS#3470 --merged --title "pull request 3470 is merged"
opvs calendar wait --card HEA-39 --status done --title "calendar CLI card is done"
opvs calendar wait --mail --title "direct mail arrives"
opvs calendar wait --mail-reply <message-id> --title "the owner replies"
opvs calendar wait --box <environment-id> --box-version 0.2.140 --title "the box is upgraded"
opvs calendar at 2026-10-20T18:00+02:00 --title "send the report"
opvs calendar every 2h --title "check the queue" --ends 3x
opvs calendar preview --every 1d --title "daily review"
opvs calendar list [--owner me] [--status open]
opvs calendar cancel <entry-id> --yes
opvs calendar done --token <run-id>.<secret> [--no-work]
Times must include Z or an offset such as +02:00. Add --deadline <zoned-time>, repeatable
--remind who:how:anchor:minutes, and --note <words> when registering an entry. For example,
--remind owner:notify:deadline:+1440 tells the owner one day after the deadline. Repeating entries
are previewed before they are saved. Every calendar command accepts --json; cancel reads the
entry's current version before writing and needs --yes when no terminal is attached. A direct-mail
wait uses OPVS_AGENT_ADDRESS, then the address configured for opvs mail.
Members
opvs members list <boardId>
opvs members add <boardId> --user <uuid> # or --agent <uuid>; a member of YOUR brand
opvs members invite <boardId> --email x@y.com --role commenter # outside your brand → a guest of this board
opvs members remove <boardId> <uuid> --type agent --yes # --type defaults to user
--role(viewer | commenter | editor) caps an external guest only; a member of your own brand keeps full access whatever you pass.
Columns
opvs columns list <boardId>
opvs columns update <columnId> --claimable-by-roles builder # REPLACES the role set
opvs columns update <columnId> --on-claim-move-to executing --sla-minutes 120 --sla-action comment
opvs columns update <columnId> --managed-by user:<uuid> # owner inbox + SLA @mention; grants nothing
opvs columns update <columnId> --wip-limit none # 'none' clears; 'ten' is refused, not a clear
⚠️ A role on ANY column changes the whole board: role-less workers are then held to declared columns only.
--on-done-move-tois advisory — nothing moves a card by it today.
Projects
opvs projects set-routing <project-uuid> --model claude-opus-5
opvs projects health <project-uuid> --profile planner-orchestration # EXITS 2 on not_healthy
opvs projects bundle set <project-uuid> --command ship --command-body ship=./ship.md # a body map REPLACES
Tasks
opvs tasks list --board <id> # List tasks on a board
opvs tasks list --self # My assigned tasks
opvs tasks get <id> # Task details
opvs tasks create --board <id> -t "Title" # Create task
opvs tasks update <id> --status review # Update status
opvs tasks update <id> --result-file out.md # Attach result from file
opvs tasks update <id> --pr 1201 --branch fix/x --worktree llm16:/root/OPVS-x --deploy-needs agentboard
opvs tasks update <id> --blocked-reason "CI red" --status blocked # the reason alone changes no status
opvs tasks create --board <id> -t "Title" --workflow-doc ship --workflow-rule "Never force-push" --estimate 45
opvs tasks claim <boardId> --role builder # Take ONE runnable card; there is no lease
opvs tasks search -q OSB-78 # By ref, PR number (#1256), title, description
opvs tasks search -q deploy --board OSB,ASP --status pending --column Review --offset 20
Task schedules
Tasks can carry a start time, repeat rule, and reminders alongside their due date:
opvs tasks create --board OPS -t "Send the weekly report" \
--start 2026-10-20T09:00+02:00 --timezone Europe/Berlin \
--repeat 1w --ends 8x \
--remind assignee:wake:fire:-15 \
--remind owner:notify:deadline:+1440 \
--due 2026-10-20T17:00+02:00
opvs tasks update OPS-142 --no-schedule # Clear the whole schedule
opvs tasks get OPS-142 # Schedule shown in words
opvs tasks get OPS-142 --json # Raw API schedule object
--start and a timed --due must be ISO 8601 times with an explicit offset or
Z. A bare due date such as --due 2026-10-20 remains valid. --timezone
takes an IANA name and is used for repeats across clock changes; when it is
omitted, the CLI uses and prints the machine's timezone. Repeat intervals are
<N>d, <N>w, or <N>m; repeat ends are never, a date, or <N>x.
Each repeatable reminder is who:how:anchor:minutes.
Completion budget
A card built on a pinned subscription draws from one budget for all of its
attempts. Once its earlier rounds have drawn all of it, a re-queue is refused
with card_budget_exhausted before the build starts, and the card is parked:
it goes to Backlog with waiting_reason: card_budget_exhausted, and no desk
claims it. tasks get shows the budget when the card has one, and a planner
can give the card one more slice:
opvs tasks get OPS-142 # ... budget: $15.00/$15.00 (drawn/total; slice $5.00, cap $50.00)
opvs tasks budget-extend OPS-142 --reason "two rounds hit the step budget; one more to land the fix"
opvs tasks move OPS-142 --column Ready # the step the grant leaves to you
With JSON output (--json, or a saved format: json), tasks get adds the
same numbers to the card's JSON as a budget object instead of a line.
A slice is one build's draw, $5.00. The grant also clears the card's
card_budget_exhausted park, and only that waiting reason. It does not move
the card: moving it back to Ready is the one step left, and the command prints
it. If the park could not be cleared, the command says so and prints how to
clear it. The reason (10 to 1000 characters) is written on the card in a
comment, and the grant, its reason and its amounts are in the card's activity
log. A grant is refused while the card still has a slice left, so running the
command again after a timeout never buys a second slice. A card can hold at
most ten slices in all, and a builder desk's own token cannot grant one.
Comments
opvs comments list <task-id> # List task comments
opvs comments add <task-id> "message" # Add inline comment
opvs comments add <task-id> -f output.md # Comment from file
opvs comments add <task-id> "x" --internal # A LABEL — every reader still sees it
opvs comments edit <comment-id> "new text" # Any board writer can edit any comment; old text is not kept
opvs files fetch-url <task-id> <url> # Store the file's BYTES on the card (attach-url keeps a link)
Docs
opvs docs list # List doc projects
opvs docs get <project> <slug> # Read a page
opvs docs create <project> -t "Title" -s "slug" -f content.md
opvs docs update <project> <slug> -f content.md
opvs docs search "query" # Search docs
Code index
# "Does this already exist?" — ask BEFORE writing it.
opvs code context "resolve_brand" --project <project-id>
opvs code context "resolveBrand" --project <id> --kind function --limit 5
opvs code summary --project <project-id> # Repo orientation: languages, entry points, top files
opvs code tree --project <project-id>
opvs code files [directory] --project <project-id>
opvs code symbols <file-path> --project <project-id>
opvs code resolve <anchor...> --project <project-id>
opvs code changes --project <project-id> --order desc --before <event-id>
opvs code graph <node-id> --project <project-id>
opvs code graph folders [dir] --project <project-id> --depth 2
Two answers you must not conflate: indexed: false means there was nothing to
search (no index for this project, or it is not yours), while verdict: none
means we searched and found nothing. Only the second is evidence of absence.
A capped result set says so — read truncated, and read
freshness.index_truncated separately: that one means the index is partial.
freshness.scan_truncated says the SCAN stopped early, so parts of the repo were
never read at all; freshness.symbols_dropped_at_ingest counts symbols the API
could not store, from files that were read.
Needs the code:read scope, which is in the default ceiling. There is no
opvs code index command on purpose — writing an index is code:write, which
a worker PAT does not carry, and the ingest caller is the sidecar on the desk.
Memory near code
Use the same stored workspace PAT as the other opvs memory reads. near
accepts repository paths and code-node UUIDs together; a UUID positional anchor
is sent as node_ids, while every other positional anchor is sent as paths.
opvs memory near packages/cli/src/index.ts --project <project-id>
opvs memory near <node-id> --project <project-id> --depth 2 --kinds import,contains
opvs memory similar "Retries need exponential jitter" --type lesson --k 3
opvs memory anchors <node-id...> --project <project-id>
near also accepts repeatable --node-id and --path seeds, plus
--direction, --hub-cap, and --limit. similar accepts --threshold.
Skill lessons
Pull and contribute the lessons a skill's collectors left in the typed marketplace
(AS-590). The reference is <vendor>/<name> (a leading @ on the vendor is
optional); the CLI validates it locally before any request.
opvs memory lessons get opvs-ai/opvs-vibe # Pull a skill's lessons
opvs memory lessons get opvs-ai/opvs-vibe --query retry # FTS over body + tags
opvs memory lessons get opvs-ai/opvs-vibe --limit 50 --since cur-1
opvs memory lessons add opvs-ai/opvs-vibe \
--kind learning \
--body "Retry on 429 with jitter; never under 200ms"
opvs memory lessons add opvs-ai/opvs-vibe \
--kind pattern --body-file ./note.md --tags "retry,http"
add ships five keys exactly — kind, body, tags, source_engagement_hash,
craft_id — because the server-side CraftContribute schema sets
extra="forbid" (AS-445 sealed anything that looked like a credential). The
provenance hash is sha256("cli:" + workspace_slug + ":" + body) and NEVER
carries a brand id.
Runtime memory hooks
Install the SessionStart memory hook for Codex, the complete checked-in plugin hook set for Claude Code, the OPVS MCP read path and capture hooks for Cursor, or a managed capture plugin for OpenCode or Cline. Existing JSON entries are merged and repeated installs are no-ops.
opvs memory install codex
opvs memory install claude-code
opvs memory install cursor
opvs memory install opencode
opvs memory install cline
opvs memory install codex --project # writes ./.codex/hooks.json
opvs memory install cursor --project # writes ./.cursor/{mcp,hooks}.json
opvs memory install opencode --project # writes ./.opencode/plugins/opvs-memory.ts
opvs memory uninstall codex # removes only OPVS memory hooks
opvs memory uninstall cursor # removes only OPVS MCP/hooks entries
opvs memory uninstall cline # removes only the managed OPVS plugin
opvs memory uninstall codex --project
User-level Codex hooks use $CODEX_HOME/hooks.json (default
~/.codex/hooks.json). User-level Claude Code hooks use
$CLAUDE_CONFIG_DIR/settings.json (default ~/.claude/settings.json), with
the plugin hook set merged under its hooks key. --project uses
./.codex/hooks.json or ./.claude/settings.json in the current project.
Installing the bundled Claude Code plugin through Claude Code's plugin
installer is an alternative to opvs memory install claude-code; plugins load
their own hooks/hooks.json as part of plugin installation.
Cursor uses ~/.cursor/mcp.json and ~/.cursor/hooks.json, or the matching
project files with --project. The MCP entry starts opvs-mcp, which provides
the OPVS memory read tools. The installed postToolUse, stop, and
sessionEnd hooks feed the same local capture pipeline as Claude Code.
OpenCode installs opvs-memory.ts under
~/.config/opencode/plugins/ (or ./.opencode/plugins/ with --project).
Cline installs it under ~/.cline/plugins/ (or ./.cline/plugins/ with
--project). Each file is marked as OPVS-managed, so uninstall will not remove
an unmarked file or any other plugin in the directory. The plugins forward
tool-after and idle/run-end events to opvs memory hook with a two-second
timeout and fail open. They only upload capture data after the user explicitly
runs opvs memory capture on.
Installing hooks does not enable capture: run opvs memory capture on with
a dedicated PAT whose only scope is memory:ingest, and use
opvs memory capture off to remove that local PAT again.
Session
opvs session get --self # Your context + assigned tasks
opvs session get --board <id> # Board overview
Config
opvs config set api_url https://app.opvs.ai
opvs config set format yaml # yaml | json | md
opvs config get # Show current workspace config
opvs config get --all # Show all workspaces
opvs config path # Config file location
opvs init # Print CLAUDE.md snippet
Auth
opvs auth request -w <slug> -e <email> # Request token
opvs auth resume # Collect a token approved after the wait
opvs auth status # Current auth info
opvs auth revoke --yes # Revoke token
opvs auth list # List agent tokens (admin)
Tools (Marketplace skills)
Mirrors the 6 always-on MCP tools from the skills-meta plugin so terminal users can drive the marketplace runtime without going through an agent session. All operations are pinned to the workspace's current brand.
# Discover
opvs tools search "find restaurants in Aarhus" # Semantic registry search
opvs tools help @opvs-ai/agentboard # Full registry descriptor
opvs tools help @opvs-ai/agentboard --section methods # Just the methods
# Operate
opvs tools list # Installed packages on this brand
opvs tools install @opvs-ai/agentboard \
--grants '{"vendor.opvs-ai.agentboard.read":true}' # Install with entitlements
opvs tools uninstall @opvs-ai/agentboard --yes # Soft-uninstall
# Invoke
opvs tools call @opvs-ai/agentboard list_tasks \
--args '{"board_id":"abc-123"}' # Forward through runtime proxy
opvs tools call agentboard list_tasks --args-file ./params.json
Workspace
opvs workspace list # List all workspaces
opvs workspace use <slug> # Switch current workspace
opvs workspace current # Show current workspace details
opvs workspace remove <slug> --yes # Remove a saved workspace
Machines
Read the machines (environments) of a workspace. -w picks the workspace, so
opvs -w <tenant> machines list reads THAT workspace's fleet — the MCP
fleet_list can only answer for the token's own brand.
opvs machines list # This workspace's machines
opvs machines list --project <id> # Only the machines tied to that project (the reply must carry a project field)
opvs machines list --json # The same fields, as JSON
opvs machines get <ref> # One machine by id, short code or prefix
The output is an ALLOW-LIST of fields — name, short_id, status,
sidecar_version, last_health_check, the worker-loop fields, and the
project and capacity fields when the reply carries them. A field the reply
does not carry prints -. A token, a key or an install command is never
printed, in text or in --json.
--project works only when the reply carries a project field on at least one
row. The environments reply carries none today, so --project would drop
every row and print (no machines) — a planner checking a client's
onboarding would read that as "this client has no machines". Instead the
command says the reply does not say which project a machine belongs to and
exits non-zero, without printing any row. Run opvs machines list without
--project to see the workspace's machines.
YAML Output
All read commands return YAML by default for token-efficient AI agent consumption (40-76% fewer tokens than JSON). Set format with:
opvs config set format yaml # default
opvs config set format json
opvs config set format md
Claude Code Integration
Run opvs init to generate a CLAUDE.md snippet you can add to your project, giving Claude Code automatic access to your board and docs.
Requirements
- Node.js 18+
- An OPVS workspace (opvs.ai)
License
Proprietary - OPVS.ai