incur CLI Framework

repository·main·Indexed 20 days ago

https://github.com/wevm/incur

A CLI framework optimized for AI agentic workflows (version 0.4.26). It features built-in agent discovery via Skills and Model Context Protocol (MCP), token-efficient TOON output, and structured I/O using schemas. incur supports building standalone binaries for macOS, Linux, and Windows, mounting HTTP APIs or MCP endpoints as CLIs, and exposing CLIs as Fetch APIs.

Tokens
28.4K
Snippets
109
Records
128
Agent score
66%

What's inside incur

  1. Overview of incur features

    main

    incur is a CLI framework designed for agentic workflows with the following core features:

    • Agent discovery: Built-in Skills and MCP sync (skills add, mcp add) for automatic agent discovery.
    • Session savings: Optimized to use up to 3× fewer tokens per session compared to MCP or skill alternatives.
    • Call-to-actions: Suggests next commands to both agents and humans after a run.
    • TOON output: A token-efficient default output format optimized for agent parsing. Supports JSON, YAML, Markdown, and JSONL alternatives.
    • --llms flag: Provides a token-efficient command manifest in Markdown or JSON schema.
    • Well-formed I/O: Uses schemas for arguments, options, environment variables, and output.
    • Inferred types: Provides generic type flow from schemas to run callbacks without manual annotations.
    • Global options: Every CLI automatically includes --format, --full-output, --help, --json, --update, and --version.
    • Standalone binaries: Supports building macOS, Linux, and Windows executables.
    • Light API surface: Minimalist API consisting of Cli.create(), .command(), and .serve().
    • Middleware: Composable before/after hooks with typed dependency injection via cli.use().
  2. Use Call-to-Actions (CTAs) to guide agents

    main

    You can suggest the next logical commands to an agent by returning CTAs from ok() or error() calls. This allows agents to chain operations without extra prompting. CTAs are fully type-inferred, providing valid command names, arguments, and options automatically.

    cli.command('list', {
      args: z.object({ state: z.enum(['open', 'closed']).default('open') }),
      run(c) {
        const items = [{ id: 1, title: 'Fix bug' }]
        return c.ok(
          { items },
          {
            cta: {
              commands: [
                { command: 'get 1', description: 'View item' },
                { command: 'list', args: { state: 'closed' }, description: 'View closed' },
              ],
            },
          },
        )
      },
    })
  3. Handle TTY vs Agent execution context

    main

    incur automatically detects if the command is running in a TTY (human user) or a non-TTY environment (agent/pipe).

    • TTY (Human): Receives formatted data only, human-readable errors, and pretty help text.
    • Non-TTY (Agent): Receives the TOON envelope for data and an error envelope for failures.

    In your run function, you can check c.agent (a boolean) to adapt behavior, such as suppressing logs when an agent is consuming the output.

    cli.command('deploy', {
      run(c) {
        if (!c.agent) console.log('Deploying...')
        return { status: 'ok' }
      },
    })
  4. Use the full output envelope

    main

    By default, commands emit only the data block. To include metadata and status, use the --full-output flag. This returns a structured envelope containing ok (boolean), data (the command result), and meta (command name, duration, and pagination info).

    Example envelope structure:

    ok: true
    data:
      name: express
      version: 4.21.2
    meta:
      command: info
      duration: 12ms
    tool info express --full-output
  5. Inject typed dependencies using Vars

    main

    You can declare a vars schema on Cli.create() to implement typed dependency injection.

    • Declaration: Define the schema using z.object() in the create() options.
    • Setting: Use c.set(key, value) within middleware to inject values into the context.
    • Accessing: Access injected values via c.var.key within command handlers.
    • Defaults: Use .default() in the schema for variables that do not require middleware to initialize.

    This pattern is useful for injecting authentication state, request IDs, or configuration throughout your CLI commands.

    const cli = Cli.create('my-cli', {
      description: 'My CLI',
      vars: z.object({
        user: z.custom<{ id: string; name: string }>(),
        requestId: z.string(),
        debug: z.boolean().default(false),
      }),
    })
    
    cli.use(async (c, next) => {
      c.set('user', await authenticate())
      c.set('requestId', crypto.randomUUID())
      await next()
    })
    
    cli.command('whoami', {
      run(c) {
        return { user: c.var.user, requestId: c.var.requestId, debug: c.var.debug }
      },
    })
  6. Configure Arguments and Options with Zod

    main

    Arguments and options are defined using Zod schemas.

    • Arguments are positional and assigned based on the order of keys in the Zod object.
    • Options are named flags (e.g., --flag).

    Supported features include:

    • Aliases: Use the alias property to define short flags (e.g., { state: 's' }).
    • Types: Automatic coercion (string $\rightarrow$ number/boolean) and support for arrays (z.array()).
    • Defaults: Use .default() for option values.
    • Environment Variables: Define an env schema to validate and parse process.env values.
    // Arguments (positional)
    args: z.object({
      repo: z.string().describe('Repository in owner/repo format'),
      branch: z.string().optional().describe('Branch name'),
    })
    
    // Options (named flags)
    options: z.object({
      state: z.enum(['open', 'closed']).default('open'),
      limit: z.number().default(30),
      verbose: z.boolean().optional(),
    })
    
    // Environment Variables
    env: z.object({
      NPM_TOKEN: z.string().optional(),
    })
  7. Configure TOON and other output formats

    main

    Incur defaults to TOON output, a format designed for LLMs that is as readable as YAML but strips braces, quotes, and redundant syntax to save tokens. You can switch formats using the --format flag or the --json shorthand.

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

    # Default TOON output
    $ my-cli hikes --location Boulder --season spring_2025
    
    # Switch to JSON
    $ my-cli status --format json
  8. Limitations of standalone executable support

    main

    The standalone executable and binary release system has the following unsupported features:

    • Authentication: Private-repository authentication for Binary.github or the release action.
    • Versioning: Prerelease versions or named update channels.
    • Package Managers: Support for Homebrew, Scoop, Winget, or other third-party package managers.
    • Release Management: GitHub Release publication, retagging, or package version management.
    • Security: Signing identities, notarization credentials, secret storage, or signing services.
    • Automation: Automatic upload via the incur build command.
  9. Use middleware to intercept CLI execution

    main

    Middleware allows you to register composable before/after hooks using cli.use(). Middleware executes in registration order using an "onion-style" pattern: each middleware must call await next() to proceed to the next handler in the chain.

    There are three levels of middleware:

    1. Root Middleware: Registered on the main Cli instance via .use(). Runs for all commands.
    2. Sub-CLI Middleware: Registered on a sub-CLI instance. Only applies to commands within that sub-CLI.
    3. Per-command Middleware: Registered directly on a command definition. Runs only for that specific command and after root/group middleware.

    Note: Middleware does not run for built-in commands like --help, --llms, --mcp, mcp add, or skills add.

    const cli = Cli.create('deploy-cli', { description: 'Deploy tools' })
      .use(async (c, next) => {
        const start = Date.now()
        await next()
        console.log(`took ${Date.now() - start}ms`)
      })
      .command('deploy', {
        run() {
          return { deployed: true }
        },
      })
  10. Organize commands into Subcommand groups

    main

    You can nest commands by creating separate Cli instances and mounting them as commands on a parent CLI. This allows for arbitrary nesting (e.g., parent cmd group subcommand).

    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 }
      },
    })
    
    // Mount the sub-CLI onto the parent
    cli.command(pr)
    
    cli.serve()
  11. Generate shell completions

    main

    Every Incur CLI includes a completions command to generate shell hook scripts for dynamic tab completion. Completions are context-aware, suggesting subcommands, options, and enum values based on the current command.

    # Bash
    eval "$(my-cli completions bash)"
    
    # Zsh
    eval "$(my-cli completions zsh)"
    
    # Fish
    my-cli completions fish | source ~/.config/fish/config.fish
  12. Configure the Binary Release workflow

    main

    To automate the creation and upload of standalone binaries to GitHub Releases, use the wevm/incur/release@v1 action. This action compiles unsigned targets, verifies checksums, tests Linux executables (glibc and musl), and uploads assets including .gz binaries, SHA256SUMS, install.sh, and install.ps1.

    Default Behavior:

    • Entry point: ./src/bin.ts
    • CLI name: Derived from the root package.json.
    • Version: The stable version in the root package.json.
    • Release target: The latest published GitHub release.

    Important Notes:

    • The action appends assets to existing releases. If using changesets/action@v1, run Incur after Changesets has published the release.
    • The action rejects pull request merge refs; run it from a trusted push or manual workflow.
    • It does not sign executables. macOS and Windows users may encounter Gatekeeper or SmartScreen warnings.
    name: Binary Release
    
    on:
      workflow_dispatch:
    
    concurrency:
      group: binary-release
    
    jobs:
      release:
        runs-on: ubuntu-latest
        permissions:
          contents: write
        steps:
          - id: release
            uses: wevm/incur/release@v1