# mkcmd-agent > Scaffolds Bun + TypeScript CLIs that agents can drive: flags instead of prompts, one JSON object on stdout with --json, exit codes 0/1/2, and a describe command. MIT. Source: https://github.com/mackenziebowes/mkcmd-agent ## Install Not on npm yet. Run from source (Bun 1.2+): git clone https://github.com/mackenziebowes/mkcmd-agent ~/mkcmd-agent cd ~/mkcmd-agent && bun install bun ~/mkcmd-agent/src/index.ts --help Any clone directory works; this manual uses ~/mkcmd-agent. Below, `mkcmd-agent` means `bun ~/mkcmd-agent/src/index.ts`. Agents: run that full command every time. Most agent tools start a fresh shell per command, so an alias set in one call is gone in the next. ## Commands mkcmd-agent init --name [--description ] [--dir ] [--force] [--dry-run] [--install] [--json] Create a project. --name: lowercase package name, optional @scope/. Default dir: ./. Refuses a non-empty dir without --force (error code target_not_empty, exit 1). --dry-run lists files and writes nothing. --install runs bun install. mkcmd-agent add --name [--summary ] [--dir ] [--force] [--dry-run] [--json] Writes src/commands/.ts and registers it in src/commands/index.ts. Names: lowercase, digits, dashes; starts with a letter; describe and help are reserved. sync-users is exported as syncUsers. The new command throws not_implemented until you write run(). Errors: not_a_project (no src/commands/index.ts), command_exists (use --force). mkcmd-agent describe All commands and flags as JSON. ## Quick start mkcmd-agent init --name my-cli --description "Does one thing well" cd my-cli && bun install bun run src/index.ts describe bun run src/index.ts hello --name Ada --json # {"ok":true,"command":"hello","result":{"greeting":"Hello, Ada!"}} bun test ## Generated project AGENTS.md README.md package.json tsconfig.json .gitignore src/index.ts runCLI({ name, about, commands }) src/core/cli.ts the framework; one file, no dependencies src/commands/index.ts export const commands = [hello] src/commands/hello.ts example command test/cli.test.ts subprocess tests Scripts: bun run start, bun test, bun run typecheck, bun run build (compiled binary in dist/). ## Output contract (every generated CLI, and mkcmd-agent itself) - --json prints exactly one JSON object on stdout: success: {"ok": true, "command": "", "result": } failure: {"ok": false, "command": "", "error": {"code": "...", "message": "...", "hint": "... (optional)", "details": }} - Without --json: result printed by the command's render(), else as key: value lines. Errors go to stderr as "error: ..." and "hint: ...". - stderr: progress/logs only. stdout: the result only. - Exit codes: 0 success (also --help, describe, --version); 1 command failed (its own error code, or "internal" for an unexpected exception); 2 usage error (code "usage": unknown command, unknown flag, missing required flag, invalid value; also no arguments at all). - describe: always pretty-printed JSON, no envelope, no --json needed. Contains name, about, contract, globalFlags, and commands[] with name, summary, description, usage, flags[] (name, short, type, required, default, description), examples[]. - --help or help : one command as text. Add --json for its describe entry inside the envelope. --help lists commands. - Global flags on every command: --json, -h/--help. -v/--version only as the first argument. - Flags: string flags take a value (--name Ada or --name=Ada). Boolean flags take none (--shout, never --shout true). Unknown flags are usage errors. Missing booleans are false; missing strings use their default or are undefined. - No prompts: a missing required flag is always an immediate usage error (exit 2) showing the command's first example. ## Writing a command // src/commands/sync.ts import { defineCommand, CliError, UsageError } from "../core/cli"; export const sync = defineCommand({ name: "sync", // required, unique summary: "Sync records from the API", // required description: "Longer --help text.", // optional flags: { since: { type: "string", required: true, description: "YYYY-MM-DD" }, limit: { type: "string", default: "100", description: "Maximum records" }, "dry-run": { type: "boolean", description: "Report without writing" }, }, args: "", // optional usage text for positionals (ctx.args) examples: ["my-cli sync --since 2026-01-01 --json"], // first one shown on a missing-flag error run: async ({ flags, args, log, json }) => { if (!/^\d{4}-\d{2}-\d{2}$/.test(flags.since)) throw new UsageError("Bad date.", "Use YYYY-MM-DD."); // exit 2 log("fetching"); // stderr; never console.log const n = Number(flags.limit); // no number flag type: convert strings yourself if (n > 1000) throw new CliError("Limit too high.", { code: "limit_too_high", hint: "Max 1000." }); // exit 1 return { synced: n, since: flags.since, dryRun: flags["dry-run"] }; // JSON-serialisable }, render: (r) => `Synced ${r.synced} records since ${r.since}`, // optional human text }); Register it: add `sync` to `export const commands = [...]` in src/commands/index.ts and import it (or use `mkcmd-agent add`). Flag spec fields: type ("string" | "boolean"), description (required), required, default, short (one letter). Types: required or defaulted strings are string; other strings are string | undefined; booleans are boolean. CliError(message, { code, hint, exitCode, details }) defaults to exit 1. details is returned as error.details with --json, and printed through render() for people: use it when a check fails but its findings matter (lint, validate, gate). Always pass a code: without one, agents get the generic "error". UsageError(message, hint) exits 2 with code "usage". Other exceptions become code "internal", exit 1. Never call process.exit or print errors yourself. ## Testing Test as a subprocess, like an agent would: const proc = Bun.spawn(["bun", "src/index.ts", "sync", "--since", "yesterday", "--json"], { stdout: "pipe", stderr: "pipe", stdin: "ignore" }); const stdout = await new Response(proc.stdout).text(); expect(await proc.exited).toBe(2); expect(JSON.parse(stdout).error.code).toBe("usage"); ## Docs - https://mkcmd.mackenziebowes.com/docs/usage/ - https://mkcmd.mackenziebowes.com/docs/contract/ - https://mkcmd.mackenziebowes.com/docs/commands/