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 file sync 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 actions 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 actions

Actions can use CLI adapters directly for structured access:

// actions/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.