@ephemushroom/opencode-claude-mem 0.6.1 → 0.7.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 CHANGED
@@ -3,55 +3,16 @@
3
3
  import { existsSync, readFileSync } from "fs";
4
4
  import { homedir } from "os";
5
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
6
  var DEFAULT_WORKER_HOST = "127.0.0.1";
52
7
  var DEFAULT_WORKER_PORT = 37777;
53
- var PLATFORM_SOURCE = "opencode";
54
8
  var SETTINGS_FILE = join(homedir(), ".claude-mem", "settings.json");
9
+ var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
10
+ var PROJECT_COUNTS_SQL = `SELECT
11
+ (SELECT COUNT(*) FROM observations
12
+ WHERE project = ?1 COLLATE NOCASE OR merged_into_project = ?1 COLLATE NOCASE) AS observations,
13
+ (SELECT COUNT(*) FROM session_summaries
14
+ WHERE project = ?1 COLLATE NOCASE OR merged_into_project = ?1 COLLATE NOCASE) AS summaries,
15
+ (SELECT COUNT(*) FROM sdk_sessions WHERE project = ?1 COLLATE NOCASE) AS sessions`;
55
16
  function isRecord(value) {
56
17
  return typeof value === "object" && value !== null && !Array.isArray(value);
57
18
  }
@@ -87,26 +48,43 @@ function parseWorkerEndpoint(env, rawSettings) {
87
48
  const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
88
49
  return { host, port };
89
50
  }
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));
51
+ function resolveDatabasePath(env, rawSettings, home) {
52
+ if (!LOOPBACK_HOSTS.has(parseWorkerEndpoint(env, rawSettings).host)) {
53
+ return null;
54
+ }
55
+ const settings = parseSettings(rawSettings);
56
+ const dir = readString(env["CLAUDE_MEM_DATA_DIR"]) ?? readString(settings["CLAUDE_MEM_DATA_DIR"]) ?? join(home, ".claude-mem");
57
+ return join(dir.replace(/^~(?=$|[\\/])/, home), "claude-mem.db");
58
+ }
59
+ function readCount(value) {
60
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
61
+ }
62
+ async function readProjectCounts(path, project) {
63
+ if (!existsSync(path)) {
64
+ return null;
65
+ }
66
+ try {
67
+ const { Database } = await import("bun:sqlite");
68
+ const db = new Database(path, { readonly: true });
69
+ try {
70
+ const row = db.query(PROJECT_COUNTS_SQL).get(project);
71
+ if (!isRecord(row)) {
72
+ return null;
73
+ }
74
+ const observations = readCount(row["observations"]);
75
+ const summaries = readCount(row["summaries"]);
76
+ const sessions = readCount(row["sessions"]);
77
+ return observations === null || summaries === null || sessions === null ? null : { observations, summaries, sessions };
78
+ } finally {
79
+ db.close();
107
80
  }
81
+ } catch {
82
+ return null;
108
83
  }
109
- return params;
84
+ }
85
+ function readEpoch(item) {
86
+ const at = item["created_at_epoch"];
87
+ return typeof at === "number" && Number.isFinite(at) ? at : null;
110
88
  }
111
89
  function readSettingsRaw() {
112
90
  try {
@@ -119,253 +97,12 @@ function getWorkerBaseUrl() {
119
97
  const endpoint = parseWorkerEndpoint(process.env, readSettingsRaw());
120
98
  return `http://${endpoint.host}:${endpoint.port}`;
121
99
  }
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
100
 
147
101
  class WorkerClient {
148
102
  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
- }
103
+ static DATABASE_PATH = resolveDatabasePath(process.env, readSettingsRaw(), homedir());
104
+ static getProjectCounts(project) {
105
+ return this.DATABASE_PATH ? readProjectCounts(this.DATABASE_PATH, project) : Promise.resolve(null);
369
106
  }
370
107
  static async getStats() {
371
108
  try {
@@ -399,7 +136,7 @@ class WorkerClient {
399
136
  return [];
400
137
  }
401
138
  const request = typeof item["request"] === "string" ? item["request"].trim() : "";
402
- return request ? [{ id: item["id"], request }] : [];
139
+ return request ? [{ id: item["id"], request, at: readEpoch(item) }] : [];
403
140
  });
404
141
  } catch {
405
142
  return [];
@@ -422,34 +159,29 @@ class WorkerClient {
422
159
  }
423
160
  const title = typeof item["title"] === "string" ? item["title"].trim() : "";
424
161
  const type = typeof item["type"] === "string" ? item["type"] : "";
425
- return title ? [{ id: item["id"], type, title }] : [];
162
+ return title ? [{ id: item["id"], type, title, at: readEpoch(item) }] : [];
426
163
  });
427
164
  } catch {
428
165
  return [];
429
166
  }
430
167
  }
