Skip to content
Tanvrit AI
Command line

The tanvrit CLI

tanvrit is the third first-party surface over the same local runtime as the desktop app and the VS Code extension — the same index, the same agents, the same staged edits, driven from a terminal or a CI job. 68 commands, one machine-readable output contract, and an explicit statement on every envelope of which execution path answered.

Linux x86_64 & ARM64 downloads · macOS and Windows build from source · v1.6.4

commands
68
backends
2
run offline
64
drive the app
53

Install

Getting the binary

On Linux, download it. The tarball is self-contained — it carries its own Java runtime, so there is nothing to install first and nothing to add to your system.

macOS Apple Silicon

tanvrit-cli-1.6.4-macOS-arm64.tar.gz
418 MB
Download v1.6.4

macOS Intel

tanvrit-cli-1.6.4-macOS-x64.tar.gz
419 MB
Download v1.6.4

Linux x86_64

tanvrit-cli-1.6.4-Linux-x64.tar.gz
419 MB
Download v1.6.4

Linux ARM64

tanvrit-cli-1.6.4-Linux-arm64.tar.gz
416 MB
Download v1.6.4
install.sh
# Linux only — this is the whole install. No JDK needed: the tarball
# carries its own jlink runtime.
curl -fLO https://dl.tanvrit.com/ai/manual/tanvrit-cli-1.6.4-Linux-x64.tar.gz
tar -xzf tanvrit-cli-1.6.4-Linux-x64.tar.gz
./tanvrit-cli-1.6.4/bin/tanvrit --help

# Put it on PATH if you want the bare `tanvrit` word to work.
sudo ln -sf "$PWD/tanvrit-cli-1.6.4/bin/tanvrit" /usr/local/bin/tanvrit

The filename's OS word is capitalised — Linux, the same spelling the .dmg/.msi/.deb names use. Storage keys are case-sensitive and there is no redirect, so a hand-typed tanvrit-cli-1.6.4-linux-x64.tar.gz is a 404, not a download. Its architecture word is x64 or arm64 — not the amd64 the .deb uses. No checksum file is published alongside it yet, so the sizes above — the Content-Length dl.tanvrit.com serves — are the only figure worth comparing after a download.

macOS and Windows: build it

Neither has a published CLI artefact — the tarball is produced by the job that also builds the .deb, and only that job runs cliDistTar today. The packaging constraint behind it is real rather than an oversight: the jpackage-based installers emit exactly one launcher per invocation, so a second executable has to be copied into the app image after the fact. Until a macOS or Windows release job does that, build it directly:

build-from-source.sh
# Build a self-contained runtime image containing the tanvrit launcher.
./gradlew :composeApp:cliRuntimeImage
./composeApp/build/cli/bin/tanvrit --help

# Or run it straight from the build tree, without producing an image.
./gradlew :composeApp:runCli -Pargs="status --json"

-Pargs is tokenized by Ant's command-line parser, which understands single and double quotes but not escaped ones — an argument containing both quote characters cannot be passed through it at all. Use --prompt-file for prompts with hostile quoting.

Straight talk

What you can and cannot do today

Being in the command tree fixes a command's invocation, its options and its exit-code contract — not that a backend does the work. Here is the honest state, mapped one-to-one to the reference below.

Downloads

Linux gets a real download; macOS and Windows do not.

v1.6.4 publishes a standalone tarball for Linux x86_64 and ARM64 — see Getting the binary. There is still no Homebrew formula, no npm package and no install command, and there is no published macOS or Windows build at all: on those two you build it from the repository, which is private today.

Packaging

The desktop installers on the download page do not contain it.

The packaging step that places tanvrit inside the app image — next to the app launcher, as a second launcher — exists and is wired to the installer tasks, but packageDeb/packageDmg/packageMsi build their own image internally and never read the one it patches. The Linux release job does build the CLI now, but it ships it as its own tarball beside the .deb, not inside it. So nothing you install from Download puts the command on your PATH.

Coverage

Being in the command tree is not the same as doing the work.

