The CLI is built to be scripted: --json gives a single JSON object on stdout, errors are a JSON envelope on stderr, and exit codes are a stable contract.
Branch on exit codes, parse only JSON
#!/usr/bin/env bash
set -u
caesar-search search "zero-downtime postgres schema migration" \
--format compact --json -o /tmp/results.json
code=$?
case "$code" in
0) jq -r '.results[].doc_id' /tmp/results.json ;;
3) echo "auth error: set CAESAR_API_KEY or run caesar-search auth login" >&2; exit 1 ;;
4) echo "API error after retries; see the stderr envelope" >&2; exit 1 ;;
5) echo "timeout: retry or raise --timeout" >&2; exit 1 ;;
*) echo "bad input (exit $code): fix the command" >&2; exit 1 ;;
esac
This example bakes in the three scripting rules:
--json on every call. Human output is not a stable interface — its wording and layout can change between releases. Errors arrive on stderr as {"error": {"code": "...", "message": "...", "hint": "..."}} (hint only when present); all fields are snake_case.
- Exit codes are the contract, not output text:
0 success, 2 bad input, 3 auth, 4 API error, 5 timeout. The full table is on CLI usage.
-o for artifact capture. It writes the payload to the file and suppresses stdout entirely, so nothing competes with the file and output limits can’t truncate JSON mid-parse. Upload the file as a CI artifact, parse it with jq.
Configuration precedence
Resolution order, highest first:
| Setting | Order |
|---|
| API key | --key flag → CAESAR_API_KEY env → config file api_key → anonymous (lower rate limit) |
| Base URL | --base-url flag → CAESAR_BASE_URL env → config file base_url → default https://search-api-staging-779189860552.europe-west1.run.app |
The general rule is flag over env over config file over built-in default. In CI, prefer a masked CAESAR_API_KEY secret over the config file — nothing to provision on disk, and caesar-search auth status --json will confirm key_source is env.
Let one layer own retries
By default the CLI retries HTTP 429 and 5xx up to 3 times (4 attempts total) with exponential backoff capped at 8 seconds, honoring a Retry-After header when present. If your scheduler or workflow engine already handles retries, disable the CLI’s so attempts aren’t multiplied:
caesar-search search "site reliability postmortem templates" --json --no-retry
With --no-retry, a 429 or 5xx fails immediately with exit 4 and the error envelope on stderr — your outer retry policy takes it from there.
The api escape hatch
caesar-search api <method> <path> makes an authenticated raw call against the API — same key resolution, retries, and timeout — for endpoints the CLI doesn’t wrap:
echo '{"query":"hello","max_results":3}' | caesar-search api POST /v1/search --input -
caesar-search api GET /skills
Methods: GET, POST, PUT, PATCH, DELETE. The path must start with /. --input <file> (or --input - for stdin) supplies a JSON body. Output is always JSON — api ignores --json — and still honors -o. The endpoint catalog lives in the API reference.
Pin versions; check for drift
Auto-updating mid-pipeline makes builds unreproducible. Instead:
- Pin the CLI in CI:
npm install -g caesar-search-cli@0.1.4, or fetch a specific release archive and verify it against checksums.txt.
- Detect drift with the read-only check and surface it — don’t act on it automatically:
caesar-search update --check --json
# {"current": "0.1.4", "latest": "0.1.5", "update_available": true, "channel": "npm"}
- Upgrade deliberately with
caesar-search update; it picks the right channel (npm, brew, standalone). Never npm update -g or brew upgrade by hand.
update --check works on every channel, including source (dev) builds. Only the actual update refuses on dev builds (exit 2).
For agents
- Always pass
--json; parse stdout only. Errors are a JSON envelope on stderr.
- Branch on exit codes (
0/2/3/4/5), never on output text.
- Prefer
-o /tmp/<name>.json then cat the file — -o suppresses stdout so harness output limits can’t truncate JSON.
- Exit 3 means auth: set
CAESAR_API_KEY or run caesar-search auth login. Anonymous still works at a lower rate limit, so don’t block on it.
- Pass
--no-retry only when your own loop handles retries; otherwise let the CLI back off.
Full common-mistakes table: CLI usage.