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
@@ -1,35 +0,0 @@
1
- # OpenClaw-Mem 🧠
2
-
3
- > Cung cấp bộ nhớ dài hạn liên tục cho tác nhân AI OpenClaw của bạn
4
-
5
- [![npm version](https://img.shields.io/npm/v/openclaw-mem.svg)](https://www.npmjs.com/package/openclaw-mem)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- [English](../../README.md) | [中文](../../README_CN.md) | Tiếng Việt
9
-
10
- OpenClaw-Mem tự động ghi lại các cuộc hội thoại của bạn và làm cho chúng có thể tìm kiếm được, cho phép trợ lý AI của bạn nhớ những gì bạn đã thảo luận giữa các phiên.
11
-
12
- ## ✨ Tính năng
13
-
14
- - **🔄 Tự động ghi nhớ** - Các cuộc hội thoại được lưu tự động
15
- - **🔍 Tìm kiếm toàn văn** - Tìm kiếm trong toàn bộ lịch sử hội thoại
16
- - **📊 Tiết lộ dần dần** - Sử dụng token hiệu quả với ngữ cảnh phân lớp
17
- - **🎯 Phát hiện chủ đề** - Tự động lập chỉ mục các cuộc thảo luận theo chủ đề
18
- - **💾 Lưu trữ cục bộ** - Tất cả dữ liệu nằm trên máy của bạn (SQLite)
19
- - **⚡ Không cần cấu hình** - Hoạt động ngay lập tức
20
-
21
- ## 🚀 Bắt đầu nhanh
22
-
23
- ```bash
24
- # Cài đặt và thiết lập (một lệnh!)
25
- npx openclaw-mem init
26
-
27
- # Khởi động lại cổng OpenClaw
28
- openclaw gateway restart
29
- ```
30
-
31
- Vậy là xong! Bắt đầu trò chuyện và các cuộc hội thoại của bạn sẽ được ghi nhớ.
32
-
33
- ## 📄 Giấy phép
34
-
35
- Giấy phép MIT - xem [LICENSE](../../LICENSE) để biết chi tiết
package/docs/logo.svg DELETED
@@ -1,32 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 60">
2
- <defs>
3
- <linearGradient id="brainGrad" x1="0%" y1="0%" x2="100%" y2="100%">
4
- <stop offset="0%" style="stop-color:#667eea"/>
5
- <stop offset="100%" style="stop-color:#764ba2"/>
6
- </linearGradient>
7
- <linearGradient id="textGrad" x1="0%" y1="0%" x2="100%" y2="0%">
8
- <stop offset="0%" style="stop-color:#667eea"/>
9
- <stop offset="50%" style="stop-color:#764ba2"/>
10
- <stop offset="100%" style="stop-color:#f093fb"/>
11
- </linearGradient>
12
- </defs>
13
-
14
- <!-- Brain icon -->
15
- <g transform="translate(5, 8)">
16
- <!-- Brain outline -->
17
- <path d="M22 8c-4 0-7 3-7 7 0 1.5.5 3 1.5 4-2 1-3.5 3-3.5 5.5 0 3.5 3 6.5 6.5 6.5h1c0 2 1.5 4 4 4s4-2 4-4h1c3.5 0 6.5-3 6.5-6.5 0-2.5-1.5-4.5-3.5-5.5 1-1 1.5-2.5 1.5-4 0-4-3-7-7-7h-4z"
18
- fill="url(#brainGrad)" opacity="0.9"/>
19
- <!-- Brain details -->
20
- <path d="M20 15c0 0 2 2 4 0M24 15c0 0 2 2 4 0M18 22h8M22 18v8"
21
- stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.6"/>
22
- <!-- Neural connections -->
23
- <circle cx="19" cy="12" r="1.5" fill="#fff" opacity="0.8"/>
24
- <circle cx="25" cy="12" r="1.5" fill="#fff" opacity="0.8"/>
25
- <circle cx="22" cy="28" r="1.5" fill="#fff" opacity="0.8"/>
26
- </g>
27
-
28
- <!-- Text -->
29
- <text x="52" y="38" font-family="'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif" font-size="26" font-weight="600" fill="url(#textGrad)">
30
- openclaw-mem
31
- </text>
32
- </svg>
@@ -1,415 +0,0 @@
1
- /**
2
- * OpenClaw-Mem Context Builder
3
- * Generates context to inject into new sessions using progressive disclosure
4
- */
5
-
6
- import database from './database.js';
7
-
8
- // Token estimation (4 chars ≈ 1 token)
9
- const CHARS_PER_TOKEN = 4;
10
-
11
- function estimateTokens(text) {
12
- if (!text) return 0;
13
- return Math.ceil(String(text).length / CHARS_PER_TOKEN);
14
- }
15
-
16
- // Type emoji mapping
17
- const TYPE_EMOJI = {
18
- 'Edit': '📝',
19
- 'Write': '✏️',
20
- 'Read': '📖',
21
- 'Bash': '💻',
22
- 'Grep': '🔍',
23
- 'Glob': '📁',
24
- 'WebFetch': '🌐',
25
- 'WebSearch': '🔎',
26
- 'Task': '🤖',
27
- 'default': '🔵'
28
- };
29
-
30
- function getTypeEmoji(toolName) {
31
- return TYPE_EMOJI[toolName] || TYPE_EMOJI.default;
32
- }
33
-
34
- // Format timestamp
35
- function formatTime(timestamp) {
36
- if (!timestamp) return '';
37
- const date = new Date(timestamp);
38
- return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
39
- }
40
-
41
- function formatDate(timestamp) {
42
- if (!timestamp) return '';
43
- const date = new Date(timestamp);
44
- return date.toISOString().split('T')[0];
45
- }
46
-
47
- // Filter out low-value observations (like recall test queries)
48
- function filterHighValueObservations(observations) {
49
- const lowValuePatterns = [
50
- '请查看 SESSION-MEMORY',
51
- 'SESSION-MEMORY.md 里没有',
52
- '请查看 SESSION-MEMORY.md,告诉我',
53
- '记忆检索当前不可用',
54
- '/memory search',
55
- '/memory get',
56
- '之前没有记录到',
57
- '没有任何关于'
58
- ];
59
-
60
- return observations.filter(obs => {
61
- const summary = obs.summary || '';
62
- // Always filter out observations matching low-value patterns
63
- const isLowValue = lowValuePatterns.some(pattern => summary.includes(pattern));
64
- if (isLowValue) return false;
65
-
66
- // Keep observations that have actual content (not just metadata)
67
- return true;
68
- });
69
- }
70
-
71
- // Group observations by date
72
- function groupByDate(observations) {
73
- const groups = {};
74
- for (const obs of observations) {
75
- const date = formatDate(obs.timestamp);
76
- if (!groups[date]) {
77
- groups[date] = [];
78
- }
79
- groups[date].push(obs);
80
- }
81
- return groups;
82
- }
83
-
84
- // Build index table (Layer 1 - compact)
85
- function buildIndexTable(observations) {
86
- if (!observations || observations.length === 0) {
87
- return '*(No recent observations)*';
88
- }
89
-
90
- const grouped = groupByDate(observations);
91
- const lines = [];
92
-
93
- for (const [date, obs] of Object.entries(grouped)) {
94
- lines.push(`### ${date}`);
95
- lines.push('');
96
- lines.push('| ID | Time | T | Summary | Tokens |');
97
- lines.push('|----|------|---|---------|--------|');
98
-
99
- for (const o of obs) {
100
- const id = `#${o.id}`;
101
- const time = formatTime(o.timestamp);
102
- const emoji = getTypeEmoji(o.tool_name);
103
- const summary = o.summary || `${o.tool_name} operation`;
104
- const truncSummary = summary.length > 50 ? summary.slice(0, 47) + '...' : summary;
105
- const tokens = `~${o.tokens_read || estimateTokens(summary)}`;
106
-
107
- lines.push(`| ${id} | ${time} | ${emoji} | ${truncSummary} | ${tokens} |`);
108
- }
109
- lines.push('');
110
- }
111
-
112
- return lines.join('\n');
113
- }
114
-
115
- // Build full details (Layer 3 - expensive)
116
- function buildFullDetails(observations, limit = 5) {
117
- if (!observations || observations.length === 0) {
118
- return '';
119
- }
120
-
121
- const toShow = observations.slice(0, limit);
122
- const lines = [];
123
-
124
- for (const o of toShow) {
125
- lines.push(`#### #${o.id} - ${o.tool_name}`);
126
- lines.push('');
127
-
128
- if (o.summary) {
129
- lines.push(`**Summary**: ${o.summary}`);
130
- lines.push('');
131
- }
132
-
133
- // Show key facts from tool input
134
- const input = o.tool_input || {};
135
- const facts = [];
136
-
137
- if (input.file_path) facts.push(`- File: \`${input.file_path}\``);
138
- if (input.command) facts.push(`- Command: \`${input.command.slice(0, 100)}\``);
139
- if (input.pattern) facts.push(`- Pattern: \`${input.pattern}\``);
140
- if (input.query) facts.push(`- Query: ${input.query.slice(0, 100)}`);
141
- if (input.url) facts.push(`- URL: ${input.url}`);
142
-
143
- if (facts.length > 0) {
144
- lines.push('**Details**:');
145
- lines.push(...facts);
146
- lines.push('');
147
- }
148
-
149
- lines.push('---');
150
- lines.push('');
151
- }
152
-
153
- return lines.join('\n');
154
- }
155
-
156
- // Build token economics summary
157
- function buildTokenEconomics(observations) {
158
- let totalDiscovery = 0;
159
- let totalRead = 0;
160
-
161
- for (const o of observations) {
162
- totalDiscovery += o.tokens_discovery || 0;
163
- totalRead += o.tokens_read || estimateTokens(o.summary || '');
164
- }
165
-
166
- const savings = totalDiscovery - totalRead;
167
- const savingsPercent = totalDiscovery > 0 ? Math.round((savings / totalDiscovery) * 100) : 0;
168
-
169
- if (totalDiscovery === 0) {
170
- return `**Observations**: ${observations.length} | **Read cost**: ~${totalRead} tokens`;
171
- }
172
-
173
- return `**Discovery**: ${totalDiscovery} tokens | **Read**: ${totalRead} tokens | **Saved**: ${savings} (${savingsPercent}%)`;
174
- }
175
-
176
- // Build retrieval instructions
177
- function buildRetrievalInstructions() {
178
- return `
179
- ---
180
-
181
- **Need more context?** Use these commands:
182
- - Search: \`/memory search <query>\`
183
- - Get details: \`/memory get <id>\`
184
- - Timeline: \`/memory timeline <id>\`
185
- `;
186
- }
187
-
188
- /**
189
- * Build topic summaries from user's actual recorded concepts
190
- * Dynamically extracts topics from what the user has discussed
191
- */
192
- function buildTopicSummaries() {
193
- // Get all recent observations to extract actual concepts
194
- const recentObs = database.getRecentObservations(null, 100);
195
-
196
- // Extract and count concepts from user's actual data
197
- const conceptCounts = {};
198
- for (const obs of recentObs) {
199
- if (obs.concepts) {
200
- const concepts = obs.concepts.split(',').map(c => c.trim()).filter(c => c.length > 1);
201
- for (const concept of concepts) {
202
- // Skip generic tool names
203
- if (['edit', 'bash', 'read', 'grep', 'write', 'glob'].includes(concept.toLowerCase())) continue;
204
- conceptCounts[concept] = (conceptCounts[concept] || 0) + 1;
205
- }
206
- }
207
- }
208
-
209
- // Get top concepts (mentioned at least twice)
210
- const topConcepts = Object.entries(conceptCounts)
211
- .filter(([_, count]) => count >= 2)
212
- .sort((a, b) => b[1] - a[1])
213
- .slice(0, 10)
214
- .map(([concept, _]) => concept);
215
-
216
- if (topConcepts.length === 0) return '';
217
-
218
- const sections = [];
219
- const seenIds = new Set();
220
-
221
- // Search for each top concept
222
- for (const concept of topConcepts.slice(0, 5)) {
223
- try {
224
- const results = database.searchObservations(concept, 3);
225
- const newResults = results.filter(r => !seenIds.has(r.id));
226
-
227
- if (newResults.length > 0) {
228
- sections.push(`### ${concept}`);
229
- sections.push('');
230
- for (const r of newResults.slice(0, 2)) {
231
- seenIds.add(r.id);
232
- const summary = r.summary || '';
233
- if (summary.length > 20) {
234
- sections.push(`- **#${r.id}**: ${summary.slice(0, 150)}${summary.length > 150 ? '...' : ''}`);
235
- }
236
- }
237
- sections.push('');
238
- }
239
- } catch (e) {
240
- // Search might fail, continue
241
- }
242
- }
243
-
244
- if (sections.length > 0) {
245
- return '## 历史话题讨论\n\n基于您的实际对话自动提取的主题:\n\n' + sections.join('\n');
246
- }
247
- return '';
248
- }
249
-
250
- /**
251
- * Build complete context for session injection
252
- */
253
- export function buildContext(projectPath, options = {}) {
254
- const {
255
- observationLimit = 50,
256
- fullDetailCount = 5,
257
- showTokenEconomics = true,
258
- showRetrievalInstructions = true
259
- } = options;
260
-
261
- // Fetch recent observations and filter out low-value ones
262
- const rawObservations = database.getRecentObservations(projectPath, observationLimit * 3); // Fetch more to compensate for filtering
263
- const observations = filterHighValueObservations(rawObservations).slice(0, observationLimit);
264
-
265
- if (observations.length === 0) {
266
- return null; // No context to inject
267
- }
268
-
269
- // Fetch recent summaries
270
- const summaries = database.getRecentSummaries(projectPath, 3);
271
-
272
- // Build context parts
273
- const parts = [];
274
-
275
- // Header
276
- parts.push('<openclaw-mem-context>');
277
- parts.push('# Recent Activity');
278
- parts.push('');
279
-
280
- // Token economics
281
- if (showTokenEconomics) {
282
- parts.push(buildTokenEconomics(observations));
283
- parts.push('');
284
- }
285
-
286
- // Topic summaries (from historical search)
287
- const topicSummaries = buildTopicSummaries();
288
- if (topicSummaries) {
289
- parts.push(topicSummaries);
290
- parts.push('');
291
- }
292
-
293
- // Index table (all observations, compact)
294
- parts.push('## Index');
295
- parts.push('');
296
- parts.push(buildIndexTable(observations));
297
-
298
- // Full details (top N)
299
- if (fullDetailCount > 0) {
300
- const details = buildFullDetails(observations, fullDetailCount);
301
- if (details) {
302
- parts.push('## Recent Details');
303
- parts.push('');
304
- parts.push(details);
305
- }
306
- }
307
-
308
- // Session summaries
309
- if (summaries.length > 0) {
310
- parts.push('## Previous Sessions');
311
- parts.push('');
312
- for (const s of summaries) {
313
- if (s.request) parts.push(`- **Goal**: ${s.request}`);
314
- if (s.completed) parts.push(`- **Completed**: ${s.completed}`);
315
- if (s.next_steps) parts.push(`- **Next**: ${s.next_steps}`);
316
- parts.push('');
317
- }
318
- }
319
-
320
- // Retrieval instructions
321
- if (showRetrievalInstructions) {
322
- parts.push(buildRetrievalInstructions());
323
- }
324
-
325
- parts.push('</openclaw-mem-context>');
326
-
327
- return parts.join('\n');
328
- }
329
-
330
- /**
331
- * Search observations and return formatted results
332
- */
333
- export function searchContext(query, limit = 20) {
334
- const results = database.searchObservations(query, limit);
335
-
336
- if (results.length === 0) {
337
- return `No observations found for query: "${query}"`;
338
- }
339
-
340
- const lines = [
341
- `## Search Results for "${query}"`,
342
- '',
343
- '| ID | Tool | Summary | Date |',
344
- '|----|------|---------|------|'
345
- ];
346
-
347
- for (const r of results) {
348
- const summary = r.summary_highlight || r.summary || `${r.tool_name} operation`;
349
- const truncSummary = summary.length > 60 ? summary.slice(0, 57) + '...' : summary;
350
- const date = formatDate(r.timestamp);
351
- lines.push(`| #${r.id} | ${r.tool_name} | ${truncSummary} | ${date} |`);
352
- }
353
-
354
- lines.push('');
355
- lines.push(`*${results.length} results. Use \`/memory get <id>\` for full details.*`);
356
-
357
- return lines.join('\n');
358
- }
359
-
360
- /**
361
- * Get full observation details by IDs
362
- */
363
- export function getObservationDetails(ids) {
364
- const observations = database.getObservations(ids);
365
-
366
- if (observations.length === 0) {
367
- return `No observations found for IDs: ${ids.join(', ')}`;
368
- }
369
-
370
- return buildFullDetails(observations, observations.length);
371
- }
372
-
373
- /**
374
- * Get timeline around an observation
375
- */
376
- export function getTimeline(anchorId, depthBefore = 3, depthAfter = 2) {
377
- const anchor = database.getObservation(anchorId);
378
- if (!anchor) {
379
- return `Observation #${anchorId} not found`;
380
- }
381
-
382
- // Get surrounding observations from same session
383
- const allObs = database.getRecentObservations(null, 100);
384
- const anchorIdx = allObs.findIndex(o => o.id === anchorId);
385
-
386
- if (anchorIdx === -1) {
387
- return buildFullDetails([anchor], 1);
388
- }
389
-
390
- const startIdx = Math.max(0, anchorIdx - depthAfter); // Note: list is DESC, so after = before in time
391
- const endIdx = Math.min(allObs.length, anchorIdx + depthBefore + 1);
392
- const timeline = allObs.slice(startIdx, endIdx).reverse();
393
-
394
- const lines = [
395
- `## Timeline around #${anchorId}`,
396
- ''
397
- ];
398
-
399
- for (const o of timeline) {
400
- const marker = o.id === anchorId ? '**→**' : ' ';
401
- const time = formatTime(o.timestamp);
402
- const emoji = getTypeEmoji(o.tool_name);
403
- const summary = o.summary || `${o.tool_name} operation`;
404
- lines.push(`${marker} ${time} ${emoji} #${o.id}: ${summary}`);
405
- }
406
-
407
- return lines.join('\n');
408
- }
409
-
410
- export default {
411
- buildContext,
412
- searchContext,
413
- getObservationDetails,
414
- getTimeline
415
- };