@@ -524,6 +524,58 @@ static const char skill_content[] =
524524 "`direction=\"both\"`.\n"
525525 "5. Results default to 10 per page — check `has_more` and use `offset`.\n" ;
526526
527+ /* ── OpenCode plugin (TypeScript, auto-discovered from ~/.config/opencode/plugins/) ──
528+ * Installs a .ts plugin that wires tool.execute.after (grep/glob graph augment)
529+ * and experimental.chat.system.transform (session reminder). The binary path is
530+ * embedded at install time via fprintf %s — same pattern as the Claude Code
531+ * hook gate script. Uses tool.execute.after (NOT .before) because .before output
532+ * mutation has no effect on the tool result the model sees (verified against
533+ * anomalyco/opencode source: packages/opencode/src/session/tools.ts). */
534+ static const char opencode_plugin_content [] =
535+ "import type { Plugin } from \"@opencode-ai/plugin\"\n"
536+ "import { spawn } from \"child_process\"\n"
537+ "\n"
538+ "const BIN = \"%s\"\n"
539+ "\n"
540+ "export default (async () => {\n"
541+ " return {\n"
542+ " \"tool.execute.after\": async (input: { tool: string; args: any }, "
543+ "output: { output: string }) => {\n"
544+ " if (input.tool !== \"grep\" && input.tool !== \"glob\") return\n"
545+ " const pattern = input.args?.pattern\n"
546+ " if (!pattern) return\n"
547+ " const toolName = input.tool === \"grep\" ? \"Grep\" : \"Glob\"\n"
548+ " try {\n"
549+ " const payload = JSON.stringify({ tool_name: toolName, "
550+ "tool_input: { pattern } })\n"
551+ " const child = spawn(BIN, [\"hook-augment\"], "
552+ "{ stdio: [\"pipe\", \"pipe\", \"ignore\"] })\n"
553+ " child.stdin.write(payload)\n"
554+ " child.stdin.end()\n"
555+ " let stdout = \"\"\n"
556+ " child.stdout.on(\"data\", (d: Buffer) => { stdout += d.toString() })\n"
557+ " await new Promise<void>((resolve) => {\n"
558+ " child.on(\"close\", () => resolve())\n"
559+ " child.on(\"error\", () => resolve())\n"
560+ " })\n"
561+ " if (!stdout.trim()) return\n"
562+ " const parsed = JSON.parse(stdout)\n"
563+ " const ctx = parsed?.hookSpecificOutput?.additionalContext\n"
564+ " if (typeof ctx === \"string\" && ctx.length > 0) {\n"
565+ " output.output += \"\\n\\n\" + ctx\n"
566+ " }\n"
567+ " } catch (e) {}\n"
568+ " },\n"
569+ " \"experimental.chat.system.transform\": async (_input: any, "
570+ "output: { system: string[] }) => {\n"
571+ " output.system.push(\"Code discovery: prefer codebase-memory-mcp "
572+ "(search_graph, trace_path, get_code_snippet, query_graph, search_code) "
573+ "over grep/file-read; run index_repository first if the project is not "
574+ "indexed.\")\n"
575+ " }\n"
576+ " }\n"
577+ "}) satisfies Plugin\n" ;
578+
527579static const char codex_instructions_content [] =
528580 "# Codebase Knowledge Graph\n"
529581 "\n"
@@ -1668,6 +1720,67 @@ int cbm_remove_opencode_mcp(const char *config_path) {
16681720 return rc ;
16691721}
16701722
1723+ /* ── OpenCode plugin install/remove ──────────────────────────────
1724+ * Writes the TypeScript plugin to ~/.config/opencode/plugins/cbm-augment.ts.
1725+ * OpenCode auto-discovers .ts files in this directory — no opencode.json entry
1726+ * needed. The plugin wires tool.execute.after (grep/glob graph augment) and
1727+ * experimental.chat.system.transform (session reminder). Follows the same
1728+ * TOCTOU-safe pattern as cbm_install_hook_gate_script: fchmod before fclose. */
1729+
1730+ #define CMM_OPENCODE_PLUGIN_NAME "cbm-augment.ts"
1731+
1732+ int cbm_upsert_opencode_plugin (const char * home , const char * binary_path , bool dry_run ) {
1733+ if (!home || !binary_path ) {
1734+ return CLI_ERR ;
1735+ }
1736+ if (strchr (binary_path , '"' ) != NULL ) {
1737+ return CLI_ERR ;
1738+ }
1739+ char plugins_dir [CLI_BUF_1K ];
1740+ snprintf (plugins_dir , sizeof (plugins_dir ), "%s/.config/opencode/plugins" , home );
1741+ if (!dry_run ) {
1742+ cbm_mkdir_p (plugins_dir , CLI_OCTAL_PERM );
1743+ }
1744+
1745+ char plugin_path [CLI_BUF_1K ];
1746+ snprintf (plugin_path , sizeof (plugin_path ), "%s/%s" , plugins_dir , CMM_OPENCODE_PLUGIN_NAME );
1747+
1748+ if (dry_run ) {
1749+ return 0 ;
1750+ }
1751+
1752+ FILE * f = fopen (plugin_path , "w" );
1753+ if (!f ) {
1754+ return CLI_ERR ;
1755+ }
1756+ (void )fprintf (f , opencode_plugin_content , binary_path );
1757+ #ifndef _WIN32
1758+ fchmod (fileno (f ), CLI_OCTAL_PERM );
1759+ #endif
1760+ (void )fclose (f );
1761+ #ifdef _WIN32
1762+ chmod (plugin_path , CLI_OCTAL_PERM );
1763+ #endif
1764+ return 0 ;
1765+ }
1766+
1767+ int cbm_remove_opencode_plugin (const char * home , bool dry_run ) {
1768+ if (!home ) {
1769+ return CLI_ERR ;
1770+ }
1771+ char plugin_path [CLI_BUF_1K ];
1772+ snprintf (plugin_path , sizeof (plugin_path ), "%s/.config/opencode/plugins/%s" ,
1773+ home , CMM_OPENCODE_PLUGIN_NAME );
1774+ if (dry_run ) {
1775+ return 0 ;
1776+ }
1777+ struct stat st ;
1778+ if (stat (plugin_path , & st ) == 0 ) {
1779+ return cbm_unlink (plugin_path );
1780+ }
1781+ return 0 ;
1782+ }
1783+
16711784/* ── Antigravity MCP config (JSON, same mcpServers format) ────── */
16721785
16731786int cbm_upsert_antigravity_mcp (const char * binary_path , const char * config_path ) {
@@ -3135,10 +3248,36 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
31353248 if (agents -> opencode ) {
31363249 char cp [CLI_BUF_1K ];
31373250 char ip [CLI_BUF_1K ];
3251+ char skills_dir [CLI_BUF_1K ];
31383252 snprintf (cp , sizeof (cp ), "%s/.config/opencode/opencode.json" , home );
31393253 snprintf (ip , sizeof (ip ), "%s/.config/opencode/AGENTS.md" , home );
3254+ snprintf (skills_dir , sizeof (skills_dir ), "%s/.config/opencode/skills" , home );
3255+
3256+ /* Plan mode: record planned writes for skills + plugin, mutate nothing
3257+ * (#388). MCP config + instructions are recorded by install_generic_agent_config. */
3258+ if (g_install_plan ) {
3259+ char plugin_path [CLI_BUF_1K ];
3260+ snprintf (plugin_path , sizeof (plugin_path ),
3261+ "%s/.config/opencode/plugins/%s" , home , CMM_OPENCODE_PLUGIN_NAME );
3262+ plan_record ("OpenCode" , "skills" , skills_dir );
3263+ plan_record ("OpenCode" , "plugin" , plugin_path );
3264+ }
3265+
31403266 install_generic_agent_config ("OpenCode" , binary_path , cp , ip , dry_run ,
31413267 cbm_upsert_opencode_mcp );
3268+
3269+ if (!g_install_plan ) {
3270+ /* Install skills (same consolidated skill as Claude Code). */
3271+ int skill_count = cbm_install_skills (skills_dir , true, dry_run );
3272+ printf (" skills: %d installed\n" , skill_count );
3273+
3274+ /* Install TypeScript plugin (tool.execute.after + system.transform). */
3275+ if (!dry_run ) {
3276+ cbm_upsert_opencode_plugin (home , binary_path , dry_run );
3277+ }
3278+ printf (" plugin: tool.execute.after (Grep/Glob graph augment) + "
3279+ "experimental.chat.system.transform (session reminder)\n" );
3280+ }
31423281 }
31433282 if (agents -> antigravity ) {
31443283 char cp [CLI_BUF_1K ];
@@ -3632,10 +3771,20 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
36323771 if (agents -> opencode ) {
36333772 char cp [CLI_BUF_1K ];
36343773 char ip [CLI_BUF_1K ];
3774+ char skills_dir [CLI_BUF_1K ];
36353775 snprintf (cp , sizeof (cp ), "%s/.config/opencode/opencode.json" , home );
36363776 snprintf (ip , sizeof (ip ), "%s/.config/opencode/AGENTS.md" , home );
3777+ snprintf (skills_dir , sizeof (skills_dir ), "%s/.config/opencode/skills" , home );
36373778 uninstall_agent_mcp_instr ((mcp_uninstall_args_t ){"OpenCode" , cp , ip }, dry_run ,
36383779 cbm_remove_opencode_mcp );
3780+ /* Remove skills. */
3781+ int removed = cbm_remove_skills (skills_dir , dry_run );
3782+ printf ("OpenCode: removed %d skill(s)\n" , removed );
3783+ /* Remove TypeScript plugin. */
3784+ if (!dry_run ) {
3785+ cbm_remove_opencode_plugin (home , dry_run );
3786+ }
3787+ printf (" removed plugin\n" );
36393788 }
36403789 if (agents -> antigravity ) {
36413790 char cp [CLI_BUF_1K ];
0 commit comments