@ephemushroom/opencode-claude-mem 0.2.4 → 0.3.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/README.md +198 -224
- package/dist/index.d.ts +3 -1
- package/dist/index.js +366 -48
- package/dist/worker-client.d.ts +29 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,57 +1,20 @@
|
|
|
1
1
|
# opencode-claude-mem
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Persistent memory for [OpenCode](https://opencode.ai), powered by
|
|
4
|
+
[Claude-Mem](https://github.com/thedotmack/claude-mem).
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Share the same Claude-Mem worker, database, and search tools between Claude Code
|
|
7
|
+
and OpenCode. Once connected, previous observations and summaries are injected
|
|
8
|
+
into new OpenCode sessions automatically.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **Inject memory context** — previous session context is automatically injected into the system prompt on every LLM call
|
|
12
|
-
- **Capture tool observations** — every tool execution is recorded as a structured observation
|
|
13
|
-
- **Summarize sessions** — when a session goes idle, the last user/assistant messages are sent for summarization
|
|
14
|
-
- **Search memory** — memory search is available via claude-mem's MCP server
|
|
15
|
-
|
|
16
|
-
## Differences from Claude Code Version
|
|
17
|
-
|
|
18
|
-
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.
|
|
19
|
-
|
|
20
|
-
This OpenCode version works differently:
|
|
21
|
-
|
|
22
|
-
| | Claude Code | OpenCode (this plugin) |
|
|
23
|
-
|---|---|---|
|
|
24
|
-
| **Architecture** | Shell commands (`node script.js`) | JavaScript plugin API |
|
|
25
|
-
| **Context injection** | `SessionStart` hook stdout → system prompt (once) | `experimental.chat.system.transform` → system prompt (every LLM call, session-level cache) |
|
|
26
|
-
| **Context freshness** | Snapshot at session start | Cached once per session (same as Claude Code) |
|
|
27
|
-
| **Session init** | `UserPromptSubmit` hook | `chat.message` hook with real user prompt |
|
|
28
|
-
| **Observations** | `PostToolUse` hook | `tool.execute.after` hook (with circular memory protection) |
|
|
29
|
-
| **Summarization** | `Stop` hook | `session.idle` event |
|
|
30
|
-
| **Memory search** | MCP server | MCP server (same — no custom tool needed) |
|
|
31
|
-
|
|
32
|
-
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.
|
|
33
|
-
|
|
34
|
-
## Prerequisites
|
|
35
|
-
|
|
36
|
-
- [Claude Code](https://claude.com/claude-code) with [claude-mem plugin](https://github.com/thedotmack/claude-mem) installed and running
|
|
37
|
-
- [OpenCode](https://opencode.ai) with plugin support
|
|
38
|
-
|
|
39
|
-
## Installation
|
|
40
|
-
|
|
41
|
-
### Step 1: Install Claude-Mem (if not already)
|
|
10
|
+
> **Note:** This plugin is a thin OpenCode adapter for an existing Claude-Mem
|
|
11
|
+
> installation. It does **not** install Claude-Mem, manage slash commands, or
|
|
12
|
+
> start the worker for you.
|
|
42
13
|
|
|
43
|
-
|
|
14
|
+
## Quick Start
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
/plugin install claude-mem
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Restart Claude Code. The worker service will start automatically on port 37777.
|
|
51
|
-
|
|
52
|
-
### Step 2: Add the Plugin
|
|
53
|
-
|
|
54
|
-
Add to your `opencode.json` (project or global `~/.config/opencode/opencode.json`):
|
|
16
|
+
1. Install and configure Claude-Mem in Claude Code.
|
|
17
|
+
2. Add this plugin to your `opencode.json`:
|
|
55
18
|
|
|
56
19
|
```json
|
|
57
20
|
{
|
|
@@ -59,241 +22,252 @@ Add to your `opencode.json` (project or global `~/.config/opencode/opencode.json
|
|
|
59
22
|
}
|
|
60
23
|
```
|
|
61
24
|
|
|
62
|
-
Restart OpenCode.
|
|
25
|
+
3. Restart OpenCode.
|
|
26
|
+
4. Start a session — memory context will be injected automatically when the
|
|
27
|
+
Claude-Mem worker is available.
|
|
28
|
+
|
|
29
|
+
## Key Features
|
|
30
|
+
|
|
31
|
+
- **Shared Memory** — Uses the same Claude-Mem worker and memory store as
|
|
32
|
+
Claude Code.
|
|
33
|
+
- **Auto-Start** — When the plugin loads and the Claude-Mem worker is not
|
|
34
|
+
already running, it spawns `bunx claude-mem start` once per OpenCode process
|
|
35
|
+
(skipped silently if `bun` is not on `PATH` or the worker is already up).
|
|
36
|
+
- **Automatic Context Injection** — Injects relevant project memory into the
|
|
37
|
+
system prompt for new OpenCode turns.
|
|
38
|
+
- **Compaction Support** — Re-injects memory context during OpenCode session
|
|
39
|
+
compaction so long conversations keep their historical context.
|
|
40
|
+
- **Observation Capture** — Sends tool observations to Claude-Mem for future
|
|
41
|
+
retrieval and summarization.
|
|
42
|
+
- **Observation Hardening** — Skips low-value meta tools, strips
|
|
43
|
+
`<claude-mem-context>` and `<private>` tags before storage, and truncates
|
|
44
|
+
oversized observation payloads by UTF-8 byte size.
|
|
45
|
+
- **Graceful Degradation** — If the worker is offline and cannot be started,
|
|
46
|
+
the plugin fails open and OpenCode continues to work normally.
|
|
63
47
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
# Check worker is running
|
|
68
|
-
curl -s http://127.0.0.1:37777/api/health
|
|
48
|
+
## How It Works
|
|
69
49
|
|
|
70
|
-
# Restart OpenCode — you should see toast: "Memory active · {project}"
|
|
71
50
|
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
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.
|
|
89
|
-
|
|
90
|
-
### Alternative: Manual Installation (from source)
|
|
91
|
-
|
|
92
|
-
If you prefer to install from source instead of npm:
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
git clone https://github.com/Ephemushroom/opencode-claude-mem.git
|
|
96
|
-
cd opencode-claude-mem
|
|
97
|
-
bun install
|
|
98
|
-
bun run build
|
|
51
|
+
OpenCode session
|
|
52
|
+
|
|
|
53
|
+
|-- plugin loads
|
|
54
|
+
| '-- connect to Claude-Mem worker on port 37777
|
|
55
|
+
|
|
|
56
|
+
|-- session.created ................ track session + reset cache
|
|
57
|
+
|-- chat.message ................... init session with real user prompt
|
|
58
|
+
|-- tool.execute.after ............. send tool observation
|
|
59
|
+
|-- system.transform ............... inject memory into system prompt
|
|
60
|
+
|-- session.compacting ............. preserve memory during compaction
|
|
61
|
+
|-- message.updated ................ capture assistant text (debounced)
|
|
62
|
+
|-- file.edited .................... record file edit observation
|
|
63
|
+
|-- session.compacted .............. summarize after compaction
|
|
64
|
+
|-- session.idle ................... flush + summarize + complete session
|
|
65
|
+
'-- session.deleted ................ flush + complete (no zombie sessions)
|
|
99
66
|
```
|
|
100
67
|
|
|
101
|
-
|
|
68
|
+
The plugin is intentionally small. It only adapts OpenCode hook events to the
|
|
69
|
+
Claude-Mem worker HTTP API. All indexing, summarization, memory search, and
|
|
70
|
+
storage stay in upstream Claude-Mem.
|
|
102
71
|
|
|
103
|
-
|
|
72
|
+
## Architecture
|
|
104
73
|
|
|
105
|
-
```bash
|
|
106
|
-
mkdir -p ~/.config/opencode/plugin
|
|
107
|
-
ln -sf "$(pwd)/dist/index.js" ~/.config/opencode/plugin/claude-mem.js
|
|
108
74
|
```
|
|
109
|
-
|
|
110
|
-
**Windows (requires elevated terminal):**
|
|
111
|
-
|
|
112
|
-
```powershell
|
|
113
|
-
# Option A: If you have gsudo / sudo installed
|
|
114
|
-
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'"
|
|
115
|
-
|
|
116
|
-
# Option B: Run PowerShell as Administrator
|
|
117
|
-
New-Item -ItemType SymbolicLink -Path "$env:USERPROFILE\.config\opencode\plugin\claude-mem.js" -Target "D:\path\to\opencode-claude-mem\dist\index.js"
|
|
75
|
+
OpenCode <-> Plugin (this repo) <-> Claude-Mem Worker (port 37777) <-> SQLite + ChromaDB
|
|
118
76
|
```
|
|
119
77
|
|
|
120
|
-
|
|
78
|
+
### Hook Mapping
|
|
121
79
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
80
|
+
| Claude Code | OpenCode plugin | Purpose |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `SessionStart` | `experimental.chat.system.transform` | Inject memory context |
|
|
83
|
+
| `SessionStart` | `experimental.session.compacting` | Preserve memory during compaction |
|
|
84
|
+
| `UserPromptSubmit` | `chat.message` | Initialize session with real user prompt |
|
|
85
|
+
| `PostToolUse` | `tool.execute.after` | Capture tool observations |
|
|
86
|
+
| _(streaming)_ | `event` (`message.updated`) | Capture assistant text (debounced) |
|
|
87
|
+
| _(streaming)_ | `event` (`file.edited`) | Record file edit observations |
|
|
88
|
+
| _(compaction)_ | `event` (`session.compacted`) | Summarize after OpenCode compacts |
|
|
89
|
+
| `Stop` | `event` (`session.idle`) | Flush + summarize + complete |
|
|
90
|
+
| `SessionEnd` | `event` (`session.deleted`) | Flush + complete (no zombie active rows) |
|
|
125
91
|
|
|
126
|
-
|
|
92
|
+
### Worker API Endpoints Used
|
|
127
93
|
|
|
128
|
-
|
|
94
|
+
| Method | Endpoint | Purpose |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `GET` | `/api/health` | Health check |
|
|
97
|
+
| `GET` | `/api/context/inject?project={name}` | Fetch formatted memory context |
|
|
98
|
+
| `POST` | `/api/sessions/init` | Initialize session |
|
|
99
|
+
| `POST` | `/api/sessions/observations` | Store tool observation |
|
|
100
|
+
| `POST` | `/api/sessions/summarize` | Trigger summarization |
|
|
101
|
+
| `POST` | `/api/sessions/complete` | Complete session |
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
Chroma connection failed: Chroma server not reachable.
|
|
132
|
-
```
|
|
103
|
+
## Installation
|
|
133
104
|
|
|
134
|
-
|
|
105
|
+
### Prerequisites
|
|
135
106
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
107
|
+
- [Claude Code](https://claude.com/claude-code) with
|
|
108
|
+
[Claude-Mem](https://github.com/thedotmack/claude-mem) installed
|
|
109
|
+
- [OpenCode](https://opencode.ai) with plugin support
|
|
110
|
+
- A running Claude-Mem worker on port `37777` (default)
|
|
139
111
|
|
|
140
|
-
|
|
112
|
+
### Step 1: Install Claude-Mem
|
|
141
113
|
|
|
142
|
-
|
|
114
|
+
In Claude Code:
|
|
143
115
|
|
|
144
|
-
|
|
116
|
+
```text
|
|
117
|
+
/plugin marketplace add thedotmack/claude-mem
|
|
118
|
+
/plugin install claude-mem
|
|
119
|
+
```
|
|
145
120
|
|
|
146
|
-
|
|
121
|
+
Restart Claude Code so the worker can start and initialize its data directory.
|
|
147
122
|
|
|
148
|
-
|
|
149
|
-
# Install Python ChromaDB CLI
|
|
150
|
-
uv tool install chromadb --python 3.13
|
|
151
|
-
```
|
|
123
|
+
### Step 2: Add the OpenCode Plugin
|
|
152
124
|
|
|
153
|
-
|
|
125
|
+
Add this plugin to your project or global `opencode.json`:
|
|
154
126
|
|
|
155
127
|
```json
|
|
156
128
|
{
|
|
157
|
-
"
|
|
129
|
+
"plugin": ["@ephemushroom/opencode-claude-mem"]
|
|
158
130
|
}
|
|
159
131
|
```
|
|
160
132
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
chroma run --path %USERPROFILE%\.claude-mem\vector-db --host 127.0.0.1 --port 8000
|
|
165
|
-
```
|
|
133
|
+
Then restart OpenCode.
|
|
166
134
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
### ChromaDB Embedding Model Crash (Bun Runtime)
|
|
170
|
-
|
|
171
|
-
After connecting to Chroma, the worker may crash with:
|
|
135
|
+
### Step 3: Verify
|
|
172
136
|
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
(evaluating 'e.cacheDir = ...')
|
|
137
|
+
```bash
|
|
138
|
+
curl -s http://127.0.0.1:37777/api/health
|
|
176
139
|
```
|
|
177
140
|
|
|
178
|
-
|
|
141
|
+
If the worker is healthy, OpenCode should show a toast like
|
|
142
|
+
`Memory active · <project>` when a session starts.
|
|
179
143
|
|
|
180
|
-
|
|
144
|
+
## Usage
|
|
181
145
|
|
|
182
|
-
|
|
146
|
+
Once installed, the plugin works automatically:
|
|
183
147
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
148
|
+
- **Context injection** — Memory is injected into the system prompt on each LLM
|
|
149
|
+
call, with session-level caching to avoid repeated worker requests.
|
|
150
|
+
- **Compaction preservation** — The same cached memory context is pushed into
|
|
151
|
+
OpenCode’s compaction path so memory survives conversation compression.
|
|
152
|
+
- **Tool observation capture** — Tool executions are stored as observations,
|
|
153
|
+
except for low-value/meta tools and Claude-Mem search tools.
|
|
154
|
+
- **Assistant message capture** — Assistant text from `message.updated` is
|
|
155
|
+
buffered with 250ms debounce per session and sent as a single
|
|
156
|
+
`assistant_message` observation per turn (avoids streaming-chunk floods).
|
|
157
|
+
- **File edit capture** — `file.edited` events are forwarded as `file_edit`
|
|
158
|
+
observations (path only; OpenCode does not include diff in the event).
|
|
159
|
+
- **Session summarization** — On `session.compacted` and `session.idle`, the
|
|
160
|
+
plugin flushes any pending assistant buffer, fetches the latest user and
|
|
161
|
+
assistant messages, and asks Claude-Mem to summarize them.
|
|
162
|
+
- **Session lifecycle hygiene** — On `session.deleted` the plugin tells the
|
|
163
|
+
worker to `completeSession`, preventing zombie `'active'` rows from
|
|
164
|
+
accumulating stale `pending_messages` (the "queueDepth never decreases"
|
|
165
|
+
failure mode).
|
|
166
|
+
|
|
167
|
+
## Memory Search
|
|
168
|
+
|
|
169
|
+
Memory search is provided by Claude-Mem’s MCP server, not by this plugin.
|
|
170
|
+
|
|
171
|
+
If your OpenCode environment already has Claude-Mem MCP tools configured, the
|
|
172
|
+
assistant can query project memory directly with Claude-Mem’s search workflow:
|
|
173
|
+
|
|
174
|
+
1. `search(query="...")`
|
|
175
|
+
2. `timeline(anchor=ID)`
|
|
176
|
+
3. `get_observations(ids=[...])`
|
|
177
|
+
|
|
178
|
+
This plugin focuses on sending memory data to the worker and injecting returned
|
|
179
|
+
context back into OpenCode sessions.
|
|
187
180
|
|
|
188
|
-
|
|
181
|
+
## Key Implementation Details
|
|
189
182
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
183
|
+
- **Thin client architecture** — `src/index.ts` handles OpenCode hooks and
|
|
184
|
+
session state; `src/worker-client.ts` is a static HTTP client.
|
|
185
|
+
- **No console logging** — The plugin never writes to `console.*` because that
|
|
186
|
+
can corrupt the OpenCode TUI.
|
|
187
|
+
- **Deferred toast** — Health toasts only happen after hook execution begins,
|
|
188
|
+
avoiding startup crashes caused by early TUI access.
|
|
189
|
+
- **Real prompt initialization** — `chat.message` sends the actual user prompt
|
|
190
|
+
to Claude-Mem instead of a synthetic placeholder.
|
|
191
|
+
- **Real summarize payloads** — On `session.idle`, the plugin reads the latest
|
|
192
|
+
session messages and sends the last user and assistant messages for summary.
|
|
193
|
+
- **Context caching** — Memory context is fetched once per session and reused
|
|
194
|
+
across prompt injection and compaction.
|
|
195
|
+
- **Circular memory protection** — Injected context is wrapped in
|
|
196
|
+
`<claude-mem-context>` tags, Claude-Mem MCP search tools are skipped, and
|
|
197
|
+
memory-related tags are stripped before storing observations.
|
|
198
|
+
- **UTF-8 truncation** — Large observation outputs are capped by byte size to
|
|
199
|
+
reduce token waste and avoid oversize worker payloads.
|
|
200
|
+
- **Field name correctness** — Worker payloads use `contentSessionId`, not
|
|
201
|
+
`claudeSessionId`.
|
|
202
|
+
|
|
203
|
+
## Differences from `bloodf/opencode-mem`
|
|
204
|
+
|
|
205
|
+
This project intentionally stays smaller in scope.
|
|
206
|
+
|
|
207
|
+
- **This plugin does**: bridge OpenCode hooks to an already-installed
|
|
208
|
+
Claude-Mem worker.
|
|
209
|
+
- **This plugin does not**: auto-install Claude-Mem, auto-edit OpenCode config,
|
|
210
|
+
auto-copy skills, auto-register slash commands, or auto-start the worker.
|
|
211
|
+
|
|
212
|
+
That keeps the runtime behavior predictable and leaves worker ownership with the
|
|
213
|
+
upstream Claude-Mem installation.
|
|
193
214
|
|
|
194
|
-
|
|
215
|
+
## Troubleshooting
|
|
195
216
|
|
|
196
|
-
|
|
197
|
-
let{env:e}=await import("@huggingface/transformers");
|
|
198
|
-
```
|
|
217
|
+
### No memory appears in OpenCode
|
|
199
218
|
|
|
200
|
-
|
|
219
|
+
- Confirm the worker is running:
|
|
201
220
|
|
|
202
221
|
```bash
|
|
203
|
-
|
|
204
|
-
bun install
|
|
222
|
+
curl -s http://127.0.0.1:37777/api/health
|
|
205
223
|
```
|
|
206
224
|
|
|
207
|
-
|
|
225
|
+
- Make sure Claude-Mem has already been installed and used from Claude Code.
|
|
226
|
+
- Start a fresh OpenCode session after the worker is healthy.
|
|
208
227
|
|
|
209
|
-
###
|
|
228
|
+
### OpenCode shows `Worker offline`
|
|
210
229
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
curl -s http://127.0.0.1:8000/api/v1/heartbeat
|
|
230
|
+
The plugin will try to launch the worker via `bunx claude-mem start` once on
|
|
231
|
+
plugin load. If that toast still appears:
|
|
214
232
|
|
|
215
|
-
|
|
216
|
-
|
|
233
|
+
- Confirm `bun` is on your `PATH` — `bun --version` should print a version.
|
|
234
|
+
- Confirm `claude-mem` is installed for `bunx`/`npx` — run
|
|
235
|
+
`bunx claude-mem --version` once to populate the cache.
|
|
236
|
+
- On Windows after a forced kill, port `37777` may stay in `TIME_WAIT` for
|
|
237
|
+
30-120 seconds; wait it out or restart Claude Code.
|
|
238
|
+
- Restart Claude Code to bring Claude-Mem back up via its own supervisor.
|
|
239
|
+
- Verify the worker port is still `37777`.
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
# Open http://localhost:37777 in browser for the web viewer
|
|
220
|
-
```
|
|
241
|
+
### OpenCode crashes on startup
|
|
221
242
|
|
|
222
|
-
|
|
243
|
+
- Update to a version with deferred TUI access.
|
|
244
|
+
- Ensure there are no `console.log`, `console.warn`, or `console.error` calls in
|
|
245
|
+
local plugin modifications.
|
|
223
246
|
|
|
224
|
-
|
|
225
|
-
CHROMA_SYNC connected (remote mode, 127.0.0.1:8000)
|
|
226
|
-
```
|
|
247
|
+
### Observations are missing or incomplete
|
|
227
248
|
|
|
228
|
-
|
|
249
|
+
- Ensure the worker API payload uses `contentSessionId`.
|
|
250
|
+
- Be aware that low-value meta tools and Claude-Mem search tools are skipped by
|
|
251
|
+
design.
|
|
252
|
+
- Very large tool outputs are truncated before storage.
|
|
229
253
|
|
|
230
|
-
|
|
254
|
+
### Memory search is unavailable
|
|
231
255
|
|
|
232
|
-
|
|
256
|
+
- This plugin does not configure MCP tools for you.
|
|
257
|
+
- Configure Claude-Mem’s MCP server separately in your OpenCode environment if
|
|
258
|
+
you want in-editor memory search.
|
|
233
259
|
|
|
234
|
-
|
|
260
|
+
## Development
|
|
235
261
|
|
|
236
262
|
```bash
|
|
263
|
+
bun install
|
|
237
264
|
bun run build
|
|
238
|
-
|
|
265
|
+
bun run lint
|
|
266
|
+
bun run fmt:check
|
|
239
267
|
```
|
|
240
268
|
|
|
241
|
-
If
|
|
242
|
-
|
|
243
|
-
## Architecture
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
OpenCode ←→ Plugin (this repo) ←→ Claude-Mem Worker (port 37777) ←→ SQLite + ChromaDB
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
The plugin is a thin HTTP client. All heavy lifting (LLM processing, storage, search) happens in the worker.
|
|
250
|
-
|
|
251
|
-
### Hook Mapping (Claude Code → OpenCode)
|
|
252
|
-
|
|
253
|
-
| Claude Code Hook | OpenCode Hook | Purpose |
|
|
254
|
-
|-----------------|---------------|---------|
|
|
255
|
-
| `SessionStart` → context | `experimental.chat.system.transform` | Inject memory into system prompt (every LLM call, session-level cache) |
|
|
256
|
-
| `UserPromptSubmit` → session-init | `chat.message` | Init session with real user prompt |
|
|
257
|
-
| `PostToolUse` → observation | `tool.execute.after` | Capture tool executions |
|
|
258
|
-
| `Stop` → summarize | `event` (session.idle) | Summarize with real message content |
|
|
259
|
-
| `Stop` → session-complete | `event` (session.idle) | Mark session complete |
|
|
260
|
-
| *(no equivalent)* | `config` + `command.execute.before` | `/memory` command for user-visible context |
|
|
261
|
-
|
|
262
|
-
### Worker API Endpoints Used
|
|
263
|
-
|
|
264
|
-
| Method | Endpoint | Purpose |
|
|
265
|
-
|--------|----------|---------|
|
|
266
|
-
| `GET` | `/api/health` | Health check |
|
|
267
|
-
| `GET` | `/api/context/inject?project={name}` | Get formatted context for system prompt |
|
|
268
|
-
| `POST` | `/api/sessions/init` | Initialize session |
|
|
269
|
-
| `POST` | `/api/sessions/observations` | Send tool observation |
|
|
270
|
-
| `POST` | `/api/sessions/summarize` | Trigger summarization |
|
|
271
|
-
| `POST` | `/api/sessions/complete` | Complete session |
|
|
272
|
-
|
|
273
|
-
## Key Implementation Details
|
|
274
|
-
|
|
275
|
-
- **Field name**: All worker API calls use `contentSessionId` (not `claudeSessionId`) — using the wrong field name causes silent failures
|
|
276
|
-
- **No console output**: All `console.log/warn/error` removed to avoid corrupting OpenCode TUI
|
|
277
|
-
- **Deferred toast**: `client.tui.showToast()` is deferred to first hook invocation (TUI not ready during plugin init, calling it crashes OpenCode)
|
|
278
|
-
- **Real prompt**: `chat.message` hook extracts actual user input instead of hardcoded "SESSION_START"
|
|
279
|
-
- **Real summarize**: `session.idle` fetches last user/assistant messages via `client.session.messages()` for meaningful summaries
|
|
280
|
-
- **Context caching**: `getCachedContext()` caches context once per session (invalidated on `session.created`) to avoid redundant worker calls
|
|
281
|
-
- **Content-type handling**: Worker returns `text/plain` markdown — `getContext()` checks content-type and uses `response.text()` instead of `response.json()`
|
|
282
|
-
- **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
|
|
283
|
-
- **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
|
|
284
|
-
- **`/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)
|
|
285
|
-
|
|
286
|
-
## Troubleshooting
|
|
287
|
-
|
|
288
|
-
| Symptom | Cause | Fix |
|
|
289
|
-
|---------|-------|-----|
|
|
290
|
-
| No toast on startup | Worker not running | Start Claude Code first, or check `curl http://127.0.0.1:37777/api/health` |
|
|
291
|
-
| "Worker offline" toast | Worker crashed or not started | Restart Claude Code |
|
|
292
|
-
| OpenCode crashes on startup | Plugin calling TUI too early | Update to latest version (deferred toast fix) |
|
|
293
|
-
| TUI display corrupted | console.log in plugin code | Ensure no console output in source |
|
|
294
|
-
| "SESSION_START" in prompts | Old version bug | Update — `session.created` no longer calls sessionInit |
|
|
295
|
-
| Chroma sync failed | Python version or Chroma not running | See [Windows-Specific Setup](#windows-specific-setup) |
|
|
296
|
-
| Observations not saved | Wrong field name | Ensure `contentSessionId` (not `claudeSessionId`) |
|
|
269
|
+
If you edit source code locally, rebuild and restart OpenCode to pick up the new
|
|
270
|
+
plugin bundle.
|
|
297
271
|
|
|
298
272
|
## License
|
|
299
273
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,11 @@ import { type Plugin } from '@opencode-ai/plugin';
|
|
|
3
3
|
* OpenCode Plugin for Claude-Mem
|
|
4
4
|
*
|
|
5
5
|
* Hooks used:
|
|
6
|
-
* - `event` — session lifecycle (session.created, session.idle
|
|
6
|
+
* - `event` — session lifecycle (session.created, session.idle, session.compacted,
|
|
7
|
+
* session.deleted, message.updated, file.edited)
|
|
7
8
|
* - `tool.execute.after` — capture tool observations
|
|
8
9
|
* - `experimental.chat.system.transform` — inject memory context into system prompt
|
|
10
|
+
* - `experimental.session.compacting` — preserve memory context during compaction
|
|
9
11
|
* - `chat.message` — session init with real user prompt
|
|
10
12
|
*
|
|
11
13
|
* Memory context is automatically injected into every conversation via system prompt.
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/worker-client.ts
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
var HEALTH_TIMEOUT_MS = 1000;
|
|
6
|
+
var AUTOSTART_POLL_INTERVAL_MS = 500;
|
|
7
|
+
var AUTOSTART_POLL_MAX_ATTEMPTS = 16;
|
|
8
|
+
|
|
3
9
|
class WorkerClient {
|
|
4
10
|
static PORT = 37777;
|
|
5
11
|
static BASE_URL = `http://127.0.0.1:${WorkerClient.PORT}`;
|
|
12
|
+
static autoStartPromise = null;
|
|
6
13
|
static async isHealthy() {
|
|
7
14
|
try {
|
|
8
15
|
const controller = new AbortController;
|
|
9
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
16
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
10
17
|
const response = await fetch(`${this.BASE_URL}/api/health`, {
|
|
11
18
|
signal: controller.signal
|
|
12
19
|
});
|
|
@@ -16,8 +23,71 @@ class WorkerClient {
|
|
|
16
23
|
return false;
|
|
17
24
|
}
|
|
18
25
|
}
|
|
19
|
-
static async ensureRunning(
|
|
20
|
-
|
|
26
|
+
static async ensureRunning() {
|
|
27
|
+
const result = await this.tryAutoStart();
|
|
28
|
+
return result === "already-running" || result === "started";
|
|
29
|
+
}
|
|
30
|
+
static tryAutoStart() {
|
|
31
|
+
if (!this.autoStartPromise) {
|
|
32
|
+
this.autoStartPromise = this.runAutoStart().catch(() => "spawn-failed");
|
|
33
|
+
}
|
|
34
|
+
return this.autoStartPromise;
|
|
35
|
+
}
|
|
36
|
+
static async runAutoStart() {
|
|
37
|
+
if (await this.isHealthy()) {
|
|
38
|
+
return "already-running";
|
|
39
|
+
}
|
|
40
|
+
const bunPath = this.findBun();
|
|
41
|
+
if (!bunPath) {
|
|
42
|
+
return "no-bun";
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const child = spawn(bunPath, ["x", "claude-mem", "start"], {
|
|
46
|
+
detached: true,
|
|
47
|
+
stdio: "ignore",
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
env: process.env
|
|
50
|
+
});
|
|
51
|
+
child.on("error", () => {});
|
|
52
|
+
child.unref();
|
|
53
|
+
} catch {
|
|
54
|
+
return "spawn-failed";
|
|
55
|
+
}
|
|
56
|
+
for (let i = 0;i < AUTOSTART_POLL_MAX_ATTEMPTS; i++) {
|
|
57
|
+
await new Promise((resolve) => setTimeout(resolve, AUTOSTART_POLL_INTERVAL_MS));
|
|
58
|
+
if (await this.isHealthy()) {
|
|
59
|
+
return "started";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return "timeout";
|
|
63
|
+
}
|
|
64
|
+
static findBun() {
|
|
65
|
+
const bunGlobal = globalThis.Bun;
|
|
66
|
+
if (bunGlobal && typeof bunGlobal.which === "function") {
|
|
67
|
+
try {
|
|
68
|
+
const found = bunGlobal.which("bun");
|
|
69
|
+
if (typeof found === "string" && found) {
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
const path = process.env.PATH || "";
|
|
75
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
76
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
77
|
+
const slash = process.platform === "win32" ? "\\" : "/";
|
|
78
|
+
const dirs = path.split(sep).filter(Boolean);
|
|
79
|
+
for (const dir of dirs) {
|
|
80
|
+
const trailing = dir.endsWith("/") || dir.endsWith("\\") ? "" : slash;
|
|
81
|
+
for (const ext of exts) {
|
|
82
|
+
const candidate = `${dir}${trailing}bun${ext}`;
|
|
83
|
+
try {
|
|
84
|
+
if (existsSync(candidate)) {
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
21
91
|
}
|
|
22
92
|
static async sessionInit(contentSessionId, project, prompt) {
|
|
23
93
|
try {
|
|
@@ -98,6 +168,101 @@ class WorkerClient {
|
|
|
98
168
|
}
|
|
99
169
|
|
|
100
170
|
// src/index.ts
|
|
171
|
+
var MAX_OBSERVATION_BYTES = 24 * 1024;
|
|
172
|
+
var MAX_TAG_REPLACEMENTS = 100;
|
|
173
|
+
var ASSISTANT_FLUSH_DEBOUNCE_MS = 250;
|
|
174
|
+
var META_TOOLS = new Set([
|
|
175
|
+
"askuserquestion",
|
|
176
|
+
"getmcpresource",
|
|
177
|
+
"listmcpresourcestool",
|
|
178
|
+
"listmcptools",
|
|
179
|
+
"skill",
|
|
180
|
+
"slashcommand",
|
|
181
|
+
"todowrite"
|
|
182
|
+
]);
|
|
183
|
+
var PRIVATE_TAG_REGEX = /<private>[\s\S]*?<\/private>/g;
|
|
184
|
+
var CONTEXT_TAG_REGEX = /<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g;
|
|
185
|
+
function stripTaggedContent(text) {
|
|
186
|
+
if (!text) {
|
|
187
|
+
return text;
|
|
188
|
+
}
|
|
189
|
+
let result = text;
|
|
190
|
+
let replacements = 0;
|
|
191
|
+
while (replacements < MAX_TAG_REPLACEMENTS && PRIVATE_TAG_REGEX.test(result)) {
|
|
192
|
+
PRIVATE_TAG_REGEX.lastIndex = 0;
|
|
193
|
+
result = result.replace(PRIVATE_TAG_REGEX, "");
|
|
194
|
+
replacements++;
|
|
195
|
+
}
|
|
196
|
+
PRIVATE_TAG_REGEX.lastIndex = 0;
|
|
197
|
+
while (replacements < MAX_TAG_REPLACEMENTS && CONTEXT_TAG_REGEX.test(result)) {
|
|
198
|
+
CONTEXT_TAG_REGEX.lastIndex = 0;
|
|
199
|
+
result = result.replace(CONTEXT_TAG_REGEX, "");
|
|
200
|
+
replacements++;
|
|
201
|
+
}
|
|
202
|
+
CONTEXT_TAG_REGEX.lastIndex = 0;
|
|
203
|
+
return result.trim();
|
|
204
|
+
}
|
|
205
|
+
function sanitizeObservationValue(value) {
|
|
206
|
+
if (typeof value === "string") {
|
|
207
|
+
return stripTaggedContent(value);
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
return value.map((item) => sanitizeObservationValue(item));
|
|
211
|
+
}
|
|
212
|
+
if (value && typeof value === "object") {
|
|
213
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeObservationValue(item)]));
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
function truncateUtf8Bytes(text, maxBytes) {
|
|
218
|
+
const encoder = new TextEncoder;
|
|
219
|
+
if (encoder.encode(text).length <= maxBytes) {
|
|
220
|
+
return text;
|
|
221
|
+
}
|
|
222
|
+
const suffix = `
|
|
223
|
+
[truncated]`;
|
|
224
|
+
const suffixBytes = encoder.encode(suffix).length;
|
|
225
|
+
const budget = Math.max(maxBytes - suffixBytes, 0);
|
|
226
|
+
let low = 0;
|
|
227
|
+
let high = text.length;
|
|
228
|
+
while (low < high) {
|
|
229
|
+
const mid = Math.ceil((low + high) / 2);
|
|
230
|
+
const candidate = text.slice(0, mid);
|
|
231
|
+
if (encoder.encode(candidate).length <= budget) {
|
|
232
|
+
low = mid;
|
|
233
|
+
} else {
|
|
234
|
+
high = mid - 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return `${text.slice(0, low)}${suffix}`;
|
|
238
|
+
}
|
|
239
|
+
function shouldSkipObservationTool(toolName) {
|
|
240
|
+
if (!toolName) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
const normalizedName = toolName.toLowerCase();
|
|
244
|
+
return normalizedName.startsWith("claude-mem_mcp-search_") || META_TOOLS.has(normalizedName);
|
|
245
|
+
}
|
|
246
|
+
function normalizeToolOutput(output) {
|
|
247
|
+
if (typeof output === "string") {
|
|
248
|
+
return output;
|
|
249
|
+
}
|
|
250
|
+
if (output === undefined || output === null) {
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return JSON.stringify(output);
|
|
255
|
+
} catch {
|
|
256
|
+
return String(output);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function extractTextFromParts(parts) {
|
|
260
|
+
if (!parts || !Array.isArray(parts)) {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
return parts.filter((p) => p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored).map((p) => p.text).join(`
|
|
264
|
+
`).trim();
|
|
265
|
+
}
|
|
101
266
|
var ClaudeMemPlugin = async (ctx) => {
|
|
102
267
|
const { project, directory, client } = ctx;
|
|
103
268
|
const projectRoot = directory || process.cwd();
|
|
@@ -122,20 +287,59 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
122
287
|
let initToastShown = false;
|
|
123
288
|
async function checkWorkerAndToast() {
|
|
124
289
|
if (workerHealthy === null) {
|
|
125
|
-
workerHealthy = await WorkerClient.
|
|
290
|
+
workerHealthy = await WorkerClient.ensureRunning();
|
|
126
291
|
}
|
|
127
292
|
if (!initToastShown) {
|
|
128
293
|
initToastShown = true;
|
|
129
294
|
if (workerHealthy) {
|
|
130
295
|
await toast(`Memory active \xB7 ${projectName}`, "success");
|
|
131
296
|
} else {
|
|
132
|
-
await toast("Worker offline \u2014 start Claude Code first", "warning", 5000);
|
|
297
|
+
await toast("Worker offline \u2014 install Claude-Mem (bunx claude-mem start) or start Claude Code first", "warning", 5000);
|
|
133
298
|
}
|
|
134
299
|
}
|
|
135
300
|
return workerHealthy;
|
|
136
301
|
}
|
|
137
302
|
let currentSessionId = null;
|
|
138
303
|
const initializedSessions = new Set;
|
|
304
|
+
const assistantBuffers = new Map;
|
|
305
|
+
async function flushAssistantBuffer(sessionId) {
|
|
306
|
+
const buf = assistantBuffers.get(sessionId);
|
|
307
|
+
if (!buf || !buf.text) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (buf.timer) {
|
|
311
|
+
clearTimeout(buf.timer);
|
|
312
|
+
buf.timer = null;
|
|
313
|
+
}
|
|
314
|
+
const { text } = buf;
|
|
315
|
+
buf.text = "";
|
|
316
|
+
try {
|
|
317
|
+
const sanitized = truncateUtf8Bytes(stripTaggedContent(text), MAX_OBSERVATION_BYTES);
|
|
318
|
+
if (sanitized) {
|
|
319
|
+
await WorkerClient.sendObservation(sessionId, "assistant_message", buf.messageId ? { messageId: buf.messageId } : {}, sanitized, projectRoot);
|
|
320
|
+
}
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
function scheduleAssistantFlush(sessionId) {
|
|
324
|
+
const buf = assistantBuffers.get(sessionId);
|
|
325
|
+
if (!buf) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (buf.timer) {
|
|
329
|
+
clearTimeout(buf.timer);
|
|
330
|
+
}
|
|
331
|
+
buf.timer = setTimeout(() => {
|
|
332
|
+
buf.timer = null;
|
|
333
|
+
flushAssistantBuffer(sessionId);
|
|
334
|
+
}, ASSISTANT_FLUSH_DEBOUNCE_MS);
|
|
335
|
+
}
|
|
336
|
+
function discardAssistantBuffer(sessionId) {
|
|
337
|
+
const buf = assistantBuffers.get(sessionId);
|
|
338
|
+
if (buf?.timer) {
|
|
339
|
+
clearTimeout(buf.timer);
|
|
340
|
+
}
|
|
341
|
+
assistantBuffers.delete(sessionId);
|
|
342
|
+
}
|
|
139
343
|
async function ensureSessionInit(sessionId, prompt) {
|
|
140
344
|
if (initializedSessions.has(sessionId)) {
|
|
141
345
|
return true;
|
|
@@ -153,56 +357,149 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
153
357
|
return false;
|
|
154
358
|
}
|
|
155
359
|
}
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
360
|
+
async function fetchAssistantMessageText(sessionId, messageId) {
|
|
361
|
+
try {
|
|
362
|
+
const result = await client.session.messages({ path: { id: sessionId } });
|
|
363
|
+
if (result.data && Array.isArray(result.data)) {
|
|
364
|
+
const target = result.data.find((m) => m?.info?.id === messageId);
|
|
365
|
+
if (target) {
|
|
366
|
+
return extractTextFromParts(target.parts);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
async function fetchLastMessages(sessionId) {
|
|
373
|
+
let user = "";
|
|
374
|
+
let assistant = "";
|
|
375
|
+
try {
|
|
376
|
+
const result = await client.session.messages({ path: { id: sessionId } });
|
|
377
|
+
if (result.data && Array.isArray(result.data)) {
|
|
378
|
+
const messages = result.data;
|
|
379
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
380
|
+
if (messages[i].info.role === "user") {
|
|
381
|
+
user = extractTextFromParts(messages[i].parts);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
for (let i = messages.length - 1;i >= 0; i--) {
|
|
386
|
+
if (messages[i].info.role === "assistant") {
|
|
387
|
+
assistant = extractTextFromParts(messages[i].parts);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch {}
|
|
393
|
+
return { user, assistant };
|
|
162
394
|
}
|
|
163
395
|
return {
|
|
164
396
|
event: async ({ event }) => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
397
|
+
switch (event.type) {
|
|
398
|
+
case "session.created": {
|
|
399
|
+
const sessionId = event.properties?.info?.id;
|
|
400
|
+
if (!sessionId) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
currentSessionId = sessionId;
|
|
404
|
+
contextCache = undefined;
|
|
405
|
+
await checkWorkerAndToast();
|
|
168
406
|
return;
|
|
169
407
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
408
|
+
case "message.updated": {
|
|
409
|
+
const info = event.properties?.info;
|
|
410
|
+
const sessionId = info?.sessionID;
|
|
411
|
+
const messageId = info?.id;
|
|
412
|
+
if (!sessionId || !messageId || info?.role !== "assistant") {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const isHealthy = await checkWorkerAndToast();
|
|
416
|
+
if (!isHealthy) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!await ensureSessionInit(sessionId)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const text = await fetchAssistantMessageText(sessionId, messageId);
|
|
423
|
+
if (!text) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
let buf = assistantBuffers.get(sessionId);
|
|
427
|
+
if (!buf) {
|
|
428
|
+
buf = { text: "", messageId: null, timer: null };
|
|
429
|
+
assistantBuffers.set(sessionId, buf);
|
|
430
|
+
}
|
|
431
|
+
buf.text = text;
|
|
432
|
+
buf.messageId = messageId;
|
|
433
|
+
scheduleAssistantFlush(sessionId);
|
|
177
434
|
return;
|
|
178
435
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
436
|
+
case "file.edited": {
|
|
437
|
+
const filePath = event.properties?.file;
|
|
438
|
+
const sessionId = currentSessionId;
|
|
439
|
+
if (!sessionId || !filePath) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const isHealthy = await checkWorkerAndToast();
|
|
443
|
+
if (!isHealthy) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!await ensureSessionInit(sessionId)) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
182
449
|
try {
|
|
183
|
-
|
|
184
|
-
path: { id: sessionId }
|
|
185
|
-
});
|
|
186
|
-
if (result.data && Array.isArray(result.data)) {
|
|
187
|
-
const messages = result.data;
|
|
188
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
189
|
-
if (messages[i].info.role === "user") {
|
|
190
|
-
lastUserMessage = extractTextFromParts(messages[i].parts);
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
195
|
-
if (messages[i].info.role === "assistant") {
|
|
196
|
-
lastAssistantMessage = extractTextFromParts(messages[i].parts);
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
450
|
+
await WorkerClient.sendObservation(sessionId, "file_edit", { path: filePath }, `File edited: ${filePath}`, projectRoot);
|
|
201
451
|
} catch {}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
case "session.compacted": {
|
|
455
|
+
const sessionId = event.properties?.sessionID || currentSessionId;
|
|
456
|
+
if (!sessionId) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
await flushAssistantBuffer(sessionId);
|
|
460
|
+
const isHealthy = await checkWorkerAndToast();
|
|
461
|
+
if (!isHealthy) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const { user, assistant } = await fetchLastMessages(sessionId);
|
|
466
|
+
await WorkerClient.summarize(sessionId, user, assistant);
|
|
467
|
+
} catch {}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
case "session.idle": {
|
|
471
|
+
const sessionId = event.properties?.sessionID || currentSessionId;
|
|
472
|
+
if (!sessionId) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
await flushAssistantBuffer(sessionId);
|
|
476
|
+
try {
|
|
477
|
+
const { user, assistant } = await fetchLastMessages(sessionId);
|
|
478
|
+
await WorkerClient.summarize(sessionId, user, assistant);
|
|
479
|
+
await WorkerClient.completeSession(sessionId);
|
|
480
|
+
await toast("Session summarized", "success", 2000);
|
|
481
|
+
} catch {}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
case "session.deleted": {
|
|
485
|
+
const sessionId = event.properties?.info?.id || currentSessionId;
|
|
486
|
+
if (!sessionId) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await flushAssistantBuffer(sessionId);
|
|
490
|
+
discardAssistantBuffer(sessionId);
|
|
491
|
+
initializedSessions.delete(sessionId);
|
|
492
|
+
if (currentSessionId === sessionId) {
|
|
493
|
+
currentSessionId = null;
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
await WorkerClient.completeSession(sessionId);
|
|
497
|
+
} catch {}
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
default: {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
206
503
|
}
|
|
207
504
|
},
|
|
208
505
|
"chat.message": async (input, output) => {
|
|
@@ -227,6 +524,25 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
227
524
|
output.system.push(`<claude-mem-context>
|
|
228
525
|
[Claude-Mem] Memory Active. Previous Context:
|
|
229
526
|
${context}
|
|
527
|
+
</claude-mem-context>`);
|
|
528
|
+
}
|
|
529
|
+
} catch {}
|
|
530
|
+
},
|
|
531
|
+
"experimental.session.compacting": async (input, output) => {
|
|
532
|
+
const sessionId = input.sessionID;
|
|
533
|
+
if (sessionId) {
|
|
534
|
+
await ensureSessionInit(sessionId);
|
|
535
|
+
}
|
|
536
|
+
const isHealthy = await checkWorkerAndToast();
|
|
537
|
+
if (!isHealthy) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const context = await getCachedContext();
|
|
542
|
+
if (context) {
|
|
543
|
+
output.context.push(`<claude-mem-context>
|
|
544
|
+
[Claude-Mem] Memory Active. Previous Context:
|
|
545
|
+
${context}
|
|
230
546
|
</claude-mem-context>`);
|
|
231
547
|
}
|
|
232
548
|
} catch {}
|
|
@@ -236,12 +552,14 @@ ${context}
|
|
|
236
552
|
if (!sessionId) {
|
|
237
553
|
return;
|
|
238
554
|
}
|
|
239
|
-
if (input.tool
|
|
555
|
+
if (shouldSkipObservationTool(input.tool)) {
|
|
240
556
|
return;
|
|
241
557
|
}
|
|
242
558
|
await ensureSessionInit(sessionId);
|
|
243
559
|
try {
|
|
244
|
-
|
|
560
|
+
const sanitizedToolInput = sanitizeObservationValue(input.args || {});
|
|
561
|
+
const sanitizedToolOutput = truncateUtf8Bytes(stripTaggedContent(normalizeToolOutput(output.output)), MAX_OBSERVATION_BYTES);
|
|
562
|
+
await WorkerClient.sendObservation(sessionId, input.tool, sanitizedToolInput, sanitizedToolOutput, projectRoot);
|
|
245
563
|
} catch {}
|
|
246
564
|
}
|
|
247
565
|
};
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -1,18 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
* Worker Client for Claude-Mem
|
|
3
|
-
* Handles communication with the local worker service running on port 37777.
|
|
4
|
-
*/
|
|
1
|
+
export type AutoStartResult = 'already-running' | 'started' | 'no-bun' | 'spawn-failed' | 'timeout' | 'skipped';
|
|
5
2
|
export declare class WorkerClient {
|
|
6
3
|
private static readonly PORT;
|
|
7
4
|
private static readonly BASE_URL;
|
|
5
|
+
/** Per-process guard so multiple OpenCode windows don't all spawn workers. */
|
|
6
|
+
private static autoStartPromise;
|
|
8
7
|
/**
|
|
9
8
|
* Check if the worker is healthy
|
|
10
9
|
*/
|
|
11
10
|
static isHealthy(): Promise<boolean>;
|
|
12
11
|
/**
|
|
13
|
-
* Ensure the worker is running.
|
|
12
|
+
* Ensure the worker is running. If healthy, returns true. Otherwise tries
|
|
13
|
+
* to spawn it via `bunx claude-mem start` in the background and resolves
|
|
14
|
+
* once the worker reports healthy or the timeout is reached.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent + serialised within the process via `autoStartPromise`.
|
|
14
17
|
*/
|
|
15
|
-
static ensureRunning(
|
|
18
|
+
static ensureRunning(): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Try to auto-start the Claude-Mem worker via `bunx claude-mem start`.
|
|
21
|
+
*
|
|
22
|
+
* Behaviour:
|
|
23
|
+
* - If the worker is already healthy, returns 'already-running' immediately.
|
|
24
|
+
* - If `bun` is not on PATH, returns 'no-bun' without attempting to spawn.
|
|
25
|
+
* - Otherwise spawns a detached `bunx claude-mem start` process and polls
|
|
26
|
+
* /api/health for ~8 seconds. Returns 'started' on success, 'timeout' on
|
|
27
|
+
* failure to come up, 'spawn-failed' if the spawn itself errored.
|
|
28
|
+
*
|
|
29
|
+
* Concurrent calls share the same in-flight promise.
|
|
30
|
+
*/
|
|
31
|
+
static tryAutoStart(): Promise<AutoStartResult>;
|
|
32
|
+
private static runAutoStart;
|
|
33
|
+
/**
|
|
34
|
+
* Locate the `bun` executable. Prefers Bun's own runtime detector when the
|
|
35
|
+
* plugin is loaded inside Bun (which OpenCode currently is), then falls back
|
|
36
|
+
* to PATH probing via `process.env.PATH`.
|
|
37
|
+
*/
|
|
38
|
+
private static findBun;
|
|
16
39
|
/**
|
|
17
40
|
* Initialize a session
|
|
18
41
|
*/
|