The tree fixes each command's invocation, its options and its exit-code contract. 64 of 68 commands are implemented in-process and 53 have a route on the running app; 2 are served by neither — provider set and policy set. With --yes they answer not_implemented on --local and remote_unsupported on a reachable --remote, exit 5 either way, and since 2026-09-11 neither of those two refusals points at the other backend. Two earlier gates are not backend-aware and still do: --local without --yes answers confirmation_required and exit 64 (the in-process backend treats every command in the tree as "supported", so the gate runs above the stub's own refusal), and --remote with no app running answers app_unreachable whose remedy says "drop --remote to run in-process" — the closed road. This bullet said "exit 5 either way" with no --yes until 2026-09-11, which is reproducibly false as typed on the --local half. The reference below marks every one of them.

Sign-in

Browser sign-in cannot complete.

tanvrit auth login is implemented, but no OAuth client is registered for it server-side, so it fails fast with oauth_client_not_registered before a browser window is opened. Do not plan around it working; see Browser sign-in.

Architecture

Two backends, one command set

The same commands are reached two ways, and they are not interchangeable.

With neither --local nor --remote, the CLI probes 127.0.0.1:19280 and picks: app reachable means attached, otherwise in-process. --local forces in-process and ignores a running app; --remote requires the app and fails with app_unreachable rather than silently answering a different question.

In-process (--local)Attached (--remote)
How it runsBoots the runtime in-process, the way the headless runner doesDrives the running desktop app over its Local API on 127.0.0.1:19280
Needs the app openNoYes
Sees state the app ownsNo — its own process onlyYes — the same staged edits and runs the desktop UI shows
Command coverage64 of 68 commands53 of 68 commands
backend on every envelope"embedded""remote"

Why tanvrit status always names the backend

Because the two are not equivalent, and a CLI that hid which one answered would be misreporting its own capabilities. In-process runs under the CLI tool surface with a full capability ceiling; attached is a client of a loopback HTTP API and gets that surface's route coverage. Without the backend named on the envelope, "this worked yesterday and errors today" is unattributable to the app having been open yesterday. One caveat we would rather state than imply: read the attached path's narrower coverage as route coverage, which is real, not as a security boundary — the tighter governance policy selected for that surface is not enforced today.

Contract

stdout is data, stderr is everything else

The rule the CLI exists to fix — it holds for every command and both output modes, with exactly one named exception.

Under --json, every stdout line is exactly one envelope and nothing else; without it, stdout is the rendered result and nothing else. Progress, warnings, diagnostics and confirmation prompts all go to stderr. It is structural rather than conventional: a backend's only handle on the terminal writes to stderr, and it cannot ask for stdout — so a future progress line that "just does a print" cannot reach a parser. --quiet silences progress; --verbose adds diagnostics; both write to stderr, and passing both is not a contradiction.

The exception: mcp serve --stdio. An MCP client that spawned the process reads JSON-RPC from its stdout, so for that one parsed shape the transport gets the real stdout and the whole envelope stream — the terminal envelope included — moves to stderr. "Every stdout line is an envelope" stays literally true, because stdout carries none. One predicate over the parsed argv names the shape, and the hand-over itself is the last step before the handler — after the capability check, the confirmation gate and the reachability probe, so every refusal that can precede a handler still renders on real stdout — and it is released in a finally. That ordering is what keeps it an auditable exception rather than a habit; for part of 2026-09-11 the hand-over happened first, and a mcp serve --stdio that was going to be refused emitted nothing on stdout at all. This paragraph said the rule was "absolute" until 2026-09-11 — true when written, falsified the same day by the stdio transport landing.

Machine-readable

The --json envelope

One envelope per line, newline-delimited, so a reader can act on each line as it arrives instead of buffering the run.

A unary command emits exactly one result or error. A streaming command emits zero or more event envelopes and then exactly one result or error; those two are mutually exclusive and terminal, and the envelope type rejects a malformed combination at construction. The schema version is a promise to scripts: it bumps only on a breaking shape change, and adding an optional field is not breaking.

FieldTypeMeaning
schemaintEnvelope schema version, currently 1. Emitted on every envelope.
kind"event" | "result" | "error"Incremental update, terminal success, or terminal failure.
commandstringThe stable command id, e.g. "agent.run".
backend"embedded" | "remote"Which execution path answered.
payloadobject | nullCommand-shaped. Absent on error envelopes.
errorobject | nullPresent on, and only on, error envelopes.
seqintMonotonic index within one invocation.

Worked example

tanvrit --json
# Unary: exactly one terminal envelope, then the process exits.
$ tanvrit search --local --repo . --limit 2 --json "retry failed uploads"
{"schema":1,"kind":"result","command":"search","backend":"embedded","payload":{ … },"seq":0}

