@ephemushroom/opencode-claude-mem 0.4.1 → 0.4.3
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 +244 -230
- package/dist/index.js +59 -11
- package/dist/tui.js +55 -12
- package/dist/worker-client.d.ts +15 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,9 +3,19 @@
|
|
|
3
3
|
Persistent memory for [OpenCode](https://opencode.ai), powered by
|
|
4
4
|
[Claude-Mem](https://github.com/thedotmack/claude-mem).
|
|
5
5
|
|
|
6
|
-
Share the same Claude-Mem worker, database, and
|
|
7
|
-
|
|
8
|
-
into new OpenCode
|
|
6
|
+
Share the same Claude-Mem worker, database, and memory across your coding
|
|
7
|
+
agents: memories written by Claude Code are visible to OpenCode, and vice
|
|
8
|
+
versa. Previous observations and summaries are injected into new OpenCode
|
|
9
|
+
sessions automatically.
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
flowchart LR
|
|
13
|
+
CC[Claude Code] -->|writes as<br/>platform_source=claude| W
|
|
14
|
+
OC[OpenCode<br/>+ this plugin] -->|writes as<br/>platform_source=opencode| W
|
|
15
|
+
W[Claude-Mem Worker<br/>127.0.0.1:37777] --> DB[(SQLite +<br/>ChromaDB)]
|
|
16
|
+
W -->|shared project memory| CC
|
|
17
|
+
W -->|shared project memory| OC
|
|
18
|
+
```
|
|
9
19
|
|
|
10
20
|
> **Note:** This plugin is a thin OpenCode adapter for an existing Claude-Mem
|
|
11
21
|
> installation. It does **not** install Claude-Mem, manage slash commands, or
|
|
@@ -23,106 +33,117 @@ into new OpenCode sessions automatically.
|
|
|
23
33
|
```
|
|
24
34
|
|
|
25
35
|
3. Restart OpenCode.
|
|
26
|
-
4. Start a session — memory context
|
|
27
|
-
|
|
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
|
-
- **Native Memory Tools** — Exposes `mem-search`, `mem-timeline`, and
|
|
43
|
-
`mem-get-observations` OpenCode tools backed by the Claude-Mem worker API,
|
|
44
|
-
covering the full search → timeline → details workflow without the Claude
|
|
45
|
-
Code compatibility bridge or an MCP server.
|
|
46
|
-
- **Sidebar Status** — Registers a TUI sidebar panel ("Memory") showing worker
|
|
47
|
-
health, project, observation/summary/session counts, and processing queue
|
|
48
|
-
depth, refreshed every few seconds.
|
|
49
|
-
- **Observation Hardening** — Skips low-value meta tools, strips
|
|
50
|
-
`<claude-mem-context>` and `<private>` tags before storage, and truncates
|
|
51
|
-
oversized observation payloads by UTF-8 byte size.
|
|
52
|
-
- **Graceful Degradation** — If the worker is offline and cannot be started,
|
|
53
|
-
the plugin fails open and OpenCode continues to work normally.
|
|
36
|
+
4. Start a session — memory context is injected automatically, the `mem-*`
|
|
37
|
+
tools become available, and a collapsible **Memory** section appears in the
|
|
38
|
+
sidebar.
|
|
54
39
|
|
|
55
|
-
|
|
40
|
+
Using [oh-my-openagent](https://github.com/code-yeongyu/oh-my-openagent)?
|
|
41
|
+
See [Disable the Claude Code bridge](#using-with-oh-my-openagent-disable-the-claude-code-bridge)
|
|
42
|
+
to avoid running two claude-mem integrations at once.
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
OpenCode session
|
|
59
|
-
|
|
|
60
|
-
|-- plugin loads
|
|
61
|
-
| '-- connect to Claude-Mem worker from env/settings/default endpoint
|
|
62
|
-
|
|
|
63
|
-
|-- tool(mem-search) ............... search memory via worker API
|
|
64
|
-
|-- session.created ................ track session + reset cache
|
|
65
|
-
|-- chat.message ................... init session with real user prompt
|
|
66
|
-
|-- tool.execute.after ............. send tool observation
|
|
67
|
-
|-- system.transform ............... inject memory into system prompt
|
|
68
|
-
|-- session.compacting ............. preserve memory during compaction
|
|
69
|
-
|-- message.updated ................ capture assistant text (debounced)
|
|
70
|
-
|-- file.edited .................... record file edit observation
|
|
71
|
-
|-- session.compacted .............. summarize after compaction
|
|
72
|
-
|-- session.idle ................... flush + summarize
|
|
73
|
-
'-- session.deleted ................ flush + complete (no zombie sessions)
|
|
74
|
-
```
|
|
44
|
+
## What You Get
|
|
75
45
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
46
|
+
| Surface | What it looks like |
|
|
47
|
+
|---|---|
|
|
48
|
+
| **System prompt** | `<claude-mem-context>` block with recent observations + session summaries for the current project |
|
|
49
|
+
| **Tools** | `mem-search` → `mem-timeline` → `mem-get-observations` (full search workflow, no MCP server needed) |
|
|
50
|
+
| **Sidebar** | `▶ Memory (online, 11.7k obs)` — click to expand recent sessions and latest observations |
|
|
51
|
+
| **Background** | Every tool call, assistant message, and file edit captured as observations; sessions summarized on idle |
|
|
79
52
|
|
|
80
|
-
|
|
53
|
+
### Sidebar
|
|
81
54
|
|
|
82
|
-
|
|
83
|
-
|
|
55
|
+
The plugin registers a sidebar section styled after OpenCode's native
|
|
56
|
+
MCP/Context sections — borderless, click-to-toggle:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
▶ Memory (online, 11.7k obs) ← collapsed (default), single line
|
|
84
60
|
```
|
|
85
61
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
```text
|
|
63
|
+
▼ Memory ← click header to expand
|
|
64
|
+
• obs 11.7k · sum 2311 · ses 1186
|
|
65
|
+
Recent sessions
|
|
66
|
+
• 实现 tui.json 自动注册(self-heal)机制…
|
|
67
|
+
• 设计 Memory 面板:跨工具记忆共享…
|
|
68
|
+
Latest
|
|
69
|
+
◆ Memory 侧边栏完整通过 CI 流水线…
|
|
70
|
+
● 修复点击不展开:改用 solid-js signal…
|
|
71
|
+
⚖ 计划按原生风格重构 Memory 面板…
|
|
72
|
+
```
|
|
89
73
|
|
|
90
|
-
|
|
74
|
+
- Summary line turns **yellow** while the worker is processing (`(queue N)`)
|
|
75
|
+
and **red** when offline (`(offline)`).
|
|
76
|
+
- Observation icons match the injected context legend: ◆ feature · ● bugfix ·
|
|
77
|
+
⚖ decision · ○ discovery · ↻ refactor · ✓ change · ⚠/⚷ security.
|
|
78
|
+
- Recent items are only fetched while expanded, keeping the collapsed poll
|
|
79
|
+
loop cheap (stats every 5s).
|
|
80
|
+
- Fails open: worker offline → shows the offline state, never blocks the TUI.
|
|
81
|
+
|
|
82
|
+
The sidebar loads via the package's `./tui` export. On startup the plugin
|
|
83
|
+
**self-heals `~/.config/opencode/tui.json`**: if it is registered as a server
|
|
84
|
+
plugin in `opencode.json` but missing from the TUI plugin list, it appends
|
|
85
|
+
itself — no manual configuration. Symlinked `tui.json` files are written
|
|
86
|
+
through, preserving dotfiles setups.
|
|
87
|
+
|
|
88
|
+
### Memory Tools
|
|
89
|
+
|
|
90
|
+
Three native OpenCode tools cover the same 3-step workflow as the upstream
|
|
91
|
+
Claude-Mem MCP server — no MCP server or stdio subprocess required:
|
|
92
|
+
|
|
93
|
+
```mermaid
|
|
94
|
+
flowchart LR
|
|
95
|
+
A["mem-search<br/><i>find IDs by query</i>"] --> B["mem-timeline<br/><i>context around an ID</i>"]
|
|
96
|
+
B --> C["mem-get-observations<br/><i>full details for IDs</i>"]
|
|
97
|
+
```
|
|
91
98
|
|
|
92
|
-
|
|
|
99
|
+
| Tool | Worker endpoint | Use it for |
|
|
93
100
|
|---|---|---|
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `PostToolUse` | `tool.execute.after` | Capture tool observations |
|
|
98
|
-
| Claude-Mem MCP `search` | `tool` (`mem-search`) | Search memory from OpenCode |
|
|
99
|
-
| Claude-Mem MCP `timeline` | `tool` (`mem-timeline`) | Chronological context around an observation |
|
|
100
|
-
| Claude-Mem MCP `get_observations` | `tool` (`mem-get-observations`) | Fetch full observation details by ID |
|
|
101
|
-
| _(streaming)_ | `event` (`message.updated`) | Capture assistant text (debounced) |
|
|
102
|
-
| _(streaming)_ | `event` (`file.edited`) | Record file edit observations |
|
|
103
|
-
| _(compaction)_ | `event` (`session.compacted`) | Summarize after OpenCode compacts |
|
|
104
|
-
| `Stop` | `event` (`session.idle`) | Flush + summarize |
|
|
105
|
-
| `SessionEnd` | `event` (`session.deleted`) | Flush + complete (no zombie active rows) |
|
|
101
|
+
| `mem-search` | `GET /api/search` | Formatted index with query, project, source, type, date, pagination, and ordering filters |
|
|
102
|
+
| `mem-timeline` | `GET /api/timeline` | Chronological records around an `anchor` ID (or auto-located via `query`) |
|
|
103
|
+
| `mem-get-observations` | `POST /api/observations/batch` | Full details for IDs — e.g. the IDs shown in the injected context |
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
## How It Works
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
107
|
+
```mermaid
|
|
108
|
+
sequenceDiagram
|
|
109
|
+
participant OC as OpenCode
|
|
110
|
+
participant P as Plugin
|
|
111
|
+
participant W as Claude-Mem Worker
|
|
112
|
+
|
|
113
|
+
OC->>P: plugin loads
|
|
114
|
+
P->>P: self-heal tui.json
|
|
115
|
+
P->>W: health check (auto-start via bunx if down)
|
|
116
|
+
OC->>P: chat.message (first user prompt)
|
|
117
|
+
P->>W: POST /api/sessions/init
|
|
118
|
+
OC->>P: system.transform
|
|
119
|
+
W-->>P: GET /api/context/inject (cached per session)
|
|
120
|
+
P-->>OC: inject <claude-mem-context> into system prompt
|
|
121
|
+
loop during the session
|
|
122
|
+
OC->>P: tool.execute.after / message.updated / file.edited
|
|
123
|
+
P->>W: POST /api/sessions/observations
|
|
124
|
+
end
|
|
125
|
+
OC->>P: session.idle / session.compacted
|
|
126
|
+
P->>W: POST /api/sessions/summarize
|
|
127
|
+
OC->>P: session.deleted
|
|
128
|
+
P->>W: POST /api/sessions/complete
|
|
129
|
+
```
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
Claude-Mem
|
|
125
|
-
|
|
131
|
+
The plugin is intentionally small: it only adapts OpenCode hook events to the
|
|
132
|
+
Claude-Mem worker HTTP API. All indexing, summarization, memory search, and
|
|
133
|
+
storage stay in upstream Claude-Mem.
|
|
134
|
+
|
|
135
|
+
### Cross-Tool Memory Sharing
|
|
136
|
+
|
|
137
|
+
Writes are attributed (`platformSource: "opencode"`), reads are shared:
|
|
138
|
+
|
|
139
|
+
| Operation | Behavior |
|
|
140
|
+
|---|---|
|
|
141
|
+
| OpenCode writes | Stored as `platform_source=opencode` |
|
|
142
|
+
| Claude Code writes | Stored as `platform_source=claude` |
|
|
143
|
+
| Either reads (inject/search) | Sees **all** memory for the project, regardless of source |
|
|
144
|
+
|
|
145
|
+
The sharing key is the **project name** (worktree directory name) — work in
|
|
146
|
+
the same project directory and memory flows both ways.
|
|
126
147
|
|
|
127
148
|
## Installation
|
|
128
149
|
|
|
@@ -131,8 +152,7 @@ while context injection and `mem-search` still search the shared project memory.
|
|
|
131
152
|
- [Claude Code](https://claude.com/claude-code) with
|
|
132
153
|
[Claude-Mem](https://github.com/thedotmack/claude-mem) installed
|
|
133
154
|
- [OpenCode](https://opencode.ai) with plugin support
|
|
134
|
-
- A running Claude-Mem worker (default `127.0.0.1:37777
|
|
135
|
-
`CLAUDE_MEM_WORKER_HOST` / `CLAUDE_MEM_WORKER_PORT` settings)
|
|
155
|
+
- A running Claude-Mem worker (default `127.0.0.1:37777`)
|
|
136
156
|
|
|
137
157
|
### Step 1: Install Claude-Mem
|
|
138
158
|
|
|
@@ -163,124 +183,115 @@ Then restart OpenCode.
|
|
|
163
183
|
curl -s http://127.0.0.1:37777/api/health
|
|
164
184
|
```
|
|
165
185
|
|
|
166
|
-
If the worker is healthy, OpenCode
|
|
167
|
-
`Memory active · <project>` when a session starts
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
186
|
+
If the worker is healthy, OpenCode shows a toast like
|
|
187
|
+
`Memory active · <project>` when a session starts, and the sidebar shows
|
|
188
|
+
`▶ Memory (online, … obs)`.
|
|
189
|
+
|
|
190
|
+
## Using with oh-my-openagent: Disable the Claude Code Bridge
|
|
191
|
+
|
|
192
|
+
oh-my-openagent ships a Claude Code compatibility layer that can load Claude
|
|
193
|
+
Code plugins — including `claude-mem@thedotmack` — inside OpenCode. Running
|
|
194
|
+
that bridge **and** this native plugin at the same time means two integrations
|
|
195
|
+
write to the same worker:
|
|
196
|
+
|
|
197
|
+
- duplicate observations for every tool call
|
|
198
|
+
- duplicate context injection and toasts
|
|
199
|
+
- bridged MCP tools (`search`, `timeline`, `get_observations`) shadowing the
|
|
200
|
+
native `mem-*` tools
|
|
201
|
+
|
|
202
|
+
Disable the bridge for claude-mem in `~/.config/opencode/oh-my-openagent.jsonc`:
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
{
|
|
206
|
+
"claude_code": {
|
|
207
|
+
"plugins_override": {
|
|
208
|
+
"claude-mem@thedotmack": false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This only disables the *bridged* claude-mem inside OpenCode. Claude Code
|
|
215
|
+
itself keeps using claude-mem normally, and memory stays shared through the
|
|
216
|
+
worker. Everything the bridge provided is covered natively by this plugin:
|
|
217
|
+
|
|
218
|
+
| Bridged (before) | Native (this plugin) |
|
|
219
|
+
|---|---|
|
|
220
|
+
| MCP `search` | `mem-search` tool |
|
|
221
|
+
| MCP `timeline` | `mem-timeline` tool |
|
|
222
|
+
| MCP `get_observations` | `mem-get-observations` tool |
|
|
223
|
+
| `SessionStart` context hook | `system.transform` injection |
|
|
224
|
+
| `PostToolUse` observation hook | `tool.execute.after` capture |
|
|
225
|
+
|
|
226
|
+
## Reference
|
|
227
|
+
|
|
228
|
+
### Hook Mapping
|
|
229
|
+
|
|
230
|
+
| Claude Code | OpenCode plugin | Purpose |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `SessionStart` | `experimental.chat.system.transform` | Inject memory context |
|
|
233
|
+
| `SessionStart` | `experimental.session.compacting` | Preserve memory during compaction |
|
|
234
|
+
| `UserPromptSubmit` | `chat.message` | Initialize session with real user prompt |
|
|
235
|
+
| `PostToolUse` | `tool.execute.after` | Capture tool observations |
|
|
236
|
+
| Claude-Mem MCP `search` | `tool` (`mem-search`) | Search memory from OpenCode |
|
|
237
|
+
| Claude-Mem MCP `timeline` | `tool` (`mem-timeline`) | Chronological context around an observation |
|
|
238
|
+
| Claude-Mem MCP `get_observations` | `tool` (`mem-get-observations`) | Fetch full observation details by ID |
|
|
239
|
+
| _(streaming)_ | `event` (`message.updated`) | Capture assistant text (debounced 250ms) |
|
|
240
|
+
| _(streaming)_ | `event` (`file.edited`) | Record file edit observations |
|
|
241
|
+
| _(compaction)_ | `event` (`session.compacted`) | Summarize after OpenCode compacts |
|
|
242
|
+
| `Stop` | `event` (`session.idle`) | Flush + summarize |
|
|
243
|
+
| `SessionEnd` | `event` (`session.deleted`) | Flush + complete (no zombie active rows) |
|
|
244
|
+
|
|
245
|
+
### Worker API Endpoints Used
|
|
246
|
+
|
|
247
|
+
| Method | Endpoint | Purpose |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| `GET` | `/api/health` | Health check |
|
|
250
|
+
| `GET` | `/api/context/inject?project={name}` | Fetch formatted memory context |
|
|
251
|
+
| `POST` | `/api/sessions/init` | Initialize session |
|
|
252
|
+
| `POST` | `/api/sessions/observations` | Store tool observation |
|
|
253
|
+
| `POST` | `/api/sessions/summarize` | Trigger summarization |
|
|
254
|
+
| `POST` | `/api/sessions/complete` | Complete session |
|
|
255
|
+
| `GET` | `/api/search?query=...&project=...&dateStart=...&dateEnd=...` | `mem-search`; also supports `limit`, `platformSource`, `type`, `obs_type`, `offset`, and `orderBy` |
|
|
256
|
+
| `GET` | `/api/timeline?project={name}&anchor={id}` | `mem-timeline` |
|
|
257
|
+
| `POST` | `/api/observations/batch` | `mem-get-observations` |
|
|
258
|
+
| `GET` | `/api/stats` + `/api/processing-status` | Sidebar status |
|
|
259
|
+
| `GET` | `/api/summaries` + `/api/observations` | Sidebar recent items (expanded only) |
|
|
260
|
+
|
|
261
|
+
The worker endpoint is resolved in this order: `CLAUDE_MEM_WORKER_HOST` /
|
|
262
|
+
`CLAUDE_MEM_WORKER_PORT` environment variables → `~/.claude-mem/settings.json`
|
|
263
|
+
→ `127.0.0.1:37777`.
|
|
264
|
+
|
|
265
|
+
### Key Implementation Details
|
|
243
266
|
|
|
244
267
|
- **Thin client architecture** — `src/index.ts` handles OpenCode hooks and
|
|
245
|
-
session state; `src/worker-client.ts` is a static HTTP client.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
268
|
+
session state; `src/worker-client.ts` is a static HTTP client; `src/tui.ts`
|
|
269
|
+
renders the sidebar; `src/tui-registration.ts` self-heals `tui.json`.
|
|
270
|
+
- **Zero runtime dependencies** — the OpenCode plugin SDK is bundled into
|
|
271
|
+
`dist/`, keeping the install footprint under ~500 KB.
|
|
272
|
+
- **Reactive sidebar** — collapse state and view data are solid-js signals
|
|
273
|
+
(shared with OpenCode's own solid instance via `--external solid-js`), so
|
|
274
|
+
clicking the header re-renders reliably; falls back to plain closures if
|
|
275
|
+
solid-js cannot be resolved.
|
|
276
|
+
- **No console logging** — `console.*` output corrupts the OpenCode TUI;
|
|
277
|
+
the plugin never logs and never throws from hooks.
|
|
278
|
+
- **Deferred toast** — health toasts only happen after hook execution begins,
|
|
249
279
|
avoiding startup crashes caused by early TUI access.
|
|
250
|
-
- **
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
- **Worker endpoint resolution** — The plugin reads `CLAUDE_MEM_WORKER_HOST` /
|
|
255
|
-
`CLAUDE_MEM_WORKER_PORT`, then `~/.claude-mem/settings.json`, then defaults to
|
|
256
|
-
`127.0.0.1:37777`.
|
|
257
|
-
- **Real summarize payloads** — On `session.idle`, the plugin reads the latest
|
|
258
|
-
session messages and sends the last user and assistant messages for summary.
|
|
259
|
-
- **Context caching** — Memory context is fetched once per session and reused
|
|
280
|
+
- **Auto-start** — if the worker is down on load, spawns
|
|
281
|
+
`bunx claude-mem start` once per OpenCode process (skipped if `bun` is not
|
|
282
|
+
on `PATH`).
|
|
283
|
+
- **Context caching** — memory context is fetched once per session and reused
|
|
260
284
|
across prompt injection and compaction.
|
|
261
|
-
- **Circular memory protection** —
|
|
262
|
-
`<claude-mem-context>` tags, Claude-Mem
|
|
263
|
-
memory
|
|
264
|
-
- **
|
|
265
|
-
|
|
266
|
-
- **Field name correctness** —
|
|
267
|
-
`claudeSessionId
|
|
268
|
-
- **
|
|
269
|
-
`
|
|
270
|
-
`
|
|
271
|
-
to inject extra LLM context before the tool call.
|
|
272
|
-
|
|
273
|
-
## Differences from `bloodf/opencode-mem`
|
|
274
|
-
|
|
275
|
-
This project intentionally stays smaller in scope.
|
|
276
|
-
|
|
277
|
-
- **This plugin does**: bridge OpenCode hooks to an already-installed
|
|
278
|
-
Claude-Mem worker.
|
|
279
|
-
- **This plugin does not**: auto-install Claude-Mem, auto-edit OpenCode config,
|
|
280
|
-
auto-copy skills, auto-register slash commands, or auto-start the worker.
|
|
281
|
-
|
|
282
|
-
That keeps the runtime behavior predictable and leaves worker ownership with the
|
|
283
|
-
upstream Claude-Mem installation.
|
|
285
|
+
- **Circular memory protection** — injected context is wrapped in
|
|
286
|
+
`<claude-mem-context>` tags, Claude-Mem search tools are skipped from
|
|
287
|
+
observation capture, and memory tags are stripped before storage.
|
|
288
|
+
- **Observation hardening** — low-value meta tools skipped; oversized payloads
|
|
289
|
+
truncated by UTF-8 byte size (24 KB cap).
|
|
290
|
+
- **Field name correctness** — worker payloads use `contentSessionId`, not
|
|
291
|
+
`claudeSessionId` (the wrong name fails silently).
|
|
292
|
+
- **Session lifecycle hygiene** — `session.deleted` triggers
|
|
293
|
+
`completeSession`, preventing zombie `active` rows from accumulating stale
|
|
294
|
+
`pending_messages`.
|
|
284
295
|
|
|
285
296
|
## Troubleshooting
|
|
286
297
|
|
|
@@ -297,49 +308,52 @@ curl -s http://127.0.0.1:37777/api/health
|
|
|
297
308
|
|
|
298
309
|
### OpenCode shows `Worker offline`
|
|
299
310
|
|
|
300
|
-
The plugin
|
|
301
|
-
plugin load. If
|
|
311
|
+
The plugin tries to launch the worker via `bunx claude-mem start` once on
|
|
312
|
+
plugin load. If the toast still appears:
|
|
302
313
|
|
|
303
314
|
- Confirm `bun` is on your `PATH` — `bun --version` should print a version.
|
|
304
|
-
- Confirm `claude-mem` is installed for `bunx
|
|
315
|
+
- Confirm `claude-mem` is installed for `bunx` — run
|
|
305
316
|
`bunx claude-mem --version` once to populate the cache.
|
|
306
317
|
- On Windows after a forced kill, port `37777` may stay in `TIME_WAIT` for
|
|
307
318
|
30-120 seconds; wait it out or restart Claude Code.
|
|
308
319
|
- Restart Claude Code to bring Claude-Mem back up via its own supervisor.
|
|
309
|
-
- Verify the worker port is still `37777`.
|
|
310
320
|
|
|
311
|
-
###
|
|
321
|
+
### Sidebar Memory section is missing
|
|
312
322
|
|
|
313
|
-
-
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
- Check `~/.config/opencode/tui.json` contains this plugin in its `plugin`
|
|
324
|
+
array — the plugin self-heals this file on load, so restarting OpenCode
|
|
325
|
+
twice (once to heal, once to load) fixes a missing entry.
|
|
326
|
+
- The sidebar requires OpenCode's `@opentui/solid` runtime; if unavailable the
|
|
327
|
+
section is skipped silently while hooks and tools keep working.
|
|
316
328
|
|
|
317
|
-
###
|
|
329
|
+
### Clicking the Memory header does nothing
|
|
330
|
+
|
|
331
|
+
- Upgrade to ≥ 0.4.2 — earlier versions used non-reactive state and the
|
|
332
|
+
toggle never re-rendered.
|
|
318
333
|
|
|
319
|
-
|
|
320
|
-
- Be aware that low-value meta tools and Claude-Mem search tools are skipped by
|
|
321
|
-
design.
|
|
322
|
-
- Very large tool outputs are truncated before storage.
|
|
334
|
+
### Duplicate observations / duplicate toasts
|
|
323
335
|
|
|
324
|
-
|
|
336
|
+
- You are likely running both this plugin and a Claude Code compatibility
|
|
337
|
+
bridge for claude-mem. See
|
|
338
|
+
[Disable the Claude Code bridge](#using-with-oh-my-openagent-disable-the-claude-code-bridge).
|
|
339
|
+
|
|
340
|
+
### Observations are missing or incomplete
|
|
325
341
|
|
|
326
|
-
-
|
|
327
|
-
|
|
328
|
-
natively, so a separate MCP server is not required.
|
|
329
|
-
- If the tools return `worker is offline`, confirm the worker health endpoint
|
|
330
|
-
responds (see above).
|
|
342
|
+
- Low-value meta tools and Claude-Mem search tools are skipped by design.
|
|
343
|
+
- Very large tool outputs are truncated before storage (24 KB).
|
|
331
344
|
|
|
332
345
|
## Development
|
|
333
346
|
|
|
334
347
|
```bash
|
|
335
348
|
bun install
|
|
336
|
-
bun run build
|
|
337
|
-
bun
|
|
338
|
-
bun run
|
|
349
|
+
bun run build # bundle dist/index.js + dist/tui.js + emit declarations
|
|
350
|
+
bun test # bun's built-in runner
|
|
351
|
+
bun run lint # oxlint
|
|
352
|
+
bun run fmt:check # oxfmt
|
|
339
353
|
```
|
|
340
354
|
|
|
341
|
-
If you edit source code locally, rebuild and restart OpenCode to pick up the
|
|
342
|
-
plugin bundle.
|
|
355
|
+
If you edit source code locally, rebuild and restart OpenCode to pick up the
|
|
356
|
+
new plugin bundle.
|
|
343
357
|
|
|
344
358
|
## License
|
|
345
359
|
|
package/dist/index.js
CHANGED
|
@@ -12381,6 +12381,27 @@ function parseWorkerEndpoint(env, rawSettings) {
|
|
|
12381
12381
|
const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
|
|
12382
12382
|
return { host, port };
|
|
12383
12383
|
}
|
|
12384
|
+
function buildSearchParams(options) {
|
|
12385
|
+
const params = new URLSearchParams;
|
|
12386
|
+
const entries = [
|
|
12387
|
+
["query", options.query ?? ""],
|
|
12388
|
+
["limit", options.limit],
|
|
12389
|
+
["project", options.project],
|
|
12390
|
+
["platformSource", options.platformSource],
|
|
12391
|
+
["type", options.type],
|
|
12392
|
+
["obs_type", options.obs_type],
|
|
12393
|
+
["dateStart", options.dateStart],
|
|
12394
|
+
["dateEnd", options.dateEnd],
|
|
12395
|
+
["offset", options.offset],
|
|
12396
|
+
["orderBy", options.orderBy]
|
|
12397
|
+
];
|
|
12398
|
+
for (const [key, value] of entries) {
|
|
12399
|
+
if (value !== undefined) {
|
|
12400
|
+
params.set(key, String(value));
|
|
12401
|
+
}
|
|
12402
|
+
}
|
|
12403
|
+
return params;
|
|
12404
|
+
}
|
|
12384
12405
|
function readSettingsRaw() {
|
|
12385
12406
|
try {
|
|
12386
12407
|
return readFileSync(SETTINGS_FILE, "utf8");
|
|
@@ -12692,12 +12713,9 @@ class WorkerClient {
|
|
|
12692
12713
|
return [];
|
|
12693
12714
|
}
|
|
12694
12715
|
}
|
|
12695
|
-
static async search(
|
|
12716
|
+
static async search(options) {
|
|
12696
12717
|
try {
|
|
12697
|
-
const params =
|
|
12698
|
-
if (limit !== undefined) {
|
|
12699
|
-
params.set("limit", String(limit));
|
|
12700
|
-
}
|
|
12718
|
+
const params = buildSearchParams(options);
|
|
12701
12719
|
const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
|
|
12702
12720
|
if (!response.ok) {
|
|
12703
12721
|
return "";
|
|
@@ -13018,18 +13036,48 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
13018
13036
|
return {
|
|
13019
13037
|
tool: {
|
|
13020
13038
|
"mem-search": tool({
|
|
13021
|
-
description: "Search Claude-Mem persistent memory
|
|
13039
|
+
description: "Search Claude-Mem persistent memory. Supports query, project, platformSource, type, obs_type, dateStart, dateEnd, offset, and orderBy filters.",
|
|
13022
13040
|
args: {
|
|
13023
|
-
query: tool.schema.string().min(1).describe("
|
|
13024
|
-
limit: tool.schema.number().int().positive().max(
|
|
13041
|
+
query: tool.schema.string().min(1).optional().describe("Optional semantic search query for Claude-Mem memory"),
|
|
13042
|
+
limit: tool.schema.number().int().positive().max(100).optional().describe("Maximum number of search results (default 20)"),
|
|
13043
|
+
project: tool.schema.string().min(1).optional().describe("Filter by project name"),
|
|
13044
|
+
platformSource: tool.schema.string().min(1).optional().describe("Filter by platform source, such as claude or opencode"),
|
|
13045
|
+
type: tool.schema.string().min(1).optional().describe("Filter by result type"),
|
|
13046
|
+
obs_type: tool.schema.string().min(1).optional().describe("Filter by observation type, such as feature or bugfix"),
|
|
13047
|
+
dateStart: tool.schema.string().min(1).optional().describe("Start date filter in ISO 8601 or YYYY-MM-DD format"),
|
|
13048
|
+
dateEnd: tool.schema.string().min(1).optional().describe("End date filter in ISO 8601 or YYYY-MM-DD format"),
|
|
13049
|
+
offset: tool.schema.number().int().min(0).optional().describe("Pagination offset"),
|
|
13050
|
+
orderBy: tool.schema.enum(["date_desc", "date_asc", "relevance"]).optional().describe("Sort order")
|
|
13025
13051
|
},
|
|
13026
|
-
execute: async ({
|
|
13052
|
+
execute: async ({
|
|
13053
|
+
query,
|
|
13054
|
+
limit,
|
|
13055
|
+
project: projectFilter,
|
|
13056
|
+
platformSource,
|
|
13057
|
+
type,
|
|
13058
|
+
obs_type,
|
|
13059
|
+
dateStart,
|
|
13060
|
+
dateEnd,
|
|
13061
|
+
offset,
|
|
13062
|
+
orderBy
|
|
13063
|
+
}) => {
|
|
13027
13064
|
const isHealthy = await checkWorkerAndToast();
|
|
13028
13065
|
if (!isHealthy) {
|
|
13029
13066
|
return "Claude-Mem worker is offline. Start Claude-Mem and retry the search.";
|
|
13030
13067
|
}
|
|
13031
|
-
const result = await WorkerClient.search(
|
|
13032
|
-
|
|
13068
|
+
const result = await WorkerClient.search({
|
|
13069
|
+
query,
|
|
13070
|
+
limit,
|
|
13071
|
+
project: projectFilter ?? projectName,
|
|
13072
|
+
platformSource,
|
|
13073
|
+
type,
|
|
13074
|
+
obs_type,
|
|
13075
|
+
dateStart,
|
|
13076
|
+
dateEnd,
|
|
13077
|
+
offset,
|
|
13078
|
+
orderBy
|
|
13079
|
+
});
|
|
13080
|
+
return result || `No Claude-Mem results found${query ? ` for "${query}"` : ""}.`;
|
|
13033
13081
|
}
|
|
13034
13082
|
}),
|
|
13035
13083
|
"mem-timeline": tool({
|
package/dist/tui.js
CHANGED
|
@@ -48,6 +48,27 @@ function parseWorkerEndpoint(env, rawSettings) {
|
|
|
48
48
|
const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
|
|
49
49
|
return { host, port };
|
|
50
50
|
}
|
|
51
|
+
function buildSearchParams(options) {
|
|
52
|
+
const params = new URLSearchParams;
|
|
53
|
+
const entries = [
|
|
54
|
+
["query", options.query ?? ""],
|
|
55
|
+
["limit", options.limit],
|
|
56
|
+
["project", options.project],
|
|
57
|
+
["platformSource", options.platformSource],
|
|
58
|
+
["type", options.type],
|
|
59
|
+
["obs_type", options.obs_type],
|
|
60
|
+
["dateStart", options.dateStart],
|
|
61
|
+
["dateEnd", options.dateEnd],
|
|
62
|
+
["offset", options.offset],
|
|
63
|
+
["orderBy", options.orderBy]
|
|
64
|
+
];
|
|
65
|
+
for (const [key, value] of entries) {
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
params.set(key, String(value));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return params;
|
|
71
|
+
}
|
|
51
72
|
function readSettingsRaw() {
|
|
52
73
|
try {
|
|
53
74
|
return readFileSync(SETTINGS_FILE, "utf8");
|
|
@@ -359,12 +380,9 @@ class WorkerClient {
|
|
|
359
380
|
return [];
|
|
360
381
|
}
|
|
361
382
|
}
|
|
362
|
-
static async search(
|
|
383
|
+
static async search(options) {
|
|
363
384
|
try {
|
|
364
|
-
const params =
|
|
365
|
-
if (limit !== undefined) {
|
|
366
|
-
params.set("limit", String(limit));
|
|
367
|
-
}
|
|
385
|
+
const params = buildSearchParams(options);
|
|
368
386
|
const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
|
|
369
387
|
if (!response.ok) {
|
|
370
388
|
return "";
|
|
@@ -519,6 +537,17 @@ async function readMemView(project, collapsed) {
|
|
|
519
537
|
recentObservations
|
|
520
538
|
};
|
|
521
539
|
}
|
|
540
|
+
async function loadSolidCore() {
|
|
541
|
+
try {
|
|
542
|
+
const mod = await import("solid-js");
|
|
543
|
+
if (typeof mod === "object" && mod !== null && typeof mod.createSignal === "function") {
|
|
544
|
+
return mod;
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
547
|
+
} catch {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
522
551
|
function materializeNode(node, solid) {
|
|
523
552
|
const element = solid.createElement(node.kind);
|
|
524
553
|
for (const [name, value] of Object.entries(node.props)) {
|
|
@@ -550,31 +579,45 @@ var tuiModule = {
|
|
|
550
579
|
if (!solid) {
|
|
551
580
|
return;
|
|
552
581
|
}
|
|
582
|
+
const solidCore = await loadSolidCore();
|
|
583
|
+
const signal = (value) => {
|
|
584
|
+
if (solidCore) {
|
|
585
|
+
return solidCore.createSignal(value);
|
|
586
|
+
}
|
|
587
|
+
let current = value;
|
|
588
|
+
return [
|
|
589
|
+
() => current,
|
|
590
|
+
(next) => {
|
|
591
|
+
current = next;
|
|
592
|
+
}
|
|
593
|
+
];
|
|
594
|
+
};
|
|
553
595
|
const project = projectNameFromDirectory(String(api.state?.path?.directory ?? ""));
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
596
|
+
const initialView = await readMemView(project, true);
|
|
597
|
+
const [collapsed, setCollapsed] = signal(true);
|
|
598
|
+
const [view, setView] = signal(initialView);
|
|
599
|
+
let currentKey = viewKey(initialView);
|
|
557
600
|
let disposed = false;
|
|
558
601
|
let inFlight = false;
|
|
559
602
|
let timer = null;
|
|
560
603
|
const refresh = async () => {
|
|
561
|
-
const nextView = await readMemView(project, collapsed);
|
|
604
|
+
const nextView = await readMemView(project, collapsed());
|
|
562
605
|
const nextKey = viewKey(nextView);
|
|
563
606
|
if (nextKey !== currentKey) {
|
|
564
|
-
currentView = nextView;
|
|
565
607
|
currentKey = nextKey;
|
|
608
|
+
setView(nextView);
|
|
566
609
|
api.renderer.requestRender();
|
|
567
610
|
}
|
|
568
611
|
};
|
|
569
612
|
const onToggle = () => {
|
|
570
|
-
|
|
613
|
+
setCollapsed(!collapsed());
|
|
571
614
|
api.renderer.requestRender();
|
|
572
615
|
refresh().catch(() => {});
|
|
573
616
|
};
|
|
574
617
|
api.slots.register({
|
|
575
618
|
order: 910,
|
|
576
619
|
slots: {
|
|
577
|
-
sidebar_content: () => materialize(buildMemNodes(
|
|
620
|
+
sidebar_content: () => materialize(buildMemNodes(view(), api.theme.current, collapsed(), onToggle), solid)
|
|
578
621
|
}
|
|
579
622
|
});
|
|
580
623
|
api.renderer.requestRender();
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -3,7 +3,21 @@ export interface WorkerEndpoint {
|
|
|
3
3
|
readonly port: number;
|
|
4
4
|
}
|
|
5
5
|
export type AutoStartResult = 'already-running' | 'started' | 'no-bun' | 'spawn-failed' | 'timeout' | 'skipped';
|
|
6
|
+
export type SearchOrder = 'date_desc' | 'date_asc' | 'relevance';
|
|
7
|
+
export interface SearchOptions {
|
|
8
|
+
readonly query?: string;
|
|
9
|
+
readonly limit?: number;
|
|
10
|
+
readonly project?: string;
|
|
11
|
+
readonly platformSource?: string;
|
|
12
|
+
readonly type?: string;
|
|
13
|
+
readonly obs_type?: string;
|
|
14
|
+
readonly dateStart?: string;
|
|
15
|
+
readonly dateEnd?: string;
|
|
16
|
+
readonly offset?: number;
|
|
17
|
+
readonly orderBy?: SearchOrder;
|
|
18
|
+
}
|
|
6
19
|
export declare function parseWorkerEndpoint(env: Record<string, string | undefined>, rawSettings: string | null): WorkerEndpoint;
|
|
20
|
+
export declare function buildSearchParams(options: SearchOptions): URLSearchParams;
|
|
7
21
|
export declare class WorkerClient {
|
|
8
22
|
private static readonly BASE_URL;
|
|
9
23
|
/** Per-process guard so multiple OpenCode windows don't all spawn workers. */
|
|
@@ -110,5 +124,5 @@ export declare class WorkerClient {
|
|
|
110
124
|
type: string;
|
|
111
125
|
title: string;
|
|
112
126
|
}[]>;
|
|
113
|
-
static search(
|
|
127
|
+
static search(options: SearchOptions): Promise<string>;
|
|
114
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ephemushroom/opencode-claude-mem",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "OpenCode plugin for Claude-Mem persistent memory system",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
26
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid --external solid-js && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
27
27
|
"dev": "tsc --watch",
|
|
28
28
|
"clean": "rm -rf dist",
|
|
29
29
|
"lint": "oxlint",
|