Melious

The contract between the melious CLI and your scripts: which commands print JSON, where each kind of output goes, what exit status means, and what works without a terminal.

Signature:

melious <command> --json

Example. A CI step that checks the key connects before sending a prompt. MELIOUS_API_KEY comes from your CI secret store:

set -euo pipefail
melious ping --json | jq -e '.connected' > /dev/null
melious run --json "Name three Hanseatic cities" > response.json

ping --json exits 0 even when it can't connect, so the script checks the connected field, not only the exit status. That pattern — check both — runs through this whole page.

JSON support

--json gives one machine-readable result on a small set of commands. Every other command refuses it.

Command--json
runSupported — the full chat completion response
decideSupported — the System One response unchanged
transcribeSupported — the same as --format json; refused with -o or another --format
rerank, embedSupported — the API response (merged across requests for embed)
imageSupported — the model and the saved file paths
pingSupported
config show, config getSupported
version, --versionSupported
auth statusSupported
trust list, add, remove, pruneSupported
Everything elseRefused

"Everything else" means bare melious, setup, launch, decide batch (its output is already JSONL), models, models info, tools and all of its subcommands, auth login, auth logout, config set, config edit, config reset, update (including --check), uninstall, completion, help, and the auth, config, and trust groups on their own.

We'd rather refuse the flag than print something that looks like JSON and isn't. A refusal:

  • exits nonzero, with empty stdout
  • prints one line on stderr saying why, ending in drop --json
  • happens before the command prompts, writes, or contacts anything — it doesn't even create ~/.melious

Two things still come first. A malformed invocation (a missing argument, an unknown command) is reported as that, and --help always prints help.

JSON shapes

Shapes are per command, not one shared envelope. For auth status and the trust commands, keys that don't apply are null rather than missing, so you can index them without probing. The ping shapes are described below.

auth status

{
  "configured": true,
  "source": "environment",
  "key_masked": "sk-mel-****************1234",
  "authenticated_at": null,
  "path": null,
  "problem": null
}

source is flag, environment, file, or none. authenticated_at and path are set only when the key comes from the credentials file. problem says why Melious didn't use a credentials file it found — for example, permissions wider than 0600. It is set only when no flag or environment key answered and the file was read; otherwise it is null. The key itself never appears, only the masked form.

ping

Without a configured key:

{
  "connected": false,
  "error": "not authenticated"
}

With a key, the result has connected, latency_ms, models (how many the API lists), and preset (your default routing preset). error is added when the connection fails.

{
  "connected": true,
  "latency_ms": 92,
  "models": 54,
  "preset": "balanced"
}

version

{
  "arch": "amd64",
  "build_date": "<BUILD_DATE>",
  "commit": "<COMMIT>",
  "go_version": "<GO_VERSION>",
  "os": "linux",
  "version": "<VERSION>"
}

melious --version --json prints the same object.

config get

The value, encoded as JSON — a string, number, boolean, or, for a group such as run, an object:

melious config get models.main --json
# "glm-5.2"
melious config get run --json
# {"max_tokens":4096,"stream":true,"system_prompt":"","temperature":0.7}

config show --json prints the whole resolved configuration as one object. The keys are on the configuration page.

trust

trust list --json prints an array, [] when nothing is trusted:

[
  {
    "id": "<WORKSPACE_ID>",
    "root": "<PROJECT_PATH>",
    "trusted_at": "2026-09-24T09:05:37.301106523Z",
    "damaged": false,
    "error": null
  }
]

A record Melious can't read has an empty root, null trusted_at, and the reason in error. damaged is true when the record's contents are corrupt, which trust prune can remove.

The other trust commands print one object each:

{ "root": "<PROJECT_PATH>", "id": "<WORKSPACE_ID>" }
{ "target": "<PROJECT_PATH>" }
{ "removed": [{ "id": "<WORKSPACE_ID>", "root": "" }] }

Those are trust add, trust remove (echoing the argument as you typed it), and trust prune. What trust records mean is on the workspace trust page.

Output and exit status

Results go to stdout. Diagnostics, warnings, and notices go to stderr. Under --json, stdout holds one JSON value or nothing.

