---
title: "CLI Adapters"
description: "Connect any CLI tool to your agent-native app with a standard adapter interface for discovery and execution."
---

# CLI Adapters

Give your agent structured access to any CLI tool — with discovery, execution, and consistent output handling.

## Overview {#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 {#the-interface}

Every CLI adapter implements `CliAdapter`:

```ts
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 {#shell-adapter}

For most CLIs, you don't need a custom class. `ShellCliAdapter` wraps any CLI binary with sensible defaults:

```ts
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 {#shell-adapter-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 {#registry}

The `CliRegistry` collects adapters so the agent can discover what's available at runtime:

```ts
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 {#custom-adapter}

When you need more than `ShellCliAdapter` provides — custom auth, output parsing, or pre/post processing — implement `CliAdapter` directly:

```ts
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 {#server-route}

Expose the registry to the UI via an API route so actions and components can discover and invoke CLIs:

```ts
// 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 {#from-scripts}

Actions can use CLI adapters directly for structured access:

```ts
// 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.