# Streaming: N events, then exactly one result or error.
$ tanvrit index build --repo . --json
{"schema":1,"kind":"event","command":"index.build","backend":"embedded","payload":{ … },"seq":0}
{"schema":1,"kind":"event","command":"index.build","backend":"embedded","payload":{ … },"seq":1}
{"schema":1,"kind":"result","command":"index.build","backend":"embedded","payload":{ … },"seq":2}

# Failure: one error envelope on stdout, and a non-zero exit code.
$ tanvrit policy set retention 30 --remote --json ; echo "exit=$?"
{"schema":1,"kind":"error","command":"policy.set","backend":"remote","error":{"code":"remote_unsupported","message":"…","remedy":"No backend implements it: …"},"seq":0}
exit=5

The payload objects are elided on purpose: no command's payload shape is pinned in code yet, so printing a plausible-looking one here would be inventing a contract that does not exist. The envelope around it is real, and so are the error code and the exit code.

The one payload that is pinned

status emits the same 16 keys on both backends, checked at runtime before it prints. A backend that cannot determine a field emits it as null, never absent — null says "same schema, unknown here", absent says "different schema", and only the first is safe for a script to branch on. Extra keys are allowed; missing ones are not.

  • backend
  • toolSurface
  • version
  • sdkVersion
  • endpoint
  • appRunning
  • localApi
  • mcpServer
  • engine
  • authenticated
  • account
  • provider
  • indexedProjects
  • indexedFiles
  • indexedSymbols
  • stagedEdits

Error codes

code is stable and scriptable; message is for humans and may be reworded, so branch on the code. remedy is present when there is a concrete next action, and it is computed rather than guessed — a command the attached path has no route for will not be told to try --remote.

CodeWhen
remote_unsupportedThe command exists but the attached backend has no route for it. The remedy names --local when the in-process backend implements it, and says which reason blocks both when neither does.
not_implementedThe command exists but the in-process backend does not do the work yet. Four commands, and since 2026-09-11 no option-level case — mcp serve --stdio used to be one and is now served. Defined by the launcher, not by the pinned code list.
app_launcher_missingmcp config was asked for a stdio client's block, and there is no Tanvrit AI app launcher beside this install to name in it. The standalone CLI tarball ships bin/, lib/ and runtime/ only.
unknown_commandNo entry in the command tree matches the argv.
bad_usageA missing required option, an integer option that is not base-10, or an unknown flag.
not_authenticatedNo stored credentials.
app_unreachable--remote was given and 127.0.0.1:19280 did not answer. Its remedy is not backend-aware: it offers "drop --remote to run in-process" even for provider set / policy set, which the in-process backend also refuses.
confirmation_requiredA mutating command, no TTY, and no --yes. On --local the gate runs above the in-process stub's own refusal, so this is the first answer even for a command that was never going to run.
verification_failedAn agent run finished and its post-run verification failed; the edits were rolled back. Exit 2, not 5.
agent_errorAn agent run failed, timed out, or was refused by the provider mid-run. Exit 3, not 5. It is the largest real-world producer of exit 3: --model <id-not-installed> takes exactly this path.
job_interruptedA training job the command followed is paused — settled INTERRUPTED, or waiting for an owner that is not running. The remedy is tanvrit train run --resume <job>. The training commands pass their own failure codes (spec_invalid, no_training_owner, model_in_use, …) through unchanged.
stopped_followingCtrl-C stopped a train / model command that was following a job on the app or tanvrit serve. The job runs on; tanvrit train watch <job> follows it again.
internalAnything else.

Scripting

Exit codes

0, 2, 3 and 64 deliberately reuse the values the existing headless runner returns, because compute nodes and shell wrappers branch on them today and the CLI takes that entry point over.

A script that already distinguishes "verification failed" from "agent error" keeps working unchanged. 1, 4, 5 and 6 are new, and were chosen from values the headless runner never returns so an existing consumer cannot mistake one for a code it already knows. 6 is its own number rather than 1 because nothing failed: a paused training job keeps its checkpoint.

