@clibuilder/axi
@clibuilder/axi adds agent-facing output conventions to a clibuilder CLI. It follows
AXI, a set of design principles for CLIs that agents call.
clibuilder already sets the exit codes. This package covers what the caller reads on stdout:
- the result, encoded in the format the caller asked for;
- the next steps, as
helplines after the result; - an error with a stable
codethe caller can branch on; - a count that says how much was left out of a long answer.
There are three formats. toon is the default: TOON, a
compact encoding that costs an agent fewer tokens to read. json is compact JSON, for a pipe into
jq. text is an aligned report for a person.
The rest of this page describes the package for clibuilder 11, with 0.2.1.
Install
Section titled “Install”npm install @clibuilder/axi clibuilderclibuilder ^11.5.0 is a peer dependency of 0.2.1. Version 0.3.0 needs clibuilder ^12. Node.js 20.19 or newer is required.
0.2.1 publishes ESM and CJS. 0.3.0 is ESM-only; require('@clibuilder/axi') still works through
Node’s require(esm).
Example
Section titled “Example”Two commands: list cuts a long list and offers --full, and show reports a missing task as an
error on stdout.
import { createOutput, formatOption, fullOption, orEmpty, truncateList } from '@clibuilder/axi'import { cli, command } from 'clibuilder'import { loadTasks } from './tasks.js' // your own logic
const listCommand = command({ name: 'list', description: 'List the open tasks', options: { format: formatOption, full: fullOption }, async run(args) { const tasks = await loadTasks(this.cwd) const { items, count, truncated } = truncateList(tasks, { limit: 30, full: args.full }) return createOutput(args.format).result( { count, tasks: orEmpty(items, '0 open tasks found') }, { help: truncated ? 'Run `my-cli list --full` to see every task' : undefined } ) }})
const showCommand = command({ name: 'show', description: 'Show one task', arguments: [{ name: 'id', description: 'task id' }], options: { format: formatOption }, async run(args) { const output = createOutput(args.format) const task = (await loadTasks(this.cwd)).find((t) => String(t.id) === args.id) if (!task) { throw output.error({ message: `no task ${args.id}`, code: 'not-found', help: 'Run `my-cli list` to see the open tasks' }) } return output.result(task) }})
cli({ name: 'my-cli', version: '1.0.0' }) .command(listCommand) .command(showCommand) .parse(process.argv)$ my-cli listcount: 30 of 847 totaltasks[30]{id,title}: ...help[1]: Run `my-cli list --full` to see every task
$ my-cli show 9999error: no task 9999code: not-foundhelp[1]: Run `my-cli list` to see the open tasksoutput.error() writes the error on stdout and returns a CliError to throw. Its exit code is 1
unless the report sets another.
args.format is typed 'toon' | 'json' | 'text' | undefined. clibuilder 11 rejects any other
value as a usage error before run() is called. In clibuilder 12 the option conflicts with the
global --format (see below).
The global --format option
Section titled “The global --format option”Exports
Section titled “Exports”Output
Section titled “Output”| Export | Purpose |
|---|---|
createOutput(format, { stdout? }) |
Binds a format. Returns result(value, { help?, toon?, json?, text? }), document(content) and error(report) |
encodeResult(value, format) |
Encodes a value without writing it |
writeResult(value, format, stdout?) |
Encodes a value and writes it with a trailing newline |
writeError(report, format, stdout?) |
Writes an error on stdout and returns its exit code |
writeDocument(content, stdout?) |
Writes content verbatim, ending on a newline. Use it for a Markdown body or a diff that is already the answer |
result() returns the value it wrote, so a test can assert on the command’s return value. Pass a
stdout writer to createOutput to capture the output instead.
Errors and usage errors
Section titled “Errors and usage errors”| Export | Purpose |
|---|---|
ErrorReport |
{ message, code, help?, details?, exitCode? } |
errorOutput(report) |
The error as a value: error, code, the details and help |
encodeError(report, format) |
Encodes an error without writing it |
createUsageErrorHandler({ format?, stdout? }) |
An onUsageError handler that writes each usage error as a structured error, with exit code 2 |
describeUsageError(error, command) |
One usage error as { code, message, help } |
import { createUsageErrorHandler } from '@clibuilder/axi'import { cli } from 'clibuilder'
cli({ name: 'my-cli', version: '1.0.0', onUsageError: createUsageErrorHandler() })The handler runs before --format is parsed, so it writes toon unless you build it with another
format.
Format
Section titled “Format”| Export | Purpose |
|---|---|
OutputFormat |
'toon' | 'json' | 'text' |
formats |
['toon', 'json', 'text'], the default first |
formatOption |
The --format option: choices toon, json and text, default toon |
defineFormatOption({ default?, description? }) |
A --format option with another default or description |
parseFormat(value) |
Validates a format string that did not come from argv. Throws on an unknown one |
renderText(value) |
The text rendering of a value |
Help lines
Section titled “Help lines”| Export | Purpose |
|---|---|
helpLines(help) |
Turns a string or a list of next steps into a list |
withHelp(value, help) |
Adds the next steps to a value under help |
Value helpers
Section titled “Value helpers”| Export | Purpose |
|---|---|
truncateText(text, { limit?, full? }) |
Cuts a text to limit characters and ends it with ... (truncated, N chars total) |
defaultTextLimit |
The default limit of truncateText: 500 |
truncateList(items, { limit, full? }) |
Keeps limit items and returns count as N of M total |
fullOption |
The --full option, which turns truncation off |
orEmpty(items, message) |
The message instead of an empty list |
homeHeader({ description, bin?, home? }) |
bin and description, for what the CLI shows when run with no arguments |
collapseHome(path, home?) |
Shortens a path under the home directory to ~/... |
The package also re-exports exitCodes from clibuilder. The
package README has more
examples, and docs/findings
records where each convention came from.
Moving to clibuilder 12
Section titled “Moving to clibuilder 12”Each export maps to a clibuilder 12 feature. Install clibuilder 12 and delete @clibuilder/axi:
@clibuilder/axi |
clibuilder 12 |
|---|---|
truncateText, truncateList, defaultTextLimit, fullOption, orEmpty, homeHeader, collapseHome |
The same names from clibuilder/axi. fullOption is declared with t.boolean().optional() |
createOutput(format).result(value, { help }), writeResult, encodeResult, withHelp, helpLines |
Return the value, or result(value, { help, render }), from run() |
output.document(content), writeDocument |
Return document(content) from run() |
output.error(report), writeError, encodeError, errorOutput, ErrorReport |
Throw a CliError with code, help and details |
formatOption, defineFormatOption, parseFormat, formats, OutputFormat |
The global --format: delete the option, read this.format, set the default with cli({ format }); the type is cli.Format |
renderText |
--format text, or result(value, { render: { text } }) |
createUsageErrorHandler, describeUsageError |
Built in: ranked, coded usage errors. Remove the onUsageError handler |
exitCodes |
exitCodes from clibuilder or clibuilder/axi |