431
- static async search(options) {
168
+ static async getDetail(kind, id) {
432
169
  try {
433
- const params = buildSearchParams(options);
434
- const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
170
+ const path = kind === "observation" ? "observation" : "session";
171
+ const response = await fetch(`${this.BASE_URL}/api/${path}/${id}`);
435
172
  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()) || "";
173
+ return null;
441
174
  }
442
- const text = await response.text();
443
- return text.trim();
175
+ const payload = await response.json();
176
+ return isRecord(payload) ? payload : null;
444
177
  } catch {
445
- return "";
178
+ return null;
446
179
  }
447
180
  }
448
181
  }
449
182
 
450
183
  // src/sidebar-model.ts
451
184
  var POLL_INTERVAL_MS = 5000;
452
- var LABEL_MAX = 40;
453
185
  var RECENT_SUMMARIES = 3;
454
186
  var RECENT_OBSERVATIONS = 3;
455
187
  var OBSERVATION_TYPE_ICONS = {
@@ -478,9 +210,37 @@ var HTML_ENTITIES = {
478
210
  function decodeEntities(value) {
479
211
  return value.replace(/&(?:lt|gt|quot|#39|amp);/g, (entity) => HTML_ENTITIES[entity] ?? entity);
480
212
  }
481
- function truncate(value) {
213
+ function stripProjectName(value, project) {
482
214
  const decoded = decodeEntities(value);
483
- return decoded.length <= LABEL_MAX ? decoded : `${decoded.slice(0, LABEL_MAX - 1)}\u2026`;
215
+ const name = project.trim();
216
+ if (!name) {
217
+ return decoded;
218
+ }
219
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
220
+ const stripped = decoded.replace(new RegExp(`(?<![\\w.@-])(?:[\\w.@-]+/)?${escaped}(?![\\w.-])`, "giu"), " ").replaceAll(/\s{2,}/g, " ").replaceAll(/\s+([\uFF0C\u3002\u3001\uFF1B\uFF1A,.:;])/g, "$1").replace(/^[\s:\uFF1A,\uFF0C\u3001;\uFF1B\u00B7|\u2013\u2014-]+/u, "").trim();
221
+ return stripped || decoded;
222
+ }
223
+ function formatAge(at, now) {
224
+ if (at === null || !Number.isFinite(at)) {
225
+ return "";
226
+ }
227
+ const minutes = Math.floor(Math.max(0, now - at) / 60000);
228
+ if (minutes < 1) {
229
+ return "just now";
230
+ }
231
+ if (minutes < 60) {
232
+ return `${minutes}m ago`;
233
+ }
234
+ const hours = Math.floor(minutes / 60);
235
+ if (hours < 24) {
236
+ return `${hours}h ago`;
237
+ }
238
+ const days = Math.floor(hours / 24);
239
+ if (days < 7) {
240
+ return `${days}d ago`;
241
+ }
242
+ const date = new Date(at);
243
+ return `${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
484
244
  }
485
245
  function formatCount(value) {
486
246
  if (value === null) {
@@ -503,68 +263,90 @@ function toneColor(tone, theme) {
503
263
  }
504
264
  return tone === "warn" ? theme.warning : theme.textMuted;
505
265
  }
506
- function bulletRow(icon, iconColor, label, theme) {
266
+ function label(color, value) {
267
+ return text({ fg: color, wrapMode: "none", truncate: true }, value);
268
+ }
269
+ function bulletRow(icon, iconColor, value, theme, labelColor = theme.text) {
507
270
  return box({ flexDirection: "row", gap: 1 }, [
508
271
  text({ fg: iconColor, flexShrink: 0 }, icon),
509
- text({ fg: theme.text, wrapMode: "none" }, truncate(label))
272
+ label(labelColor, decodeEntities(value))
273
+ ]);
274
+ }
275
+ function recentRow(icon, iconColor, title, age, theme, onSelect) {
276
+ return box({ flexDirection: "column", ...onSelect ? { onMouseDown: onSelect } : {} }, [
277
+ bulletRow(icon, iconColor, title, theme),
278
+ ...age ? [box({ paddingLeft: 2 }, [label(theme.textMuted, age)])] : []
510
279
  ]);
511
280
  }
512
- function expandedRows(view, theme) {
281
+ function expandedRows(view, theme, onSelect) {
513
282
  if (!view.healthy) {
514
283
  return [bulletRow("\u2022", theme.error, "worker offline", theme)];
515
284
  }
285
+ const counts = `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`;
516
286
  const rows = [
517
- bulletRow("\u2022", theme.success, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`, theme)
287
+ bulletRow("\u2022", theme.success, view.scope === "all" ? `all \xB7 ${counts}` : counts, theme)
518
288
  ];
289
+ if (view.scope === "project") {
290
+ rows.push(bulletRow("\u2022", theme.textMuted, `all ${formatCount(view.totalObservations)} obs`, theme, theme.textMuted));
291
+ }
519
292
  if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
520
293
  rows.push(bulletRow("\u2022", theme.warning, `processing \xB7 queue ${view.queueDepth ?? 0}`, theme));
521
294
  }
522
295
  if (view.recentSummaries.length > 0) {
523
296
  rows.push(text({ fg: theme.textMuted }, "Recent sessions"));
524
297
  for (const summary of view.recentSummaries) {
525
- rows.push(bulletRow("\u2022", theme.textMuted, summary.request, theme));
298
+ rows.push(recentRow("\u2022", theme.textMuted, stripProjectName(summary.request, view.project), formatAge(summary.at, view.asOf), theme, onSelect && (() => onSelect("summary", summary.id))));
526
299
  }
527
300
  }
528
301
  if (view.recentObservations.length > 0) {
529
302
  rows.push(text({ fg: theme.textMuted }, "Latest"));
530
303
  for (const observation of view.recentObservations) {
531
304
  const icon = OBSERVATION_TYPE_ICONS[observation.type] ?? "\u25CB";
532
- rows.push(bulletRow(icon, theme.info, observation.title, theme));
305
+ rows.push(recentRow(icon, theme.info, stripProjectName(observation.title, view.project), [formatAge(observation.at, view.asOf), `#${observation.id}`].filter(Boolean).join(" \xB7 "), theme, onSelect && (() => onSelect("observation", observation.id))));
533
306
  }
534
307
  }
308
+ if (onSelect && view.recentSummaries.length + view.recentObservations.length > 0) {
309
+ rows.push(label(theme.textMuted, "Click an item for details"));
310
+ }
535
311
  return rows;
536
312
  }
537
- function buildMemNodes(view, theme, collapsed, onToggle) {
313
+ function buildMemNodes(view, theme, collapsed, onToggle, onSelect) {
538
314
  const summary = summaryLine(view);
539
315
  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)] : []
316
+ text({ fg: theme.text, flexShrink: 0 }, collapsed ? "\u25B6" : "\u25BC"),
317
+ text({ fg: theme.info, attributes: 1, flexShrink: 0 }, "Memory"),
318
+ collapsed ? text({ fg: toneColor(summary.tone, theme) }, summary.label) : label(theme.textMuted, view.project)
543
319
  ]);