CodeNameMeaning
0OKSuccess.
1FAILUREThe command ran and failed — internal, and any code this table does not name.
2VERIFICATION_FAILEDAn agent run completed but verification failed; edits were rolled back. verification_failed.
3AGENT_ERRORAgent error, timeout, or a mid-run provider refusal. agent_error.
4NOT_AUTHENTICATEDNo credentials. not_authenticated.
5UNSUPPORTEDThis backend cannot serve the command — remote_unsupported, not_implemented, app_launcher_missing and app_unreachable all land here.
6INTERRUPTEDA followed training job is paused, not failed: job_interrupted. Declared, but the launcher does not route the training codes to their numbers yet — until it does those runs exit 1, while error.code is already right.
64BAD_USAGEBad argv — bad_usage, unknown_command, and confirmation_required. That last one surprises people: being asked for --yes is a 64, not a 5.

Flags

Global options

Accepted by every command and handled before dispatch.

Two shared options recur through the reference with fixed spellings and defaults, declared once so no subcommand can mean something slightly different by them: --repo <dir> (default .) and --limit <n> (default 20).

OptionShortMeaning
--json-jEmit the newline-delimited envelope stream on stdout.
--quiet-qSuppress progress output on stderr.
--verbose-vVerbose diagnostics on stderr.
--localForce in-process execution, ignoring a running app.
--remoteRequire the running app; fail if it is not reachable.
--endpoint <url>Local API base URL. TANVRIT_ENDPOINT does the same for every invocation.
--yes-yAssume yes for confirmation prompts.
--help-hShow help for this command.

Confirmation on mutating commands

Commands marked change state you might want back. Before executing one the CLI asks, and three rules apply in order: --yes wins everywhere, including non-interactively, because it is an explicit and auditable answer; no TTY and no --yes is an immediate refusal (confirmation_required) rather than a prompt blocking on a stdin nobody will type into; and with a real console only y or yes counts — anything else, including end-of-input, is a no.

Reference

Command reference

All commands, in the groups and the display order the command tree declares.

All 68 commands. marks a mutating command (needs --yes without a TTY); marks a streaming one. The two right-hand columns are the honest part: they are read from the backends' own implementation lists, not from the tree.

Reading the columns. In-process means the command does its work with the app closed; a dash there means it answers not_implemented and exits 5. Attached means the running desktop app has a route for it; a dash there means remote_unsupported, exit 5, with a remedy naming --local when the in-process backend implements it. A row with two dashes is served by neither — there are 2 of those, and those two refusals name no backend at all. Reaching them takes --yes and, on --remote, a running app; the confirmation_required and app_unreachable answers that come first are not backend-aware.

These two columns are a snapshot of the backends' own implementation lists, re-read on 11 September 2026 and again on 25 September 2026, when the 26 training commands landed; coverage is being filled in and nothing in CI compares this table against the Kotlin. It had drifted badly before that re-read — 24 of the 41 rows it had then understated what the build does (19 wrong In-process cells, 12 wrong Attached cells, 7 rows wrong in both) — so treat it as a guide and your own build as the authority: run the command with --json and read the backend field and the envelope. This sentence published "21" from 2026-09-11 until it was recounted from the diff the same day.

auth

4 commands

Sign-in state. Tokens are written to the same credential store the desktop app uses, so signing in on one surface authenticates the other.

CommandOptions / argsIn-processAttached
auth login✎

Sign in through your browser. --email becomes an OAuth login_hint.

--email <addr>yesno
auth logout✎

Clear stored credentials.

—yesno
auth whoami

Show the signed-in account.

—yesno
auth hf-token

Store the Hugging Face token read from stdin — never from argv, never echoed. --status says stored, environment or unset, never the value. Storing and --clear ask to confirm (--yes in a script); --status does not.

--clear, --statusyesno

Sign-in is a local-keychain operation, so it is in-process only — the Local API exposes no account route. auth hf-token is in-process only for a stronger reason: secrets never travel over the API, so on a headless host you run it there (ssh -t) or put HUGGINGFACE_TOKEN in its service env file. auth login is written and tested but cannot complete today; see Browser sign-in.

index

3 commands

The local code index that search, grounding and agent runs read from.

CommandOptions / argsIn-processAttached
index build✎ ⇄

Index a repository for search and grounding. --force re-indexes files already current.

--repo <dir>, --timeout <min> (0 disables), --forceyesyes
index status

Index freshness and counts.

--repo <dir>yesyes
index clear✎

Delete the index for a repository.

--repo <dir>yesyes

