@axiom-lattice/cli-a2a
v0.2.15
Published
Multi-provider A2A (Agent-to-Agent) CLI gateway wrapping local coding agents (OpenCode, Codex, Claude Code) as spec-compliant A2A servers
Maintainers
Readme
@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 JSONProviders
| 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 presetCodex Options
--codex-model <model> Codex model ID (e.g. gpt-4o)
--codex-api-key <key> OpenAI API keyClaude Code Options
--claude-model <model> Claude Code model ID
--claude-api-key <key> Anthropic API keyGateway 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 exitnpx @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-xxxInternal 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-xxxSame Network
npx cli-a2a --provider opencode --port 3001 \
--advertise-host 192.168.1.42 \
--register-url http://gateway:8080 --register-key sk-xxxMulti-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
