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.
- package/HOOK.md +125 -0
- package/LICENSE +1 -1
- package/MCP.json +11 -0
- package/README.md +158 -167
- package/backfill-embeddings.js +79 -0
- package/context-builder.js +703 -0
- package/database.js +625 -0
- package/debug-logger.js +280 -0
- package/extractor.js +268 -0
- package/gateway-llm.js +250 -0
- package/handler.js +941 -0
- package/mcp-http-api.js +424 -0
- package/mcp-server.js +605 -0
- package/mem-get.sh +24 -0
- package/mem-search.sh +17 -0
- package/monitor.js +112 -0
- package/package.json +58 -30
- 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
|
@@ -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
|
-
[](https://www.npmjs.com/package/openclaw-mem)
|
|
6
|
-
[](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>
|
package/lib/context-builder.js
DELETED
|
@@ -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
|
-
};
|