@ephemushroom/opencode-claude-mem 0.2.5 → 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 +36 -8
- package/dist/index.d.ts +3 -1
- package/dist/index.js +256 -46
- package/dist/worker-client.d.ts +29 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,6 +30,9 @@ into new OpenCode sessions automatically.
|
|
|
30
30
|
|
|
31
31
|
- **Shared Memory** — Uses the same Claude-Mem worker and memory store as
|
|
32
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).
|
|
33
36
|
- **Automatic Context Injection** — Injects relevant project memory into the
|
|
34
37
|
system prompt for new OpenCode turns.
|
|
35
38
|
- **Compaction Support** — Re-injects memory context during OpenCode session
|
|
@@ -39,8 +42,8 @@ into new OpenCode sessions automatically.
|
|
|
39
42
|
- **Observation Hardening** — Skips low-value meta tools, strips
|
|
40
43
|
`<claude-mem-context>` and `<private>` tags before storage, and truncates
|
|
41
44
|
oversized observation payloads by UTF-8 byte size.
|
|
42
|
-
- **Graceful Degradation** — If the worker is offline
|
|
43
|
-
and OpenCode continues to work normally.
|
|
45
|
+
- **Graceful Degradation** — If the worker is offline and cannot be started,
|
|
46
|
+
the plugin fails open and OpenCode continues to work normally.
|
|
44
47
|
|
|
45
48
|
## How It Works
|
|
46
49
|
|
|
@@ -55,7 +58,11 @@ OpenCode session
|
|
|
55
58
|
|-- tool.execute.after ............. send tool observation
|
|
56
59
|
|-- system.transform ............... inject memory into system prompt
|
|
57
60
|
|-- session.compacting ............. preserve memory during compaction
|
|
58
|
-
|
|
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)
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
The plugin is intentionally small. It only adapts OpenCode hook events to the
|
|
@@ -76,8 +83,11 @@ OpenCode <-> Plugin (this repo) <-> Claude-Mem Worker (port 37777) <-> SQLite +
|
|
|
76
83
|
| `SessionStart` | `experimental.session.compacting` | Preserve memory during compaction |
|
|
77
84
|
| `UserPromptSubmit` | `chat.message` | Initialize session with real user prompt |
|
|
78
85
|
| `PostToolUse` | `tool.execute.after` | Capture tool observations |
|
|
79
|
-
|
|
|
80
|
-
|
|
|
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) |
|
|
81
91
|
|
|
82
92
|
### Worker API Endpoints Used
|
|
83
93
|
|
|
@@ -141,8 +151,18 @@ Once installed, the plugin works automatically:
|
|
|
141
151
|
OpenCode’s compaction path so memory survives conversation compression.
|
|
142
152
|
- **Tool observation capture** — Tool executions are stored as observations,
|
|
143
153
|
except for low-value/meta tools and Claude-Mem search tools.
|
|
144
|
-
- **
|
|
145
|
-
|
|
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).
|
|
146
166
|
|
|
147
167
|
## Memory Search
|
|
148
168
|
|
|
@@ -207,7 +227,15 @@ curl -s http://127.0.0.1:37777/api/health
|
|
|
207
227
|
|
|
208
228
|
### OpenCode shows `Worker offline`
|
|
209
229
|
|
|
210
|
-
|
|
230
|
+
The plugin will try to launch the worker via `bunx claude-mem start` once on
|
|
231
|
+
plugin load. If that toast still appears:
|
|
232
|
+
|
|
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.
|
|
211
239
|
- Verify the worker port is still `37777`.
|
|
212
240
|
|
|
213
241
|
### OpenCode crashes on startup
|
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 {
|
|
@@ -100,6 +170,7 @@ class WorkerClient {
|
|
|
100
170
|
// src/index.ts
|
|
101
171
|
var MAX_OBSERVATION_BYTES = 24 * 1024;
|
|
102
172
|
var MAX_TAG_REPLACEMENTS = 100;
|
|
173
|
+
var ASSISTANT_FLUSH_DEBOUNCE_MS = 250;
|
|
103
174
|
var META_TOOLS = new Set([
|
|
104
175
|
"askuserquestion",
|
|
105
176
|
"getmcpresource",
|
|
@@ -185,6 +256,13 @@ function normalizeToolOutput(output) {
|
|
|
185
256
|
return String(output);
|
|
186
257
|
}
|
|
187
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
|
+
}
|
|
188
266
|
var ClaudeMemPlugin = async (ctx) => {
|
|
189
267
|
const { project, directory, client } = ctx;
|
|
190
268
|
const projectRoot = directory || process.cwd();
|
|
@@ -209,20 +287,59 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
209
287
|
let initToastShown = false;
|
|
210
288
|
async function checkWorkerAndToast() {
|
|
211
289
|
if (workerHealthy === null) {
|
|
212
|
-
workerHealthy = await WorkerClient.
|
|
290
|
+
workerHealthy = await WorkerClient.ensureRunning();
|
|
213
291
|
}
|
|
214
292
|
if (!initToastShown) {
|
|
215
293
|
initToastShown = true;
|
|
216
294
|
if (workerHealthy) {
|
|
217
295
|
await toast(`Memory active \xB7 ${projectName}`, "success");
|
|
218
296
|
} else {
|
|
219
|
-
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);
|
|
220
298
|
}
|
|
221
299
|
}
|
|
222
300
|
return workerHealthy;
|
|
223
301
|
}
|
|
224
302
|
let currentSessionId = null;
|
|
225
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
|
+
}
|
|
226
343
|
async function ensureSessionInit(sessionId, prompt) {
|
|
227
344
|
if (initializedSessions.has(sessionId)) {
|
|
228
345
|
return true;
|
|
@@ -240,56 +357,149 @@ var ClaudeMemPlugin = async (ctx) => {
|
|
|
240
357
|
return false;
|
|
241
358
|
}
|
|
242
359
|
}
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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 };
|
|
249
394
|
}
|
|
250
395
|
return {
|
|
251
396
|
event: async ({ event }) => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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();
|
|
255
406
|
return;
|
|
256
407
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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);
|
|
264
434
|
return;
|
|
265
435
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
}
|
|
269
449
|
try {
|
|
270
|
-
|
|
271
|
-
path: { id: sessionId }
|
|
272
|
-
});
|
|
273
|
-
if (result.data && Array.isArray(result.data)) {
|
|
274
|
-
const messages = result.data;
|
|
275
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
276
|
-
if (messages[i].info.role === "user") {
|
|
277
|
-
lastUserMessage = extractTextFromParts(messages[i].parts);
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
for (let i = messages.length - 1;i >= 0; i--) {
|
|
282
|
-
if (messages[i].info.role === "assistant") {
|
|
283
|
-
lastAssistantMessage = extractTextFromParts(messages[i].parts);
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
450
|
+
await WorkerClient.sendObservation(sessionId, "file_edit", { path: filePath }, `File edited: ${filePath}`, projectRoot);
|
|
288
451
|
} catch {}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
503
|
}
|
|
294
504
|
},
|
|
295
505
|
"chat.message": async (input, output) => {
|
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
|
*/
|