Machine-readable mode
Every non-streaming sona command accepts --json and emits a single-line envelope. Streaming commands (chat) emit one envelope per chunk (ndJSON). This is the surface you wire orchestrators, CI pipelines, and parent agents against, with no scraping of human text and no fragile regex.
Envelope shape
One of two shapes, picked by success or failure:
{ "ok": true, "command": "status", "result": { "running": true, "pid": 12345 } }
{ "ok": false, "command": "uninstall", "error": { "code": "confirmation_required", "message": "uninstall is destructive; --json requires explicit --yes to skip the prompt." } }Fields:
ok(boolean): true on success, false on error.command(string): the subcommand that wrote the envelope (status,update,uninstall, …). Matches what you typed aftersona.result(object, success only): the command-specific payload. See the table below for the per-command shape.error.code(string, error only): a machine-readable identifier. Stable across releases.error.message(string, error only): a human-readable explanation. It may change between releases, so do not match against it.
Exit code reflects the same semantics: 0 for success, non-zero for errors. The exit code is the orchestrator's primary signal, and the envelope is the explanation.
Commands
| Command | Success result keys | Notable error codes |
|---|---|---|
sona --version --json | version, jre | None |
sona --help --json | version, commands[], globalOptions[] | None |
sona status --json | running, pid, reason | None (exits 1 when not running) |
sona stop --json | stopped, pid, used_force | signal_rejected, timeout, interrupted |
sona init --json | configExists, configPath | interactive_required (with --add-channel) |
sona update --json | action (one of installed / already_latest), from, to, current | endpoint_unusable, no_download_url, install_failed |
sona update --check --json | action: "check", current, latest, url, upToDate | endpoint_unusable |
sona update --rollback --json | action: "rolled_back" | no_previous_version, rollback_failed |
sona uninstall --dry-run --json | dryRun: true, paths[] | None |
sona uninstall --yes --json | dryRun: false, removed, failed, skipped, removedPaths[], failedPaths[] | confirmation_required, daemon_stop_failed |
sona doctor --json | schemaVersion, overall (one of ok / warn / fail), checks[] | None (its own document, not the envelope) |
sona logs --count --json | count | None |
sona chat --json | per-chunk records (ndJSON, see CLI channel) | None |
sona search --json "<q>" | query, total, hits[] | None |
sona config show --json | masked sona.yml as a config map | None |
sona cost --json [WINDOW] | window, totalUsd, byProvider, byWorkspace | None |
doctor --jsonpredates the envelope contract (TNS-466 axis 2). It emits its own document withschemaVersion,overall, andchecks. The shape is stable, but do not assume the standard envelope.
Examples
Health check in a CI script
if ! sona doctor --json | jq -e '.overall == "ok"' > /dev/null; then
echo "Sona not healthy; aborting deploy"
exit 1
fiUpgrade only when a newer version exists
LATEST=$(sona update --check --json | jq -r '.result.latest')
CURRENT=$(sona update --check --json | jq -r '.result.current')
if [ "$LATEST" != "$CURRENT" ]; then
sona update --json | jq .
fiAudit what uninstall would touch
sona uninstall --dry-run --json | jq '.result.paths[]'
# → "/Users/me/.sona"
# → "/Users/me/.local/bin/sona"
# → "/Users/me/.local/share/sona"Read config state without triggering the wizard
sona init --json | jq '{exists: .result.configExists, path: .result.configPath}'
# → { "exists": true, "path": "/Users/me/.sona/sona.yml" }init --json is a query. The interactive wizard is never triggered, and only configExists and configPath are reported. To bootstrap from scratch, fall back to interactive sona init and let the operator walk the prompts. init --add-channel --json is rejected with interactive_required for the same reason: the channel walkthrough is multi-prompt and doesn't translate to a single envelope.
Destructive commands and --json
sona uninstall is destructive. In --json mode, the prompt would be silently skipped, so Sona refuses rather than allow that. --json alone returns confirmation_required (exit 1):
{ "ok": false, "command": "uninstall", "error": { "code": "confirmation_required", "message": "..." } }Pass --yes explicitly to opt in:
sona uninstall --yes --json--dry-run --json is always safe (no filesystem touch) and does not need --yes.
Streaming vs. one-shot
This page covers one-shot commands. Streaming commands (sona chat --json) emit ndJSON: one envelope per chunk, terminated by a done: true record. See the CLI channel page for the per-chunk shape and a worked example.
Stability
ok,command,result/errorkeys: stable. Renaming would break every orchestrator. Treated as semver-major.error.codevalues: stable within a minor release. New codes may appear in minor releases, but existing codes never change spelling.resultpayload keys per command: additive. New keys may appear, and existing keys keep their meaning.error.messagetext: unstable. It is written for humans, so never match against it.
See Sona how-it-works for the architecture context, or the TnsAI.Sona release notes for the per-version changelog.