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/index.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ACE Flash Memory MCP Server
|
|
4
|
+
* Model Context Protocol server for managing insights with feedback
|
|
5
|
+
*/
|
|
6
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { log } from './logger.js';
|
|
10
|
+
import { addInsight, listIndex, getDetail, markHelpful, markHarmful, searchInsights, deleteInsight, exportMemory, } from './tools.js';
|
|
11
|
+
const server = new Server({
|
|
12
|
+
name: 'mcp-learning-memory',
|
|
13
|
+
version: '0.2.0',
|
|
14
|
+
}, {
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
// List available tools
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
|
+
return {
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: 'add_insight',
|
|
25
|
+
description: 'Add a new insight to memory',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
title: {
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'Title of the insight (max 200 chars)',
|
|
32
|
+
},
|
|
33
|
+
tags: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Tags for categorization (max 10 tags, 50 chars each)',
|
|
37
|
+
},
|
|
38
|
+
content: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
description: 'Content of the insight (max 10000 chars)',
|
|
41
|
+
},
|
|
42
|
+
overwrite: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'If true, overwrite existing insight with same title (default: false)',
|
|
45
|
+
},
|
|
46
|
+
sourceUrl: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'Optional source URL for the insight (max 2000 chars)',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ['title', 'tags', 'content'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'list_index',
|
|
56
|
+
description: 'List all insights (index view without full content) with optional filtering and sorting',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
tags: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { type: 'string' },
|
|
63
|
+
description: 'Filter by tags (AND logic - insight must have all specified tags)',
|
|
64
|
+
},
|
|
65
|
+
minHelpful: {
|
|
66
|
+
type: 'number',
|
|
67
|
+
description: 'Minimum helpful count (default: 0)',
|
|
68
|
+
},
|
|
69
|
+
sortBy: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
enum: ['created', 'lastUsed', 'helpful'],
|
|
72
|
+
description: 'Sort field (default: lastUsed)',
|
|
73
|
+
},
|
|
74
|
+
limit: {
|
|
75
|
+
type: 'number',
|
|
76
|
+
description: 'Maximum number of results (default: 50)',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'get_detail',
|
|
83
|
+
description: 'Get full details of a specific insight',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
id: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
description: 'ID of the insight to retrieve',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
required: ['id'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'mark_helpful',
|
|
97
|
+
description: 'Mark an insight as helpful',
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
id: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'ID of the insight to mark as helpful',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ['id'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'mark_harmful',
|
|
111
|
+
description: 'Mark an insight as harmful',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
id: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
description: 'ID of the insight to mark as harmful',
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
required: ['id'],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'search_insights',
|
|
125
|
+
description: 'Search insights by query string with contextual ranking',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
query: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: 'Search query string (searches in title, content, and tags)',
|
|
132
|
+
},
|
|
133
|
+
limit: {
|
|
134
|
+
type: 'number',
|
|
135
|
+
description: 'Maximum number of results (default: 5)',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ['query'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'delete_insight',
|
|
143
|
+
description: 'Delete an insight by ID',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
id: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
description: 'ID of the insight to delete',
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ['id'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'export_memory',
|
|
157
|
+
description: 'Export all insights in JSON or Markdown format',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
format: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: ['json', 'markdown'],
|
|
164
|
+
description: 'Export format: json (default) or markdown',
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
// Handle tool calls
|
|
173
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
174
|
+
const { name, arguments: args } = request.params;
|
|
175
|
+
try {
|
|
176
|
+
switch (name) {
|
|
177
|
+
case 'add_insight': {
|
|
178
|
+
const { title, tags, content, overwrite, sourceUrl } = args;
|
|
179
|
+
const addResult = await addInsight(title, tags, content, overwrite ?? false, sourceUrl);
|
|
180
|
+
// Return human-readable format
|
|
181
|
+
if (addResult.success) {
|
|
182
|
+
const data = addResult.data;
|
|
183
|
+
return {
|
|
184
|
+
content: [{
|
|
185
|
+
type: 'text',
|
|
186
|
+
text: `${addResult.message}\n\nID: ${data.id}\nOverwritten: ${data.overwritten ? 'Yes' : 'No'}`,
|
|
187
|
+
}],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Handle error case (including duplicate detection)
|
|
192
|
+
let errorText = addResult.message;
|
|
193
|
+
if (addResult.data) {
|
|
194
|
+
const dupData = addResult.data;
|
|
195
|
+
if (dupData.duplicate) {
|
|
196
|
+
errorText += `\n\nExisting insight:\nID: ${dupData.existingId}\nTitle: "${dupData.existingTitle}"`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: 'text', text: errorText }],
|
|
201
|
+
isError: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
case 'list_index': {
|
|
206
|
+
const { tags, minHelpful, sortBy, limit } = args;
|
|
207
|
+
const listResult = await listIndex({ tags, minHelpful, sortBy, limit });
|
|
208
|
+
// Return human-readable text format instead of JSON
|
|
209
|
+
if (listResult.success && Array.isArray(listResult.data)) {
|
|
210
|
+
const entries = listResult.data;
|
|
211
|
+
if (entries.length === 0) {
|
|
212
|
+
return {
|
|
213
|
+
content: [{ type: 'text', text: 'No insights found.' }],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const lines = [`${listResult.message}\n`];
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
lines.push(`[${entry.id}] ${entry.title}`);
|
|
219
|
+
let statsLine = `Tags: ${entry.tags.join(', ') || 'none'} | Stats: Helpful ${entry.helpful} / Harmful ${entry.harmful} (Trust: ${entry.trustScore})`;
|
|
220
|
+
if (entry.warning) {
|
|
221
|
+
statsLine += ` ⚠️ ${entry.warning}`;
|
|
222
|
+
}
|
|
223
|
+
lines.push(statsLine);
|
|
224
|
+
if (entry.sourceUrl) {
|
|
225
|
+
lines.push(`Source: ${entry.sourceUrl}`);
|
|
226
|
+
}
|
|
227
|
+
if (entry.contentPreview) {
|
|
228
|
+
const preview = entry.contentTruncated
|
|
229
|
+
? entry.contentPreview + '...'
|
|
230
|
+
: entry.contentPreview;
|
|
231
|
+
lines.push(`> ${preview}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push('');
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
content: [{ type: 'text', text: lines.join('\n').trim() }],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Handle error case
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: 'text', text: listResult.message }],
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
case 'get_detail': {
|
|
248
|
+
const { id } = args;
|
|
249
|
+
const detailResult = await getDetail(id);
|
|
250
|
+
// Return human-readable format
|
|
251
|
+
if (detailResult.success) {
|
|
252
|
+
const insight = detailResult.data;
|
|
253
|
+
// Calculate trust score for display
|
|
254
|
+
const trustScore = Number(((insight.helpful - insight.harmful * 2) / (insight.helpful + insight.harmful + 1)).toFixed(2));
|
|
255
|
+
const lines = [
|
|
256
|
+
insight.title,
|
|
257
|
+
``,
|
|
258
|
+
`ID: ${insight.id}`,
|
|
259
|
+
`Tags: ${insight.tags.join(', ')}`,
|
|
260
|
+
`Stats: Helpful ${insight.helpful} / Harmful ${insight.harmful} (Trust: ${trustScore})`,
|
|
261
|
+
`Created: ${insight.created}`,
|
|
262
|
+
`Last Used: ${insight.lastUsed}`,
|
|
263
|
+
];
|
|
264
|
+
if (insight.sourceUrl) {
|
|
265
|
+
lines.push(`Source: ${insight.sourceUrl}`);
|
|
266
|
+
}
|
|
267
|
+
if (trustScore < 0) {
|
|
268
|
+
lines.push(`⚠️ Warning: Low trust score`);
|
|
269
|
+
}
|
|
270
|
+
lines.push(``, `Content:`, insight.content);
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: 'text', text: lines.join('\n') }],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
return {
|
|
277
|
+
content: [{ type: 'text', text: detailResult.message }],
|
|
278
|
+
isError: true,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
case 'mark_helpful': {
|
|
283
|
+
const { id } = args;
|
|
284
|
+
const helpfulResult = await markHelpful(id);
|
|
285
|
+
// Return human-readable format
|
|
286
|
+
if (helpfulResult.success) {
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: 'text', text: helpfulResult.message }],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: 'text', text: helpfulResult.message }],
|
|
294
|
+
isError: true,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
case 'mark_harmful': {
|
|
299
|
+
const { id } = args;
|
|
300
|
+
const harmfulResult = await markHarmful(id);
|
|
301
|
+
// Return human-readable format
|
|
302
|
+
if (harmfulResult.success) {
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: 'text', text: harmfulResult.message }],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: 'text', text: harmfulResult.message }],
|
|
310
|
+
isError: true,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
case 'search_insights': {
|
|
315
|
+
const { query, limit } = args;
|
|
316
|
+
const searchResult = await searchInsights(query, limit);
|
|
317
|
+
// Return human-readable text format instead of JSON
|
|
318
|
+
if (searchResult.success && Array.isArray(searchResult.data)) {
|
|
319
|
+
const entries = searchResult.data;
|
|
320
|
+
if (entries.length === 0) {
|
|
321
|
+
return {
|
|
322
|
+
content: [{ type: 'text', text: searchResult.message }],
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const lines = [`${searchResult.message}\n`];
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
lines.push(`[${entry.id}] ${entry.title}`);
|
|
328
|
+
let statsLine = `Tags: ${entry.tags.join(', ') || 'none'} | Stats: Helpful ${entry.helpful} / Harmful ${entry.harmful} (Trust: ${entry.trustScore})`;
|
|
329
|
+
if (entry.warning) {
|
|
330
|
+
statsLine += ` ⚠️ ${entry.warning}`;
|
|
331
|
+
}
|
|
332
|
+
lines.push(statsLine);
|
|
333
|
+
if (entry.sourceUrl) {
|
|
334
|
+
lines.push(`Source: ${entry.sourceUrl}`);
|
|
335
|
+
}
|
|
336
|
+
if (entry.contentPreview) {
|
|
337
|
+
const preview = entry.contentTruncated
|
|
338
|
+
? entry.contentPreview + '...'
|
|
339
|
+
: entry.contentPreview;
|
|
340
|
+
lines.push(`> ${preview}`);
|
|
341
|
+
}
|
|
342
|
+
lines.push('');
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: 'text', text: lines.join('\n').trim() }],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Handle error case
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: 'text', text: searchResult.message }],
|
|
352
|
+
isError: true,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
case 'delete_insight': {
|
|
357
|
+
const { id } = args;
|
|
358
|
+
const deleteResult = await deleteInsight(id);
|
|
359
|
+
if (deleteResult.success) {
|
|
360
|
+
return {
|
|
361
|
+
content: [{ type: 'text', text: deleteResult.message }],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: 'text', text: deleteResult.message }],
|
|
367
|
+
isError: true,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
case 'export_memory': {
|
|
372
|
+
const { format } = args;
|
|
373
|
+
const exportResult = await exportMemory(format ?? 'json');
|
|
374
|
+
if (exportResult.success) {
|
|
375
|
+
const resultData = exportResult.data;
|
|
376
|
+
return {
|
|
377
|
+
content: [{
|
|
378
|
+
type: 'text',
|
|
379
|
+
text: `${exportResult.message}\n\n${resultData.data}`,
|
|
380
|
+
}],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: 'text', text: exportResult.message }],
|
|
386
|
+
isError: true,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
default:
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: 'text',
|
|
395
|
+
text: `Unknown tool: ${name}`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
isError: true,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
log.error('Tool execution error:', error);
|
|
404
|
+
return {
|
|
405
|
+
content: [
|
|
406
|
+
{
|
|
407
|
+
type: 'text',
|
|
408
|
+
text: `Error executing tool: ${error.message}`,
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
isError: true,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
// Start the server
|
|
416
|
+
async function main() {
|
|
417
|
+
const transport = new StdioServerTransport();
|
|
418
|
+
await server.connect(transport);
|
|
419
|
+
log.info('MCP Learning Memory server running');
|
|
420
|
+
}
|
|
421
|
+
main().catch((error) => {
|
|
422
|
+
log.error('Server error:', error);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
});
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
package/dist/storage.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File storage with atomic writes
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { log } from './logger.js';
|
|
8
|
+
export function getMemoryPath() {
|
|
9
|
+
const xdgDataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
10
|
+
const aceDir = join(xdgDataHome, 'ace-flash-memory');
|
|
11
|
+
return join(aceDir, 'memory.json');
|
|
12
|
+
}
|
|
13
|
+
function isValidInsight(data) {
|
|
14
|
+
if (typeof data !== 'object' || data === null)
|
|
15
|
+
return false;
|
|
16
|
+
const obj = data;
|
|
17
|
+
const hasRequiredFields = typeof obj.id === 'string' &&
|
|
18
|
+
typeof obj.title === 'string' &&
|
|
19
|
+
Array.isArray(obj.tags) &&
|
|
20
|
+
obj.tags.every((tag) => typeof tag === 'string') &&
|
|
21
|
+
typeof obj.content === 'string' &&
|
|
22
|
+
typeof obj.helpful === 'number' &&
|
|
23
|
+
typeof obj.harmful === 'number' &&
|
|
24
|
+
typeof obj.created === 'string' &&
|
|
25
|
+
typeof obj.lastUsed === 'string';
|
|
26
|
+
if (!hasRequiredFields)
|
|
27
|
+
return false;
|
|
28
|
+
// sourceUrl is optional, but if present must be a string
|
|
29
|
+
if (obj.sourceUrl !== undefined && typeof obj.sourceUrl !== 'string') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function isValidMemory(data) {
|
|
35
|
+
if (typeof data !== 'object' || data === null)
|
|
36
|
+
return false;
|
|
37
|
+
const obj = data;
|
|
38
|
+
if (!Array.isArray(obj.insights))
|
|
39
|
+
return false;
|
|
40
|
+
// Validate each insight in the array
|
|
41
|
+
return obj.insights.every(isValidInsight);
|
|
42
|
+
}
|
|
43
|
+
async function createBackupAndReturnEmpty(memoryPath, reason) {
|
|
44
|
+
log.error(reason);
|
|
45
|
+
// Use ISO timestamp format (matching documentation examples)
|
|
46
|
+
// Replace colons and dots with hyphens for filesystem compatibility
|
|
47
|
+
const isoTimestamp = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-');
|
|
48
|
+
const randomSuffix = Math.random().toString(36).slice(2, 11);
|
|
49
|
+
const backupPath = `${memoryPath}.corrupt.${isoTimestamp}.${randomSuffix}`;
|
|
50
|
+
try {
|
|
51
|
+
await fs.rename(memoryPath, backupPath);
|
|
52
|
+
log.info(`Corrupted file backed up to: ${backupPath}`);
|
|
53
|
+
}
|
|
54
|
+
catch (backupError) {
|
|
55
|
+
log.error('Failed to create backup:', backupError);
|
|
56
|
+
// Even if backup fails, we still return empty memory to allow recovery
|
|
57
|
+
}
|
|
58
|
+
return { insights: [] };
|
|
59
|
+
}
|
|
60
|
+
export async function loadMemory() {
|
|
61
|
+
const memoryPath = getMemoryPath();
|
|
62
|
+
try {
|
|
63
|
+
const data = await fs.readFile(memoryPath, 'utf-8');
|
|
64
|
+
const parsed = JSON.parse(data);
|
|
65
|
+
if (!isValidMemory(parsed)) {
|
|
66
|
+
return await createBackupAndReturnEmpty(memoryPath, 'Memory file has invalid structure, creating backup and starting fresh');
|
|
67
|
+
}
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
const nodeError = error;
|
|
72
|
+
if (nodeError.code === 'ENOENT') {
|
|
73
|
+
log.info('Memory file not found, creating new one');
|
|
74
|
+
return { insights: [] };
|
|
75
|
+
}
|
|
76
|
+
// Handle corrupted JSON (SyntaxError from JSON.parse)
|
|
77
|
+
if (error instanceof SyntaxError) {
|
|
78
|
+
return await createBackupAndReturnEmpty(memoryPath, 'Memory file is corrupted (invalid JSON), creating backup and starting fresh');
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export async function saveMemory(memory) {
|
|
84
|
+
const memoryPath = getMemoryPath();
|
|
85
|
+
const xdgDataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
|
86
|
+
const memoryDir = join(xdgDataHome, 'ace-flash-memory');
|
|
87
|
+
// Use unique temp file: process.pid + random suffix to avoid collisions
|
|
88
|
+
const tmpPath = `${memoryPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 11)}`;
|
|
89
|
+
// Ensure directory exists
|
|
90
|
+
await fs.mkdir(memoryDir, { recursive: true });
|
|
91
|
+
// Atomic write: write to unique .tmp then rename
|
|
92
|
+
await fs.writeFile(tmpPath, JSON.stringify(memory, null, 2), 'utf-8');
|
|
93
|
+
await fs.rename(tmpPath, memoryPath);
|
|
94
|
+
log.info('Memory saved successfully');
|
|
95
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool implementations for MCP server
|
|
3
|
+
*/
|
|
4
|
+
import type { ToolResponse, ListIndexParams } from './types.js';
|
|
5
|
+
export declare function addInsight(title: string, tags: string[], content: string, overwrite?: boolean, sourceUrl?: string): Promise<ToolResponse>;
|
|
6
|
+
export declare function listIndex(params?: ListIndexParams): Promise<ToolResponse>;
|
|
7
|
+
export declare function getDetail(id: string): Promise<ToolResponse>;
|
|
8
|
+
export declare function markHelpful(id: string): Promise<ToolResponse>;
|
|
9
|
+
export declare function markHarmful(id: string): Promise<ToolResponse>;
|
|
10
|
+
export declare function searchInsights(query: string, limit?: number): Promise<ToolResponse>;
|
|
11
|
+
export declare function deleteInsight(id: string): Promise<ToolResponse>;
|
|
12
|
+
export declare function exportMemory(format?: 'json' | 'markdown'): Promise<ToolResponse>;
|