index build is the one command that streams on both paths, but only the in-process path reports mid-pass progress: the app's /v1/index/reindex route runs the pass and answers once, so attached runs keep the envelope grammar and say so on stderr rather than inventing progress events.

agent

5 commands

One non-interactive agent task, and the run history around it. Option names match the headless runner's, so an existing invocation ports across unchanged.

CommandOptions / argsIn-processAttached
agent list

List installed agents.

—yesyes
agent run✎ ⇄

Run one agent task.

--repo <dir>, --agent <id>, --prompt <text>, --prompt-file <path>, --model <id>, --provider <id>, --output-dir <dir>, --auto-approve, --max-tool-calls <n> (default 50), --max-repair-attempts <n> (default 2), --no-indexyesyes
agent runs

List recent agent runs.

--limit <n>yesyes
agent stream <runId> ⇄

Attach to a running agent.

—noyes
agent stop <runId>✎

Stop a running agent.

—noyes

agent run works on both paths: attached it posts to /v1/agent/run, and with the app closed --local boots the runtime in-process and runs the agent itself. Only agent stream and agent stop are attached-only — an embedded run is synchronous and process-local, so there is no second process to attach to or stop. This note said agent run was attached-only and answered not_implemented with the app closed; that was read off this page's own stale availability columns and was never true of EmbeddedBackend.IMPLEMENTED. Corrected 2026-09-11. Use --prompt-file for prompts with hostile quoting.

edits

5 commands

The staged-edit queue. Nothing an agent proposes touches disk until one of these applies it.

CommandOptions / argsIn-processAttached
edits list

List staged edits.

—yesyes
edits show <editId>

Show one staged edit as a diff.

—yesyes
edits apply <editId>✎

Apply one staged edit.

—yesyes
edits reject <editId>✎

Discard one staged edit.

—yesyes
edits apply-all✎

Apply every staged edit.

—yesyes

Staged edits are the clearest case for attaching rather than forcing --local: an in-process run boots its own graph and sees its own queue, so edits list --local will never show the edits the desktop UI has staged in another process. That is process-local state, not a missing capability.

mcp

4 commands

The Model Context Protocol surface — the catalogue, the per-client credential, and the client config blocks.

CommandOptions / argsIn-processAttached
mcp serve ⇄

Serve the MCP tool catalogue — --stdio for a client that spawned it (Claude Desktop, Zed, Continue, Windsurf, Cline), --http for HTTP/SSE on 127.0.0.1:19281. Exactly one.

--stdio, --http, --client-id <id>yesno
mcp tools

List the advertised tool catalogue.

—yesno
mcp credential

Print this client's MCP credential.

—yesyes
mcp config

Print MCP client configuration. Omit --client for an index of all seven.

--client claude-desktop | claude-code | cursor | zed | continue-dev | windsurf | clineyesno

mcp serve, mcp tools and mcp config are in-process only — the Local API has no catalogue or config-rendering route, and mcp credential is the one here with a route of its own. All four do real work: mcp serve --http serves the catalogue, and mcp config --client cursor prints a config block and exits 0. mcp serve --stdio serves the stdio transport from this process: stdout carries the client's JSON-RPC and the whole --json envelope stream moves to stderr for that one run.Pass --local in a client config. It is not a nicety: mcp.serve has no route on the attached backend, so without it the probe hands the spawn to a running desktop app and the client gets remote_unsupported (exit 5) instead of a session. The refusal does reach stdout — the hand-over is the last step before the handler, after the capability check — but for part of 2026-09-11 it was decided from argv instead, and the same run emitted an empty --json stdout with the error on stderr. This note said --stdio was refused (not_implemented, exit 5) until 2026-09-11; the transport landed later the same day. One refusal is still worth knowing before you script it: five of the seven --client values are stdio clients whose config block has to name that launcher, so on an install with none beside it — the standalone CLI tarball ships bin/, lib/ and runtime/ only — mcp config answers app_launcher_missing, exit 5. That refusal is not a dead end: its remedy names three ways on, cheapest first — (1) point the client at your own launcher with "args": ["mcp", "serve", "--stdio", "--local", "--client-id", "<alias>"], which works from the very install that refused; (2) install the desktop app, or put its launcher on PATH, and re-run for a block that needs no editing; (3) use one of the two HTTP clients that need no launcher at all (claude-code, cursor). This sentence stopped at option (3) until 2026-09-11, describing the refusal as the dead end it had already stopped being — docs/CLI.md documented all three roads and this page documented one. This note also said both commands were "declared and not implemented on either path yet" until 2026-09-11; they are both in EmbeddedBackend.IMPLEMENTED. None of this affects the MCP server the desktop app already runs on localhost:19281 — that is what the quickstart wires up, and it is unchanged by the CLI.

