Skip to content
/ incur Public

CLI framework for agents and humans

License

Notifications You must be signed in to change notification settings

wevm/incur

Repository files navigation

incur

Features · Quickprompt · Install · Usage · Walkthrough · License

Features

  • Agent discovery: built-in Skills and MCP sync (skills add, mcp add) so agents find your CLI automatically
  • Session savings: up to 3× fewer tokens per session vs. MCP or skill alternatives
  • Call-to-actions: suggest next commands to agents and humans after a run
  • TOON output: token-efficient default format that agents parse easily, with JSON, YAML, Markdown, and JSONL alternatives
  • --llms flag: token-efficient command manifest in Markdown or JSON schema
  • Well-formed I/O: Schemas schemas for arguments, options, environment variables, and output
  • Inferred types: generic type flow from schemas to run callbacks with zero manual annotations
  • Global options: --format, --json, --verbose, --help, --version on every CLI for free
  • Light API surface: Cli.create(), .command(), .serve() – that's it

Quickprompt

Prompt your agent:

Skills (recommended – lighter on tokens)

Run `npx incur skills add`, then show me how to build CLIs with incur.

MCP

Run `npx incur mcp add`, then show me how to build CLIs with incur.

Install

npm i incur
pnpm i incur
bun i incur

Usage

Single-command CLI

Pass run directly to Cli.create() for CLIs that do one thing.

import { Cli, z } from 'incur'

Cli.create('greet', {
  description: 'A greeting CLI',
  args: z.object({
    name: z.string().describe('Name to greet'),
  }),
  run({ args }) {
    return { message: `hello ${args.name}` }
  },
}).serve()
$ greet world
# → message: hello world
$ greet --help
# greet – A greeting CLI
#
# Usage: greet <name>
#
# Arguments:
#   name  Name to greet
#
# Built-in Commands:
#   mcp add     Register as an MCP server
#   skills add  Sync skill files to your agent
#
# Global Options:
#   --format <toon|json|yaml|md|jsonl>  Output format
#   --help                              Show help
#   --llms                              Print LLM-readable manifest
#   --mcp                               Start as MCP stdio server
#   --verbose                           Show full output envelope
#   --version                           Show version

Multi-command CLI

Chain .command() calls to register subcommands.

import { Cli, z } from 'incur'

Cli.create('my-cli', {
  description: 'My CLI',
})
  .command('status', {
    description: 'Show repo status',
    run() {
      return { clean: true }
    },
  })
  .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'),
    }),
    alias: { saveDev: 'D' },
    run({ args }) {
      return { added: 1, packages: 451 }
    },
  })
  .serve()
$ my-cli status
# → clean: true

$ my-cli install express -D
# → added: 1
# → packages: 451
$ my-cli --help
# my-cli – My CLI
#
# Usage: my-cli <command>
#
# Commands:
#   install  Install a package
#   status   Show repo status
#
# Built-in Commands:
#   mcp add     Register as an MCP server
#   skills add  Sync skill files to your agent
#
# Global Options:
#   --format <toon|json|yaml|md|jsonl>  Output format
#   --help                              Show help
#   --llms                              Print LLM-readable manifest
#   --mcp                               Start as MCP stdio server
#   --verbose                           Show full output envelope
#   --version                           Show version

Sub-command CLI

Create a separate Cli and mount it with .command(cli) to nest command groups.

const cli = Cli.create('my-cli', { description: 'My CLI' })

// Create a `pr` group.
const pr = Cli.create('pr', { description: 'Pull request commands' }).command('list', {
  description: 'List pull requests',
  options: z.object({
    state: z.enum(['open', 'closed', 'all']).default('open'),
  }),
  run({ options }) {
    return { prs: [], state: options.state }
  },
})

cli
  .command(pr) // Link the `pr` group.
  .serve()
