Run local coding-agent CLIs from Node.js through one small Node-only API.
coding-agent-runner wraps the process and protocol details for:
codex: Codex CLI viacodex app-server --listen stdio://claude: Claude Code via nativestream-jsoncursor: Cursor Agent viacursor-agent acpopencode: OpenCode viaopencode acppi: Pi viapi-acp
It is positioned next to ACP-style local agent interoperability, but with a friendlier application API. Instead of making every app deal with provider-specific commands, stdio transports, JSON-RPC shapes, stream-json differences, cancellation, and session ids directly, this package exposes a small SDK surface: pick a provider, pass cwd and prompt, then consume normalized events or a final result.
It does not provide a UI, database, task queue, sandbox, credential manager, or memory layer. It starts local CLI processes, speaks their stdio protocols, normalizes streaming events, and returns turn results that local apps can render or store however they want.
Use this package when you are building a local app, desktop app, daemon, or developer tool that needs to call installed coding agents from Node.js.
Good fits:
- Run Codex or Claude Code from a desktop app.
- Stream Cursor/OpenCode/Pi tool progress into your own UI.
- Detect which coding CLIs are installed on a user's machine.
- Keep a lightweight multi-turn session without adopting a larger agent platform.
Not a fit:
- Browser-only applications.
- Hosted LLM API calls.
- Sandboxed execution by default.
- Provider installation, login, billing, or credential management.
npm install coding-agent-runnerInstall and authenticate the underlying CLI separately. For example, codex, claude, cursor-agent, opencode, or pi-acp must be available on PATH.
Runtime requirements:
- Node.js 20 or newer
- ESM project or dynamic
import() - At least one supported provider CLI installed locally
import { runCliAgent } from "coding-agent-runner";
const result = await runCliAgent({
provider: "codex",
cwd: process.cwd(),
model: "gpt-5.5",
prompt: "Inspect this repository and summarize the test command.",
});
console.log(result.output);runCliAgent() is the simplest API. It creates a provider process, runs one prompt, consumes the stream, closes the process, and returns the final output.
Pass model on the top-level runner options:
await runCliAgent({
provider: "claude",
cwd: process.cwd(),
model: "sonnet",
prompt: "Review the staged diff.",
});model is passed directly to Codex thread startup and Claude Code's --model flag. ACP providers (cursor, opencode, and pi) expose model switching inconsistently today; when a provider supports a model flag or env var, pass it through spawn.args or spawn.env.
Pass systemPrompt on the top-level runner options:
await runCliAgent({
provider: "codex",
cwd: process.cwd(),
model: "gpt-5.5",
systemPrompt: "You are a concise senior TypeScript reviewer.",
prompt: "Review this repository.",
});Provider behavior:
- Codex: mapped to app-server
developerInstructions. - Claude: mapped to Claude Code
--append-system-prompt. - ACP providers (
cursor,opencode, andpi): wrapped into the prompt as explicit<system>and<user>blocks because ACP does not currently define a portable system-prompt field.
Pass local stdio MCP servers with mcpServers when a turn needs extra tools or context:
await runCliAgent({
provider: "claude",
cwd: process.cwd(),
model: "sonnet",
mcpServers: [{
name: "docs",
command: "npx",
args: ["-y", "@acme/docs-mcp"],
env: { DOCS_TOKEN: process.env.DOCS_TOKEN },
}],
prompt: "Use the docs MCP server and review this module.",
});Provider behavior:
- Codex: mapped to app-server
config.mcp_servers. - Claude: written to a temporary Claude Code
--mcp-configfile. - ACP providers (
cursor,opencode, andpi): passed to ACP session setup as stdio MCP servers.
undefined environment values are dropped before provider config is created.
Pass local skill references with skills:
await runCliAgent({
provider: "codex",
cwd: process.cwd(),
skills: [{ name: "code-review", path: "/path/to/skills/code-review" }],
prompt: "Review the staged diff.",
});Provider behavior:
- Codex: sent as structured app-server
skillinput blocks before the user text and also referenced indeveloperInstructionsfor compatibility with CLI builds that do not materialize ad-hoc skills automatically. - Claude and ACP providers: added to the system prompt as explicit skill references, instructing the provider to read the skill path when relevant.
import { streamCliAgent } from "coding-agent-runner";
for await (const event of streamCliAgent({
provider: "claude",
cwd: process.cwd(),
prompt: "Add tests for the auth module.",
})) {
if (event.type === "text_delta") process.stdout.write(event.text);
if (event.type === "tool_start") console.log("tool:", event.name);
}streamCliAgent() is still one-shot, but it yields normalized progress events as the provider runs.
import { createCodingAgentRunner } from "coding-agent-runner";
const runner = await createCodingAgentRunner({
provider: "cursor",
cwd: "/path/to/project",
});
try {
for await (const event of runner.stream({ prompt: "Inspect the codebase." })) {
console.log(event);
}
for await (const event of runner.stream({ prompt: "Now make the change." })) {
console.log(event);
}
} finally {
await runner.close();
}The stateful runner keeps the last provider session id in memory and reuses it on the next turn.
import { detectCliAgents } from "coding-agent-runner";
const agents = await detectCliAgents();
console.table(agents);Detection is best-effort. It scans PATH, runs each provider's version command with a timeout, and does not try to log in or mutate provider state.
import { runCliAgent } from "coding-agent-runner";
await runCliAgent({
provider: "opencode",
cwd: process.cwd(),
prompt: "Summarize this package.",
spawn: {
command: "/custom/bin/opencode",
args: ["acp"],
},
});Use command overrides when your host app bundles a CLI adapter or stores it outside PATH.
import { runCliAgent } from "coding-agent-runner";
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(new Error("Timed out")), 30_000);
try {
await runCliAgent({
provider: "claude",
cwd: process.cwd(),
prompt: "Run the test suite and fix failures.",
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}Cancellation is best-effort and provider-specific. The package forwards the abort signal to the active transport and cleans up child processes it owns.
This package does not sandbox provider processes. If you need isolation, pass a wrapper command:
await runCliAgent({
provider: "codex",
cwd: process.cwd(),
prompt: "Inspect this repository.",
spawn: {
wrapper: {
command: "sandbox-exec",
args: ["-f", "/path/to/profile.sb"],
},
},
});The wrapper is invoked as:
<wrapper.command> <wrapper.args...> <provider.command> <provider.args...>
The friendly streaming API emits:
type CodingAgentEvent =
| { type: "text_delta"; text: string }
| { type: "thinking_start"; id: string }
| { type: "thinking_delta"; text: string }
| { type: "thinking_end"; id: string }
| { type: "tool_start"; id: string; name: string; input?: unknown }
| { type: "tool_update"; id: string; name: string; input?: unknown; output?: string }
| { type: "tool_end"; id: string; name: string; output: string; isError: boolean }
| { type: "done"; output: string; sessionId: string | null; stopReason: string }
| { type: "error"; error: Error };The package also exports the lower-level building blocks:
runAgentTurn()for the existing one-shot provider dispatcherrunCodexTurn()andacquireCodexAppServer()runClaudeNative()AcpConnectionmapStreamEventToAgentEvents()buildDefaultSpawn()andlistProviderConfigs()
Lower-level APIs use the internal provider ids: codex-cli, claude-code-cli, cursor-cli, opencode-cli, and pi-cli.
Friendly APIs accept short provider ids:
| Public id | Default command | Internal id |
|---|---|---|
codex |
codex app-server --listen stdio:// |
codex-cli |
claude |
claude -p --output-format stream-json --input-format stream-json --verbose |
claude-code-cli |
cursor |
cursor-agent acp |
cursor-cli |
opencode |
opencode acp |
opencode-cli |
pi |
pi-acp |
pi-cli |
This library does not sandbox provider processes. The spawned CLI runs with the working directory, environment, credentials, and filesystem permissions you give it. If you need isolation, pass a wrapper command in spawn options or run this package inside your own sandbox/container.
Claude Code permission bypass is not enabled by default. To add --dangerously-skip-permissions, call runClaudeNative() with dangerouslySkipPermissions: true.
npm install
npm run typecheck
npm test
npm run build
npm pack --dry-runnpm run check runs typecheck, tests, and build.
Use the demo when you want to type real prompts and watch normalized runner events as they happen, similar to inspecting a lightweight TUI trace:
npm run demo -- codex --model gpt-5.5
npm run demo -- claude --model sonnet
npm run demo -- codex --model gpt-5.5 --system-prompt "You are concise"Inside the demo, type a prompt and press Enter. It prints streamed answer text plus process events such as thinking_start, thinking_delta, thinking_end, tool_start, tool_update, tool_end, done, sessionId, and elapsed time. The same runner instance is reused, so follow-up prompts continue the same session when the provider supports it.
Codex often emits thinking_start and thinking_end lifecycle events without exposing private reasoning text; the demo shows those lifecycle markers instead of inventing hidden chain-of-thought content.
To pass one input and exit:
npm run demo -- codex --model gpt-5.5 --prompt "Reply with exactly DEMO_OK"Useful demo commands:
/session Show the current provider session id
/cwd Show the working directory
/help Show interactive commands
/exit Quit
For machine-readable traces:
npm run demo -- codex --model gpt-5.5 --prompt "List two test commands" --json-eventsThe default test suite uses mocks and protocol fixtures so CI does not need local agent logins. To verify a real installed provider on your machine, run the manual smoke scripts:
npm run smoke:claude
npm run smoke:codex
npm run smoke:cursor
npm run smoke:opencode
npm run smoke:piEach smoke script builds the package, starts the real local CLI, checks streaming output, verifies one session resume turn, and checks cancellation. The scripts require the underlying CLI to be installed, authenticated, and available on PATH.
To run only the streaming check:
npm run smoke:claude -- --stream-onlyTo run with an explicit model for Codex or Claude:
npm run smoke:claude -- --model sonnet
npm run smoke:codex -- --model gpt-5.5To test a CLI outside PATH, pass a command override:
CAR_OPENCODE_COMMAND=/custom/bin/opencode npm run smoke:opencode
PI_ACP_BIN=/custom/bin/pi-acp npm run smoke:piUse these when you want a real, non-mocked check for higher-level runner options:
npm run smoke:real
npm run smoke:real:codex
npm run smoke:real:claudesmoke:real defaults to Codex and Claude Code. It builds the package, starts the real installed CLI, and verifies:
systemPromptexact-token behaviorskillsbehavior through a temporary localSKILL.md
The script requires the selected CLIs to be installed, authenticated, and available on PATH. You can tune models and providers:
npm run smoke:real -- --providers codex,claude --codex-model gpt-5.5 --claude-model sonnet
npm run smoke:real -- --providers cursor,opencode,pi --skip-missingTo include a real MCP server config in the smoke run:
npm run smoke:real:claude -- \
--mcp-name docs \
--mcp-command npx \
--mcp-args-json '["-y","@acme/docs-mcp"]' \
--mcp-prompt "Use the docs MCP server and reply with DOCS_OK" \
--mcp-expected DOCS_OKMIT