openclaw-mem 1.0.4 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/HOOK.md +125 -0
  2. package/LICENSE +1 -1
  3. package/MCP.json +11 -0
  4. package/README.md +158 -167
  5. package/backfill-embeddings.js +79 -0
  6. package/context-builder.js +703 -0
  7. package/database.js +625 -0
  8. package/debug-logger.js +280 -0
  9. package/extractor.js +268 -0
  10. package/gateway-llm.js +250 -0
  11. package/handler.js +941 -0
  12. package/mcp-http-api.js +424 -0
  13. package/mcp-server.js +605 -0
  14. package/mem-get.sh +24 -0
  15. package/mem-search.sh +17 -0
  16. package/monitor.js +112 -0
  17. package/package.json +58 -30
  18. package/realtime-monitor.js +371 -0
  19. package/session-watcher.js +192 -0
  20. package/setup.js +114 -0
  21. package/sync-recent.js +63 -0
  22. package/README_CN.md +0 -201
  23. package/bin/openclaw-mem.js +0 -117
  24. package/docs/locales/README_AR.md +0 -35
  25. package/docs/locales/README_DE.md +0 -35
  26. package/docs/locales/README_ES.md +0 -35
  27. package/docs/locales/README_FR.md +0 -35
  28. package/docs/locales/README_HE.md +0 -35
  29. package/docs/locales/README_HI.md +0 -35
  30. package/docs/locales/README_ID.md +0 -35
  31. package/docs/locales/README_IT.md +0 -35
  32. package/docs/locales/README_JA.md +0 -57
  33. package/docs/locales/README_KO.md +0 -35
  34. package/docs/locales/README_NL.md +0 -35
  35. package/docs/locales/README_PL.md +0 -35
  36. package/docs/locales/README_PT.md +0 -35
  37. package/docs/locales/README_RU.md +0 -35
  38. package/docs/locales/README_TH.md +0 -35
  39. package/docs/locales/README_TR.md +0 -35
  40. package/docs/locales/README_UK.md +0 -35
  41. package/docs/locales/README_VI.md +0 -35
  42. package/docs/logo.svg +0 -32
  43. package/lib/context-builder.js +0 -415
  44. package/lib/database.js +0 -309
  45. package/lib/handler.js +0 -494
  46. package/scripts/commands.js +0 -141
  47. package/scripts/init.js +0 -248