$ my-cli pr list --state closed
# → prs: (empty)
# → state: closed
$ my-cli --help
# my-cli – My CLI
#
# Usage: my-cli <command>
#
# Commands:
#   pr  Pull request commands
#
# Built-in Commands:
#   mcp add     Register as an MCP server
#   skills add  Sync skill files to your agent
#
# Global Options:
#   --format <toon|json|yaml|md|jsonl>  Output format
#   --help                              Show help
#   --llms                              Print LLM-readable manifest
#   --mcp                               Start as MCP stdio server
#   --verbose                           Show full output envelope
#   --version                           Show version

Walkthrough

Agent discovery

Agents can only use your CLI if they know it exists. incur solves this with three built-in discovery mechanisms – no manual config, no copy-pasting tool definitions:

# Auto-generate and install agent skill files (recommended – lighter on tokens)
my-cli skills add

# Register as an MCP server for your agents
my-cli mcp add

# Output machine-readable manifest
my-cli --llms

Session savings

Most CLIs expose tools via MCP or a single monolithic skill file. incur combines on-demand skill loading with TOON output to cut token usage across the entire session – from discovery through invocation and response.

The table below models a session with a 20-command CLI producing verbose output.

  • Session start – tokens consumed just by having the tool available. MCP injects all tool schemas into every turn; skills only load frontmatter (name + description).
  • Discovery – tokens to learn what commands exist and how to call them. MCP gets this at session start; skills load the full skill file on demand; incur splits by command group so only relevant commands are loaded.
  • Invocation (×5) – tokens per tool call.
  • Response (×5) – tokens in CLI output. MCP and skills return JSON; incur defaults to TOON which strips braces, quotes, and keys.
┌─────────────────┬────────────┬──────────────────┬─────────┬───────────────┐
│                 │ MCP + JSON │ One Skill + JSON │   incur │ vs. incur     │
├─────────────────┼────────────┼──────────────────┼─────────┼───────────────┤
│ Session start   │      6,747 │              624 │     805 │         ↓8.4× │
│ Discovery       │          0 │           11,489 │     387 │        ↓29.7× │
│ Invocation (×5) │        110 │               65 │      65 │         ↓1.7× │
│ Response (×5)   │     10,940 │           10,800 │   5,790 │         ↓1.9× │
├─────────────────┼────────────┼──────────────────┼─────────┼───────────────┤
│ Cost            │    $0.0325 │          $0.0410 │ $0.0131 │         ↓3.1× │
└─────────────────┴────────────┴──────────────────┴─────────┴───────────────┘

Call-to-actions

Without CTAs, agents have to guess what to do next or ask the user. With CTAs, your CLI tells the agent exactly which commands are relevant after each run, so it can chain operations without extra prompting.

Return CTAs from ok() or error() to suggest next steps. cta parameters are also fully type-inferred, so agents get valid command names, arguments, and options for free.

cli.command('list', {
  args: z.object({ state: z.enum(['open', 'closed']).default('open') }),
  run({ args, ok }) {
    const items = [{ id: 1, title: 'Fix bug' }]
    return ok({ items }, {
      cta: {
        commands: [
          { command: 'get 1', description: 'View item' },
          { command: 'list', args: { state: 'closed' }, description: 'View closed' },
        ],
      },
    })
  },
})
$ my-cli list
# → items:
# →   - id: 1
# →     title: Fix bug
# Next:
#   my-cli get 1 – View item
#   my-cli list closed – View closed

Light API surface

A small API means agents can build entire CLIs in a single pass without needing to learn framework abstractions. Three functions: create, command, serve, and everything else (parsing, help, validation, output formatting, agent discovery) is handled automatically:

import { Cli, z } from 'incur'

// Define sub-command groups
const db = Cli.create('db', { description: 'Database commands' })
  .command('migrate', { description: 'Run migrations', run: () => ({ migrated: true }) })

// Create the root CLI
Cli.create('tool', { description: 'A tool' })
  // Register commands
  .command('run', { description: 'Run a task', run: () => ({ ok: true }) })
  // Mount sub-command groups
  .command(db)    
  // Serve the CLI
  .serve()       
