incur is a TypeScript framework for building CLIs that work for both AI agents and humans. Use when creating new CLIs.
TypeScript framework for building CLIs for agents and human consumption. Strictly typed schemas for arguments and options, structured output envelopes, auto-generated skill files, and agent discovery via Skills, MCP, and --llms.
npm i incur
pnpm i incur
bun i incur
import { Cli, z } from 'incur'
const cli = Cli.create('greet', {
description: 'A greeting CLI',
args: z.object({
name: z.string().describe('Name to greet'),
}),
run({ args }) {
return { message: `hello ${args.name}` }
},
})
cli.serve()
greet world
# → message: hello world
Cli.create() is the entry point. It has two modes:
Pass run to create a CLI with no subcommands:
const cli = Cli.create('tool', {
description: 'Does one thing',
args: z.object({ file: z.string() }),
run({ args, options }) {
return { processed: args.file }
},
})
Omit run to create a CLI that registers subcommands via .command():
const cli = Cli.create('gh', {
version: '1.0.0',
description: 'GitHub CLI',
})
cli.command('status', {
description: 'Show repo status',
run() {
return { clean: true }
},
})
cli.serve()
cli.command('install', {
description: 'Install a package',
args: z.object({
package: z.string().optional().describe('Package name'),
}),
options: z.object({
saveDev: z.boolean().optional().describe('Save as dev dependency'),
global: z.boolean().optional().describe('Install globally'),
}),
alias: { saveDev: 'D', global: 'g' },
output: z.object({
added: z.number(),
packages: z.number(),
}),
examples: [
{ args: { package: 'express' }, description: 'Install a package' },
{
args: { package: 'vitest' },
options: { saveDev: true },
description: 'Install as dev dependency',
},
],
run({ args, options }) {
return { added: 1, packages: 451 }
},
})
.command() is chainable — it returns the CLI instance:
cli
.command('ping', { run: () => ({ pong: true }) })
.command('version', { run: () => ({ version: '1.0.0' }) })
Create a sub-CLI and mount it as a command group:
const cli = Cli.create('gh', { description: 'GitHub CLI' })
const pr = Cli.create('pr', { description: 'Pull request commands' })
pr.command('list', {
description: 'List pull requests',
options: z.object({
state: z.enum(['open', 'closed', 'all']).default('open'),
}),
run({ options }) {
return { prs: [], state: options.state }
},
})
pr.command('view', {
description: 'View a pull request',
args: z.object({ number: z.number() }),
run({ args }) {
return { number: args.number, title: 'Fix bug' }
},
})
// Mount onto the parent CLI
cli.command(pr)
cli.serve()
gh pr list --state closed
gh pr view 42
Groups nest arbitrarily:
const cli = Cli.create('gh', { description: 'GitHub CLI' })
const pr = Cli.create('pr', { description: 'Pull requests' })
const review = Cli.create('review', { description: 'Review commands' })
review.command('approve', { run: () => ({ approved: true }) })
pr.command(review)
cli.command(pr)
// → gh pr review approve
All schemas use Zod. Arguments are positional (assigned by schema key order). Options are named flags.