mcp-learning-memory 0.2.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/README.md +394 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +424 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +8 -0
- package/dist/storage.d.ts +7 -0
- package/dist/storage.js +95 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +559 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.js +4 -0
- package/dist/validation.d.ts +31 -0
- package/dist/validation.js +153 -0
- package/package.json +47 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool implementations for MCP server
|
|
3
|
+
*/
|
|
4
|
+
import { loadMemory, saveMemory } from './storage.js';
|
|
5
|
+
import { validateInsightInput, normalizeTags, generateId, levenshteinDistance, validateListIndexParams, validateSearchQuery, } from './validation.js';
|
|
6
|
+
import { log } from './logger.js';
|
|
7
|
+
// Simple mutex to prevent concurrent read-modify-write races
|
|
8
|
+
class Mutex {
|
|
9
|
+
locked = false;
|
|
10
|
+
queue = [];
|
|
11
|
+
async acquire() {
|
|
12
|
+
if (!this.locked) {
|
|
13
|
+
this.locked = true;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await new Promise(resolve => this.queue.push(resolve));
|
|
17
|
+
}
|
|
18
|
+
release() {
|
|
19
|
+
const next = this.queue.shift();
|
|
20
|
+
if (next) {
|
|
21
|
+
next();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.locked = false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async runExclusive(fn) {
|
|
28
|
+
await this.acquire();
|
|
29
|
+
try {
|
|
30
|
+
return await fn();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
this.release();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const memoryMutex = new Mutex();
|
|
38
|
+
/**
|
|
39
|
+
* Calculate trust score based on helpful and harmful feedback
|
|
40
|
+
* Formula: (helpful - harmful * 2) / (helpful + harmful + 1)
|
|
41
|
+
*/
|
|
42
|
+
function calculateTrustScore(helpful, harmful) {
|
|
43
|
+
return Number(((helpful - harmful * 2) / (helpful + harmful + 1)).toFixed(2));
|
|
44
|
+
}
|
|
45
|
+
export async function addInsight(title, tags, content, overwrite = false, sourceUrl) {
|
|
46
|
+
try {
|
|
47
|
+
// Validate input
|
|
48
|
+
const validationError = validateInsightInput(title, tags, content, sourceUrl);
|
|
49
|
+
if (validationError) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
message: validationError,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Normalize tags and title
|
|
56
|
+
const normalizedTags = normalizeTags(tags);
|
|
57
|
+
const trimmedTitle = title.trim();
|
|
58
|
+
let resultId = '';
|
|
59
|
+
let wasOverwritten = false;
|
|
60
|
+
// Load, update, and save memory with mutex protection
|
|
61
|
+
await memoryMutex.runExclusive(async () => {
|
|
62
|
+
const memory = await loadMemory();
|
|
63
|
+
// Check for duplicate or similar title using Levenshtein distance
|
|
64
|
+
let existingIndex = -1;
|
|
65
|
+
let similarTitle = '';
|
|
66
|
+
for (let i = 0; i < memory.insights.length; i++) {
|
|
67
|
+
const insight = memory.insights[i];
|
|
68
|
+
const isExactMatch = insight.title.toLowerCase() === trimmedTitle.toLowerCase();
|
|
69
|
+
// For very short titles (< 3 chars), only check exact match to avoid false positives
|
|
70
|
+
// For longer titles (>= 3 chars), use Levenshtein distance < 3 (catches typos)
|
|
71
|
+
// Examples: "Bug Fix" → "Bux Fix" (distance=2), "Test" → "Fest" (distance=1)
|
|
72
|
+
const minLength = Math.min(insight.title.length, trimmedTitle.length);
|
|
73
|
+
let isSimilar = false;
|
|
74
|
+
if (isExactMatch) {
|
|
75
|
+
isSimilar = true;
|
|
76
|
+
}
|
|
77
|
+
else if (minLength >= 3) {
|
|
78
|
+
const distance = levenshteinDistance(insight.title, trimmedTitle);
|
|
79
|
+
isSimilar = distance < 3;
|
|
80
|
+
}
|
|
81
|
+
if (isSimilar) {
|
|
82
|
+
existingIndex = i;
|
|
83
|
+
similarTitle = insight.title;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (existingIndex !== -1) {
|
|
88
|
+
if (!overwrite) {
|
|
89
|
+
const error = new Error(`Similar title found: "${similarTitle}" is too similar to "${trimmedTitle}". Use overwrite=true to replace.`);
|
|
90
|
+
error.duplicate = true;
|
|
91
|
+
error.existingId = memory.insights[existingIndex].id;
|
|
92
|
+
error.existingTitle = similarTitle;
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
// Overwrite: keep ID and created date, update everything else
|
|
96
|
+
const existing = memory.insights[existingIndex];
|
|
97
|
+
existing.title = trimmedTitle;
|
|
98
|
+
existing.tags = normalizedTags;
|
|
99
|
+
existing.content = content.trim();
|
|
100
|
+
existing.lastUsed = new Date().toISOString();
|
|
101
|
+
existing.sourceUrl = sourceUrl?.trim();
|
|
102
|
+
resultId = existing.id;
|
|
103
|
+
wasOverwritten = true;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Calculate next ID number
|
|
107
|
+
let maxNum = 0;
|
|
108
|
+
for (const insight of memory.insights) {
|
|
109
|
+
const match = insight.id.match(/^ace-(\d+)$/);
|
|
110
|
+
if (match) {
|
|
111
|
+
const num = parseInt(match[1], 10);
|
|
112
|
+
if (num > maxNum)
|
|
113
|
+
maxNum = num;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const nextNum = maxNum + 1;
|
|
117
|
+
// Create new insight
|
|
118
|
+
const now = new Date().toISOString();
|
|
119
|
+
const insight = {
|
|
120
|
+
id: generateId(nextNum),
|
|
121
|
+
title: trimmedTitle,
|
|
122
|
+
tags: normalizedTags,
|
|
123
|
+
content: content.trim(),
|
|
124
|
+
helpful: 0,
|
|
125
|
+
harmful: 0,
|
|
126
|
+
created: now,
|
|
127
|
+
lastUsed: now,
|
|
128
|
+
sourceUrl: sourceUrl?.trim(),
|
|
129
|
+
};
|
|
130
|
+
memory.insights.push(insight);
|
|
131
|
+
resultId = insight.id;
|
|
132
|
+
}
|
|
133
|
+
await saveMemory(memory);
|
|
134
|
+
});
|
|
135
|
+
log.info(`${wasOverwritten ? 'Updated' : 'Added'} insight: ${resultId}`);
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
message: wasOverwritten
|
|
139
|
+
? `Insight updated successfully (ID: ${resultId})`
|
|
140
|
+
: `Insight added successfully with ID: ${resultId}`,
|
|
141
|
+
data: { id: resultId, overwritten: wasOverwritten },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
log.error('Error adding insight:', error);
|
|
146
|
+
// Check if this is a duplicate error
|
|
147
|
+
const err = error;
|
|
148
|
+
if (err.duplicate) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
message: err.message,
|
|
152
|
+
data: {
|
|
153
|
+
duplicate: true,
|
|
154
|
+
existingId: err.existingId,
|
|
155
|
+
existingTitle: err.existingTitle,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
message: `Failed to add insight: ${err.message}`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function listIndex(params = {}) {
|
|
166
|
+
try {
|
|
167
|
+
// Validate parameters
|
|
168
|
+
const validationError = validateListIndexParams(params);
|
|
169
|
+
if (validationError) {
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
message: validationError,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const memory = await loadMemory();
|
|
176
|
+
// Normalize filter tags to lowercase for consistent matching
|
|
177
|
+
const filterTags = params.tags ? normalizeTags(params.tags) : undefined;
|
|
178
|
+
const minHelpful = params.minHelpful ?? 0;
|
|
179
|
+
const sortBy = params.sortBy ?? 'lastUsed';
|
|
180
|
+
const limit = params.limit ?? 50;
|
|
181
|
+
const PREVIEW_LIMIT = 100;
|
|
182
|
+
let index = memory.insights.map(insight => {
|
|
183
|
+
const trustScore = calculateTrustScore(insight.helpful, insight.harmful);
|
|
184
|
+
const entry = {
|
|
185
|
+
id: insight.id,
|
|
186
|
+
title: insight.title,
|
|
187
|
+
tags: insight.tags,
|
|
188
|
+
helpful: insight.helpful,
|
|
189
|
+
harmful: insight.harmful,
|
|
190
|
+
created: insight.created,
|
|
191
|
+
lastUsed: insight.lastUsed,
|
|
192
|
+
contentPreview: insight.content.substring(0, PREVIEW_LIMIT),
|
|
193
|
+
contentTruncated: insight.content.length > PREVIEW_LIMIT,
|
|
194
|
+
sourceUrl: insight.sourceUrl,
|
|
195
|
+
trustScore,
|
|
196
|
+
};
|
|
197
|
+
if (trustScore < 0) {
|
|
198
|
+
entry.warning = 'Low trust score';
|
|
199
|
+
}
|
|
200
|
+
return entry;
|
|
201
|
+
});
|
|
202
|
+
// Apply tag filters (AND logic - insight must have ALL filter tags)
|
|
203
|
+
if (filterTags && filterTags.length > 0) {
|
|
204
|
+
index = index.filter(insight => filterTags.every(filterTag => insight.tags.some(insightTag => insightTag === filterTag)));
|
|
205
|
+
}
|
|
206
|
+
// Apply minHelpful filter
|
|
207
|
+
if (minHelpful > 0) {
|
|
208
|
+
index = index.filter(insight => insight.helpful >= minHelpful);
|
|
209
|
+
}
|
|
210
|
+
// Sort by specified field
|
|
211
|
+
if (sortBy === 'created') {
|
|
212
|
+
index.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
213
|
+
}
|
|
214
|
+
else if (sortBy === 'helpful') {
|
|
215
|
+
index.sort((a, b) => b.helpful - a.helpful);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Default: sort by lastUsed
|
|
219
|
+
index.sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime());
|
|
220
|
+
}
|
|
221
|
+
// Apply limit
|
|
222
|
+
const totalResults = index.length;
|
|
223
|
+
index = index.slice(0, limit);
|
|
224
|
+
const isFiltered = filterTags !== undefined || minHelpful > 0;
|
|
225
|
+
const isLimited = totalResults > index.length;
|
|
226
|
+
let message;
|
|
227
|
+
if (isFiltered && isLimited) {
|
|
228
|
+
message = `Found ${totalResults} insights (filtered, showing ${index.length})`;
|
|
229
|
+
}
|
|
230
|
+
else if (isFiltered) {
|
|
231
|
+
message = `Found ${totalResults} insights (filtered)`;
|
|
232
|
+
}
|
|
233
|
+
else if (isLimited) {
|
|
234
|
+
message = `Found ${totalResults} insights (showing ${index.length})`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
message = `Found ${index.length} insights`;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
message,
|
|
242
|
+
data: index,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
log.error('Error listing index:', error);
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
message: `Failed to list index: ${error.message}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export async function getDetail(id) {
|
|
254
|
+
try {
|
|
255
|
+
let insightData;
|
|
256
|
+
// Load, update, and save memory with mutex protection
|
|
257
|
+
await memoryMutex.runExclusive(async () => {
|
|
258
|
+
const memory = await loadMemory();
|
|
259
|
+
const insight = memory.insights.find(i => i.id === id);
|
|
260
|
+
if (!insight) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
// Update lastUsed timestamp on every access
|
|
264
|
+
// NOTE: This writes to disk on every getDetail call. For current requirements
|
|
265
|
+
// (< 10 ops/min, single user) this is acceptable. For high-frequency access,
|
|
266
|
+
// consider adding debounce or batching lastUsed updates.
|
|
267
|
+
insight.lastUsed = new Date().toISOString();
|
|
268
|
+
await saveMemory(memory);
|
|
269
|
+
insightData = insight;
|
|
270
|
+
});
|
|
271
|
+
if (!insightData) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
message: `Insight not found: ${id}`,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
message: 'Insight retrieved successfully',
|
|
280
|
+
data: insightData,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
log.error('Error getting detail:', error);
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
message: `Failed to get detail: ${error.message}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export async function markHelpful(id) {
|
|
292
|
+
try {
|
|
293
|
+
let helpfulCount = 0;
|
|
294
|
+
let found = false;
|
|
295
|
+
// Load, update, and save memory with mutex protection
|
|
296
|
+
await memoryMutex.runExclusive(async () => {
|
|
297
|
+
const memory = await loadMemory();
|
|
298
|
+
const insight = memory.insights.find(i => i.id === id);
|
|
299
|
+
if (!insight) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
insight.helpful += 1;
|
|
303
|
+
// Update lastUsed on feedback (acceptable for current load)
|
|
304
|
+
insight.lastUsed = new Date().toISOString();
|
|
305
|
+
await saveMemory(memory);
|
|
306
|
+
helpfulCount = insight.helpful;
|
|
307
|
+
found = true;
|
|
308
|
+
});
|
|
309
|
+
if (!found) {
|
|
310
|
+
return {
|
|
311
|
+
success: false,
|
|
312
|
+
message: `Insight not found: ${id}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
log.info(`Marked helpful: ${id}`);
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
message: `Insight marked as helpful (count: ${helpfulCount})`,
|
|
319
|
+
data: { helpful: helpfulCount },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
log.error('Error marking helpful:', error);
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
message: `Failed to mark helpful: ${error.message}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function markHarmful(id) {
|
|
331
|
+
try {
|
|
332
|
+
let harmfulCount = 0;
|
|
333
|
+
let found = false;
|
|
334
|
+
// Load, update, and save memory with mutex protection
|
|
335
|
+
await memoryMutex.runExclusive(async () => {
|
|
336
|
+
const memory = await loadMemory();
|
|
337
|
+
const insight = memory.insights.find(i => i.id === id);
|
|
338
|
+
if (!insight) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
insight.harmful += 1;
|
|
342
|
+
// Update lastUsed on feedback (acceptable for current load)
|
|
343
|
+
insight.lastUsed = new Date().toISOString();
|
|
344
|
+
await saveMemory(memory);
|
|
345
|
+
harmfulCount = insight.harmful;
|
|
346
|
+
found = true;
|
|
347
|
+
});
|
|
348
|
+
if (!found) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
message: `Insight not found: ${id}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
log.info(`Marked harmful: ${id}`);
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
message: `Insight marked as harmful (count: ${harmfulCount})`,
|
|
358
|
+
data: { harmful: harmfulCount },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
log.error('Error marking harmful:', error);
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
message: `Failed to mark harmful: ${error.message}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
export async function searchInsights(query, limit = 5) {
|
|
370
|
+
try {
|
|
371
|
+
// Validate query
|
|
372
|
+
const validationError = validateSearchQuery(query);
|
|
373
|
+
if (validationError) {
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
message: validationError,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const memory = await loadMemory();
|
|
380
|
+
// Normalize query
|
|
381
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
382
|
+
// Score each insight
|
|
383
|
+
const PREVIEW_LIMIT = 100;
|
|
384
|
+
const scoredInsights = memory.insights.map(insight => {
|
|
385
|
+
let baseScore = 0;
|
|
386
|
+
const normalizedTitle = insight.title.toLowerCase();
|
|
387
|
+
const normalizedContent = insight.content.toLowerCase();
|
|
388
|
+
// Title scoring
|
|
389
|
+
if (normalizedTitle === normalizedQuery) {
|
|
390
|
+
baseScore += 10; // Exact match in title
|
|
391
|
+
}
|
|
392
|
+
else if (normalizedTitle.includes(normalizedQuery)) {
|
|
393
|
+
baseScore += 5; // Partial match in title
|
|
394
|
+
}
|
|
395
|
+
// Content scoring
|
|
396
|
+
if (normalizedContent.includes(normalizedQuery)) {
|
|
397
|
+
baseScore += 3; // Match in content
|
|
398
|
+
}
|
|
399
|
+
// Tags scoring
|
|
400
|
+
// Note: tags are already normalized to lowercase during storage
|
|
401
|
+
for (const tag of insight.tags) {
|
|
402
|
+
if (tag.includes(normalizedQuery)) {
|
|
403
|
+
baseScore += 2; // Match in tag
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Helpful count bonus
|
|
407
|
+
baseScore += insight.helpful;
|
|
408
|
+
// Recent usage bonus (within 7 days)
|
|
409
|
+
const lastUsedDate = new Date(insight.lastUsed);
|
|
410
|
+
const now = new Date();
|
|
411
|
+
const daysDiff = (now.getTime() - lastUsedDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
412
|
+
if (daysDiff >= 0 && daysDiff <= 7) {
|
|
413
|
+
baseScore += 3;
|
|
414
|
+
}
|
|
415
|
+
// Calculate trust score and add to ranking
|
|
416
|
+
const trustScore = calculateTrustScore(insight.helpful, insight.harmful);
|
|
417
|
+
baseScore += trustScore * 5;
|
|
418
|
+
// Apply decay factor based on days since last used
|
|
419
|
+
// Decay from 1.0 to 0.1 over 90 days
|
|
420
|
+
const daysSinceUsed = Math.max(0, daysDiff);
|
|
421
|
+
const decayFactor = Math.max(0.1, 1 - daysSinceUsed / 90);
|
|
422
|
+
const effectiveScore = baseScore * decayFactor;
|
|
423
|
+
const entry = {
|
|
424
|
+
id: insight.id,
|
|
425
|
+
title: insight.title,
|
|
426
|
+
tags: insight.tags,
|
|
427
|
+
helpful: insight.helpful,
|
|
428
|
+
harmful: insight.harmful,
|
|
429
|
+
created: insight.created,
|
|
430
|
+
lastUsed: insight.lastUsed,
|
|
431
|
+
contentPreview: insight.content.substring(0, PREVIEW_LIMIT),
|
|
432
|
+
contentTruncated: insight.content.length > PREVIEW_LIMIT,
|
|
433
|
+
sourceUrl: insight.sourceUrl,
|
|
434
|
+
trustScore,
|
|
435
|
+
score: effectiveScore,
|
|
436
|
+
};
|
|
437
|
+
if (trustScore < 0) {
|
|
438
|
+
entry.warning = 'Low trust score';
|
|
439
|
+
}
|
|
440
|
+
return entry;
|
|
441
|
+
});
|
|
442
|
+
// Filter insights with score > 0
|
|
443
|
+
const matchedInsights = scoredInsights.filter(insight => insight.score > 0);
|
|
444
|
+
// Sort by score descending
|
|
445
|
+
matchedInsights.sort((a, b) => b.score - a.score);
|
|
446
|
+
// Apply limit
|
|
447
|
+
const limitedInsights = matchedInsights.slice(0, limit);
|
|
448
|
+
// Remove score field before returning
|
|
449
|
+
const results = limitedInsights.map(({ score, ...rest }) => rest);
|
|
450
|
+
if (results.length === 0) {
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
message: `No insights found matching query: "${query}"`,
|
|
454
|
+
data: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
success: true,
|
|
459
|
+
message: `Found ${results.length} insights matching query: "${query}"`,
|
|
460
|
+
data: results,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
log.error('Error searching insights:', error);
|
|
465
|
+
return {
|
|
466
|
+
success: false,
|
|
467
|
+
message: `Failed to search insights: ${error.message}`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
export async function deleteInsight(id) {
|
|
472
|
+
try {
|
|
473
|
+
let found = false;
|
|
474
|
+
// Load, update, and save memory with mutex protection
|
|
475
|
+
await memoryMutex.runExclusive(async () => {
|
|
476
|
+
const memory = await loadMemory();
|
|
477
|
+
const index = memory.insights.findIndex(i => i.id === id);
|
|
478
|
+
if (index === -1) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
memory.insights.splice(index, 1);
|
|
482
|
+
await saveMemory(memory);
|
|
483
|
+
found = true;
|
|
484
|
+
});
|
|
485
|
+
if (!found) {
|
|
486
|
+
return {
|
|
487
|
+
success: false,
|
|
488
|
+
message: 'Insight not found',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
log.info(`Deleted insight: ${id}`);
|
|
492
|
+
return {
|
|
493
|
+
success: true,
|
|
494
|
+
message: `Deleted ${id}`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
log.error('Error deleting insight:', error);
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
message: `Failed to delete insight: ${error.message}`,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
export async function exportMemory(format = 'json') {
|
|
506
|
+
try {
|
|
507
|
+
const memory = await loadMemory();
|
|
508
|
+
let data;
|
|
509
|
+
if (format === 'markdown') {
|
|
510
|
+
// Format as readable markdown
|
|
511
|
+
const lines = [
|
|
512
|
+
'# Learning Memory Export',
|
|
513
|
+
'',
|
|
514
|
+
`> Exported: ${new Date().toISOString()}`,
|
|
515
|
+
`> Total insights: ${memory.insights.length}`,
|
|
516
|
+
'',
|
|
517
|
+
'---',
|
|
518
|
+
'',
|
|
519
|
+
];
|
|
520
|
+
for (const insight of memory.insights) {
|
|
521
|
+
const trustScore = calculateTrustScore(insight.helpful, insight.harmful);
|
|
522
|
+
lines.push(`## ${insight.title}`);
|
|
523
|
+
lines.push('');
|
|
524
|
+
lines.push(`**ID:** ${insight.id}`);
|
|
525
|
+
lines.push(`**Tags:** ${insight.tags.join(', ')}`);
|
|
526
|
+
lines.push(`**Stats:** Helpful ${insight.helpful} / Harmful ${insight.harmful} (Trust: ${trustScore})`);
|
|
527
|
+
lines.push(`**Created:** ${insight.created}`);
|
|
528
|
+
lines.push(`**Last Used:** ${insight.lastUsed}`);
|
|
529
|
+
if (insight.sourceUrl) {
|
|
530
|
+
lines.push(`**Source:** ${insight.sourceUrl}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push('### Content');
|
|
534
|
+
lines.push('');
|
|
535
|
+
lines.push(insight.content);
|
|
536
|
+
lines.push('');
|
|
537
|
+
lines.push('---');
|
|
538
|
+
lines.push('');
|
|
539
|
+
}
|
|
540
|
+
data = lines.join('\n');
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
// Format as JSON
|
|
544
|
+
data = JSON.stringify(memory, null, 2);
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
success: true,
|
|
548
|
+
message: `Exported ${memory.insights.length} insights as ${format}`,
|
|
549
|
+
data: { data, count: memory.insights.length },
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
log.error('Error exporting memory:', error);
|
|
554
|
+
return {
|
|
555
|
+
success: false,
|
|
556
|
+
message: `Failed to export memory: ${error.message}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core data structures for ACE Flash Memory MCP
|
|
3
|
+
*/
|
|
4
|
+
export interface Insight {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
tags: string[];
|
|
8
|
+
content: string;
|
|
9
|
+
helpful: number;
|
|
10
|
+
harmful: number;
|
|
11
|
+
created: string;
|
|
12
|
+
lastUsed: string;
|
|
13
|
+
sourceUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface Memory {
|
|
16
|
+
insights: Insight[];
|
|
17
|
+
}
|
|
18
|
+
export interface IndexEntry {
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
helpful: number;
|
|
23
|
+
harmful: number;
|
|
24
|
+
created: string;
|
|
25
|
+
lastUsed: string;
|
|
26
|
+
contentPreview: string;
|
|
27
|
+
contentTruncated: boolean;
|
|
28
|
+
sourceUrl?: string;
|
|
29
|
+
trustScore: number;
|
|
30
|
+
warning?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ToolResponse {
|
|
33
|
+
success: boolean;
|
|
34
|
+
message: string;
|
|
35
|
+
data?: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface ListIndexParams {
|
|
38
|
+
tags?: string[];
|
|
39
|
+
minHelpful?: number;
|
|
40
|
+
sortBy?: 'created' | 'lastUsed' | 'helpful';
|
|
41
|
+
limit?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface SearchInsightsParams {
|
|
44
|
+
query: string;
|
|
45
|
+
limit?: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ScoredInsight extends IndexEntry {
|
|
48
|
+
score: number;
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation and normalization
|
|
3
|
+
*/
|
|
4
|
+
export declare const LIMITS: {
|
|
5
|
+
readonly TITLE_MAX: 200;
|
|
6
|
+
readonly TAGS_MAX: 10;
|
|
7
|
+
readonly TAG_LENGTH_MAX: 50;
|
|
8
|
+
readonly CONTENT_MAX: 10000;
|
|
9
|
+
readonly SOURCE_URL_MAX: 2000;
|
|
10
|
+
};
|
|
11
|
+
export declare function validateInsightInput(title: string, tags: string[], content: string, sourceUrl?: string): string | null;
|
|
12
|
+
export declare function normalizeTags(tags: string[]): string[];
|
|
13
|
+
export declare function generateId(nextNumber: number): string;
|
|
14
|
+
/**
|
|
15
|
+
* Calculate Levenshtein distance between two strings (case-insensitive)
|
|
16
|
+
* Used for detecting similar titles in auto-deduplication
|
|
17
|
+
*/
|
|
18
|
+
export declare function levenshteinDistance(str1: string, str2: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Validate parameters for list_index tool
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateListIndexParams(params: {
|
|
23
|
+
tags?: string[];
|
|
24
|
+
minHelpful?: number;
|
|
25
|
+
sortBy?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Validate search query for search_insights tool
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateSearchQuery(query: string): string | null;
|