openclaw-mem 1.0.4 → 1.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.
Files changed (47) hide show
  1. package/HOOK.md +125 -0
  2. package/LICENSE +1 -1
  3. package/MCP.json +11 -0
  4. package/README.md +158 -167
  5. package/backfill-embeddings.js +79 -0
  6. package/context-builder.js +703 -0
  7. package/database.js +625 -0
  8. package/debug-logger.js +280 -0
  9. package/extractor.js +268 -0
  10. package/gateway-llm.js +250 -0
  11. package/handler.js +941 -0
  12. package/mcp-http-api.js +424 -0
  13. package/mcp-server.js +605 -0
  14. package/mem-get.sh +24 -0
  15. package/mem-search.sh +17 -0
  16. package/monitor.js +112 -0
  17. package/package.json +58 -30
  18. package/realtime-monitor.js +371 -0
  19. package/session-watcher.js +192 -0
  20. package/setup.js +114 -0
  21. package/sync-recent.js +63 -0
  22. package/README_CN.md +0 -201
  23. package/bin/openclaw-mem.js +0 -117
  24. package/docs/locales/README_AR.md +0 -35
  25. package/docs/locales/README_DE.md +0 -35
  26. package/docs/locales/README_ES.md +0 -35
  27. package/docs/locales/README_FR.md +0 -35
  28. package/docs/locales/README_HE.md +0 -35
  29. package/docs/locales/README_HI.md +0 -35
  30. package/docs/locales/README_ID.md +0 -35
  31. package/docs/locales/README_IT.md +0 -35
  32. package/docs/locales/README_JA.md +0 -57
  33. package/docs/locales/README_KO.md +0 -35
  34. package/docs/locales/README_NL.md +0 -35
  35. package/docs/locales/README_PL.md +0 -35
  36. package/docs/locales/README_PT.md +0 -35
  37. package/docs/locales/README_RU.md +0 -35
  38. package/docs/locales/README_TH.md +0 -35
  39. package/docs/locales/README_TR.md +0 -35
  40. package/docs/locales/README_UK.md +0 -35
  41. package/docs/locales/README_VI.md +0 -35
  42. package/docs/logo.svg +0 -32
  43. package/lib/context-builder.js +0 -415
  44. package/lib/database.js +0 -309
  45. package/lib/handler.js +0 -494
  46. package/scripts/commands.js +0 -141
  47. package/scripts/init.js +0 -248