serve / service

2 commands

The headless host: the runtime in the foreground, or as a systemd user service.

CommandOptions / argsIn-processAttached
serve ⇄

Run the local Tanvrit runtime (Local API + MCP) in the foreground until interrupted.

—yesno
service install✎

Install tanvrit serve as a systemd user service (Linux): the unit, a 0600 env file, enable --now, and linger — which fails loudly, with the command to run, when sudo wants a password.

--unit-name <name>, --dry-runyesno

model / provider

14 commands

Local model lifecycle and the AI provider your runs route through.

CommandOptions / argsIn-processAttached
model list

The local registry — every model this host can serve, with its lineage. --cloud lists the provider catalogue instead.

--cloudyesyes
model load <modelId>✎

Load a local model.

—yesyes
model unload <modelId>✎

Unload a local model.

—yesyes
model pull <modelId>✎ ⇄

Download a model from the catalogue.

—yesno
model import <path>✎ ⇄

Register a GGUF file, a split set or a Hugging Face directory, and follow the import job until it settles. --yes also confirms you have the right to use the file.

--mode copy | reference, --name <n>, --task chat | completion, --convert q4_k_m[,q8_0]yesyes
model convert <modelId>✎ ⇄

Convert a registered Hugging Face directory to GGUF (an export job), followed until it settles.

--artifacts gguf_q4_k_m,…yesyes
model info <modelId>

One registry model: its record, GGUF header and lineage chain.

—yesyes
model rm <modelId>✎

Remove a model from the registry. Removing a reference never deletes your file.

—yesyes
model chat <modelId> ⇄

Chat with a registry model — in-process, or through the running owner's /v1/chat/completions.

--prompt <text> | --messages <file|->, --system <text>, --max-tokens <n>yesyes
model compare <modelId>✎ ⇄

Base-vs-tuned samples side by side (an eval job).

--to <modelId>, --prompts <file|-> (required, up to 8), --max-tokens <n>yesyes
model rate <modelId>✎

Rate one reply, optionally with a correction, for the next retrain.

--verdict good | bad (required), --prompt <text> --output <text> | --sample <file|->, --correction <text>, --compare-model <id>yesyes
provider list

List AI providers and their status.

—yesyes
provider set <providerId>✎

Set the active provider.

—nono
provider test <providerId>

Validate a provider's credentials.

—yesyes

model list changed meaning on 25 September 2026: it lists the local registry on both paths, with the same row keys, and the provider catalogue it listed before is model list --cloud, unchanged.

train

14 commands

Fine-tune a model on this host or on a remote one — the job runs in the app, in `tanvrit serve`, or in the command itself.

CommandOptions / argsIn-processAttached
train setup✎ ⇄

Provision the training runtime (several GB), followed until it settles.

--profile auto | mlx | hf-cu130 | hf-cu126 | hf-rocm72 | hf-cpu, --build-llama-cppyesyes
train doctor

The capability report: what this host can train and serve.

--refresh, --measure-memcgyesyes
train schema

The training-spec JSON Schema, or a bundled example spec.

--example <name>yesyes
train validate <spec|->

Validate a spec on the host that would run it.

--planyesyes
train start <spec|->✎

Queue a job on the running owner (the app or tanvrit serve).

--idempotency-key <key>yesyes
train run <spec|->✎ ⇄

Run a job (or resume one) and follow it until it settles. With no owner, the command itself runs the job; Ctrl-C checkpoints.

--resume <jobId>, --batch-size <n>, --grad-accum <n>yesyes
train watch <jobId> ⇄

Follow a job's events until it settles.

--after <seq>yesyes
train list

Training jobs, newest first.

--state <state>, --kind <kind>, --limit <n>yesyes
train status <jobId>

One job: summary, artifacts, attempts and its latest events.

--specyesyes
train logs <jobId>

The job's redacted runner log.

--tail <n>yesyes
train cancel <jobId>✎

Stop a job, checkpointing first unless --now.

