@ephemushroom/opencode-claude-mem 0.4.3 → 0.5.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/dist/v2.js ADDED
@@ -0,0 +1,992 @@
1
+ // @bun
2
+ // node_modules/@opencode-ai/plugin-v2/dist/promise/plugin.js
3
+ function define(plugin) {
4
+ return plugin;
5
+ }
6
+
7
+ // src/shared.ts
8
+ var MAX_OBSERVATION_BYTES = 24 * 1024;
9
+ var MAX_TAG_REPLACEMENTS = 100;
10
+ var META_TOOLS = new Set([
11
+ "askuserquestion",
12
+ "getmcpresource",
13
+ "listmcpresourcestool",
14
+ "listmcptools",
15
+ "mem-get-observations",
16
+ "mem-search",
17
+ "mem-timeline",
18
+ "skill",
19
+ "slashcommand",
20
+ "todowrite"
21
+ ]);
22
+ var PRIVATE_TAG_REGEX = /<private>[\s\S]*?<\/private>/g;
23
+ var CONTEXT_TAG_REGEX = /<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g;
24
+ function stripTaggedContent(text) {
25
+ if (!text) {
26
+ return text;
27
+ }
28
+ let result = text;
29
+ let replacements = 0;
30
+ while (replacements < MAX_TAG_REPLACEMENTS && PRIVATE_TAG_REGEX.test(result)) {
31
+ PRIVATE_TAG_REGEX.lastIndex = 0;
32
+ result = result.replace(PRIVATE_TAG_REGEX, "");
33
+ replacements++;
34
+ }
35
+ PRIVATE_TAG_REGEX.lastIndex = 0;
36
+ while (replacements < MAX_TAG_REPLACEMENTS && CONTEXT_TAG_REGEX.test(result)) {
37
+ CONTEXT_TAG_REGEX.lastIndex = 0;
38
+ result = result.replace(CONTEXT_TAG_REGEX, "");
39
+ replacements++;
40
+ }
41
+ CONTEXT_TAG_REGEX.lastIndex = 0;
42
+ return result.trim();
43
+ }
44
+ function sanitizeObservationValue(value) {
45
+ if (typeof value === "string") {
46
+ return stripTaggedContent(value);
47
+ }
48
+ if (Array.isArray(value)) {
49
+ return value.map((item) => sanitizeObservationValue(item));
50
+ }
51
+ if (value && typeof value === "object") {
52
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeObservationValue(item)]));
53
+ }
54
+ return value;
55
+ }
56
+ function truncateUtf8Bytes(text, maxBytes) {
57
+ const encoder = new TextEncoder;
58
+ if (encoder.encode(text).length <= maxBytes) {
59
+ return text;
60
+ }
61
+ const suffix = `
62
+ [truncated]`;
63
+ const suffixBytes = encoder.encode(suffix).length;
64
+ const budget = Math.max(maxBytes - suffixBytes, 0);
65
+ let low = 0;
66
+ let high = text.length;
67
+ while (low < high) {
68
+ const mid = Math.ceil((low + high) / 2);
69
+ const candidate = text.slice(0, mid);
70
+ if (encoder.encode(candidate).length <= budget) {
71
+ low = mid;
72
+ } else {
73
+ high = mid - 1;
74
+ }
75
+ }
76
+ return `${text.slice(0, low)}${suffix}`;
77
+ }
78
+ function shouldSkipObservationTool(toolName) {
79
+ if (!toolName) {
80
+ return true;
81
+ }
82
+ const normalizedName = toolName.toLowerCase().replaceAll("_", "-");
83
+ if (normalizedName.includes("mcp-search") || /(?:^|[-.])mem-(?:search|timeline|get-observations)$/.test(normalizedName)) {
84
+ return true;
85
+ }
86
+ return META_TOOLS.has(normalizedName);
87
+ }
88
+ function normalizeToolOutput(output) {
89
+ if (typeof output === "string") {
90
+ return output;
91
+ }
92
+ if (output === undefined || output === null) {
93
+ return "";
94
+ }
95
+ try {
96
+ return JSON.stringify(output);
97
+ } catch {
98
+ return String(output);
99
+ }
100
+ }
101
+ function extractTextFromParts(parts) {
102
+ if (!parts || !Array.isArray(parts)) {
103
+ return "";
104
+ }
105
+ return parts.filter((p) => p && p.type === "text" && typeof p.text === "string" && !p.synthetic && !p.ignored).map((p) => p.text).join(`
106
+ `).trim();
107
+ }
108
+
109
+ // src/worker-client.ts
110
+ import { existsSync, readFileSync } from "fs";
111
+ import { homedir } from "os";
112
+ import { join } from "path";
113
+ import { spawn } from "child_process";
114
+ var HEALTH_TIMEOUT_MS = 1000;
115
+ var WRITE_TIMEOUT_MS = 1e4;
116
+ var AUTOSTART_POLL_INTERVAL_MS = 500;
117
+ var AUTOSTART_POLL_MAX_ATTEMPTS = 16;
118
+ var DEFAULT_WORKER_HOST = "127.0.0.1";
119
+ var DEFAULT_WORKER_PORT = 37777;
120
+ var PLATFORM_SOURCE = "opencode";
121
+ var SETTINGS_FILE = join(homedir(), ".claude-mem", "settings.json");
122
+ function isRecord(value) {
123
+ return typeof value === "object" && value !== null && !Array.isArray(value);
124
+ }
125
+ function readString(value) {
126
+ if (typeof value !== "string") {
127
+ return null;
128
+ }
129
+ const trimmed = value.trim();
130
+ return trimmed || null;
131
+ }
132
+ function readPort(value) {
133
+ const text = readString(value);
134
+ if (!text || !/^\d+$/.test(text)) {
135
+ return null;
136
+ }
137
+ const port = Number(text);
138
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
139
+ }
140
+ function parseSettings(rawSettings) {
141
+ if (!rawSettings) {
142
+ return {};
143
+ }
144
+ try {
145
+ const parsed = JSON.parse(rawSettings);
146
+ return isRecord(parsed) ? parsed : {};
147
+ } catch {
148
+ return {};
149
+ }
150
+ }
151
+ function parseWorkerEndpoint(env, rawSettings) {
152
+ const settings = parseSettings(rawSettings);
153
+ const host = readString(env["CLAUDE_MEM_WORKER_HOST"]) ?? readString(settings["CLAUDE_MEM_WORKER_HOST"]) ?? DEFAULT_WORKER_HOST;
154
+ const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
155
+ return { host, port };
156
+ }
157
+ function buildSearchParams(options) {
158
+ const params = new URLSearchParams;
159
+ const entries = [
160
+ ["query", options.query ?? ""],
161
+ ["limit", options.limit],
162
+ ["project", options.project],
163
+ ["platformSource", options.platformSource],
164
+ ["type", options.type],
165
+ ["obs_type", options.obs_type],
166
+ ["dateStart", options.dateStart],
167
+ ["dateEnd", options.dateEnd],
168
+ ["offset", options.offset],
169
+ ["orderBy", options.orderBy]
170
+ ];
171
+ for (const [key, value] of entries) {
172
+ if (value !== undefined) {
173
+ params.set(key, String(value));
174
+ }
175
+ }
176
+ return params;
177
+ }
178
+ function readSettingsRaw() {
179
+ try {
180
+ return readFileSync(SETTINGS_FILE, "utf8");
181
+ } catch {
182
+ return null;
183
+ }
184
+ }
185
+ function getWorkerBaseUrl() {
186
+ const endpoint = parseWorkerEndpoint(process.env, readSettingsRaw());
187
+ return `http://${endpoint.host}:${endpoint.port}`;
188
+ }
189
+ function extractTextContent(value) {
190
+ if (typeof value === "string") {
191
+ return value.trim() || null;
192
+ }
193
+ if (!isRecord(value)) {
194
+ return null;
195
+ }
196
+ const { content } = value;
197
+ if (typeof content === "string") {
198
+ return content.trim() || null;
199
+ }
200
+ if (Array.isArray(content)) {
201
+ const text = content.map((part) => {
202
+ if (!isRecord(part) || part["type"] !== "text" || typeof part["text"] !== "string") {
203
+ return "";
204
+ }
205
+ return part["text"];
206
+ }).filter(Boolean).join(`
207
+ `).trim();
208
+ return text || null;
209
+ }
210
+ const text = JSON.stringify(value, null, 2);
211
+ return text === "{}" || text === "null" ? null : text;
212
+ }
213
+
214
+ class WorkerClient {
215
+ static BASE_URL = getWorkerBaseUrl();
216
+ static autoStartPromise = null;
217
+ static async isHealthy() {
218
+ try {
219
+ const response = await fetch(`${this.BASE_URL}/api/health`, {
220
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
221
+ });
222
+ return response.ok;
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+ static async ensureRunning() {
228
+ await this.tryAutoStart();
229
+ return this.isHealthy();
230
+ }
231
+ static tryAutoStart() {
232
+ if (!this.autoStartPromise) {
233
+ this.autoStartPromise = this.runAutoStart().catch(() => "spawn-failed");
234
+ }
235
+ return this.autoStartPromise;
236
+ }
237
+ static async runAutoStart() {
238
+ if (await this.isHealthy()) {
239
+ return "already-running";
240
+ }
241
+ const bunPath = this.findBun();
242
+ if (!bunPath) {
243
+ return "no-bun";
244
+ }
245
+ try {
246
+ const child = spawn(bunPath, ["x", "claude-mem", "start"], {
247
+ detached: true,
248
+ stdio: "ignore",
249
+ windowsHide: true,
250
+ env: process.env
251
+ });
252
+ child.on("error", () => {});
253
+ child.unref();
254
+ } catch {
255
+ return "spawn-failed";
256
+ }
257
+ for (let i = 0;i < AUTOSTART_POLL_MAX_ATTEMPTS; i++) {
258
+ await new Promise((resolve) => setTimeout(resolve, AUTOSTART_POLL_INTERVAL_MS));
259
+ if (await this.isHealthy()) {
260
+ return "started";
261
+ }
262
+ }
263
+ return "timeout";
264
+ }
265
+ static findBun() {
266
+ const bunGlobal = globalThis.Bun;
267
+ if (bunGlobal && typeof bunGlobal.which === "function") {
268
+ try {
269
+ const found = bunGlobal.which("bun");
270
+ if (typeof found === "string" && found) {
271
+ return found;
272
+ }
273
+ } catch {}
274
+ }
275
+ const path = process.env.PATH || "";
276
+ const sep = process.platform === "win32" ? ";" : ":";
277
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
278
+ const slash = process.platform === "win32" ? "\\" : "/";
279
+ const dirs = path.split(sep).filter(Boolean);
280
+ for (const dir of dirs) {
281
+ const trailing = dir.endsWith("/") || dir.endsWith("\\") ? "" : slash;
282
+ for (const ext of exts) {
283
+ const candidate = `${dir}${trailing}bun${ext}`;
284
+ try {
285
+ if (existsSync(candidate)) {
286
+ return candidate;
287
+ }
288
+ } catch {}
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ static async sessionInit(contentSessionId, project, prompt) {
294
+ try {
295
+ const response = await fetch(`${this.BASE_URL}/api/sessions/init`, {
296
+ method: "POST",
297
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
298
+ headers: { "Content-Type": "application/json" },
299
+ body: JSON.stringify({
300
+ contentSessionId,
301
+ project,
302
+ prompt,
303
+ platformSource: PLATFORM_SOURCE
304
+ })
305
+ });
306
+ if (!response.ok) {
307
+ return null;
308
+ }
309
+ return await response.json();
310
+ } catch {
311
+ return null;
312
+ }
313
+ }
314
+ static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd, toolUseId) {
315
+ try {
316
+ const response = await fetch(`${this.BASE_URL}/api/sessions/observations`, {
317
+ method: "POST",
318
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
319
+ headers: { "Content-Type": "application/json" },
320
+ body: JSON.stringify({
321
+ contentSessionId,
322
+ tool_name: toolName,
323
+ tool_input: toolInput,
324
+ tool_response: toolResponse,
325
+ cwd,
326
+ platformSource: PLATFORM_SOURCE,
327
+ tool_use_id: toolUseId
328
+ })
329
+ });
330
+ return response.ok;
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+ static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage, metadata = {}) {
336
+ try {
337
+ const response = await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
338
+ method: "POST",
339
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
340
+ headers: { "Content-Type": "application/json" },
341
+ body: JSON.stringify({
342
+ contentSessionId,
343
+ last_user_message: lastUserMessage,
344
+ last_assistant_message: lastAssistantMessage,
345
+ platformSource: PLATFORM_SOURCE,
346
+ observedModel: metadata.observedModel,
347
+ observedBilling: metadata.observedBilling
348
+ })
349
+ });
350
+ return response.ok;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ static async getContext(project) {
356
+ try {
357
+ const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`);
358
+ if (!response.ok) {
359
+ return null;
360
+ }
361
+ const contentType = response.headers.get("content-type") || "";
362
+ if (contentType.includes("application/json")) {
363
+ return extractTextContent(await response.json());
364
+ }
365
+ const text = await response.text();
366
+ return text.trim() || null;
367
+ } catch {
368
+ return null;
369
+ }
370
+ }
371
+ static async timeline(options) {
372
+ try {
373
+ const params = new URLSearchParams({ project: options.project });
374
+ if (options.anchor !== undefined) {
375
+ params.set("anchor", String(options.anchor));
376
+ }
377
+ if (options.query) {
378
+ params.set("query", options.query);
379
+ }
380
+ if (options.depthBefore !== undefined) {
381
+ params.set("depth_before", String(options.depthBefore));
382
+ }
383
+ if (options.depthAfter !== undefined) {
384
+ params.set("depth_after", String(options.depthAfter));
385
+ }
386
+ const response = await fetch(`${this.BASE_URL}/api/timeline?${params.toString()}`);
387
+ if (!response.ok) {
388
+ return "";
389
+ }
390
+ const contentType = response.headers.get("content-type") || "";
391
+ if (contentType.includes("application/json")) {
392
+ return extractTextContent(await response.json()) || "";
393
+ }
394
+ const text = await response.text();
395
+ return text.trim();
396
+ } catch {
397
+ return "";
398
+ }
399
+ }
400
+ static async getObservations(ids, project) {
401
+ try {
402
+ const body = { ids };
403
+ if (project) {
404
+ body["project"] = project;
405
+ }
406
+ const response = await fetch(`${this.BASE_URL}/api/observations/batch`, {
407
+ method: "POST",
408
+ headers: { "Content-Type": "application/json" },
409
+ body: JSON.stringify(body)
410
+ });
411
+ if (!response.ok) {
412
+ return "";
413
+ }
414
+ const contentType = response.headers.get("content-type") || "";
415
+ if (contentType.includes("application/json")) {
416
+ const payload = await response.json();
417
+ if (Array.isArray(payload)) {
418
+ return payload.length ? JSON.stringify(payload, null, 2) : "";
419
+ }
420
+ return extractTextContent(payload) || "";
421
+ }
422
+ const text = await response.text();
423
+ return text.trim();
424
+ } catch {
425
+ return "";
426
+ }
427
+ }
428
+ static async getStats() {
429
+ try {
430
+ const [statsRes, processingRes] = await Promise.all([
431
+ fetch(`${this.BASE_URL}/api/stats`),
432
+ fetch(`${this.BASE_URL}/api/processing-status`)
433
+ ]);
434
+ if (!statsRes.ok) {
435
+ return null;
436
+ }
437
+ const stats = await statsRes.json();
438
+ const processing = processingRes.ok ? await processingRes.json() : undefined;
439
+ return { database: stats.database, processing };
440
+ } catch {
441
+ return null;
442
+ }
443
+ }
444
+ static async getRecentSummaries(project, limit) {
445
+ try {
446
+ const params = new URLSearchParams({ project, limit: String(limit) });
447
+ const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
448
+ if (!response.ok) {
449
+ return [];
450
+ }
451
+ const payload = await response.json();
452
+ if (!Array.isArray(payload.items)) {
453
+ return [];
454
+ }
455
+ return payload.items.flatMap((item) => {
456
+ if (!isRecord(item) || typeof item["id"] !== "number") {
457
+ return [];
458
+ }
459
+ const request = typeof item["request"] === "string" ? item["request"].trim() : "";
460
+ return request ? [{ id: item["id"], request }] : [];
461
+ });
462
+ } catch {
463
+ return [];
464
+ }
465
+ }
466
+ static async getRecentObservations(project, limit) {
467
+ try {
468
+ const params = new URLSearchParams({ project, limit: String(limit) });
469
+ const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
470
+ if (!response.ok) {
471
+ return [];
472
+ }
473
+ const payload = await response.json();
474
+ if (!Array.isArray(payload.items)) {
475
+ return [];
476
+ }
477
+ return payload.items.flatMap((item) => {
478
+ if (!isRecord(item) || typeof item["id"] !== "number") {
479
+ return [];
480
+ }
481
+ const title = typeof item["title"] === "string" ? item["title"].trim() : "";
482
+ const type = typeof item["type"] === "string" ? item["type"] : "";
483
+ return title ? [{ id: item["id"], type, title }] : [];
484
+ });
485
+ } catch {
486
+ return [];
487
+ }
488
+ }
489
+ static async search(options) {
490
+ try {
491
+ const params = buildSearchParams(options);
492
+ const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
493
+ if (!response.ok) {
494
+ return "";
495
+ }
496
+ const contentType = response.headers.get("content-type") || "";
497
+ if (contentType.includes("application/json")) {
498
+ return extractTextContent(await response.json()) || "";
499
+ }
500
+ const text = await response.text();
501
+ return text.trim();
502
+ } catch {
503
+ return "";
504
+ }
505
+ }
506
+ }
507
+
508
+ // src/tui-registration.ts
509
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
510
+ import { homedir as homedir2 } from "os";
511
+ import { join as join2 } from "path";
512
+ var PACKAGE_NAME = "@ephemushroom/opencode-claude-mem";
513
+ var SIDEBAR_ENTRIES = [PACKAGE_NAME, `${PACKAGE_NAME}/cli`, `${PACKAGE_NAME}/tui`];
514
+ function getConfigDir() {
515
+ const custom = process.env["OPENCODE_CONFIG_DIR"]?.trim();
516
+ return custom || join2(homedir2(), ".config", "opencode");
517
+ }
518
+ function readJson(filePath) {
519
+ try {
520
+ const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
521
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
522
+ } catch {
523
+ return null;
524
+ }
525
+ }
526
+ function isCliEntry(entry) {
527
+ if (typeof entry === "string") {
528
+ return true;
529
+ }
530
+ if (typeof entry !== "object" || entry === null || !("package" in entry)) {
531
+ return false;
532
+ }
533
+ return typeof entry.package === "string" && (!("options" in entry) || typeof entry.options === "object" && entry.options !== null && !Array.isArray(entry.options));
534
+ }
535
+ function v2PluginEntries(config) {
536
+ const { plugins } = config;
537
+ if (plugins === undefined) {
538
+ return [];
539
+ }
540
+ return Array.isArray(plugins) && plugins.every(isCliEntry) ? plugins : null;
541
+ }
542
+ function ensureCliPluginEntry() {
543
+ try {
544
+ const configDir = getConfigDir();
545
+ const cliJsonPath = join2(configDir, "cli.json");
546
+ let config = {};
547
+ if (existsSync2(cliJsonPath)) {
548
+ const parsed = readJson(cliJsonPath);
549
+ if (!parsed) {
550
+ return "malformed";
551
+ }
552
+ config = parsed;
553
+ }
554
+ const plugins = v2PluginEntries(config);
555
+ if (!plugins) {
556
+ return "malformed";
557
+ }
558
+ if (plugins.some((entry) => {
559
+ const specifier = typeof entry === "string" ? entry : entry.package;
560
+ return SIDEBAR_ENTRIES.some((name) => specifier === name || specifier.startsWith(`${name}@`));
561
+ })) {
562
+ return "already-present";
563
+ }
564
+ mkdirSync(configDir, { recursive: true });
565
+ const next = { ...config, plugins: [...plugins, PACKAGE_NAME] };
566
+ writeFileSync(cliJsonPath, `${JSON.stringify(next, null, 2)}
567
+ `);
568
+ return "added";
569
+ } catch {
570
+ return "failed";
571
+ }
572
+ }
573
+
574
+ // src/v2.ts
575
+ var EVENT_RETRY_DELAY_MS = 1e4;
576
+ var v2_default = define({
577
+ id: "claude-mem",
578
+ setup: async (ctx) => {
579
+ ensureCliPluginEntry();
580
+ const controller = new AbortController;
581
+ const initializedSessions = new Set;
582
+ const sessionInits = new Map;
583
+ const contextCache = new Map;
584
+ const sessionDirs = new Map;
585
+ const sessionUserTexts = new Map;
586
+ const sessionAssistantTexts = new Map;
587
+ const sessionModels = new Map;
588
+ const defaultDirectory = ctx.location.directory;
589
+ async function checkWorker() {
590
+ return !controller.signal.aborted && await WorkerClient.ensureRunning();
591
+ }
592
+ async function loadSessionLocation(sessionId) {
593
+ if (sessionDirs.has(sessionId)) {
594
+ return true;
595
+ }
596
+ try {
597
+ const session = await ctx.session.get({ sessionID: sessionId });
598
+ if (controller.signal.aborted || session.location.directory !== defaultDirectory) {
599
+ return false;
600
+ }
601
+ sessionDirs.set(sessionId, session.location.directory);
602
+ return true;
603
+ } catch {
604
+ return false;
605
+ }
606
+ }
607
+ function projectNameFromDirectory(directory) {
608
+ return directory.split(/[\\/]/).findLast(Boolean) || "unknown-project";
609
+ }
610
+ function getProjectName(sessionId) {
611
+ return projectNameFromDirectory(sessionDirs.get(sessionId) || defaultDirectory);
612
+ }
613
+ async function ensureSessionInit(sessionId, prompt) {
614
+ if (initializedSessions.has(sessionId)) {
615
+ return true;
616
+ }
617
+ const existing = sessionInits.get(sessionId);
618
+ if (existing) {
619
+ return existing;
620
+ }
621
+ const pending = (async () => {
622
+ if (!await checkWorker() || !await loadSessionLocation(sessionId)) {
623
+ return false;
624
+ }
625
+ const result = await WorkerClient.sessionInit(sessionId, getProjectName(sessionId), prompt || "SESSION_START");
626
+ if (!result || controller.signal.aborted || !sessionInits.has(sessionId)) {
627
+ return false;
628
+ }
629
+ initializedSessions.add(sessionId);
630
+ return true;
631
+ })();
632
+ sessionInits.set(sessionId, pending);
633
+ try {
634
+ return await pending;
635
+ } finally {
636
+ if (sessionInits.get(sessionId) === pending) {
637
+ sessionInits.delete(sessionId);
638
+ }
639
+ }
640
+ }
641
+ async function getCachedContext(sessionId) {
642
+ if (!contextCache.has(sessionId)) {
643
+ const context = await WorkerClient.getContext(getProjectName(sessionId));
644
+ if (context !== null && !controller.signal.aborted && sessionDirs.has(sessionId)) {
645
+ contextCache.set(sessionId, context);
646
+ }
647
+ }
648
+ return contextCache.get(sessionId) ?? null;
649
+ }
650
+ async function sendObservation(sessionId, toolName, toolInput, toolResponse, toolUseId) {
651
+ if (!await ensureSessionInit(sessionId)) {
652
+ return;
653
+ }
654
+ try {
655
+ await WorkerClient.sendObservation(sessionId, toolName, toolInput, toolResponse, sessionDirs.get(sessionId) || defaultDirectory, toolUseId);
656
+ } catch {}
657
+ }
658
+ function extractLastUserText(messages) {
659
+ if (!Array.isArray(messages)) {
660
+ return "";
661
+ }
662
+ for (const message of messages.toReversed()) {
663
+ if (message && typeof message === "object" && "role" in message && message.role === "user" && "content" in message && Array.isArray(message.content)) {
664
+ const text = extractTextFromParts(message.content);
665
+ if (text) {
666
+ return text;
667
+ }
668
+ }
669
+ }
670
+ return "";
671
+ }
672
+ function normalizeToolResult(result) {
673
+ if (typeof result === "string") {
674
+ return result;
675
+ }
676
+ if (!result || typeof result !== "object") {
677
+ return normalizeToolOutput(result);
678
+ }
679
+ const content = "content" in result ? result.content : undefined;
680
+ if (typeof content === "string") {
681
+ return content;
682
+ }
683
+ if (Array.isArray(content)) {
684
+ const text = extractTextFromParts(content);
685
+ if (text) {
686
+ return text;
687
+ }
688
+ }
689
+ if ("output" in result && result.output !== undefined) {
690
+ return normalizeToolOutput(result.output);
691
+ }
692
+ return normalizeToolOutput(result);
693
+ }
694
+ await ctx.session.hook("context", async (event) => {
695
+ const sessionId = event.sessionID;
696
+ if (!sessionId) {
697
+ return;
698
+ }
699
+ if (!await loadSessionLocation(sessionId)) {
700
+ return;
701
+ }
702
+ sessionModels.set(sessionId, event.model.id);
703
+ sessionUserTexts.set(sessionId, extractLastUserText(event.messages));
704
+ await ensureSessionInit(sessionId, sessionUserTexts.get(sessionId));
705
+ if (!await checkWorker()) {
706
+ return;
707
+ }
708
+ try {
709
+ const context = await getCachedContext(sessionId);
710
+ if (context) {
711
+ event.system.push({
712
+ type: "text",
713
+ text: `<claude-mem-context>
714
+ [Claude-Mem] Memory Active. Previous Context:
715
+ ${context}
716
+ </claude-mem-context>`
717
+ });
718
+ }
719
+ } catch {}
720
+ });
721
+ await ctx.tool.hook("execute.after", async (event) => {
722
+ const sessionId = event.sessionID;
723
+ if (!sessionId || shouldSkipObservationTool(event.tool)) {
724
+ return;
725
+ }
726
+ if (!await checkWorker()) {
727
+ return;
728
+ }
729
+ const sanitizedInput = sanitizeObservationValue(event.input ?? {});
730
+ const sanitizedOutput = truncateUtf8Bytes(stripTaggedContent(event.status === "error" ? event.error?.message ?? String(event.error ?? "") : normalizeToolResult(event.result)), MAX_OBSERVATION_BYTES);
731
+ await sendObservation(sessionId, event.tool, sanitizedInput, sanitizedOutput, event.id);
732
+ });
733
+ await ctx.tool.transform((tools) => {
734
+ tools.add({
735
+ name: "mem-search",
736
+ description: "Search Claude-Mem persistent memory. Supports query, project, platformSource, type, obs_type, dateStart, dateEnd, offset, and orderBy filters.",
737
+ input: {
738
+ type: "object",
739
+ properties: {
740
+ query: {
741
+ type: "string",
742
+ description: "Optional semantic search query for Claude-Mem memory"
743
+ },
744
+ limit: {
745
+ type: "integer",
746
+ minimum: 1,
747
+ maximum: 100,
748
+ description: "Maximum number of search results (default 20)"
749
+ },
750
+ project: { type: "string", description: "Filter by project name" },
751
+ platformSource: {
752
+ type: "string",
753
+ description: "Filter by platform source, such as claude or opencode"
754
+ },
755
+ type: { type: "string", description: "Filter by result type" },
756
+ obs_type: {
757
+ type: "string",
758
+ description: "Filter by observation type, such as feature or bugfix"
759
+ },
760
+ dateStart: {
761
+ type: "string",
762
+ description: "Start date filter in ISO 8601 or YYYY-MM-DD format"
763
+ },
764
+ dateEnd: {
765
+ type: "string",
766
+ description: "End date filter in ISO 8601 or YYYY-MM-DD format"
767
+ },
768
+ offset: { type: "integer", minimum: 0, description: "Pagination offset" },
769
+ orderBy: {
770
+ type: "string",
771
+ enum: ["date_desc", "date_asc", "relevance"],
772
+ description: "Sort order"
773
+ }
774
+ },
775
+ additionalProperties: false
776
+ },
777
+ options: { codemode: false },
778
+ execute: async (input, toolContext) => {
779
+ const args = input ?? {};
780
+ if (!await checkWorker()) {
781
+ return {
782
+ content: "Claude-Mem worker is offline. Start Claude-Mem and retry the search."
783
+ };
784
+ }
785
+ const result = await WorkerClient.search({
786
+ query: args.query,
787
+ limit: args.limit,
788
+ project: args.project ?? getProjectName(toolContext.sessionID),
789
+ platformSource: args.platformSource,
790
+ type: args.type,
791
+ obs_type: args.obs_type,
792
+ dateStart: args.dateStart,
793
+ dateEnd: args.dateEnd,
794
+ offset: args.offset,
795
+ orderBy: args.orderBy
796
+ });
797
+ return {
798
+ content: result || `No Claude-Mem results found${args.query ? ` for "${args.query}"` : ""}.`
799
+ };
800
+ }
801
+ });
802
+ tools.add({
803
+ name: "mem-timeline",
804
+ description: "Get chronological Claude-Mem context around an observation. Use after mem-search: pass an observation ID as anchor (or a query to find the anchor automatically) to see what happened before/after.",
805
+ input: {
806
+ type: "object",
807
+ properties: {
808
+ anchor: {
809
+ type: "integer",
810
+ minimum: 1,
811
+ description: "Observation ID to center the timeline around"
812
+ },
813
+ query: {
814
+ type: "string",
815
+ description: "Query to locate the anchor automatically (used when anchor is omitted)"
816
+ },
817
+ depth_before: {
818
+ type: "integer",
819
+ minimum: 0,
820
+ maximum: 20,
821
+ description: "Items before the anchor (default 3)"
822
+ },
823
+ depth_after: {
824
+ type: "integer",
825
+ minimum: 0,
826
+ maximum: 20,
827
+ description: "Items after the anchor (default 3)"
828
+ }
829
+ },
830
+ additionalProperties: false
831
+ },
832
+ options: { codemode: false },
833
+ execute: async (input, toolContext) => {
834
+ const args = input ?? {};
835
+ if (args.anchor === undefined && !args.query) {
836
+ return { content: "Provide either an anchor observation ID or a query." };
837
+ }
838
+ if (!await checkWorker()) {
839
+ return { content: "Claude-Mem worker is offline. Start Claude-Mem and retry." };
840
+ }
841
+ const result = await WorkerClient.timeline({
842
+ project: getProjectName(toolContext.sessionID),
843
+ anchor: args.anchor,
844
+ query: args.query,
845
+ depthBefore: args.depth_before,
846
+ depthAfter: args.depth_after
847
+ });
848
+ return { content: result || "No Claude-Mem timeline results found." };
849
+ }
850
+ });
851
+ tools.add({
852
+ name: "mem-get-observations",
853
+ description: "Fetch full Claude-Mem observation details by ID. Use for IDs shown in the injected memory context or returned by mem-search/mem-timeline.",
854
+ input: {
855
+ type: "object",
856
+ properties: {
857
+ ids: {
858
+ type: "array",
859
+ items: { type: "integer", minimum: 1 },
860
+ description: "Observation IDs to fetch"
861
+ }
862
+ },
863
+ required: ["ids"],
864
+ additionalProperties: false
865
+ },
866
+ options: { codemode: false },
867
+ execute: async (input, toolContext) => {
868
+ const args = input ?? {};
869
+ if (!await checkWorker()) {
870
+ return { content: "Claude-Mem worker is offline. Start Claude-Mem and retry." };
871
+ }
872
+ const result = await WorkerClient.getObservations(args.ids || [], getProjectName(toolContext.sessionID));
873
+ return {
874
+ content: result || `No Claude-Mem observations found for IDs [${(args.ids || []).join(", ")}].`
875
+ };
876
+ }
877
+ });
878
+ });
879
+ let aborted = false;
880
+ let retryTimer = undefined;
881
+ const subscribeAndHandleEvents = async () => {
882
+ try {
883
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
884
+ if (aborted) {
885
+ return;
886
+ }
887
+ try {
888
+ await handleEvent(event);
889
+ } catch {}
890
+ }
891
+ } catch {}
892
+ if (!aborted) {
893
+ retryTimer = setTimeout(() => {
894
+ subscribeAndHandleEvents();
895
+ }, EVENT_RETRY_DELAY_MS);
896
+ }
897
+ };
898
+ async function handleEvent(event) {
899
+ if (event.location && event.location.directory !== defaultDirectory) {
900
+ return;
901
+ }
902
+ switch (event.type) {
903
+ case "session.created": {
904
+ const { data } = event;
905
+ if (!data?.sessionID) {
906
+ return;
907
+ }
908
+ const directory = data.location?.directory;
909
+ if (typeof directory === "string" && directory) {
910
+ if (directory !== defaultDirectory) {
911
+ return;
912
+ }
913
+ sessionDirs.set(data.sessionID, directory);
914
+ }
915
+ contextCache.delete(data.sessionID);
916
+ await checkWorker();
917
+ return;
918
+ }
919
+ case "session.text.ended": {
920
+ const { data } = event;
921
+ const sessionId = data?.sessionID;
922
+ const text = data?.text;
923
+ if (typeof sessionId !== "string" || typeof text !== "string" || !text) {
924
+ return;
925
+ }
926
+ if (!await loadSessionLocation(sessionId)) {
927
+ return;
928
+ }
929
+ sessionAssistantTexts.set(sessionId, text);
930
+ if (!await checkWorker()) {
931
+ return;
932
+ }
933
+ const sanitized = truncateUtf8Bytes(stripTaggedContent(text), MAX_OBSERVATION_BYTES);
934
+ if (sanitized) {
935
+ await sendObservation(sessionId, "assistant_message", { messageId: data.assistantMessageID }, sanitized, `${data.assistantMessageID}:${data.ordinal}`);
936
+ }
937
+ return;
938
+ }
939
+ case "session.execution.succeeded":
940
+ case "session.compaction.ended": {
941
+ const sessionId = event.data?.sessionID;
942
+ if (typeof sessionId !== "string") {
943
+ return;
944
+ }
945
+ if (!await checkWorker()) {
946
+ return;
947
+ }
948
+ if (!initializedSessions.has(sessionId)) {
949
+ return;
950
+ }
951
+ try {
952
+ await WorkerClient.summarize(sessionId, sessionUserTexts.get(sessionId) || "", sessionAssistantTexts.get(sessionId) || "", { observedModel: sessionModels.get(sessionId) });
953
+ } catch {}
954
+ return;
955
+ }
956
+ case "session.deleted": {
957
+ const sessionId = event.data?.sessionID;
958
+ if (typeof sessionId !== "string") {
959
+ return;
960
+ }
961
+ initializedSessions.delete(sessionId);
962
+ sessionInits.delete(sessionId);
963
+ contextCache.delete(sessionId);
964
+ sessionDirs.delete(sessionId);
965
+ sessionUserTexts.delete(sessionId);
966
+ sessionAssistantTexts.delete(sessionId);
967
+ sessionModels.delete(sessionId);
968
+ return;
969
+ }
970
+ default: {
971
+ return;
972
+ }
973
+ }
974
+ }
975
+ subscribeAndHandleEvents();
976
+ return () => {
977
+ aborted = true;
978
+ controller.abort();
979
+ clearTimeout(retryTimer);
980
+ initializedSessions.clear();
981
+ sessionInits.clear();
982
+ contextCache.clear();
983
+ sessionDirs.clear();
984
+ sessionUserTexts.clear();
985
+ sessionAssistantTexts.clear();
986
+ sessionModels.clear();
987
+ };
988
+ }
989
+ });
990
+ export {
991
+ v2_default as default
992
+ };