package/lib/handler.js DELETED
@@ -1,494 +0,0 @@
1
- /**
2
- * OpenClaw-Mem Hook Handler
3
- *
4
- * Captures session content and provides memory context injection.
5
- *
6
- * Events handled:
7
- * - command:new - Save session content before reset
8
- * - gateway:startup - Initialize memory system
9
- * - agent:bootstrap - Inject historical context
10
- */
11
-
12
- import fs from 'node:fs/promises';
13
- import path from 'node:path';
14
- import os from 'node:os';
15
- import { fileURLToPath } from 'node:url';
16
-
17
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
-
19
- // Lazy load modules
20
- let database = null;
21
- let contextBuilder = null;
22
-
23
- async function loadModules() {
24
- if (database && contextBuilder) return true;
25
-
26
- try {
27
- const dbModule = await import('./database.js');
28
- const ctxModule = await import('./context-builder.js');
29
- database = dbModule.default || dbModule.database;
30
- contextBuilder = ctxModule.default || ctxModule;
31
- return true;
32
- } catch (err) {
33
- console.error('[openclaw-mem] Failed to load modules:', err.message);
34
- return false;
35
- }
36
- }
37
-
38
- // Generate UUID
39
- function generateId() {
40
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
41
- const r = Math.random() * 16 | 0;
42
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
43
- return v.toString(16);
44
- });
45
- }
46
-
47
- // Estimate tokens
48
- function estimateTokens(text) {
49
- if (!text) return 0;
50
- return Math.ceil(String(text).length / 4);
51
- }
52
-
53
- // Simple hash function for content deduplication
54
- function hashContent(text) {
55
- if (!text) return '';
56
- let hash = 0;
57
- for (let i = 0; i < text.length; i++) {
58
- const char = text.charCodeAt(i);
59
- hash = ((hash << 5) - hash) + char;
60
- hash = hash & hash; // Convert to 32bit integer
61
- }
62
- return hash.toString(16);
63
- }
64
-
65
- /**
66
- * Read session transcript and extract conversation
67
- */
68
- async function extractSessionContent(sessionFile, maxMessages = 20) {
69
- try {
70
- if (!sessionFile) return null;
71
-
72
- const content = await fs.readFile(sessionFile, 'utf-8');
73
- const lines = content.trim().split('\n');
74
-
75
- const messages = [];
76
- for (const line of lines) {
77
- try {
78
- const entry = JSON.parse(line);
79
- if (entry.type === 'message' && entry.message) {
80
- const msg = entry.message;
81
- if ((msg.role === 'user' || msg.role === 'assistant') && msg.content) {
82
- const text = Array.isArray(msg.content)
83
- ? msg.content.find(c => c.type === 'text')?.text
84
- : msg.content;
85
-
86
- if (text && !text.startsWith('/')) {
87
- messages.push({
88
- role: msg.role,
89
- content: text.slice(0, 500) // Truncate long messages
90
- });
91
- }
92
- }
93
- }
94
- } catch {
95
- // Skip invalid lines
96
- }
97
- }
98
-
99
- return messages.slice(-maxMessages);
100
- } catch (err) {
101
- console.error('[openclaw-mem] Failed to read session file:', err.message);
102
- return null;
103
- }
104
- }
105
-
106
- /**
107
- * Handle gateway:startup event
108
- */
109
- async function handleGatewayStartup(event) {
110
- console.log('[openclaw-mem] Gateway startup - initializing memory system');
111
-
112
- if (!await loadModules()) return;
113
-
114
- const stats = database.getStats();
115
- console.log(`[openclaw-mem] Memory stats: ${stats.total_sessions} sessions, ${stats.total_observations} observations`);
116
- }
117
-
118
- /**
119
- * Handle agent:bootstrap event
120
- * Inject historical context into new agent sessions
121
- * AND capture incoming message to database
122
- */
123
- async function handleAgentBootstrap(event) {
124
- console.log('[openclaw-mem] Agent bootstrap:', event.sessionKey);
125
- console.log('[openclaw-mem] Event keys:', Object.keys(event));
126
- console.log('[openclaw-mem] Context exists:', !!event.context);
127
-
128
- if (!await loadModules()) return;
129
-
130
- // IMPORTANT: Ensure event.context exists (modify event directly, not a local copy)
131
- if (!event.context) {
132
- console.log('[openclaw-mem] WARNING: event.context is missing, creating it');
133
- event.context = {};
134
- }
135
-
136
- console.log('[openclaw-mem] Context keys:', Object.keys(event.context));
137
-
138
- const workspaceDir = event.context.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
139
- const sessionKey = event.sessionKey || 'unknown';
140
-
141
- console.log('[openclaw-mem] workspaceDir:', workspaceDir);
142
- console.log('[openclaw-mem] bootstrapFiles exists:', !!event.context.bootstrapFiles);
143
- console.log('[openclaw-mem] bootstrapFiles is array:', Array.isArray(event.context.bootstrapFiles));
144
- console.log('[openclaw-mem] bootstrapFiles length before:', event.context.bootstrapFiles?.length);
145
- // Debug: show structure of first file
146
- if (event.context.bootstrapFiles?.[0]) {
147
- const sample = event.context.bootstrapFiles[0];
148
- console.log('[openclaw-mem] Sample file keys:', Object.keys(sample));
149
- console.log('[openclaw-mem] Sample file name:', sample.name);
150
- console.log('[openclaw-mem] Sample has content:', !!sample.content);
151
- }
152
-
153
- // ============ NEW: Capture incoming messages to database ============
154
- // This ensures every message through gateway is captured, not just on /new
155
- // Messages can be in: event.messages (array), event.message, or event.context.userMessage
156
- let messagesToCapture = [];
157
-
158
- // ============ Capture messages from session file ============
159
- // At bootstrap time, the incoming message isn't in the event yet
160
- // But we can read the session file which contains previous messages
161
-
162
- // Construct session file path from sessionKey
163
- // Session files are stored at ~/.openclaw/agents/main/sessions/<sessionKey>.jsonl
164
- const agentId = event.context?.agentId || 'main';
165
- const sessionFile = path.join(os.homedir(), '.openclaw', 'agents', agentId, 'sessions', `${sessionKey}.jsonl`);
166
- console.log('[openclaw-mem] Constructed session file path:', sessionFile);
167
-
168
- // Check if session file exists
169
- let sessionFileExists = false;
170
- try {
171
- await fs.access(sessionFile);
172
- sessionFileExists = true;
173
- } catch {
174
- sessionFileExists = false;
175
- }
176
-
177
- if (sessionFileExists) {
178
- console.log('[openclaw-mem] Found session file:', sessionFile);
179
- try {
180
- const messages = await extractSessionContent(sessionFile, 50);
181
- if (messages && messages.length > 0) {
182
- console.log(`[openclaw-mem] Found ${messages.length} messages in session file`);
183
-
184
- // Get or create session for this sessionKey
185
- let dbSessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
186
-
187
- // Track which messages we've already saved (to avoid duplicates)
188
- const savedHashes = new Set();
189
- try {
190
- const existing = database.getRecentObservations(null, 100);
191
- for (const obs of existing) {
192
- // Content is stored in the 'result' field as JSON
193
- try {
194
- const result = JSON.parse(obs.result || '{}');
195
- if (result.content) {
196
- savedHashes.add(hashContent(result.content));
197
- }
198
- } catch {
199
- // If result isn't JSON, use summary
200
- if (obs.summary) {
201
- savedHashes.add(hashContent(obs.summary));
202
- }
203
- }
204
- }
205
- console.log(`[openclaw-mem] Loaded ${savedHashes.size} existing message hashes`);
206
- } catch (e) {
207
- console.log('[openclaw-mem] Could not check existing observations:', e.message);
208
- }
209
-
210
- let newCount = 0;
211
- for (const msg of messages) {
212
- const contentHash = hashContent(msg.content);
213
- if (savedHashes.has(contentHash)) {
214
- continue; // Skip already saved messages
215
- }
216
-
217
- const toolName = msg.role === 'assistant' ? 'AssistantMessage' : 'UserMessage';
218
- const summary = msg.content.slice(0, 100) + (msg.content.length > 100 ? '...' : '');
219
- database.saveObservation(
220
- dbSessionId,
221
- toolName,
222
- { role: msg.role, sessionKey },
223
- { content: msg.content },
224
- {
225
- summary,
226
- concepts: msg.role,
227
- tokensDiscovery: estimateTokens(msg.content),
228
- tokensRead: estimateTokens(summary)
229
- }
230
- );
231
- savedHashes.add(contentHash);
232
- newCount++;
233
- }
234
-
235
- if (newCount > 0) {
236
- console.log(`[openclaw-mem] ✓ Saved ${newCount} new messages to database`);
237
- } else {
238
- console.log('[openclaw-mem] All messages already in database');
239
- }
240
- }
241
- } catch (err) {
242
- console.log('[openclaw-mem] Could not read session file:', err.message);
243
- }
244
- } else {
245
- console.log('[openclaw-mem] No session file found in context');
246
- }
247
- // ============ END: Capture messages ============
248
-
249
- // Build context to inject
250
- const memContext = contextBuilder.buildContext(workspaceDir, {
251
- observationLimit: 30,
252
- fullDetailCount: 3
253
- });
254
-
255
- if (memContext) {
256
- console.log(`[openclaw-mem] Built context: ${memContext.length} chars`);
257
-
258
- // Strategy: Write memory context to a dedicated file on disk
259
- // This ensures AI can read it with the Read tool
260
- const memContextFile = path.join(workspaceDir, 'SESSION-MEMORY.md');
261
- try {
262
- await fs.writeFile(memContextFile, memContext, 'utf-8');
263
- console.log(`[openclaw-mem] ✓ Written SESSION-MEMORY.md to disk (${memContext.length} chars)`);
264
- } catch (err) {
265
- console.error('[openclaw-mem] Failed to write SESSION-MEMORY.md:', err.message);
266
- }
267
-
268
- // Also modify bootstrapFiles array for system prompt injection
269
- if (event.context.bootstrapFiles && Array.isArray(event.context.bootstrapFiles)) {
270
- const memoryFile = event.context.bootstrapFiles.find(f => f.name === 'MEMORY.md');
271
- if (memoryFile && memoryFile.content && !memoryFile.missing) {
272
- memoryFile.content = memoryFile.content + '\n\n---\n\n# Session Memory\n\nSee SESSION-MEMORY.md for recent activity and conversation history.\n\n' + memContext;
273
- console.log('[openclaw-mem] ✓ Appended to MEMORY.md in bootstrapFiles');
274
- }
275
- }
276
- } else {
277
- console.log('[openclaw-mem] No context to inject (empty memory)');
278
- }
279
- }
280
-
281
- // Track active sessions by sessionKey
282
- const activeSessions = new Map();
283
-
284
- /**
285
- * Get or create a session ID for a given sessionKey
286
- */
287
- function getOrCreateSessionForKey(sessionKey, workspaceDir) {
288
- if (activeSessions.has(sessionKey)) {
289
- return activeSessions.get(sessionKey);
290
- }
291
-
292
- const sessionId = generateId();
293
- database.createSession(sessionId, workspaceDir, sessionKey, 'bootstrap');
294
- activeSessions.set(sessionKey, sessionId);
295
-
296
- // Clean up old sessions after 1 hour
297
- setTimeout(() => {
298
- if (activeSessions.get(sessionKey) === sessionId) {
299
- activeSessions.delete(sessionKey);
300
- database.endSession(sessionId);
301
- }
302
- }, 60 * 60 * 1000);
303
-
304
- console.log(`[openclaw-mem] Created new session ${sessionId} for key ${sessionKey}`);
305
- return sessionId;
306
- }
307
-
308
- /**
309
- * Handle command:new event
310
- * Save session content before reset
311
- */
312
- async function handleCommandNew(event) {
313
- console.log('[openclaw-mem] Command new - saving session');
314
-
315
- if (!await loadModules()) return;
316
-
317
- const context = event.context || {};
318
- const sessionKey = event.sessionKey || 'unknown';
319
- const sessionId = generateId();
320
-
321
- // Get workspace and session info
322
- const workspaceDir = context.workspaceDir ||
323
- context.cfg?.agents?.defaults?.workspace ||
324
- path.join(os.homedir(), '.openclaw', 'workspace');
325
-
326
- const sessionEntry = context.previousSessionEntry || context.sessionEntry || {};
327
- const sessionFile = sessionEntry.sessionFile;
328
-
329
- // Create session record
330
- database.createSession(sessionId, workspaceDir, sessionKey, context.commandSource || 'command');
331
-
332
- // Extract session content
333
- const messages = await extractSessionContent(sessionFile, 20);
334
-
335
- if (messages && messages.length > 0) {
336
- console.log(`[openclaw-mem] Extracted ${messages.length} messages from session`);
337
-
338
- // Save each message as an observation
339
- for (const msg of messages) {
340
- const toolName = msg.role === 'user' ? 'UserMessage' : 'AssistantMessage';
341
- const summary = msg.content.slice(0, 100) + (msg.content.length > 100 ? '...' : '');
342
-
343
- database.saveObservation(
344
- sessionId,
345
- toolName,
346
- { role: msg.role },
347
- { content: msg.content },
348
- {
349
- summary,
350
- concepts: msg.role,
351
- tokensDiscovery: estimateTokens(msg.content),
352
- tokensRead: estimateTokens(summary)
353
- }
354
- );
355
- }
356
-
357
- // Generate session summary
358
- const userMessages = messages.filter(m => m.role === 'user');
359
- const assistantMessages = messages.filter(m => m.role === 'assistant');
360
-
361
- const summaryContent = `Session with ${messages.length} messages (${userMessages.length} user, ${assistantMessages.length} assistant)`;
362
- const firstUserMsg = userMessages[0]?.content?.slice(0, 200) || '';
363
-
364
- database.saveSummary(
365
- sessionId,
366
- summaryContent,
367
- firstUserMsg,
368
- `Discussed: ${assistantMessages.slice(-1)[0]?.content?.slice(0, 100) || 'various topics'}`,
369
- null
370
- );
371
-
372
- console.log('[openclaw-mem] Session saved successfully');
373
- }
374
-
375
- // End session
376
- database.endSession(sessionId);
377
- }
378
-
379
- /**
380
- * Handle agent:response event
381
- * Capture assistant responses to database
382
- */
383
- async function handleAgentResponse(event) {
384
- console.log('[openclaw-mem] Agent response event');
385
-
386
- if (!await loadModules()) return;
387
-
388
- const sessionKey = event.sessionKey || 'unknown';
389
- const response = event.response || event.message || event.content;
390
- const workspaceDir = event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
391
-
392
- if (response && typeof response === 'string' && response.trim()) {
393
- console.log('[openclaw-mem] Capturing assistant response:', response.slice(0, 50) + '...');
394
-
395
- let sessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
396
-
397
- const summary = response.slice(0, 100) + (response.length > 100 ? '...' : '');
398
- database.saveObservation(
399
- sessionId,
400
- 'AssistantMessage',
401
- { role: 'assistant', sessionKey },
402
- { content: response },
403
- {
404
- summary,
405
- concepts: 'assistant',
406
- tokensDiscovery: estimateTokens(response),
407
- tokensRead: estimateTokens(summary)
408
- }
409
- );
410
- console.log('[openclaw-mem] ✓ Assistant response saved to database');
411
- }
412
- }
413
-
414
- /**
415
- * Handle message events
416
- * Alternative event type for capturing messages
417
- */
418
- async function handleMessage(event) {
419
- console.log('[openclaw-mem] Message event:', event.action || 'unknown');
420
-
421
- if (!await loadModules()) return;
422
-
423
- const sessionKey = event.sessionKey || 'unknown';
424
- const message = event.message || event.content || event.text;
425
- const role = event.role || event.action || 'user';
426
- const workspaceDir = event.context?.workspaceDir || path.join(os.homedir(), '.openclaw', 'workspace');
427
-
428
- if (message && typeof message === 'string' && message.trim() && !message.startsWith('/')) {
429
- console.log(`[openclaw-mem] Capturing ${role} message:`, message.slice(0, 50) + '...');
430
-
431
- let sessionId = getOrCreateSessionForKey(sessionKey, workspaceDir);
432
-
433
- const toolName = role === 'assistant' ? 'AssistantMessage' : 'UserMessage';
434
- const summary = message.slice(0, 100) + (message.length > 100 ? '...' : '');
435
- database.saveObservation(
436
- sessionId,
437
- toolName,
438
- { role, sessionKey },
439
- { content: message },
440
- {
441
- summary,
442
- concepts: role,
443
- tokensDiscovery: estimateTokens(message),
444
- tokensRead: estimateTokens(summary)
445
- }
446
- );
447
- console.log(`[openclaw-mem] ✓ ${role} message saved to database`);
448
- }
449
- }
450
-
451
- /**
452
- * Main hook handler
453
- */
454
- const openclawMemHandler = async (event) => {
455
- const eventType = event.type;
456
- const eventAction = event.action;
457
-
458
- console.log('[openclaw-mem] Event:', eventType, eventAction || '');
459
-
460
- try {
461
- if (eventType === 'gateway' && eventAction === 'startup') {
462
- await handleGatewayStartup(event);
463
- return;
464
- }
465
-
466
- if (eventType === 'agent' && eventAction === 'bootstrap') {
467
- await handleAgentBootstrap(event);
468
- return;
469
- }
470
-
471
- if (eventType === 'command' && eventAction === 'new') {
472
- await handleCommandNew(event);
473
- return;
474
- }
475
-
476
- // Handle agent response to capture assistant messages
477
- if (eventType === 'agent' && eventAction === 'response') {
478
- await handleAgentResponse(event);
479
- return;
480
- }
481
-
482
- // Handle message events (alternative event type)
483
- if (eventType === 'message') {
484
- await handleMessage(event);
485
- return;
486
- }
487
-
488
- } catch (err) {
489
- console.error('[openclaw-mem] Handler error:', err.message);
490
- console.error(err.stack);
491
- }
492
- };
493
-
494
- export default openclawMemHandler;
@@ -1,141 +0,0 @@
1
- /**
2
- * OpenClaw-Mem CLI Commands
3
- */
4
-
5
- import path from 'node:path';
6
- import os from 'node:os';
7
- import fs from 'node:fs';
8
-
9
- // Colors
10
- const c = {
11
- reset: '\x1b[0m',
12
- green: '\x1b[32m',
13
- red: '\x1b[31m',
14
- yellow: '\x1b[33m',
15
- blue: '\x1b[34m',
16
- cyan: '\x1b[36m',
17
- bold: '\x1b[1m',
18
- dim: '\x1b[2m'
19
- };
20
-
21
- function log(msg, color = 'reset') {
22
- console.log(`${c[color]}${msg}${c.reset}`);
23
- }
24
-
25
- async function loadDatabase() {
26
- // Try multiple locations for database module
27
- const locations = [
28
- path.join(os.homedir(), '.openclaw', 'hooks', 'openclaw-mem', 'database.js'),
29
- new URL('../lib/database.js', import.meta.url).pathname
30
- ];
31
-
32
- for (const dbPath of locations) {
33
- if (fs.existsSync(dbPath)) {
34
- const db = await import(dbPath);
35
- return db.default || db;
36
- }
37
- }
38
-
39
- // Try to import directly (for development)
40
- try {
41
- const db = await import('../lib/database.js');
42
- return db.default || db;
43
- } catch {
44
- throw new Error('OpenClaw-Mem not installed. Run: npx openclaw-mem init');
45
- }
46
- }
47
-
48
- export async function status() {
49
- console.log(`
50
- ${c.cyan}╔══════════════════════════════════════════════════════════╗
51
- ║ OpenClaw-Mem Status ║
52
- ╚══════════════════════════════════════════════════════════╝${c.reset}
53
- `);
54
-
55
- try {
56
- const database = await loadDatabase();
57
- const stats = database.getStats();
58
-
59
- console.log(`${c.bold}Database Statistics:${c.reset}`);
60
- console.log(` Sessions: ${c.green}${stats.total_sessions || 0}${c.reset}`);
61
- console.log(` Observations: ${c.green}${stats.total_observations || 0}${c.reset}`);
62
- console.log(` Summaries: ${c.green}${stats.total_summaries || 0}${c.reset}`);
63
- console.log('');
64
- console.log(`${c.bold}Token Economics:${c.reset}`);
65
- console.log(` Discovery: ${stats.total_discovery_tokens || 0} tokens`);
66
- console.log(` Read: ${stats.total_read_tokens || 0} tokens`);
67
-
68
- const saved = (stats.total_discovery_tokens || 0) - (stats.total_read_tokens || 0);
69
- const savedPct = stats.total_discovery_tokens
70
- ? Math.round((saved / stats.total_discovery_tokens) * 100)
71
- : 0;
72
- console.log(` ${c.green}Saved: ${saved} tokens (${savedPct}%)${c.reset}`);
73
-
74
- // Show recent activity
75
- console.log('');
76
- console.log(`${c.bold}Recent Activity:${c.reset}`);
77
-
78
- const recent = database.getRecentObservations(null, 5);
79
- if (recent.length === 0) {
80
- console.log(` ${c.dim}No observations yet. Start chatting!${c.reset}`);
81
- } else {
82
- for (const obs of recent) {
83
- const time = new Date(obs.timestamp).toLocaleString();
84
- const summary = obs.summary?.slice(0, 50) || '(no summary)';
85
- console.log(` ${c.dim}${time}${c.reset} ${summary}${obs.summary?.length > 50 ? '...' : ''}`);
86
- }
87
- }
88
-
89
- console.log('');
90
- console.log(`${c.dim}Database: ~/.openclaw-mem/memory.db${c.reset}`);
91
-
92
- } catch (err) {
93
- log(`Error: ${err.message}`, 'red');
94
- process.exit(1);
95
- }
96
- }
97
-
98
- export async function search(query) {
99
- console.log(`
100
- ${c.cyan}Searching for: "${query}"${c.reset}
101
- `);
102
-
103
- try {
104
- const database = await loadDatabase();
105
- const results = database.searchObservations(query, 10);
106
-
107
- if (results.length === 0) {
108
- log('No results found.', 'yellow');
109
- console.log(`${c.dim}Try a different search term or check your spelling.${c.reset}`);
110
- return;
111
- }
112
-
113
- console.log(`${c.green}Found ${results.length} result(s):${c.reset}`);
114
- console.log('');
115
-
116
- for (const r of results) {
117
- const time = new Date(r.timestamp).toLocaleString();
118
- console.log(`${c.bold}#${r.id}${c.reset} ${c.dim}(${time})${c.reset}`);
119
- console.log(` ${c.cyan}${r.tool_name}${c.reset}`);
120
-
121
- // Show summary with highlighting
122
- let summary = r.summary || '(no summary)';
123
- // Simple highlight - wrap query terms in color codes
124
- const terms = query.toLowerCase().split(/\s+/);
125
- for (const term of terms) {
126
- if (term.length > 2) {
127
- const regex = new RegExp(`(${term})`, 'gi');
128
- summary = summary.replace(regex, `${c.yellow}$1${c.reset}`);
129
- }
130
- }
131
- console.log(` ${summary}`);
132
- console.log('');
133
- }
134
-
135
- console.log(`${c.dim}Use "/memory get <id>" in chat for full details.${c.reset}`);
136
-
137
- } catch (err) {
138
- log(`Error: ${err.message}`, 'red');
139
- process.exit(1);
140
- }
141
- }