@vheins/local-memory-mcp 0.1.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/DASHBOARD.md +129 -0
- package/HYBRID_SEARCH.md +204 -0
- package/IMPLEMENTATION.md +159 -0
- package/README.md +175 -0
- package/dist/capabilities.d.ts +22 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +23 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/dashboard/dashboard.test.d.ts +2 -0
- package/dist/dashboard/dashboard.test.d.ts.map +1 -0
- package/dist/dashboard/dashboard.test.js +362 -0
- package/dist/dashboard/dashboard.test.js.map +1 -0
- package/dist/dashboard/public/app.js +1187 -0
- package/dist/dashboard/public/chart.js +0 -0
- package/dist/dashboard/public/index.html +967 -0
- package/dist/dashboard/server.d.ts +3 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +297 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/mcp/client.d.ts +34 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +181 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/client.test.d.ts +2 -0
- package/dist/mcp/client.test.d.ts.map +1 -0
- package/dist/mcp/client.test.js +130 -0
- package/dist/mcp/client.test.js.map +1 -0
- package/dist/prompts/registry.d.ts +39 -0
- package/dist/prompts/registry.d.ts.map +1 -0
- package/dist/prompts/registry.js +90 -0
- package/dist/prompts/registry.js.map +1 -0
- package/dist/resources/index.d.ts +17 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +100 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/index.test.d.ts +2 -0
- package/dist/resources/index.test.d.ts.map +1 -0
- package/dist/resources/index.test.js +96 -0
- package/dist/resources/index.test.js.map +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +60 -0
- package/dist/router.js.map +1 -0
- package/dist/router.test.d.ts +2 -0
- package/dist/router.test.d.ts.map +1 -0
- package/dist/router.test.js +113 -0
- package/dist/router.test.js.map +1 -0
- package/dist/search_memory_example.d.ts +3 -0
- package/dist/search_memory_example.d.ts.map +1 -0
- package/dist/search_memory_example.js +56 -0
- package/dist/search_memory_example.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +91 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/sqlite.d.ts +95 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +537 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/storage/sqlite.test.d.ts +2 -0
- package/dist/storage/sqlite.test.d.ts.map +1 -0
- package/dist/storage/sqlite.test.js +358 -0
- package/dist/storage/sqlite.test.js.map +1 -0
- package/dist/storage/vectors.stub.d.ts +12 -0
- package/dist/storage/vectors.stub.d.ts.map +1 -0
- package/dist/storage/vectors.stub.js +88 -0
- package/dist/storage/vectors.stub.js.map +1 -0
- package/dist/store_memory_example.d.ts +3 -0
- package/dist/store_memory_example.d.ts.map +1 -0
- package/dist/store_memory_example.js +69 -0
- package/dist/store_memory_example.js.map +1 -0
- package/dist/test_quotes_client.d.ts +3 -0
- package/dist/test_quotes_client.d.ts.map +1 -0
- package/dist/test_quotes_client.js +72 -0
- package/dist/test_quotes_client.js.map +1 -0
- package/dist/tools/memory.delete.d.ts +9 -0
- package/dist/tools/memory.delete.d.ts.map +1 -0
- package/dist/tools/memory.delete.js +22 -0
- package/dist/tools/memory.delete.js.map +1 -0
- package/dist/tools/memory.recap.d.ts +4 -0
- package/dist/tools/memory.recap.d.ts.map +1 -0
- package/dist/tools/memory.recap.js +42 -0
- package/dist/tools/memory.recap.js.map +1 -0
- package/dist/tools/memory.search.d.ts +5 -0
- package/dist/tools/memory.search.d.ts.map +1 -0
- package/dist/tools/memory.search.js +192 -0
- package/dist/tools/memory.search.js.map +1 -0
- package/dist/tools/memory.search.test.d.ts +2 -0
- package/dist/tools/memory.search.test.d.ts.map +1 -0
- package/dist/tools/memory.search.test.js +181 -0
- package/dist/tools/memory.search.test.js.map +1 -0
- package/dist/tools/memory.store.d.ts +5 -0
- package/dist/tools/memory.store.d.ts.map +1 -0
- package/dist/tools/memory.store.js +41 -0
- package/dist/tools/memory.store.js.map +1 -0
- package/dist/tools/memory.summarize.d.ts +4 -0
- package/dist/tools/memory.summarize.d.ts.map +1 -0
- package/dist/tools/memory.summarize.js +13 -0
- package/dist/tools/memory.summarize.js.map +1 -0
- package/dist/tools/memory.update.d.ts +5 -0
- package/dist/tools/memory.update.d.ts.map +1 -0
- package/dist/tools/memory.update.js +31 -0
- package/dist/tools/memory.update.js.map +1 -0
- package/dist/tools/schemas.d.ts +334 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +251 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/git-scope.d.ts +8 -0
- package/dist/utils/git-scope.d.ts.map +1 -0
- package/dist/utils/git-scope.js +38 -0
- package/dist/utils/git-scope.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +40 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.test.d.ts +2 -0
- package/dist/utils/logger.test.d.ts.map +1 -0
- package/dist/utils/logger.test.js +84 -0
- package/dist/utils/logger.test.js.map +1 -0
- package/dist/utils/mcp-response.d.ts +44 -0
- package/dist/utils/mcp-response.d.ts.map +1 -0
- package/dist/utils/mcp-response.js +81 -0
- package/dist/utils/mcp-response.js.map +1 -0
- package/dist/utils/normalize.d.ts +4 -0
- package/dist/utils/normalize.d.ts.map +1 -0
- package/dist/utils/normalize.js +51 -0
- package/dist/utils/normalize.js.map +1 -0
- package/dist/utils/normalize.test.d.ts +2 -0
- package/dist/utils/normalize.test.d.ts.map +1 -0
- package/dist/utils/normalize.test.js +159 -0
- package/dist/utils/normalize.test.js.map +1 -0
- package/dist/utils/query-expander.d.ts +2 -0
- package/dist/utils/query-expander.d.ts.map +1 -0
- package/dist/utils/query-expander.js +50 -0
- package/dist/utils/query-expander.js.map +1 -0
- package/dist/utils/query-expander.test.d.ts +2 -0
- package/dist/utils/query-expander.test.d.ts.map +1 -0
- package/dist/utils/query-expander.test.js +35 -0
- package/dist/utils/query-expander.test.js.map +1 -0
- package/docs/PRD.md +199 -0
- package/docs/PROMPT-agent.md +139 -0
- package/docs/SPEC-git-scope.md +172 -0
- package/docs/SPEC-heuristics.md +199 -0
- package/docs/SPEC-server.md +243 -0
- package/docs/SPEC-skeleton.md +255 -0
- package/docs/SPEC-sqlite-schema.md +183 -0
- package/docs/SPEC-tool-schema.md +201 -0
- package/docs/SPEC-vector-search.md +198 -0
- package/docs/TEST-scenarios.md +179 -0
- package/package.json +43 -0
- package/scripts/update-null-titles-ai.mjs +272 -0
- package/scripts/update-titles-batch.mjs +71 -0
- package/scripts/update-titles.mjs +66 -0
- package/seed-data.mjs +151 -0
- package/src/capabilities.ts +22 -0
- package/src/dashboard/dashboard.test.ts +546 -0
- package/src/dashboard/public/app.js +1187 -0
- package/src/dashboard/public/chart.js +0 -0
- package/src/dashboard/public/index.html +967 -0
- package/src/dashboard/server.ts +347 -0
- package/src/mcp/client.test.ts +164 -0
- package/src/mcp/client.ts +212 -0
- package/src/prompts/registry.ts +89 -0
- package/src/resources/index.test.ts +132 -0
- package/src/resources/index.ts +113 -0
- package/src/router.test.ts +145 -0
- package/src/router.ts +80 -0
- package/src/server.ts +99 -0
- package/src/storage/sqlite.test.ts +504 -0
- package/src/storage/sqlite.ts +688 -0
- package/src/storage/vectors.stub.ts +101 -0
- package/src/tools/memory.delete.ts +37 -0
- package/src/tools/memory.recap.ts +61 -0
- package/src/tools/memory.search.test.ts +276 -0
- package/src/tools/memory.search.ts +244 -0
- package/src/tools/memory.store.ts +56 -0
- package/src/tools/memory.summarize.ts +23 -0
- package/src/tools/memory.update.ts +46 -0
- package/src/tools/schemas.ts +261 -0
- package/src/types.ts +36 -0
- package/src/utils/git-scope.ts +42 -0
- package/src/utils/logger.test.ts +125 -0
- package/src/utils/logger.ts +53 -0
- package/src/utils/mcp-response.ts +116 -0
- package/src/utils/normalize.test.ts +203 -0
- package/src/utils/normalize.ts +53 -0
- package/src/utils/query-expander.test.ts +40 -0
- package/src/utils/query-expander.ts +60 -0
- package/storage/.gitkeep +5 -0
- package/test.sh +48 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { MemorySearchSchema } from "./schemas.js";
|
|
2
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
3
|
+
import { VectorStore, MemoryEntry } from "../types.js";
|
|
4
|
+
import { normalize } from "../utils/normalize.js";
|
|
5
|
+
import { handleMemoryRecap } from "./memory.recap.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
8
|
+
import { expandQuery } from "../utils/query-expander.js";
|
|
9
|
+
|
|
10
|
+
// Hybrid search configuration — weights when vector store is active
|
|
11
|
+
const HYBRID_WEIGHTS_VECTOR = {
|
|
12
|
+
similarity: 0.6,
|
|
13
|
+
vector: 0.3,
|
|
14
|
+
importance: 0.1
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Weights when vector store returns no results (normalized to sum = 1.0)
|
|
18
|
+
const HYBRID_WEIGHTS_NO_VECTOR = {
|
|
19
|
+
similarity: 0.85,
|
|
20
|
+
importance: 0.15
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
interface ScoredMemory {
|
|
24
|
+
memory: MemoryEntry;
|
|
25
|
+
similarityScore: number;
|
|
26
|
+
vectorScore: number;
|
|
27
|
+
importanceBoost: number;
|
|
28
|
+
finalScore: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function handleMemorySearch(
|
|
32
|
+
params: any,
|
|
33
|
+
db: SQLiteStore,
|
|
34
|
+
vectors: VectorStore
|
|
35
|
+
): Promise<McpResponse> {
|
|
36
|
+
// Validate input
|
|
37
|
+
const validated = MemorySearchSchema.parse(params);
|
|
38
|
+
|
|
39
|
+
// STEP 0: Pre-search recap — only if includeRecap === true
|
|
40
|
+
let recapContext = "";
|
|
41
|
+
if (validated.includeRecap) {
|
|
42
|
+
try {
|
|
43
|
+
const recapResult = await handleMemoryRecap(
|
|
44
|
+
{ repo: validated.repo, limit: 20 },
|
|
45
|
+
db
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Find text content that contains JSON data
|
|
49
|
+
const textContent = recapResult.content.find((c) => c.type === "text" && c.text);
|
|
50
|
+
if (textContent && textContent.type === "text") {
|
|
51
|
+
try {
|
|
52
|
+
const recapData = JSON.parse(textContent.text) as { summary?: string };
|
|
53
|
+
if (recapData.summary) {
|
|
54
|
+
recapContext = recapData.summary;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Not JSON, might be plain text summary
|
|
58
|
+
recapContext = textContent.text;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error("Failed to get recap context", { error: String(error) });
|
|
63
|
+
// Continue anyway - recap is optional
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Expand query using prompt intent
|
|
68
|
+
const searchQuery = expandQuery(validated.query, validated.prompt);
|
|
69
|
+
|
|
70
|
+
// STEP 1: Repo filter (HARD) + Lightweight similarity scoring
|
|
71
|
+
const similarityResults = db.searchBySimilarity(
|
|
72
|
+
searchQuery,
|
|
73
|
+
validated.repo,
|
|
74
|
+
validated.limit * 3 // Get more candidates for vector re-ranking
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
let candidates: Array<{ memory: MemoryEntry; similarityScore: number }>;
|
|
78
|
+
|
|
79
|
+
if (similarityResults.length > 0 && similarityResults[0].similarity > 0.1) {
|
|
80
|
+
// Use similarity results as candidates
|
|
81
|
+
candidates = similarityResults.map((result) => ({
|
|
82
|
+
memory: {
|
|
83
|
+
id: result.id,
|
|
84
|
+
type: result.type,
|
|
85
|
+
title: result.title,
|
|
86
|
+
content: result.content,
|
|
87
|
+
importance: result.importance,
|
|
88
|
+
scope: result.scope,
|
|
89
|
+
created_at: result.created_at,
|
|
90
|
+
updated_at: result.updated_at,
|
|
91
|
+
hit_count: result.hit_count,
|
|
92
|
+
recall_count: result.recall_count,
|
|
93
|
+
last_used_at: result.last_used_at,
|
|
94
|
+
expires_at: result.expires_at,
|
|
95
|
+
},
|
|
96
|
+
similarityScore: result.similarity
|
|
97
|
+
}));
|
|
98
|
+
} else {
|
|
99
|
+
// Fallback: keyword search as candidates
|
|
100
|
+
const allResults = db.searchByRepo(validated.repo, {
|
|
101
|
+
types: validated.types,
|
|
102
|
+
minImportance: validated.minImportance,
|
|
103
|
+
limit: validated.limit * 3
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const normalized = normalize(searchQuery);
|
|
107
|
+
const queryWords = normalized.split(/\s+/).filter(w => w.length > 2);
|
|
108
|
+
|
|
109
|
+
const filtered = allResults.filter((entry) => {
|
|
110
|
+
const normalizedContent = normalize(entry.content);
|
|
111
|
+
return queryWords.some(word => normalizedContent.includes(word));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const memoriesToUse = filtered.length > 0 ? filtered : allResults;
|
|
115
|
+
|
|
116
|
+
candidates = memoriesToUse.map(memory => ({
|
|
117
|
+
memory,
|
|
118
|
+
similarityScore: 0.5 // Default similarity for keyword matches
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// STEP 2: OPTIONAL vector similarity re-rank (only on top candidates)
|
|
123
|
+
let scoredMemories: ScoredMemory[];
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
// Attempt vector search on candidates (if available)
|
|
127
|
+
const vectorResults = await vectors.search(searchQuery, candidates.length);
|
|
128
|
+
|
|
129
|
+
if (vectorResults.length > 0) {
|
|
130
|
+
// Create a map of memory ID to vector score
|
|
131
|
+
const vectorScoreMap = new Map<string, number>();
|
|
132
|
+
for (const vr of vectorResults) {
|
|
133
|
+
vectorScoreMap.set(vr.id, vr.score);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Combine scores using hybrid formula (vector active: 0.6 + 0.3 + 0.1 = 1.0)
|
|
137
|
+
scoredMemories = candidates.map(({ memory, similarityScore }) => {
|
|
138
|
+
const vectorScore = vectorScoreMap.get(memory.id) || 0;
|
|
139
|
+
const importanceBoost = memory.importance / 5;
|
|
140
|
+
|
|
141
|
+
const finalScore =
|
|
142
|
+
(similarityScore * HYBRID_WEIGHTS_VECTOR.similarity) +
|
|
143
|
+
(vectorScore * HYBRID_WEIGHTS_VECTOR.vector) +
|
|
144
|
+
(importanceBoost * HYBRID_WEIGHTS_VECTOR.importance);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
memory,
|
|
148
|
+
similarityScore,
|
|
149
|
+
vectorScore,
|
|
150
|
+
importanceBoost,
|
|
151
|
+
finalScore
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
// No vector results — use similarity-only ranking (0.85 + 0.15 = 1.0)
|
|
156
|
+
scoredMemories = candidates.map(({ memory, similarityScore }) => {
|
|
157
|
+
const importanceBoost = memory.importance / 5;
|
|
158
|
+
|
|
159
|
+
const finalScore =
|
|
160
|
+
(similarityScore * HYBRID_WEIGHTS_NO_VECTOR.similarity) +
|
|
161
|
+
(importanceBoost * HYBRID_WEIGHTS_NO_VECTOR.importance);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
memory,
|
|
165
|
+
similarityScore,
|
|
166
|
+
vectorScore: 0,
|
|
167
|
+
importanceBoost,
|
|
168
|
+
finalScore
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
// Vector search failed — gracefully degrade to similarity-only
|
|
174
|
+
logger.warn("Vector search failed, using similarity-only", { error: String(error) });
|
|
175
|
+
|
|
176
|
+
scoredMemories = candidates.map(({ memory, similarityScore }) => {
|
|
177
|
+
const importanceBoost = memory.importance / 5;
|
|
178
|
+
|
|
179
|
+
const finalScore =
|
|
180
|
+
(similarityScore * HYBRID_WEIGHTS_NO_VECTOR.similarity) +
|
|
181
|
+
(importanceBoost * HYBRID_WEIGHTS_NO_VECTOR.importance);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
memory,
|
|
185
|
+
similarityScore,
|
|
186
|
+
vectorScore: 0,
|
|
187
|
+
importanceBoost,
|
|
188
|
+
finalScore
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// STEP 3: Sort by final score and take top results
|
|
194
|
+
scoredMemories.sort((a, b) => b.finalScore - a.finalScore);
|
|
195
|
+
|
|
196
|
+
const results = scoredMemories
|
|
197
|
+
.slice(0, validated.limit)
|
|
198
|
+
.map(sm => sm.memory);
|
|
199
|
+
|
|
200
|
+
// STEP 4: Increment hit_count for returned memories
|
|
201
|
+
for (const memory of results) {
|
|
202
|
+
db.incrementHitCount(memory.id);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// STEP 5: Log the query for recent queries feature
|
|
206
|
+
db.logAction('search', validated.repo, { query: validated.query, resultCount: results.length });
|
|
207
|
+
|
|
208
|
+
const resultData = {
|
|
209
|
+
query: validated.query,
|
|
210
|
+
prompt: validated.prompt || null,
|
|
211
|
+
results,
|
|
212
|
+
matchReason: validated.prompt
|
|
213
|
+
? `Results ranked by relevance to "${validated.query}" with context: ${validated.prompt}`
|
|
214
|
+
: `Results ranked by relevance to "${validated.query}"`,
|
|
215
|
+
recapContext: recapContext ? `Recent memories context:\n${recapContext}` : undefined
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const firstResult = results[0];
|
|
219
|
+
const resultName = firstResult?.title
|
|
220
|
+
? `${firstResult.type} - ${firstResult.title}`
|
|
221
|
+
: (firstResult ? `${firstResult.type} - ${firstResult.content.substring(0, 40)}...` : `Found ${results.length} results`);
|
|
222
|
+
|
|
223
|
+
return createMcpResponse(
|
|
224
|
+
resultData,
|
|
225
|
+
`Found ${results.length} memories matching "${validated.query}"`,
|
|
226
|
+
{
|
|
227
|
+
query: validated.query,
|
|
228
|
+
results: results.map(r => ({
|
|
229
|
+
id: r.id,
|
|
230
|
+
type: r.type,
|
|
231
|
+
title: r.title,
|
|
232
|
+
content: r.content,
|
|
233
|
+
importance: r.importance,
|
|
234
|
+
scope: r.scope,
|
|
235
|
+
created_at: r.created_at,
|
|
236
|
+
updated_at: r.updated_at,
|
|
237
|
+
hit_count: r.hit_count,
|
|
238
|
+
recall_count: r.recall_count,
|
|
239
|
+
last_used_at: r.last_used_at,
|
|
240
|
+
expires_at: r.expires_at
|
|
241
|
+
}))
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { MemoryStoreSchema } from "./schemas.js";
|
|
3
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
4
|
+
import { VectorStore, MemoryEntry } from "../types.js";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
7
|
+
|
|
8
|
+
export async function handleMemoryStore(
|
|
9
|
+
params: any,
|
|
10
|
+
db: SQLiteStore,
|
|
11
|
+
vectors: VectorStore
|
|
12
|
+
): Promise<McpResponse> {
|
|
13
|
+
// Validate input
|
|
14
|
+
const validated = MemoryStoreSchema.parse(params);
|
|
15
|
+
|
|
16
|
+
// Create memory entry
|
|
17
|
+
const now = new Date().toISOString();
|
|
18
|
+
|
|
19
|
+
// Compute expires_at if ttlDays is provided
|
|
20
|
+
const expires_at = validated.ttlDays != null
|
|
21
|
+
? new Date(Date.now() + validated.ttlDays * 86400000).toISOString()
|
|
22
|
+
: null;
|
|
23
|
+
|
|
24
|
+
const entry: MemoryEntry = {
|
|
25
|
+
id: randomUUID(),
|
|
26
|
+
type: validated.type,
|
|
27
|
+
content: validated.content,
|
|
28
|
+
importance: validated.importance,
|
|
29
|
+
scope: validated.scope,
|
|
30
|
+
created_at: now,
|
|
31
|
+
updated_at: now,
|
|
32
|
+
hit_count: 0,
|
|
33
|
+
recall_count: 0,
|
|
34
|
+
last_used_at: null,
|
|
35
|
+
expires_at,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Store in SQLite
|
|
39
|
+
db.insert(entry);
|
|
40
|
+
|
|
41
|
+
// Automatically generate and store vector embedding
|
|
42
|
+
try {
|
|
43
|
+
await vectors.upsert(entry.id, entry.content);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
logger.warn("Failed to generate vector embedding", { error: String(error) });
|
|
46
|
+
// Continue anyway - vectors are optional for search fallback
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Log the write action
|
|
50
|
+
db.logAction('write', validated.scope.repo, { memoryId: entry.id, resultCount: 1 });
|
|
51
|
+
|
|
52
|
+
return createMcpResponse(
|
|
53
|
+
{ success: true, id: entry.id },
|
|
54
|
+
`Stored memory ${entry.id.slice(0, 8)}...`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MemorySummarizeSchema } from "./schemas.js";
|
|
2
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
3
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
4
|
+
|
|
5
|
+
export async function handleMemorySummarize(
|
|
6
|
+
params: any,
|
|
7
|
+
db: SQLiteStore
|
|
8
|
+
): Promise<McpResponse> {
|
|
9
|
+
// Validate input
|
|
10
|
+
const validated = MemorySummarizeSchema.parse(params);
|
|
11
|
+
|
|
12
|
+
// Create summary from signals
|
|
13
|
+
const summary = validated.signals.join("\n- ");
|
|
14
|
+
const fullSummary = `Project summary:\n- ${summary}`;
|
|
15
|
+
|
|
16
|
+
// Store summary
|
|
17
|
+
db.upsertSummary(validated.repo, fullSummary);
|
|
18
|
+
|
|
19
|
+
return createMcpResponse(
|
|
20
|
+
{ success: true },
|
|
21
|
+
`Updated summary for repo "${validated.repo}"`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MemoryUpdateSchema } from "./schemas.js";
|
|
2
|
+
import { SQLiteStore } from "../storage/sqlite.js";
|
|
3
|
+
import { VectorStore } from "../types.js";
|
|
4
|
+
import { createMcpResponse, McpResponse } from "../utils/mcp-response.js";
|
|
5
|
+
|
|
6
|
+
export async function handleMemoryUpdate(
|
|
7
|
+
params: any,
|
|
8
|
+
db: SQLiteStore,
|
|
9
|
+
vectors: VectorStore
|
|
10
|
+
): Promise<McpResponse> {
|
|
11
|
+
// Validate input
|
|
12
|
+
const validated = MemoryUpdateSchema.parse(params);
|
|
13
|
+
|
|
14
|
+
// Check if memory exists
|
|
15
|
+
const existing = db.getById(validated.id);
|
|
16
|
+
if (!existing) {
|
|
17
|
+
throw new Error(`Memory not found: ${validated.id}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Update in SQLite
|
|
21
|
+
const updates: { title?: string; content?: string; importance?: number } = {};
|
|
22
|
+
if (validated.title !== undefined) {
|
|
23
|
+
updates.title = validated.title;
|
|
24
|
+
}
|
|
25
|
+
if (validated.content !== undefined) {
|
|
26
|
+
updates.content = validated.content;
|
|
27
|
+
}
|
|
28
|
+
if (validated.importance !== undefined) {
|
|
29
|
+
updates.importance = validated.importance;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
db.update(validated.id, updates);
|
|
33
|
+
|
|
34
|
+
// Update vector if content changed
|
|
35
|
+
if (validated.content !== undefined) {
|
|
36
|
+
await vectors.upsert(validated.id, validated.content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log the update action
|
|
40
|
+
db.logAction('update', existing.scope.repo, { memoryId: validated.id, resultCount: 1 });
|
|
41
|
+
|
|
42
|
+
return createMcpResponse(
|
|
43
|
+
{ success: true },
|
|
44
|
+
`Updated memory ${validated.id.slice(0, 8)}...`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Shared schema components
|
|
4
|
+
export const MemoryScopeSchema = z.object({
|
|
5
|
+
repo: z.string().min(1),
|
|
6
|
+
branch: z.string().optional(),
|
|
7
|
+
folder: z.string().optional(),
|
|
8
|
+
language: z.string().optional()
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const MemoryTypeSchema = z.enum(["code_fact", "decision", "mistake", "pattern"]);
|
|
12
|
+
|
|
13
|
+
// Tool schemas
|
|
14
|
+
export const MemoryStoreSchema = z.object({
|
|
15
|
+
type: MemoryTypeSchema,
|
|
16
|
+
title: z.string().min(3).max(100),
|
|
17
|
+
content: z.string().min(10),
|
|
18
|
+
importance: z.number().min(1).max(5),
|
|
19
|
+
scope: MemoryScopeSchema,
|
|
20
|
+
ttlDays: z.number().min(1).optional()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const MemoryUpdateSchema = z.object({
|
|
24
|
+
id: z.string().uuid(),
|
|
25
|
+
title: z.string().min(3).max(100).optional(),
|
|
26
|
+
content: z.string().min(10).optional(),
|
|
27
|
+
importance: z.number().min(1).max(5).optional()
|
|
28
|
+
}).refine(
|
|
29
|
+
(data) => data.content !== undefined || data.title !== undefined || data.importance !== undefined,
|
|
30
|
+
{ message: "At least one of title, content or importance must be provided" }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
export const MemorySearchSchema = z.object({
|
|
34
|
+
query: z.string().min(3),
|
|
35
|
+
prompt: z.string().optional(),
|
|
36
|
+
repo: z.string().min(1),
|
|
37
|
+
types: z.array(MemoryTypeSchema).optional(),
|
|
38
|
+
minImportance: z.number().min(1).max(5).optional(),
|
|
39
|
+
limit: z.number().min(1).max(10).default(5),
|
|
40
|
+
includeRecap: z.boolean().default(false)
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const MemoryRecapSchema = z.object({
|
|
44
|
+
repo: z.string().min(1),
|
|
45
|
+
limit: z.number().min(1).max(50).default(20),
|
|
46
|
+
offset: z.number().min(0).default(0)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const MemorySummarizeSchema = z.object({
|
|
50
|
+
repo: z.string().min(1),
|
|
51
|
+
signals: z.array(z.string().max(200)).min(1)
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Tool definitions for MCP
|
|
55
|
+
export const TOOL_DEFINITIONS = [
|
|
56
|
+
{
|
|
57
|
+
name: "memory-store",
|
|
58
|
+
description: "Store a new memory entry that affects future behavior",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
type: {
|
|
63
|
+
type: "string",
|
|
64
|
+
enum: ["code_fact", "decision", "mistake", "pattern"],
|
|
65
|
+
description: "Type of memory being stored"
|
|
66
|
+
},
|
|
67
|
+
title: {
|
|
68
|
+
type: "string",
|
|
69
|
+
minLength: 3,
|
|
70
|
+
maxLength: 100,
|
|
71
|
+
description: "Short title for the memory (required)"
|
|
72
|
+
},
|
|
73
|
+
content: {
|
|
74
|
+
type: "string",
|
|
75
|
+
minLength: 10,
|
|
76
|
+
description: "The memory content (must be durable knowledge)"
|
|
77
|
+
},
|
|
78
|
+
importance: {
|
|
79
|
+
type: "number",
|
|
80
|
+
minimum: 1,
|
|
81
|
+
maximum: 5,
|
|
82
|
+
description: "Importance score (1-5)"
|
|
83
|
+
},
|
|
84
|
+
scope: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
repo: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "Repository name (required)"
|
|
90
|
+
},
|
|
91
|
+
branch: {
|
|
92
|
+
type: "string",
|
|
93
|
+
description: "Git branch (optional)"
|
|
94
|
+
},
|
|
95
|
+
folder: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Specific folder path (optional)"
|
|
98
|
+
},
|
|
99
|
+
language: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "Programming language (optional)"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: ["repo"]
|
|
105
|
+
},
|
|
106
|
+
ttlDays: {
|
|
107
|
+
type: "number",
|
|
108
|
+
minimum: 1,
|
|
109
|
+
description: "Time-to-live in days. Memory will expire after this many days (optional)"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
required: ["type", "title", "content", "importance", "scope"]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "memory-update",
|
|
117
|
+
description: "Update an existing memory entry",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
id: {
|
|
122
|
+
type: "string",
|
|
123
|
+
format: "uuid",
|
|
124
|
+
description: "Memory entry ID"
|
|
125
|
+
},
|
|
126
|
+
title: {
|
|
127
|
+
type: "string",
|
|
128
|
+
minLength: 3,
|
|
129
|
+
maxLength: 100,
|
|
130
|
+
description: "Updated title (optional)"
|
|
131
|
+
},
|
|
132
|
+
content: {
|
|
133
|
+
type: "string",
|
|
134
|
+
minLength: 10,
|
|
135
|
+
description: "Updated content (optional)"
|
|
136
|
+
},
|
|
137
|
+
importance: {
|
|
138
|
+
type: "number",
|
|
139
|
+
minimum: 1,
|
|
140
|
+
maximum: 5,
|
|
141
|
+
description: "Updated importance (optional)"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
required: ["id"]
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "memory-search",
|
|
149
|
+
description: "Search for relevant memories in the current repository",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
query: {
|
|
154
|
+
type: "string",
|
|
155
|
+
minLength: 3,
|
|
156
|
+
description: "Search query"
|
|
157
|
+
},
|
|
158
|
+
prompt: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "Context/prompt to help determine relevance. Use this to specify what kind of information you're looking for (optional)"
|
|
161
|
+
},
|
|
162
|
+
repo: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Repository name (required)"
|
|
165
|
+
},
|
|
166
|
+
types: {
|
|
167
|
+
type: "array",
|
|
168
|
+
items: {
|
|
169
|
+
type: "string",
|
|
170
|
+
enum: ["code_fact", "decision", "mistake", "pattern"]
|
|
171
|
+
},
|
|
172
|
+
description: "Filter by memory types (optional)"
|
|
173
|
+
},
|
|
174
|
+
minImportance: {
|
|
175
|
+
type: "number",
|
|
176
|
+
minimum: 1,
|
|
177
|
+
maximum: 5,
|
|
178
|
+
description: "Minimum importance score (optional)"
|
|
179
|
+
},
|
|
180
|
+
limit: {
|
|
181
|
+
type: "number",
|
|
182
|
+
minimum: 1,
|
|
183
|
+
maximum: 10,
|
|
184
|
+
default: 5,
|
|
185
|
+
description: "Maximum number of results"
|
|
186
|
+
},
|
|
187
|
+
includeRecap: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
default: false,
|
|
190
|
+
description: "Include recent memories recap context in the response (optional, default false)"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
required: ["query", "repo"]
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "memory-summarize",
|
|
198
|
+
description: "Update the summary for a repository",
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
repo: {
|
|
203
|
+
type: "string",
|
|
204
|
+
description: "Repository name"
|
|
205
|
+
},
|
|
206
|
+
signals: {
|
|
207
|
+
type: "array",
|
|
208
|
+
items: {
|
|
209
|
+
type: "string",
|
|
210
|
+
maxLength: 200
|
|
211
|
+
},
|
|
212
|
+
minItems: 1,
|
|
213
|
+
description: "High-level signals to include in summary"
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
required: ["repo", "signals"]
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "memory-delete",
|
|
221
|
+
description: "Soft-delete a memory entry (remove from active use)",
|
|
222
|
+
inputSchema: {
|
|
223
|
+
type: "object",
|
|
224
|
+
properties: {
|
|
225
|
+
id: {
|
|
226
|
+
type: "string",
|
|
227
|
+
format: "uuid",
|
|
228
|
+
description: "Memory entry ID to delete"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
required: ["id"]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "memory-recap",
|
|
236
|
+
description: "Get the last 20 memories from a repository for context",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
repo: {
|
|
241
|
+
type: "string",
|
|
242
|
+
description: "Repository name (required)"
|
|
243
|
+
},
|
|
244
|
+
limit: {
|
|
245
|
+
type: "number",
|
|
246
|
+
minimum: 1,
|
|
247
|
+
maximum: 50,
|
|
248
|
+
default: 20,
|
|
249
|
+
description: "Maximum number of memories to retrieve"
|
|
250
|
+
},
|
|
251
|
+
offset: {
|
|
252
|
+
type: "number",
|
|
253
|
+
minimum: 0,
|
|
254
|
+
default: 0,
|
|
255
|
+
description: "Number of memories to skip for pagination (optional, default 0)"
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
required: ["repo"]
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
];
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared types for MCP Local Memory
|
|
2
|
+
|
|
3
|
+
export type MemoryType = "code_fact" | "decision" | "mistake" | "pattern";
|
|
4
|
+
|
|
5
|
+
export type MemoryScope = {
|
|
6
|
+
repo: string;
|
|
7
|
+
branch?: string;
|
|
8
|
+
folder?: string;
|
|
9
|
+
language?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MemoryEntry = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: MemoryType;
|
|
15
|
+
title?: string;
|
|
16
|
+
content: string;
|
|
17
|
+
importance: number;
|
|
18
|
+
scope: MemoryScope;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
hit_count: number;
|
|
22
|
+
recall_count: number;
|
|
23
|
+
last_used_at: string | null;
|
|
24
|
+
expires_at: string | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type VectorResult = {
|
|
28
|
+
id: string;
|
|
29
|
+
score: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface VectorStore {
|
|
33
|
+
upsert(id: string, text: string): Promise<void>;
|
|
34
|
+
remove(id: string): Promise<void>;
|
|
35
|
+
search(query: string, limit: number): Promise<VectorResult[]>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
export function resolveGitScope(cwd = process.cwd()) {
|
|
5
|
+
// 1. Try git root
|
|
6
|
+
try {
|
|
7
|
+
const root = execSync("git rev-parse --show-toplevel", {
|
|
8
|
+
cwd,
|
|
9
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
10
|
+
})
|
|
11
|
+
.toString()
|
|
12
|
+
.trim();
|
|
13
|
+
|
|
14
|
+
const repo = path.basename(root);
|
|
15
|
+
|
|
16
|
+
let branch: string | undefined;
|
|
17
|
+
try {
|
|
18
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
19
|
+
cwd,
|
|
20
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
21
|
+
})
|
|
22
|
+
.toString()
|
|
23
|
+
.trim();
|
|
24
|
+
} catch {}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
repo,
|
|
28
|
+
branch
|
|
29
|
+
};
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
// 2. Fallback: project folder
|
|
33
|
+
const fallback = path.basename(cwd);
|
|
34
|
+
|
|
35
|
+
if (fallback) {
|
|
36
|
+
return {
|
|
37
|
+
repo: fallback
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error("Unable to resolve project scope (no git repo, no folder)");
|
|
42
|
+
}
|