$ tool --help
# Usage: tool <command>
#
# Commands:
#   run  Run a task
#   db   Database commands

TOON output

Every token an agent spends reading CLI output is a token it can’t spend reasoning. incur defaults to TOON – a format that’s as readable as YAML but with no quoting, no braces, and no redundant syntax. Agents parse it easily and use up to 60% fewer tokens compared to JSON.

$ my-cli hikes --location Boulder --season spring_2025
# → context:
# →   task: Our favorite hikes together
# →   location: Boulder
# →   season: spring_2025
# → friends[3]: ana,luis,sam
# → hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}:
# →   1,Blue Lake Trail,7.5,320,ana,true
# →   2,Ridge Overlook,9.2,540,luis,false
# →   3,Wildflower Loop,5.1,180,sam,true

Switch formats with --format or --json:

$ my-cli status --format json
# → {
# →   "context": {
# →     "task": "Our favorite hikes together",
# →     "location": "Boulder",
# →     "season": "spring_2025"
# →   },
# →   "friends": ["ana", "luis", "sam"],
# →   "hikes": [
# →   ... + 1000 more tokens
# → ]
# → }

Supported formats: toon, json, yaml, md, jsonl.

Well-formed I/O

Agents fail when they guess at argument formats or misinterpret output structure. incur eliminates this by declaring schemas for arguments, options, environment variables, and output – every input is validated before run executes, and every output has a known shape that agents can rely on without parsing heuristics:

cli.command('deploy', {
  args: z.object({ env: z.enum(['staging', 'production']) }),
  options: z.object({ force: z.boolean().optional() }),
  env: z.object({ DEPLOY_TOKEN: z.string() }),
  output: z.object({ url: z.string(), duration: z.number() }),
  run({ args, options, env }) {
    return { url: `https://${args.env}.example.com`, duration: 3.2 }
  },
})

Streaming

Use async *run to stream chunks incrementally. Yield objects for structured data or plain strings for text:

cli.command('logs', {
  description: 'Tail logs',
  async *run() {
    yield 'connecting...'
    yield 'streaming logs'
    yield 'done'
  },
})
$ my-cli logs
# → connecting...
# → streaming logs
# → done

Each yielded value is written as a line in human/TOON mode. With --format jsonl, each chunk becomes {"type":"chunk","data":"..."}. You can also yield objects:

async *run() {
  yield { progress: 50 }
  yield { progress: 100 }
}

Use ok() or error() as the return value to attach CTAs or signal failure:

async *run({ ok }) {
  yield { step: 1 }
  yield { step: 2 }
  return ok(undefined, { cta: { commands: ['status'] } })
}

Inferred types

Type safety isn’t just for humans – agents building CLIs with incur get immediate feedback when they pass the wrong argument type or return the wrong shape. Schemas flow through generics so run callbacks, output, and cta commands are all fully inferred with zero manual annotations:

cli.command('greet', {
  args: z.object({ name: z.string() }),
  options: z.object({ loud: z.boolean().default(false) }),
  output: z.object({ message: z.string() }),
  run({ args, options, ok }) {
    args.name
    //   ^? (property) name: string
    options.loud
    //      ^? (property) loud: boolean
    return ok({ message: `hello ${args.name}` }, {  
    //          ^? (property) message: string
      cta: { commands: ['greet world'] },
    //                   ^? 'greet' | 'other-cmd'
    })
  },
})

Global options

Every incur CLI includes these flags automatically:

Flag Description
--help, -h Show help for the CLI or a specific command
--version Print CLI version
--llms Output agent-readable command manifest
--mcp Start as an MCP stdio server
--json Shorthand for --format json
--format <fmt> Output format: toon, json, yaml, md
--verbose Include full envelope (ok, data, meta)

API Reference

TODO

License

MIT

About

CLI framework for agents and humans

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 3

  •  
  •  
  •