@aman_asmuei/amem 0.5.0 → 0.7.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 (53) hide show
  1. package/README.md +49 -7
  2. package/dist/cli.js +7 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/database.d.ts +15 -0
  5. package/dist/database.js +129 -19
  6. package/dist/database.js.map +1 -1
  7. package/dist/database.test.d.ts +1 -0
  8. package/dist/database.test.js +275 -0
  9. package/dist/database.test.js.map +1 -0
  10. package/dist/embeddings.js +30 -2
  11. package/dist/embeddings.js.map +1 -1
  12. package/dist/embeddings.test.d.ts +1 -0
  13. package/dist/embeddings.test.js +106 -0
  14. package/dist/embeddings.test.js.map +1 -0
  15. package/dist/index.js +158 -80
  16. package/dist/index.js.map +1 -1
  17. package/dist/memory.d.ts +19 -2
  18. package/dist/memory.js +108 -35
  19. package/dist/memory.js.map +1 -1
  20. package/dist/memory.test.d.ts +1 -0
  21. package/dist/memory.test.js +171 -0
  22. package/dist/memory.test.js.map +1 -0
  23. package/dist/schemas.d.ts +209 -31
  24. package/dist/schemas.js +54 -1
  25. package/dist/schemas.js.map +1 -1
  26. package/dist/tools/graph.d.ts +3 -0
  27. package/dist/tools/graph.js +344 -0
  28. package/dist/tools/graph.js.map +1 -0
  29. package/dist/tools/helpers.d.ts +7 -0
  30. package/dist/tools/helpers.js +23 -0
  31. package/dist/tools/helpers.js.map +1 -0
  32. package/dist/tools/index.d.ts +4 -0
  33. package/dist/tools/index.js +19 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/tools/log.d.ts +3 -0
  36. package/dist/tools/log.js +244 -0
  37. package/dist/tools/log.js.map +1 -0
  38. package/dist/tools/memory.d.ts +4 -0
  39. package/dist/tools/memory.js +1245 -0
  40. package/dist/tools/memory.js.map +1 -0
  41. package/dist/tools/reminders.d.ts +3 -0
  42. package/dist/tools/reminders.js +228 -0
  43. package/dist/tools/reminders.js.map +1 -0
  44. package/dist/tools/versions.d.ts +3 -0
  45. package/dist/tools/versions.js +118 -0
  46. package/dist/tools/versions.js.map +1 -0
  47. package/dist/tools.test.d.ts +1 -0
  48. package/dist/tools.test.js +217 -0
  49. package/dist/tools.test.js.map +1 -0
  50. package/package.json +1 -2
  51. package/dist/tools.d.ts +0 -6
  52. package/dist/tools.js +0 -1812
  53. package/dist/tools.js.map +0 -1
