@ephemushroom/opencode-claude-mem 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -4
- package/dist/index.d.ts +7 -2
- package/dist/index.js +57 -55
- package/dist/worker-client.d.ts +0 -4
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -8,10 +8,30 @@ Enables OpenCode to share the same memory database as Claude Code — observatio
|
|
|
8
8
|
|
|
9
9
|
This plugin communicates with the Claude-Mem worker service (HTTP API on port 37777) to:
|
|
10
10
|
|
|
11
|
+
- **Inject memory context** — previous session context is injected into the system prompt on every LLM call
|
|
11
12
|
- **Capture tool observations** — every tool execution is recorded as a structured observation
|
|
12
|
-
- **Inject memory context** — previous session context is injected into the system prompt via `/api/context/inject`
|
|
13
13
|
- **Summarize sessions** — when a session goes idle, the last user/assistant messages are sent for summarization
|
|
14
|
-
- **Search memory** —
|
|
14
|
+
- **Search memory** — memory search is available via claude-mem's MCP server
|
|
15
|
+
- **View context on demand** — `/memory` command displays the current memory context in the chat flow
|
|
16
|
+
|
|
17
|
+
## Differences from Claude Code Version
|
|
18
|
+
|
|
19
|
+
The Claude Code version of claude-mem uses shell-based hooks where `SessionStart` stdout is automatically injected into the system prompt — one-time, at session start.
|
|
20
|
+
|
|
21
|
+
This OpenCode version works differently:
|
|
22
|
+
|
|
23
|
+
| | Claude Code | OpenCode (this plugin) |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| **Architecture** | Shell commands (`node script.js`) | JavaScript plugin API |
|
|
26
|
+
| **Context injection** | `SessionStart` hook stdout → system prompt (once) | `experimental.chat.system.transform` → system prompt (every LLM call, session-level cache) |
|
|
27
|
+
| **Context freshness** | Snapshot at session start | Cached once per session (same as Claude Code) |
|
|
28
|
+
| **User-visible context** | Displayed in TUI automatically | `/memory` command to view on demand |
|
|
29
|
+
| **Session init** | `UserPromptSubmit` hook | `chat.message` hook with real user prompt |
|
|
30
|
+
| **Observations** | `PostToolUse` hook | `tool.execute.after` hook (with circular memory protection) |
|
|
31
|
+
| **Summarization** | `Stop` hook | `session.idle` event |
|
|
32
|
+
| **Memory search** | MCP server | MCP server (same — no custom tool needed) |
|
|
33
|
+
|
|
34
|
+
Both versions behave the same way: context is fetched once per session and cached. The OpenCode plugin uses `experimental.chat.system.transform` which fires on every LLM call, but the context is session-level cached so the worker is only called once.
|
|
15
35
|
|
|
16
36
|
## Prerequisites
|
|
17
37
|
|
|
@@ -52,6 +72,25 @@ curl -s http://127.0.0.1:37777/api/health
|
|
|
52
72
|
# Restart OpenCode — you should see toast: "Memory active · {project}"
|
|
53
73
|
```
|
|
54
74
|
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### Automatic (no action needed)
|
|
78
|
+
|
|
79
|
+
Once installed, the plugin works automatically:
|
|
80
|
+
|
|
81
|
+
- **System prompt injection** — memory context is injected into every LLM call via `experimental.chat.system.transform`. The LLM sees your project's observation history, past decisions, and session summaries. Context is cached once per session to avoid redundant worker calls.
|
|
82
|
+
- **Tool observation capture** — every tool execution (file reads, edits, searches, etc.) is recorded as an observation in the memory database. Claude-mem's own MCP tools are automatically filtered to prevent circular memory.
|
|
83
|
+
- **Session summarization** — when a session goes idle, the last user/assistant exchange is summarized and stored.
|
|
84
|
+
- **Toast notification** — on session start, a "Memory active · {project}" toast confirms the plugin is connected to the worker.
|
|
85
|
+
|
|
86
|
+
### `/memory` Command
|
|
87
|
+
|
|
88
|
+
Type `/memory` in the chat to display the current memory context inline. This shows you exactly what the LLM sees in its system prompt — the observation index, token economics, and session history.
|
|
89
|
+
|
|
90
|
+
### Memory Search
|
|
91
|
+
|
|
92
|
+
Memory search is provided by claude-mem's MCP server. If you have the claude-mem MCP configured in OpenCode, the LLM can search project history directly via MCP tools — no plugin-level tool needed.
|
|
93
|
+
|
|
55
94
|
### Alternative: Manual Installation (from source)
|
|
56
95
|
|
|
57
96
|
If you prefer to install from source instead of npm:
|
|
@@ -217,11 +256,12 @@ The plugin is a thin HTTP client. All heavy lifting (LLM processing, storage, se
|
|
|
217
256
|
|
|
218
257
|
| Claude Code Hook | OpenCode Hook | Purpose |
|
|
219
258
|
|-----------------|---------------|---------|
|
|
220
|
-
| `SessionStart` → context | `experimental.chat.system.transform` | Inject memory into system prompt |
|
|
259
|
+
| `SessionStart` → context | `experimental.chat.system.transform` | Inject memory into system prompt (every LLM call, session-level cache) |
|
|
221
260
|
| `UserPromptSubmit` → session-init | `chat.message` | Init session with real user prompt |
|
|
222
261
|
| `PostToolUse` → observation | `tool.execute.after` | Capture tool executions |
|
|
223
262
|
| `Stop` → summarize | `event` (session.idle) | Summarize with real message content |
|
|
224
263
|
| `Stop` → session-complete | `event` (session.idle) | Mark session complete |
|
|
264
|
+
| *(no equivalent)* | `config` + `command.execute.before` | `/memory` command for user-visible context |
|
|
225
265
|
|
|
226
266
|
### Worker API Endpoints Used
|
|
227
267
|
|
|
@@ -233,7 +273,6 @@ The plugin is a thin HTTP client. All heavy lifting (LLM processing, storage, se
|
|
|
233
273
|
| `POST` | `/api/sessions/observations` | Send tool observation |
|
|
234
274
|
| `POST` | `/api/sessions/summarize` | Trigger summarization |
|
|
235
275
|
| `POST` | `/api/sessions/complete` | Complete session |
|
|
236
|
-
| `GET` | `/api/search?q={query}&project={name}` | Search memory |
|
|
237
276
|
|
|
238
277
|
## Key Implementation Details
|
|
239
278
|
|
|
@@ -242,6 +281,11 @@ The plugin is a thin HTTP client. All heavy lifting (LLM processing, storage, se
|
|
|
242
281
|
- **Deferred toast**: `client.tui.showToast()` is deferred to first hook invocation (TUI not ready during plugin init, calling it crashes OpenCode)
|
|
243
282
|
- **Real prompt**: `chat.message` hook extracts actual user input instead of hardcoded "SESSION_START"
|
|
244
283
|
- **Real summarize**: `session.idle` fetches last user/assistant messages via `client.session.messages()` for meaningful summaries
|
|
284
|
+
- **Context caching**: `getCachedContext()` caches context once per session (invalidated on `session.created`) to avoid redundant worker calls
|
|
285
|
+
- **Content-type handling**: Worker returns `text/plain` markdown — `getContext()` checks content-type and uses `response.text()` instead of `response.json()`
|
|
286
|
+
- **Context tag wrapping**: Injected context is wrapped in `<claude-mem-context>` tags so the worker's tag-stripping logic can remove it from observations, preventing circular memory
|
|
287
|
+
- **Circular memory protection**: `tool.execute.after` filters out claude-mem's own MCP tools (`claude-mem_mcp-search_*`) to prevent search results from being re-recorded as observations
|
|
288
|
+
- **`/memory` command**: Registered via `config` hook, handled by `command.execute.before` — displays context as an `ignored: true` message (visible to user, not sent to LLM)
|
|
245
289
|
|
|
246
290
|
## Troubleshooting
|
|
247
291
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,12 +3,17 @@ import { type Plugin } from '@opencode-ai/plugin';
|
|
|
3
3
|
* OpenCode Plugin for Claude-Mem
|
|
4
4
|
*
|
|
5
5
|
* Hooks used:
|
|
6
|
+
* - `config` — register /memory command
|
|
7
|
+
* - `command.execute.before` — handle /memory command
|
|
6
8
|
* - `event` — session lifecycle (session.created, session.idle)
|
|
7
9
|
* - `tool.execute.after` — capture tool observations
|
|
8
10
|
* - `experimental.chat.system.transform` — inject memory context into system prompt
|
|
9
|
-
* - `chat.message` —
|
|
10
|
-
* - `tool` — mem-search custom tool
|
|
11
|
+
* - `chat.message` — session init with real user prompt
|
|
11
12
|
*
|
|
12
13
|
* Uses TUI toast notifications for status feedback.
|
|
13
14
|
*/
|
|
14
15
|
export declare const ClaudeMemPlugin: Plugin;
|
|
16
|
+
declare const _default: {
|
|
17
|
+
server: Plugin;
|
|
18
|
+
};
|
|
19
|
+
export default _default;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/index.ts
|
|
3
|
-
import { tool } from "@opencode-ai/plugin";
|
|
4
|
-
|
|
5
2
|
// src/worker-client.ts
|
|
6
3
|
class WorkerClient {
|
|
7
4
|
static PORT = 37777;
|
|
@@ -80,31 +77,24 @@ class WorkerClient {
|
|
|
80
77
|
if (!response.ok) {
|
|
81
78
|
return null;
|
|
82
79
|
}
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
const contentType = response.headers.get("content-type") || "";
|
|
81
|
+
if (contentType.includes("application/json")) {
|
|
82
|
+
const data = await response.json();
|
|
83
|
+
if (typeof data === "string") {
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
if (data && typeof data.content === "string") {
|
|
87
|
+
return data.content;
|
|
88
|
+
}
|
|
89
|
+
const text2 = JSON.stringify(data, null, 2);
|
|
90
|
+
return text2 === "{}" || text2 === "null" ? null : text2;
|
|
89
91
|
}
|
|
90
|
-
const text =
|
|
91
|
-
return text
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
return text.trim() || null;
|
|
92
94
|
} catch {
|
|
93
95
|
return null;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
static async search(query, project) {
|
|
97
|
-
try {
|
|
98
|
-
const response = await fetch(`${this.BASE_URL}/api/search?q=${encodeURIComponent(query)}&project=${encodeURIComponent(project)}`);
|
|
99
|
-
if (!response.ok) {
|
|
100
|
-
return "Search failed";
|
|
101
|
-
}
|
|
102
|
-
const data = await response.json();
|
|
103
|
-
return JSON.stringify(data, null, 2);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
return `Error performing search: ${error}`;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
98
|
}
|
|
109
99
|
|
|
110
100
|
// src/index.ts
|
|
@@ -132,19 +122,17 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
132
122
|
await toast(text.slice(0, 200), "info");
|
|
133
123
|
}
|
|
134
124
|
}
|
|
135
|
-
|
|
136
|
-
let contextCache = null;
|
|
125
|
+
let contextCache = undefined;
|
|
137
126
|
async function getCachedContext() {
|
|
138
|
-
if (contextCache
|
|
139
|
-
return contextCache
|
|
127
|
+
if (contextCache !== undefined) {
|
|
128
|
+
return contextCache;
|
|
140
129
|
}
|
|
141
130
|
const context = await WorkerClient.getContext(projectName);
|
|
142
|
-
contextCache =
|
|
131
|
+
contextCache = context;
|
|
143
132
|
return context;
|
|
144
133
|
}
|
|
145
134
|
let workerHealthy = null;
|
|
146
135
|
let initToastShown = false;
|
|
147
|
-
let contextDisplayedInline = false;
|
|
148
136
|
async function checkWorkerAndToast() {
|
|
149
137
|
if (workerHealthy === null) {
|
|
150
138
|
workerHealthy = await WorkerClient.isHealthy();
|
|
@@ -152,9 +140,7 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
152
140
|
if (!initToastShown) {
|
|
153
141
|
initToastShown = true;
|
|
154
142
|
if (workerHealthy) {
|
|
155
|
-
|
|
156
|
-
await toast(`Memory active \xB7 ${projectName}`, "success");
|
|
157
|
-
}
|
|
143
|
+
await toast(`Memory active \xB7 ${projectName}`, "success");
|
|
158
144
|
} else {
|
|
159
145
|
await toast("Worker offline \u2014 start Claude Code first", "warning", 5000);
|
|
160
146
|
}
|
|
@@ -188,6 +174,34 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
188
174
|
`).trim();
|
|
189
175
|
}
|
|
190
176
|
return {
|
|
177
|
+
config: async (input) => {
|
|
178
|
+
input.command ??= {};
|
|
179
|
+
input.command["memory"] = {
|
|
180
|
+
template: "/memory",
|
|
181
|
+
description: "Show memory context from Claude-Mem"
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
"command.execute.before": async (input) => {
|
|
185
|
+
if (input.command !== "memory") {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const sessionId = input.sessionID || currentSessionId;
|
|
189
|
+
if (!sessionId) {
|
|
190
|
+
throw new Error("__MEMORY_NO_SESSION__");
|
|
191
|
+
}
|
|
192
|
+
const isHealthy = await checkWorkerAndToast();
|
|
193
|
+
if (!isHealthy) {
|
|
194
|
+
await sendStatusMessage(sessionId, "\u26A0 Claude-Mem worker is offline");
|
|
195
|
+
throw new Error("__MEMORY_HANDLED__");
|
|
196
|
+
}
|
|
197
|
+
const context = await getCachedContext();
|
|
198
|
+
if (context) {
|
|
199
|
+
await sendStatusMessage(sessionId, context);
|
|
200
|
+
} else {
|
|
201
|
+
await sendStatusMessage(sessionId, `No memory context available for ${projectName}`);
|
|
202
|
+
}
|
|
203
|
+
throw new Error("__MEMORY_HANDLED__");
|
|
204
|
+
},
|
|
191
205
|
event: async ({ event }) => {
|
|
192
206
|
if (event.type === "session.created") {
|
|
193
207
|
const sessionId = event.properties?.info?.id;
|
|
@@ -195,18 +209,8 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
195
209
|
return;
|
|
196
210
|
}
|
|
197
211
|
currentSessionId = sessionId;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
contextDisplayedInline = true;
|
|
201
|
-
}
|
|
202
|
-
const isHealthy = await checkWorkerAndToast();
|
|
203
|
-
if (isHealthy) {
|
|
204
|
-
if (context) {
|
|
205
|
-
await sendStatusMessage(sessionId, context);
|
|
206
|
-
} else {
|
|
207
|
-
await toast(`Memory active \xB7 ${projectName}`, "success", 2000);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
212
|
+
contextCache = undefined;
|
|
213
|
+
await checkWorkerAndToast();
|
|
210
214
|
}
|
|
211
215
|
if (event.type === "session.idle") {
|
|
212
216
|
const sessionId = event.properties?.sessionID || currentSessionId;
|
|
@@ -261,8 +265,10 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
261
265
|
try {
|
|
262
266
|
const context = await getCachedContext();
|
|
263
267
|
if (context) {
|
|
264
|
-
output.system.push(
|
|
265
|
-
|
|
268
|
+
output.system.push(`<claude-mem-context>
|
|
269
|
+
[Claude-Mem] Memory Active. Previous Context:
|
|
270
|
+
${context}
|
|
271
|
+
</claude-mem-context>`);
|
|
266
272
|
}
|
|
267
273
|
} catch {}
|
|
268
274
|
},
|
|
@@ -271,22 +277,18 @@ ${context}`);
|
|
|
271
277
|
if (!sessionId) {
|
|
272
278
|
return;
|
|
273
279
|
}
|
|
280
|
+
if (input.tool.startsWith("claude-mem_mcp-search_")) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
274
283
|
await ensureSessionInit(sessionId);
|
|
275
284
|
try {
|
|
276
285
|
await WorkerClient.sendObservation(sessionId, input.tool, input.args || {}, output.output, projectRoot);
|
|
277
286
|
} catch {}
|
|
278
|
-
},
|
|
279
|
-
tool: {
|
|
280
|
-
"mem-search": tool({
|
|
281
|
-
description: "Search project history and memory. Use this to find information about past decisions, code changes, or bug fixes.",
|
|
282
|
-
args: {
|
|
283
|
-
query: tool.schema.string()
|
|
284
|
-
},
|
|
285
|
-
execute: async (args) => await WorkerClient.search(args.query, projectName)
|
|
286
|
-
})
|
|
287
287
|
}
|
|
288
288
|
};
|
|
289
289
|
};
|
|
290
|
+
var src_default = { server: ClaudeMemPlugin };
|
|
290
291
|
export {
|
|
292
|
+
src_default as default,
|
|
291
293
|
ClaudeMemPlugin
|
|
292
294
|
};
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -37,8 +37,4 @@ export declare class WorkerClient {
|
|
|
37
37
|
* Uses /api/context/inject which returns rich markdown with observations + session summaries.
|
|
38
38
|
*/
|
|
39
39
|
static getContext(project: string): Promise<string | null>;
|
|
40
|
-
/**
|
|
41
|
-
* Perform Search (used by mem-search tool)
|
|
42
|
-
*/
|
|
43
|
-
static search(query: string, project: string): Promise<string>;
|
|
44
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ephemushroom/opencode-claude-mem",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "OpenCode plugin for Claude-Mem persistent memory system",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
".": {
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
"scripts": {
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
"access": "public"
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
|
-
"@opencode-ai/plugin": "
|
|
52
|
+
"@opencode-ai/plugin": "^1.3.0"
|
|
49
53
|
},
|
|
50
54
|
"devDependencies": {
|
|
51
55
|
"@types/bun": "latest",
|