openclaw-mem 1.0.3 → 1.2.1
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/HOOK.md +125 -0
- package/LICENSE +1 -1
- package/MCP.json +11 -0
- package/README.md +140 -129
- package/context-builder.js +703 -0
- package/database.js +520 -0
- package/debug-logger.js +280 -0
- package/extractor.js +211 -0
- package/gateway-llm.js +155 -0
- package/handler.js +1122 -0
- package/mcp-http-api.js +356 -0
- package/mcp-server.js +525 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +53 -29
- package/realtime-monitor.js +371 -0
- package/session-watcher.js +192 -0
- package/setup.js +114 -0
- package/sync-recent.js +63 -0
- package/README_CN.md +0 -184
- package/bin/openclaw-mem.js +0 -117
- package/lib/context-builder.js +0 -415
- package/lib/database.js +0 -309
- package/lib/handler.js +0 -494
- package/scripts/commands.js +0 -141
- 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;
|
package/scripts/commands.js
DELETED
|
@@ -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
|
-
}
|