CLI Adapters
Give your agent structured access to any CLI tool — with discovery, execution, and consistent output handling.
Overview
Agents are great at running CLI commands. But without structure, every script reinvents how to invoke a CLI, check if it's installed, and parse its output.
CLI adapters solve this with a small interface — similar to database adapters but for command-line tools. Each adapter wraps a single CLI (gh, ffmpeg, stripe, aws) and provides:
- Discovery — the agent can list what CLIs are available and what they do
- Availability checks — is the CLI installed?
- Consistent execution — stdout, stderr, and exit code in a standard format
The interface
Every CLI adapter implements CliAdapter:
import type { CliAdapter, CliResult } from "@agent-native/core/adapters/cli";
interface CliAdapter {
name: string; // "gh", "stripe", "ffmpeg"
description: string; // What the agent sees during discovery
isAvailable(): Promise<boolean>;
execute(args: string[]): Promise<CliResult>;
}
interface CliResult {
stdout: string;
stderr: string;
exitCode: number;
}ShellCliAdapter
For most CLIs, you don't need a custom class. ShellCliAdapter wraps any CLI binary with sensible defaults:
import { ShellCliAdapter } from "@agent-native/core/adapters/cli";
const gh = new ShellCliAdapter({
command: "gh",
description: "GitHub CLI — manage repos, PRs, issues, and releases",
});
const ffmpeg = new ShellCliAdapter({
command: "ffmpeg",
description: "Audio/video processing and transcoding",
timeoutMs: 120_000, // 2 min for long encodes
});
const stripe = new ShellCliAdapter({
command: "stripe",
description: "Stripe CLI — manage payments, webhooks, and customers",
env: { STRIPE_API_KEY: process.env.STRIPE_SECRET_KEY! },
});Options
| Option | Type | Description |
|---|---|---|
command | string | Binary name or path (required) |
description | string | What the CLI does — shown to the agent (required) |
name | string | Display name (defaults to command) |
env | Record | Extra environment variables merged with process.env |
cwd | string | Working directory (defaults to process.cwd()) |
timeoutMs | number | Execution timeout (default: 30000) |
Registry
The CliRegistry collects adapters so the agent can discover what's available at runtime:
import { CliRegistry, ShellCliAdapter } from "@agent-native/core/adapters/cli";
const cliRegistry = new CliRegistry();
cliRegistry.register(new ShellCliAdapter({
command: "gh",
description: "GitHub CLI — manage repos, PRs, issues, and releases",
}));
cliRegistry.register(new ShellCliAdapter({
command: "ffmpeg",
description: "Audio/video processing and transcoding",
}));
// List all registered CLIs
cliRegistry.list();
// → [{ name: "gh", ... }, { name: "ffmpeg", ... }]
// List only installed CLIs
await cliRegistry.listAvailable();
// → [{ name: "gh", ... }] (if ffmpeg isn't installed)
// Get a full summary for agent discovery
await cliRegistry.describe();
// → [{ name: "gh", description: "...", available: true },
// { name: "ffmpeg", description: "...", available: false }]
// Execute a command
const gh = cliRegistry.get("gh");
const result = await gh?.execute(["pr", "list", "--json", "title,url"]);
console.log(result?.stdout);Custom adapters
When you need more than ShellCliAdapter provides — custom auth, output parsing, or pre/post processing — implement CliAdapter directly:
import type { CliAdapter, CliResult } from "@agent-native/core/adapters/cli";
import { execFile } from "node:child_process";
export class DockerAdapter implements CliAdapter {
name = "docker";
description = "Docker container management — build, run, and manage containers";
async isAvailable(): Promise<boolean> {
try {
const result = await this.execute(["info", "--format", "{{.ServerVersion}}"]);
return result.exitCode === 0;
} catch {
return false;
}
}
async execute(args: string[]): Promise<CliResult> {
return new Promise((resolve) => {
execFile("docker", args, {
timeout: 60_000,
maxBuffer: 10 * 1024 * 1024,
encoding: "utf-8",
}, (error, stdout, stderr) => {
resolve({
stdout: stdout ?? "",
stderr: stderr ?? "",
exitCode: (error as any)?.code ?? 0,
});
});
});
}
}Server route
Expose the registry to the UI via an API route so scripts and components can discover and invoke CLIs:
// server/index.ts
import { createServer } from "@agent-native/core";
import { CliRegistry, ShellCliAdapter } from "@agent-native/core/adapters/cli";
const app = createServer();
const cliRegistry = new CliRegistry();
cliRegistry.register(new ShellCliAdapter({
command: "gh",
description: "GitHub CLI",
}));
// Discovery endpoint — agent can query this
app.get("/api/cli", async (_req, res) => {
const tools = await cliRegistry.describe();
res.json(tools);
});
// Execution endpoint
app.post("/api/cli/:name", async (req, res) => {
const adapter = cliRegistry.get(req.params.name);
if (!adapter) return res.status(404).json({ error: "CLI not found" });
const { args } = req.body;
const result = await adapter.execute(args ?? []);
res.json(result);
});Using from scripts
Scripts can use CLI adapters directly for structured access:
// scripts/list-prs.ts
import { ShellCliAdapter } from "@agent-native/core/adapters/cli";
const gh = new ShellCliAdapter({
command: "gh",
description: "GitHub CLI",
});
export default async function listPrs() {
if (!await gh.isAvailable()) {
console.error("GitHub CLI not installed. Run: brew install gh");
process.exit(1);
}
const result = await gh.execute([
"pr", "list", "--json", "title,url,state", "--limit", "10"
]);
if (result.exitCode !== 0) {
console.error(result.stderr);
process.exit(1);
}
const prs = JSON.parse(result.stdout);
const fs = await import("node:fs/promises");
await fs.writeFile("data/prs.json", JSON.stringify(prs, null, 2));
console.log(`Fetched ${prs.length} PRs`);
}Or skip the adapter entirely and call the CLI directly in a script — adapters are useful when you want discovery, availability checks, and consistent error handling, but they're not required. Use whichever approach fits.