Exit status is 0 on success and nonzero on failure, with a few cases that trip scripts up:

CommandPlain outputWith --json
auth statusNonzero when no key is configuredAlways 0 — read configured
pingNonzero without a key or connection0 — read connected
trust pruneNonzero if a record was left behindMay print removed and still exit nonzero
decide --threshold3 when the noul answer is below the thresholdSame
decide batch1 if any line failed, 130 after ctrl+cRefused
A refused --jsonNot applicableNonzero, empty stdout

trust prune reports the records it did remove even when others failed, because those are gone either way. If it removed nothing and failed, stdout is empty. Check the exit status and the fields together.

--quiet isn't a format

--quiet trims decoration: symbols, headers, next-step hints. It doesn't make output uniform across commands, and it never suppresses a launched agent's output or the workspace trust question. A few commands print something easy to parse under it — auth status --quiet prints one field=value line per fact that applies, and trust list --quiet prints one trusted path per line:

configured=true
source=environment
key_masked=sk-mel-****************1234

The set of lines varies with what's true. When you need a fixed shape, use --json.

Startup behavior

What melious does with no command depends on flags, whether you're signed in, and whether it has a terminal.

InvocationIn a terminalWithout a terminal
melious, no keyOpens setup, then the launcherError with a remedy
melious, key configuredOpens the launcherError with a remedy
melious setupReopens setup, ends at the shellError with a remedy
melious --no-color or NO_COLORStatic summaryStatic summary
melious --quietHelpHelp
melious --jsonRefusedRefused

The no-terminal error names the way out: melious --no-color for the static summary, and melious auth login --stdin to sign in. The static summary shows your version and sign-in state, and with a key it checks the connection first. --version beats every other flag, and --quiet beats --no-color.

Decisions in scripts

melious decide answers typed questions about a state — a ticket, a log line, a record — with a decision model. Two flags turn the answer into something a shell can branch on.

--pick <NAME> prints only one answer and a newline: the label for a choice, the nearest level for a score, and the number (up to 4 significant digits) for a noul. It can't be combined with --json.

resolution=$(melious decide --state-file ticket.txt --spec refund --pick resolution)

--threshold <NAME>=<X> turns a noul answer into an exit status: 0 when the value is <X> or more, and 3 when it's less. The output doesn't change. 3 means "below the threshold" and nothing else, so it can't be confused with an error, which exits 1:

set +e
melious decide --state-file ticket.txt --spec refund --threshold escalate=0.5 > /dev/null
status=$?
set -e
case $status in
  0) echo "escalate to a human" ;;
  3) echo "close automatically" ;;
  *) echo "decide failed" >&2; exit 1 ;;
esac

--threshold works on noul questions only, and <X> must be between 0 and 1. It works alongside --pick and --json. For the whole response, use --json; it prints the API body unchanged.

Without --pick or --json, stdout holds the answers as bars plus a footer line with the tokens, duration, model, and request ID. --quiet drops the footer, and also the stderr note Melious prints when it picks the only decision model for you.

The state is the argument or --state-file. Melious reads stdin only when you give neither, so a while read loop works as written:

while read -r ticket; do
  melious decide "$ticket" --spec refund --pick resolution
done < tickets.txt

Without --model, decide uses the spec's model, then models.decision. In a script, set one of them — when your key can use more than one decision model, decide stops with an error that lists them instead of asking. Configuration has the full order.

An API error prints <status> <code> · <param> · <message> (request: <id>) on stderr and exits 1.

Retryable errors — 429, 502, 503, 504, a network error, and 409 request_in_progress — are sent again, up to 3 attempts in total. Which idempotency key the retry carries depends on what went wrong:

  • After a 429, a 503, or another error response from the API, the request failed and wasn't charged. The API remembers that failure under its key and answers every later use of the key with 409 idempotency_failed, so Melious retries under a new key: <KEY>-r1, then <KEY>-r2.
  • After a 502, a 504, a network error, or 409 request_in_progress, the first request may have run, or still be running. Melious sends the same key again, and if it ran, you get its stored answer rather than a second charge. If it had failed, the API answers 409 idempotency_failed, and Melious moves on to the next key.

