@@ -265,17 +265,23 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
265265
266266 adapter_sb_t sb = {0 };
267267 emit_header (& sb , "OpenCode" );
268- sb_append (& sb , "// OpenCode already reaches every tool over MCP; this adds only the\n"
269- "// automatic graph lookup before a grep/glob, which other clients get\n"
270- "// through their own hook configuration.\n" );
268+ sb_append (& sb , "// OpenCode already reaches every tool over MCP; this module adds the\n"
269+ "// context surfaces other clients get through hook configuration: graph\n"
270+ "// lookup after grep/glob, index-coverage notes after read, session-start\n"
271+ "// tier routing (carried on the first tool result of each session, since\n"
272+ "// OpenCode documents no context-output lifecycle hook), and reinjection\n"
273+ "// after compaction via the documented experimental surface.\n" );
271274 sb_append (& sb , "import { spawn } from 'node:child_process';\n\n" );
272275 sb_append (& sb , "const BIN = '" );
273276 sb_append (& sb , bin );
274277 sb_append (& sb , "';\n\n" );
275278
276- /* hook-augment requires hook_event_name and accepts Grep/Glob only under
277- * PreToolUse; omitting it makes the whole hook a silent no-op. */
278- sb_append (& sb , "function augment(tool, args) {\n"
279+ /* hook-augment requires hook_event_name; its default dialect accepts
280+ * Grep/Glob under PreToolUse, Read under PostToolUse, and the
281+ * SessionStart lifecycle event — and emits the Claude JSON envelope, so
282+ * the plugin unwraps additionalContext instead of pasting raw JSON into
283+ * the tool output. Every failure path resolves to '' (fail open). */
284+ sb_append (& sb , "function augment(payload) {\n"
279285 " return new Promise((resolve) => {\n"
280286 " const child = spawn(BIN, ['hook-augment'], {\n"
281287 " stdio: ['pipe', 'pipe', 'ignore'],\n"
@@ -284,27 +290,67 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
284290 " let out = '';\n"
285291 " child.stdout.on('data', (d) => (out += d.toString()));\n"
286292 " child.on('error', () => resolve(''));\n"
287- " child.on('close', () => resolve(out));\n"
288- " child.stdin.end(JSON.stringify({\n"
289- " hook_event_name: 'PreToolUse',\n"
290- " tool_name: tool,\n"
291- " tool_input: args ?? {},\n"
292- " }));\n"
293+ " child.on('close', () => {\n"
294+ " try {\n"
295+ " const ctx = JSON.parse(out)?.hookSpecificOutput?.additionalContext;\n"
296+ " resolve(typeof ctx === 'string' ? ctx : '');\n"
297+ " } catch { resolve(''); }\n"
298+ " });\n"
299+ " child.stdin.end(JSON.stringify(payload));\n"
293300 " });\n"
294301 "}\n\n" );
295302
296303 sb_append (& sb ,
297- "export const CodebaseMemory = async () => ({\n"
298- " 'tool.execute.after': async (input, output) => {\n"
299- " const tool = input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' "
300- ": null;\n"
301- " if (!tool) return;\n"
302- " const extra = await augment(tool, output?.args);\n"
303- " if (extra && typeof output?.output === 'string') {\n"
304- " output.output += '\\n' + extra;\n"
305- " }\n"
306- " },\n"
307- "});\n" );
304+ "export const CodebaseMemory = async (ctx) => {\n"
305+ " const dir = ctx?.directory;\n"
306+ " const seen = new Set();\n"
307+ " const lifecycle = () =>\n"
308+ " augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
309+ " return {\n"
310+ " 'tool.execute.after': async (input, output) => {\n"
311+ " if (typeof output?.output !== 'string') return;\n"
312+ " const pieces = [];\n"
313+ " const sid = input?.sessionID;\n"
314+ " if (typeof sid === 'string' && !seen.has(sid)) {\n"
315+ " seen.add(sid);\n"
316+ " pieces.push(await lifecycle());\n"
317+ " }\n"
318+ " const args = output?.args ?? {};\n"
319+ " const search =\n"
320+ " input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' : null;\n"
321+ " if (search) {\n"
322+ " pieces.push(await augment({\n"
323+ " hook_event_name: 'PreToolUse',\n"
324+ " tool_name: search,\n"
325+ " tool_input: args,\n"
326+ " cwd: dir,\n"
327+ " }));\n"
328+ " } else if (input?.tool === 'read') {\n"
329+ " const filePath = args.filePath ?? args.file_path ?? args.path;\n"
330+ " if (typeof filePath === 'string' && filePath) {\n"
331+ " pieces.push(await augment({\n"
332+ " hook_event_name: 'PostToolUse',\n"
333+ " tool_name: 'Read',\n"
334+ " tool_input: { file_path: filePath },\n"
335+ " cwd: dir,\n"
336+ " }));\n"
337+ " }\n"
338+ " }\n"
339+ " const extra = pieces.filter(Boolean).join('\\n');\n"
340+ " if (extra) {\n"
341+ " output.output += '\\n' + extra;\n"
342+ " }\n"
343+ " },\n"
344+ " // Documented (experimental) compaction surface: output.context is the\n"
345+ " // mutable array of context strings for the rebuilt session.\n"
346+ " 'experimental.session.compacting': async (_input, output) => {\n"
347+ " const note = await lifecycle();\n"
348+ " if (note && Array.isArray(output?.context)) {\n"
349+ " output.context.push(note);\n"
350+ " }\n"
351+ " },\n"
352+ " };\n"
353+ "};\n" );
308354
309355 if (sb .failed ) {
310356 free (sb .buf );
0 commit comments