diff --git a/README.md b/README.md index f2a1b47..996c887 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) or [OpenCode](https://opencode.ai) as the MCP host +- [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) as the MCP host - ~500 MB free disk ### Install @@ -133,40 +133,6 @@ 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 @@ -174,7 +140,7 @@ iai-mcp doctor iai-mcp daemon status ``` -Restart your MCP host (Claude Code or OpenCode). Start a session, do some work, exit. Then: +Restart Claude Code. Start a session, do some work, exit. Then: ```bash tail ~/.iai-mcp/logs/capture-$(date -u +%Y-%m-%d).log @@ -362,8 +328,6 @@ 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 deleted file mode 100644 index b066e3c..0000000 --- a/deploy/opencode/iai-mcp-capture.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * 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}`); - } - }, - }; -};