openclaw-mem 1.0.4 → 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 +146 -168
- 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 -201
- package/bin/openclaw-mem.js +0 -117
- package/docs/locales/README_AR.md +0 -35
- package/docs/locales/README_DE.md +0 -35
- package/docs/locales/README_ES.md +0 -35
- package/docs/locales/README_FR.md +0 -35
- package/docs/locales/README_HE.md +0 -35
- package/docs/locales/README_HI.md +0 -35
- package/docs/locales/README_ID.md +0 -35
- package/docs/locales/README_IT.md +0 -35
- package/docs/locales/README_JA.md +0 -57
- package/docs/locales/README_KO.md +0 -35
- package/docs/locales/README_NL.md +0 -35
- package/docs/locales/README_PL.md +0 -35
- package/docs/locales/README_PT.md +0 -35
- package/docs/locales/README_RU.md +0 -35
- package/docs/locales/README_TH.md +0 -35
- package/docs/locales/README_TR.md +0 -35
- package/docs/locales/README_UK.md +0 -35
- package/docs/locales/README_VI.md +0 -35
- package/docs/logo.svg +0 -32
- 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/mcp-server.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw-Mem MCP Server
|
|
4
|
+
*
|
|
5
|
+
* 实现 MCP (Model Context Protocol) 标准接口
|
|
6
|
+
* 提供 3 层记忆检索工作流:search → timeline → get_observations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
import {
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
import database from './database.js';
|
|
16
|
+
|
|
17
|
+
// ============ 工具函数 ============
|
|
18
|
+
|
|
19
|
+
function formatTime(timestamp) {
|
|
20
|
+
if (!timestamp) return '';
|
|
21
|
+
const date = new Date(timestamp);
|
|
22
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatDate(timestamp) {
|
|
26
|
+
if (!timestamp) return '';
|
|
27
|
+
const date = new Date(timestamp);
|
|
28
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
29
|
+
return date.toISOString().split('T')[0];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatDateHeading(dateOrKey) {
|
|
33
|
+
if (!dateOrKey) return '';
|
|
34
|
+
let date;
|
|
35
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateOrKey)) {
|
|
36
|
+
date = new Date(`${dateOrKey}T00:00:00`);
|
|
37
|
+
} else {
|
|
38
|
+
date = new Date(dateOrKey);
|
|
39
|
+
}
|
|
40
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
41
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function truncateText(text, max = 80) {
|
|
45
|
+
if (!text) return '';
|
|
46
|
+
const clean = String(text).replace(/\s+/g, ' ').trim();
|
|
47
|
+
if (clean.length <= max) return clean;
|
|
48
|
+
return clean.slice(0, Math.max(0, max - 3)) + '...';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function estimateTokens(text) {
|
|
52
|
+
if (!text) return 0;
|
|
53
|
+
return Math.ceil(String(text).length / 4);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Type mapping
|
|
57
|
+
const TYPE_EMOJI = {
|
|
58
|
+
'session-request': '📋',
|
|
59
|
+
'discovery': '🔵',
|
|
60
|
+
'bugfix': '🔴',
|
|
61
|
+
'feature': '🟣',
|
|
62
|
+
'refactor': '🔄',
|
|
63
|
+
'change': '✅',
|
|
64
|
+
'decision': '⚖️',
|
|
65
|
+
'problem-solution': '💡',
|
|
66
|
+
'gotcha': '⚠️',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function getTypeLabel(observation) {
|
|
70
|
+
const type = observation?.type || 'discovery';
|
|
71
|
+
return TYPE_EMOJI[type] || '🔵';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeIds(input) {
|
|
75
|
+
const ids = [];
|
|
76
|
+
const pushId = (value) => {
|
|
77
|
+
if (value === null || value === undefined) return;
|
|
78
|
+
const cleaned = String(value).replace(/^#/, '').trim();
|
|
79
|
+
if (!cleaned) return;
|
|
80
|
+
const parsed = Number(cleaned);
|
|
81
|
+
if (!Number.isNaN(parsed)) ids.push(parsed);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (Array.isArray(input)) {
|
|
85
|
+
input.forEach(pushId);
|
|
86
|
+
return ids;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (typeof input === 'string') {
|
|
90
|
+
input.split(/[,\s]+/).forEach(pushId);
|
|
91
|
+
return ids;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pushId(input);
|
|
95
|
+
return ids;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============ 搜索功能 ============
|
|
99
|
+
|
|
100
|
+
function search(args = {}) {
|
|
101
|
+
const query = typeof args === 'string' ? args : (args.query || args.q || '*');
|
|
102
|
+
const limit = args.limit ?? args.maxResults ?? 30;
|
|
103
|
+
const project = args.project || null;
|
|
104
|
+
const type = args.type || args.obs_type || null;
|
|
105
|
+
const dateStart = args.dateStart || null;
|
|
106
|
+
const dateEnd = args.dateEnd || null;
|
|
107
|
+
|
|
108
|
+
let results;
|
|
109
|
+
|
|
110
|
+
if (query === '*' || !query) {
|
|
111
|
+
// 获取最近的 observations
|
|
112
|
+
results = database.getRecentObservations(project, limit * 2);
|
|
113
|
+
} else {
|
|
114
|
+
// 搜索
|
|
115
|
+
results = database.searchObservations(query, limit * 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 过滤
|
|
119
|
+
if (type) {
|
|
120
|
+
results = results.filter(r => r.type === type);
|
|
121
|
+
}
|
|
122
|
+
if (dateStart) {
|
|
123
|
+
const start = new Date(dateStart).getTime();
|
|
124
|
+
results = results.filter(r => new Date(r.timestamp).getTime() >= start);
|
|
125
|
+
}
|
|
126
|
+
if (dateEnd) {
|
|
127
|
+
const end = new Date(dateEnd).getTime() + 86400000; // 包含当天
|
|
128
|
+
results = results.filter(r => new Date(r.timestamp).getTime() < end);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
results = results.slice(0, limit);
|
|
132
|
+
|
|
133
|
+
if (results.length === 0) {
|
|
134
|
+
return `No observations found for query: "${query}"`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 按日期分组
|
|
138
|
+
const grouped = new Map();
|
|
139
|
+
for (const obs of results) {
|
|
140
|
+
const dateKey = formatDate(obs.timestamp) || 'Unknown';
|
|
141
|
+
if (!grouped.has(dateKey)) {
|
|
142
|
+
grouped.set(dateKey, []);
|
|
143
|
+
}
|
|
144
|
+
grouped.get(dateKey).push(obs);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const lines = [
|
|
148
|
+
`Found ${results.length} result(s) matching "${query}"`,
|
|
149
|
+
''
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
for (const [dateKey, obs] of grouped.entries()) {
|
|
153
|
+
const heading = formatDateHeading(dateKey) || dateKey;
|
|
154
|
+
lines.push(`### ${heading}`);
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push('| ID | Time | T | Title | Read |');
|
|
157
|
+
lines.push('|----|------|---|-------|------|');
|
|
158
|
+
|
|
159
|
+
for (const o of obs) {
|
|
160
|
+
const id = `#${o.id}`;
|
|
161
|
+
const time = formatTime(o.timestamp);
|
|
162
|
+
const typeLabel = getTypeLabel(o);
|
|
163
|
+
const title = truncateText(o.narrative || o.summary || `${o.tool_name} operation`, 60);
|
|
164
|
+
const tokens = `~${o.tokens_read || estimateTokens(title)}`;
|
|
165
|
+
lines.push(`| ${id} | ${time} | ${typeLabel} | ${title} | ${tokens} |`);
|
|
166
|
+
}
|
|
167
|
+
lines.push('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
lines.push(`*Use \`timeline\` or \`get_observations\` for full details.*`);
|
|
171
|
+
|
|
172
|
+
return lines.join('\n');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============ Timeline 功能 ============
|
|
176
|
+
|
|
177
|
+
function timeline(args = {}) {
|
|
178
|
+
let anchorId;
|
|
179
|
+
|
|
180
|
+
if (typeof args === 'number' || typeof args === 'string') {
|
|
181
|
+
anchorId = Number(String(args).replace(/^#/, ''));
|
|
182
|
+
} else {
|
|
183
|
+
const anchor = args.anchor ?? args.id ?? args.observation_id ?? args.observationId;
|
|
184
|
+
|
|
185
|
+
// 如果提供了 query,自动查找 anchor
|
|
186
|
+
if (!anchor && args.query) {
|
|
187
|
+
const searchResults = database.searchObservations(args.query, 1);
|
|
188
|
+
if (searchResults.length > 0) {
|
|
189
|
+
anchorId = searchResults[0].id;
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
anchorId = Number(String(anchor ?? '').replace(/^#/, ''));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Number.isNaN(anchorId)) {
|
|
197
|
+
return 'No anchor ID provided. Use timeline(anchor=<ID>) or timeline(query="...")';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const depthBefore = Number(args.depth_before ?? args.before ?? 3);
|
|
201
|
+
const depthAfter = Number(args.depth_after ?? args.after ?? 2);
|
|
202
|
+
|
|
203
|
+
const anchor = database.getObservation(anchorId);
|
|
204
|
+
if (!anchor) {
|
|
205
|
+
return `Observation #${anchorId} not found`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 获取周围的 observations
|
|
209
|
+
const allObs = database.getRecentObservations(null, 100);
|
|
210
|
+
const anchorIdx = allObs.findIndex(o => o.id === anchorId);
|
|
211
|
+
|
|
212
|
+
if (anchorIdx === -1) {
|
|
213
|
+
// 只返回 anchor 本身
|
|
214
|
+
return buildFullDetails([anchor], 1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 注意:列表是 DESC 排序,所以 "after" 在时间上是 "before" 在索引上
|
|
218
|
+
const startIdx = Math.max(0, anchorIdx - depthAfter);
|
|
219
|
+
const endIdx = Math.min(allObs.length, anchorIdx + depthBefore + 1);
|
|
220
|
+
const timelineObs = allObs.slice(startIdx, endIdx).reverse();
|
|
221
|
+
|
|
222
|
+
const lines = [
|
|
223
|
+
`## Timeline around #${anchorId}`,
|
|
224
|
+
'',
|
|
225
|
+
'| | Time | T | ID | Title |',
|
|
226
|
+
'|---|------|---|-----|-------|'
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
for (const o of timelineObs) {
|
|
230
|
+
const marker = o.id === anchorId ? '→' : '';
|
|
231
|
+
const time = formatTime(o.timestamp);
|
|
232
|
+
const typeLabel = getTypeLabel(o);
|
|
233
|
+
const title = truncateText(o.narrative || o.summary || `${o.tool_name} operation`, 70);
|
|
234
|
+
lines.push(`| ${marker} | ${time} | ${typeLabel} | #${o.id} | ${title} |`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(`*Use \`get_observations(ids=[...])\` for full details.*`);
|
|
239
|
+
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============ Get Observations 功能 ============
|
|
244
|
+
|
|
245
|
+
function buildFullDetails(observations, limit = 10) {
|
|
246
|
+
if (!observations || observations.length === 0) {
|
|
247
|
+
return 'No observations found.';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const toShow = observations.slice(0, limit);
|
|
251
|
+
const lines = [];
|
|
252
|
+
|
|
253
|
+
for (const o of toShow) {
|
|
254
|
+
const title = (o.narrative || o.summary || `${o.tool_name} operation`).replace(/\s+/g, ' ').trim();
|
|
255
|
+
const typeLabel = getTypeLabel(o);
|
|
256
|
+
const dateLabel = formatDateHeading(o.timestamp);
|
|
257
|
+
const timeLabel = formatTime(o.timestamp);
|
|
258
|
+
|
|
259
|
+
lines.push(`## #${o.id} ${typeLabel} ${truncateText(title, 100)}`);
|
|
260
|
+
lines.push('');
|
|
261
|
+
|
|
262
|
+
if (dateLabel || timeLabel) {
|
|
263
|
+
lines.push(`**Time**: ${[dateLabel, timeLabel].filter(Boolean).join(' ')}`);
|
|
264
|
+
}
|
|
265
|
+
if (o.tool_name) {
|
|
266
|
+
lines.push(`**Tool**: ${o.tool_name}`);
|
|
267
|
+
}
|
|
268
|
+
if (o.type) {
|
|
269
|
+
lines.push(`**Type**: ${o.type}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push('');
|
|
272
|
+
|
|
273
|
+
if (o.summary) {
|
|
274
|
+
lines.push(`**Summary**: ${o.summary}`);
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (o.narrative && o.narrative !== o.summary) {
|
|
279
|
+
lines.push(`**Narrative**: ${o.narrative}`);
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 解析 facts
|
|
284
|
+
let facts = o.facts;
|
|
285
|
+
if (typeof facts === 'string') {
|
|
286
|
+
try {
|
|
287
|
+
facts = JSON.parse(facts);
|
|
288
|
+
} catch {
|
|
289
|
+
facts = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (Array.isArray(facts) && facts.length > 0) {
|
|
293
|
+
lines.push('**Facts**:');
|
|
294
|
+
for (const fact of facts.slice(0, 8)) {
|
|
295
|
+
if (fact) lines.push(`- ${fact}`);
|
|
296
|
+
}
|
|
297
|
+
lines.push('');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 文件信息
|
|
301
|
+
let filesRead = o.files_read;
|
|
302
|
+
let filesModified = o.files_modified;
|
|
303
|
+
if (typeof filesRead === 'string') {
|
|
304
|
+
try { filesRead = JSON.parse(filesRead); } catch { filesRead = null; }
|
|
305
|
+
}
|
|
306
|
+
if (typeof filesModified === 'string') {
|
|
307
|
+
try { filesModified = JSON.parse(filesModified); } catch { filesModified = null; }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (Array.isArray(filesRead) && filesRead.length > 0) {
|
|
311
|
+
lines.push(`**Files Read**: ${filesRead.map(f => `\`${f}\``).join(', ')}`);
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(filesModified) && filesModified.length > 0) {
|
|
314
|
+
lines.push(`**Files Modified**: ${filesModified.map(f => `\`${f}\``).join(', ')}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Tool input 关键信息
|
|
318
|
+
let input = o.tool_input;
|
|
319
|
+
if (typeof input === 'string') {
|
|
320
|
+
try { input = JSON.parse(input); } catch { input = {}; }
|
|
321
|
+
}
|
|
322
|
+
input = input || {};
|
|
323
|
+
|
|
324
|
+
const inputFacts = [];
|
|
325
|
+
if (input.file_path) inputFacts.push(`File: \`${input.file_path}\``);
|
|
326
|
+
if (input.command) inputFacts.push(`Command: \`${input.command.slice(0, 100)}\``);
|
|
327
|
+
if (input.pattern) inputFacts.push(`Pattern: \`${input.pattern}\``);
|
|
328
|
+
if (input.query) inputFacts.push(`Query: ${input.query.slice(0, 100)}`);
|
|
329
|
+
if (input.url) inputFacts.push(`URL: ${input.url}`);
|
|
330
|
+
|
|
331
|
+
if (inputFacts.length > 0) {
|
|
332
|
+
lines.push('');
|
|
333
|
+
lines.push('**Details**: ' + inputFacts.join(' | '));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
lines.push('');
|
|
337
|
+
lines.push('---');
|
|
338
|
+
lines.push('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return lines.join('\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function get_observations(args = {}) {
|
|
345
|
+
const ids = Array.isArray(args)
|
|
346
|
+
? normalizeIds(args)
|
|
347
|
+
: normalizeIds(args.ids ?? args.id ?? args.observation_ids ?? args.observationIds);
|
|
348
|
+
|
|
349
|
+
if (!ids.length) {
|
|
350
|
+
return 'No observation IDs provided. Use get_observations(ids=[1, 2, 3])';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const observations = database.getObservations(ids);
|
|
354
|
+
|
|
355
|
+
if (observations.length === 0) {
|
|
356
|
+
return `No observations found for IDs: ${ids.join(', ')}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return buildFullDetails(observations, observations.length);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============ __IMPORTANT 功能 ============
|
|
363
|
+
|
|
364
|
+
function __IMPORTANT() {
|
|
365
|
+
return `## 3-LAYER MEMORY RETRIEVAL WORKFLOW
|
|
366
|
+
|
|
367
|
+
**ALWAYS follow this workflow to minimize token usage:**
|
|
368
|
+
|
|
369
|
+
1. **search(query)** → Get index with IDs (~50-100 tokens/result)
|
|
370
|
+
\`search(query="...", limit=20)\`
|
|
371
|
+
|
|
372
|
+
2. **timeline(anchor=ID)** → Get context around interesting results
|
|
373
|
+
\`timeline(anchor=<ID>, depth_before=3, depth_after=2)\`
|
|
374
|
+
|
|
375
|
+
3. **get_observations(ids=[...])** → Fetch full details ONLY for filtered IDs
|
|
376
|
+
\`get_observations(ids=[1, 2, 3])\`
|
|
377
|
+
|
|
378
|
+
**NEVER fetch full details without filtering first. 10x token savings.**
|
|
379
|
+
|
|
380
|
+
### Quick Examples
|
|
381
|
+
|
|
382
|
+
- Search recent: \`search(query="*", limit=10)\`
|
|
383
|
+
- Search topic: \`search(query="database migration")\`
|
|
384
|
+
- Get context: \`timeline(anchor=123)\`
|
|
385
|
+
- Get details: \`get_observations(ids=[123, 124, 125])\`
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============ MCP Server 设置 ============
|
|
390
|
+
|
|
391
|
+
const TOOLS = [
|
|
392
|
+
{
|
|
393
|
+
name: '__IMPORTANT',
|
|
394
|
+
description: '3-LAYER WORKFLOW: 1. search(query) → index 2. timeline(anchor) → context 3. get_observations(ids) → details. NEVER fetch details without filtering first.',
|
|
395
|
+
inputSchema: {
|
|
396
|
+
type: 'object',
|
|
397
|
+
properties: {},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: 'search',
|
|
402
|
+
description: 'Step 1: Search memory. Returns index with IDs. Params: query, limit, project, type, dateStart, dateEnd',
|
|
403
|
+
inputSchema: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
properties: {
|
|
406
|
+
query: { type: 'string', description: 'Search query (use "*" for recent)' },
|
|
407
|
+
limit: { type: 'number', description: 'Max results (default 30)' },
|
|
408
|
+
project: { type: 'string', description: 'Filter by project path' },
|
|
409
|
+
type: { type: 'string', description: 'Filter by type: discovery, bugfix, feature, refactor, change, decision' },
|
|
410
|
+
dateStart: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
|
|
411
|
+
dateEnd: { type: 'string', description: 'End date (YYYY-MM-DD)' },
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
name: 'timeline',
|
|
417
|
+
description: 'Step 2: Get context around results. Params: anchor (observation ID) OR query (finds anchor automatically), depth_before, depth_after',
|
|
418
|
+
inputSchema: {
|
|
419
|
+
type: 'object',
|
|
420
|
+
properties: {
|
|
421
|
+
anchor: { type: 'number', description: 'Observation ID to center on' },
|
|
422
|
+
query: { type: 'string', description: 'Auto-find anchor from search query' },
|
|
423
|
+
depth_before: { type: 'number', description: 'Observations before anchor (default 3)' },
|
|
424
|
+
depth_after: { type: 'number', description: 'Observations after anchor (default 2)' },
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'get_observations',
|
|
430
|
+
description: 'Step 3: Fetch full details for filtered IDs. Params: ids (array of observation IDs, required)',
|
|
431
|
+
inputSchema: {
|
|
432
|
+
type: 'object',
|
|
433
|
+
properties: {
|
|
434
|
+
ids: {
|
|
435
|
+
type: 'array',
|
|
436
|
+
items: { type: 'number' },
|
|
437
|
+
description: 'Array of observation IDs to fetch (required)',
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
required: ['ids'],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
// 创建 MCP Server
|
|
446
|
+
const server = new Server(
|
|
447
|
+
{
|
|
448
|
+
name: 'openclaw-mem-search',
|
|
449
|
+
version: '1.0.0',
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
capabilities: {
|
|
453
|
+
tools: {},
|
|
454
|
+
},
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
// 注册工具列表
|
|
459
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
460
|
+
return { tools: TOOLS };
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
// 处理工具调用
|
|
464
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
465
|
+
const { name, arguments: args } = request.params;
|
|
466
|
+
|
|
467
|
+
console.error(`[openclaw-mem-mcp] Tool called: ${name}`);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
let result;
|
|
471
|
+
|
|
472
|
+
switch (name) {
|
|
473
|
+
case '__IMPORTANT':
|
|
474
|
+
result = __IMPORTANT();
|
|
475
|
+
break;
|
|
476
|
+
|
|
477
|
+
case 'search':
|
|
478
|
+
result = search(args || {});
|
|
479
|
+
break;
|
|
480
|
+
|
|
481
|
+
case 'timeline':
|
|
482
|
+
result = timeline(args || {});
|
|
483
|
+
break;
|
|
484
|
+
|
|
485
|
+
case 'get_observations':
|
|
486
|
+
result = get_observations(args || {});
|
|
487
|
+
break;
|
|
488
|
+
|
|
489
|
+
default:
|
|
490
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: 'text',
|
|
497
|
+
text: result,
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error(`[openclaw-mem-mcp] Error:`, error.message);
|
|
503
|
+
return {
|
|
504
|
+
content: [
|
|
505
|
+
{
|
|
506
|
+
type: 'text',
|
|
507
|
+
text: `Error: ${error.message}`,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
isError: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// 启动服务器
|
|
516
|
+
async function main() {
|
|
517
|
+
const transport = new StdioServerTransport();
|
|
518
|
+
await server.connect(transport);
|
|
519
|
+
console.error('[openclaw-mem-mcp] MCP Server started (stdio)');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
main().catch((error) => {
|
|
523
|
+
console.error('[openclaw-mem-mcp] Fatal error:', error);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
});
|
package/mem-get.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# mem-get.sh - 获取记忆详情
|
|
3
|
+
# 用法: mem-get.sh ID1 [ID2 ID3 ...]
|
|
4
|
+
|
|
5
|
+
if [ -z "$1" ]; then
|
|
6
|
+
echo "用法: mem-get.sh ID1 [ID2 ID3 ...]"
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
PORT=18790
|
|
11
|
+
|
|
12
|
+
# 构建 JSON 数组
|
|
13
|
+
IDS=""
|
|
14
|
+
for id in "$@"; do
|
|
15
|
+
if [ -n "$IDS" ]; then
|
|
16
|
+
IDS="$IDS,$id"
|
|
17
|
+
else
|
|
18
|
+
IDS="$id"
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
curl -s -X POST "http://127.0.0.1:$PORT/get_observations" \
|
|
23
|
+
-H "Content-Type: application/json" \
|
|
24
|
+
-d "{\"ids\":[$IDS]}"
|
package/mem-search.sh
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# mem-search.sh - 记忆搜索包装脚本(自动处理中文 URL 编码)
|
|
3
|
+
# 用法: mem-search.sh "搜索词" [limit]
|
|
4
|
+
|
|
5
|
+
QUERY="$1"
|
|
6
|
+
LIMIT="${2:-10}"
|
|
7
|
+
PORT=18790
|
|
8
|
+
|
|
9
|
+
if [ -z "$QUERY" ]; then
|
|
10
|
+
echo "用法: mem-search.sh \"搜索词\" [limit]"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# 使用 POST + JSON 避免 URL 编码问题
|
|
15
|
+
curl -s -X POST "http://127.0.0.1:$PORT/search" \
|
|
16
|
+
-H "Content-Type: application/json" \
|
|
17
|
+
-d "{\"query\":\"$QUERY\",\"limit\":$LIMIT}"
|
package/monitor.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OpenClaw-Mem Real-time Monitor
|
|
4
|
+
* Watches for new observations and displays them in real-time
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
|
|
11
|
+
const DB_PATH = path.join(os.homedir(), '.openclaw-mem', 'memory.db');
|
|
12
|
+
const POLL_INTERVAL = 1000; // 1 second
|
|
13
|
+
|
|
14
|
+
const db = new Database(DB_PATH, { readonly: true });
|
|
15
|
+
|
|
16
|
+
// Get the latest observation ID at startup
|
|
17
|
+
let lastId = db.prepare('SELECT MAX(id) as maxId FROM observations').get()?.maxId || 0;
|
|
18
|
+
let lastSummaryId = db.prepare('SELECT MAX(id) as maxId FROM summaries').get()?.maxId || 0;
|
|
19
|
+
|
|
20
|
+
console.log('\x1b[36m╔══════════════════════════════════════════════════════════════╗\x1b[0m');
|
|
21
|
+
console.log('\x1b[36m║\x1b[0m \x1b[1m🧠 OpenClaw-Mem Real-time Monitor\x1b[0m \x1b[36m║\x1b[0m');
|
|
22
|
+
console.log('\x1b[36m╠══════════════════════════════════════════════════════════════╣\x1b[0m');
|
|
23
|
+
console.log('\x1b[36m║\x1b[0m Database: ~/.openclaw-mem/memory.db \x1b[36m║\x1b[0m');
|
|
24
|
+
console.log('\x1b[36m║\x1b[0m Starting from observation #' + lastId.toString().padEnd(33) + '\x1b[36m║\x1b[0m');
|
|
25
|
+
console.log('\x1b[36m║\x1b[0m Press Ctrl+C to stop \x1b[36m║\x1b[0m');
|
|
26
|
+
console.log('\x1b[36m╚══════════════════════════════════════════════════════════════╝\x1b[0m');
|
|
27
|
+
console.log('');
|
|
28
|
+
|
|
29
|
+
const TYPE_COLORS = {
|
|
30
|
+
'discovery': '\x1b[34m🔵\x1b[0m',
|
|
31
|
+
'refactor': '\x1b[35m🔄\x1b[0m',
|
|
32
|
+
'bugfix': '\x1b[31m🔴\x1b[0m',
|
|
33
|
+
'feature': '\x1b[32m🟢\x1b[0m',
|
|
34
|
+
'decision': '\x1b[33m⚖️\x1b[0m',
|
|
35
|
+
'session-request': '\x1b[36m📝\x1b[0m',
|
|
36
|
+
'problem-solution': '\x1b[33m💡\x1b[0m'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function formatTime(timestamp) {
|
|
40
|
+
if (!timestamp) return '';
|
|
41
|
+
const date = new Date(timestamp);
|
|
42
|
+
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function truncate(text, max = 60) {
|
|
46
|
+
if (!text) return '';
|
|
47
|
+
const clean = String(text).replace(/\s+/g, ' ').trim();
|
|
48
|
+
if (clean.length <= max) return clean;
|
|
49
|
+
return clean.slice(0, max - 3) + '...';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getTypeIcon(type) {
|
|
53
|
+
return TYPE_COLORS[type] || '\x1b[37m⚪\x1b[0m';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function checkNewObservations() {
|
|
57
|
+
try {
|
|
58
|
+
const newObs = db.prepare(`
|
|
59
|
+
SELECT id, timestamp, tool_name, type, summary, narrative
|
|
60
|
+
FROM observations
|
|
61
|
+
WHERE id > ?
|
|
62
|
+
ORDER BY id ASC
|
|
63
|
+
`).all(lastId);
|
|
64
|
+
|
|
65
|
+
for (const obs of newObs) {
|
|
66
|
+
const time = formatTime(obs.timestamp);
|
|
67
|
+
const icon = getTypeIcon(obs.type);
|
|
68
|
+
const tool = obs.tool_name || 'unknown';
|
|
69
|
+
const summary = truncate(obs.narrative || obs.summary || `${tool} operation`, 50);
|
|
70
|
+
|
|
71
|
+
console.log(`\x1b[90m${time}\x1b[0m ${icon} \x1b[1m#${obs.id}\x1b[0m [\x1b[33m${tool}\x1b[0m] ${summary}`);
|
|
72
|
+
lastId = obs.id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for new summaries
|
|
76
|
+
const newSummaries = db.prepare(`
|
|
77
|
+
SELECT id, created_at, request, learned, completed, next_steps
|
|
78
|
+
FROM summaries
|
|
79
|
+
WHERE id > ?
|
|
80
|
+
ORDER BY id ASC
|
|
81
|
+
`).all(lastSummaryId);
|
|
82
|
+
|
|
83
|
+
for (const sum of newSummaries) {
|
|
84
|
+
const time = formatTime(sum.created_at);
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(`\x1b[90m${time}\x1b[0m \x1b[32m📋 Session Summary #${sum.id}\x1b[0m`);
|
|
87
|
+
if (sum.request) console.log(` \x1b[36m请求:\x1b[0m ${truncate(sum.request, 70)}`);
|
|
88
|
+
if (sum.learned) console.log(` \x1b[35m学到:\x1b[0m ${truncate(sum.learned, 70)}`);
|
|
89
|
+
if (sum.completed) console.log(` \x1b[32m完成:\x1b[0m ${truncate(sum.completed, 70)}`);
|
|
90
|
+
if (sum.next_steps) console.log(` \x1b[33m下步:\x1b[0m ${truncate(sum.next_steps, 70)}`);
|
|
91
|
+
console.log('');
|
|
92
|
+
lastSummaryId = sum.id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Database might be locked, retry next interval
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Start polling
|
|
101
|
+
const interval = setInterval(checkNewObservations, POLL_INTERVAL);
|
|
102
|
+
|
|
103
|
+
// Handle graceful shutdown
|
|
104
|
+
process.on('SIGINT', () => {
|
|
105
|
+
console.log('\n\x1b[90m监控已停止\x1b[0m');
|
|
106
|
+
clearInterval(interval);
|
|
107
|
+
db.close();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Initial check
|
|
112
|
+
checkNewObservations();
|