@dustindu/opencode-cabinet
v0.0.2
Published
A shareable agent cabinet for opencode: registers agents, skills, commands, MCP servers and providers from one plugin.
Maintainers
Readme
opencode-cabinet
A single opencode plugin that installs a whole agent cabinet — 8 specialist agents, 40 skills, 12 commands, MCP servers, model providers, the shared L1 norms, and a hard guard that keeps multi-step work state on disk — and wires in its supporting tools for you: the codebase-memory-mcp code graph and the opencode-mem persistent memory.
Instead of copying a pile of files into ~/.config/opencode/, you install one package. On first load it assembles the rest.
receive → triage → dispatch → collect → review → accept → reportWhat you get
| Piece | Count | Notes |
|---|---|---|
| Agents | 8 | pmo (lead) · scout · researcher · planner · builder-lite · builder · designer · critic |
| Skills | 40 | engineering domains, design, research, planning, review |
| Commands | 12 | slash commands |
| MCP servers | 2 by default | context7, gh_grep; MCP_DOCKER opt-in (the codebase-memory-mcp entry is owned by cbm itself) |
| Providers | 4 | the cch* relay providers + their models |
| Norms | L1 | injected into every agent's system prompt |
| Guard | — | .pmo/ single-writer enforcement + plan-format validation |
pmo becomes the default agent. It dispatches; it doesn't write product code.
Install
- Add the plugin to your opencode config:
opencode plugin add @dustindu/opencode-cabinet- Configure it with the wizard:
cabinet init # or: npx @dustindu/opencode-cabinet initIt asks for your relay key, lets you pick a model per agent (prefilled from your
current opencode.json), and stores the embedding key — see
Configure. Prefer environment variables instead?
export CABINET_API_KEY=sk-...- Reload. That's it — the first load runs the assembly in the background.
The bundled providers point at a private relay (
cch.tianyou.space). You need a key for that relay, or editcontent/providers.jsonto point at your own OpenAI-compatible endpoint. Without a key the cabinet still installs — agents just fall back to opencode's default model.
Install from a local checkout
{
"plugin": ["/absolute/path/to/opencode-cabinet"]
}(opencode resolves a local directory via <dir>/server then <dir>/index; this
repo ships a root index.ts for exactly that.)
The assembly (cbm + memory)
On first load the plugin runs a one-time assembly, in this order:
- cbm — if
codebase-memory-mcpis missing it runs the official installer (curl … install.sh | bash); then lets cbm configure its own opencode surface (MCP entry, skill, agents, AGENTS.md block). cbm owns that surface and keeps it up to date. - repair — cbm writes a V1-shaped
plugins/cbm-augment.tsthat opencode v2 cannot load. The assembly strips cbm's managed block (or writes the V2 module when cbm wrote one that is V1-only, or when the file is absent). - memory — adds the upstream
opencode-memplugin at a pinned spec (seememorySpec), replacing any other memory entry in the config's plugin list. A fork placed in theplugins/directory is loaded by opencode itself and is invisible to that list —cabinet doctorreports it instead. - memory config — writes
~/.config/opencode/opencode-mem.jsoncfrom the bundled template only if you don't have one, preconfigured for remote embeddings (no local model download) and an opencode provider for extraction. - guard plugins — writes the bundled
templates/plugin/*.ts(model fallback, comment / read-before-write / tool-output / empty-task-retry guards) into~/.config/opencode/plugins/, only where one is not already present — your edits in those files are never taken back. Their tuning lives incabinet.jsonc:model-fallback.tsreads each agent'sfallbackfrom theagentsroster and the thresholds / error signatures / last-resort model from thefallbackblock, preferringcabinet.jsoncover a leftovermodel-fallback-config.json(read for compatibility only). A fresh machine then assembles into the same posture with no files copied by hand. Turn off with"guardPlugins": false.
A marker at ~/.config/opencode/.cabinet/state.json records the result.
cabinet init # interactive configuration wizard
cabinet models # resolve every agent's primary + fallback and show which file set them
cabinet status # last assembly result
cabinet doctor # read-only probe of cbm / memory / augment / guard-plugin state
cabinet setup # reset the marker and reload to re-run the assembly
cabinet sync # update the live guard plugins to what this package ships (backs up)Set "assemble": false in cabinet.jsonc to manage cbm/memory yourself —
cabinet then only registers its own content.
Configure — cabinet init
cabinet init is an interactive wizard that writes
~/.config/opencode/cabinet.jsonc:
- Relay API key — one key, shared by the bundled
cch*providers. - A model per agent — for the 8 cabinet agents and opencode's built-in
general/exploresubagents. Defaults are prefilled from your currentcabinet.jsonc, else each agent's own frontmatter (and, for the two built-ins, the shippedcontent/roster.json), so pressing Enter keeps everything as-is. Pick by number from the printed catalog, or type anyprovider/model; if the chosen model has reasoning variants you're asked which one. Each agent is then asked for a fallback model — what themodel-fallbackguard plugin switches to when the primary errors. The whole roster is written out in full, so one block holds every agent's primary, variant and fallback. - Embedding key — for
llm.tianyou.space(memory). The model is fixed atBAAI/bge-m3(1024-dim); only the key is asked.
Keys are stored in plaintext in cabinet.jsonc (and the embedding key in
opencode-mem.jsonc) — the same way opencode.json stores its own provider
keys. Anything overwritten is backed up to <file>.bak-<timestamp> first, and
nothing is written until you confirm.
cabinet init # interactive
cabinet init --yes # accept all defaults, no prompts
cabinet init --print # print the generated config to stdout, write nothing
cabinet init --key sk-... # supply the relay key non-interactively
cabinet init --embed-key sk-.. # supply the embedding key non-interactively
cabinet init --no-neutralize # don't touch opencode.json's agent block
cabinet init --project # write <cwd>/.opencode/cabinet.jsonc instead
cabinet init --reload # run `cabinet setup` when doneBecause cabinet registers after opencode's config loader, its model choices win
over an agent block in opencode.json. The wizard therefore offers to remove
the now-redundant agent.general / agent.explore entries for you.
Why not on install? opencode installs plugins with npm's engine but forces
ignoreScripts, so a packagepostinstallnever runs. The wizard is therefore an explicit command — and the plugin prints a reminder on load while no key is configured.
Customize
You can also edit ~/.config/opencode/cabinet.jsonc (or
<project>/.opencode/cabinet.jsonc) by hand — see cabinet.example.jsonc.
Highlights:
{
"assemble": true, // run the cbm/memory assembly
"cbm": "auto", // "auto" | "hint" | "off"
"memory": true, // install/keep opencode-mem
"memorySpec": "[email protected]",
// The roster: one entry per agent, primary and fallback together.
// "model" primary, "provider/model"
// "variant" reasoning level, one of that model's variants
// "fallback" model to switch to when the primary errors, "provider/model#variant"
// Keys may be cabinet agents OR opencode built-ins (general/explore). This is
// the single place to maintain the model system: an entry wins over the agent's
// frontmatter default, so re-routing a model never needs a package release.
// Drop an entry to fall back to that agent's shipped default; drop only its
// "fallback" to use "fallback.defaultFallback" instead.
// `cabinet models` prints every agent's primary + fallback and which source
// won (env > agents > models > frontmatter), and flags a dangling reference.
"agents": {
"pmo": { "model": "cch-codex/gpt-6.1-sol", "variant": "medium", "fallback": "cch-response/deepseek-v4.1-flash#high" },
"builder": { "model": "cch-response/deepseek-v4.1-flash", "variant": "high" },
"general": { "model": "cch-response/muse-spark-1.3-contributor", "variant": "xhigh" },
"explore": { "model": "cch-open/gemini-3.8-flash" }
},
// Tuning for the bundled model-fallback guard plugin (thresholds, error
// signatures, last-resort model). Per-agent chains live in "agents" above.
// A fallback naming the very model that is failing is not a fallback: the
// plugin then tries "defaultFallback", and if that is the same model too it
// logs `no-distinct-fallback` and leaves the session alone instead of switching
// in a circle. `cabinet doctor` and `cabinet models` flag that shape up front.
"fallback": {
"defaultFallback": { "providerID": "cch-response", "modelID": "deepseek-v4.1-flash", "variant": "high" },
"retryThreshold": 3, "cooldownMs": 120000
},
"mcp": { "context7": true, "gh_grep": true, "MCP_DOCKER": false }
}~/.config/opencode/cabinet.jsonc and a project's .opencode/cabinet.jsonc are
merged: scalars from the project layer win outright, and the maps (agents,
models, mcp, embedding) merge key by key — so a project file that names one
agent does not erase the other nine. Everything is read when the plugin loads:
editing any of these files takes effect on the next opencode restart.
The provider catalog is additive
content/providers.json describes the cch* relay: its providers, models,
context limits and reasoning variants. A fresh install self-registers that whole
system. Each agent's default primary lives in its own frontmatter
(content/agent/<id>.md); content/roster.json holds only what has no other home —
every agent's default fallback, plus the primaries of opencode's built-in
general / explore. cabinet init reads both and writes the whole roster into
cabinet.jsonc.
Your own declarations always win. Cabinet reads the provider block of your
opencode.json / opencode.jsonc (global and project) and only supplies what
you left out:
| You write | Result |
|---|---|
| nothing | the catalog's provider, models, limits and variants are used |
| provider.cch-open.options.apiKey | your key is used, the catalog's baseURL still applies |
| provider.cch-open.models.glm-5.3.limit | your limit wins, the model's other fields stay catalog-supplied |
| provider.cch-open.models.glm-5.3.variants.turbo | your variant is added next to the catalog's |
| provider.my-own-provider | untouched — cabinet never writes providers it doesn't ship |
opencode drops a whole
providerblock that fails its schema check, with only a warning in the log. Declare alimitwith bothcontextandoutput.
Environment overrides
| Var | Effect |
|---|---|
| CABINET_API_KEY | relay key for the bundled providers (a literal apiKey in cabinet.jsonc wins over it) |
| CABINET_MODEL_<AGENT> | override an agent's model id (e.g. CABINET_MODEL_PMO=cch-open/glm-5.3) — wins over cabinet.jsonc and frontmatter |
| CABINET_EMBED_URL / CABINET_EMBED_MODEL / CABINET_EMBED_DIM | memory embeddings (applied at setup time) |
| CABINET_MEM_PROVIDER / CABINET_MEM_MODEL | memory extraction model (applied at setup time) |
| CABINET_EMBED_KEY | memory embedding key (kept live via env:// in the generated config) |
| CBM_BIN | path to the codebase-memory-mcp binary |
| CABINET_WORKSPACE | value for {env:CABINET_WORKSPACE} (docker MCP gateway) |
Optional dependencies
The cabinet degrades gracefully when these are missing:
| Feature | Needs | Without it |
|---|---|---|
| Code graph tools | codebase-memory-mcp binary (auto-installed) | graph nudges are skipped, L1 code-graph block is pruned |
| Docker MCP gateway | Docker + docker mcp | enable MCP_DOCKER in config |
| Persistent memory | the opencode-mem plugin (auto-added) | the L1 "Memory" line is pruned automatically |
| Browser | the ego-browser skill | not bundled — install it separately; web open / screenshot falls back to opencode's built-in tools |
How it works
opencode v2 loads its own agent / mcp / skill / command / provider
config through plugins — the built-ins are opencode.config.agent,
opencode.config.mcp, etc. A user plugin gets the same context, so this package
registers everything programmatically from the bundled content/ directory:
src/index.ts orchestrator (registers content, triggers the assembly)
src/setup.ts the assembly: ensureCbm / repairCbmAugment / ensureMemory / writeMemoryConfig
src/content.ts reads content/ (frontmatter, {env:VAR} substitution)
src/config.ts reads cabinet.jsonc
src/guard.ts .pmo/ single-writer guard (hard block via execute.before)
src/instruction.ts injects content/AGENTS.md into every session
bin/cabinet.mjs control CLI (init / setup / doctor / models / sync / status)
bin/init.mjs the configuration wizard
bin/lib.mjs shared CLI helpers
templates/ opencode-mem.jsonc + the repaired cbm-augment.ts + the guard plugins
content/ the single source of truth (agents, skills, commands, AGENTS.md,
providers.json = the model catalog, roster.json = default fallbacks
+ the built-ins' primaries, mcp.json)Compatibility
Built and verified against opencode v2.0.23–v2.0.24. The ctx.*.transform API
used here is real but not formally documented, and opencode is moving fast — a
future release may change it. If the cabinet stops loading, check the opencode
log for loading plugin and any [opencode-cabinet] errors.
License
MIT