Moving to a new key after 409 idempotency_failed doesn't use up an attempt. So a key that failed in an earlier run — your own --idempotency-key, or a batch line's key — goes on to <KEY>-r1, <KEY>-r2 and so on instead of stopping. Every run without --idempotency-key gets a fresh random key.

Batch decisions

melious decide batch sends one decision request per line of a JSONL file and writes one result line per input line. There's no server-side batch for decisions — /v1/batches doesn't accept them — so the CLI runs the requests from your machine, 4 at a time by default.

melious decide batch tickets.jsonl --spec refund -o results.jsonl

Input

Each line is a JSON object. Empty lines are ignored.

FieldRequiredMeaning
stateYesThe context for this line, any JSON value
idNoA string or number, unique in the file. Defaults to line-<n>, counting from 1.
questionsNoQuestions that replace the spec questions with the same name, for this line only
{"id": "4128", "state": "Ticket #4128: customer requests a refund 11 days after purchase; the order has shipped."}
{"id": "4129", "state": {"ticket": 4129, "text": "Package arrived damaged."}}

Melious reads and checks every line before it sends anything. If a line isn't valid, it prints up to 10 errors with line numbers, sends nothing, and exits 1.

Output

Lines are written as they complete, so their order isn't the input order — add --sort to rewrite --output in input order when the run ends. Each line is written in one piece, so a stopped run never leaves half a line behind.

A success line:

{"id": "4128", "model": "<DECISION_MODEL_ID>", "answers": {"resolution": {"type": "choice", "choice": "credit", "probabilities": {"refund": 0.25, "credit": 0.7, "deny": 0.05}, "confidence": 0.7}}, "usage": {"input_tokens": 214}, "request_id": "<REQUEST_ID>"}

A failure line:

{"id": "4129", "error": {"status": 503, "code": "service_unavailable", "message": "<MESSAGE>", "param": null, "request_id": "<REQUEST_ID>"}}

answers has the same shape as in the System One response.

When stderr is a terminal, a progress line there shows done, failed, rate, and time left. It's redrawn in place and cleared when the run ends, and a summary goes to stderr instead. The progress line during a run, then the summary at its end:

412/1000 · 3 failed · 38/s · eta 0:15
Done: 997 ok, 3 failed · 214,302 input tokens · results.jsonl

failed is left out of the progress line while nothing has failed, and the file name is left out of the summary without --output. --quiet hides both. The exit status is 0 when every line succeeded and 1 when any line failed.

When more than 100 lines are about to go out from a file and stdin and stderr are both terminals, Melious asks first: Send 1,000 decisions to <MODEL_ID>?. With --resume, the count is the lines still to send. Answer no and it prints Nothing sent. and exits 0. --yes skips the question; input from stdin is never asked about.

Retries and stops

Each line gets up to 5 attempts on a retryable error, with the same key rules as decide: a new key (-r1, -r2, …) after a 429, a 503 or another API error, the same key after a 502, a 504 or a network error. A line whose key failed in an earlier run gets 409 idempotency_failed and goes again at once under the next key, without using up an attempt, so --resume can always finish a line once the API is healthy. A 429 pauses all workers for the time the API names in Retry-After. After the last attempt, the line gets a failure line and the run goes on. A failure without an HTTP response, such as a network error, has status 0.

Some errors stop the whole run: 401, 402, and the 403 codes auth_insufficient_scope, model_not_allowed, and inference_model_restricted. Melious starts no new lines and lets the running ones finish. It prints a Stopped: … summary and the error with a reminder to fix it and run the same command with --resume, and exits 1. Lines that never started get no output line, so --resume picks them up later.

ctrl+c works the same way: no new lines, up to 10 seconds for the running ones, then Stopped. Run the same command with --resume to continue. and exit status 130. --sort doesn't run after ctrl+c; pass it to the resumed run, and it sorts the whole file when that run ends.

Resume without paying twice

--resume skips every ID that already has a success line in --output and appends the rest. Lines that failed are sent again. A last line cut off by a crash is removed first, so the appended lines start clean.

