@a5c-ai/agent-mux is a unified TypeScript SDK and CLI (amux) for driving heterogeneous coding-agent harnesses — Claude Code, Codex, Gemini, Copilot, Cursor, OpenCode, OpenCode HTTP, pi, omp, openclaw, hermes, qwen — through one contract.
It spawns real subprocesses, normalizes their streaming output into a shared AgentEvent stream, and exposes each harness's sessions, config, auth, and plugins through a single AgentMuxClient interface. Invocations can run locally, in Docker, over SSH, or in a Kubernetes pod with no code change.
# SDK + CLI
npm install @a5c-ai/agent-mux
# SDK only
npm install @a5c-ai/agent-mux-core @a5c-ai/agent-mux-adapters
# Zero-install CLI
npx @a5c-ai/agent-mux --helpRequires Node.js 20.9.0 or later. ESM-first, with a CJS compatibility shim.
# Run a prompt against a harness you have installed locally
amux run --agent claude-code --prompt "Summarize README.md"
# Streaming JSON events (one per line)
amux run --agent codex --prompt "Add a test for foo()" --json
# Pick a profile (named RunOptions preset)
amux run --profile fast-claude --prompt "..."SDK:
import { createClient } from '@a5c-ai/agent-mux';
const client = createClient();
const handle = client.run({ agent: 'claude-code', prompt: 'hello' });
for await (const event of handle.events()) {
if (event.type === 'text_delta') process.stdout.write(event.text);
}
await handle.done;| Agent | CLI | Session dir |
|---|---|---|
claude / claude-code |
claude |
~/.claude/projects |
codex |
codex |
~/.codex/sessions |
gemini |
gemini |
~/.gemini/sessions |
copilot |
gh copilot |
~/.config/github-copilot/sessions |
cursor |
cursor-agent |
~/.cursor/sessions |
opencode |
opencode |
~/.config/opencode/sessions |
opencode-http |
opencode serve |
~/.config/opencode/sessions |
pi |
pi |
~/.pi/agent/sessions |
omp |
omp |
~/.omp/agent/sessions |
openclaw |
openclaw |
~/.openclaw/sessions |
hermes |
hermes |
~/.hermes/sessions |
qwen |
qwen |
~/.qwen/sessions |
agent-mux-remote |
amux |
(transport-delegated) |
Install or update any of them through amux:
amux install claude-code # npm / brew / manual, adapter-specified
amux update codex
amux detect --all --json # probe installations
amux detect-host # is this shell already inside a harness?Every harness — regardless of wire format (JSONL, stream-json envelopes, SSE, app-server frames, ANSI) — is normalized into the same AgentEvent union: text_delta, thinking_delta, tool_call_start, tool_input_delta, tool_call_ready, tool_result, message_stop, cost, error. Write your UI once; swap harnesses without touching render code.
client.sessions(agent) lists, reads, resumes, forks, and watches persistent harness sessions from their on-disk stores (~/.claude/projects, ~/.codex/sessions, ~/.gemini/sessions, …). RunOptions.sessionId reconnects; forkSessionId branches a new session from an existing transcript. noSession: true runs ephemerally.
One option flips where the subprocess actually runs: local, docker (with volumes/image/env), ssh (identity file, remote cwd), or k8s (namespace, image, pod spec overlay). The adapter contract is unchanged — streaming, sessions, and cost events still flow back over the chosen transport.
amux hooks install wires a command to a harness event (PreToolUse, PostToolUse, Stop, UserPromptSubmit, …). Native hooks are written into the harness's own config (e.g. ~/.claude/settings.json); harnesses without native hook support get a virtual hook layer driven off the event stream. Same UX, different mechanism.
amux plugins install <server> --agent claude adds an MCP server entry in the harness's config file. Supported on claude, codex, gemini, cursor, opencode, opencode-http, openclaw (and qwen via MCP-compatible config). list / uninstall work symmetrically.
amux skill add <agent> <folder> copies a skill folder into the harness convention path (e.g. .claude/skills/, .codex/skills/). amux agent add <agent> <file> does the same for sub-agent definitions (e.g. .claude/agents/). Both accept --global / --project scope, --force to overwrite, and have list / remove / where subcommands. File-convention only — no native harness command needed.
amux doctor reports each harness's binary presence, version, auth state (env var, config file, browser token), and config directory health. amux detect --all --json is the machine-readable form. amux install <agent> / update <agent> dispatches to the adapter-declared install method (npm, brew, curl script).
Every adapter declares an AgentCapabilities record (streaming, thinking, parallel tools, MCP, PTY requirement, image IO, skills, subagents, platforms) and a ModelCapabilities[] list (context window, pricing, thinking budgets, tool support). Build agent pickers, cost estimators, and feature-gated UIs directly off the declared capability surface.
Adapters that expose pricing/usage emit cost events (input/output tokens, cache reads, USD). amux run --json surfaces them on stdout; the SDK bubbles them through handle.events() alongside text.
RunOptions presets under a name. amux profiles lists them; amux run --profile fast-claude applies one. Layerable over explicit flags.
@a5c-ai/agent-mux-harness-mock ships deterministic scenario scripts (claudeCodeSuccess, codexToolError, …) and a MockProcess that replays them without any real binary, API key, or network. Pair with WorkspaceSandbox to test file-operation side effects. amux run --use-mock-harness drives the full CLI against mocks.
amux remote install <host> --harness <agent> uploads the SDK, installs the harness on the target, and verifies. Works over ssh and k8s; used to stand up stateless runners.
amux detect-host returns which harness (if any) this shell is already running inside — useful for adapters that want to refuse re-entry or skip re-auth.
import { createClient } from '@a5c-ai/agent-mux';
const client = createClient();
const handle = client.run({
agent: 'claude-code',
prompt: 'Refactor src/api.ts to use async/await',
model: 'claude-sonnet-4-20250514',
});
let totalUsd = 0;
for await (const event of handle.events()) {
switch (event.type) {
case 'text_delta':
process.stdout.write(event.delta);
break;
case 'thinking_delta':
process.stderr.write(`\x1b[90m${event.delta}\x1b[0m`);
break;
case 'tool_call_start':
console.log(`\n→ ${event.toolName}(${event.toolCallId})`);
break;
case 'tool_input_delta':
process.stdout.write(event.delta);
break;
case 'tool_result':
console.log(`\n← ${event.toolCallId} (${event.durationMs}ms)`);
break;
case 'cost':
totalUsd = event.cost.totalUsd ?? totalUsd;
console.log(`\n$${totalUsd.toFixed(4)} so far`);
break;
case 'error':
console.error(`error: ${event.message}`);
break;
}
}
await handle.done;Use the exported sumCost / filterEvents helpers — no per-call reduce:
import { createClient, sumCost, filterEvents } from '@a5c-ai/agent-mux';
const sessions = createClient().sessions('claude-code');
const [last] = await sessions.list();
const { events } = await sessions.read(last.sessionId);
const totals = sumCost(events);
console.log(`$${totals.totalUsd.toFixed(4)} · ${totals.totalTokens} tok · ${totals.costEventCount} cost events`);
for (const tr of filterEvents(events, 'tool_result')) {
console.log(`${tr.toolName} took ${tr.durationMs}ms`);
}// Reconnect to an existing session
const resumed = client.run({ agent: 'claude-code', sessionId: last.sessionId, prompt: 'continue' });
// Branch a new session from an existing transcript
const forked = client.run({ agent: 'claude-code', forkSessionId: last.sessionId, prompt: 'try a different approach' });async function runWithBudget(agent: string, prompt: string, maxUsd: number) {
const handle = client.run({ agent, prompt });
let spent = 0;
for await (const ev of handle.events()) {
if (ev.type === 'cost') {
spent = ev.cost.totalUsd ?? spent;
if (spent > maxUsd) { await handle.stop('budget exceeded'); break; }
}
}
return spent;
}# Realtime: filter cost events from JSON stream
amux run --agent claude-code --prompt '...' --json | jq 'select(.type=="cost")'
# Non-realtime: sum cost from a stored session
amux sessions list --agent claude-code --json
amux sessions read --agent claude-code --id <sid> --json \
| jq '[.events[]|select(.type=="cost")|.cost.totalUsd]|add'A single RunOptions.invocation (or CLI --mode) picks where the harness runs:
client.run({ agent: 'claude-code', prompt: '...', invocation: { mode: 'local' } });
client.run({ agent: 'codex', prompt: '...', invocation: {
mode: 'docker', image: 'ghcr.io/openai/codex', volumes: ['/cache:/cache'],
}});
client.run({ agent: 'claude-code', prompt: '...', invocation: {
mode: 'ssh', host: 'user@builder', identityFile: '~/.ssh/id_ed25519',
}});
client.run({ agent: 'gemini', prompt: '...', invocation: {
mode: 'k8s', namespace: 'runners', image: 'ghcr.io/google/gemini-cli',
}});Remote bootstrap (installs amux + harness on the target, then verifies):
amux remote install builder.example.com --harness claude-code
amux remote update builder.example.com --harness codex --mode sshSee docs/13-invocation-modes.md.
The repository ships a Dockerfile that builds an image with one or more harnesses pre-installed via amux install:
docker build --build-arg HARNESSES=claude-code,codex -t amux .
docker run --rm -it -v "$PWD:/workspace" amux run --agent claude-code --prompt '...'@a5c-ai/agent-mux-harness-mock simulates harness subprocesses with no real binary, API key, or network. Use it for deterministic adapter tests.
import { MockProcess, claudeCodeSuccess, WorkspaceSandbox } from '@a5c-ai/agent-mux-harness-mock';
const sandbox = new WorkspaceSandbox();
const proc = new MockProcess(claudeCodeSuccess);
// Drive proc.stdout / stdin; apply scenario.fileOperations against sandbox.Note: There are two different projects named "OpenCode":
- ✅ Supported: anomalyco/opencode - This is the OpenCode project that agent-mux supports
- ❌ Not supported:
opencode-ai/opencode(renamed to charmbracelet/crush) - This project is not supported by agent-mux
When referring to OpenCode in agent-mux documentation and code, we always mean the anomalyco/opencode project.
npm installat the repo root.npm run build— TypeScript build across all workspaces.npm test— vitest unit tests.npm run test:e2e— end-to-end suite.npm run lint— ESLint with a localmax-file-linesrule (400 effective lines).npm run hooks:install— wires.githooks/pre-commit, which runs build + lint + tests on staged changes.
Specs live in docs/ (numbered 01–14); they are the source of truth for API shape and behavior. File a PR with spec updates alongside code changes.
See CONTRIBUTING.md for the manual publish / version-bump flow.
This repo is an npm workspaces monorepo. Each published package has its own README:
@a5c-ai/agent-mux— meta-package (SDK + CLI convenience install).@a5c-ai/agent-mux-core— core types, client, stream engine.@a5c-ai/agent-mux-adapters— built-in harness adapters.@a5c-ai/agent-mux-cli— theamuxcommand-line binary.@a5c-ai/agent-mux-harness-mock— mock harness simulator for tests.
License: MIT.