@corti/cli
v0.10.0
Published
Corti CLI — wire Corti as an OpenAI-compatible provider into terminal AI coding agents (OpenCode, Pi, Crush, ForgeCode, Codex CLI) and more.
Readme
Corti CLI
Wire Corti as an OpenAI-compatible provider into your terminal AI coding agent.
npx @corti/cli@latest init modelsThe 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" |
| 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 | TOML | [[providers]] entry with api_key_vars = "CORTI_BEARER" (selected by default) |
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 init models [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
--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 |
| ---- | ------- |
| 0 | Full success (including "everything already up to date" and clean --dry-run) |
| 1 | Internal error, or plan declined |
| 2 | Usage/validation error (bad flag, missing flag under --yes, no TTY without --yes) |
| 3 | Auth failure (HTTP 401/403 from the probe) |
| 4 | Network/API failure (DNS, timeout, TLS, connection refused, 5xx) |
| 5 | Run failed with nothing modified (total write failure, backup gate, stale plan) |
| 6 | Partial success (some tools configured, some failed; includes env-write failure) |
| 130 | Interrupted (SIGINT/SIGTERM) |
Every failure carries a stable E_* code documented in docs/errors.md.
--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 init models --yes --json --tools opencode --model corti-1 \
--client-id … --client-secret … # writes, then emits the result document
corti init models --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.
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.- 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
New shells load CORTI_* automatically if you accepted the shell rc block (global scope). For the current 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 init models --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
