feat: OpenCode feature parity — plugin + skills + hooks - #616
Conversation
|
Huge thanks for opening this PR and for the work you put into it. The maintainer shop is currently full, so this may sit for a bit before it gets a proper review. We will come back to this as soon as possible with real feedback; I wanted to make sure it did not sit unacknowledged in the meantime. |
Add full OpenCode integration matching Claude Code parity: - TypeScript plugin (tool.execute.after for grep/glob graph augmentation, experimental.chat.system.transform for session reminder) - Consolidated codebase-memory skill installation - Plugin auto-discovered from ~/.config/opencode/plugins/ (no config entry needed) - Skill auto-discovered from ~/.config/opencode/skills/ Design note: uses tool.execute.after (NOT .before as proposed in DeusData#585) because source verification against anomalyco/opencode proved .before output mutation has no effect — the tool uses original args, not output.args. The .after hook mutates output.output which is model-visible. Non-blocking: all plugin failures silently swallowed (consistent with Claude Code hook gate script behavior). TOCTOU-safe fchmod pattern. Defensive binary-path quote rejection (security). 5 new tests: plugin install/idempotent/quote-reject/remove, skills install. Closes DeusData#585 Signed-off-by: ZeR020 <ZeR020@users.noreply.github.com>
98a9684 to
7116a76
Compare
|
Reviewed alongside the two other agent-install PRs in the queue, since all three occupy the same area. Half of this has shipped; the other half is a direction question. The skills half is now on main. Commit The plugin half is genuinely novel — Why the plugin is a maintainer call rather than a merge:
I have put that to the maintainer. The instruction content itself is clean — I checked specifically, since shipped agent-facing text is content an agent will follow, and yours is product-consistent steering with no goal manipulation and no leaked personal context. Two defects to fix if it does go ahead:
Smaller: under Your tests are good — five round-trips covering fresh install, idempotent overwrite, quote rejection, and remove-then-remove-again. Worth saying, because install code is easy to leave untested. Please hold off reworking until the direction answer arrives; the whole block needs re-basing onto the |
|
Thank you for this, and I am sorry it took as long as it did to come back with a real answer. The decision: OpenCode stays an MCP-only integration, so we are not taking the plugin. The reasoning is about what this project ships rather than about the quality of your work, and I want to give you the whole of it. Why
There is a second reason, specific to the mechanism: the plugin binds us to OpenCode's What OpenCode users already getThis is the part that made the decision easier, and it is worth stating plainly: OpenCode already has the full tool surface. So what your plugin adds is not capability — it is the automatic grep/glob reflex. Real value, but a narrower gap than it first appears, and the way we deliver that elsewhere is through each client's own native hook configuration (Claude Code, Codex, Copilot, Gemini, Factory each have one). OpenCode does not have an equivalent, which is exactly why you had to write a plugin. That is a gap in OpenCode's extension model, not something we want to route around by shipping a runtime artifact. Two things you should know regardlessThe skills half of your PR already shipped. Main installs the OpenCode skill and its uninstall path, with tests, and went further with an agent profile — And the plugin does not currently run. I checked empirically rather than assuming: the payload sends One more that would have bitten a Windows user: the If the ground shiftsIf OpenCode ever ships a declarative hook configuration like Claude Code's or Codex's, this becomes a straightforward addition and we would want it — I will come back to this PR and to you if that happens. Thank you again. Reverse-engineering the hook points from their source, honouring abort signals, and scanning stdout for the last parseable JSON line rather than trusting the first are all the instincts of someone who has debugged this class of integration before. |
|
Following up because the decision changed. The OpenCode augmentation is being implemented — #1392, with When I closed this, the objection was shipping a Your hook-point research is what made this possible at all. Working out that Two defects from the original are closed by construction, and both are pinned by tests — mentioning them because they are worth knowing if the code lives on in your own fork:
One thing I recorded rather than papered over: Thank you for the original work, and for the care in it. |
pi has no MCP client, and OpenCode has no declarative hook configuration -- verified against OpenCode's own plugin documentation, which states hooks are available only through JavaScript/TypeScript plugin modules. For those two clients a module is the only extension point. We do not want to ship such a module as a repository asset. DeusData#534 proposed one embedding 380 lines of TypeScript as C string literals and registering 7 of the 15 registry tools; every tool added afterwards would have been silently missing for that client. Hand-maintained copies of the tool list have already produced defects here (DeusData#1361, and the smoke-invariants count). So the module is GENERATED from the live registry instead. cbm_client_adapter_pi walks cbm_mcp_tool_count()/cbm_mcp_tool_name() and registers every tool, which makes drift structurally impossible rather than merely discouraged: adding a tool to TOOLS[] adds it to every generated adapter with no second edit. Nothing in the repository is a .ts file. Both emitters wrap their output in ownership markers so a caller can rewrite its own block and leave a user-authored file alone. Two defects from the prior proposals are closed by construction: - Path escaping. DeusData#616's template rejected the double quote but not the backslash, so a Windows home like C:\Users\urs\bin produced an invalid unicode escape and the whole auto-loaded plugin failed to parse, which is worse than an absent plugin. cbm_client_adapter_escape_js escapes backslash, quote, newline and CR, and fails closed rather than emitting a truncated literal; generation aborts if the path cannot be escaped. - The silent no-op. DeusData#616's payload omitted hook_event_name, which hook-augment requires and without which it accepts nothing, so the plugin emitted zero bytes for six weeks. The generated payload carries it and a test pins it. Recorded honestly: the OpenCode emitter hooks tool.execute.after, whose ability to modify a tool's output is NOT part of OpenCode's documented plugin contract (only tool.execute.before's argument mutation is). If they change it the augmentation stops with no error. That risk is accepted deliberately and the emitter says so in a comment, so a future reader does not have to rediscover it. This commit adds the generator and its tests only; wiring into the install and uninstall routines follows separately so the two are reviewable apart. Tests: every registry tool appears in the pi module (revert-checked by simulating the 7-of-15 subset, which fails); Windows/quote/newline escaping and its truncation boundary; the OpenCode payload carries hook_event_name and registers no tools; NULL/empty binary paths generate nothing. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Wires the registry-driven generators into install, plan and uninstall. pi gets ~/.pi/agent/extensions/cbmem.ts, its only route to the graph -- pi has no MCP client. OpenCode gets ~/.config/opencode/plugins/cbm-augment.ts, which adds no tools (it already reaches all of them over MCP) and supplies only the automatic graph lookup before a grep/glob that other clients get from their own hook configuration. OpenCode has none; its plugin system is the only extension point, verified against their plugin documentation. Both go in as a MARKED BLOCK rather than a whole-file write. Those directories are auto-loaded, so a user may legitimately keep their own module there, and an install routine that clobbers it is destroying user content. Uninstall removes only our block and leaves the rest of the file, matching every other uninstall path here. A generator returning NULL is a hard error rather than a skip. A silently absent extension is precisely the failure that left the earlier proposal a no-op for six weeks, so it must surface as an install error instead of a quiet success. Note for reviewers: the generated body deliberately carries NO ownership markers. cbm_text_upsert_managed_block adds them itself and rejects content that already contains them -- an existing guard test (test_cli.c:6018) caught that integration mistake before CI did, which is the test doing exactly its job. Reimplements the ideas from two community PRs, both closed as superseded by this work, with credit to their authors: - DeusData#534 (@Tensorboyalive) proposed the pi extension. It embedded 380 lines of TypeScript as C string literals and registered 7 of the 15 registry tools; this generates from the registry so the surface cannot drift, and ships no .ts in the repository. Their marker-delimited editing, their uninstall test asserting the markers are GONE, and their defensive stdout parsing (take the last line that parses, not the first) are all carried forward here. - DeusData#616 (@ZeR020) proposed the OpenCode plugin. Its payload omitted hook_event_name, which hook-augment requires, so it produced zero bytes; and its path template rejected the double quote but not the backslash, so a Windows home like C:\Users\urs\bin produced an invalid unicode escape and the auto-loaded plugin failed to parse. Both are closed by construction here and pinned by tests. Their hook-point research is what made the OpenCode side possible at all. Tests: install writes both extensions and the dry run does not; uninstall removes our block while preserving a user's own content in the same file. Co-Authored-By: Tensorboyalive <Tensorboyalive@users.noreply.github.com> Co-Authored-By: ZeR020 <ZeR020@users.noreply.github.com> Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Summary
Closes #585. Adds full OpenCode feature parity with Claude Code:
~/.config/opencode/plugins/cbm-augment.ts) — auto-discovered, noopencode.jsonentry neededcodebase-memoryskill (~/.config/opencode/skills/codebase-memory/SKILL.md) — same skill content as Claude CodeDesign Decision:
tool.execute.after(not.before)Issue #585 proposed
tool.execute.beforefor context injection. However, source verification againstanomalyco/opencodeproved that.beforeis non-functional for this purpose:tool.execute.before(packages/opencode/src/session/tools.ts:82):args. Output{ args }is a fresh object; mutation has zero effect — the tool uses the originalargsvariable, not the mutated output.tool.execute.after(packages/opencode/src/session/tools.ts:92):output.output(type:string) is model-visible tool result text.output.outputin place IS model-visible.input.argsavailable (haspatternfor grep/glob).This PR uses
.afterso that graph context is appended to the tool result the model actually sees.How It Works
Plugin (
cbm-augment.ts)tool.execute.afterhook:input.toolis"grep"or"glob"(OpenCode uses lowercase tool names)"Grep"/"Glob"for the agent-agnostichook-augmentbinary<binary> hook-augment, writes{"tool_name":"Grep","tool_input":{"pattern":"..."}}to stdinhookSpecificOutput.additionalContextfrom stdoutoutput.outputwith\n\nseparatorcbm-code-discovery-gate)experimental.chat.system.transformhook:output.system(rebuilt fresh each turn, no accumulation)CMM_SESSION_REMINDER_CMDused by Codex/Gemini/AntigravityInstall/Uninstall
Install path (
install_cli_agent_configs):cbm_install_skills(skills_dir, true, dry_run)— reuses existing functioncbm_upsert_opencode_plugin(home, binary_path, dry_run)— writes plugin with embedded binary path--plan) recordsskills+pluginentries without mutatingUninstall path (
uninstall_cli_agents):cbm_remove_skills(skills_dir, dry_run)cbm_remove_opencode_plugin(home, dry_run)— unlinks plugin file (unlike Claude Code which leaves inert scripts, OpenCode auto-discovers plugins so the file must be deleted)Security
fchmod(fileno(f), ...)beforefclose(f)(same pattern ascbm_install_hook_gate_script)"are rejected (prevents injection into the TypeScriptBIN = "%s"string)Tests
5 new tests in
tests/test_cli.c:cli_upsert_opencode_plugin_fresh— verifies plugin file written with correct binary path, hook names, andsatisfies Plugincli_upsert_opencode_plugin_idempotent— verifies re-install overwrites cleanly with new pathcli_upsert_opencode_plugin_rejects_quote— verifies security: binary paths with"are rejectedcli_remove_opencode_plugin— verifies install → remove → file gone, and idempotent removecli_opencode_skills_installed— verifies SKILL.md written to correct path with correct contentAll 5 pass. 5698 total tests pass. 1 pre-existing failure (
incr_full_indexintest_incremental.c) is unrelated — an RSS memory threshold assertion that's environment-dependent.Files Changed
src/cli/cli.c(+149 lines):opencode_plugin_contentstatic string,cbm_upsert_opencode_plugin(),cbm_remove_opencode_plugin(), install/uninstall path modificationssrc/cli/cli.h(+9 lines): function declarationstests/test_cli.c(+130 lines): 5 new tests +RUN_TESTregistrations