544
320
  return [
545
321
  box({ flexDirection: "column", paddingTop: 1 }, [
546
322
  header,
547
- ...collapsed ? [] : [box({ flexDirection: "column", paddingLeft: 2 }, expandedRows(view, theme))]
323
+ ...collapsed ? [] : [box({ flexDirection: "column", paddingLeft: 2 }, expandedRows(view, theme, onSelect))]
548
324
  ])
549
325
  ];
550
326
  }
551
327
  function viewKey(view) {
552
328
  return JSON.stringify(view);
553
329
  }
554
- function readCount(value) {
330
+ function readCount2(value) {
555
331
  return typeof value === "number" && Number.isFinite(value) ? value : null;
556
332
  }
557
333
  async function readMemView(project, collapsed) {
558
- const stats = await WorkerClient.getStats();
334
+ const [stats, counts] = await Promise.all([
335
+ WorkerClient.getStats(),
336
+ WorkerClient.getProjectCounts(project)
337
+ ]);
559
338
  if (!stats) {
560
339
  return {
561
340
  healthy: false,
562
341
  project,
342
+ scope: "all",
343
+ totalObservations: null,
563
344
  observations: null,
564
345
  sessions: null,
565
346
  summaries: null,
566
347
  queueDepth: null,
567
348
  isProcessing: false,
349
+ asOf: 0,
568
350
  recentSummaries: [],
569
351
  recentObservations: []
570
352
  };
@@ -573,18 +355,84 @@ async function readMemView(project, collapsed) {
573
355
  WorkerClient.getRecentSummaries(project, RECENT_SUMMARIES),
574
356
  WorkerClient.getRecentObservations(project, RECENT_OBSERVATIONS)
575
357
  ]);
358
+ const totalObservations = readCount2(stats.database?.observations);
576
359
  return {
577
360
  healthy: true,
578
361
  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),
362
+ scope: counts ? "project" : "all",
363
+ totalObservations,
364
+ observations: counts ? counts.observations : totalObservations,
365
+ sessions: counts ? counts.sessions : readCount2(stats.database?.sessions),
366
+ summaries: counts ? counts.summaries : readCount2(stats.database?.summaries),
367
+ queueDepth: readCount2(stats.processing?.queueDepth),
583
368
  isProcessing: stats.processing?.isProcessing === true,
369
+ asOf: Math.floor(Date.now() / 60000) * 60000,
584
370
  recentSummaries,
585
371
  recentObservations
586
372
  };
587
373
  }