@@ -0,0 +1,1245 @@
1
+ import { z } from "zod";
2
+ import { MemoryType, recallMemories, detectConflict, consolidateMemories } from "../memory.js";
3
+ import { generateEmbedding, cosineSimilarity } from "../embeddings.js";
4
+ import { RecallResultSchema, ContextResultSchema, ExtractResultSchema, StatsResultSchema, ExportResultSchema, InjectResultSchema, ConsolidateResultSchema, DetailResultSchema, } from "../schemas.js";
5
+ import { TYPE_ORDER, MEMORY_TYPES, CHARACTER_LIMIT, shortId, formatAge } from "./helpers.js";
6
+ export function registerMemoryTools(server, db, project, autoScope) {
7
+ // ── memory_store ──────────────────────────────────────────
8
+ server.registerTool("memory_store", {
9
+ title: "Store Memory",
10
+ description: `Store a developer memory. Types: correction (highest priority — hard constraints), decision (architectural choice + rationale), pattern (coding style/habit), preference (tool/style preference), topology (where things are in the codebase), fact (general knowledge). Always include tags for better recall.
11
+
12
+ Args:
13
+ - content (string): The memory content — be specific and self-contained
14
+ - type (enum): Memory type — corrections are highest priority
15
+ - tags (string[]): Tags for filtering (e.g., ['typescript', 'auth', 'testing'])
16
+ - confidence (number 0-1): How confident is this memory. Corrections from user = 1.0
17
+ - source (string): Where this memory came from (default: 'conversation')
18
+
19
+ Returns:
20
+ Confirmation with memory ID, or conflict detection if a similar memory exists.`,
21
+ inputSchema: z.object({
22
+ content: z.string().min(1, "Content is required").max(10000, "Content too long — max 10,000 characters").describe("The memory content — be specific and include context"),
23
+ type: z.enum(MEMORY_TYPES).describe("Memory type — corrections are highest priority"),
24
+ tags: z.array(z.string()).default([]).describe("Tags for filtering (e.g., ['typescript', 'auth', 'testing'])"),
25
+ confidence: z.number().min(0).max(1).default(0.8).describe("How confident is this memory (0-1). Corrections from user = 1.0"),
26
+ source: z.string().default("conversation").describe("Where this memory came from"),
27
+ scope: z.string().optional().describe("Memory scope — 'global' or 'project:<name>'. Auto-detected from type if omitted."),
28
+ }).strict(),
29
+ // outputSchema omitted — z.union() causes _zod serialization errors in MCP SDK
30
+ annotations: {
31
+ readOnlyHint: false,
32
+ destructiveHint: false,
33
+ idempotentHint: false,
34
+ openWorldHint: false,
35
+ },
36
+ }, async ({ content, type, tags, confidence, source, scope }) => {
37
+ try {
38
+ const embedding = await generateEmbedding(content);
39
+ // Single pass over recent memories: conflict detection, confidence boost, and reinforcement
40
+ // Limit to 5000 most recently accessed to stay fast at scale
41
+ if (embedding) {
42
+ const existing = db.getRecentWithEmbeddings(5000);
43
+ const toReinforce = [];
44
+ const toBoostConfidence = [];
45
+ for (const mem of existing) {
46
+ if (!mem.embedding)
47
+ continue;
48
+ const sim = cosineSimilarity(embedding, mem.embedding);
49
+ if (sim > 0.85) {
50
+ // Near-duplicate — conflict resolution
51
+ const conflict = detectConflict(content, mem.content, sim);
52
+ if (conflict.isConflict) {
53
+ db.updateConfidence(mem.id, Math.max(mem.confidence, confidence));
54
+ return {
55
+ content: [{
56
+ type: "text",
57
+ text: `Memory conflict detected. Similar memory exists (${(sim * 100).toFixed(0)}% match): "${mem.content}" — updated its confidence instead of creating duplicate.\n\nIf these are genuinely different memories, rephrase to be more distinct.`,
58
+ }],
59
+ structuredContent: {
60
+ action: "conflict_resolved",
61
+ existingId: mem.id,
62
+ similarity: Number((sim * 100).toFixed(0)),
63
+ existingContent: mem.content,
64
+ },
65
+ };
66
+ }
67
+ }
68
+ if (sim > 0.8) {
69
+ toBoostConfidence.push({ id: mem.id, confidence: mem.confidence });
70
+ }
71
+ else if (sim > 0.6) {
72
+ toReinforce.push(mem.id);
73
+ }
74
+ }
75
+ const id = db.insertMemory({ content, type: type, tags, confidence, source, embedding, scope: scope ?? autoScope(type) });
76
+ // Apply boosts and reinforcements collected in the single pass
77
+ for (const b of toBoostConfidence) {
78
+ db.updateConfidence(b.id, Math.min(1.0, b.confidence + 0.1));
79
+ }
80
+ for (const rId of toReinforce) {
81
+ db.touchAccess(rId);
82
+ }
83
+ const stats = db.getStats();
84
+ const evolvedNote = toReinforce.length > 0 ? ` Reinforced ${toReinforce.length} related memories.` : "";
85
+ return {
86
+ content: [{
87
+ type: "text",
88
+ text: `Stored ${type} memory (${shortId(id)}). Confidence: ${confidence}. Tags: [${tags.join(", ")}]. Total memories: ${stats.total}.${evolvedNote}`,
89
+ }],
90
+ structuredContent: {
91
+ action: "stored",
92
+ id,
93
+ type,
94
+ confidence,
95
+ tags,
96
+ total: stats.total,
97
+ reinforced: toReinforce.length,
98
+ },
99
+ };
100
+ }
101
+ // No embeddings available — check content hash for exact duplicates, then store
102
+ const existingByHash = db.findByContentHash(content);
103
+ if (existingByHash) {
104
+ db.updateConfidence(existingByHash.id, Math.max(existingByHash.confidence, confidence));
105
+ return {
106
+ content: [{
107
+ type: "text",
108
+ text: `Exact duplicate detected: "${existingByHash.content}" — updated confidence instead of creating duplicate.`,
109
+ }],
110
+ structuredContent: {
111
+ action: "conflict_resolved",
112
+ existingId: existingByHash.id,
113
+ similarity: 100,
114
+ existingContent: existingByHash.content,
115
+ },
116
+ };
117
+ }
118
+ const id = db.insertMemory({ content, type: type, tags, confidence, source, embedding, scope: scope ?? autoScope(type) });
119
+ const stats = db.getStats();
120
+ return {
121
+ content: [{
122
+ type: "text",
123
+ text: `Stored ${type} memory (${shortId(id)}). Confidence: ${confidence}. Tags: [${tags.join(", ")}]. Total memories: ${stats.total}.`,
124
+ }],
125
+ structuredContent: {
126
+ action: "stored",
127
+ id,
128
+ type,
129
+ confidence,
130
+ tags,
131
+ total: stats.total,
132
+ reinforced: 0,
133
+ },
134
+ };
135
+ }
136
+ catch (error) {
137
+ return {
138
+ isError: true,
139
+ content: [{
140
+ type: "text",
141
+ text: `Error storing memory: ${error instanceof Error ? error.message : String(error)}. Ensure the database is accessible and content is valid.`,
142
+ }],
143
+ };
144
+ }
145
+ });
146
+ // ── memory_recall ─────────────────────────────────────────
147
+ server.registerTool("memory_recall", {
148
+ title: "Recall Memories",
149
+ description: `Search memories semantically. Returns the most relevant memories ranked by relevance, recency, confidence, and importance. Use this when you need to remember something about the user, project, or past decisions.
150
+
151
+ Args:
152
+ - query (string): What to search for — natural language works best
153
+ - limit (number 1-50): Max results to return (default: 10)
154
+ - type (enum, optional): Filter by memory type
155
+ - tag (string, optional): Filter by tag
156
+ - min_confidence (number 0-1, optional): Minimum confidence threshold
157
+ - compact (boolean, optional): If true, return compact index (~50-100 tokens) with IDs for progressive disclosure. Use memory_detail to get full content.
158
+ - explain (boolean, optional): If true, include detailed score breakdown showing how each factor (relevance, recency, confidence, importance) contributed to the ranking.
159
+
160
+ Returns:
161
+ Ranked list of memories with scores, confidence, age, and tags. If compact=true, returns a compact index with short IDs and previews. If explain=true, includes per-memory scoring explanation.`,
162
+ inputSchema: z.object({
163
+ query: z.string().min(1, "Query is required").describe("What to search for — natural language works best"),
164
+ limit: z.number().int().min(1).max(50).default(10).describe("Max results to return"),
165
+ type: z.enum(MEMORY_TYPES).optional().describe("Filter by memory type"),
166
+ tag: z.string().optional().describe("Filter by tag"),
167
+ min_confidence: z.number().min(0).max(1).optional().describe("Minimum confidence threshold"),
168
+ compact: z.boolean().default(false).describe("If true, return compact index (~50-100 tokens) with IDs for progressive disclosure. Use memory_detail to get full content."),
169
+ explain: z.boolean().default(false).describe("If true, include detailed score breakdown per memory showing relevance source, recency decay, confidence, and type importance."),
170
+ }).strict(),
171
+ outputSchema: RecallResultSchema,
172
+ annotations: {
173
+ readOnlyHint: false,
174
+ destructiveHint: false,
175
+ idempotentHint: false,
176
+ openWorldHint: false,
177
+ },
178
+ }, async ({ query, limit, type, tag, min_confidence, compact, explain }) => {
179
+ try {
180
+ const queryEmbedding = await generateEmbedding(query);
181
+ const results = recallMemories(db, {
182
+ query,
183
+ queryEmbedding,
184
+ limit,
185
+ type: type,
186
+ tag,
187
+ minConfidence: min_confidence,
188
+ scope: project,
189
+ explain,
190
+ });
191
+ for (const r of results) {
192
+ db.touchAccess(r.id);
193
+ }
194
+ if (results.length === 0) {
195
+ return {
196
+ content: [{ type: "text", text: `No memories found for: "${query}". Try broadening your search or using different keywords.` }],
197
+ structuredContent: {
198
+ query,
199
+ total: 0,
200
+ memories: [],
201
+ },
202
+ };
203
+ }
204
+ if (compact) {
205
+ const compactLines = results.map((r) => {
206
+ const preview = r.content.slice(0, 80) + (r.content.length > 80 ? "..." : "");
207
+ return `${shortId(r.id)} [${r.type}] ${preview} (${(r.score * 100).toFixed(0)}%)`;
208
+ });
209
+ const tokenEstimate = compactLines.join("\n").split(/\s+/).length;
210
+ return {
211
+ content: [{
212
+ type: "text",
213
+ text: `${results.length} memories (~${tokenEstimate} tokens):\n${compactLines.join("\n")}\n\nUse memory_detail with IDs for full content.`,
214
+ }],
215
+ structuredContent: {
216
+ query,
217
+ total: results.length,
218
+ compact: true,
219
+ tokenEstimate,
220
+ memories: results.map(r => ({
221
+ id: r.id,
222
+ type: r.type,
223
+ preview: r.content.slice(0, 80),
224
+ score: Number(r.score.toFixed(3)),
225
+ confidence: r.confidence,
226
+ })),
227
+ },
228
+ };
229
+ }
230
+ const memoriesData = results.map((r) => {
231
+ const base = {
232
+ id: r.id,
233
+ content: r.content,
234
+ type: r.type,
235
+ score: Number(r.score.toFixed(3)),
236
+ confidence: r.confidence,
237
+ tags: r.tags,
238
+ age: formatAge(r.createdAt),
239
+ };
240
+ if (explain && "explanation" in r) {
241
+ base.explanation = r.explanation;
242
+ }
243
+ return base;
244
+ });
245
+ const lines = results.map((r, i) => {
246
+ const age = formatAge(r.createdAt);
247
+ const conf = (r.confidence * 100).toFixed(0);
248
+ let line = `${i + 1}. [${r.type}] ${r.content}\n Score: ${r.score.toFixed(3)} | Confidence: ${conf}% | Age: ${age} | Tags: [${r.tags.join(", ")}]`;
249
+ if (explain && "explanation" in r) {
250
+ const e = r.explanation;
251
+ line += `\n ── Breakdown: relevance=${e.relevance.toFixed(3)} (${e.relevanceSource}) × recency=${e.recency} (${e.hoursSinceAccess}h ago) × confidence=${e.confidence} × importance=${e.importance} (${e.importanceLabel})`;
252
+ }
253
+ return line;
254
+ });
255
+ return {
256
+ content: [{
257
+ type: "text",
258
+ text: `Found ${results.length} memories for "${query}":\n\n${lines.join("\n\n")}`,
259
+ }],
260
+ structuredContent: {
261
+ query,
262
+ total: results.length,
263
+ memories: memoriesData,
264
+ },
265
+ };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ isError: true,
270
+ content: [{
271
+ type: "text",
272
+ text: `Error recalling memories: ${error instanceof Error ? error.message : String(error)}. Try a different query or check the database.`,
273
+ }],
274
+ };
275
+ }
276
+ });
277
+ // ── memory_detail ────────────────────────────────────────
278
+ server.registerTool("memory_detail", {
279
+ title: "Get Memory Details",
280
+ description: "Retrieve full details for specific memory IDs. Use after memory_recall with compact=true to get full content for selected memories. Supports partial IDs (first 8 chars).",
281
+ inputSchema: z.object({
282
+ ids: z.array(z.string()).min(1).max(20).describe("Memory IDs (full or first 8 chars) to retrieve"),
283
+ }).strict(),
284
+ outputSchema: DetailResultSchema,
285
+ annotations: {
286
+ readOnlyHint: false,
287
+ destructiveHint: false,
288
+ idempotentHint: false,
289
+ openWorldHint: false,
290
+ },
291
+ }, async ({ ids }) => {
292
+ try {
293
+ const found = ids.map(id => {
294
+ const fullId = db.resolveId(id);
295
+ if (!fullId)
296
+ return null;
297
+ const mem = db.getById(fullId);
298
+ if (!mem)
299
+ return null;
300
+ db.touchAccess(mem.id);
301
+ return mem;
302
+ }).filter((m) => m !== null);
303
+ if (found.length === 0) {
304
+ return {
305
+ content: [{ type: "text", text: "No memories found for the given IDs." }],
306
+ structuredContent: { total: 0, tokenEstimate: 0, memories: [] },
307
+ };
308
+ }
309
+ const lines = found.map((r) => {
310
+ const age = formatAge(r.createdAt);
311
+ const conf = (r.confidence * 100).toFixed(0);
312
+ return `[${r.type}] ${r.content}\nID: ${shortId(r.id)} | Confidence: ${conf}% | Age: ${age} | Tags: [${r.tags.join(", ")}]`;
313
+ });
314
+ const tokenEstimate = lines.join("\n\n").split(/\s+/).length;
315
+ return {
316
+ content: [{
317
+ type: "text",
318
+ text: `${found.length} memories (~${tokenEstimate} tokens):\n\n${lines.join("\n\n")}`,
319
+ }],
320
+ structuredContent: {
321
+ total: found.length,
322
+ tokenEstimate,
323
+ memories: found.map(r => ({
324
+ id: r.id,
325
+ content: r.content,
326
+ type: r.type,
327
+ confidence: r.confidence,
328
+ tags: r.tags,
329
+ age: formatAge(r.createdAt),
330
+ scope: r.scope,
331
+ })),
332
+ },
333
+ };
334
+ }
335
+ catch (error) {
336
+ return {
337
+ isError: true,
338
+ content: [{
339
+ type: "text",
340
+ text: `Error retrieving memories: ${error instanceof Error ? error.message : String(error)}`,
341
+ }],
342
+ };
343
+ }
344
+ });
345
+ // ── memory_context ────────────────────────────────────────
346
+ server.registerTool("memory_context", {
347
+ title: "Get Memory Context",
348
+ description: `Get all relevant context for a topic — combines memories across types to build a complete picture. Use at the start of a task to load relevant background. Returns corrections first (they override other context).
349
+
350
+ Args:
351
+ - topic (string): The topic or task you need context for
352
+ - max_tokens (number): Approximate token budget for context (default: 2000)
353
+
354
+ Returns:
355
+ Markdown-formatted context grouped by memory type, with corrections first.`,
356
+ inputSchema: z.object({
357
+ topic: z.string().min(1, "Topic is required").describe("The topic or task you need context for"),
358
+ max_tokens: z.number().int().min(100).max(10000).default(2000).describe("Approximate token budget for context"),
359
+ }).strict(),
360
+ outputSchema: ContextResultSchema,
361
+ annotations: {
362
+ readOnlyHint: false,
363
+ destructiveHint: false,
364
+ idempotentHint: false,
365
+ openWorldHint: false,
366
+ },
367
+ }, async ({ topic, max_tokens }) => {
368
+ try {
369
+ const queryEmbedding = await generateEmbedding(topic);
370
+ const results = recallMemories(db, {
371
+ query: topic,
372
+ queryEmbedding,
373
+ limit: 50,
374
+ scope: project,
375
+ });
376
+ if (results.length === 0) {
377
+ return {
378
+ content: [{ type: "text", text: `No context found for: "${topic}". Store some memories first using memory_store or memory_extract.` }],
379
+ structuredContent: {
380
+ topic,
381
+ groups: [],
382
+ memoriesUsed: 0,
383
+ },
384
+ };
385
+ }
386
+ const grouped = {};
387
+ for (const r of results) {
388
+ if (!grouped[r.type])
389
+ grouped[r.type] = [];
390
+ grouped[r.type].push(r);
391
+ }
392
+ let output = `## Context for: ${topic}\n\n`;
393
+ let approxTokens = 0;
394
+ const CHARS_PER_TOKEN = 4;
395
+ for (const t of TYPE_ORDER) {
396
+ const memories = grouped[t];
397
+ if (!memories || memories.length === 0)
398
+ continue;
399
+ const header = `### ${t.charAt(0).toUpperCase() + t.slice(1)}s\n`;
400
+ output += header;
401
+ approxTokens += header.length / CHARS_PER_TOKEN;
402
+ for (const m of memories) {
403
+ const line = `- ${m.content} (${(m.confidence * 100).toFixed(0)}% confidence)\n`;
404
+ approxTokens += line.length / CHARS_PER_TOKEN;
405
+ if (approxTokens > max_tokens)
406
+ break;
407
+ output += line;
408
+ }
409
+ output += "\n";
410
+ if (approxTokens > max_tokens)
411
+ break;
412
+ }
413
+ for (const r of results)
414
+ db.touchAccess(r.id);
415
+ const groups = TYPE_ORDER
416
+ .filter(t => grouped[t] && grouped[t].length > 0)
417
+ .map(t => ({
418
+ type: t,
419
+ memories: grouped[t].map(m => ({
420
+ content: m.content,
421
+ confidence: m.confidence,
422
+ })),
423
+ }));
424
+ return {
425
+ content: [{ type: "text", text: output.trim() }],
426
+ structuredContent: {
427
+ topic,
428
+ groups,
429
+ memoriesUsed: results.length,
430
+ },
431
+ };
432
+ }
433
+ catch (error) {
434
+ return {
435
+ isError: true,
436
+ content: [{
437
+ type: "text",
438
+ text: `Error loading context: ${error instanceof Error ? error.message : String(error)}. Try a different topic or check the database.`,
439
+ }],
440
+ };
441
+ }
442
+ });
443
+ // ── memory_forget ─────────────────────────────────────────
444
+ server.registerTool("memory_forget", {
445
+ title: "Forget Memory",
446
+ description: `Delete a specific memory by ID, or delete all memories matching a query. Use when information is outdated, wrong, or the user explicitly asks to forget something.
447
+
448
+ Args:
449
+ - id (string, optional): Specific memory ID to delete
450
+ - query (string, optional): Delete all memories matching this query (requires confirmation)
451
+ - confirm (boolean): Must be true to actually delete when using query-based deletion (default: false)
452
+
453
+ Returns:
454
+ Deletion confirmation, or a preview of matching memories when confirm=false.
455
+
456
+ Error Handling:
457
+ - Returns error if neither id nor query is provided
458
+ - Returns error if memory ID not found`,
459
+ inputSchema: z.object({
460
+ id: z.string().optional().describe("Specific memory ID to delete"),
461
+ query: z.string().optional().describe("Delete all memories matching this query (requires confirmation)"),
462
+ confirm: z.boolean().default(false).describe("Must be true to actually delete when using query-based deletion"),
463
+ }).strict(),
464
+ // outputSchema omitted — z.union() causes _zod serialization errors in MCP SDK
465
+ annotations: {
466
+ readOnlyHint: false,
467
+ destructiveHint: true,
468
+ idempotentHint: true,
469
+ openWorldHint: false,
470
+ },
471
+ }, async ({ id, query, confirm }) => {
472
+ try {
473
+ if (id) {
474
+ const fullId = db.resolveId(id) ?? id;
475
+ const memory = db.getById(fullId);
476
+ if (!memory) {
477
+ return {
478
+ isError: true,
479
+ content: [{ type: "text", text: `Memory ${id} not found. Use memory_recall to search for the correct ID.` }],
480
+ };
481
+ }
482
+ db.deleteMemory(fullId);
483
+ return {
484
+ content: [{ type: "text", text: `Deleted memory: "${memory.content}" (${memory.type})` }],
485
+ structuredContent: {
486
+ action: "deleted",
487
+ id: fullId,
488
+ content: memory.content,
489
+ type: memory.type,
490
+ },
491
+ };
492
+ }
493
+ if (query) {
494
+ const queryEmbedding = await generateEmbedding(query);
495
+ const matches = recallMemories(db, { query, queryEmbedding, limit: 20, minConfidence: 0, scope: project });
496
+ if (matches.length === 0) {
497
+ return {
498
+ content: [{ type: "text", text: `No memories found matching "${query}".` }],
499
+ structuredContent: {
500
+ action: "preview",
501
+ query,
502
+ total: 0,
503
+ previewed: [],
504
+ },
505
+ };
506
+ }
507
+ if (!confirm) {
508
+ const preview = matches.slice(0, 5).map((m, i) => `${i + 1}. [${shortId(m.id)}] ${m.content}`).join("\n");
509
+ return {
510
+ content: [{
511
+ type: "text",
512
+ text: `Found ${matches.length} memories matching "${query}". Preview:\n${preview}\n\nCall again with confirm=true to delete these.`,
513
+ }],
514
+ structuredContent: {
515
+ action: "preview",
516
+ query,
517
+ total: matches.length,
518
+ previewed: matches.slice(0, 5).map(m => ({ id: m.id, content: m.content })),
519
+ },
520
+ };
521
+ }
522
+ for (const m of matches)
523
+ db.deleteMemory(m.id);
524
+ return {
525
+ content: [{ type: "text", text: `Deleted ${matches.length} memories matching "${query}".` }],
526
+ structuredContent: {
527
+ action: "bulk_deleted",
528
+ query,
529
+ deleted: matches.length,
530
+ },
531
+ };
532
+ }
533
+ return {
534
+ isError: true,
535
+ content: [{ type: "text", text: "Provide either an id or a query to delete memories." }],
536
+ };
537
+ }
538
+ catch (error) {
539
+ return {
540
+ isError: true,
541
+ content: [{
542
+ type: "text",
543
+ text: `Error forgetting memory: ${error instanceof Error ? error.message : String(error)}`,
544
+ }],
545
+ };
546
+ }
547
+ });
548
+ // ── memory_extract ─────────────────────────────────────────
549
+ server.registerTool("memory_extract", {
550
+ title: "Extract Memories from Conversation",
551
+ description: `Extract and store multiple memories from the current conversation in one call. Use this PROACTIVELY:
552
+
553
+ WHEN to extract:
554
+ - User corrects your approach → correction (confidence: 1.0)
555
+ - An architectural decision is made → decision (confidence: 0.9)
556
+ - You notice a coding pattern the user prefers → pattern (confidence: 0.7)
557
+ - User expresses a tool/style preference → preference (confidence: 0.8)
558
+ - You learn where something is in the codebase → topology (confidence: 0.7)
559
+ - A project fact is established → fact (confidence: 0.6)
560
+
561
+ HOW OFTEN: Every ~10 exchanges, or when the conversation is ending, or after any significant decision/correction.
562
+
563
+ Each memory should be a specific, self-contained statement that would be useful in a future conversation without additional context.
564
+
565
+ Args:
566
+ - memories (array): Array of {content, type, tags, confidence} objects
567
+ - source (string): Source identifier (default: 'conversation')
568
+
569
+ Returns:
570
+ Summary of stored, reinforced, and skipped memories with details.`,
571
+ inputSchema: z.object({
572
+ memories: z.array(z.object({
573
+ content: z.string().min(1, "Content is required").max(10000, "Content too long — max 10,000 characters").describe("Specific, self-contained memory statement"),
574
+ type: z.enum(MEMORY_TYPES).describe("Memory type"),
575
+ tags: z.array(z.string()).default([]).describe("Relevant tags"),
576
+ confidence: z.number().min(0).max(1).default(0.8).describe("Confidence level"),
577
+ }).strict()).min(1, "At least one memory is required").describe("Array of memories to extract and store"),
578
+ source: z.string().default("conversation").describe("Source identifier"),
579
+ }).strict(),
580
+ outputSchema: ExtractResultSchema,
581
+ annotations: {
582
+ readOnlyHint: false,
583
+ destructiveHint: false,
584
+ idempotentHint: false,
585
+ openWorldHint: false,
586
+ },
587
+ }, async ({ memories: memoryInputs, source }) => {
588
+ try {
589
+ let stored = 0;
590
+ let reinforced = 0;
591
+ const details = [];
592
+ const structuredDetails = [];
593
+ // Load existing embeddings once (not per-memory)
594
+ const existingWithEmbeddings = db.getRecentWithEmbeddings(5000);
595
+ // Pre-compute embeddings (async) then batch all DB writes in a transaction
596
+ const pendingOps = [];
597
+ for (const input of memoryInputs) {
598
+ const embedding = await generateEmbedding(input.content);
599
+ let isDuplicate = false;
600
+ if (embedding) {
601
+ for (const mem of existingWithEmbeddings) {
602
+ if (!mem.embedding)
603
+ continue;
604
+ const sim = cosineSimilarity(embedding, mem.embedding);
605
+ if (sim > 0.85) {
606
+ pendingOps.push({
607
+ op: "reinforce",
608
+ memId: mem.id,
609
+ confidence: mem.confidence,
610
+ content: mem.content,
611
+ inputContent: input.content,
612
+ similarity: sim,
613
+ });
614
+ isDuplicate = true;
615
+ break;
616
+ }
617
+ }
618
+ }
619
+ if (!isDuplicate) {
620
+ pendingOps.push({ op: "store", input, embedding });
621
+ }
622
+ }
623
+ // Execute all DB writes atomically
624
+ db.transaction(() => {
625
+ for (const pending of pendingOps) {
626
+ if (pending.op === "reinforce") {
627
+ db.updateConfidence(pending.memId, Math.min(1.0, pending.confidence + 0.1));
628
+ db.touchAccess(pending.memId);
629
+ reinforced++;
630
+ details.push(` ~ Reinforced: "${pending.content}" (${(pending.similarity * 100).toFixed(0)}% match)`);
631
+ structuredDetails.push({
632
+ action: "reinforced",
633
+ content: pending.inputContent,
634
+ matchedContent: pending.content,
635
+ similarity: Number((pending.similarity * 100).toFixed(0)),
636
+ });
637
+ }
638
+ else {
639
+ const id = db.insertMemory({
640
+ content: pending.input.content,
641
+ type: pending.input.type,
642
+ tags: pending.input.tags,
643
+ confidence: pending.input.confidence,
644
+ source,
645
+ embedding: pending.embedding,
646
+ scope: autoScope(pending.input.type),
647
+ });
648
+ stored++;
649
+ details.push(` + Stored [${pending.input.type}]: "${pending.input.content}" (${shortId(id)})`);
650
+ structuredDetails.push({
651
+ action: "stored",
652
+ content: pending.input.content,
653
+ type: pending.input.type,
654
+ id: shortId(id),
655
+ });
656
+ }
657
+ }
658
+ });
659
+ const stats = db.getStats();
660
+ const summary = [
661
+ `Extraction complete: ${stored} stored, ${reinforced} reinforced.`,
662
+ `Total memories: ${stats.total}.`,
663
+ "",
664
+ ...details,
665
+ ].join("\n");
666
+ return {
667
+ content: [{ type: "text", text: summary }],
668
+ structuredContent: {
669
+ stored,
670
+ reinforced,
671
+ total: stats.total,
672
+ details: structuredDetails,
673
+ },
674
+ };
675
+ }
676
+ catch (error) {
677
+ return {
678
+ isError: true,
679
+ content: [{
680
+ type: "text",
681
+ text: `Error extracting memories: ${error instanceof Error ? error.message : String(error)}`,
682
+ }],
683
+ };
684
+ }
685
+ });
686
+ // ── memory_stats ──────────────────────────────────────────
687
+ server.registerTool("memory_stats", {
688
+ title: "Memory Statistics",
689
+ description: `Show memory statistics: total count, breakdown by type, confidence distribution, embedding coverage.
690
+
691
+ Args: None
692
+
693
+ Returns:
694
+ Formatted statistics including total count, per-type breakdown, confidence distribution, and embedding coverage.`,
695
+ inputSchema: z.object({}).strict(),
696
+ outputSchema: StatsResultSchema,
697
+ annotations: {
698
+ readOnlyHint: true,
699
+ destructiveHint: false,
700
+ idempotentHint: true,
701
+ openWorldHint: false,
702
+ },
703
+ }, async () => {
704
+ try {
705
+ // Use SQL aggregation — no full table load
706
+ const stats = db.getStats();
707
+ if (stats.total === 0) {
708
+ return {
709
+ content: [{ type: "text", text: "No memories stored yet. Use memory_store or memory_extract to create memories." }],
710
+ structuredContent: {
711
+ total: 0,
712
+ byType: {},
713
+ confidence: { high: 0, medium: 0, low: 0 },
714
+ embeddingCoverage: { withEmbeddings: 0, total: 0 },
715
+ },
716
+ };
717
+ }
718
+ const typeLines = TYPE_ORDER
719
+ .filter(t => (stats.byType[t] || 0) > 0)
720
+ .map(t => ` ${t}: ${stats.byType[t]}`);
721
+ const { high: highConf, medium: medConf, low: lowConf } = db.getConfidenceStats();
722
+ const withEmbeddings = db.getEmbeddingCount();
723
+ const text = [
724
+ `Total memories: ${stats.total}`,
725
+ "",
726
+ "By type:",
727
+ ...typeLines,
728
+ "",
729
+ "Confidence:",
730
+ ` High (\u226580%): ${highConf}`,
731
+ ` Medium (50-79%): ${medConf}`,
732
+ ` Low (<50%): ${lowConf}`,
733
+ "",
734
+ `Embeddings: ${withEmbeddings}/${stats.total}`,
735
+ ].join("\n");
736
+ return {
737
+ content: [{ type: "text", text }],
738
+ structuredContent: {
739
+ total: stats.total,
740
+ byType: stats.byType,
741
+ confidence: { high: highConf, medium: medConf, low: lowConf },
742
+ embeddingCoverage: { withEmbeddings, total: stats.total },
743
+ },
744
+ };
745
+ }
746
+ catch (error) {
747
+ return {
748
+ isError: true,
749
+ content: [{
750
+ type: "text",
751
+ text: `Error fetching stats: ${error instanceof Error ? error.message : String(error)}`,
752
+ }],
753
+ };
754
+ }
755
+ });
756
+ // ── memory_export ─────────────────────────────────────────
757
+ server.registerTool("memory_export", {
758
+ title: "Export Memories",
759
+ description: `Export all memories in a chosen format, grouped by type. Useful for backup, review, or sharing.
760
+
761
+ Args:
762
+ - format ("markdown" | "json", optional): Export format (default: "markdown")
763
+
764
+ Returns:
765
+ Formatted export with all memories grouped by type, including confidence, tags, and metadata.`,
766
+ inputSchema: z.object({
767
+ format: z.enum(["markdown", "json"]).default("markdown").describe("Export format"),
768
+ }).strict(),
769
+ outputSchema: ExportResultSchema,
770
+ annotations: {
771
+ readOnlyHint: true,
772
+ destructiveHint: false,
773
+ idempotentHint: true,
774
+ openWorldHint: false,
775
+ },
776
+ }, async ({ format }) => {
777
+ try {
778
+ const all = db.getAllForProject(project);
779
+ if (all.length === 0) {
780
+ return {
781
+ content: [{ type: "text", text: "No memories to export. Use memory_store or memory_extract to create memories." }],
782
+ structuredContent: {
783
+ exportedAt: new Date().toISOString(),
784
+ total: 0,
785
+ markdown: "",
786
+ truncated: false,
787
+ },
788
+ };
789
+ }
790
+ if (format === "json") {
791
+ const grouped = {};
792
+ for (const t of TYPE_ORDER) {
793
+ const memories = all.filter(m => m.type === t);
794
+ if (memories.length > 0) {
795
+ grouped[t] = memories.map(m => ({
796
+ id: m.id,
797
+ content: m.content,
798
+ confidence: m.confidence,
799
+ tags: m.tags,
800
+ scope: m.scope,
801
+ createdAt: m.createdAt,
802
+ }));
803
+ }
804
+ }
805
+ const jsonStr = JSON.stringify({ exportedAt: new Date().toISOString(), total: all.length, memories: grouped }, null, 2);
806
+ let truncated = false;
807
+ let output = jsonStr;
808
+ if (output.length > CHARACTER_LIMIT) {
809
+ output = output.slice(0, CHARACTER_LIMIT) + "\n... (truncated)";
810
+ truncated = true;
811
+ }
812
+ return {
813
+ content: [{ type: "text", text: output }],
814
+ structuredContent: {
815
+ exportedAt: new Date().toISOString(),
816
+ total: all.length,
817
+ markdown: output,
818
+ truncated,
819
+ },
820
+ };
821
+ }
822
+ let md = `# Amem Memory Export\n\n`;
823
+ md += `*Exported: ${new Date().toISOString()}*\n`;
824
+ md += `*Total: ${all.length} memories*\n\n`;
825
+ for (const t of TYPE_ORDER) {
826
+ const memories = all.filter(m => m.type === t);
827
+ if (memories.length === 0)
828
+ continue;
829
+ md += `## ${t.charAt(0).toUpperCase() + t.slice(1)}s\n\n`;
830
+ for (const m of memories) {
831
+ const conf = (m.confidence * 100).toFixed(0);
832
+ md += `- **${m.content}** (${conf}% confidence)\n`;
833
+ if (m.tags.length > 0) {
834
+ md += ` Tags: ${m.tags.join(", ")}\n`;
835
+ }
836
+ md += "\n";
837
+ }
838
+ }
839
+ // Truncate if exceeding character limit
840
+ let truncated = false;
841
+ if (md.length > CHARACTER_LIMIT) {
842
+ md = md.slice(0, CHARACTER_LIMIT);
843
+ md += `\n\n---\n*Output truncated at ${CHARACTER_LIMIT} characters. Use memory_recall with filters to view specific memories.*`;
844
+ truncated = true;
845
+ }
846
+ return {
847
+ content: [{ type: "text", text: md.trim() }],
848
+ structuredContent: {
849
+ exportedAt: new Date().toISOString(),
850
+ total: all.length,
851
+ markdown: md.trim(),
852
+ truncated,
853
+ },
854
+ };
855
+ }
856
+ catch (error) {
857
+ return {
858
+ isError: true,
859
+ content: [{
860
+ type: "text",
861
+ text: `Error exporting memories: ${error instanceof Error ? error.message : String(error)}`,
862
+ }],
863
+ };
864
+ }
865
+ });
866
+ // ── memory_inject ─────────────────────────────────────────
867
+ server.registerTool("memory_inject", {
868
+ title: "Inject Memory Context",
869
+ description: `Proactively inject relevant corrections and decisions for a topic. Use this AUTOMATICALLY at the start of any task to ensure hard constraints are respected.
870
+
871
+ Unlike memory_context (which returns all types), memory_inject focuses on the two most critical types:
872
+ - **Corrections** — hard constraints that MUST be followed (returned as a list)
873
+ - **Decisions** — architectural choices that SHOULD inform the approach (returned as a list)
874
+
875
+ This is the recommended tool for proactive context injection. Call it before writing any code.
876
+
877
+ Args:
878
+ - topic (string): The topic or task about to be worked on
879
+
880
+ Returns:
881
+ Structured object with corrections list, decisions list, and formatted context string.`,
882
+ inputSchema: z.object({
883
+ topic: z.string().min(1, "Topic is required").describe("The topic or task about to be worked on"),
884
+ }).strict(),
885
+ outputSchema: InjectResultSchema,
886
+ annotations: {
887
+ readOnlyHint: false,
888
+ destructiveHint: false,
889
+ idempotentHint: false,
890
+ openWorldHint: false,
891
+ },
892
+ }, async ({ topic }) => {
893
+ try {
894
+ const queryEmbedding = await generateEmbedding(topic);
895
+ const results = recallMemories(db, {
896
+ query: topic,
897
+ queryEmbedding,
898
+ limit: 30,
899
+ scope: project,
900
+ });
901
+ const corrections = results
902
+ .filter(r => r.type === MemoryType.CORRECTION)
903
+ .map(r => r.content);
904
+ const decisions = results
905
+ .filter(r => r.type === MemoryType.DECISION)
906
+ .map(r => r.content);
907
+ let context = "";
908
+ if (corrections.length > 0) {
909
+ context += "## Corrections (MUST follow)\n";
910
+ context += corrections.map(c => `- ${c}`).join("\n");
911
+ context += "\n\n";
912
+ }
913
+ if (decisions.length > 0) {
914
+ context += "## Decisions (SHOULD follow)\n";
915
+ context += decisions.map(d => `- ${d}`).join("\n");
916
+ context += "\n";
917
+ }
918
+ if (corrections.length === 0 && decisions.length === 0) {
919
+ return {
920
+ content: [{ type: "text", text: `No corrections or decisions found for: "${topic}".` }],
921
+ structuredContent: {
922
+ topic,
923
+ corrections: [],
924
+ decisions: [],
925
+ context: "",
926
+ memoriesUsed: 0,
927
+ },
928
+ };
929
+ }
930
+ // Only touch access for memories actually surfaced to the user
931
+ for (const r of results) {
932
+ if (r.type === MemoryType.CORRECTION || r.type === MemoryType.DECISION) {
933
+ db.touchAccess(r.id);
934
+ }
935
+ }
936
+ return {
937
+ content: [{ type: "text", text: context.trim() }],
938
+ structuredContent: {
939
+ topic,
940
+ corrections,
941
+ decisions,
942
+ context: context.trim(),
943
+ memoriesUsed: corrections.length + decisions.length,
944
+ },
945
+ };
946
+ }
947
+ catch (error) {
948
+ return {
949
+ isError: true,
950
+ content: [{
951
+ type: "text",
952
+ text: `Error injecting context: ${error instanceof Error ? error.message : String(error)}`,
953
+ }],
954
+ };
955
+ }
956
+ });
957
+ // ── memory_consolidate ──────────────────────────────────
958
+ server.registerTool("memory_consolidate", {
959
+ title: "Consolidate Memories",
960
+ description: `Analyze and optimize the memory database. Merges near-duplicates, prunes stale low-value memories, and promotes frequently-accessed ones. This keeps your memory system lean and high-signal over months of use.
961
+
962
+ NEVER auto-prunes corrections (they are always preserved).
963
+
964
+ Args:
965
+ - confirm (boolean): false = preview what would change (default), true = execute changes
966
+ - max_stale_days (number): Days of inactivity before a memory is considered stale (default: 60)
967
+ - min_confidence (number): Minimum confidence for stale memories to survive (default: 0.3)
968
+ - min_access_count (number): Minimum access count for stale memories to survive (default: 2)
969
+ - enable_decay (boolean): Enable confidence decay for stale non-correction memories (default: false)
970
+ - decay_factor (number 0-1): Multiplier applied to confidence per consolidation cycle (default: 0.95)
971
+
972
+ Returns:
973
+ Report with merged/pruned/promoted/decayed counts, health score, and detailed action list.`,
974
+ inputSchema: z.object({
975
+ confirm: z.boolean().default(false).describe("false = preview (safe), true = execute consolidation"),
976
+ max_stale_days: z.number().int().min(1).default(60).describe("Days of inactivity before considering a memory stale"),
977
+ min_confidence: z.number().min(0).max(1).default(0.3).describe("Confidence threshold for stale memory pruning"),
978
+ min_access_count: z.number().int().min(0).default(2).describe("Minimum access count for stale memories to survive pruning"),
979
+ enable_decay: z.boolean().default(false).describe("Enable confidence decay for stale non-correction memories"),
980
+ decay_factor: z.number().min(0.5).max(1).default(0.95).describe("Multiplier applied to confidence per consolidation cycle"),
981
+ }).strict(),
982
+ outputSchema: ConsolidateResultSchema,
983
+ annotations: {
984
+ readOnlyHint: false,
985
+ destructiveHint: true,
986
+ idempotentHint: true,
987
+ openWorldHint: false,
988
+ },
989
+ }, async ({ confirm, max_stale_days, min_confidence, min_access_count, enable_decay, decay_factor }) => {
990
+ try {
991
+ const report = consolidateMemories(db, cosineSimilarity, {
992
+ dryRun: !confirm,
993
+ maxStaleDays: max_stale_days,
994
+ minConfidence: min_confidence,
995
+ minAccessCount: min_access_count,
996
+ enableDecay: enable_decay,
997
+ decayFactor: decay_factor,
998
+ });
999
+ const mode = confirm ? "EXECUTED" : "PREVIEW (dry run)";
1000
+ const lines = [
1001
+ `Memory Consolidation — ${mode}`,
1002
+ "",
1003
+ `Health Score: ${report.healthScore}/100`,
1004
+ `Before: ${report.before.total} memories`,
1005
+ `After: ${report.after.total} memories`,
1006
+ "",
1007
+ `Merged: ${report.merged} near-duplicates`,
1008
+ `Pruned: ${report.pruned} stale memories`,
1009
+ `Promoted: ${report.promoted} frequently-used memories`,
1010
+ ...(report.decayed > 0 ? [`Decayed: ${report.decayed} stale memories (confidence reduced)`] : []),
1011
+ ];
1012
+ if (report.actions.length > 0) {
1013
+ lines.push("", "Details:");
1014
+ for (const a of report.actions) {
1015
+ const prefix = a.action === "merged" ? "~" : a.action === "pruned" ? "-" : a.action === "decayed" ? "v" : "+";
1016
+ lines.push(` ${prefix} ${a.description}`);
1017
+ }
1018
+ }
1019
+ if (!confirm && (report.merged > 0 || report.pruned > 0 || report.decayed > 0)) {
1020
+ lines.push("", "Call again with confirm=true to execute these changes.");
1021
+ }
1022
+ return {
1023
+ content: [{ type: "text", text: lines.join("\n") }],
1024
+ structuredContent: {
1025
+ merged: report.merged,
1026
+ pruned: report.pruned,
1027
+ promoted: report.promoted,
1028
+ decayed: report.decayed,
1029
+ healthScore: report.healthScore,
1030
+ before: report.before,
1031
+ after: report.after,
1032
+ actions: report.actions,
1033
+ },
1034
+ };
1035
+ }
1036
+ catch (error) {
1037
+ return {
1038
+ isError: true,
1039
+ content: [{
1040
+ type: "text",
1041
+ text: `Error consolidating memories: ${error instanceof Error ? error.message : String(error)}`,
1042
+ }],
1043
+ };
1044
+ }
1045
+ });
1046
+ // ── memory_patch ──────────────────────────────────────────
1047
+ server.registerTool("memory_patch", {
1048
+ title: "Patch Memory",
1049
+ description: `Apply a targeted, AI-executable patch to an existing memory. Unlike delete+recreate, patches are surgical — they update a single field while automatically snapshotting the previous state into version history for full reversibility.
1050
+
1051
+ Use this when:
1052
+ - Correcting a memory that is mostly right but has a wrong detail
1053
+ - Updating confidence after validation
1054
+ - Retagging a memory for better recall
1055
+ - Reclassifying type (e.g. fact → decision)
1056
+
1057
+ Every patch creates a version snapshot. Use memory_versions to view history or roll back.
1058
+
1059
+ Args:
1060
+ - id (string): Memory ID to patch (short IDs like first 8 chars work)
1061
+ - field (enum): Which field to change — content | confidence | tags | type
1062
+ - value (string | number | string[]): New value for the field
1063
+ - reason (string): Why this patch is being made — stored in version history`,
1064
+ inputSchema: z.object({
1065
+ id: z.string().min(1, "Memory ID is required").describe("Memory ID — full UUID or first 8 characters"),
1066
+ field: z.enum(["content", "confidence", "tags", "type"]).describe("Which field to patch"),
1067
+ value: z.union([
1068
+ z.string(),
1069
+ z.number().min(0).max(1),
1070
+ z.array(z.string()),
1071
+ ]).describe("New value — string for content/type, number 0-1 for confidence, string[] for tags"),
1072
+ reason: z.string().min(1).describe("Why this patch is being made — stored in version history"),
1073
+ }).strict().refine(({ field, value }) => {
1074
+ if (field === "confidence")
1075
+ return typeof value === "number";
1076
+ if (field === "tags")
1077
+ return Array.isArray(value);
1078
+ if (field === "content" || field === "type")
1079
+ return typeof value === "string";
1080
+ return true;
1081
+ }, { message: "Value type must match field: string for content/type, number for confidence, string[] for tags" }),
1082
+ // outputSchema omitted — z.union() causes _zod serialization errors in MCP SDK
1083
+ annotations: {
1084
+ readOnlyHint: false,
1085
+ destructiveHint: false,
1086
+ idempotentHint: false,
1087
+ openWorldHint: false,
1088
+ },
1089
+ }, async ({ id, field, value, reason }) => {
1090
+ try {
1091
+ const fullId = db.resolveId(id);
1092
+ if (!fullId) {
1093
+ return {
1094
+ content: [{ type: "text", text: `No memory found with ID starting with "${id}".` }],
1095
+ structuredContent: { action: "not_found", id },
1096
+ };
1097
+ }
1098
+ const mem = db.getById(fullId);
1099
+ if (!mem) {
1100
+ return {
1101
+ content: [{ type: "text", text: `Memory "${fullId}" not found.` }],
1102
+ structuredContent: { action: "not_found", id: fullId },
1103
+ };
1104
+ }
1105
+ const previousContent = field === "content" ? mem.content
1106
+ : field === "confidence" ? String(mem.confidence)
1107
+ : field === "tags" ? JSON.stringify(mem.tags)
1108
+ : mem.type;
1109
+ const success = db.patchMemory(fullId, { field, value, reason });
1110
+ if (!success) {
1111
+ return {
1112
+ isError: true,
1113
+ content: [{ type: "text", text: `Failed to patch memory "${fullId}". Unknown field or DB error.` }],
1114
+ };
1115
+ }
1116
+ // Regenerate embedding if content changed
1117
+ if (field === "content" && typeof value === "string") {
1118
+ const newEmbedding = await generateEmbedding(value);
1119
+ if (newEmbedding)
1120
+ db.updateEmbedding(fullId, newEmbedding);
1121
+ }
1122
+ const displayValue = Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
1123
+ return {
1124
+ content: [{
1125
+ type: "text",
1126
+ text: `Patched memory (${shortId(fullId)}): ${field} → ${displayValue}\nReason: ${reason}\nPrevious ${field}: ${previousContent}\nVersion snapshot saved.`,
1127
+ }],
1128
+ structuredContent: {
1129
+ action: "patched",
1130
+ id: fullId,
1131
+ field,
1132
+ previousContent,
1133
+ reason,
1134
+ versionSaved: true,
1135
+ },
1136
+ };
1137
+ }
1138
+ catch (error) {
1139
+ return {
1140
+ isError: true,
1141
+ content: [{
1142
+ type: "text",
1143
+ text: `Error patching memory: ${error instanceof Error ? error.message : String(error)}`,
1144
+ }],
1145
+ };
1146
+ }
1147
+ });
1148
+ // ── memory_import ────────────────────────────────────────
1149
+ server.registerTool("memory_import", {
1150
+ title: "Import Memories from JSON",
1151
+ description: `Import memories from a JSON array. Use this to restore from a backup, migrate between machines, or seed memories from another source.
1152
+
1153
+ Each memory in the array should have: content, type, tags, confidence. Duplicates are detected by content hash and skipped.
1154
+
1155
+ Args:
1156
+ - memories (array): Array of {content, type, tags, confidence} objects to import
1157
+ - source (string): Import source identifier (default: 'import')
1158
+
1159
+ Returns:
1160
+ Summary of imported, skipped (duplicate), and total memories.`,
1161
+ inputSchema: z.object({
1162
+ memories: z.array(z.object({
1163
+ content: z.string().min(1).max(10000),
1164
+ type: z.enum(MEMORY_TYPES),
1165
+ tags: z.array(z.string()).default([]),
1166
+ confidence: z.number().min(0).max(1).default(0.8),
1167
+ }).strict()).min(1).max(500).describe("Array of memories to import"),
1168
+ source: z.string().default("import").describe("Import source identifier"),
1169
+ }).strict(),
1170
+ outputSchema: ExtractResultSchema,
1171
+ annotations: {
1172
+ readOnlyHint: false,
1173
+ destructiveHint: false,
1174
+ idempotentHint: true,
1175
+ openWorldHint: false,
1176
+ },
1177
+ }, async ({ memories: memoryInputs, source }) => {
1178
+ try {
1179
+ let stored = 0;
1180
+ let skipped = 0;
1181
+ const details = [];
1182
+ // Pre-compute embeddings, then batch all DB writes
1183
+ const pendingOps = [];
1184
+ for (const input of memoryInputs) {
1185
+ // Skip exact duplicates by content hash
1186
+ const existing = db.findByContentHash(input.content);
1187
+ if (existing) {
1188
+ skipped++;
1189
+ details.push({
1190
+ action: "reinforced",
1191
+ content: input.content,
1192
+ matchedContent: existing.content,
1193
+ similarity: 100,
1194
+ });
1195
+ continue;
1196
+ }
1197
+ const embedding = await generateEmbedding(input.content);
1198
+ pendingOps.push({ input, embedding });
1199
+ }
1200
+ db.transaction(() => {
1201
+ for (const { input, embedding } of pendingOps) {
1202
+ const id = db.insertMemory({
1203
+ content: input.content,
1204
+ type: input.type,
1205
+ tags: input.tags,
1206
+ confidence: input.confidence,
1207
+ source,
1208
+ embedding,
1209
+ scope: autoScope(input.type),
1210
+ });
1211
+ stored++;
1212
+ details.push({
1213
+ action: "stored",
1214
+ content: input.content,
1215
+ type: input.type,
1216
+ id: shortId(id),
1217
+ });
1218
+ }
1219
+ });
1220
+ const stats = db.getStats();
1221
+ return {
1222
+ content: [{
1223
+ type: "text",
1224
+ text: `Import complete: ${stored} imported, ${skipped} duplicates skipped. Total memories: ${stats.total}.`,
1225
+ }],
1226
+ structuredContent: {
1227
+ stored,
1228
+ reinforced: skipped,
1229
+ total: stats.total,
1230
+ details,
1231
+ },
1232
+ };
1233
+ }
1234
+ catch (error) {
1235
+ return {
1236
+ isError: true,
1237
+ content: [{
1238
+ type: "text",
1239
+ text: `Error importing memories: ${error instanceof Error ? error.message : String(error)}`,
1240
+ }],
1241
+ };
1242
+ }
1243
+ });
1244
+ }
1245
+ //# sourceMappingURL=memory.js.map