@ephemushroom/opencode-claude-mem 0.1.0
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/LICENSE +21 -0
- package/README.md +260 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +258 -0
- package/dist/worker-client.d.ts +44 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bryan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# opencode-claude-mem
|
|
2
|
+
|
|
3
|
+
OpenCode plugin for [Claude-Mem](https://github.com/thedotmack/claude-mem) persistent memory system.
|
|
4
|
+
|
|
5
|
+
Enables OpenCode to share the same memory database as Claude Code — observations, summaries, and context injection all work across both editors.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
This plugin communicates with the Claude-Mem worker service (HTTP API on port 37777) to:
|
|
10
|
+
|
|
11
|
+
- **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
|
+
- **Summarize sessions** — when a session goes idle, the last user/assistant messages are sent for summarization
|
|
14
|
+
- **Search memory** — `mem-search` tool lets the LLM query project history
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- [Claude Code](https://claude.com/claude-code) with [claude-mem plugin](https://github.com/thedotmack/claude-mem) installed and running
|
|
19
|
+
- [OpenCode](https://opencode.ai) with plugin support
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Step 1: Install Claude-Mem (if not already)
|
|
24
|
+
|
|
25
|
+
In Claude Code terminal:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/plugin marketplace add thedotmack/claude-mem
|
|
29
|
+
/plugin install claude-mem
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Restart Claude Code. The worker service will start automatically on port 37777.
|
|
33
|
+
|
|
34
|
+
### Step 2: Add the Plugin
|
|
35
|
+
|
|
36
|
+
Add to your `opencode.json` (project or global `~/.config/opencode/opencode.json`):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"plugin": ["@ephemushroom/opencode-claude-mem"]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Restart OpenCode. The plugin will be installed automatically.
|
|
45
|
+
|
|
46
|
+
### Step 3: Verify
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Check worker is running
|
|
50
|
+
curl -s http://127.0.0.1:37777/api/health
|
|
51
|
+
|
|
52
|
+
# Restart OpenCode — you should see toast: "Memory active · {project}"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Alternative: Manual Installation (from source)
|
|
56
|
+
|
|
57
|
+
If you prefer to install from source instead of npm:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/Ephemushroom/opencode-claude-mem.git
|
|
61
|
+
cd opencode-claude-mem
|
|
62
|
+
bun install
|
|
63
|
+
bun run build
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then symlink into OpenCode:
|
|
67
|
+
|
|
68
|
+
**macOS / Linux:**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mkdir -p ~/.config/opencode/plugin
|
|
72
|
+
ln -sf "$(pwd)/dist/index.js" ~/.config/opencode/plugin/claude-mem.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Windows (requires elevated terminal):**
|
|
76
|
+
|
|
77
|
+
```powershell
|
|
78
|
+
# Option A: If you have gsudo / sudo installed
|
|
79
|
+
sudo powershell -Command "New-Item -ItemType SymbolicLink -Path '$env:USERPROFILE\.config\opencode\plugin\claude-mem.js' -Target 'D:\path\to\opencode-claude-mem\dist\index.js'"
|
|
80
|
+
|
|
81
|
+
# Option B: Run PowerShell as Administrator
|
|
82
|
+
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.config\opencode\plugin\claude-mem.js" -Target "D:\path\to\opencode-claude-mem\dist\index.js"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Windows-Specific Setup
|
|
88
|
+
|
|
89
|
+
### Python Version for ChromaDB
|
|
90
|
+
|
|
91
|
+
Claude-Mem uses ChromaDB for vector semantic search. ChromaDB's `chroma-mcp` tool requires **Python ≤ 3.13** (Pydantic V1 is incompatible with Python 3.14+).
|
|
92
|
+
|
|
93
|
+
If `uv` defaults to Python 3.14, you'll see:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Chroma connection failed: Chroma server not reachable.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Fix — set global Python version:**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv python pin --global 3.13
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This creates a global `.python-version` file in `%APPDATA%\uv\` so all `uvx` calls default to 3.13.
|
|
106
|
+
|
|
107
|
+
### ChromaDB on Windows x64
|
|
108
|
+
|
|
109
|
+
The npm `chromadb` package only supports Windows ARM64, not x64. The worker will fail to start Chroma natively.
|
|
110
|
+
|
|
111
|
+
**Fix — use Python ChromaDB instead:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Install Python ChromaDB CLI
|
|
115
|
+
uv tool install chromadb --python 3.13
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Update `~/.claude-mem/settings.json`:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"CLAUDE_MEM_CHROMA_MODE": "remote"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Start Chroma server manually:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
chroma run --path %USERPROFILE%\.claude-mem\vector-db --host 127.0.0.1 --port 8000
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> **Note**: Chroma server must be running before Claude Code starts. Consider creating a startup script or scheduled task.
|
|
133
|
+
|
|
134
|
+
### ChromaDB Embedding Model Crash (Bun Runtime)
|
|
135
|
+
|
|
136
|
+
After connecting to Chroma, the worker may crash with:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Collection setup failed: undefined is not an object
|
|
140
|
+
(evaluating 'e.cacheDir = ...')
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This happens because `@huggingface/transformers` returns `undefined` when imported through Bun's CJS shim.
|
|
144
|
+
|
|
145
|
+
**Fix — patch `worker-service.cjs`:**
|
|
146
|
+
|
|
147
|
+
Locate the bundled worker file:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
~/.claude/plugins/cache/thedotmack/claude-mem/<version>/scripts/worker-service.cjs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Find this pattern (minified):
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
let{env:e}=await Promise.resolve().then(()=>(F9(),N9));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Replace with:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
let{env:e}=await import("@huggingface/transformers");
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then install the dependency:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
cd ~/.claude/plugins/cache/thedotmack/claude-mem/<version>
|
|
169
|
+
bun install
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
> ⚠️ **This patch is lost on plugin updates.** You'll need to re-apply after each claude-mem version upgrade.
|
|
173
|
+
|
|
174
|
+
### Verifying ChromaDB Works
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Check Chroma server
|
|
178
|
+
curl -s http://127.0.0.1:8000/api/v1/heartbeat
|
|
179
|
+
|
|
180
|
+
# Check worker health
|
|
181
|
+
curl -s http://127.0.0.1:37777/api/health
|
|
182
|
+
|
|
183
|
+
# Check for sync errors in worker logs
|
|
184
|
+
# Open http://localhost:37777 in browser for the web viewer
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
When working correctly, worker logs should show:
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
CHROMA_SYNC connected (remote mode, 127.0.0.1:8000)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
> **ChromaDB is optional.** Without it, claude-mem falls back to SQLite FTS5 full-text search. You lose semantic/vector search but keyword search still works.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
After editing source:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
bun run build
|
|
203
|
+
# Restart OpenCode to pick up changes
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If using symlink install, changes are picked up after rebuild. If using npm install, bump version and republish.
|
|
207
|
+
|
|
208
|
+
## Architecture
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
OpenCode ←→ Plugin (this repo) ←→ Claude-Mem Worker (port 37777) ←→ SQLite + ChromaDB
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The plugin is a thin HTTP client. All heavy lifting (LLM processing, storage, search) happens in the worker.
|
|
215
|
+
|
|
216
|
+
### Hook Mapping (Claude Code → OpenCode)
|
|
217
|
+
|
|
218
|
+
| Claude Code Hook | OpenCode Hook | Purpose |
|
|
219
|
+
|-----------------|---------------|---------|
|
|
220
|
+
| `SessionStart` → context | `experimental.chat.system.transform` | Inject memory into system prompt |
|
|
221
|
+
| `UserPromptSubmit` → session-init | `chat.message` | Init session with real user prompt |
|
|
222
|
+
| `PostToolUse` → observation | `tool.execute.after` | Capture tool executions |
|
|
223
|
+
| `Stop` → summarize | `event` (session.idle) | Summarize with real message content |
|
|
224
|
+
| `Stop` → session-complete | `event` (session.idle) | Mark session complete |
|
|
225
|
+
|
|
226
|
+
### Worker API Endpoints Used
|
|
227
|
+
|
|
228
|
+
| Method | Endpoint | Purpose |
|
|
229
|
+
|--------|----------|---------|
|
|
230
|
+
| `GET` | `/api/health` | Health check |
|
|
231
|
+
| `GET` | `/api/context/inject?project={name}` | Get formatted context for system prompt |
|
|
232
|
+
| `POST` | `/api/sessions/init` | Initialize session |
|
|
233
|
+
| `POST` | `/api/sessions/observations` | Send tool observation |
|
|
234
|
+
| `POST` | `/api/sessions/summarize` | Trigger summarization |
|
|
235
|
+
| `POST` | `/api/sessions/complete` | Complete session |
|
|
236
|
+
| `GET` | `/api/search?q={query}&project={name}` | Search memory |
|
|
237
|
+
|
|
238
|
+
## Key Implementation Details
|
|
239
|
+
|
|
240
|
+
- **Field name**: All worker API calls use `contentSessionId` (not `claudeSessionId`) — using the wrong field name causes silent failures
|
|
241
|
+
- **No console output**: All `console.log/warn/error` removed to avoid corrupting OpenCode TUI
|
|
242
|
+
- **Deferred toast**: `client.tui.showToast()` is deferred to first hook invocation (TUI not ready during plugin init, calling it crashes OpenCode)
|
|
243
|
+
- **Real prompt**: `chat.message` hook extracts actual user input instead of hardcoded "SESSION_START"
|
|
244
|
+
- **Real summarize**: `session.idle` fetches last user/assistant messages via `client.session.messages()` for meaningful summaries
|
|
245
|
+
|
|
246
|
+
## Troubleshooting
|
|
247
|
+
|
|
248
|
+
| Symptom | Cause | Fix |
|
|
249
|
+
|---------|-------|-----|
|
|
250
|
+
| No toast on startup | Worker not running | Start Claude Code first, or check `curl http://127.0.0.1:37777/api/health` |
|
|
251
|
+
| "Worker offline" toast | Worker crashed or not started | Restart Claude Code |
|
|
252
|
+
| OpenCode crashes on startup | Plugin calling TUI too early | Update to latest version (deferred toast fix) |
|
|
253
|
+
| TUI display corrupted | console.log in plugin code | Ensure no console output in source |
|
|
254
|
+
| "SESSION_START" in prompts | Old version bug | Update — `session.created` no longer calls sessionInit |
|
|
255
|
+
| Chroma sync failed | Python version or Chroma not running | See [Windows-Specific Setup](#windows-specific-setup) |
|
|
256
|
+
| Observations not saved | Wrong field name | Ensure `contentSessionId` (not `claudeSessionId`) |
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Plugin } from '@opencode-ai/plugin';
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode Plugin for Claude-Mem
|
|
4
|
+
*
|
|
5
|
+
* Hooks used:
|
|
6
|
+
* - `event` — session lifecycle (session.created, session.idle)
|
|
7
|
+
* - `tool.execute.after` — capture tool observations
|
|
8
|
+
* - `experimental.chat.system.transform` — inject memory context into system prompt
|
|
9
|
+
* - `chat.message` — fallback session init when event hook doesn't fire
|
|
10
|
+
* - `tool` — mem-search custom tool
|
|
11
|
+
*
|
|
12
|
+
* Uses TUI toast notifications for status feedback.
|
|
13
|
+
*/
|
|
14
|
+
export declare const ClaudeMemPlugin: Plugin;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { tool } from "@opencode-ai/plugin";
|
|
4
|
+
|
|
5
|
+
// src/worker-client.ts
|
|
6
|
+
class WorkerClient {
|
|
7
|
+
static PORT = 37777;
|
|
8
|
+
static BASE_URL = `http://127.0.0.1:${WorkerClient.PORT}`;
|
|
9
|
+
static async isHealthy() {
|
|
10
|
+
try {
|
|
11
|
+
const controller = new AbortController;
|
|
12
|
+
const timeoutId = setTimeout(() => controller.abort(), 1000);
|
|
13
|
+
const response = await fetch(`${this.BASE_URL}/api/health`, {
|
|
14
|
+
signal: controller.signal
|
|
15
|
+
});
|
|
16
|
+
clearTimeout(timeoutId);
|
|
17
|
+
return response.ok;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static async ensureRunning(_projectRoot) {
|
|
23
|
+
return await this.isHealthy();
|
|
24
|
+
}
|
|
25
|
+
static async sessionInit(contentSessionId, project, prompt) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(`${this.BASE_URL}/api/sessions/init`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: { "Content-Type": "application/json" },
|
|
30
|
+
body: JSON.stringify({ contentSessionId, project, prompt })
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return await response.json();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd) {
|
|
41
|
+
try {
|
|
42
|
+
await fetch(`${this.BASE_URL}/api/sessions/observations`, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
contentSessionId,
|
|
47
|
+
tool_name: toolName,
|
|
48
|
+
tool_input: toolInput,
|
|
49
|
+
tool_response: toolResponse,
|
|
50
|
+
cwd
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage) {
|
|
56
|
+
try {
|
|
57
|
+
await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "Content-Type": "application/json" },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
contentSessionId,
|
|
62
|
+
last_user_message: lastUserMessage,
|
|
63
|
+
last_assistant_message: lastAssistantMessage
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
static async completeSession(contentSessionId) {
|
|
69
|
+
try {
|
|
70
|
+
await fetch(`${this.BASE_URL}/api/sessions/complete`, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json" },
|
|
73
|
+
body: JSON.stringify({ contentSessionId })
|
|
74
|
+
});
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
static async getContext(project) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const data = await response.json();
|
|
84
|
+
if (typeof data === "string") {
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
if (data && typeof data.content === "string") {
|
|
88
|
+
return data.content;
|
|
89
|
+
}
|
|
90
|
+
const text = JSON.stringify(data, null, 2);
|
|
91
|
+
return text === "{}" || text === "null" ? null : text;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
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
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/index.ts
|
|
111
|
+
var ClaudeMemPlugin = async (ctx) => {
|
|
112
|
+
const { project, directory, client } = ctx;
|
|
113
|
+
const projectRoot = directory || process.cwd();
|
|
114
|
+
const projectName = project?.worktree ? project.worktree.split(/[\\/]/).findLast(Boolean) || "unknown-project" : "unknown-project";
|
|
115
|
+
async function toast(message, variant = "info", duration = 3000) {
|
|
116
|
+
try {
|
|
117
|
+
await client.tui.showToast({
|
|
118
|
+
body: { title: "Claude-Mem", message, variant, duration }
|
|
119
|
+
});
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
let workerHealthy = null;
|
|
123
|
+
let initToastShown = false;
|
|
124
|
+
async function checkWorkerAndToast() {
|
|
125
|
+
if (workerHealthy === null) {
|
|
126
|
+
workerHealthy = await WorkerClient.isHealthy();
|
|
127
|
+
}
|
|
128
|
+
if (!initToastShown) {
|
|
129
|
+
initToastShown = true;
|
|
130
|
+
if (workerHealthy) {
|
|
131
|
+
await toast(`Memory active \xB7 ${projectName}`, "success");
|
|
132
|
+
} else {
|
|
133
|
+
await toast("Worker offline \u2014 start Claude Code first", "warning", 5000);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return workerHealthy;
|
|
137
|
+
}
|
|
138
|
+
let currentSessionId = null;
|
|
139
|
+
const initializedSessions = new Set;
|
|
140
|
+
async function ensureSessionInit(sessionId, prompt) {
|
|
141
|
+
if (initializedSessions.has(sessionId)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
const isHealthy = await checkWorkerAndToast();
|
|
145
|
+
if (!isHealthy) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
await WorkerClient.sessionInit(sessionId, projectName, prompt || "SESSION_START");
|
|
150
|
+
initializedSessions.add(sessionId);
|
|
151
|
+
currentSessionId = sessionId;
|
|
152
|
+
return true;
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function extractTextFromParts(parts) {
|
|
158
|
+
if (!parts || !Array.isArray(parts)) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
162
|
+
`).trim();
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
event: async ({ event }) => {
|
|
166
|
+
if (event.type === "session.created") {
|
|
167
|
+
const sessionId = event.properties?.info?.id;
|
|
168
|
+
if (!sessionId) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
currentSessionId = sessionId;
|
|
172
|
+
const isHealthy = await checkWorkerAndToast();
|
|
173
|
+
if (isHealthy) {
|
|
174
|
+
await toast("Session linked to memory", "info", 2000);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (event.type === "session.idle") {
|
|
178
|
+
const sessionId = event.properties?.sessionID || currentSessionId;
|
|
179
|
+
if (!sessionId) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
let lastUserMessage = "";
|
|
184
|
+
let lastAssistantMessage = "";
|
|
185
|
+
try {
|
|
186
|
+
const result = await client.session.messages({
|
|
187
|
+
path: { id: sessionId }
|
|
188
|
+
});
|
|
189
|
+
if (result.data && Array.isArray(result.data)) {
|
|
190
|
+
const messages = result.data;
|
|
191
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
192
|
+
if (messages[i].info.role === "user") {
|
|
193
|
+
lastUserMessage = extractTextFromParts(messages[i].parts);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
198
|
+
if (messages[i].info.role === "assistant") {
|
|
199
|
+
lastAssistantMessage = extractTextFromParts(messages[i].parts);
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch {}
|
|
205
|
+
await WorkerClient.summarize(sessionId, lastUserMessage, lastAssistantMessage);
|
|
206
|
+
await WorkerClient.completeSession(sessionId);
|
|
207
|
+
await toast("Session summarized", "success", 2000);
|
|
208
|
+
} catch {}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
"chat.message": async (input, output) => {
|
|
212
|
+
const sessionId = input.sessionID;
|
|
213
|
+
if (sessionId) {
|
|
214
|
+
const userPrompt = extractTextFromParts(output.parts);
|
|
215
|
+
await ensureSessionInit(sessionId, userPrompt || undefined);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
219
|
+
const sessionId = input.sessionID;
|
|
220
|
+
if (sessionId) {
|
|
221
|
+
await ensureSessionInit(sessionId);
|
|
222
|
+
}
|
|
223
|
+
const isHealthy = await checkWorkerAndToast();
|
|
224
|
+
if (!isHealthy) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const context = await WorkerClient.getContext(projectName);
|
|
229
|
+
if (context) {
|
|
230
|
+
output.system.push(`[Claude-Mem] Memory Active. Previous Context:
|
|
231
|
+
${context}`);
|
|
232
|
+
}
|
|
233
|
+
} catch {}
|
|
234
|
+
},
|
|
235
|
+
"tool.execute.after": async (input, output) => {
|
|
236
|
+
const sessionId = input.sessionID || currentSessionId;
|
|
237
|
+
if (!sessionId) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await ensureSessionInit(sessionId);
|
|
241
|
+
try {
|
|
242
|
+
await WorkerClient.sendObservation(sessionId, input.tool, input.args || {}, output.output, projectRoot);
|
|
243
|
+
} catch {}
|
|
244
|
+
},
|
|
245
|
+
tool: {
|
|
246
|
+
"mem-search": tool({
|
|
247
|
+
description: "Search project history and memory. Use this to find information about past decisions, code changes, or bug fixes.",
|
|
248
|
+
args: {
|
|
249
|
+
query: tool.schema.string()
|
|
250
|
+
},
|
|
251
|
+
execute: async (args) => await WorkerClient.search(args.query, projectName)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
export {
|
|
257
|
+
ClaudeMemPlugin
|
|
258
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker Client for Claude-Mem
|
|
3
|
+
* Handles communication with the local worker service running on port 37777.
|
|
4
|
+
*/
|
|
5
|
+
export declare class WorkerClient {
|
|
6
|
+
private static readonly PORT;
|
|
7
|
+
private static readonly BASE_URL;
|
|
8
|
+
/**
|
|
9
|
+
* Check if the worker is healthy
|
|
10
|
+
*/
|
|
11
|
+
static isHealthy(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the worker is running.
|
|
14
|
+
*/
|
|
15
|
+
static ensureRunning(_projectRoot: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Initialize a session
|
|
18
|
+
*/
|
|
19
|
+
static sessionInit(contentSessionId: string, project: string, prompt: string): Promise<{
|
|
20
|
+
sessionDbId: number;
|
|
21
|
+
promptNumber: number;
|
|
22
|
+
} | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Send observation
|
|
25
|
+
*/
|
|
26
|
+
static sendObservation(contentSessionId: string, toolName: string, toolInput: any, toolResponse: any, cwd: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Trigger summarization
|
|
29
|
+
*/
|
|
30
|
+
static summarize(contentSessionId: string, lastUserMessage: string, lastAssistantMessage: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Complete session
|
|
33
|
+
*/
|
|
34
|
+
static completeSession(contentSessionId: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Get pre-formatted context for system prompt injection.
|
|
37
|
+
* Uses /api/context/inject which returns rich markdown with observations + session summaries.
|
|
38
|
+
*/
|
|
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
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ephemushroom/opencode-claude-mem",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin for Claude-Mem persistent memory system",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm --external @opencode-ai/plugin && tsc --emitDeclarationOnly",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"lint": "oxlint",
|
|
22
|
+
"fmt": "oxfmt --write src/",
|
|
23
|
+
"fmt:check": "oxfmt --check src/",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"prepublishOnly": "bun run clean && bun run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"opencode",
|
|
29
|
+
"claude-mem",
|
|
30
|
+
"memory",
|
|
31
|
+
"plugin",
|
|
32
|
+
"persistent-memory"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/Ephemushroom/opencode-claude-mem.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/Ephemushroom/opencode-claude-mem/issues"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/Ephemushroom/opencode-claude-mem#readme",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@opencode-ai/plugin": "latest"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"oxfmt": "^0.33.0",
|
|
53
|
+
"oxlint": "^1.48.0",
|
|
54
|
+
"typescript": "^5.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|