ワンクリックで
incur
incur is a TypeScript framework for building CLIs that work for both AI agents and humans. Use when creating new CLIs.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
incur is a TypeScript framework for building CLIs that work for both AI agents and humans. Use when creating new CLIs.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
| name | incur |
| description | incur is a TypeScript framework for building CLIs that work for both AI agents and humans. Use when creating new CLIs. |
| command | incur |
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.
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
Mount an HTTP server as a command with .command('name', { fetch }). Argv is translated into HTTP requests using curl-style flags:
import { Cli } from "incur";
import { Hono } from "hono";
const app = new Hono();
app.get("/users", (c) => c.json({ users: [{ id: 1, name: "Alice" }] }));
app.post("/users", async (c) => c.json({ created: true, ...(await c.req.json()) }, 201));
Cli.create("my-cli", { description: "My CLI" }).command("api", { fetch: app.fetch }).serve();
my-cli api users # GET /users
my-cli api users -X POST -d '{"name":"Bob"}' # POST /users
my-cli api users --limit 5 # GET /users?limit=5
Pass an OpenAPI spec alongside fetch to generate typed subcommands with args, options, and descriptions from the spec:
Cli.create("my-cli", { description: "My CLI" })
.command("api", { fetch: app.fetch, openapi: spec })
.serve();
my-cli api listUsers --limit 5 # GET /users?limit=5
my-cli api getUser 42 # GET /users/42
my-cli api createUser --name Bob # POST /users with body
my-cli api --help # shows typed subcommands
Works with any (Request) => Response handler — Hono, Elysia, etc. Specs from @hono/zod-openapi are supported directly.
Expose your CLI as a standard Fetch API handler with cli.fetch. Works with Bun, Cloudflare Workers, Deno, Hono, and anything that accepts (req: Request) => Response.
import { Cli, z } from "incur";
const cli = Cli.create("my-cli", { version: "1.0.0" }).command("users", {
args: z.object({ id: z.coerce.number().optional() }),
options: z.object({ limit: z.coerce.number().default(10) }),
run(c) {
if (c.args.id) return { id: c.args.id, name: "Alice" };
return { users: [{ id: 1, name: "Alice" }], limit: c.options.limit };
},
});
Bun.serve(cli); // Bun
Deno.serve(cli.fetch); // Deno
export default cli; // Cloudflare Workers
app.all("*", (c) => cli.fetch(c.request)); // Elysia
app.use((c) => cli.fetch(c.req.raw)); // Hono
export const GET = cli.fetch; // Next.js
export const POST = cli.fetch;
Request mapping:
| HTTP | CLI equivalent |
|---|---|
GET /users?limit=5 | my-cli users --limit 5 |
GET /users/42 | my-cli users 42 (positional arg) |
POST /users with JSON body | my-cli users --name Bob |
GET / | root command (or 404) |
Responses are JSON envelopes: { "ok": true, "data": { ... }, "meta": { "command": "users", "duration": "3ms" } }.
Error status codes: 400 for validation errors, 404 for unknown commands, 500 for thrown errors.
Async generator commands stream as NDJSON (application/x-ndjson). Middleware runs the same as serve().
If a resolved command is a fetch gateway (.command('api', { fetch })), the request is forwarded to the nested handler.
The fetch handler exposes an MCP endpoint at /mcp. Agents can discover and call commands as MCP tools over HTTP:
POST /mcp { "jsonrpc": "2.0", "method": "initialize", ... }
POST /mcp { "jsonrpc": "2.0", "method": "tools/list", ... }
POST /mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "users", ... } }
The MCP server is initialized lazily on the first /mcp request. Non-/mcp paths route to the command API as usual.
All schemas use Zod. Arguments are positional (assigned by schema key order). Options are named flags.
args: z.object({
repo: z.string().describe("Repository in owner/repo format"),
branch: z.string().optional().describe("Branch name"),
});
tool clone owner/repo main
# ^^^^^^^^^^ ^^^^
# repo branch
options: z.object({
state: z.enum(["open", "closed"]).default("open").describe("Filter by state"),
limit: z.number().default(30).describe("Max results"),
label: z.array(z.string()).optional().describe("Filter by labels"),
verbose: z.boolean().optional().describe("Show details"),
});
Supported parsing:
--flag value and --flag=value-f value short aliases (via alias property)--verbose boolean flags (true), --no-verbose (false)--label bug --label feature array options.default(), optionality from .optional()alias: { state: 's', limit: 'l' }
tool list -s closed -l 10
Mark options as deprecated with .meta({ deprecated: true }). Shows [deprecated] in --help, **Deprecated.** in skill docs, deprecated: true in JSON Schema, and emits a stderr warning in TTY mode:
options: z.object({
zone: z.string().optional().describe("Availability zone").meta({ deprecated: true }),
region: z.string().optional().describe("Target region"),
});
env: z.object({
NPM_TOKEN: z.string().optional().describe("Auth token"),
NPM_REGISTRY: z.string().default("https://registry.npmjs.org").describe("Registry URL"),
});
Environment variables are parsed from process.env and validated against the Zod schema.
Define alternative usage patterns to show in --help instead of the auto-generated synopsis:
Cli.create("curl.md", {
args: z.object({ url: z.string() }),
options: z.object({ objective: z.string().optional() }),
usage: [
{ args: { url: true } },
{ args: { url: true }, options: { objective: true } },
{ prefix: "cat file.txt |", suffix: "| head" },
],
run({ args }) {
return { content: "..." };
},
});
Renders in help as:
Usage: curl.md <url>
curl.md <url> --objective <objective>
cat file.txt | curl.md | head
Each usage entry supports:
| Property | Type | Description |
|---|---|---|
args | Partial<Record<key, true>> | Argument keys to include as <key> placeholders |
options | Partial<Record<key, true>> | Option keys to include as --key <key> flags |
prefix | string | Text prepended before the command (e.g. piping) |
suffix | string | Text appended after the command |
Both args and options are strictly typed from the Zod schemas — only valid keys are allowed.
Usage patterns also work on subcommands via .command().
Every command returns data. incur wraps it in a structured envelope and serializes to the requested format.
Define output to declare the return shape:
cli.command("info", {
output: z.object({
name: z.string(),
version: z.string(),
}),
run() {
return { name: "express", version: "4.21.2" };
},
});
When output is provided, TypeScript enforces that run() returns the correct shape.
Control with --format <fmt> or --json:
| Flag | Format | Description |
|---|---|---|
| (default) | TOON | Token-efficient, ~40% fewer tokens than JSON |
--format json | JSON | JSON.parse()-safe |
--format yaml | YAML | Human-readable |
--format md | Markdown | Tables for docs/issues |
With --verbose, the full envelope is emitted:
tool info express --verbose
ok: true
data:
name: express
version: 4.21.2
meta:
command: info
duration: 12ms
Without --verbose, only data is emitted. On errors, only the error block is emitted.
Use --filter-output to prune command output to specific keys. Supports dot-notation for nested access, array slices with [start,end], and comma-separated paths:
cli.command("users", {
description: "List users",
run() {
return {
users: [
{ name: "Alice", email: "alice@example.com", role: "admin" },
{ name: "Bob", email: "bob@example.com", role: "user" },
{ name: "Carol", email: "carol@example.com", role: "user" },
],
};
},
});
tool users --filter-output users.name
# → [3]: Alice,Bob,Carol
tool users --filter-output users[0,2].name
# → users[2]{name}:
# → Alice
# → Bob
Use --token-count, --token-limit, and --token-offset to manage large outputs. Tokens are estimated using LLM tokenization rules (~96% accuracy).
# Check token count
tool users --token-count
# → 42
# Limit to first 20 tokens
tool users --token-limit 20
# Paginate with offset
tool users --token-offset 20 --token-limit 20
With --verbose, truncated output includes meta.nextOffset for programmatic pagination.
Use --schema to print the JSON Schema for a command's arguments, environment variables, options, and output:
cli.command("install", {
description: "Install a package",
args: z.object({
package: z.string().describe("Package name"),
}),
options: z.object({
saveDev: z.boolean().optional().describe("Save as dev dependency"),
}),
run({ args }) {
return { added: 1 };
},
});
tool install --schema
# → args:
# → type: object
# → properties:
# → package:
# → type: string
# → options:
# → type: object
# → properties:
# → saveDev:
# → type: boolean
Use --schema --format json for machine-readable output. Not supported on fetch commands.
incur adapts output based on whether stdout is a TTY:
| Scenario | TTY (human) | Non-TTY (agent/pipe) |
|---|---|---|
| Command output | Formatted data only | TOON envelope |
| Errors | Human-readable message | Error envelope |
--help | Pretty help text | Same |
--json / --format | Overrides to structured | Same |
agent booleanThe run context includes agent — true when stdout is not a TTY (piped or consumed by an agent), false when running in a terminal:
cli.command("deploy", {
run(c) {
if (!c.agent) console.log("Deploying...");
return { status: "ok" };
},
});
ok() and error() helpersUse the context helpers for explicit result control:
run(c) {
const item = await db.find(c.args.id)
if (!item)
return error({
code: 'NOT_FOUND',
message: `Item ${c.args.id} not found`,
retryable: false,
})
return c.ok(item)
}
Suggest next commands to guide agents on success:
run(c) {
const result = { id: 42, name: c.args.name }
return c.ok(result, {
cta: {
description: 'Suggested commands:',
commands: [
{ command: 'get', args: { id: 42 }, description: 'View the item' },
'list',
],
},
})
}
Or on errors, to help agents self-correct:
run(c) {
const token = process.env.GH_TOKEN
if (!token)
return c.error({
code: 'NOT_AUTHENTICATED',
message: 'GitHub token not found',
retryable: true,
cta: {
description: 'To authenticate:',
commands: [
{ command: 'auth login', description: 'Log in to GitHub' },
{ command: 'config set', options: { token: true }, description: 'Set token manually' },
],
},
})
// ...
}
Every incur CLI has built-in Model Context Protocol (MCP) support — exposing commands as MCP tools that agents can call directly.
mcp add built-in commandRegister the CLI as an MCP server for your agents:
my-cli mcp add
This registers the CLI with your agent's MCP config. Works with Claude Code, Cursor, Amp, and others out of the box.
Options:
| Flag | Description |
|---|---|
-c, --command | Override the command agents will run to start the server |
--agent <agent> | Target a specific agent (e.g. claude-code, cursor) |
--no-global | Install to project instead of globally |
--mcp flagStart the CLI as an MCP stdio server:
my-cli --mcp
This exposes all commands as MCP tools over stdin/stdout. Command groups are flattened with underscores (e.g. pr_list, pr_view). Arguments and options are merged into a single flat input schema.
All incur-based CLIs can auto-generate and install agent skill files with skills add:
my-cli skills add
This generates Markdown skill files from your command definitions and installs them so agents discover your CLI automatically.
It is also possible to configure skills add:
const cli = Cli.create("my-cli", {
sync: {
depth: 1,
include: ["_root"],
suggestions: ["install react as a dependency", "check for outdated packages"],
},
});
| Option | Type | Description |
|---|---|---|
depth | number | Grouping depth for skill files. 0 = single file, 1 = one per top-level command. Default: 1 |
include | string[] | Glob patterns for additional SKILL.md files to include. Use '_root' for the project-level SKILL.md |
suggestions | string[] | Example prompts shown after sync to help users get started |
--llms flagEvery incur CLI gets a built-in --llms flag that outputs a machine-readable manifest of all commands:
tool --llms
Outputs Markdown skill documentation by default.
# tool install
Install a package
## Arguments
| Name | Type | Required | Description |
| --------- | -------- | -------- | ----------------------- |
| `package` | `string` | no | Package name to install |
## Options
| Flag | Type | Default | Description |
| ----------- | --------- | ------- | ---------------------- |
| `--saveDev` | `boolean` | | Save as dev dependency |
| `--global` | `boolean` | | Install globally |
Use --llms --format json for JSON schema manifest:
{
"version": "incur.v1",
"commands": [
{
"name": "install",
"description": "Install a package",
"schema": {
"args": { "type": "object", "properties": { "package": { "type": "string" } } },
"options": { "type": "object", "properties": { "saveDev": { "type": "boolean" } } },
"output": { "type": "object", "properties": { "added": { "type": "number" } } }
}
}
]
}
| 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) |
cli.command("deploy", {
args: z.object({ env: z.enum(["staging", "production"]) }),
options: z.object({ force: z.boolean().optional() }),
examples: [
{ args: { env: "staging" }, description: "Deploy to staging" },
{ args: { env: "production" }, options: { force: true }, description: "Force deploy to prod" },
],
run({ args }) {
return { deployed: args.env };
},
});
Examples appear in --help output and generated skill files.
cli.command("publish", {
hint: "Requires NPM_TOKEN to be set in your environment.",
// ...
});
Hints are displayed after examples in help output and included in skill files.
Control whether output data is displayed to humans. 'all' (default) shows output to everyone. 'agent-only' suppresses data in human/TTY mode while still returning it via --json, --format, or --verbose.
cli.command("deploy", {
outputPolicy: "agent-only",
run() {
return { id: "deploy-123", url: "https://staging.example.com" };
},
});
Set on a group or root CLI to inherit across children. Children can override:
const sub = Cli.create("internal", { outputPolicy: "agent-only" });
sub.command("sync", { run: () => ({ synced: true }) }); // inherits agent-only
sub.command("status", { outputPolicy: "all", run: () => ({}) }); // overrides
Register composable before/after hooks with cli.use(). Middleware executes in registration order, onion-style. Each calls await next() to proceed.
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 };
},
});
$ deploy-cli deploy
# → deployed: true
# took 12ms
Middleware on a sub-CLI only applies to its commands:
const admin = Cli.create("admin", { description: "Admin commands" })
.use(async (c, next) => {
if (!isAdmin()) throw new Error("forbidden");
await next();
})
.command("reset", { run: () => ({ reset: true }) });
cli.command(admin); // middleware only runs for `my-cli admin reset`
$ my-cli admin reset
# → reset: true
$ my-cli other-cmd
# middleware does not run
Per-command middleware runs after root and group middleware, and only for that command:
import { Cli, middleware, z } from "incur";
const cli = Cli.create("my-cli", {
description: "My CLI",
vars: z.object({ user: z.custom<{ id: string }>() }),
});
const requireAuth = middleware<typeof cli.vars>((c, next) => {
if (!c.var.user) throw new Error("must be logged in");
return next();
});
cli.command("deploy", {
middleware: [requireAuth],
run() {
return { deployed: true };
},
});
$ my-cli deploy
# Error: must be logged in
$ my-cli other-cmd
# per-command middleware does not run
Declare a vars schema on create() to inject typed variables. Middleware sets them with c.set(), handlers read them via c.var. Use .default() for vars that don't need middleware:
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 };
},
});
$ my-cli whoami
# → user:
# → id: u_123
# → name: Alice
# → requestId: 550e8400-e29b-41d4-a716-446655440000
# → debug: false
Middleware does not run for built-in commands (--help, --llms, --mcp, mcp add, skills add).
Call .serve() to parse process.argv and run:
cli.serve();
For testing, pass custom argv and DI overrides:
let output = "";
await cli.serve(["install", "express", "--json"], {
stdout(s) {
output += s;
},
exit() {},
});
serve() options| Option | Type | Description |
|---|---|---|
stdout | (s: string) => void | Override stdout writer |
exit | (code: number) => void | Override exit handler |
env | Record<string, string | undefined> | Override environment variables |
cli.fetch(req: Request): Promise<Response>Expose the CLI as a Fetch API handler. See Serve CLI as Fetch API for full details.
Bun.serve(cli);
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";
},
});
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'] } })
}
Generate type definitions for your CLI's command map to get typed CTAs:
incur gen
This creates a incur.generated.ts file that registers your commands on the Cli.Commands type, enabling autocomplete on CTA command names, args, and options.
import { Cli, z } from "incur";
const cli = Cli.create("npm", {
version: "10.9.2",
description: "The package manager for JavaScript.",
sync: {
suggestions: ["install react as a dependency", "check for outdated packages"],
},
});
cli.command("install", {
description: "Install a package",
args: z.object({
package: z.string().optional().describe("Package name to install"),
}),
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().describe("Number of packages added"),
packages: z.number().describe("Total packages"),
}),
examples: [
{ args: { package: "express" }, description: "Install a package" },
{
args: { package: "vitest" },
options: { saveDev: true },
description: "Install as dev dependency",
},
],
run({ args }) {
if (!args.package) return { added: 120, packages: 450 };
return { added: 1, packages: 451 };
},
});
cli.command("outdated", {
description: "Check for outdated packages",
options: z.object({
global: z.boolean().describe("Check global packages"),
}),
alias: { global: "g" },
output: z.object({
packages: z.array(
z.object({
name: z.string(),
current: z.string(),
wanted: z.string(),
latest: z.string(),
}),
),
}),
run() {
return {
packages: [{ name: "express", current: "4.18.0", wanted: "4.21.2", latest: "4.21.2" }],
};
},
});
cli.serve();
export default cli;
Always
export default cliso thatincur gencan import it and generate types.