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
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw-Mem 调试日志工具
4
+ *
5
+ * 显示完整的 API 调用流程:
6
+ * 1. 用户消息
7
+ * 2. AI 调用的 exec 命令
8
+ * 3. API 收到的请求
9
+ * 4. API 返回的响应
10
+ * 5. AI 最终回复
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+ import http from 'http';
17
+
18
+ const SESSIONS_DIR = path.join(os.homedir(), '.openclaw', 'agents', 'main', 'sessions');
19
+ const LOG_FILE = '/tmp/openclaw-debug.log';
20
+
21
+ // 颜色
22
+ const c = {
23
+ reset: '\x1b[0m',
24
+ bold: '\x1b[1m',
25
+ dim: '\x1b[2m',
26
+ red: '\x1b[31m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m',
29
+ blue: '\x1b[34m',
30
+ magenta: '\x1b[35m',
31
+ cyan: '\x1b[36m',
32
+ bgRed: '\x1b[41m',
33
+ bgGreen: '\x1b[42m',
34
+ bgYellow: '\x1b[43m',
35
+ bgBlue: '\x1b[44m',
36
+ };
37
+
38
+ function log(msg) {
39
+ const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
40
+ const line = `[${ts}] ${msg}`;
41
+ console.log(line);
42
+ fs.appendFileSync(LOG_FILE, line.replace(/\x1b\[[0-9;]*m/g, '') + '\n');
43
+ }
44
+
45
+ function logSection(title, color = 'cyan') {
46
+ console.log();
47
+ console.log(`${c[color]}${'═'.repeat(60)}${c.reset}`);
48
+ console.log(`${c[color]}${c.bold} ${title}${c.reset}`);
49
+ console.log(`${c[color]}${'═'.repeat(60)}${c.reset}`);
50
+ }
51
+
52
+ function logEvent(icon, label, content, color = 'white') {
53
+ const ts = new Date().toLocaleTimeString('zh-CN', { hour12: false });
54
+ console.log();
55
+ console.log(`${c.dim}[${ts}]${c.reset} ${icon} ${c[color]}${c.bold}${label}${c.reset}`);
56
+ if (content) {
57
+ const lines = String(content).split('\n').slice(0, 15);
58
+ lines.forEach(line => {
59
+ console.log(` ${c.dim}│${c.reset} ${line.slice(0, 120)}`);
60
+ });
61
+ if (String(content).split('\n').length > 15) {
62
+ console.log(` ${c.dim}│ ... (更多内容省略)${c.reset}`);
63
+ }
64
+ }
65
+ fs.appendFileSync(LOG_FILE, `[${ts}] ${icon} ${label}\n${content || ''}\n\n`);
66
+ }
67
+
68
+ // 监控 Session 文件
69
+ let lastSessionFile = null;
70
+ let processedLines = new Set();
71
+
72
+ function findLatestSession() {
73
+ try {
74
+ const files = fs.readdirSync(SESSIONS_DIR)
75
+ .filter(f => f.endsWith('.jsonl'))
76
+ .map(f => ({
77
+ name: f,
78
+ path: path.join(SESSIONS_DIR, f),
79
+ mtime: fs.statSync(path.join(SESSIONS_DIR, f)).mtime
80
+ }))
81
+ .sort((a, b) => b.mtime - a.mtime);
82
+ return files[0]?.path || null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function watchSession() {
89
+ const latestSession = findLatestSession();
90
+
91
+ if (latestSession !== lastSessionFile) {
92
+ lastSessionFile = latestSession;
93
+ processedLines.clear();
94
+ if (latestSession) {
95
+ logEvent('📁', '监控会话文件', path.basename(latestSession), 'cyan');
96
+ }
97
+ }
98
+
99
+ if (!lastSessionFile) return;
100
+
101
+ try {
102
+ const content = fs.readFileSync(lastSessionFile, 'utf-8');
103
+ const lines = content.trim().split('\n');
104
+
105
+ for (const line of lines) {
106
+ const lineKey = line.slice(0, 150);
107
+ if (processedLines.has(lineKey)) continue;
108
+ processedLines.add(lineKey);
109
+
110
+ try {
111
+ const entry = JSON.parse(line);
112
+ if (entry.type !== 'message') continue;
113
+
114
+ const msg = entry.message;
115
+ if (!msg) continue;
116
+
117
+ // 用户消息
118
+ if (msg.role === 'user') {
119
+ let text = '';
120
+ if (Array.isArray(msg.content)) {
121
+ const textPart = msg.content.find(c => c.type === 'text');
122
+ text = textPart?.text || '';
123
+ } else {
124
+ text = msg.content || '';
125
+ }
126
+ if (text && !text.startsWith('A new session')) {
127
+ logEvent('👤', '用户消息', text.slice(0, 300), 'green');
128
+ }
129
+ }
130
+
131
+ // AI 响应
132
+ if (msg.role === 'assistant' && msg.content) {
133
+ // 检查工具调用
134
+ if (Array.isArray(msg.content)) {
135
+ for (const block of msg.content) {
136
+ if (block.type === 'toolCall') {
137
+ const toolName = block.name;
138
+ const args = block.arguments;
139
+
140
+ if (toolName === 'exec') {
141
+ let argsObj;
142
+ try {
143
+ argsObj = typeof args === 'string' ? JSON.parse(args) : args;
144
+ } catch {
145
+ argsObj = { command: args };
146
+ }
147
+
148
+ logEvent('🔧', `工具调用: ${toolName}`,
149
+ `命令: ${argsObj.command || JSON.stringify(argsObj)}`, 'yellow');
150
+
151
+ // 高亮显示 curl 命令
152
+ if (argsObj.command && argsObj.command.includes('curl')) {
153
+ console.log(` ${c.bgYellow}${c.bold} CURL 命令 ${c.reset}`);
154
+ console.log(` ${c.yellow}${argsObj.command}${c.reset}`);
155
+ }
156
+ }
157
+ }
158
+
159
+ if (block.type === 'text' && block.text) {
160
+ logEvent('🤖', 'AI 响应', block.text.slice(0, 400), 'blue');
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ // 工具结果
167
+ if (msg.role === 'toolResult') {
168
+ const toolName = msg.toolName || 'unknown';
169
+ let result = '';
170
+ if (Array.isArray(msg.content)) {
171
+ const textPart = msg.content.find(c => c.type === 'text');
172
+ result = textPart?.text || '';
173
+ } else {
174
+ result = msg.content || '';
175
+ }
176
+
177
+ if (toolName === 'exec') {
178
+ const isEmpty = !result || result === '(no output)';
179
+ const color = isEmpty ? 'red' : 'green';
180
+ const icon = isEmpty ? '❌' : '✅';
181
+ logEvent(icon, `exec 返回结果`, result || '(空)', color);
182
+
183
+ if (isEmpty) {
184
+ console.log(` ${c.bgRed}${c.bold} 警告: exec 返回空!curl 可能失败了 ${c.reset}`);
185
+ }
186
+ }
187
+ }
188
+
189
+ } catch (e) {
190
+ // 解析失败,忽略
191
+ }
192
+ }
193
+ } catch {
194
+ // 文件读取失败
195
+ }
196
+ }
197
+
198
+ // 创建代理 API 服务器来拦截请求
199
+ const PROXY_PORT = 18791;
200
+ const TARGET_PORT = 18790;
201
+
202
+ function startProxyServer() {
203
+ const proxy = http.createServer((req, res) => {
204
+ let body = '';
205
+ req.on('data', chunk => body += chunk);
206
+ req.on('end', () => {
207
+ // 记录请求
208
+ logEvent('📥', `API 请求: ${req.method} ${req.url}`,
209
+ body ? `Body: ${body}` : '(无 body)', 'magenta');
210
+
211
+ // 转发到真实 API
212
+ const options = {
213
+ hostname: '127.0.0.1',
214
+ port: TARGET_PORT,
215
+ path: req.url,
216
+ method: req.method,
217
+ headers: req.headers
218
+ };
219
+
220
+ const proxyReq = http.request(options, (proxyRes) => {
221
+ let responseBody = '';
222
+ proxyRes.on('data', chunk => responseBody += chunk);
223
+ proxyRes.on('end', () => {
224
+ // 记录响应
225
+ const preview = responseBody.slice(0, 500);
226
+ logEvent('📤', `API 响应 (${proxyRes.statusCode})`, preview,
227
+ proxyRes.statusCode === 200 ? 'green' : 'red');
228
+
229
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
230
+ res.end(responseBody);
231
+ });
232
+ });
233
+
234
+ proxyReq.on('error', (err) => {
235
+ logEvent('❌', 'API 请求失败', err.message, 'red');
236
+ res.writeHead(500);
237
+ res.end('Proxy error');
238
+ });
239
+
240
+ if (body) proxyReq.write(body);
241
+ proxyReq.end();
242
+ });
243
+ });
244
+
245
+ proxy.listen(PROXY_PORT, '127.0.0.1', () => {
246
+ log(`${c.green}代理服务器运行在 http://127.0.0.1:${PROXY_PORT}${c.reset}`);
247
+ log(`${c.dim}(转发到 http://127.0.0.1:${TARGET_PORT})${c.reset}`);
248
+ });
249
+
250
+ return proxy;
251
+ }
252
+
253
+ // 主函数
254
+ function main() {
255
+ // 清空日志文件
256
+ fs.writeFileSync(LOG_FILE, '');
257
+
258
+ logSection('OpenClaw-Mem 调试日志工具', 'cyan');
259
+ log('');
260
+ log(`${c.bold}监控内容:${c.reset}`);
261
+ log(` • Session 文件: ${SESSIONS_DIR}`);
262
+ log(` • 日志文件: ${LOG_FILE}`);
263
+ log('');
264
+ log(`${c.yellow}提示: 按 Ctrl+C 退出${c.reset}`);
265
+ log('');
266
+
267
+ // 开始监控
268
+ setInterval(watchSession, 300);
269
+
270
+ logSection('等待事件...', 'dim');
271
+ }
272
+
273
+ main();
274
+
275
+ process.on('SIGINT', () => {
276
+ console.log();
277
+ log(`${c.yellow}调试日志已停止${c.reset}`);
278
+ log(`${c.dim}完整日志保存在: ${LOG_FILE}${c.reset}`);
279
+ process.exit(0);
280
+ });
package/extractor.js ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * OpenClaw-Mem LLM Extractor
3
+ *
4
+ * Structured observation extraction inspired by claude-mem's observer agent pattern.
5
+ * Uses DeepSeek API to produce rich, searchable memory records.
6
+ */
7
+
8
+ import { callGatewayChat } from './gateway-llm.js';
9
+
10
+ // ── Valid concept categories (fixed taxonomy for consistent search) ──
11
+ const VALID_CONCEPTS = [
12
+ 'how-it-works', // understanding mechanisms
13
+ 'why-it-exists', // purpose or rationale
14
+ 'what-changed', // modifications made
15
+ 'problem-solution', // issues and their fixes
16
+ 'gotcha', // traps or edge cases
17
+ 'pattern', // reusable approach
18
+ 'trade-off' // pros/cons of a decision
19
+ ];
20
+
21
+ // ── Cache ──
22
+ const conceptCache = new Map();
23
+ const CACHE_MAX_SIZE = 1000;
24
+ const CACHE_TTL = 60 * 60 * 1000; // 1 hour
25
+
26
+ function getCacheKey(text) {
27
+ let hash = 0;
28
+ const str = text.slice(0, 500);
29
+ for (let i = 0; i < str.length; i++) {
30
+ const char = str.charCodeAt(i);
31
+ hash = ((hash << 5) - hash) + char;
32
+ hash = hash & hash;
33
+ }
34
+ return hash.toString(16);
35
+ }
36
+
37
+ function cleanCache() {
38
+ if (conceptCache.size > CACHE_MAX_SIZE) {
39
+ const now = Date.now();
40
+ for (const [key, value] of conceptCache.entries()) {
41
+ if (now - value.timestamp > CACHE_TTL) {
42
+ conceptCache.delete(key);
43
+ }
44
+ }
45
+ if (conceptCache.size > CACHE_MAX_SIZE) {
46
+ const entries = [...conceptCache.entries()];
47
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
48
+ const toRemove = entries.slice(0, entries.length - CACHE_MAX_SIZE / 2);
49
+ for (const [key] of toRemove) {
50
+ conceptCache.delete(key);
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Extract concepts from text using LLM
58
+ */
59
+ export async function extractConcepts(text, options = {}) {
60
+ if (!text || typeof text !== 'string' || text.trim().length < 10) {
61
+ return [];
62
+ }
63
+
64
+ const cacheKey = getCacheKey(text);
65
+ const cached = conceptCache.get(cacheKey);
66
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
67
+ return cached.concepts;
68
+ }
69
+
70
+ try {
71
+ const content = await callGatewayChat([
72
+ {
73
+ role: 'system',
74
+ content: `You are a knowledge classifier. Categorize the given text into 2-4 concept categories from this fixed list:
75
+ - how-it-works: understanding mechanisms or implementation details
76
+ - why-it-exists: purpose, rationale, or motivation
77
+ - what-changed: modifications, updates, or configuration changes
78
+ - problem-solution: issues encountered and their fixes
79
+ - gotcha: traps, edge cases, or surprising behavior
80
+ - pattern: reusable approaches or best practices
81
+ - trade-off: pros/cons analysis or design decisions
82
+
83
+ Return ONLY a JSON array of matching categories. No explanation.`
84
+ },
85
+ {
86
+ role: 'user',
87
+ content: text.slice(0, 2000)
88
+ }
89
+ ], { sessionKey: 'extract-concepts', temperature: 0.1, max_tokens: 100 });
90
+
91
+ if (!content) return [];
92
+
93
+ let concepts = [];
94
+ try {
95
+ const match = content.match(/\[[\s\S]*?\]/);
96
+ if (match) {
97
+ concepts = JSON.parse(match[0]);
98
+ }
99
+ } catch (parseErr) {
100
+ console.error('[openclaw-mem] Failed to parse concepts response:', parseErr.message);
101
+ return [];
102
+ }
103
+
104
+ // Validate against fixed taxonomy
105
+ concepts = concepts
106
+ .filter(c => typeof c === 'string')
107
+ .map(c => c.trim().toLowerCase())
108
+ .filter(c => VALID_CONCEPTS.includes(c))
109
+ .slice(0, 4);
110
+
111
+ cleanCache();
112
+ conceptCache.set(cacheKey, { concepts, timestamp: Date.now() });
113
+ return concepts;
114
+ } catch (err) {
115
+ console.error('[openclaw-mem] Concept extraction error:', err.message);
116
+ return [];
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Extract structured observation from a tool call
122
+ *
123
+ * Produces rich, searchable records with:
124
+ * - Accurate type classification
125
+ * - Descriptive title (short, action-oriented)
126
+ * - Detailed narrative (what happened, how it works, why it matters)
127
+ * - Structured facts (self-contained, grep-friendly)
128
+ * - Fixed concept categories
129
+ */
130
+ export async function extractFromToolCall(data) {
131
+ const { tool_name, tool_input, tool_response, filesRead, filesModified } = data;
132
+
133
+ // Provide generous context (2000 chars each, not 300)
134
+ const inputStr = typeof tool_input === 'string'
135
+ ? tool_input.slice(0, 2000)
136
+ : JSON.stringify(tool_input, null, 0).slice(0, 2000);
137
+
138
+ const responseStr = typeof tool_response === 'string'
139
+ ? tool_response.slice(0, 2000)
140
+ : JSON.stringify(tool_response, null, 0).slice(0, 2000);
141
+
142
+ try {
143
+ const content = await callGatewayChat([
144
+ {
145
+ role: 'system',
146
+ content: `You are OpenClaw-Mem, a specialized observer that creates searchable memory records for FUTURE SESSIONS.
147
+
148
+ Your job: analyze a tool call and produce a structured observation capturing what was LEARNED, BUILT, FIXED, or CONFIGURED.
149
+
150
+ RULES:
151
+ - Record deliverables and capabilities, not process steps
152
+ - Use action verbs: implemented, fixed, deployed, configured, migrated, optimized, discovered, decided
153
+ - The "narrative" field is the most important: explain WHAT happened, HOW it works, and WHY it matters
154
+ - Facts must be self-contained statements (each fact should make sense without the others)
155
+ - Title should be a short noun phrase (3-10 words) capturing the core topic
156
+
157
+ TYPE DEFINITIONS (pick exactly one):
158
+ - bugfix: something was broken and is now fixed
159
+ - feature: new capability or functionality added
160
+ - refactor: code restructured without behavior change
161
+ - change: generic modification (docs, config, dependencies)
162
+ - discovery: learning about existing system, reading code, exploring
163
+ - decision: architectural or design choice with rationale
164
+
165
+ CONCEPT CATEGORIES (pick 1-3):
166
+ - how-it-works: understanding mechanisms
167
+ - why-it-exists: purpose or rationale
168
+ - what-changed: modifications made
169
+ - problem-solution: issues and their fixes
170
+ - gotcha: traps or edge cases
171
+ - pattern: reusable approach
172
+ - trade-off: pros/cons of a decision
173
+
174
+ Return ONLY valid JSON, no markdown fences, no explanation.`
175
+ },
176
+ {
177
+ role: 'user',
178
+ content: `Tool: ${tool_name}
179
+ Input: ${inputStr}
180
+ Output: ${responseStr}
181
+ Files read: ${filesRead?.join(', ') || 'none'}
182
+ Files modified: ${filesModified?.join(', ') || 'none'}
183
+
184
+ Return JSON:
185
+ {
186
+ "type": "one of: bugfix|feature|refactor|change|discovery|decision",
187
+ "title": "Short descriptive title (3-10 words)",
188
+ "narrative": "2-4 sentences: what was done, how it works, why it matters. Be specific and include key details.",
189
+ "facts": ["Self-contained fact 1", "Self-contained fact 2", "...up to 5"],
190
+ "concepts": ["category1", "category2"]
191
+ }`
192
+ }
193
+ ], { sessionKey: 'extract-toolcall', temperature: 0.2, max_tokens: 800 });
194
+
195
+ if (!content) throw new Error('empty response');
196
+
197
+ const match = content.match(/\{[\s\S]*\}/);
198
+ if (match) {
199
+ const result = JSON.parse(match[0]);
200
+
201
+ // Validate type
202
+ const validTypes = ['bugfix', 'feature', 'refactor', 'change', 'discovery', 'decision'];
203
+ const type = validTypes.includes(result.type) ? result.type : 'discovery';
204
+
205
+ // Validate concepts against fixed taxonomy
206
+ const concepts = Array.isArray(result.concepts)
207
+ ? result.concepts.filter(c => VALID_CONCEPTS.includes(c)).slice(0, 3)
208
+ : [];
209
+
210
+ return {
211
+ type,
212
+ title: (result.title || '').slice(0, 120),
213
+ narrative: (result.narrative || '').slice(0, 1000),
214
+ facts: Array.isArray(result.facts)
215
+ ? result.facts.filter(f => typeof f === 'string').slice(0, 5)
216
+ : [],
217
+ concepts: concepts.length > 0 ? concepts : ['how-it-works']
218
+ };
219
+ }
220
+ } catch (err) {
221
+ console.error('[openclaw-mem] Tool extraction error:', err.message);
222
+ }
223
+
224
+ return {
225
+ type: 'discovery',
226
+ title: '',
227
+ narrative: '',
228
+ facts: [],
229
+ concepts: ['how-it-works']
230
+ };
231
+ }
232
+
233
+ /**
234
+ * Batch extract concepts from multiple texts
235
+ */
236
+ export async function batchExtractConcepts(texts) {
237
+ const results = new Map();
238
+
239
+ const uncached = [];
240
+ for (const text of texts) {
241
+ const cacheKey = getCacheKey(text);
242
+ const cached = conceptCache.get(cacheKey);
243
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
244
+ results.set(text, cached.concepts);
245
+ } else {
246
+ uncached.push(text);
247
+ }
248
+ }
249
+
250
+ const BATCH_SIZE = 5;
251
+ for (let i = 0; i < uncached.length; i += BATCH_SIZE) {
252
+ const batch = uncached.slice(i, i + BATCH_SIZE);
253
+ const promises = batch.map(text => extractConcepts(text));
254
+ const batchResults = await Promise.all(promises);
255
+
256
+ for (let j = 0; j < batch.length; j++) {
257
+ results.set(batch[j], batchResults[j]);
258
+ }
259
+ }
260
+
261
+ return results;
262
+ }
263
+
264
+ export default {
265
+ extractConcepts,
266
+ extractFromToolCall,
267
+ batchExtractConcepts
268
+ };