@corti/cli
v1.3.1
Published
Corti CLI — wire Corti as an OpenAI-compatible provider into terminal AI coding agents (OpenCode, Pi, Crush, ForgeCode, Codex CLI) and more.
Downloads
3,782
Readme
Corti CLI
Wire Corti as an OpenAI-compatible provider into your terminal AI coding agent.
npx @corti/cli@latest models initThe command prompts you to paste an .env block from the Corti Console (Developer quickstart → Copy all as .env variables) containing CORTI_TENANT_NAME, CORTI_CLIENT_ID, and CORTI_CLIENT_SECRET. It constructs the Bearer token (base64 of <tenant>:client_credentials:<client_id>:<client_secret>) and writes a corti provider entry into each tool you select. Pass -i, --interactive to enter credentials field-by-field instead.
| Tool | File written | Format | Notes |
| ------------- | --------------------------------------------------------- | ------ | ----- |
| OpenCode | opencode.json (or ~/.config/opencode/opencode.json) | JSON | Full provider entry — CORTI_BEARER referenced via {env:…} |
| Pi | .pi/agent/models.json (or ~/.pi/agent/models.json) | JSON | providers.corti entry, api: "openai-completions", apiKey: "CORTI_BEARER" (bare env name) |
| ZCode | ~/.zcode/v2/config.json (global only) | JSON | provider.corti entry with the bearer embedded (ZCode has no env indirection); restart ZCode after, see below |
| Crush | .crush.json (or ~/.config/crush/crush.json) | JSON | Full provider entry — $CORTI_BEARER substitution |
| ForgeCode | .forge.toml + .forge/.credentials.json | TOML+JSON | [[providers]] entry (selected by default); bearer embedded in sidecar .credentials.json (no env indirection) |
It also writes a managed # >>> corti-cli >>> block to .env with:
CORTI_CLIENT_IDCORTI_CLIENT_SECRETCORTI_TENANTCORTI_BEARER(base64 of<tenant>:client_credentials:<client_id>:<client_secret>, used by the tools)CORTI_BASE_URL
Each tool config references CORTI_BEARER rather than embedding the secret in the JSON/TOML file, so the credential stays out of version control. Two exceptions embed the bearer because the tool offers no env indirection: ForgeCode's .credentials.json sidecar and ZCode's ~/.zcode/v2/config.json (both global/home-dir files, never in your repo).
Under global scope it also offers to add a managed block to your shell rc (~/.zshrc, ~/.bashrc / ~/.bash_profile, or fish config, resolved from $SHELL) so new shells load the CORTI_* variables from ~/.env automatically. The block exports only CORTI_* lines — it never sources the whole file and never contains secret values. It shows up as its own row on the plan screen (press s there to skip it), and the file is backed up before its first edit. Project scope never touches your shell rc.
Usage
corti models init [options]
--client-id <id> Corti client ID (else $CORTI_CLIENT_ID, else prompted)
--client-secret <secret> Corti client secret (else $CORTI_CLIENT_SECRET, else prompted)
--tenant-name <name> Corti tenant name (else $CORTI_TENANT, else prompted; default base)
--base-url <url> Corti base URL (else $CORTI_BASE_URL, else prompted; default https://ai.eu.corti.app/v1)
--region <region> Corti region, e.g. eu (else $CORTI_ENVIRONMENT) — sets the base URL
--model <id> Default model (else $CORTI_MODEL, else picked from /models, else prompted)
--tools <list> Comma-separated subset: opencode,pi,zcode,crush,forgecode (else $CORTI_TOOLS)
--scope <project|global> Where to write configs (else $CORTI_SCOPE; default global)
--shell-rc Wire your shell rc to load CORTI_* into new shells (headless is opt-in; interactive default)
--no-shell-rc Never touch your shell rc file
-y, --yes Non-interactive; fail if required values are missing
-i, --interactive Prompt for credentials field-by-field instead of pasting a .env block
--fresh Ignore credential/region CORTI_* env vars (shell-exported or from ~/.env/./.env); use to re-provision to a different region/tenant (a harvested ~/.env block otherwise re-uses the prior region's credentials)
--dry-run Print file changes without writing them
--no-verify Skip the /models probe (--yes skips prompts, not verification)
--json Emit exactly one JSON document on stdout; all text to stderr (else $CORTI_JSON)
--no-color Disable color output (NO_COLOR and TERM=dumb are also honored)
--theme <mode> Palette for light/dark terminals (else $CORTI_THEME; default auto — probes the background)Resolution order per value: flag → env var → interactive prompt. With --yes, missing required values fail fast (exit 2), the /models probe still runs (--no-verify opts out), and Ink/React are never loaded.
When a newer version is published, the wizard shows a one-line update row at startup; headless runs print the notice on stderr after the command finishes (never in CI, --json runs, or piped output; set NO_UPDATE_NOTIFIER=1 to opt out). Prefer npx @corti/cli@latest — it always runs the newest release.
Exit codes
| Code | Meaning | E_* codes |
| ---- | ------- | ----------- |
| 0 | Full success (including "everything already up to date" and clean --dry-run) | E_NO_MODELS, E_MODEL_UNKNOWN (warnings; the run continues) |
| 1 | Internal error, or plan declined | E_DECLINED, E_INTERNAL, E_DEVICE_DENIED |
| 2 | Usage/validation error (bad flag, missing flag under --yes, no TTY without --yes) | E_USAGE |
| 3 | Auth failure (HTTP 401/403 from the probe) | E_AUTH |
| 4 | Network/API failure (DNS, timeout, TLS, connection refused, 5xx) | E_API, E_DNS, E_TIMEOUT, E_TLS, E_NET, E_DEVICE, E_DEVICE_EXPIRED |
| 5 | Run failed with nothing modified (total write failure, backup gate, stale plan) | E_PLAN, E_WRITE, E_BACKUP, E_STALE_PLAN |
| 6 | Partial success (some tools configured, some failed; includes env-write failure) | E_PARTIAL, E_ENV_WRITE |
| 130 | Interrupted (SIGINT/SIGTERM) | — |
The E_* code is stable, printed with every failure, and carried in --json.
--json
--json puts exactly one versioned JSON document on stdout — on success and failure — with all
human-readable progress on stderr. Combine with --yes (writes) or --dry-run (preview):
corti models init --yes --json --tools opencode --model corti-1 \
--client-id … --client-secret … # writes, then emits the result document
corti models init --json --dry-run --tools opencode --model corti-1 \
--client-id … --client-secret … # previews with would-* actionsThe document carries ok, exitCode, resolved (each value with its provenance), probe,
per-file results (with diffs — secret-bearing files are diffstat-only, never a diff body),
env, backupDir, warnings, and error: {code, message} | null. Secrets never appear in it.
models list
corti models list probes GET {base-url}/models and prints the available model ids:
corti models list --bearer $CORTI_BEARER --base-url https://ai.eu.corti.app/v1corti-s1 # ctx 524,288 · cost in 2 out 8
corti-s1-mini # ctx 262,144 · cost in 1 out 4
corti-s1-tiny # ctx 32,768Each line is the bare model id first (so awk/cut pipelines stay intact), then # and any
metadata the API provides — context window and cost per million tokens, only the keys that are
present. Models with no metadata print as a bare id.
With --json the document adds a modelDetails sibling, index-for-index with models, carrying
each model's metadata (capabilities, cost, effort, maxInputTokens) verbatim from the API:
corti models list --json --bearer $CORTI_BEARER --base-url https://ai.eu.corti.app/v1{
"$schema": "https://corti.app/schemas/cli-list-models.v1.json",
"ok": true,
"exitCode": 0,
"models": ["corti-s1", "corti-s1-mini", "corti-s1-tiny"],
"count": 3,
"modelDetails": [
{ "id": "corti-s1", "maxInputTokens": 524288, "capabilities": { "reasoning": true, "image_input": false, "tool_call": true, "temperature": true }, "cost": { "input": 2, "output": 8, "cacheRead": 0.2 }, "effort": { "supported": true, "levels": ["high", "max"] } },
{ "id": "corti-s1-mini", "maxInputTokens": 262144, "capabilities": { "image_input": true } },
{ "id": "corti-s1-tiny" }
],
"probe": { "status": "ok", "httpStatus": 200, "latencyMs": 42 },
"error": null
}modelDetails is always present (never omitted, even when empty), so a consumer can distinguish
"old deployment, no metadata support" from "new deployment, this tenant's models happen to have
none" by the key's presence rather than its contents. The models array stays string[] — adding
modelDetails is an additive change, $schema stays v1.
Backups
Before modifying any existing file, the CLI copies it to
~/.local/share/corti/backups/<timestamp>/ (mode 0700, manifest included) and verifies the file
on disk still matches what was planned — a mismatch aborts the whole run with nothing written.
What it does
- Builds the Bearer token as base64 of
<tenant>:client_credentials:<client_id>:<client_secret>. - Probes
GET {base-url}/modelswith that Bearer. The response carries per-model metadata — context window, cost, capabilities (reasoning, image input, tool calling), and reasoning-effort levels — which the CLI writes into each tool's config. A field the API omits is omitted from the config too; the tool falls back to its own default.- On success, lets you pick a model.
- On failure (auth or network), surfaces the status in the side panel and falls back to a typed model id.
- Detects which tools are installed (best-effort) and pre-selects them.
- Asks before writing. Each writer merges into existing config rather than overwriting unrelated keys.
- Writes are atomic (temp file + rename) and idempotent — a second run is a no-op.
After init
If you accepted the shell rc block (global scope), CORTI_* loads automatically in shells you open from now on — the shell you ran init in is not one of them, so open a new terminal. For that shell — or if you skipped the block or used project scope — load .env manually so CORTI_BEARER is available when the coding tool starts:
set -a; . .env; set +aThen run the tool:
opencode --model corti/<model> # OpenCode
pi # Pi (select provider 'corti')
crush # Crush (select provider 'corti')
forge # ForgeCodeZCode is a desktop app: restart it after init (it loads providers at launch), then pick the
Corti provider in chat.
Development
pnpm install
pnpm build
pnpm test
node ./dist/cli.js models init --dry-run \
--client-id test-client-id \
--client-secret test-client-secret \
--base-url https://ai.eu.corti.app/v1 \
--model corti-1 \
--tools opencode,pi,zcode,crush,forgecodeReleasing
Releases are automated — merging to main runs semantic-release, which derives the
version from the conventional commit message and publishes to npm via OIDC. Since PRs are
squash-merged, that commit message is the PR title.
fix:→ patch ·feat:→ minor ·feat!:/BREAKING CHANGE:→ major- Prerelease: push a suffixed tag —
git tag v1.2.0-rc.1 && git push origin v1.2.0-rc.1(publishes under thercdist-tag, neverlatest) - Don't hand-push a bare
vX.Y.Ztag — CI rejects it; stable goes throughmain
License
MIT
