@axiom-lattice/cli-a2a
Multi-provider A2A (Agent-to-Agent) CLI gateway. Wraps local coding agents
(OpenCode, Codex, Claude Code) as spec-compliant A2A servers. Drop a JSON
config, get a fully spec-compliant A2A agent — launchable via npx.
Quick Start
# 1. Start your local OpenCode server
opencode serve
# 2. Launch the A2A gateway
npx @axiom-lattice/cli-a2a --provider opencode --port 3000
# 3. Verify
curl http://localhost:3000/health
# → { "status": "healthy", "agent": "CLI A2A Agent", "provider": "opencode" }
curl http://localhost:3000/.well-known/agent-card.json
# → A2A v0.3 AgentCard JSON
Providers
| Provider | Status | Connection |
|---|---|---|
opencode | ✅ Ready | HTTP API (opencode serve) |
codex | ✅ Ready | CLI subprocess (codex) |
claude | ✅ Ready | CLI subprocess (claude -p) |
Usage
npx @axiom-lattice/cli-a2a [options]
Options
--provider <name> Provider: opencode | codex | claude
--config, -c <path> Path to agent JSON config file
--port, -p <number> A2A server port (default: 3000)
--hostname <addr> Bind address (default: 0.0.0.0)
--advertise-host <host> Hostname for agent card URLs
--agent-name <name> Agent display name
--agent-description <desc> Agent description
--log-level <level> debug | info | warn | error (default: info)
OpenCode Options
--opencode-url <url> OpenCode server URL (default: http://localhost:4096)
--directory, -d <path> Project directory
--model, -m <provider/model> LLM model
--agent <name> OpenCode agent preset
Codex Options
--codex-model <model> Codex model ID (e.g. gpt-4o)
--codex-api-key <key> OpenAI API key
Claude Code Options
--claude-model <model> Claude Code model ID
--claude-api-key <key> Anthropic API key
Gateway Registration
The CLI can auto-register with an @axiom-lattice/gateway server so the
gateway discovers and routes to it as a first-class remote agent.
--register-url <url> Gateway URL to register this agent with
--register-key <key> API key for gateway authentication
--unregister-on-shutdown Remove agent from gateway on exit
npx @axiom-lattice/cli-a2a \
--provider opencode \
--port 3001 \
--register-url http://gateway:8080 \
--register-key sk-a2a-gateway-key \
--unregister-on-shutdown &
On startup the CLI POSTs to {registerUrl}/api/assistants with a
graphDefinition of type a2a_remote pointing to its own agent card.
On exit (if --unregister-on-shutdown) it DELETEs the assistant.
After registration the agent appears alongside all other agents in the
gateway and can be invoked via the standard /api/runs endpoint.
Configuration File
{
"provider": "opencode",
"agentCard": {
"name": "My Coding Agent",
"description": "A coding assistant powered by OpenCode",
"skills": [
{
"id": "code-gen",
"name": "Code Generation",
"description": "Generate and refactor code",
"tags": ["coding", "typescript"]
}
]
},
"server": {
"port": 3000,
"advertiseHost": "localhost"
},
"opencode": {
"baseUrl": "http://localhost:4096",
"projectDirectory": "/path/to/project",
"model": "anthropic/claude-sonnet-4"
},
"features": {
"autoApprovePermissions": true,
"streamArtifactChunks": false
},
"timeouts": {
"prompt": 600000
}
}
Config layers merge in priority order: defaults ← config file ← env vars ← CLI args.
Environment Variables
| Variable | Maps to |
|---|---|
OPENCODE_URL | opencode.baseUrl |
OPENCODE_DIRECTORY | opencode.projectDirectory |
OPENCODE_MODEL | opencode.model |
OPENCODE_AGENT | opencode.agent |
OPENAI_API_KEY | codex.apiKey |
ANTHROPIC_API_KEY | claude.apiKey |
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /health | Health check |
GET | /.well-known/agent-card.json | A2A Agent Card |
POST | /a2a/jsonrpc | A2A JSON-RPC transport |
POST | /a2a/rest | A2A REST transport |
Programmatic API
import {
createA2AServer,
resolveConfig,
registerExecutor,
createOpenCodeExecutor,
createCodexExecutor,
createClaudeExecutor,
SessionBindingStore,
} from '@axiom-lattice/cli-a2a';
// Register executors
registerExecutor('opencode', createOpenCodeExecutor);
registerExecutor('codex', createCodexExecutor);
registerExecutor('claude', createClaudeExecutor);
// Load config
const config = resolveConfig('agent-config.json');
// Start server
const server = await createA2AServer(config);
// Graceful shutdown
process.on('SIGINT', async () => {
await server.shutdown();
process.exit(0);
});
Multi-Turn Conversations
The A2A protocol uses contextId to group messages into a single conversation.
cli-a2a maps each contextId to an underlying CLI session, enabling multi-turn
continuity across all three providers:
| Provider | New Session | Resume Session | Session ID Source |
|---|---|---|---|
| Codex | codex exec --json <prompt> | codex exec resume <thread_id> <prompt> | thread_id from JSONL thread.started event |
| Claude Code | claude -p --session-id <uuid> <prompt> | claude -p --resume <uuid> <prompt> | Generated UUID |
| OpenCode | opencode run --format json <prompt> | opencode run --session <sessionID> <prompt> | sessionID from JSON events |
How it works:
- The A2A client sends a message with a
contextId cli-a2alooks up thecontextIdin the SessionBindingStore- If found: uses the provider's resume command to continue the CLI session
- If not found: creates a new CLI session and saves the binding
- Subsequent messages with the same
contextIdreuse the same CLI session
History is maintained by the underlying CLI tool — no prompt concatenation needed.
Session Persistence
Bindings are persisted to disk so that cli-a2a restarts preserve conversations:
- Default path:
~/Library/Application Support/axiom-lattice/cli-a2a/sessions.json - Format: JSON array of
{ provider, contextId, sessionId, ... } - TTL: 24 hours by default (configurable via
session.ttl) - Cleanup: Expired bindings removed on load and periodically (every 1h)
To disable persistence:
{
"session": {
"persist": false
}
}
To disable session reuse entirely (each request creates a new session):
{
"session": {
"reuseByContext": false
}
}
Session Config
{
"session": {
"reuseByContext": true,
"persist": true,
"storePath": "~/custom/path/sessions.json",
"ttl": 86400000,
"cleanupInterval": 3600000
}
}
| Field | Default | Description |
|---|---|---|
reuseByContext | true | Map contextId to CLI session for multi-turn |
persist | true | Save bindings to disk across restarts |
storePath | ~/Library/Application Support/axiom-lattice/cli-a2a/sessions.json | Custom binding store path |
ttl | 86400000 (24h) | Session TTL in milliseconds |
cleanupInterval | 3600000 (1h) | Cleanup interval in milliseconds |
Architecture
┌──────────────────────────────────────────────────────┐
│ cli-a2a (Express A2A Server) │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌──────────────────┐│
│ │ Agent Card │ │ JSON-RPC │ │ REST ││
│ └─────────────┘ └──────────┘ └──────────────────┘│
│ │ │
│ ┌──────────────────────▼───────────────────────────┐│
│ │ Executor Registry ││
│ │ ┌────────────┐ ┌─────────┐ ┌──────────────────┐ ││
│ │ │ OpenCode │ │ Codex │ │ Claude Code │ ││
│ │ │ Executor │ │ Executor│ │ Executor │ ││
│ │ └─────┬──────┘ └────┬────┘ └───────┬──────────┘ ││
│ └────────┼─────────────┼──────────────┼────────────┘│
│ │ │ │ │
│ ┌────────▼─────────────▼──────────────▼────────────┐│
│ │ SessionBindingStore ││
│ │ contextId → CLI sessionId + file persistence ││
│ └──────────────────────────────────────────────────┘│
└───────────┼─────────────┼──────────────┼─────────────┘
│ HTTP │ spawn │ spawn
┌───────▼────────┐ ┌──▼───────┐ ┌───▼────────────┐
│ opencode serve │ │ codex CLI│ │ claude CLI (-p) │
└────────────────┘ └──────────┘ └────────────────┘
Networking — Exposing to External Networks
The agent runs a local HTTP server. To reach it from outside, use the built-in WebSocket bridge (no SSH tunnel or ngrok required).
Built-in WebSocket Bridge (Recommended)
# Agent — opens outbound WebSocket to gateway
npx @axiom-lattice/cli-a2a \
--provider opencode \
--port 3001 \
--bridge-url ws://gateway:8080/api/a2a/bridge \
--bridge-key sk-xxx \
--register-url http://gateway:8080 \
--register-key sk-xxx
Internal Network (agent) Public (gateway)
┌──────────────────┐ ┌─────────────────────┐
│ cli-a2a :3001 │──WebSocket──→│ /api/a2a/bridge │
│ ↑ │ outbound │ ↑ │
│ │ local call │ │ │ HTTP proxy │
│ │ │ │ /api/a2a/bridge/ │
│ Express A2A │ │ proxy/{agentId}/* │
│ OpenCode/Codex/ │ │ │
│ Claude │ │ A2A_REMOTE agent → │
└──────────────────┘ └─────────────────────┘
How it works:
- Agent opens an outbound WebSocket to gateway (outbound almost never blocked)
- Gateway stores the connection keyed by agent ID
- Gateway exposes
GET/POST /api/a2a/bridge/proxy/{agentId}/* - When the orchestrator calls the proxy URL, gateway pushes the request over WebSocket
- Agent makes a local HTTP call to its Express server, returns the response
The agent's agentCardUrl in registration points to
http://gateway:8080/api/a2a/bridge/proxy/{agentId}/.well-known/agent-card.json.
SSH Reverse Tunnel (Alternative)
ssh -R 3001:localhost:3001 [email protected] -N &
npx cli-a2a --provider opencode --port 3001 \
--advertise-host public-bastion.com \
--register-url http://gateway:8080 --register-key sk-xxx
Same Network
npx cli-a2a --provider opencode --port 3001 \
--advertise-host 192.168.1.42 \
--register-url http://gateway:8080 --register-key sk-xxx
Multi-Project
Run multiple instances on different ports for different projects:
npx @axiom-lattice/cli-a2a --provider opencode -d /proj/frontend --port 3001 &
npx @axiom-lattice/cli-a2a --provider opencode -d /proj/backend --port 3002 &
Package Exports
| Export | Description |
|---|---|
createA2AServer(config) | Start an Express A2A server |
resolveConfig(path?, overrides?) | Merge config layers |
loadConfigFile(path) | Read and parse a JSON config file |
registerExecutor(provider, factory) | Register an executor factory |
createOpenCodeExecutor(config, store) | Create an OpenCode executor |
createCodexExecutor(config, store) | Create a Codex executor |
createClaudeExecutor(config, store) | Create a Claude executor |
SessionBindingStore | contextId → sessionId persistence |
generateContextId() | Generate a new UUID contextId |
logger, LogLevel | Structured logger |
License
ISC