Multi-backend agent CLI plugin suite — write once, drive Claude / Codex / Gemini interchangeably from any host. Ships three host-specific plugins (each drives the OTHER backends), an OpenAI-API-compatible HTTP facade, two bin/ CLIs, and a reusable JS library under lib/ for embedding the runners directly.
TL;DR — Get an OpenAI-compatible endpoint that routes to whichever underlying CLI you have installed:
npx -p artagon-agent-cli-plugin artagon-openai-server --port 3000 # In any OpenAI SDK: base_url=http://localhost:3000/v1, model="claude-sonnet-4-6"
The major coding-agent CLIs (claude, codex, gemini) speak different protocols, ship different SDKs, and live in different host platforms. This suite provides:
- One library, three backends —
runStatelessTurn(BACKEND_NAMES.X, options)returns aTurnResultregardless of which CLI ran. - One HTTP facade, three backends — point any OpenAI SDK at
localhost:3000/v1and usemodel="claude-…" | "gpt-5" | "gemini-…"(or the explicit<backend>:<model>form). - Three host-specific plugins — install in Claude Code or Codex CLI to drive the OTHER backends from inside the host with slash commands.
| Use case | Install |
|---|---|
| Library / one-off CLI | npm i -g artagon-agent-cli-plugin (or npx) |
| OpenAI-compatible HTTP server | npx -p artagon-agent-cli-plugin artagon-openai-server --port 3000 |
| Claude Code plugin (drive Codex + Gemini from Claude) | claude plugin marketplace add artagon/artagon-agent-cli-plugin then claude plugin install claude@artagon-agent-cli-plugin |
| Codex CLI plugin (drive Gemini + Claude from Codex) | Same marketplace registry, then claude plugin install codex@artagon-agent-cli-plugin (Codex shares the install registry with Claude Code on this machine — see docs/INSTALL.md) |
| Gemini-host plugin (drives Codex + Claude from Gemini) | claude plugin install gemini@artagon-agent-cli-plugin |
Legacy /gemini:* commands (single-backend Gemini ACP) |
See docs/legacy-gemini-plugin.md |
| Homebrew | Pending — brew install artagon/tap/artagon-agent-cli-plugin once the tap is published. See docs/homebrew-tap.md for the publish recipe + version-bump workflow. |
After global install, three binaries are on PATH:
artagon-agent <backend> "<prompt>" [flags] # one-shot dispatch
artagon-openai-server [--port N] [--host H] # OpenAI Chat Completions facade
artagon-stats [--json] [--since <iso>] # cost / token usage aggregatorartagon-stats reads the local cost log written by every dispatch ($XDG_STATE_HOME/artagon-agent-cli-plugin/cost.jsonl by default — see docs/observability.md) and prints a per-backend summary or machine-readable JSON. Use --budget / --budget-usd to assert against a CI budget.
If you're developing the plugin or testing an unreleased build:
git clone https://github.com/artagon/artagon-agent-cli-plugin.git
cd artagon-agent-cli-plugin
pnpm install
pnpm vendor:lib # materializes lib/ into each plugin tree (REQUIRED — see Limitations)
claude plugin marketplace add "$(pwd)"
claude plugin install gemini@artagon-agent-cli-pluginThe marketplace install copies the plugin source tree as-is and does not run npm install. Plugin scripts use #lib/* imports that resolve via a per-plugin package.json. To make these resolve at install-time, the repo-root lib/ is vendored into each plugins/<host>/lib/. This is committed to git but kept in sync via pnpm vendor:lib (CI runs pnpm vendor:lib:check to fail on drift).
artagon-agent claude "Review the staged diff"
artagon-agent codex "Refactor the cache layer" --effort high
artagon-agent gemini "Summarize what changed in HEAD~1..HEAD"
artagon-agent claude "Write a test plan" --json # full TurnResult as JSONartagon-openai-server --port 3000# Any OpenAI SDK speaks to it:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:3000/v1", api_key="unused")
resp = client.chat.completions.create(
model="claude-sonnet-4-6", # or codex / gemini / "<backend>:<model>"
messages=[{"role": "user", "content": "summarize this repo"}],
stream=True, # SSE streaming supported
stream_options={"include_usage": True}, # token tallies in final chunk
)
for chunk in resp:
print(chunk.choices[0].delta.content or "", end="")The facade also supports:
- CORS:
--cors '*'(or single-origin / comma-separated allowlist; or$ARTAGON_FACADE_CORS). Off by default for safety. Required when calling from a browser-based client. - API-key auth:
--api-key sk-...(single or comma-separated; or$ARTAGON_FACADE_API_KEY). Off by default. When set, every/v1/*request must carryAuthorization: Bearer <key>./healthis exempt. - finish_reason mapping: each backend's stop dialect (
end_turn/MAX_TOKENS/tool_use) maps to OpenAI's canonical set (stop/length/content_filter/tool_calls).
After installing the host plugin (Claude Code, Codex CLI, or Gemini), invoke the OTHER backends as slash commands without leaving:
# Inside Claude Code (claude plugin installed):
/codex:prompt "Refactor the cache layer using the strategy pattern"
/gemini:prompt "Summarize this commit"
# Inside Codex CLI (codex plugin installed):
/claude:prompt "Review this PR for breaking changes"
/gemini:prompt "Suggest edge-case tests"
# In a Gemini host (gemini plugin installed):
/claude:prompt "Architect the migration"
/codex:prompt "Write the migration script"
Every turn (HTTP facade or direct dispatch) appends a JSONL row to $XDG_STATE_HOME/artagon-agent-cli-plugin/cost.jsonl. View totals via:
artagon-stats # text summary + 5 most recent
artagon-stats --json --recent 50 # full JSON for toolingOr as slash commands inside any host plugin:
/<plugin>:stats— turns / tokens / wall-clock / per-backend breakdown / estimated $ + cache savings/<plugin>:budget [--limit N | --limit-usd N] [--month]— soft budget vs. used; observability, never blocks a turn
Pricing lives in lib/cost/pricing.mjs (Claude Sonnet/Opus/Haiku, GPT-5/o-series, Gemini Pro/Flash) with prompt-cache discount tiers (Anthropic 10% read / +25% write, OpenAI 50% read). Override the rate table at runtime via $ARTAGON_PRICING_OVERRIDE (JSON).
import { runStatelessTurn } from "artagon-agent-cli-plugin/lib/runners/dispatch.mjs";
import { BACKEND_NAMES } from "artagon-agent-cli-plugin/lib/backends/names.mjs";
const turn = await runStatelessTurn(BACKEND_NAMES.CLAUDE, {
prompt: "Explain this code",
cwd: process.cwd(),
timeoutMs: 5 * 60 * 1000,
onUpdate: (u) => process.stdout.write(u.content?.text ?? ""), // streaming
});
console.log(turn.usage);Each plugin is named for its install host (not for what it drives). The convention: a plugin in host X provides commands that drive the OTHER backends. The host's own backend is what you're already talking to, so it doesn't need a slash command.
| Plugin | Install host | Drives | Cross-pollination commands |
|---|---|---|---|
plugins/claude/ |
Claude Code | Codex + Gemini | /codex:prompt, /gemini:prompt |
plugins/codex/ |
Codex CLI | Gemini + Claude | /gemini:prompt, /claude:prompt |
plugins/gemini/ |
Gemini host | Codex + Claude | /claude:prompt, /codex:prompt |
The runtime under lib/ is CLI-only — no in-process SDKs. Per-CLI argv builders + per-CLI translators map each CLI's stream-json output to a uniform TurnResult. See docs/plugins.md for the model and docs/architecture.md for the layered shape.
All six cross-driver paths (3 hosts × 2 other backends) have been smoke-tested against the real CLI binaries from the marketplace install cache:
| From plugin | → To backend | Result |
|---|---|---|
claude |
codex |
✅ |
claude |
gemini |
✅ |
codex |
claude |
✅ |
codex |
gemini |
✅ |
gemini |
claude |
✅ |
gemini |
codex |
✅ |
(Test methodology: node ~/.config/agents/plugins/cache/.../plugins/<host>/scripts/<other>-prompt.mjs "What is 2+2?" — every path returns 4 plus a usage record.)
- Node.js ≥ 18.18 (uses native subpath imports +
fetch) - At least one of:
claude,codex,geminiinstalled and authenticated for the backends you want to drive - For local OpenAI facade only: nothing else (binds 127.0.0.1)
The plugin does not manage CLI authentication. You must already have run claude /login, codex login, or gemini auth (whichever you intend to drive). The plugin shells out to the binary as the current user; whatever auth state the binary has is what gets used.
| Area | Constraint |
|---|---|
| Marketplace install ≠ npm install | The Claude Code / Codex marketplace install copies the plugin tree as-is and does not follow symlinks or run npm install. The repo-root lib/ is vendored into each plugins/<host>/lib/ to compensate. Run pnpm vendor:lib before testing a local marketplace install; CI's pnpm vendor:lib:check enforces drift-freedom. |
| Schema drift risk | Per-CLI translators (lib/translate/<x>-stream.mjs) parse each CLI's stream-json output. CLI vendors change schemas regularly without semver. We fixed three drift bugs as recently as ffcd43f. If a translator returns null for the agent-message text event, you'll see empty turn output — file an issue with the raw event JSON. |
| One-shot only (today) | runStatelessTurn and the slash commands are stateless — single prompt → single turn → exit. Multi-turn streaming-input variants are roadmap, not shipped. |
| macOS / Linux only | No Windows testing. Symlink-handling, Unix sockets (used by the legacy /gemini:* broker), and cp -R semantics differ enough on Windows that we don't claim support. |
| Conservative event mapping | Translators map only the events they recognize; unknown shapes return null and the runner accumulates no text for them. Trade-off: predictable behavior vs. coverage of vendor-specific events. The runner does not infer from unknown shapes. |
| Cost rates may be stale | Pricing in lib/cost/pricing.mjs is a snapshot. Vendors change rates quarterly; we update on best-effort. Use $ARTAGON_PRICING_OVERRIDE to plug in fresh rates without forking. |
| No native OpenRouter / Bedrock | Routes-via-the-facade only (treat OpenRouter as <openrouter-host>:<openai-compat-model>). Native backend support is roadmap. |
Codex exec --json quirks |
Codex's exec --json requires the working directory to be a git repo (or --skip-git-repo-check). The runner is currently invoked from the caller's cwd; if that's not a git repo, codex will refuse. Pass extraArgs: ["--skip-git-repo-check"] to bypass. |
| Vendored lib/ size | The committed-vendored lib/ adds ~324KB × 3 plugin trees ≈ 1MB to the repo. We accept this cost rather than rely on a build step the marketplace install can't run. |
| Doc | What |
|---|---|
docs/architecture.md |
Layered system overview + key invariants |
docs/plugins.md |
Multi-plugin cross-pollination model |
docs/runners.md |
Stateless runners + dispatcher + TurnResult shape |
docs/openai-facade.md |
OpenAI Chat Completions HTTP facade — endpoints, auth, CORS, finish_reason mapping |
docs/cli-options-research.md |
Per-CLI flag taxonomy (claude/codex/gemini) |
docs/backends/{gemini,codex,claude}.md |
Per-backend specifics |
docs/middleware-architecture.md |
Redaction-first composer + 6 middlewares |
docs/observability.md |
Logger / wire-log / OpenTelemetry tracing |
docs/legacy-gemini-plugin.md |
Original /gemini:* commands (broker-shared multi-turn ACP) |
docs/INSTALL.md |
Full install recipes for Claude Code and Codex CLI |
CHANGELOG.md |
Release history |
host (Claude Code / Codex CLI / OpenAI SDK consumer)
│
▼
Plugin shell (commands/<x>-prompt.md) │ bin/artagon-agent │ lib/server/openai-facade
│
▼
runStatelessTurn(BACKEND_NAMES.<X>, options)
│
▼
runners (claude-print / codex-exec / gemini-print)
│ ↓ buildArgs (pure) ← argv from typed config
│ ↓ spawn(<cli>, args) ← subprocess
│ ↓ consumeStreamJson ← line-by-line stream consumer
│ ↓ translate<X>StreamEvent ← pure event mapper to ACP shape
▼
TurnResult { text, toolCalls, toolResults, usage, reason }
Releases are signed via npm provenance + Sigstore (npm publish --provenance over GitHub Actions OIDC) and ship a CycloneDX SBOM as a release artifact:
- npm tarball provenance verifiable with
npm audit signatures - CycloneDX SBOM (JSON + XML) attached to each GitHub release
actions/attest-sbom@v1andactions/attest-build-provenance@v1add separate Sigstore attestations- Triggered by
git tag -s v* && git push --tags(see.github/workflows/npm-publish.yml)
Vulnerability disclosure goes through a private channel — do not file public GitHub issues for security bugs. See SECURITY.md for the full policy:
- Preferred: open a private security advisory
- Or email
security@artagon.dev - 3-business-day acknowledgement, 7-day initial assessment, 90-day default coordinated-disclosure window
The page also indexes the in-repo hardening (CodeQL extended pack, SHA-pinned actions, OIDC provenance, crypto.timingSafeEqual for API keys, mode-0o600 cost log, no stack-trace exposure on HTTP errors, PID-reuse-hardened orphan reaper, three-layer credential redaction with cross-layer-invariant CI test).
Currently working (verified end-to-end):
- Three CLI runners (claude/codex/gemini) — stateless one-shot, with
signal,timeoutMs, AbortController-style cancellation - All 6 cross-driver paths smoke-tested against the marketplace-install cache (see Verified above)
- OpenAI Chat Completions HTTP facade including SSE streaming
- Per-process pid-file orphan tracking
- Multi-backend dispatcher
- 784 passing tests (504 unit + 263 integration + 17 property), 0 typecheck errors, biome clean — see CI badge for live status
Roadmap:
- Streaming-input (multi-turn) variants of the runners
- Homebrew tap
- Native OpenRouter and AWS Bedrock backends (today: route via the OpenAI facade)
- Codex
--skip-git-repo-checkpropagation as a runner config knob - Schema-drift CI canary (run real CLIs against canned prompts to catch translator regressions)
MIT — see LICENSE.