--nowyesyes
train resume <jobId>✎

Resume an interrupted or cancelled job on the running owner.

--batch-size <n>, --grad-accum <n>yesyes
train retry <jobId>✎

Retry a failed job's export or model registration.

--what auto | export | registrationyesyes
train delete <jobId>✎

Delete a settled job's run directory. Registered models stay.

—yesyes

Output kinds are fixed per command: train start, resume and retry answer at once and refuse with no_training_owner when neither the app nor tanvrit serve owns the host; train run is the form that runs the job itself. A followed job that ends paused exits 6 (job_interrupted), with the remedy tanvrit train run --resume <job> — see the exit-code table for what is and is not wired yet.

dataset

3 commands

Training data, content-addressed on the host that trains.

CommandOptions / argsIn-processAttached
dataset add <file>✎ ⇄

Add a JSON Lines dataset; re-adding the same bytes is a no-op. Streams the bytes, SHA-256 checked, to a remote host.

--name <n>yesyes
dataset list

List datasets.

—yesyes
dataset compose✎

One new dataset from datasets of one format and/or a model's rated replies.

--concat <ds-id> (repeatable), --feedback <modelId>, --format sft | preference, --name <n>yesyes

knowledge / packs / policy

8 commands

Team knowledge, Domain Packs, and the org policy the runtime enforces locally.

CommandOptions / argsIn-processAttached
knowledge search <query…>

Search team knowledge.

--limit <n>yesyes
knowledge add <body…>✎

Add a knowledge entry. The title is the entry's identity — the store upserts on it.

--title <text> (required)yesyes
knowledge list

List knowledge entries.

--limit <n>yesyes
pack list

List domain packs.

—yesyes
pack enable <packId>✎

Enable a domain pack.

—yesyes
pack disable <packId>✎

Disable a domain pack.

—yesyes
policy show

Show the effective org policy.

—yesyes
policy set <key> <value>✎

Set an org policy value.

—nono

This note claimed until 2026-09-11 that "marketplace, org policy, provider configuration and knowledge writes are the four categories the attached backend deliberately has no route for" and that most had no in-process handler either. Both halves are false and were already false in docs/CLI.md: pack *, policy show, provider list, provider test and all three knowledge commands have routes and are served on both paths. What is actually left is two commands — provider set and policy set — which no backend serves, each for a reason the refusal states: the active provider is in-memory state a second process cannot durably change, and org policy is issued by the gateway, so a machine that could rewrite its own governance rule would not be governed by it. Summarising a partition into categories is what went wrong; the column is transcribed per command instead.

status / config

4 commands

Where everything is, and which execution path you are actually talking to.

CommandOptions / argsIn-processAttached
status

Show app, server and backend status.

—yesyes
config get [key]

Read a config value. The key is optional.

—yesyes
config set <key> <value>✎

Write a config value.

—yesyes
config path

Print config and data locations.

—yesno

Auth

Browser sign-in

A browser flow of the shape gh, gcloud, firebase and wrangler use — OAuth 2.0 authorization code with PKCE (S256), a public client that ships no secret.

tanvrit auth login binds a throwaway HTTP server on 127.0.0.1, opens your browser at the provider's authorize endpoint with that loopback as the redirect target, catches the redirect and exchanges the code for tokens. Four properties worth knowing:

Endpoints are discovered, never hardcoded

Read from the issuer's /.well-known/openid-configuration. TANVRIT_OAUTH_ISSUER points the flow at a self-hosted deployment and TANVRIT_OAUTH_CLIENT_ID overrides the client id, both without a rebuild.

The URL is always printed

Not only when a browser fails to launch — over SSH or in a container there is nothing to open, and pasting it still works.

There is no one-time code to paste back

The code is handled in the browser, where the provider owns it, and the loopback catch completes the exchange.

Tokens go to the same credential store the desktop app writes

So signing in through either surface authenticates both.

Not usable yet

Blocked on a server-side record

The authorization server has no OAuth client registered for the CLI, and the discovery document advertises no dynamic-registration endpoint, so the CLI cannot register itself. Someone has to create a public client server-side — token auth method none, PKCE required, redirect URIs covering http://127.0.0.1:*/callback. Until that exists, tanvrit auth login fails with oauth_client_not_registered and a message naming exactly that, checked before a browser window is opened so nobody lands on a provider error page. No code change is needed once the record exists.