feat: bring claude, pi and prime-agent to the opencode standard (+ claude-vllm) #101
Reference in New Issue
Block a user
Delete Branch "feat/agent-parity"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Brings Claude Code, pi and prime-agent up to the standard the opencode addon
set, and adds
claude-vllm. Four commits, each independently reviewable.1.
claude-vllm— Claude Code on the homelab LLMllm.ad.itaz.euis LiteLLM in front of vLLM, and LiteLLM already serves theAnthropic Messages API on
/v1/messages— verified with a real completion. Sothere is no bridge to build; pointing
ANTHROPIC_BASE_URLat it is the wholeintegration.
claude-vllmreuses whatever another agent is already configured with(
~/.pi/agent→~/.prime/agent→ opencode's config, first hit wins), takingthe base URL and credential from the same source so one gateway's URL is never
paired with another's key.
Two settings that are easy to miss:
ANTHROPIC_SMALL_FAST_MODEL/ANTHROPIC_DEFAULT_HAIKU_MODEL— otherwise thebackground and summarisation calls ask for a real Haiku the gateway does not
serve, and every one 404s.
CLAUDE_CODE_MAX_CONTEXT_TOKENSfrom the provider's declaredcontextWindow—Claude Code assumes 200k for models it has no table for, and
deepseek-v4-*is 393k, so it would auto-compact at half capacity.
On
claude-mcpctl: mcpctl's LLM layer is a client, not a server. mcpdserves
/api/v1/llms(management) and its adapters call out to providers forgating, prompt selection and agent chat; nothing serves
/v1/messages. RoutingClaude through mcpctl would mean adding an Anthropic passthrough that re-wraps
LiteLLM — worth doing only if mcpctl in the LLM path buys something of its own
(per-project gating of model calls, prompt audit, budgets), which is an mcpd
endpoint rather than a wrapper script.
2. One constant
mcpctlMCP entry for Claude Codeconfig claudenamed the.mcp.jsonentry after the project, and the file ismerged — so configuring a second project left the first mounted alongside it.
Every project you had ever configured stayed connected, with duplicate tool
names and nothing saying which was active.
The entry is now always
mcpctl; switching rewrites what sits behind it. ClaudeCode can reconnect an existing MCP server from
/mcp, so a switch lands withoutrestarting, and the tool prefix stays stable. Legacy entries are retired on the
next run, recognised by the pairing that makes it safe: our command, named
after the very project it bridges to. A hand-configured server is never touched.
Shaping lives in
config/claude-mcp.tsas pure functions, so the merge,migration and active-project detection are unit-tested rather than inferred from
a command's side effects. Also adds
--dry-runand--skip-marker.3. Claude Code: status line +
/mcpctlswitcherThe two things every other host had and Claude Code did not.
mcpctl statuslineresolves the project from.mcp.json, falling back to a.mcpctl-projectmarker. It reads the directory from the JSON Claude Codepipes in, so it follows
/cwd. Prints nothing when no project is active./mcpctl [project]switches and reminds you to reconnect from/mcp.allowed-toolsis scoped to four exactmcpctlinvocations.Three things found by running it, not by reasoning:
settings.jsonand strips unknown keys fromstatusLine— the_mcpctl_managedmarker came back gone. Ownership is nowdecided by the command string. (Hooks keep their marker; statusLine does not.)
A composed line like
my-prompt && mcpctl statuslineis deliberately notclaimed.
!`…`block in a slash command is permission-checked againstallowed-tools. Omittingstatuslinefailed the whole command before themodel saw anything. A test now asserts every pre-executed command is covered.
ANTHROPIC_AUTH_TOKENandANTHROPIC_API_KEYmakes Claude Codewarn that auth may not work.
claude-vllmnow sets only the former.Plus a pre-existing test-isolation bug this work would have made worse:
config claudewrote into the developer's real~/.claudewhen the suite ran —which is how an untagged duplicate of the skills-sync
SessionStarthook gotthere. Everything now honours
CLAUDE_CONFIG_DIR(Claude Code's own override:correct behaviour first, isolation second),
config claudegains--claude-dir, and the suite is verified to leave~/.claudebyte-identical.4. prime-agent switcher extracted; pi
--dry-run; docsThe prime-agent switcher existed only as a 275-line string literal, so
nothing typechecked or linted it — the exact gap that let a wrong
ctx.ui.select()shape ship in the pi extension. It now lives atsrc/prime-agent-ext/mcpctl-switch.tswith a generator, a tsconfig checking itagainst the real
@earendil-works/pi-coding-agenttypes, eslint coverage and anembed-freshness test.
The extraction was verified byte-identical before any edit. Linting then
found six problems in code nothing had ever checked — object-truthiness null
guards, a nullable-string conditional, a missing return type. All
behaviour-preserving, but exactly the class of thing that ships silently.
Also:
config pi --dry-run(the last agent without it); the SessionStart hookinstaller drops untagged duplicates of its own exact command; and
docs/claude-integration.md+docs/prime-agent-extension.md, the twointegrations with no page.
Deliberately not done
prime-agent keeps its per-project MCP entry name. Its switcher already
unmounts the previous project, so it never accumulates entries the way
config claudedid — the bug the constant name fixes does not exist there. Theremaining gap is tool-prefix stability. Changing it would also re-key
auth.jsonfrommcp:<project>tomcp:mcpctl, giving up per-project tokencaching and needing a migration. Reasoning is written up in
docs/prime-agent-extension.mdrather than quietly skipped.Verification
Live, in a real Claude Code session: status line renders
mcpctl:homeautomation;/mcpctl docmostswitches and the line updates tomcpctl:docmostin the samesession;
claude-vllm --model deepseek-v4-fastcompletes against the homelab.683 tests pass (57 files).
tsc --buildplus all three extension typechecks(
pi-ext,opencode-ext,prime-agent-ext) and the completions check areclean.
~/.claudeverified byte-identical after a full suite run.Parity, after
--dry-runThe gateway at llm.ad.itaz.eu is LiteLLM in front of vLLM, and LiteLLM already serves the Anthropic Messages API on /v1/messages — verified with a real completion. So Claude Code needs no bridge: pointing ANTHROPIC_BASE_URL at it is the whole integration. `claude-vllm` exists only to stop you pasting four exports each time. It reuses what another agent is already configured with — ~/.pi/agent, ~/.prime/agent, then opencode's config, first hit wins — taking the base URL and the credential from the same source so one gateway's URL is never paired with another's key. Beyond the obvious ANTHROPIC_* vars it sets two that are easy to miss: - ANTHROPIC_SMALL_FAST_MODEL / ANTHROPIC_DEFAULT_HAIKU_MODEL, or the background and summarisation calls ask the gateway for a real Haiku it does not serve and every one 404s; - CLAUDE_CODE_MAX_CONTEXT_TOKENS from the provider's declared contextWindow, because Claude Code assumes 200k for models it has no table for — and deepseek-v4-* is 393k, so it would auto-compact at half capacity. On `claude-mcpctl`: mcpctl's LLM layer is a client, not a server. mcpd serves /api/v1/llms (management) and its adapters call out to providers for gating, prompt selection and agent chat; nothing serves /v1/messages. Routing Claude through mcpctl would mean adding an Anthropic-shaped passthrough that re-wraps LiteLLM — worth doing only if mcpctl in the LLM path buys something of its own (per-project gating of model calls, prompt audit, budgets), which is a mcpd endpoint rather than a wrapper script. Verified: `claude-vllm --model deepseek-v4-fast -- -p "..."` completes against deepseek on the homelab, with the unknown-model context warning gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNPmcpctlMCP entry instead of one per project d7055a0953Claude Code had neither of the things opencode, pi and prime-agent all have: a visible active project, and a way to change it from inside a session. It has no plugin API that can draw a widget or open a picker, but it does run a command for its status line and it does load slash commands — which is enough for both. - `mcpctl statusline` prints the active project (from .mcp.json, falling back to a .mcpctl-project marker) and is wired into settings.json. It reads the directory out of the JSON Claude Code pipes in, so it follows /cwd rather than reporting wherever the binary was launched. Prints nothing when no project is active: an empty line beats "none" on every unrelated repo. - `/mcpctl [project]` switches and reminds you to reconnect from /mcp. allowed-tools is scoped to the four exact mcpctl invocations it needs. Three things found by running it rather than reasoning about it: - Claude Code REWRITES settings.json against its own schema and strips unknown keys from `statusLine` — our `_mcpctl_managed` marker came back gone, so ownership is now determined by the command string. (Hooks keep their marker; statusLine does not.) A composed line like `my-prompt && mcpctl statusline` is deliberately not claimed. - Every `!`-prefixed block in a slash command is permission-checked against allowed-tools. Omitting `statusline` failed the whole command before the model saw anything. A test now asserts every pre-executed command is covered. - Setting ANTHROPIC_AUTH_TOKEN *and* ANTHROPIC_API_KEY makes Claude Code warn that auth may not work; claude-vllm now sets only the former and clears an inherited API key. Also fixes a pre-existing test-isolation bug this work would have made worse: `config claude` wrote into the developer's real ~/.claude when the suite ran, which is how an untagged duplicate of the skills-sync SessionStart hook got there. Both the hook installer and the new UI installers now honour CLAUDE_CONFIG_DIR (Claude Code's own override — correct behaviour first, isolation second), `config claude` gains --claude-dir for parity with --pi-dir and --opencode-dir, and the suite is verified to leave ~/.claude byte-identical. Verified live: status line renders `mcpctl:homeautomation`, `/mcpctl docmost` switches and the line updates to `mcpctl:docmost` in the same session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNPThe prime-agent switcher existed only as a 275-line string literal inside prime-agent-extension.ts, so nothing typechecked or linted it — the exact gap that let a wrong ctx.ui.select() option shape ship in the pi extension. It now lives at src/prime-agent-ext/mcpctl-switch.ts with a generator, a tsconfig checking it against the real @earendil-works/pi-coding-agent types, eslint coverage and an embed-freshness test, matching pi and opencode. The extraction was verified byte-identical before any edit, so the behaviour shipped today is exactly what was captured. Linting it then found six problems in code nothing had ever checked: object-truthiness null guards, a nullable string conditional and a missing return type. All behaviour-preserving to fix, but exactly the class of thing that ships silently when nothing is looking. Also: - `config pi` gains --dry-run, the last agent without it. - The SessionStart hook installer now drops untagged duplicates of its own exact command — rows left behind before the marker existed, or by a suite that used to write into a real ~/.claude. Invisible in the UI; they just run the sync twice per session. A hook the user wrote is never touched, even one calling `mcpctl skills sync` with different flags. - docs/claude-integration.md and docs/prime-agent-extension.md, the two integrations that had no page. prime-agent deliberately keeps its per-project MCP entry name rather than the constant `mcpctl` claude and opencode now use: its switcher already unmounts the previous project, so it never accumulates entries, and re-keying auth.json from mcp:<project> to mcp:mcpctl would give up per-project token caching and needs a migration. Documented as its own change rather than folded in here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVwuCjuMoA13gmzYEfcrNPPull request closed