package/mcp-server.js ADDED
@@ -0,0 +1,605 @@
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
+ import { callGatewayEmbeddings } from './gateway-llm.js';
17
+
18
+ // ============ 工具函数 ============
19
+
20
+ function formatTime(timestamp) {
21
+ if (!timestamp) return '';
22
+ const date = new Date(timestamp);
23
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
24
+ }
25
+
26
+ function formatDate(timestamp) {
27
+ if (!timestamp) return '';
28
+ const date = new Date(timestamp);
29
+ if (Number.isNaN(date.getTime())) return '';
30
+ return date.toISOString().split('T')[0];
31
+ }
32
+
33
+ function formatDateHeading(dateOrKey) {
34
+ if (!dateOrKey) return '';
35
+ let date;
36
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateOrKey)) {
37
+ date = new Date(`${dateOrKey}T00:00:00`);
38
+ } else {
39
+ date = new Date(dateOrKey);
40
+ }
41
+ if (Number.isNaN(date.getTime())) return '';
42
+ return date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
43
+ }
44
+
45
+ function truncateText(text, max = 80) {
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, Math.max(0, max - 3)) + '...';
50
+ }
51
+
52
+ function estimateTokens(text) {
53
+ if (!text) return 0;
54
+ return Math.ceil(String(text).length / 4);
55
+ }
56
+
57
+ // Type mapping
58
+ const TYPE_EMOJI = {
59
+ 'session-request': '📋',
60
+ 'discovery': '🔵',
61
+ 'bugfix': '🔴',
62
+ 'feature': '🟣',
63
+ 'refactor': '🔄',
64
+ 'change': '✅',
65
+ 'decision': '⚖️',
66
+ 'problem-solution': '💡',
67
+ 'gotcha': '⚠️',
68
+ };
69
+
70
+ function getTypeLabel(observation) {
71
+ const type = observation?.type || 'discovery';
72
+ return TYPE_EMOJI[type] || '🔵';
73
+ }
74
+
75
+ function normalizeIds(input) {
76
+ const ids = [];
77
+ const pushId = (value) => {
78
+ if (value === null || value === undefined) return;
79
+ const cleaned = String(value).replace(/^#/, '').trim();
80
+ if (!cleaned) return;
81
+ const parsed = Number(cleaned);
82
+ if (!Number.isNaN(parsed)) ids.push(parsed);
83
+ };
84
+
85
+ if (Array.isArray(input)) {
86
+ input.forEach(pushId);
87
+ return ids;
88
+ }
89
+
90
+ if (typeof input === 'string') {
91
+ input.split(/[,\s]+/).forEach(pushId);
92
+ return ids;
93
+ }
94
+
95
+ pushId(input);
96
+ return ids;
97
+ }
98
+
99
+ // ============ 搜索功能 ============
100
+
101
+ /**
102
+ * Hybrid search: merge FTS5 keyword results with vector KNN results.
103
+ * FTS results get fts_score (normalized 0-1), vector results get vec_score (1 - distance).
104
+ * Results found in both get a 0.2 intersection bonus.
105
+ */
106
+ function mergeHybridResults(ftsResults, vectorResults, limit) {
107
+ // Normalize FTS scores (rank is negative, lower is better)
108
+ let ftsMin = Infinity, ftsMax = -Infinity;
109
+ for (const r of ftsResults) {
110
+ const rank = Math.abs(r.rank ?? 0);
111
+ if (rank < ftsMin) ftsMin = rank;
112
+ if (rank > ftsMax) ftsMax = rank;
113
+ }
114
+ const ftsRange = ftsMax - ftsMin || 1;
115
+
116
+ const scoreMap = new Map(); // id -> { obs, ftsScore, vecScore }
117
+
118
+ for (const r of ftsResults) {
119
+ const rank = Math.abs(r.rank ?? 0);
120
+ const ftsScore = 1 - ((rank - ftsMin) / ftsRange); // normalize to 0-1, higher is better
121
+ scoreMap.set(r.id, { obs: r, ftsScore, vecScore: 0 });
122
+ }
123
+
124
+ for (const v of vectorResults) {
125
+ const vecScore = 1 - (v.distance ?? 0); // cosine distance -> similarity
126
+ const existing = scoreMap.get(v.observation_id);
127
+ if (existing) {
128
+ existing.vecScore = vecScore;
129
+ } else {
130
+ // Need to fetch the full observation for vector-only results
131
+ const obs = database.getObservation(v.observation_id);
132
+ if (obs) {
133
+ scoreMap.set(v.observation_id, { obs, ftsScore: 0, vecScore });
134
+ }
135
+ }
136
+ }
137
+
138
+ // Calculate combined scores
139
+ const scored = [];
140
+ for (const [id, entry] of scoreMap) {
141
+ const { obs, ftsScore, vecScore } = entry;
142
+ const inBoth = ftsScore > 0 && vecScore > 0;
143
+ const combined = (0.4 * ftsScore) + (0.6 * vecScore) + (inBoth ? 0.2 : 0);
144
+ scored.push({ obs, combined, ftsScore, vecScore });
145
+ }
146
+
147
+ scored.sort((a, b) => b.combined - a.combined);
148
+ return scored.slice(0, limit);
149
+ }
150
+
151
+ async function search(args = {}) {
152
+ const query = typeof args === 'string' ? args : (args.query || args.q || '*');
153
+ const limit = args.limit ?? args.maxResults ?? 30;
154
+ const project = args.project || null;
155
+ const type = args.type || args.obs_type || null;
156
+ const dateStart = args.dateStart || null;
157
+ const dateEnd = args.dateEnd || null;
158
+
159
+ let results;
160
+
161
+ if (query === '*' || !query) {
162
+ // 获取最近的 observations — no embedding needed for recent listing
163
+ results = database.getRecentObservations(project, limit * 2);
164
+ } else {
165
+ // Hybrid search: FTS5 + vector KNN
166
+ const ftsResults = database.searchObservations(query, limit * 2);
167
+
168
+ // Try vector search in parallel
169
+ let vectorResults = [];
170
+ try {
171
+ const embedding = await callGatewayEmbeddings(query);
172
+ if (embedding) {
173
+ vectorResults = database.searchByVector(embedding, limit * 2);
174
+ }
175
+ } catch (err) {
176
+ console.error('[openclaw-mem-mcp] Vector search error:', err.message);
177
+ }
178
+
179
+ if (vectorResults.length > 0) {
180
+ // Merge hybrid results
181
+ const merged = mergeHybridResults(ftsResults, vectorResults, limit * 2);
182
+ results = merged.map(m => m.obs);
183
+ console.error(`[openclaw-mem-mcp] Hybrid search: ${ftsResults.length} FTS + ${vectorResults.length} vector → ${results.length} merged`);
184
+ } else {
185
+ // Fallback to FTS-only
186
+ results = ftsResults;
187
+ console.error(`[openclaw-mem-mcp] FTS-only search: ${results.length} results`);
188
+ }
189
+ }
190
+
191
+ // 过滤
192
+ if (type) {
193
+ results = results.filter(r => r.type === type);
194
+ }
195
+ if (dateStart) {
196
+ const start = new Date(dateStart).getTime();
197
+ results = results.filter(r => new Date(r.timestamp).getTime() >= start);
198
+ }
199
+ if (dateEnd) {
200
+ const end = new Date(dateEnd).getTime() + 86400000; // 包含当天
201
+ results = results.filter(r => new Date(r.timestamp).getTime() < end);
202
+ }
203
+
204
+ results = results.slice(0, limit);
205
+
206
+ if (results.length === 0) {
207
+ return `No observations found for query: "${query}"`;
208
+ }
209
+
210
+ // 按日期分组
211
+ const grouped = new Map();
212
+ for (const obs of results) {
213
+ const dateKey = formatDate(obs.timestamp) || 'Unknown';
214
+ if (!grouped.has(dateKey)) {
215
+ grouped.set(dateKey, []);
216
+ }
217
+ grouped.get(dateKey).push(obs);
218
+ }
219
+
220
+ const lines = [
221
+ `Found ${results.length} result(s) matching "${query}"`,
222
+ ''
223
+ ];
224
+
225
+ for (const [dateKey, obs] of grouped.entries()) {
226
+ const heading = formatDateHeading(dateKey) || dateKey;
227
+ lines.push(`### ${heading}`);
228
+ lines.push('');
229
+ lines.push('| ID | Time | T | Title | Read |');
230
+ lines.push('|----|------|---|-------|------|');
231
+
232
+ for (const o of obs) {
233
+ const id = `#${o.id}`;
234
+ const time = formatTime(o.timestamp);
235
+ const typeLabel = getTypeLabel(o);
236
+ const title = truncateText(o.narrative || o.summary || `${o.tool_name} operation`, 60);
237
+ const tokens = `~${o.tokens_read || estimateTokens(title)}`;
238
+ lines.push(`| ${id} | ${time} | ${typeLabel} | ${title} | ${tokens} |`);
239
+ }
240
+ lines.push('');
241
+ }
242
+
243
+ lines.push(`*Use \`timeline\` or \`get_observations\` for full details.*`);
244
+
245
+ return lines.join('\n');
246
+ }
247
+
248
+ // ============ Timeline 功能 ============
249
+
250
+ function timeline(args = {}) {
251
+ let anchorId;
252
+
253
+ if (typeof args === 'number' || typeof args === 'string') {
254
+ anchorId = Number(String(args).replace(/^#/, ''));
255
+ } else {
256
+ const anchor = args.anchor ?? args.id ?? args.observation_id ?? args.observationId;
257
+
258
+ // 如果提供了 query,自动查找 anchor
259
+ if (!anchor && args.query) {
260
+ const searchResults = database.searchObservations(args.query, 1);
261
+ if (searchResults.length > 0) {
262
+ anchorId = searchResults[0].id;
263
+ }
264
+ } else {
265
+ anchorId = Number(String(anchor ?? '').replace(/^#/, ''));
266
+ }
267
+ }
268
+
269
+ if (Number.isNaN(anchorId)) {
270
+ return 'No anchor ID provided. Use timeline(anchor=<ID>) or timeline(query="...")';
271
+ }
272
+
273
+ const depthBefore = Number(args.depth_before ?? args.before ?? 3);
274
+ const depthAfter = Number(args.depth_after ?? args.after ?? 2);
275
+
276
+ const anchor = database.getObservation(anchorId);
277
+ if (!anchor) {
278
+ return `Observation #${anchorId} not found`;
279
+ }
280
+
281
+ // 获取周围的 observations
282
+ const allObs = database.getRecentObservations(null, 100);
283
+ const anchorIdx = allObs.findIndex(o => o.id === anchorId);
284
+
285
+ if (anchorIdx === -1) {
286
+ // 只返回 anchor 本身
287
+ return buildFullDetails([anchor], 1);
288
+ }
289
+
290
+ // 注意:列表是 DESC 排序,所以 "after" 在时间上是 "before" 在索引上
291
+ const startIdx = Math.max(0, anchorIdx - depthAfter);
292
+ const endIdx = Math.min(allObs.length, anchorIdx + depthBefore + 1);
293
+ const timelineObs = allObs.slice(startIdx, endIdx).reverse();
294
+
295
+ const lines = [
296
+ `## Timeline around #${anchorId}`,
297
+ '',
298
+ '| | Time | T | ID | Title |',
299
+ '|---|------|---|-----|-------|'
300
+ ];
301
+
302
+ for (const o of timelineObs) {
303
+ const marker = o.id === anchorId ? '→' : '';
304
+ const time = formatTime(o.timestamp);
305
+ const typeLabel = getTypeLabel(o);
306
+ const title = truncateText(o.narrative || o.summary || `${o.tool_name} operation`, 70);
307
+ lines.push(`| ${marker} | ${time} | ${typeLabel} | #${o.id} | ${title} |`);
308
+ }
309
+
310
+ lines.push('');
311
+ lines.push(`*Use \`get_observations(ids=[...])\` for full details.*`);
312
+
313
+ return lines.join('\n');
314
+ }
315
+
316
+ // ============ Get Observations 功能 ============
317
+
318
+ function buildFullDetails(observations, limit = 10) {
319
+ if (!observations || observations.length === 0) {
320
+ return 'No observations found.';
321
+ }
322
+
323
+ const toShow = observations.slice(0, limit);
324
+ const lines = [];
325
+
326
+ for (const o of toShow) {
327
+ const title = (o.narrative || o.summary || `${o.tool_name} operation`).replace(/\s+/g, ' ').trim();
328
+ const typeLabel = getTypeLabel(o);
329
+ const dateLabel = formatDateHeading(o.timestamp);
330
+ const timeLabel = formatTime(o.timestamp);
331
+
332
+ lines.push(`## #${o.id} ${typeLabel} ${truncateText(title, 100)}`);
333
+ lines.push('');
334
+
335
+ if (dateLabel || timeLabel) {
336
+ lines.push(`**Time**: ${[dateLabel, timeLabel].filter(Boolean).join(' ')}`);
337
+ }
338
+ if (o.tool_name) {
339
+ lines.push(`**Tool**: ${o.tool_name}`);
340
+ }
341
+ if (o.type) {
342
+ lines.push(`**Type**: ${o.type}`);
343
+ }
344
+ lines.push('');
345
+
346
+ if (o.summary) {
347
+ lines.push(`**Summary**: ${o.summary}`);
348
+ lines.push('');
349
+ }
350
+
351
+ if (o.narrative && o.narrative !== o.summary) {
352
+ lines.push(`**Narrative**: ${o.narrative}`);
353
+ lines.push('');
354
+ }
355
+
356
+ // 解析 facts
357
+ let facts = o.facts;
358
+ if (typeof facts === 'string') {
359
+ try {
360
+ facts = JSON.parse(facts);
361
+ } catch {
362
+ facts = null;
363
+ }
364
+ }
365
+ if (Array.isArray(facts) && facts.length > 0) {
366
+ lines.push('**Facts**:');
367
+ for (const fact of facts.slice(0, 8)) {
368
+ if (fact) lines.push(`- ${fact}`);
369
+ }
370
+ lines.push('');
371
+ }
372
+
373
+ // 文件信息
374
+ let filesRead = o.files_read;
375
+ let filesModified = o.files_modified;
376
+ if (typeof filesRead === 'string') {
377
+ try { filesRead = JSON.parse(filesRead); } catch { filesRead = null; }
378
+ }
379
+ if (typeof filesModified === 'string') {
380
+ try { filesModified = JSON.parse(filesModified); } catch { filesModified = null; }
381
+ }
382
+
383
+ if (Array.isArray(filesRead) && filesRead.length > 0) {
384
+ lines.push(`**Files Read**: ${filesRead.map(f => `\`${f}\``).join(', ')}`);
385
+ }
386
+ if (Array.isArray(filesModified) && filesModified.length > 0) {
387
+ lines.push(`**Files Modified**: ${filesModified.map(f => `\`${f}\``).join(', ')}`);
388
+ }
389
+
390
+ // Tool input 关键信息
391
+ let input = o.tool_input;
392
+ if (typeof input === 'string') {
393
+ try { input = JSON.parse(input); } catch { input = {}; }
394
+ }
395
+ input = input || {};
396
+
397
+ const inputFacts = [];
398
+ if (input.file_path) inputFacts.push(`File: \`${input.file_path}\``);
399
+ if (input.command) inputFacts.push(`Command: \`${input.command.slice(0, 100)}\``);
400
+ if (input.pattern) inputFacts.push(`Pattern: \`${input.pattern}\``);
401
+ if (input.query) inputFacts.push(`Query: ${input.query.slice(0, 100)}`);
402
+ if (input.url) inputFacts.push(`URL: ${input.url}`);
403
+
404
+ if (inputFacts.length > 0) {
405
+ lines.push('');
406
+ lines.push('**Details**: ' + inputFacts.join(' | '));
407
+ }
408
+
409
+ lines.push('');
410
+ lines.push('---');
411
+ lines.push('');
412
+ }
413
+
414
+ return lines.join('\n');
415
+ }
416
+
417
+ function get_observations(args = {}) {
418
+ const ids = Array.isArray(args)
419
+ ? normalizeIds(args)
420
+ : normalizeIds(args.ids ?? args.id ?? args.observation_ids ?? args.observationIds);
421
+
422
+ if (!ids.length) {
423
+ return 'No observation IDs provided. Use get_observations(ids=[1, 2, 3])';
424
+ }
425
+
426
+ const observations = database.getObservations(ids);
427
+
428
+ if (observations.length === 0) {
429
+ return `No observations found for IDs: ${ids.join(', ')}`;
430
+ }
431
+
432
+ return buildFullDetails(observations, observations.length);
433
+ }
434
+
435
+ // ============ __IMPORTANT 功能 ============
436
+
437
+ function __IMPORTANT() {
438
+ return `## 3-LAYER MEMORY RETRIEVAL WORKFLOW
439
+
440
+ **ALWAYS follow this workflow to minimize token usage:**
441
+
442
+ 1. **search(query)** → Get index with IDs (~50-100 tokens/result)
443
+ \`search(query="...", limit=20)\`
444
+
445
+ 2. **timeline(anchor=ID)** → Get context around interesting results
446
+ \`timeline(anchor=<ID>, depth_before=3, depth_after=2)\`
447
+
448
+ 3. **get_observations(ids=[...])** → Fetch full details ONLY for filtered IDs
449
+ \`get_observations(ids=[1, 2, 3])\`
450
+
451
+ **NEVER fetch full details without filtering first. 10x token savings.**
452
+
453
+ ### Quick Examples
454
+
455
+ - Search recent: \`search(query="*", limit=10)\`
456
+ - Search topic: \`search(query="database migration")\`
457
+ - Get context: \`timeline(anchor=123)\`
458
+ - Get details: \`get_observations(ids=[123, 124, 125])\`
459
+ `;
460
+ }
461
+
462
+ // ============ MCP Server 设置 ============
463
+
464
+ const TOOLS = [
465
+ {
466
+ name: '__IMPORTANT',
467
+ description: '3-LAYER WORKFLOW: 1. search(query) → index 2. timeline(anchor) → context 3. get_observations(ids) → details. NEVER fetch details without filtering first.',
468
+ inputSchema: {
469
+ type: 'object',
470
+ properties: {},
471
+ },
472
+ },
473
+ {
474
+ name: 'search',
475
+ description: 'Step 1: Search memory. Returns index with IDs. Params: query, limit, project, type, dateStart, dateEnd',
476
+ inputSchema: {
477
+ type: 'object',
478
+ properties: {
479
+ query: { type: 'string', description: 'Search query (use "*" for recent)' },
480
+ limit: { type: 'number', description: 'Max results (default 30)' },
481
+ project: { type: 'string', description: 'Filter by project path' },
482
+ type: { type: 'string', description: 'Filter by type: discovery, bugfix, feature, refactor, change, decision' },
483
+ dateStart: { type: 'string', description: 'Start date (YYYY-MM-DD)' },
484
+ dateEnd: { type: 'string', description: 'End date (YYYY-MM-DD)' },
485
+ },
486
+ },
487
+ },
488
+ {
489
+ name: 'timeline',
490
+ description: 'Step 2: Get context around results. Params: anchor (observation ID) OR query (finds anchor automatically), depth_before, depth_after',
491
+ inputSchema: {
492
+ type: 'object',
493
+ properties: {
494
+ anchor: { type: 'number', description: 'Observation ID to center on' },
495
+ query: { type: 'string', description: 'Auto-find anchor from search query' },
496
+ depth_before: { type: 'number', description: 'Observations before anchor (default 3)' },
497
+ depth_after: { type: 'number', description: 'Observations after anchor (default 2)' },
498
+ },
499
+ },
500
+ },
501
+ {
502
+ name: 'get_observations',
503
+ description: 'Step 3: Fetch full details for filtered IDs. Params: ids (array of observation IDs, required)',
504
+ inputSchema: {
505
+ type: 'object',
506
+ properties: {
507
+ ids: {
508
+ type: 'array',
509
+ items: { type: 'number' },
510
+ description: 'Array of observation IDs to fetch (required)',
511
+ },
512
+ },
513
+ required: ['ids'],
514
+ },
515
+ },
516
+ ];
517
+
518
+ // 创建 MCP Server
519
+ const server = new Server(
520
+ {
521
+ name: 'openclaw-mem-search',
522
+ version: '1.0.0',
523
+ },
524
+ {
525
+ capabilities: {
526
+ tools: {},
527
+ },
528
+ }
529
+ );
530
+
531
+ // 注册工具列表
532
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
533
+ return { tools: TOOLS };
534
+ });
535
+
536
+ // 处理工具调用
537
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
538
+ const { name, arguments: args } = request.params;
539
+
540
+ console.error(`[openclaw-mem-mcp] Tool called: ${name}`);
541
+
542
+ try {
543
+ let result;
544
+
545
+ switch (name) {
546
+ case '__IMPORTANT':
547
+ result = __IMPORTANT();
548
+ break;
549
+
550
+ case 'search':
551
+ result = await search(args || {});
552
+ break;
553
+
554
+ case 'timeline':
555
+ result = timeline(args || {});
556
+ break;
557
+
558
+ case 'get_observations':
559
+ result = get_observations(args || {});
560
+ break;
561
+
562
+ default:
563
+ throw new Error(`Unknown tool: ${name}`);
564
+ }
565
+
566
+ return {
567
+ content: [
568
+ {
569
+ type: 'text',
570
+ text: result,
571
+ },
572
+ ],
573
+ };
574
+ } catch (error) {
575
+ console.error(`[openclaw-mem-mcp] Error:`, error.message);
576
+ return {
577
+ content: [
578
+ {
579
+ type: 'text',
580
+ text: `Error: ${error.message}`,
581
+ },
582
+ ],
583
+ isError: true,
584
+ };
585
+ }
586
+ });
587
+
588
+ // 启动服务器
589
+ async function main() {
590
+ const transport = new StdioServerTransport();
591
+ await server.connect(transport);
592
+ console.error('[openclaw-mem-mcp] MCP Server started (stdio)');
593
+
594
+ // Preload embedding model in background so first search doesn't timeout
595
+ callGatewayEmbeddings('warmup').then(() => {
596
+ console.error('[openclaw-mem-mcp] Embedding model preloaded');
597
+ }).catch(() => {
598
+ console.error('[openclaw-mem-mcp] Embedding model preload failed (will retry on first search)');
599
+ });
600
+ }
601
+
602
+ main().catch((error) => {
603
+ console.error('[openclaw-mem-mcp] Fatal error:', error);
604
+ process.exit(1);
605
+ });
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}"