@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.
Files changed (196) hide show
  1. package/DASHBOARD.md +129 -0
  2. package/HYBRID_SEARCH.md +204 -0
  3. package/IMPLEMENTATION.md +159 -0
  4. package/README.md +175 -0
  5. package/dist/capabilities.d.ts +22 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +23 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/dashboard/dashboard.test.d.ts +2 -0
  10. package/dist/dashboard/dashboard.test.d.ts.map +1 -0
  11. package/dist/dashboard/dashboard.test.js +362 -0
  12. package/dist/dashboard/dashboard.test.js.map +1 -0
  13. package/dist/dashboard/public/app.js +1187 -0
  14. package/dist/dashboard/public/chart.js +0 -0
  15. package/dist/dashboard/public/index.html +967 -0
  16. package/dist/dashboard/server.d.ts +3 -0
  17. package/dist/dashboard/server.d.ts.map +1 -0
  18. package/dist/dashboard/server.js +297 -0
  19. package/dist/dashboard/server.js.map +1 -0
  20. package/dist/mcp/client.d.ts +34 -0
  21. package/dist/mcp/client.d.ts.map +1 -0
  22. package/dist/mcp/client.js +181 -0
  23. package/dist/mcp/client.js.map +1 -0
  24. package/dist/mcp/client.test.d.ts +2 -0
  25. package/dist/mcp/client.test.d.ts.map +1 -0
  26. package/dist/mcp/client.test.js +130 -0
  27. package/dist/mcp/client.test.js.map +1 -0
  28. package/dist/prompts/registry.d.ts +39 -0
  29. package/dist/prompts/registry.d.ts.map +1 -0
  30. package/dist/prompts/registry.js +90 -0
  31. package/dist/prompts/registry.js.map +1 -0
  32. package/dist/resources/index.d.ts +17 -0
  33. package/dist/resources/index.d.ts.map +1 -0
  34. package/dist/resources/index.js +100 -0
  35. package/dist/resources/index.js.map +1 -0
  36. package/dist/resources/index.test.d.ts +2 -0
  37. package/dist/resources/index.test.d.ts.map +1 -0
  38. package/dist/resources/index.test.js +96 -0
  39. package/dist/resources/index.test.js.map +1 -0
  40. package/dist/router.d.ts +4 -0
  41. package/dist/router.d.ts.map +1 -0
  42. package/dist/router.js +60 -0
  43. package/dist/router.js.map +1 -0
  44. package/dist/router.test.d.ts +2 -0
  45. package/dist/router.test.d.ts.map +1 -0
  46. package/dist/router.test.js +113 -0
  47. package/dist/router.test.js.map +1 -0
  48. package/dist/search_memory_example.d.ts +3 -0
  49. package/dist/search_memory_example.d.ts.map +1 -0
  50. package/dist/search_memory_example.js +56 -0
  51. package/dist/search_memory_example.js.map +1 -0
  52. package/dist/server.d.ts +3 -0
  53. package/dist/server.d.ts.map +1 -0
  54. package/dist/server.js +91 -0
  55. package/dist/server.js.map +1 -0
  56. package/dist/storage/sqlite.d.ts +95 -0
  57. package/dist/storage/sqlite.d.ts.map +1 -0
  58. package/dist/storage/sqlite.js +537 -0
  59. package/dist/storage/sqlite.js.map +1 -0
  60. package/dist/storage/sqlite.test.d.ts +2 -0
  61. package/dist/storage/sqlite.test.d.ts.map +1 -0
  62. package/dist/storage/sqlite.test.js +358 -0
  63. package/dist/storage/sqlite.test.js.map +1 -0
  64. package/dist/storage/vectors.stub.d.ts +12 -0
  65. package/dist/storage/vectors.stub.d.ts.map +1 -0
  66. package/dist/storage/vectors.stub.js +88 -0
  67. package/dist/storage/vectors.stub.js.map +1 -0
  68. package/dist/store_memory_example.d.ts +3 -0
  69. package/dist/store_memory_example.d.ts.map +1 -0
  70. package/dist/store_memory_example.js +69 -0
  71. package/dist/store_memory_example.js.map +1 -0
  72. package/dist/test_quotes_client.d.ts +3 -0
  73. package/dist/test_quotes_client.d.ts.map +1 -0
  74. package/dist/test_quotes_client.js +72 -0
  75. package/dist/test_quotes_client.js.map +1 -0
  76. package/dist/tools/memory.delete.d.ts +9 -0
  77. package/dist/tools/memory.delete.d.ts.map +1 -0
  78. package/dist/tools/memory.delete.js +22 -0
  79. package/dist/tools/memory.delete.js.map +1 -0
  80. package/dist/tools/memory.recap.d.ts +4 -0
  81. package/dist/tools/memory.recap.d.ts.map +1 -0
  82. package/dist/tools/memory.recap.js +42 -0
  83. package/dist/tools/memory.recap.js.map +1 -0
  84. package/dist/tools/memory.search.d.ts +5 -0
  85. package/dist/tools/memory.search.d.ts.map +1 -0
  86. package/dist/tools/memory.search.js +192 -0
  87. package/dist/tools/memory.search.js.map +1 -0
  88. package/dist/tools/memory.search.test.d.ts +2 -0
  89. package/dist/tools/memory.search.test.d.ts.map +1 -0
  90. package/dist/tools/memory.search.test.js +181 -0
  91. package/dist/tools/memory.search.test.js.map +1 -0
  92. package/dist/tools/memory.store.d.ts +5 -0
  93. package/dist/tools/memory.store.d.ts.map +1 -0
  94. package/dist/tools/memory.store.js +41 -0
  95. package/dist/tools/memory.store.js.map +1 -0
  96. package/dist/tools/memory.summarize.d.ts +4 -0
  97. package/dist/tools/memory.summarize.d.ts.map +1 -0
  98. package/dist/tools/memory.summarize.js +13 -0
  99. package/dist/tools/memory.summarize.js.map +1 -0
  100. package/dist/tools/memory.update.d.ts +5 -0
  101. package/dist/tools/memory.update.d.ts.map +1 -0
  102. package/dist/tools/memory.update.js +31 -0
  103. package/dist/tools/memory.update.js.map +1 -0
  104. package/dist/tools/schemas.d.ts +334 -0
  105. package/dist/tools/schemas.d.ts.map +1 -0
  106. package/dist/tools/schemas.js +251 -0
  107. package/dist/tools/schemas.js.map +1 -0
  108. package/dist/types.d.ts +31 -0
  109. package/dist/types.d.ts.map +1 -0
  110. package/dist/types.js +3 -0
  111. package/dist/types.js.map +1 -0
  112. package/dist/utils/git-scope.d.ts +8 -0
  113. package/dist/utils/git-scope.d.ts.map +1 -0
  114. package/dist/utils/git-scope.js +38 -0
  115. package/dist/utils/git-scope.js.map +1 -0
  116. package/dist/utils/logger.d.ts +7 -0
  117. package/dist/utils/logger.d.ts.map +1 -0
  118. package/dist/utils/logger.js +40 -0
  119. package/dist/utils/logger.js.map +1 -0
  120. package/dist/utils/logger.test.d.ts +2 -0
  121. package/dist/utils/logger.test.d.ts.map +1 -0
  122. package/dist/utils/logger.test.js +84 -0
  123. package/dist/utils/logger.test.js.map +1 -0
  124. package/dist/utils/mcp-response.d.ts +44 -0
  125. package/dist/utils/mcp-response.d.ts.map +1 -0
  126. package/dist/utils/mcp-response.js +81 -0
  127. package/dist/utils/mcp-response.js.map +1 -0
  128. package/dist/utils/normalize.d.ts +4 -0
  129. package/dist/utils/normalize.d.ts.map +1 -0
  130. package/dist/utils/normalize.js +51 -0
  131. package/dist/utils/normalize.js.map +1 -0
  132. package/dist/utils/normalize.test.d.ts +2 -0
  133. package/dist/utils/normalize.test.d.ts.map +1 -0
  134. package/dist/utils/normalize.test.js +159 -0
  135. package/dist/utils/normalize.test.js.map +1 -0
  136. package/dist/utils/query-expander.d.ts +2 -0
  137. package/dist/utils/query-expander.d.ts.map +1 -0
  138. package/dist/utils/query-expander.js +50 -0
  139. package/dist/utils/query-expander.js.map +1 -0
  140. package/dist/utils/query-expander.test.d.ts +2 -0
  141. package/dist/utils/query-expander.test.d.ts.map +1 -0
  142. package/dist/utils/query-expander.test.js +35 -0
  143. package/dist/utils/query-expander.test.js.map +1 -0
  144. package/docs/PRD.md +199 -0
  145. package/docs/PROMPT-agent.md +139 -0
  146. package/docs/SPEC-git-scope.md +172 -0
  147. package/docs/SPEC-heuristics.md +199 -0
  148. package/docs/SPEC-server.md +243 -0
  149. package/docs/SPEC-skeleton.md +255 -0
  150. package/docs/SPEC-sqlite-schema.md +183 -0
  151. package/docs/SPEC-tool-schema.md +201 -0
  152. package/docs/SPEC-vector-search.md +198 -0
  153. package/docs/TEST-scenarios.md +179 -0
  154. package/package.json +43 -0
  155. package/scripts/update-null-titles-ai.mjs +272 -0
  156. package/scripts/update-titles-batch.mjs +71 -0
  157. package/scripts/update-titles.mjs +66 -0
  158. package/seed-data.mjs +151 -0
  159. package/src/capabilities.ts +22 -0
  160. package/src/dashboard/dashboard.test.ts +546 -0
  161. package/src/dashboard/public/app.js +1187 -0
  162. package/src/dashboard/public/chart.js +0 -0
  163. package/src/dashboard/public/index.html +967 -0
  164. package/src/dashboard/server.ts +347 -0
  165. package/src/mcp/client.test.ts +164 -0
  166. package/src/mcp/client.ts +212 -0
  167. package/src/prompts/registry.ts +89 -0
  168. package/src/resources/index.test.ts +132 -0
  169. package/src/resources/index.ts +113 -0
  170. package/src/router.test.ts +145 -0
  171. package/src/router.ts +80 -0
  172. package/src/server.ts +99 -0
  173. package/src/storage/sqlite.test.ts +504 -0
  174. package/src/storage/sqlite.ts +688 -0
  175. package/src/storage/vectors.stub.ts +101 -0
  176. package/src/tools/memory.delete.ts +37 -0
  177. package/src/tools/memory.recap.ts +61 -0
  178. package/src/tools/memory.search.test.ts +276 -0
  179. package/src/tools/memory.search.ts +244 -0
  180. package/src/tools/memory.store.ts +56 -0
  181. package/src/tools/memory.summarize.ts +23 -0
  182. package/src/tools/memory.update.ts +46 -0
  183. package/src/tools/schemas.ts +261 -0
  184. package/src/types.ts +36 -0
  185. package/src/utils/git-scope.ts +42 -0
  186. package/src/utils/logger.test.ts +125 -0
  187. package/src/utils/logger.ts +53 -0
  188. package/src/utils/mcp-response.ts +116 -0
  189. package/src/utils/normalize.test.ts +203 -0
  190. package/src/utils/normalize.ts +53 -0
  191. package/src/utils/query-expander.test.ts +40 -0
  192. package/src/utils/query-expander.ts +60 -0
  193. package/storage/.gitkeep +5 -0
  194. package/test.sh +48 -0
  195. package/tsconfig.json +21 -0
  196. 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
+ }