@alex900530/claude-persistent-memory 1.0.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/LICENSE +21 -0
- package/README.md +271 -0
- package/bin/setup.js +418 -0
- package/bin/uninstall.js +122 -0
- package/config.default.js +49 -0
- package/config.js +7 -0
- package/hooks/post-tool-memory-hook.js +151 -0
- package/hooks/pre-compact-hook.js +61 -0
- package/hooks/pre-tool-memory-hook.js +148 -0
- package/hooks/session-end-hook.js +159 -0
- package/hooks/user-prompt-hook.js +151 -0
- package/lib/compact-analyzer.js +319 -0
- package/lib/embedding-client.js +113 -0
- package/lib/llm-client.js +61 -0
- package/lib/memory-db.js +1310 -0
- package/lib/utils.js +92 -0
- package/package.json +44 -0
- package/services/embedding-server.js +108 -0
- package/services/llm-server.js +421 -0
- package/services/memory-mcp-server.js +252 -0
- package/tools/rebuild-vectors.js +27 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Memory MCP Server [v1.0]
|
|
4
|
+
*
|
|
5
|
+
* Pull model: exposes memory search as MCP tools, invoked by Claude on demand.
|
|
6
|
+
* Complements the Push model (hooks that auto-inject context).
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - memory_search: hybrid search (BM25 + vector similarity)
|
|
10
|
+
* - memory_save: save new memory
|
|
11
|
+
* - memory_validate: validate memory usefulness (adjust confidence)
|
|
12
|
+
* - memory_stats: view memory statistics
|
|
13
|
+
*
|
|
14
|
+
* Transport: stdio (Claude Code standard)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// Try standard import first, fallback to manual path resolution
|
|
20
|
+
let McpServer, StdioServerTransport;
|
|
21
|
+
try {
|
|
22
|
+
({ McpServer } = require('@modelcontextprotocol/sdk/dist/cjs/server/mcp.js'));
|
|
23
|
+
({ StdioServerTransport } = require('@modelcontextprotocol/sdk/dist/cjs/server/stdio.js'));
|
|
24
|
+
} catch (e) {
|
|
25
|
+
const SDK_DIR = path.join(__dirname, '..', 'node_modules', '@modelcontextprotocol', 'sdk', 'dist', 'cjs', 'server');
|
|
26
|
+
({ McpServer } = require(path.join(SDK_DIR, 'mcp.js')));
|
|
27
|
+
({ StdioServerTransport } = require(path.join(SDK_DIR, 'stdio.js')));
|
|
28
|
+
}
|
|
29
|
+
const z = require('zod');
|
|
30
|
+
|
|
31
|
+
// ============ Memory modules ============
|
|
32
|
+
|
|
33
|
+
const memoryDb = require('../lib/memory-db');
|
|
34
|
+
|
|
35
|
+
// Embedding client for hybrid search via embedding server
|
|
36
|
+
let embeddingClient;
|
|
37
|
+
try {
|
|
38
|
+
embeddingClient = require('../lib/embedding-client');
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// embedding-client not available, will fall back to memoryDb.search
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute hybrid search (prefer embedding service, fallback to inline search)
|
|
45
|
+
*/
|
|
46
|
+
async function hybridSearch(query, limit, options = {}) {
|
|
47
|
+
let useEmbeddingService = false;
|
|
48
|
+
|
|
49
|
+
if (embeddingClient) {
|
|
50
|
+
try {
|
|
51
|
+
useEmbeddingService = await embeddingClient.isServerRunning();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (useEmbeddingService) {
|
|
58
|
+
return embeddingClient.search(query, limit);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fallback: inline hybrid search
|
|
62
|
+
try {
|
|
63
|
+
return await memoryDb.search(query, limit, options);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
// Final fallback: pure BM25
|
|
66
|
+
const keywords = memoryDb.extractKeywords(query);
|
|
67
|
+
const ftsQuery = keywords.map(k => `"${k}"`).join(' OR ');
|
|
68
|
+
return memoryDb.quickSearch(ftsQuery, limit);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============ MCP Server definition ============
|
|
73
|
+
|
|
74
|
+
const server = new McpServer({
|
|
75
|
+
name: 'memory',
|
|
76
|
+
version: '1.0.0'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- Tool: memory_search ---
|
|
80
|
+
server.tool(
|
|
81
|
+
'memory_search',
|
|
82
|
+
'Search persistent memories (hybrid BM25 + vector semantic retrieval). Use when you need to recall previous context, patterns, decisions, or bug fix records.',
|
|
83
|
+
{
|
|
84
|
+
query: z.string().describe('Search query (natural language, supports Chinese and English)'),
|
|
85
|
+
limit: z.number().optional().default(5).describe('Number of results to return (default 5)'),
|
|
86
|
+
type: z.enum(['fact', 'decision', 'bug', 'pattern', 'context', 'preference', 'skill']).optional().describe('Filter by memory type'),
|
|
87
|
+
domain: z.enum(['orm', 'api', 'frontend', 'backend', 'testing', 'memory', 'general']).optional().describe('Filter by domain')
|
|
88
|
+
},
|
|
89
|
+
async ({ query, limit = 5, type, domain }) => {
|
|
90
|
+
try {
|
|
91
|
+
const options = {};
|
|
92
|
+
if (type) options.type = type;
|
|
93
|
+
if (domain) options.domain = domain;
|
|
94
|
+
|
|
95
|
+
const results = await hybridSearch(query, limit, options);
|
|
96
|
+
|
|
97
|
+
if (!results || results.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: 'No relevant memories found.' }]
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Mark memories as used
|
|
104
|
+
const usedIds = results.map(r => r.id).filter(Boolean);
|
|
105
|
+
if (usedIds.length > 0) {
|
|
106
|
+
memoryDb.markMemoriesUsed(usedIds);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Format results
|
|
110
|
+
const formatted = results.map((r, i) => {
|
|
111
|
+
const confidence = r.confidence ? `${Math.round(r.confidence * 100)}%` : 'N/A';
|
|
112
|
+
const vecSim = r.vectorSimilarity ? r.vectorSimilarity.toFixed(3) : 'N/A';
|
|
113
|
+
const bm25 = r.bm25Score ? r.bm25Score.toFixed(1) : '0';
|
|
114
|
+
const date = r.createdAt ? r.createdAt.slice(0, 10) : r.date || 'unknown';
|
|
115
|
+
|
|
116
|
+
let content = r.content || r.rawContent || '';
|
|
117
|
+
// Truncate overly long content
|
|
118
|
+
if (content.length > 500) {
|
|
119
|
+
content = content.slice(0, 500) + '...';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return [
|
|
123
|
+
`## Memory #${r.id} [${r.type || 'unknown'}/${r.domain || 'general'}] (confidence: ${confidence})`,
|
|
124
|
+
`date: ${date} | vecSim: ${vecSim} | BM25: ${bm25}`,
|
|
125
|
+
'',
|
|
126
|
+
content
|
|
127
|
+
].join('\n');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: `Found ${results.length} relevant memories:\n\n${formatted.join('\n\n---\n\n')}`
|
|
134
|
+
}]
|
|
135
|
+
};
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: 'text', text: `Search failed: ${e.message}` }],
|
|
139
|
+
isError: true
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// --- Tool: memory_save ---
|
|
146
|
+
server.tool(
|
|
147
|
+
'memory_save',
|
|
148
|
+
'Save a new persistent memory. Use to record important patterns, decisions, bug fixes, user preferences, etc.',
|
|
149
|
+
{
|
|
150
|
+
content: z.string().describe('Memory content to save'),
|
|
151
|
+
type: z.enum(['fact', 'decision', 'bug', 'pattern', 'context', 'preference']).optional().default('context').describe('Memory type'),
|
|
152
|
+
domain: z.enum(['orm', 'api', 'frontend', 'backend', 'testing', 'memory', 'general']).optional().default('general').describe('Domain'),
|
|
153
|
+
confidence: z.number().min(0.3).max(0.9).optional().default(0.7).describe('Confidence (0.3-0.9)')
|
|
154
|
+
},
|
|
155
|
+
async ({ content, type = 'context', domain = 'general', confidence = 0.7 }) => {
|
|
156
|
+
try {
|
|
157
|
+
const result = await memoryDb.save(content, {
|
|
158
|
+
type,
|
|
159
|
+
domain,
|
|
160
|
+
confidence,
|
|
161
|
+
source: 'mcp-tool'
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
content: [{
|
|
166
|
+
type: 'text',
|
|
167
|
+
text: `Memory saved (ID: ${result.id}, type: ${type}, domain: ${domain}, confidence: ${confidence})`
|
|
168
|
+
}]
|
|
169
|
+
};
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: 'text', text: `Save failed: ${e.message}` }],
|
|
173
|
+
isError: true
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// --- Tool: memory_validate ---
|
|
180
|
+
server.tool(
|
|
181
|
+
'memory_validate',
|
|
182
|
+
'Validate whether a memory was helpful. Helpful increases confidence by +0.1, unhelpful decreases by -0.05.',
|
|
183
|
+
{
|
|
184
|
+
memory_id: z.number().describe('Memory ID'),
|
|
185
|
+
is_valid: z.boolean().describe('Whether the memory was helpful (true=helpful, false=not helpful)')
|
|
186
|
+
},
|
|
187
|
+
async ({ memory_id, is_valid }) => {
|
|
188
|
+
try {
|
|
189
|
+
memoryDb.validateMemory(memory_id, is_valid);
|
|
190
|
+
const action = is_valid ? 'increased +0.1' : 'decreased -0.05';
|
|
191
|
+
return {
|
|
192
|
+
content: [{
|
|
193
|
+
type: 'text',
|
|
194
|
+
text: `Memory #${memory_id} validated, confidence ${action}`
|
|
195
|
+
}]
|
|
196
|
+
};
|
|
197
|
+
} catch (e) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: 'text', text: `Validation failed: ${e.message}` }],
|
|
200
|
+
isError: true
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// --- Tool: memory_stats ---
|
|
207
|
+
server.tool(
|
|
208
|
+
'memory_stats',
|
|
209
|
+
'View memory system statistics: total memories, type distribution, domain distribution, cluster status, etc.',
|
|
210
|
+
{},
|
|
211
|
+
async () => {
|
|
212
|
+
try {
|
|
213
|
+
const stats = memoryDb.getStats();
|
|
214
|
+
|
|
215
|
+
const lines = [
|
|
216
|
+
`## Memory System Statistics`,
|
|
217
|
+
`- Total memories: ${stats.totalMemories}`,
|
|
218
|
+
`- Total clusters: ${stats.totalClusters} (mature: ${stats.matureClusters})`,
|
|
219
|
+
'',
|
|
220
|
+
'### By Type',
|
|
221
|
+
...Object.entries(stats.byType).map(([k, v]) => ` - ${k}: ${v}`),
|
|
222
|
+
'',
|
|
223
|
+
'### By Domain',
|
|
224
|
+
...Object.entries(stats.byDomain).map(([k, v]) => ` - ${k}: ${v}`)
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: 'text', text: lines.join('\n') }]
|
|
229
|
+
};
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: `Failed to get stats: ${e.message}` }],
|
|
233
|
+
isError: true
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// ============ Start server ============
|
|
240
|
+
|
|
241
|
+
async function main() {
|
|
242
|
+
const transport = new StdioServerTransport();
|
|
243
|
+
await server.connect(transport);
|
|
244
|
+
// MCP server is now running via stdio
|
|
245
|
+
// stderr is used for logs, does not affect MCP protocol communication
|
|
246
|
+
process.stderr.write('[memory-mcp] Server started\n');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
main().catch(e => {
|
|
250
|
+
process.stderr.write(`[memory-mcp] Fatal: ${e.message}\n`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const memoryDb = require('../lib/memory-db');
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
console.log('Rebuilding all embeddings...');
|
|
6
|
+
const vecResult = await memoryDb.rebuildAllEmbeddings();
|
|
7
|
+
console.log('Embeddings done:', vecResult);
|
|
8
|
+
|
|
9
|
+
console.log('Rebuilding FTS index...');
|
|
10
|
+
const db = memoryDb.getDb();
|
|
11
|
+
db.exec('DROP TABLE IF EXISTS memories_fts');
|
|
12
|
+
db.exec(`CREATE VIRTUAL TABLE memories_fts USING fts5(content, structured_content, summary, tags, keywords)`);
|
|
13
|
+
const rows = db.prepare('SELECT id, content, structured_content, summary, tags, keywords FROM memories').all();
|
|
14
|
+
let count = 0;
|
|
15
|
+
const insert = db.prepare('INSERT INTO memories_fts(rowid, content, structured_content, summary, tags, keywords) VALUES (?, ?, ?, ?, ?, ?)');
|
|
16
|
+
for (const r of rows) {
|
|
17
|
+
try {
|
|
18
|
+
insert.run(r.id, memoryDb.tokenize(r.content || ''), memoryDb.tokenize(r.structured_content || ''), memoryDb.tokenize(r.summary || ''), memoryDb.tokenize(r.tags || ''), memoryDb.tokenize(r.keywords || ''));
|
|
19
|
+
count++;
|
|
20
|
+
} catch (e) {}
|
|
21
|
+
}
|
|
22
|
+
console.log(`FTS done: ${count}/${rows.length} indexed`);
|
|
23
|
+
|
|
24
|
+
memoryDb.closeDb();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch(e => { console.error(e); process.exit(1); });
|