@ephemushroom/opencode-claude-mem 0.4.4 → 0.6.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/cli.js ADDED
@@ -0,0 +1,743 @@
1
+ // @bun
2
+ // src/worker-client.ts
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { homedir } from "os";
5
+ import { join } from "path";
6
+ import { spawn } from "child_process";
7
+
8
+ // src/shared.ts
9
+ var MAX_OBSERVATION_BYTES = 24 * 1024;
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
+ function stripTaggedContent(text) {
23
+ let result = "";
24
+ let offset = 0;
25
+ const stack = [];
26
+ for (const match of text.matchAll(/<(\/?)(private|claude-mem-context)\b[^>]*>/gi)) {
27
+ if (stack.length === 0) {
28
+ result += text.slice(offset, match.index);
29
+ }
30
+ const name = match[2].toLowerCase();
31
+ if (match[1] === "/") {
32
+ if (stack.at(-1) === name) {
33
+ stack.pop();
34
+ }
35
+ } else {
36
+ stack.push(name);
37
+ }
38
+ offset = match.index + match[0].length;
39
+ }
40
+ if (stack.length === 0) {
41
+ result += text.slice(offset);
42
+ }
43
+ return result.trim();
44
+ }
45
+
46
+ // src/worker-client.ts
47
+ var HEALTH_TIMEOUT_MS = 1000;
48
+ var WRITE_TIMEOUT_MS = 1e4;
49
+ var AUTOSTART_POLL_INTERVAL_MS = 500;
50
+ var AUTOSTART_POLL_MAX_ATTEMPTS = 16;
51
+ var DEFAULT_WORKER_HOST = "127.0.0.1";
52
+ var DEFAULT_WORKER_PORT = 37777;
53
+ var PLATFORM_SOURCE = "opencode";
54
+ var SETTINGS_FILE = join(homedir(), ".claude-mem", "settings.json");
55
+ function isRecord(value) {
56
+ return typeof value === "object" && value !== null && !Array.isArray(value);
57
+ }
58
+ function readString(value) {
59
+ if (typeof value !== "string") {
60
+ return null;
61
+ }
62
+ const trimmed = value.trim();
63
+ return trimmed || null;
64
+ }
65
+ function readPort(value) {
66
+ const text = readString(value);
67
+ if (!text || !/^\d+$/.test(text)) {
68
+ return null;
69
+ }
70
+ const port = Number(text);
71
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
72
+ }
73
+ function parseSettings(rawSettings) {
74
+ if (!rawSettings) {
75
+ return {};
76
+ }
77
+ try {
78
+ const parsed = JSON.parse(rawSettings);
79
+ return isRecord(parsed) ? parsed : {};
80
+ } catch {
81
+ return {};
82
+ }
83
+ }
84
+ function parseWorkerEndpoint(env, rawSettings) {
85
+ const settings = parseSettings(rawSettings);
86
+ const host = readString(env["CLAUDE_MEM_WORKER_HOST"]) ?? readString(settings["CLAUDE_MEM_WORKER_HOST"]) ?? DEFAULT_WORKER_HOST;
87
+ const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
88
+ return { host, port };
89
+ }
90
+ function buildSearchParams(options) {
91
+ const params = new URLSearchParams;
92
+ const entries = [
93
+ ["query", options.query ?? ""],
94
+ ["limit", options.limit],
95
+ ["project", options.project],
96
+ ["platformSource", options.platformSource],
97
+ ["type", options.type],
98
+ ["obs_type", options.obs_type],
99
+ ["dateStart", options.dateStart],
100
+ ["dateEnd", options.dateEnd],
101
+ ["offset", options.offset],
102
+ ["orderBy", options.orderBy]
103
+ ];
104
+ for (const [key, value] of entries) {
105
+ if (value !== undefined) {
106
+ params.set(key, String(value));
107
+ }
108
+ }
109
+ return params;
110
+ }
111
+ function readSettingsRaw() {
112
+ try {
113
+ return readFileSync(SETTINGS_FILE, "utf8");
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function getWorkerBaseUrl() {
119
+ const endpoint = parseWorkerEndpoint(process.env, readSettingsRaw());
120
+ return `http://${endpoint.host}:${endpoint.port}`;
121
+ }
122
+ function extractTextContent(value) {
123
+ if (typeof value === "string") {
124
+ return value.trim() || null;
125
+ }
126
+ if (!isRecord(value)) {
127
+ return null;
128
+ }
129
+ const { content } = value;
130
+ if (typeof content === "string") {
131
+ return content.trim() || null;
132
+ }
133
+ if (Array.isArray(content)) {
134
+ const text = content.map((part) => {
135
+ if (!isRecord(part) || part["type"] !== "text" || typeof part["text"] !== "string") {
136
+ return "";
137
+ }
138
+ return part["text"];
139
+ }).filter(Boolean).join(`
140
+ `).trim();
141
+ return text || null;
142
+ }
143
+ const text = JSON.stringify(value, null, 2);
144
+ return text === "{}" || text === "null" ? null : text;
145
+ }
146
+
147
+ class WorkerClient {
148
+ static BASE_URL = getWorkerBaseUrl();
149
+ static autoStartPromise = null;
150
+ static async isHealthy() {
151
+ try {
152
+ const response = await fetch(`${this.BASE_URL}/api/health`, {
153
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
154
+ });
155
+ return response.ok;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ static async ensureRunning() {
161
+ await this.tryAutoStart();
162
+ return this.isHealthy();
163
+ }
164
+ static tryAutoStart() {
165
+ if (!this.autoStartPromise) {
166
+ this.autoStartPromise = this.runAutoStart().catch(() => "spawn-failed");
167
+ }
168
+ return this.autoStartPromise;
169
+ }
170
+ static async runAutoStart() {
171
+ if (await this.isHealthy()) {
172
+ return "already-running";
173
+ }
174
+ const bunPath = this.findBun();
175
+ if (!bunPath) {
176
+ return "no-bun";
177
+ }
178
+ try {
179
+ const child = spawn(bunPath, ["x", "claude-mem", "start"], {
180
+ detached: true,
181
+ stdio: "ignore",
182
+ windowsHide: true,
183
+ env: process.env
184
+ });
185
+ child.on("error", () => {});
186
+ child.unref();
187
+ } catch {
188
+ return "spawn-failed";
189
+ }
190
+ for (let i = 0;i < AUTOSTART_POLL_MAX_ATTEMPTS; i++) {
191
+ await new Promise((resolve) => setTimeout(resolve, AUTOSTART_POLL_INTERVAL_MS));
192
+ if (await this.isHealthy()) {
193
+ return "started";
194
+ }
195
+ }
196
+ return "timeout";
197
+ }
198
+ static findBun() {
199
+ const bunGlobal = globalThis.Bun;
200
+ if (bunGlobal && typeof bunGlobal.which === "function") {
201
+ try {
202
+ const found = bunGlobal.which("bun");
203
+ if (typeof found === "string" && found) {
204
+ return found;
205
+ }
206
+ } catch {}
207
+ }
208
+ const path = process.env.PATH || "";
209
+ const sep = process.platform === "win32" ? ";" : ":";
210
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
211
+ const slash = process.platform === "win32" ? "\\" : "/";
212
+ const dirs = path.split(sep).filter(Boolean);
213
+ for (const dir of dirs) {
214
+ const trailing = dir.endsWith("/") || dir.endsWith("\\") ? "" : slash;
215
+ for (const ext of exts) {
216
+ const candidate = `${dir}${trailing}bun${ext}`;
217
+ try {
218
+ if (existsSync(candidate)) {
219
+ return candidate;
220
+ }
221
+ } catch {}
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+ static async sessionInit(contentSessionId, project, prompt) {
227
+ try {
228
+ const response = await fetch(`${this.BASE_URL}/api/sessions/init`, {
229
+ method: "POST",
230
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
231
+ headers: { "Content-Type": "application/json" },
232
+ body: JSON.stringify({
233
+ contentSessionId,
234
+ project,
235
+ prompt,
236
+ platformSource: PLATFORM_SOURCE
237
+ })
238
+ });
239
+ if (!response.ok) {
240
+ return null;
241
+ }
242
+ const value = await response.json();
243
+ if (!isRecord(value) || typeof value.sessionDbId !== "number" || typeof value.promptNumber !== "number" || !Number.isFinite(value.sessionDbId) || !Number.isFinite(value.promptNumber)) {
244
+ return null;
245
+ }
246
+ return {
247
+ sessionDbId: value.sessionDbId,
248
+ promptNumber: value.promptNumber,
249
+ skipped: value.skipped === true,
250
+ reason: typeof value.reason === "string" ? value.reason : undefined
251
+ };
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd, toolUseId) {
257
+ try {
258
+ const response = await fetch(`${this.BASE_URL}/api/sessions/observations`, {
259
+ method: "POST",
260
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
261
+ headers: { "Content-Type": "application/json" },
262
+ body: JSON.stringify({
263
+ contentSessionId,
264
+ tool_name: toolName,
265
+ tool_input: toolInput,
266
+ tool_response: toolResponse,
267
+ cwd,
268
+ platformSource: PLATFORM_SOURCE,
269
+ tool_use_id: toolUseId
270
+ })
271
+ });
272
+ return response.ok;
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+ static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage, metadata = {}) {
278
+ try {
279
+ const response = await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
280
+ method: "POST",
281
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
282
+ headers: { "Content-Type": "application/json" },
283
+ body: JSON.stringify({
284
+ contentSessionId,
285
+ last_user_message: stripTaggedContent(lastUserMessage),
286
+ last_assistant_message: stripTaggedContent(lastAssistantMessage),
287
+ platformSource: PLATFORM_SOURCE,
288
+ observedModel: metadata.observedModel,
289
+ observedBilling: metadata.observedBilling
290
+ })
291
+ });
292
+ return response.ok;
293
+ } catch {
294
+ return false;
295
+ }
296
+ }
297
+ static async getContext(project) {
298
+ try {
299
+ const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`, { signal: AbortSignal.timeout(2000) });
300
+ if (!response.ok) {
301
+ return null;
302
+ }
303
+ const contentType = response.headers.get("content-type") || "";
304
+ if (contentType.includes("application/json")) {
305
+ return extractTextContent(await response.json());
306
+ }
307
+ const text = await response.text();
308
+ return text.trim() || null;
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ static async timeline(options) {
314
+ try {
315
+ const params = new URLSearchParams({ project: options.project });
316
+ if (options.anchor !== undefined) {
317
+ params.set("anchor", String(options.anchor));
318
+ }
319
+ if (options.query) {
320
+ params.set("query", options.query);
321
+ }
322
+ if (options.depthBefore !== undefined) {
323
+ params.set("depth_before", String(options.depthBefore));
324
+ }
325
+ if (options.depthAfter !== undefined) {
326
+ params.set("depth_after", String(options.depthAfter));
327
+ }
328
+ const response = await fetch(`${this.BASE_URL}/api/timeline?${params.toString()}`);
329
+ if (!response.ok) {
330
+ return "";
331
+ }
332
+ const contentType = response.headers.get("content-type") || "";
333
+ if (contentType.includes("application/json")) {
334
+ return extractTextContent(await response.json()) || "";
335
+ }
336
+ const text = await response.text();
337
+ return text.trim();
338
+ } catch {
339
+ return "";
340
+ }
341
+ }
342
+ static async getObservations(ids, project) {
343
+ try {
344
+ const body = { ids };
345
+ if (project) {
346
+ body["project"] = project;
347
+ }
348
+ const response = await fetch(`${this.BASE_URL}/api/observations/batch`, {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/json" },
351
+ body: JSON.stringify(body)
352
+ });
353
+ if (!response.ok) {
354
+ return "";
355
+ }
356
+ const contentType = response.headers.get("content-type") || "";
357
+ if (contentType.includes("application/json")) {
358
+ const payload = await response.json();
359
+ if (Array.isArray(payload)) {
360
+ return payload.length ? JSON.stringify(payload, null, 2) : "";
361
+ }
362
+ return extractTextContent(payload) || "";
363
+ }
364
+ const text = await response.text();
365
+ return text.trim();
366
+ } catch {
367
+ return "";
368
+ }
369
+ }
370
+ static async getStats() {
371
+ try {
372
+ const [statsRes, processingRes] = await Promise.all([
373
+ fetch(`${this.BASE_URL}/api/stats`),
374
+ fetch(`${this.BASE_URL}/api/processing-status`)
375
+ ]);
376
+ if (!statsRes.ok) {
377
+ return null;
378
+ }
379
+ const stats = await statsRes.json();
380
+ const processing = processingRes.ok ? await processingRes.json() : undefined;
381
+ return { database: stats.database, processing };
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+ static async getRecentSummaries(project, limit) {
387
+ try {
388
+ const params = new URLSearchParams({ project, limit: String(limit) });
389
+ const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
390
+ if (!response.ok) {
391
+ return [];
392
+ }
393
+ const payload = await response.json();
394
+ if (!Array.isArray(payload.items)) {
395
+ return [];
396
+ }
397
+ return payload.items.flatMap((item) => {
398
+ if (!isRecord(item) || typeof item["id"] !== "number") {
399
+ return [];
400
+ }
401
+ const request = typeof item["request"] === "string" ? item["request"].trim() : "";
402
+ return request ? [{ id: item["id"], request }] : [];
403
+ });
404
+ } catch {
405
+ return [];
406
+ }
407
+ }
408
+ static async getRecentObservations(project, limit) {
409
+ try {
410
+ const params = new URLSearchParams({ project, limit: String(limit) });
411
+ const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
412
+ if (!response.ok) {
413
+ return [];
414
+ }
415
+ const payload = await response.json();
416
+ if (!Array.isArray(payload.items)) {
417
+ return [];
418
+ }
419
+ return payload.items.flatMap((item) => {
420
+ if (!isRecord(item) || typeof item["id"] !== "number") {
421
+ return [];
422
+ }
423
+ const title = typeof item["title"] === "string" ? item["title"].trim() : "";
424
+ const type = typeof item["type"] === "string" ? item["type"] : "";
425
+ return title ? [{ id: item["id"], type, title }] : [];
426
+ });
427
+ } catch {
428
+ return [];
429
+ }
430
+ }
431
+ static async search(options) {
432
+ try {
433
+ const params = buildSearchParams(options);
434
+ const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
435
+ if (!response.ok) {
436
+ return "";
437
+ }
438
+ const contentType = response.headers.get("content-type") || "";
439
+ if (contentType.includes("application/json")) {
440
+ return extractTextContent(await response.json()) || "";
441
+ }
442
+ const text = await response.text();
443
+ return text.trim();
444
+ } catch {
445
+ return "";
446
+ }
447
+ }
448
+ }
449
+
450
+ // src/sidebar-model.ts
451
+ var POLL_INTERVAL_MS = 5000;
452
+ var LABEL_MAX = 40;
453
+ var RECENT_SUMMARIES = 3;
454
+ var RECENT_OBSERVATIONS = 3;
455
+ var OBSERVATION_TYPE_ICONS = {
456
+ bugfix: "\u25CF",
457
+ feature: "\u25C6",
458
+ refactor: "\u21BB",
459
+ change: "\u2713",
460
+ discovery: "\u25CB",
461
+ decision: "\u2696",
462
+ security_alert: "\u26A0",
463
+ security_note: "\u26B7"
464
+ };
465
+ function box(props, children) {
466
+ return { kind: "box", props, children };
467
+ }
468
+ function text(props, value) {
469
+ return { kind: "text", props, text: value };
470
+ }
471
+ var HTML_ENTITIES = {
472
+ "&lt;": "<",
473
+ "&gt;": ">",
474
+ "&quot;": '"',
475
+ "&#39;": "'",
476
+ "&amp;": "&"
477
+ };
478
+ function decodeEntities(value) {
479
+ return value.replace(/&(?:lt|gt|quot|#39|amp);/g, (entity) => HTML_ENTITIES[entity] ?? entity);
480
+ }
481
+ function truncate(value) {
482
+ const decoded = decodeEntities(value);
483
+ return decoded.length <= LABEL_MAX ? decoded : `${decoded.slice(0, LABEL_MAX - 1)}\u2026`;
484
+ }
485
+ function formatCount(value) {
486
+ if (value === null) {
487
+ return "-";
488
+ }
489
+ return value >= 1e4 ? `${(value / 1000).toFixed(1)}k` : String(value);
490
+ }
491
+ function summaryLine(view) {
492
+ if (!view.healthy) {
493
+ return { label: "(offline)", tone: "err" };
494
+ }
495
+ if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
496
+ return { label: `(queue ${view.queueDepth ?? 0})`, tone: "warn" };
497
+ }
498
+ return { label: `(online, ${formatCount(view.observations)} obs)`, tone: "ok" };
499
+ }
500
+ function toneColor(tone, theme) {
501
+ if (tone === "err") {
502
+ return theme.error;
503
+ }
504
+ return tone === "warn" ? theme.warning : theme.textMuted;
505
+ }
506
+ function bulletRow(icon, iconColor, label, theme) {
507
+ return box({ flexDirection: "row", gap: 1 }, [
508
+ text({ fg: iconColor, flexShrink: 0 }, icon),
509
+ text({ fg: theme.text, wrapMode: "none" }, truncate(label))
510
+ ]);
511
+ }
512
+ function expandedRows(view, theme) {
513
+ if (!view.healthy) {
514
+ return [bulletRow("\u2022", theme.error, "worker offline", theme)];
515
+ }
516
+ const rows = [
517
+ bulletRow("\u2022", theme.success, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`, theme)
518
+ ];
519
+ if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
520
+ rows.push(bulletRow("\u2022", theme.warning, `processing \xB7 queue ${view.queueDepth ?? 0}`, theme));
521
+ }
522
+ if (view.recentSummaries.length > 0) {
523
+ rows.push(text({ fg: theme.textMuted }, "Recent sessions"));
524
+ for (const summary of view.recentSummaries) {
525
+ rows.push(bulletRow("\u2022", theme.textMuted, summary.request, theme));
526
+ }
527
+ }
528
+ if (view.recentObservations.length > 0) {
529
+ rows.push(text({ fg: theme.textMuted }, "Latest"));
530
+ for (const observation of view.recentObservations) {
531
+ const icon = OBSERVATION_TYPE_ICONS[observation.type] ?? "\u25CB";
532
+ rows.push(bulletRow(icon, theme.info, observation.title, theme));
533
+ }
534
+ }
535
+ return rows;
536
+ }
537
+ function buildMemNodes(view, theme, collapsed, onToggle) {
538
+ const summary = summaryLine(view);
539
+ const header = box({ flexDirection: "row", gap: 1, onMouseDown: onToggle }, [
540
+ text({ fg: theme.text }, collapsed ? "\u25B6" : "\u25BC"),
541
+ text({ fg: theme.info }, "Memory"),
542
+ ...collapsed ? [text({ fg: toneColor(summary.tone, theme) }, summary.label)] : []
543
+ ]);
544
+ return [
545
+ box({ flexDirection: "column", paddingTop: 1 }, [
546
+ header,
547
+ ...collapsed ? [] : [box({ flexDirection: "column", paddingLeft: 2 }, expandedRows(view, theme))]
548
+ ])
549
+ ];
550
+ }
551
+ function viewKey(view) {
552
+ return JSON.stringify(view);
553
+ }
554
+ function readCount(value) {
555
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
556
+ }
557
+ async function readMemView(project, collapsed) {
558
+ const stats = await WorkerClient.getStats();
559
+ if (!stats) {
560
+ return {
561
+ healthy: false,
562
+ project,
563
+ observations: null,
564
+ sessions: null,
565
+ summaries: null,
566
+ queueDepth: null,
567
+ isProcessing: false,
568
+ recentSummaries: [],
569
+ recentObservations: []
570
+ };
571
+ }
572
+ const [recentSummaries, recentObservations] = collapsed ? [[], []] : await Promise.all([
573
+ WorkerClient.getRecentSummaries(project, RECENT_SUMMARIES),
574
+ WorkerClient.getRecentObservations(project, RECENT_OBSERVATIONS)
575
+ ]);
576
+ return {
577
+ healthy: true,
578
+ project,
579
+ observations: readCount(stats.database?.observations),
580
+ sessions: readCount(stats.database?.sessions),
581
+ summaries: readCount(stats.database?.summaries),
582
+ queueDepth: readCount(stats.processing?.queueDepth),
583
+ isProcessing: stats.processing?.isProcessing === true,
584
+ recentSummaries,
585
+ recentObservations
586
+ };
587
+ }
588
+
589
+ // node_modules/@opencode-ai/plugin-v2/dist/tui/plugin.js
590
+ function define(plugin) {
591
+ return plugin;
592
+ }
593
+
594
+ // src/cli.ts
595
+ var REFRESH_EVENTS = ["session.created", "session.execution.succeeded"];
596
+ function emptyView(project) {
597
+ return {
598
+ healthy: false,
599
+ project,
600
+ observations: null,
601
+ sessions: null,
602
+ summaries: null,
603
+ queueDepth: null,
604
+ isProcessing: false,
605
+ recentSummaries: [],
606
+ recentObservations: []
607
+ };
608
+ }
609
+ var cli_default = define({
610
+ id: "claude-mem.tui",
611
+ setup: async (ctx) => {
612
+ const solid = await import("@opentui/solid").catch(() => null);
613
+ if (!solid) {
614
+ return;
615
+ }
616
+ const project = String(ctx.location?.directory ?? process.cwd()).split(/[\\/]/).filter(Boolean).findLast(Boolean) ?? "unknown-project";
617
+ const [state, setState] = ctx.storage.memory("claude-mem.sidebar", {
618
+ initial: { collapsed: true, view: emptyView(project) }
619
+ });
620
+ let disposed = false;
621
+ let inFlight = false;
622
+ let refreshPending = false;
623
+ let timer = null;
624
+ const refresh = async () => {
625
+ if (disposed) {
626
+ return;
627
+ }
628
+ if (inFlight) {
629
+ refreshPending = true;
630
+ return;
631
+ }
632
+ inFlight = true;
633
+ try {
634
+ do {
635
+ refreshPending = false;
636
+ const { collapsed } = state;
637
+ const next = await readMemView(project, collapsed);
638
+ if (disposed) {
639
+ return;
640
+ }
641
+ if (collapsed === state.collapsed) {
642
+ setState((draft) => {
643
+ if (viewKey(draft.view) !== viewKey(next)) {
644
+ draft.view = next;
645
+ }
646
+ });
647
+ }
648
+ } while (refreshPending);
649
+ } catch {} finally {
650
+ inFlight = false;
651
+ }
652
+ };
653
+ const onToggle = () => {
654
+ if (disposed) {
655
+ return;
656
+ }
657
+ setState((draft) => {
658
+ draft.collapsed = !draft.collapsed;
659
+ });
660
+ refresh().catch(() => {});
661
+ };
662
+ const disposeSlot = ctx.ui.slot({
663
+ append: "sidebar.content",
664
+ render: () => materialize(() => buildMemNodes(state.view, themeFromResolved(ctx.theme), state.collapsed, onToggle), solid)
665
+ });
666
+ const disposeEvents = REFRESH_EVENTS.map((type) => ctx.data.on(type, () => {
667
+ refresh().catch(() => {});
668
+ }));
669
+ const schedule = () => {
670
+ timer = setTimeout(tick, POLL_INTERVAL_MS);
671
+ };
672
+ const tick = async () => {
673
+ if (disposed) {
674
+ return;
675
+ }
676
+ await refresh();
677
+ if (!disposed) {
678
+ schedule();
679
+ }
680
+ };
681
+ refresh().catch(() => {}).finally(() => {
682
+ if (disposed) {
683
+ return;
684
+ }
685
+ schedule();
686
+ if (!state.view.healthy) {
687
+ ctx.ui.toast.show({
688
+ title: "Claude-Mem",
689
+ message: "Worker offline \u2014 start Claude Code or run bunx claude-mem start",
690
+ variant: "warning",
691
+ duration: 5000
692
+ });
693
+ }
694
+ }).catch(() => {});
695
+ return () => {
696
+ if (disposed) {
697
+ return;
698
+ }
699
+ disposed = true;
700
+ if (timer) {
701
+ clearTimeout(timer);
702
+ }
703
+ disposeSlot();
704
+ for (const dispose of disposeEvents) {
705
+ dispose();
706
+ }
707
+ };
708
+ }
709
+ });
710
+ function themeFromResolved(resolved) {
711
+ const { text } = resolved;
712
+ return {
713
+ text: text.default,
714
+ textMuted: text.subdued,
715
+ info: text.action.primary.default,
716
+ success: text.feedback.success.default,
717
+ warning: text.feedback.warning.default,
718
+ error: text.feedback.error.default,
719
+ borderSubtle: resolved.border.default
720
+ };
721
+ }
722
+ function materializeNode(node, solid) {
723
+ const element = solid.createElement(node.kind);
724
+ for (const [name, value] of Object.entries(node.props)) {
725
+ solid.setProp(element, name, value);
726
+ }
727
+ if (node.kind === "text") {
728
+ solid.insert(element, node.text ?? "");
729
+ }
730
+ for (const child of node.children ?? []) {
731
+ solid.insert(element, materializeNode(child, solid));
732
+ }
733
+ return element;
734
+ }
735
+ function materialize(nodes, solid) {
736
+ const root = solid.createElement("box");
737
+ solid.setProp(root, "flexDirection", "column");
738
+ solid.insert(root, () => nodes().map((node) => materializeNode(node, solid)));
739
+ return root;
740
+ }
741
+ export {
742
+ cli_default as default
743
+ };