cli-to-js - turn any CLI into a JS API
Wraps any command-line tool as a typed JavaScript API agents can call directly. Saves writing a custom MCP for every CLI you want to expose.
cli-to-js answers a question that comes up the moment you try to build agent tooling: "I need the agent to call this CLI - do I really have to wrap every flag and subcommand by hand?" The library reads --help on any binary, parses the output, and hands you back a typed JavaScript object where subcommands are methods, flags are options, and everything just works.
The author marks the project experimental up front, but the shape of it is the right shape. For an agent that needs to drive git, claude, kubectl, or anything else CLI-shaped, this saves you writing a custom MCP server or a hand-rolled wrapper for every single tool.
import { convertCliToJs } from "cli-to-js";
const git = await convertCliToJs("git");
const claude = await convertCliToJs("claude");
const { stdout } = await git.diff({ nameOnly: true, _: ["HEAD~1"] });
const changedFiles = stdout.trim().split("\n");
for (const file of changedFiles) {
const review = await claude({
print: true,
model: "sonnet",
_: [`Review ${file} for bugs and suggest fixes`],
});
if (review.stdout.includes("no issues")) continue;
console.log(`${file}:`, review.stdout);
}
That's a real working example from the README - the git diff and claude --print paths both go through the same Proxy-based API that was generated from --help parsing.
Why this matters for agents
The piece that makes this more than a developer convenience: $validate catches hallucinated flag names before spawning a process. The model says --massage instead of --message? You get a structured error back with a Levenshtein-based suggestion the agent can self-correct from in one retry, instead of a confused subprocess and a wasted turn.
For multi-turn agent loops driving CLIs, that single feature is the difference between "this works" and "this barely works."
Quick start
npm install cli-to-js
The default usage runs --help on the binary, parses the output into a schema, and returns a Proxy where every subcommand is a method:
import { convertCliToJs } from "cli-to-js";
const api = await convertCliToJs("my-tool");
const result = await api.build({ output: "dist", minify: true });
// -> my-tool build --output dist --minify
console.log(result.stdout);
console.log(result.exitCode);
The flag mapping is the part that's just opinionated enough to be useful and predictable:
| JS option | CLI output |
|---|---|
{ verbose: true } | --verbose |
{ verbose: false } | (omitted) |
{ output: "file.txt" } | --output file.txt |
{ dryRun: true } | --dry-run |
{ v: true } | -v |
{ include: ["a", "b"] } | --include a --include b |
{ _: ["file.txt"] } | file.txt |
camelCase becomes kebab-case, single-letter keys become short flags, the _ array becomes positional arguments. No surprises.
TypeScript types, two ways
Out of the box, every subcommand returns Promise<CommandResult> with proper typing on $schema, $parse, $spawn. No codegen needed for the basic surface.
For tighter per-subcommand types, pass a generic:
const git = await convertCliToJs<{
commit: { message?: string; all?: boolean; amend?: boolean };
push: { force?: boolean; setUpstream?: string };
}>("git");
git.commit({ message: "hello" }); // autocompletes
git.push({ foobar: true }); // type error
Or generate a .d.ts from the parsed schema and check it in:
npx cli-to-js git --dts --subcommands -o git.d.ts
The latter is the right move for production agents - you commit the types and the agent gets autocomplete plus validation without re-parsing --help on every cold start.
Subcommand parsing
By default only the root --help is parsed. Enable subcommands: true to walk all subcommand help texts up front:
const git = await convertCliToJs("git", { subcommands: true });
Or parse on demand:
const git = await convertCliToJs("git");
const commitSchema = await git.$parse("commit");
await git.$parse(); // parse all discovered subcommands
Handles commander-style aliases like init|setup and add|install - the primary name is canonical.
Validation - the agent-safety story
Before running a command, validate options against the parsed schema:
const errors = git.$validate("commit", { massage: "fix typo" });
// [{
// kind: "unknown-flag",
// name: "massage",
// suggestion: "message",
// message: 'Unknown flag "massage". Did you mean "message"?'
// }]
if (errors.length === 0) {
await git.commit({ message: "fix typo" });
}
The validator catches:
- Unknown flags with Levenshtein-based did-you-mean suggestions.
- Type mismatches - boolean flags getting values, value-taking flags getting booleans.
- Missing required positionals.
- Too many positionals.
Empty array means valid. Non-empty array means the agent should retry with the suggestions instead of spawning a guaranteed-to-fail process.
When to reach for it
- You're building agent tooling that needs to call multiple CLIs and don't want to hand-wrap each one.
- You want types and validation for CLI surfaces that change between versions of the underlying tool.
- You're testing CLI tooling from JS and want a typed surface instead of
child_process.spawnstrings. - You'd otherwise write a custom MCP server for every CLI you want an agent to drive.
When not to
- One-shot scripts.
child_process.spawnis shorter. - CLIs with broken or non-standard
--helpoutput. The parser is good but it's not magic. - Workflows where you specifically want raw stdout/stderr handling that doesn't fit the
CommandResultshape (rare; if it applies, you'll know).
Trade-offs to know
The "experimental" label at the top of the README is real - APIs may change without notice. Pin a specific version if you build production agent infrastructure on this.
--help parsing is heuristic by nature. Highly customised help output (rich formatted tables, banners, footers) sometimes confuses the parser. The fromHelpText(name, helpTextString) escape hatch lets you pre-process the help text yourself if needed:
import { fromHelpText } from "cli-to-js";
const api = fromHelpText("my-tool", helpTextString);
await api.build({ watch: true });
For agents at scale, the right pattern is probably: generate the .d.ts once, commit it, refresh on tool upgrades. That's how you get the typing benefit without paying the --help parsing latency on every cold start.
The $validate step is the killer feature for agent reliability - if you only take one thing from this library, take that. Wrap every agent-issued CLI call in a validate-then-spawn pattern and you'll cut a meaningful fraction of "agent typed the wrong flag" failures.
Related entries
tui-use - drive interactive REPLs from agents
Lets agents interact with programs that expect a human at the keyboard - REPLs, debuggers, TUI apps - things bash pipes cannot reach. Fills the gap between shell and full computer-use.
passmark - Playwright AI regression testing
Open-source Playwright library for AI-driven browser regression testing with intelligent caching, auto-healing locators, and multi-model verification. Designed to keep flaky AI tests stable across model versions.
bunqueue - SQLite-backed job queue for Bun
High-performance Bun job queue with SQLite persistence, dead-letter queue, cron scheduling, and S3 backups. Marketed as BullMQ alternative for AI agent workloads.
agent-prism - React components for agent traces
React component library for visualizing distributed traces from AI agents. Drop-in widgets for timelines, span trees, and tool-call breakdowns from LangChain or custom runtimes.