374
+ function field(record, key) {
375
+ const value = record[key];
376
+ return typeof value === "string" ? decodeEntities(value).trim() : "";
377
+ }
378
+ function listLines(value) {
379
+ const items = [...value.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((match) => match[1] ?? "");
380
+ if (items.length > 0) {
381
+ return items.map((item) => `\u2022 ${item.trim()}`);
382
+ }
383
+ if (value.startsWith("[")) {
384
+ try {
385
+ const parsed = JSON.parse(value);
386
+ if (Array.isArray(parsed)) {
387
+ return parsed.filter((item) => typeof item === "string").map((item) => `\u2022 ${item}`);
388
+ }
389
+ } catch {}
390
+ }
391
+ return value ? [value] : [];
392
+ }
393
+ function section(heading, lines) {
394
+ return lines.length > 0 ? ["", heading, ...lines] : [];
395
+ }
396
+ function formatTime(record) {
397
+ const at = record["created_at_epoch"];
398
+ return typeof at === "number" && Number.isFinite(at) ? new Date(at).toLocaleString() : "";
399
+ }
400
+ function describeDetail(kind, id, record) {
401
+ if (!record) {
402
+ return null;
403
+ }
404
+ const meta = [kind === "observation" ? field(record, "type") : "session", formatTime(record)];
405
+ if (kind === "observation") {
406
+ return {
407
+ title: `#${id} ${field(record, "title")}`.trim(),
408
+ message: [
409
+ field(record, "subtitle"),
410
+ ...section("Narrative", listLines(field(record, "narrative"))),
411
+ ...section("Facts", listLines(field(record, "facts"))),
412
+ ...section("Files modified", listLines(field(record, "files_modified"))),
413
+ "",
414
+ [...meta, `get_observations([${id}])`].filter(Boolean).join(" \xB7 ")
415
+ ].join(`
416
+ `).trim()
417
+ };
418
+ }
419
+ return {
420
+ title: field(record, "request") || `Session #${id}`,
421
+ message: [
422
+ ...section("Investigated", listLines(field(record, "investigated"))),
423
+ ...section("Learned", listLines(field(record, "learned"))),
424
+ ...section("Completed", listLines(field(record, "completed"))),
425
+ ...section("Next steps", listLines(field(record, "next_steps"))),
426
+ ...section("Notes", listLines(field(record, "notes"))),
427
+ "",
428
+ [...meta, `#${id}`].filter(Boolean).join(" \xB7 ")
429
+ ].join(`
430
+ `).trim()
431
+ };
432
+ }
433
+ async function readDetail(kind, id) {
434
+ return describeDetail(kind, id, await WorkerClient.getDetail(kind, id));
435
+ }
588
436
 
589
437
  // node_modules/@opencode/plugin/dist/tui/plugin.js
590
438
  function define(plugin) {
@@ -599,11 +447,14 @@ function emptyView(project) {
599
447
  return {
600
448
  healthy: false,
601
449
  project,
450
+ scope: "all",
451
+ totalObservations: null,
602
452
  observations: null,
603
453
  sessions: null,
604
454
  summaries: null,
605
455
  queueDepth: null,
606
456
  isProcessing: false,
457
+ asOf: 0,
607
458
  recentSummaries: [],
608
459
  recentObservations: []
609
460
  };
@@ -661,9 +512,33 @@ var cli_default = define({
661
512
  });
662
513
  refresh().catch(() => {});
663
514
  };
515
+ let opening = false;
516
+ const onSelect = (kind, id) => {
517
+ if (disposed || opening) {
518
+ return;
519
+ }
520
+ opening = true;
521
+ readDetail(kind, id).then((detail) => {
522
+ if (disposed) {
523
+ return;
524
+ }
525
+ if (!detail) {
526
+ ctx.ui.toast.show({
527
+ title: "Claude-Mem",
528
+ message: `Could not load ${kind} #${id} from the Worker`,
529
+ variant: "warning",
530
+ duration: 3000
531
+ });
532
+ return;
533
+ }
534
+ return ctx.ui.dialog.alert(detail);
535
+ }).catch(() => {}).finally(() => {
536
+ opening = false;
537
+ });
538
+ };
664
539
  const disposeSlot = ctx.ui.slot({
665
540
  append: "sidebar.content",
666
- render: () => materialize(() => buildMemNodes(state.view, themeFromResolved(ctx.theme), state.collapsed, onToggle), solid)
541
+ render: () => materialize(() => buildMemNodes(state.view, themeFromResolved(ctx.theme), state.collapsed, onToggle, onSelect), solid)
667
542
  });
668
543
  const disposeEvents = REFRESH_EVENTS.map((type) => ctx.data.on(type, () => {
669
544
  refresh().catch(() => {});