melious decide batch tickets.jsonl --spec refund -o results.jsonl --resume

A resumed file can hold an old failure line and a new success line for the same ID. --sort cleans that up: it rewrites the file in input order and keeps the last line for each ID. Without it, read the file with the last line per ID winning.

Every line also carries an idempotency key built from the model ID, the request body, and the line ID. Run the same input with the same spec again within 24 hours — to a new output file, or with --resume — and the API returns the stored answers, with the same request IDs, instead of running and billing them again. Change the state, the questions, the model, or the ID, and it's a new request with a new charge. After 24 hours, the stored answers expire — see idempotency.

A failed request isn't charged and has no stored answer, so a line that failed is retried under its next key and is charged once, when it succeeds.

Running without a terminal

Anything that asks a question needs a terminal to ask on. In CI, cron, or a pipe, those commands exit nonzero and tell you what to do instead:

  • Bare melious and setup
  • auth login without --stdin
  • config reset and uninstall without --yes, and update without --yes when a newer release is available
  • tools install <TOOL> when Melious would run the install itself — it prints the command it would have run
  • launch in a workspace you haven't trusted
  • trust add on a workspace too broad to trust by default, such as your home directory, which needs a terminal to accept the risk

Model commands (decide, decide batch, transcribe, rerank, embed, image) don't ask anything when --model, a spec's model, or models.decision settles the choice, or when your key can use only one model of the type. When several models fit, they ask you to pick on a terminal (stdin and stderr) and, without one, stop with more than one <type> model is available (<IDS>); choose one with --model.

Unattended flags

These flags answer the question in advance. Know what each one does before you put it in a script:

CommandWhat it does unattended
config reset --yesReplaces config.json with {}. Credentials, trust, and agent configs stay.
update --yesDownloads the latest release and replaces the running binary.
uninstall --yesRemoves the binary and all of ~/.melious.
tools install --allRuns every automatic install action for missing agents. Agents that need a missing prerequisite are reported as blocked and skipped.

update downloads over HTTPS and smoke-tests the new binary, but it doesn't verify a checksum or signature. If there's no newer release, it exits 0 without asking.

uninstall --yes takes your stored credentials, trust records, generated agent configs, and config with it. It doesn't undo PATH or shell profile edits, remove the agents you installed, or unset MELIOUS_API_KEY.

tools install --all runs only the automatic actions (npm for Pi outside Windows, uv for Vibe on Windows). Agents whose prerequisite is missing are reported as blocked. Manual-only agents are listed with their instructions. It exits 1 if any install failed or any tool was blocked; manual-only and unavailable tools don't change the exit status. See Manage coding agents.

Safe credentials in CI

Set MELIOUS_API_KEY from your CI system's secret store. It outranks a stored credentials file and leaves nothing on disk. Commands that call the API read it.

If a job needs a stored file, pipe the key in with --stdin from the secret source rather than the command line:

melious auth login --stdin < <PATH_TO_KEY_FILE>

Don't pass a real key with --api-key. It works on every command except auth login, but a command line is visible to other processes and ends up in logs.

--trust-once is a different thing entirely: workspace consent, not authentication. It lets melious launch start an agent in an untrusted workspace for one launch without recording a grant, and it has nothing to do with your key. It still refuses locations too broad to trust, such as your home directory or a filesystem root. For scripted launches, see the workspace trust page.

Reproducible runs

With --quiet or --json, Melious skips two things it otherwise does after a command finishes: refreshing the cached remote defaults, and checking for a newer release (which prints the "update available" notice on stderr). The auth, trust, version, update, and uninstall commands skip both anyway.

The refresh can change models.main, models.secondary, models.lightweight, defaults.tool, and defaults.preset between runs, and it happens even without an API key. We don't send product telemetry, but that isn't the same as no network traffic. For runs that must behave the same every time, pin the values that matter in your config, or per command:

melious config set models.main <MODEL_ID>
melious run --model <MODEL_ID> --preset balanced "<PROMPT>"

config show labels each of those values as (default), (remote), or (your override), so you can see which ones are still following remote changes. melious config set autoupdate false turns off the update check without touching the refresh.

On this page