From 8116a5643f118a6d5ddc9e691194f614f3932d50 Mon Sep 17 00:00:00 2001 From: Apunkt Date: Fri, 22 May 2026 10:03:09 +0200 Subject: [PATCH 1/2] feat: ship OpenCode capture plugin in deploy/opencode/ --- README.md | 40 ++++++++- deploy/opencode/iai-mcp-capture.js | 125 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 deploy/opencode/iai-mcp-capture.js diff --git a/README.md b/README.md index 996c887..f2a1b47 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ I built this for myself. It worked. I've been running it daily for months, and n - macOS (Apple Silicon tested) or Linux - Python 3.11 or 3.12 - Node.js 18+ -- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as the MCP host +- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) or [OpenCode](https://opencode.ai) as the MCP host - ~500 MB free disk ### Install @@ -133,6 +133,40 @@ Use the absolute path. `~` and `$HOME` won't expand here. For Claude Desktop (untested), edit `~/Library/Application Support/Claude/claude_desktop_config.json`. +### Install the OpenCode plugin + +This is the OpenCode equivalent of the Claude Code Stop hook. It listens for idle sessions and writes a deferred-capture JSONL file for the daemon to drain. + +```bash +mkdir -p ~/.config/opencode/plugins +cp deploy/opencode/iai-mcp-capture.js ~/.config/opencode/plugins/ +``` + +Make sure `@opencode-ai/plugin` is installed: + +```bash +cd ~/.config/opencode +npm install @opencode-ai/plugin +``` + +### Connect OpenCode + +Add the MCP server to `~/.config/opencode/config.json`: + +```json +{ + "mcp": { + "iai-mcp": { + "type": "local", + "command": ["node", "/absolute/path/to/iai-mcp/mcp-wrapper/dist/index.js"], + "enabled": true + } + } +} +``` + +Use the absolute path to `mcp-wrapper/dist/index.js`. The plugin file (`iai-mcp-capture.js`) must already be in `~/.config/opencode/plugins/`. + ### Verify ```bash @@ -140,7 +174,7 @@ iai-mcp doctor iai-mcp daemon status ``` -Restart Claude Code. Start a session, do some work, exit. Then: +Restart your MCP host (Claude Code or OpenCode). Start a session, do some work, exit. Then: ```bash tail ~/.iai-mcp/logs/capture-$(date -u +%Y-%m-%d).log @@ -328,6 +362,8 @@ Limitations worth knowing about: Claude Code is the primary host, validated in daily use. +OpenCode is supported via the `iai-mcp-capture.js` plugin (see Install the OpenCode plugin above) and MCP server config in `~/.config/opencode/config.json`. + Claude Desktop should work (uses `claude_desktop_config.json` instead of `~/.claude.json`) but hasn't been tested end to end. Other MCP-over-stdio hosts speak the same protocol and should work in principle. Not tested. diff --git a/deploy/opencode/iai-mcp-capture.js b/deploy/opencode/iai-mcp-capture.js new file mode 100644 index 0000000..b066e3c --- /dev/null +++ b/deploy/opencode/iai-mcp-capture.js @@ -0,0 +1,125 @@ +/** + * iai-mcp session capture plugin for OpenCode. + * + * Listens for `session.idle` events, reads the session transcript via the + * SDK client, and writes it as a deferred-capture JSONL file under + * `~/.iai-mcp/.deferred-captures/`. The iai-mcp daemon picks it up on its + * next tick (same path as the Claude Code Stop hook's `--no-spawn` mode). + * + * Fail-safe by design: any exception is caught and logged — the plugin + * never throws and never blocks OpenCode. + * + * Deferred JSONL format (v1, per D7.1-04): + * Line 1: {"version":1,"deferred_at":"","session_id":"","cwd":""} + * Lines 2..N: {"text":"","cue":"session turn ","tier":"episodic","role":"user|assistant","ts":""} + */ + +const DEFERRED_DIR = process.env.HOME + ? `${process.env.HOME}/.iai-mcp/.deferred-captures` + : `${process.cwd()}/.iai-mcp/.deferred-captures`; +const MAX_TURNS = 200; + +function extractText(parts) { + if (!parts || !Array.isArray(parts)) return ""; + const texts = parts + .filter((p) => p.type === "text" && p.text) + .map((p) => p.text); + return texts.join("\n").trim(); +} + +function nowISO() { + return new Date().toISOString(); +} + +async function writeDeferredCapture(client, sessionId, cwd) { + const fs = await import("node:fs"); + const path = await import("node:path"); + + // Ensure deferred directory exists + if (!fs.existsSync(DEFERRED_DIR)) { + fs.mkdirSync(DEFERRED_DIR, { recursive: true }); + } + + const outPath = path.join( + DEFERRED_DIR, + `${sessionId}-${Math.floor(Date.now() / 1000)}.jsonl` + ); + + const header = { + version: 1, + deferred_at: nowISO(), + session_id: sessionId, + cwd: cwd || process.cwd(), + }; + + const lines = [JSON.stringify(header, null, null)]; + + try { + // Fetch session messages via SDK + // The plugin client has the same API as the standalone SDK client + const resp = await client.session.messages({ + path: { id: sessionId }, + }); + const messages = resp.data || []; + + let turnCount = 0; + for (const msg of messages) { + if (turnCount >= MAX_TURNS) break; + turnCount++; + + const role = msg.info?.role; + if (role !== "user" && role !== "assistant") continue; + + const text = extractText(msg.parts); + if (!text) continue; + + const event = { + text, + cue: `session ${sessionId} turn ${turnCount}`, + tier: "episodic", + role, + ts: nowISO(), + }; + lines.push(JSON.stringify(event, null, null)); + } + } catch (err) { + // Session might have been deleted or SDK unavailable — write header only. + // The daemon drain treats header-only files as no-op. + console.error( + `[iai-mcp] session.messages failed for ${sessionId}: ${err.message}` + ); + } + + fs.writeFileSync(outPath, lines.join("\n") + "\n"); + return outPath; +} + +export const IaiMcpCapture = async ({ client, client: { session }, $, directory }) => { + return { + event: async ({ event }) => { + if (event.type !== "session.idle") return; + + const sessionId = event.properties?.sessionID + ? event.properties.sessionID + : event.properties?.sessionId + ? event.properties.sessionId + : event.properties?.session_id; + + if (!sessionId) { + console.error("[iai-mcp] session.idle event missing sessionId"); + return; + } + + try { + const outPath = await writeDeferredCapture( + client, + sessionId, + directory?.path || process.cwd() + ); + } catch (err) { + // NEVER throw — hook is best-effort + console.error(`[iai-mcp] capture failed for ${sessionId}: ${err.message}`); + } + }, + }; +}; From 402880cc4f1d8a95f7ac6a7db72bf7f5c9e75b08 Mon Sep 17 00:00:00 2001 From: renovate-bot Date: Fri, 22 May 2026 08:05:21 +0000 Subject: [PATCH 2/2] chore(deps): update dependency @types/node to v22.19.19 --- mcp-wrapper/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mcp-wrapper/package-lock.json b/mcp-wrapper/package-lock.json index 6d9757b..7de7d58 100644 --- a/mcp-wrapper/package-lock.json +++ b/mcp-wrapper/package-lock.json @@ -518,9 +518,9 @@ } }, "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", "dependencies": {