Skip to content

@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 help lines after the result;
  • an error with a stable code the 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.

Terminal window
npm install @clibuilder/axi clibuilder

clibuilder ^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).

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)
Terminal window
$ my-cli list
count: 30 of 847 total
tasks[30]{id,title}:
...
help[1]: Run `my-cli list --full` to see every task
$ my-cli show 9999
error: no task 9999
code: not-found
help[1]: Run `my-cli list` to see the open tasks

output.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).

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.

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.

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
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
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.

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