burgee
An agent-native CLI framework, drop-in compatible with commander and yargs. One declaration; help, --json, --schema, --mcp and completions all projected from it.
A burgee is the small swallowtail flag a boat flies to say which club or fleet it belongs to — a flag of identity, not of instruction. That is what this framework does for a command-line program: a command declares itself once, and every surface is that declaration read by a different reader.
It replaces commander and yargs: burgee/commander and burgee/yargs are drop-in,
graded by each one's own test suite. Change one import and the same program answers agents
too — --json for results, --schema for the command tree, --mcp for an MCP server.
Start here
npm install burgee// cli.mjs — the whole CLI
import { defineCommand, run } from 'burgee';
run(defineCommand({
name: 'greet',
description: 'Greet someone by name',
options: { name: { type: 'string', required: true, description: 'who to greet' } },
run: ({ options }) => ({ greeting: `hello, ${options.name}` }),
}));$ node cli.mjs --name ada
greeting: hello, ada
$ node cli.mjs --json --name ada
{"ok":true,"data":{"greeting":"hello, ada"}}
$ node cli.mjs # exit 2
error: missing required option --name
hint: pass --name <value>One file. No build step, no config file, no directory convention. A test enforces that on every commit.
One declaration, every surface
defineCommand() ──▶ manifest ──┬──▶ human help
├──▶ --json one stable envelope
├──▶ --schema versioned, one document per surface
├──▶ --mcp an MCP server, generated
├──▶ completions bash · zsh · fish · pwsh
├──▶ TypeScript types
└──▶ docs + llms.txtNothing here needs keeping in sync, because nothing is written twice.
Already on commander?
Drop-in compatible with both incumbents, graded by their own test suites — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' on the compatibility page — with the pass rate published and ratcheting:
- import { Command } from 'commander';
+ import { Command } from 'burgee/commander';Your code and your tests are unchanged. A façade is never called "compatible" until its host's own suite passes 100%; below that the rate is published instead of claimed.
What is in the box
| Import | Gives you |
|---|---|
burgee | defineCommand(), run(), the exit-code contract and the JSON envelope. |
burgee/commander | The commander API, graded by commander's suite. |
burgee/testing | Run a command in-process and assert on its result — no spawning. |
burgee/brand | One brand declaration → flag, favicon, OG card, cover, lockup. The logo above is its own output. |
burgee/plugin | definePlugin(), validate(), CONTRACT and PluginError — the host, at the subpath every package in the family publishes its host at. |
Writing a plugin
import { definePlugin } from 'burgee/plugin';
export default definePlugin({
name: 'acme',
commands: [{ path: ['audit'], description: 'Audit the tree', options: {}, effects: 'read_only', run: () => ({ findings: 0 }) }],
});A plugin's command is read by exactly the code a first-party one is read by, so the same
refusals apply: reserved option names, duplicate flags, a contributed path that is already
declared — and effects, which every runnable command declares. definePlugin stamps the
contract this burgee was compiled against; an object that reaches use() without one is
refused rather than accepted on trust, because burgee's extension point shipped before it
validated anything.
Status
burgee is published and working: the engine, the exit-code contract, the JSON envelope,
help from the manifest, plugins with hook filters, and a burgee/commander façade that
runs a real commander program. The dev loop, prompts, lazy commands and groups are not
here yet.
Roadmap, architecture and the 114-requirement floor: https://github.com/ofri-peretz/burgee
FAQ
Is burgee a commander alternative?
Yes — and a yargs alternative. It is drop-in compatible with both: change
import { Command } from 'commander' to import { Command } from 'burgee/commander' (or
yargs to burgee/yargs) and your code and tests are unchanged. Compatibility is graded by
each host's own test suite in CI, not asserted. Side by side:
burgee vs commander and
burgee vs yargs.
How do I make my CLI usable by an AI agent?
Declare it with defineCommand() — or keep it on commander or yargs syntax through the
drop-in front ends. Every command then answers --json with one stable envelope,
--schema with the whole command tree as data, and exits 2 when the command was wrong
so an agent knows to rewrite it rather than retry. None of it is written by hand; it is
the declaration read by a different reader.
Your CLI is an agent tool has each surface.
How do I expose a CLI over MCP?
Run it with --mcp: the same manifest is served as MCP tools over stdio. A command becomes
a tool only when it declares its effects (read_only, idempotent or non_idempotent),
so nothing reaches an agent by accident. Register it with any stdio client:
{ "mcpServers": { "mytool": { "command": "npx", "args": ["mytool", "--mcp"] } } }Does it have dependencies?
None outside the burgee family. burgee installs five packages from that family —
bellpull, closeout, linegauge, roundel and seniority — and each of those takes
nothing from outside it either: one repository, one release pipeline, one supply chain to
audit.
Part of the burgee family: a CLI on burgee declares what it is, roundel carries its colours, flagstaff flies it, and caique answers back. Each is an independent package; none requires the others.
MIT © Ofri Peretz — see LICENSE.
Benchmarks
Every number here is produced by npm run bench and published at /docs/benchmarks.
Graded by the incumbent's own test suite:
| suite | passing |
|---|---|
commander | 1360 / 1360 |
meow | 132 / 148 |
yargs | 804 / 804 |
Where it sits
Plugins register under the commands and hooks keys, against the one schema the whole family shares.
Nothing in this family builds on it yet, and it builds on bellpull, closeout, linegauge, roundel, seniority.