@aman_asmuei/amem 0.5.1 → 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.
- package/README.md +49 -7
- package/dist/cli.js +7 -7
- package/dist/cli.js.map +1 -1
- package/dist/database.d.ts +15 -0
- package/dist/database.js +129 -19
- package/dist/database.js.map +1 -1
- package/dist/database.test.d.ts +1 -0
- package/dist/database.test.js +275 -0
- package/dist/database.test.js.map +1 -0
- package/dist/embeddings.js +30 -2
- package/dist/embeddings.js.map +1 -1
- package/dist/embeddings.test.d.ts +1 -0
- package/dist/embeddings.test.js +106 -0
- package/dist/embeddings.test.js.map +1 -0
- package/dist/index.js +158 -80
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +108 -35
- package/dist/memory.js.map +1 -1
- package/dist/memory.test.d.ts +1 -0
- package/dist/memory.test.js +171 -0
- package/dist/memory.test.js.map +1 -0
- package/dist/schemas.d.ts +209 -31
- package/dist/schemas.js +54 -1
- package/dist/schemas.js.map +1 -1
- package/dist/tools/graph.d.ts +3 -0
- package/dist/tools/graph.js +344 -0
- package/dist/tools/graph.js.map +1 -0
- package/dist/tools/helpers.d.ts +7 -0
- package/dist/tools/helpers.js +23 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +19 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log.d.ts +3 -0
- package/dist/tools/log.js +244 -0
- package/dist/tools/log.js.map +1 -0
- package/dist/tools/memory.d.ts +4 -0
- package/dist/tools/memory.js +1245 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/reminders.d.ts +3 -0
- package/dist/tools/reminders.js +228 -0
- package/dist/tools/reminders.js.map +1 -0
- package/dist/tools/versions.d.ts +3 -0
- package/dist/tools/versions.js +118 -0
- package/dist/tools/versions.js.map +1 -0
- package/dist/tools.test.d.ts +1 -0
- package/dist/tools.test.js +217 -0
- package/dist/tools.test.js.map +1 -0
- package/package.json +1 -2
- package/dist/tools.d.ts +0 -6
- package/dist/tools.js +0 -1885
- package/dist/tools.js.map +0 -1
package/dist/tools.js
DELETED
|
@@ -1,1885 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { MemoryType, recallMemories, detectConflict, consolidateMemories } from "./memory.js";
|
|
3
|
-
import { generateEmbedding, cosineSimilarity } from "./embeddings.js";
|
|
4
|
-
import { StoreResultSchema, RecallResultSchema, ContextResultSchema, ForgetResultSchema, ExtractResultSchema, StatsResultSchema, ExportResultSchema, InjectResultSchema, ConsolidateResultSchema, PatchResultSchema, LogAppendResultSchema, LogRecallResultSchema, RelateResultSchema, VersionResultSchema, TemporalResultSchema, } from "./schemas.js";
|
|
5
|
-
const MEMORY_TYPES = Object.values(MemoryType);
|
|
6
|
-
const CHARACTER_LIMIT = 50_000;
|
|
7
|
-
export const TYPE_ORDER = ["correction", "decision", "pattern", "preference", "topology", "fact"];
|
|
8
|
-
export function formatAge(timestamp) {
|
|
9
|
-
const ms = Date.now() - timestamp;
|
|
10
|
-
const minutes = Math.floor(ms / 60000);
|
|
11
|
-
if (minutes < 60)
|
|
12
|
-
return `${minutes}m ago`;
|
|
13
|
-
const hours = Math.floor(minutes / 60);
|
|
14
|
-
if (hours < 24)
|
|
15
|
-
return `${hours}h ago`;
|
|
16
|
-
const days = Math.floor(hours / 24);
|
|
17
|
-
if (days < 30)
|
|
18
|
-
return `${days}d ago`;
|
|
19
|
-
const months = Math.floor(days / 30);
|
|
20
|
-
return `${months}mo ago`;
|
|
21
|
-
}
|
|
22
|
-
export function registerTools(server, db, project) {
|
|
23
|
-
const GLOBAL_TYPES = ["correction", "preference", "pattern"];
|
|
24
|
-
function autoScope(type) {
|
|
25
|
-
return GLOBAL_TYPES.includes(type) ? "global" : project;
|
|
26
|
-
}
|
|
27
|
-
// ── memory_store ──────────────────────────────────────────
|
|
28
|
-
server.registerTool("memory_store", {
|
|
29
|
-
title: "Store Memory",
|
|
30
|
-
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.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
- content (string): The memory content — be specific and self-contained
|
|
34
|
-
- type (enum): Memory type — corrections are highest priority
|
|
35
|
-
- tags (string[]): Tags for filtering (e.g., ['typescript', 'auth', 'testing'])
|
|
36
|
-
- confidence (number 0-1): How confident is this memory. Corrections from user = 1.0
|
|
37
|
-
- source (string): Where this memory came from (default: 'conversation')
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
Confirmation with memory ID, or conflict detection if a similar memory exists.`,
|
|
41
|
-
inputSchema: z.object({
|
|
42
|
-
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"),
|
|
43
|
-
type: z.enum(MEMORY_TYPES).describe("Memory type — corrections are highest priority"),
|
|
44
|
-
tags: z.array(z.string()).default([]).describe("Tags for filtering (e.g., ['typescript', 'auth', 'testing'])"),
|
|
45
|
-
confidence: z.number().min(0).max(1).default(0.8).describe("How confident is this memory (0-1). Corrections from user = 1.0"),
|
|
46
|
-
source: z.string().default("conversation").describe("Where this memory came from"),
|
|
47
|
-
scope: z.string().optional().describe("Memory scope — 'global' or 'project:<name>'. Auto-detected from type if omitted."),
|
|
48
|
-
}).strict(),
|
|
49
|
-
outputSchema: StoreResultSchema,
|
|
50
|
-
annotations: {
|
|
51
|
-
readOnlyHint: false,
|
|
52
|
-
destructiveHint: false,
|
|
53
|
-
idempotentHint: false,
|
|
54
|
-
openWorldHint: false,
|
|
55
|
-
},
|
|
56
|
-
}, async ({ content, type, tags, confidence, source, scope }) => {
|
|
57
|
-
try {
|
|
58
|
-
const embedding = await generateEmbedding(content);
|
|
59
|
-
// Single pass over existing memories: conflict detection + reinforcement
|
|
60
|
-
if (embedding) {
|
|
61
|
-
const existing = db.getAllWithEmbeddings();
|
|
62
|
-
for (const mem of existing) {
|
|
63
|
-
if (!mem.embedding)
|
|
64
|
-
continue;
|
|
65
|
-
const sim = cosineSimilarity(embedding, mem.embedding);
|
|
66
|
-
const conflict = detectConflict(content, mem.content, sim);
|
|
67
|
-
if (conflict.isConflict) {
|
|
68
|
-
db.updateConfidence(mem.id, Math.max(mem.confidence, confidence));
|
|
69
|
-
return {
|
|
70
|
-
content: [{
|
|
71
|
-
type: "text",
|
|
72
|
-
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.`,
|
|
73
|
-
}],
|
|
74
|
-
structuredContent: {
|
|
75
|
-
action: "conflict_resolved",
|
|
76
|
-
existingId: mem.id,
|
|
77
|
-
similarity: Number((sim * 100).toFixed(0)),
|
|
78
|
-
existingContent: mem.content,
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (sim > 0.8) {
|
|
83
|
-
db.updateConfidence(mem.id, Math.min(1.0, mem.confidence + 0.1));
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const id = db.insertMemory({ content, type: type, tags, confidence, source, embedding, scope: scope ?? autoScope(type) });
|
|
87
|
-
// Reinforce related memories (0.6-0.8 range) using already-loaded embeddings
|
|
88
|
-
let evolved = 0;
|
|
89
|
-
for (const mem of existing) {
|
|
90
|
-
if (!mem.embedding)
|
|
91
|
-
continue;
|
|
92
|
-
const sim = cosineSimilarity(embedding, mem.embedding);
|
|
93
|
-
if (sim > 0.6 && sim <= 0.8) {
|
|
94
|
-
db.touchAccess(mem.id);
|
|
95
|
-
evolved++;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
const stats = db.getStats();
|
|
99
|
-
const evolvedNote = evolved > 0 ? ` Reinforced ${evolved} related memories.` : "";
|
|
100
|
-
return {
|
|
101
|
-
content: [{
|
|
102
|
-
type: "text",
|
|
103
|
-
text: `Stored ${type} memory (${id.slice(0, 8)}). Confidence: ${confidence}. Tags: [${tags.join(", ")}]. Total memories: ${stats.total}.${evolvedNote}`,
|
|
104
|
-
}],
|
|
105
|
-
structuredContent: {
|
|
106
|
-
action: "stored",
|
|
107
|
-
id,
|
|
108
|
-
type,
|
|
109
|
-
confidence,
|
|
110
|
-
tags,
|
|
111
|
-
total: stats.total,
|
|
112
|
-
reinforced: evolved,
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// No embeddings available — store directly
|
|
117
|
-
const id = db.insertMemory({ content, type: type, tags, confidence, source, embedding, scope: scope ?? autoScope(type) });
|
|
118
|
-
const stats = db.getStats();
|
|
119
|
-
return {
|
|
120
|
-
content: [{
|
|
121
|
-
type: "text",
|
|
122
|
-
text: `Stored ${type} memory (${id.slice(0, 8)}). Confidence: ${confidence}. Tags: [${tags.join(", ")}]. Total memories: ${stats.total}.`,
|
|
123
|
-
}],
|
|
124
|
-
structuredContent: {
|
|
125
|
-
action: "stored",
|
|
126
|
-
id,
|
|
127
|
-
type,
|
|
128
|
-
confidence,
|
|
129
|
-
tags,
|
|
130
|
-
total: stats.total,
|
|
131
|
-
reinforced: 0,
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
return {
|
|
137
|
-
isError: true,
|
|
138
|
-
content: [{
|
|
139
|
-
type: "text",
|
|
140
|
-
text: `Error storing memory: ${error instanceof Error ? error.message : String(error)}. Ensure the database is accessible and content is valid.`,
|
|
141
|
-
}],
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
// ── memory_recall ─────────────────────────────────────────
|
|
146
|
-
server.registerTool("memory_recall", {
|
|
147
|
-
title: "Recall Memories",
|
|
148
|
-
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.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
- query (string): What to search for — natural language works best
|
|
152
|
-
- limit (number 1-50): Max results to return (default: 10)
|
|
153
|
-
- type (enum, optional): Filter by memory type
|
|
154
|
-
- tag (string, optional): Filter by tag
|
|
155
|
-
- min_confidence (number 0-1, optional): Minimum confidence threshold
|
|
156
|
-
- compact (boolean, optional): If true, return compact index (~50-100 tokens) with IDs for progressive disclosure. Use memory_detail to get full content.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Ranked list of memories with scores, confidence, age, and tags. If compact=true, returns a compact index with short IDs and previews.`,
|
|
160
|
-
inputSchema: z.object({
|
|
161
|
-
query: z.string().min(1, "Query is required").describe("What to search for — natural language works best"),
|
|
162
|
-
limit: z.number().int().min(1).max(50).default(10).describe("Max results to return"),
|
|
163
|
-
type: z.enum(MEMORY_TYPES).optional().describe("Filter by memory type"),
|
|
164
|
-
tag: z.string().optional().describe("Filter by tag"),
|
|
165
|
-
min_confidence: z.number().min(0).max(1).optional().describe("Minimum confidence threshold"),
|
|
166
|
-
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."),
|
|
167
|
-
}).strict(),
|
|
168
|
-
outputSchema: RecallResultSchema,
|
|
169
|
-
annotations: {
|
|
170
|
-
readOnlyHint: false,
|
|
171
|
-
destructiveHint: false,
|
|
172
|
-
idempotentHint: false,
|
|
173
|
-
openWorldHint: false,
|
|
174
|
-
},
|
|
175
|
-
}, async ({ query, limit, type, tag, min_confidence, compact }) => {
|
|
176
|
-
try {
|
|
177
|
-
const queryEmbedding = await generateEmbedding(query);
|
|
178
|
-
const results = recallMemories(db, {
|
|
179
|
-
query,
|
|
180
|
-
queryEmbedding,
|
|
181
|
-
limit,
|
|
182
|
-
type: type,
|
|
183
|
-
tag,
|
|
184
|
-
minConfidence: min_confidence,
|
|
185
|
-
scope: project,
|
|
186
|
-
});
|
|
187
|
-
for (const r of results) {
|
|
188
|
-
db.touchAccess(r.id);
|
|
189
|
-
}
|
|
190
|
-
if (results.length === 0) {
|
|
191
|
-
return {
|
|
192
|
-
content: [{ type: "text", text: `No memories found for: "${query}". Try broadening your search or using different keywords.` }],
|
|
193
|
-
structuredContent: {
|
|
194
|
-
query,
|
|
195
|
-
total: 0,
|
|
196
|
-
memories: [],
|
|
197
|
-
},
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
if (compact) {
|
|
201
|
-
const compactLines = results.map((r, i) => {
|
|
202
|
-
const preview = r.content.slice(0, 80) + (r.content.length > 80 ? "..." : "");
|
|
203
|
-
return `${r.id.slice(0, 8)} [${r.type}] ${preview} (${(r.score * 100).toFixed(0)}%)`;
|
|
204
|
-
});
|
|
205
|
-
const tokenEstimate = compactLines.join("\n").split(/\s+/).length;
|
|
206
|
-
return {
|
|
207
|
-
content: [{
|
|
208
|
-
type: "text",
|
|
209
|
-
text: `${results.length} memories (~${tokenEstimate} tokens):\n${compactLines.join("\n")}\n\nUse memory_detail with IDs for full content.`,
|
|
210
|
-
}],
|
|
211
|
-
structuredContent: {
|
|
212
|
-
query,
|
|
213
|
-
total: results.length,
|
|
214
|
-
compact: true,
|
|
215
|
-
tokenEstimate,
|
|
216
|
-
memories: results.map(r => ({
|
|
217
|
-
id: r.id,
|
|
218
|
-
type: r.type,
|
|
219
|
-
preview: r.content.slice(0, 80),
|
|
220
|
-
score: Number(r.score.toFixed(3)),
|
|
221
|
-
confidence: r.confidence,
|
|
222
|
-
})),
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
const memoriesData = results.map((r) => ({
|
|
227
|
-
id: r.id,
|
|
228
|
-
content: r.content,
|
|
229
|
-
type: r.type,
|
|
230
|
-
score: Number(r.score.toFixed(3)),
|
|
231
|
-
confidence: r.confidence,
|
|
232
|
-
tags: r.tags,
|
|
233
|
-
age: formatAge(r.createdAt),
|
|
234
|
-
}));
|
|
235
|
-
const lines = results.map((r, i) => {
|
|
236
|
-
const age = formatAge(r.createdAt);
|
|
237
|
-
const conf = (r.confidence * 100).toFixed(0);
|
|
238
|
-
return `${i + 1}. [${r.type}] ${r.content}\n Score: ${r.score.toFixed(3)} | Confidence: ${conf}% | Age: ${age} | Tags: [${r.tags.join(", ")}]`;
|
|
239
|
-
});
|
|
240
|
-
return {
|
|
241
|
-
content: [{
|
|
242
|
-
type: "text",
|
|
243
|
-
text: `Found ${results.length} memories for "${query}":\n\n${lines.join("\n\n")}`,
|
|
244
|
-
}],
|
|
245
|
-
structuredContent: {
|
|
246
|
-
query,
|
|
247
|
-
total: results.length,
|
|
248
|
-
memories: memoriesData,
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
return {
|
|
254
|
-
isError: true,
|
|
255
|
-
content: [{
|
|
256
|
-
type: "text",
|
|
257
|
-
text: `Error recalling memories: ${error instanceof Error ? error.message : String(error)}. Try a different query or check the database.`,
|
|
258
|
-
}],
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
// ── memory_detail ────────────────────────────────────────
|
|
263
|
-
server.registerTool("memory_detail", {
|
|
264
|
-
title: "Get Memory Details",
|
|
265
|
-
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).",
|
|
266
|
-
inputSchema: z.object({
|
|
267
|
-
ids: z.array(z.string()).min(1).max(20).describe("Memory IDs (full or first 8 chars) to retrieve"),
|
|
268
|
-
}).strict(),
|
|
269
|
-
}, async ({ ids }) => {
|
|
270
|
-
try {
|
|
271
|
-
const allMemories = db.getAll();
|
|
272
|
-
const found = ids.map(id => {
|
|
273
|
-
const mem = allMemories.find(m => m.id === id || m.id.startsWith(id));
|
|
274
|
-
if (!mem)
|
|
275
|
-
return null;
|
|
276
|
-
db.touchAccess(mem.id);
|
|
277
|
-
return mem;
|
|
278
|
-
}).filter((m) => m !== null);
|
|
279
|
-
if (found.length === 0) {
|
|
280
|
-
return {
|
|
281
|
-
content: [{ type: "text", text: "No memories found for the given IDs." }],
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
const lines = found.map((r) => {
|
|
285
|
-
const age = formatAge(r.createdAt);
|
|
286
|
-
const conf = (r.confidence * 100).toFixed(0);
|
|
287
|
-
return `[${r.type}] ${r.content}\nID: ${r.id.slice(0, 8)} | Confidence: ${conf}% | Age: ${age} | Tags: [${r.tags.join(", ")}]`;
|
|
288
|
-
});
|
|
289
|
-
const tokenEstimate = lines.join("\n\n").split(/\s+/).length;
|
|
290
|
-
return {
|
|
291
|
-
content: [{
|
|
292
|
-
type: "text",
|
|
293
|
-
text: `${found.length} memories (~${tokenEstimate} tokens):\n\n${lines.join("\n\n")}`,
|
|
294
|
-
}],
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
return {
|
|
299
|
-
isError: true,
|
|
300
|
-
content: [{
|
|
301
|
-
type: "text",
|
|
302
|
-
text: `Error retrieving memories: ${error instanceof Error ? error.message : String(error)}`,
|
|
303
|
-
}],
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
// ── memory_context ────────────────────────────────────────
|
|
308
|
-
server.registerTool("memory_context", {
|
|
309
|
-
title: "Get Memory Context",
|
|
310
|
-
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).
|
|
311
|
-
|
|
312
|
-
Args:
|
|
313
|
-
- topic (string): The topic or task you need context for
|
|
314
|
-
- max_tokens (number): Approximate token budget for context (default: 2000)
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
Markdown-formatted context grouped by memory type, with corrections first.`,
|
|
318
|
-
inputSchema: z.object({
|
|
319
|
-
topic: z.string().min(1, "Topic is required").describe("The topic or task you need context for"),
|
|
320
|
-
max_tokens: z.number().int().min(100).max(10000).default(2000).describe("Approximate token budget for context"),
|
|
321
|
-
}).strict(),
|
|
322
|
-
outputSchema: ContextResultSchema,
|
|
323
|
-
annotations: {
|
|
324
|
-
readOnlyHint: false,
|
|
325
|
-
destructiveHint: false,
|
|
326
|
-
idempotentHint: false,
|
|
327
|
-
openWorldHint: false,
|
|
328
|
-
},
|
|
329
|
-
}, async ({ topic, max_tokens }) => {
|
|
330
|
-
try {
|
|
331
|
-
const queryEmbedding = await generateEmbedding(topic);
|
|
332
|
-
const results = recallMemories(db, {
|
|
333
|
-
query: topic,
|
|
334
|
-
queryEmbedding,
|
|
335
|
-
limit: 50,
|
|
336
|
-
scope: project,
|
|
337
|
-
});
|
|
338
|
-
if (results.length === 0) {
|
|
339
|
-
return {
|
|
340
|
-
content: [{ type: "text", text: `No context found for: "${topic}". Store some memories first using memory_store or memory_extract.` }],
|
|
341
|
-
structuredContent: {
|
|
342
|
-
topic,
|
|
343
|
-
groups: [],
|
|
344
|
-
memoriesUsed: 0,
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
const grouped = {};
|
|
349
|
-
for (const r of results) {
|
|
350
|
-
if (!grouped[r.type])
|
|
351
|
-
grouped[r.type] = [];
|
|
352
|
-
grouped[r.type].push(r);
|
|
353
|
-
}
|
|
354
|
-
let output = `## Context for: ${topic}\n\n`;
|
|
355
|
-
let approxTokens = 0;
|
|
356
|
-
const CHARS_PER_TOKEN = 4;
|
|
357
|
-
for (const t of TYPE_ORDER) {
|
|
358
|
-
const memories = grouped[t];
|
|
359
|
-
if (!memories || memories.length === 0)
|
|
360
|
-
continue;
|
|
361
|
-
const header = `### ${t.charAt(0).toUpperCase() + t.slice(1)}s\n`;
|
|
362
|
-
output += header;
|
|
363
|
-
approxTokens += header.length / CHARS_PER_TOKEN;
|
|
364
|
-
for (const m of memories) {
|
|
365
|
-
const line = `- ${m.content} (${(m.confidence * 100).toFixed(0)}% confidence)\n`;
|
|
366
|
-
approxTokens += line.length / CHARS_PER_TOKEN;
|
|
367
|
-
if (approxTokens > max_tokens)
|
|
368
|
-
break;
|
|
369
|
-
output += line;
|
|
370
|
-
}
|
|
371
|
-
output += "\n";
|
|
372
|
-
if (approxTokens > max_tokens)
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
for (const r of results)
|
|
376
|
-
db.touchAccess(r.id);
|
|
377
|
-
const groups = TYPE_ORDER
|
|
378
|
-
.filter(t => grouped[t] && grouped[t].length > 0)
|
|
379
|
-
.map(t => ({
|
|
380
|
-
type: t,
|
|
381
|
-
memories: grouped[t].map(m => ({
|
|
382
|
-
content: m.content,
|
|
383
|
-
confidence: m.confidence,
|
|
384
|
-
})),
|
|
385
|
-
}));
|
|
386
|
-
return {
|
|
387
|
-
content: [{ type: "text", text: output.trim() }],
|
|
388
|
-
structuredContent: {
|
|
389
|
-
topic,
|
|
390
|
-
groups,
|
|
391
|
-
memoriesUsed: results.length,
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
return {
|
|
397
|
-
isError: true,
|
|
398
|
-
content: [{
|
|
399
|
-
type: "text",
|
|
400
|
-
text: `Error loading context: ${error instanceof Error ? error.message : String(error)}. Try a different topic or check the database.`,
|
|
401
|
-
}],
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
// ── memory_forget ─────────────────────────────────────────
|
|
406
|
-
server.registerTool("memory_forget", {
|
|
407
|
-
title: "Forget Memory",
|
|
408
|
-
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.
|
|
409
|
-
|
|
410
|
-
Args:
|
|
411
|
-
- id (string, optional): Specific memory ID to delete
|
|
412
|
-
- query (string, optional): Delete all memories matching this query (requires confirmation)
|
|
413
|
-
- confirm (boolean): Must be true to actually delete when using query-based deletion (default: false)
|
|
414
|
-
|
|
415
|
-
Returns:
|
|
416
|
-
Deletion confirmation, or a preview of matching memories when confirm=false.
|
|
417
|
-
|
|
418
|
-
Error Handling:
|
|
419
|
-
- Returns error if neither id nor query is provided
|
|
420
|
-
- Returns error if memory ID not found`,
|
|
421
|
-
inputSchema: z.object({
|
|
422
|
-
id: z.string().optional().describe("Specific memory ID to delete"),
|
|
423
|
-
query: z.string().optional().describe("Delete all memories matching this query (requires confirmation)"),
|
|
424
|
-
confirm: z.boolean().default(false).describe("Must be true to actually delete when using query-based deletion"),
|
|
425
|
-
}).strict(),
|
|
426
|
-
outputSchema: ForgetResultSchema,
|
|
427
|
-
annotations: {
|
|
428
|
-
readOnlyHint: false,
|
|
429
|
-
destructiveHint: true,
|
|
430
|
-
idempotentHint: true,
|
|
431
|
-
openWorldHint: false,
|
|
432
|
-
},
|
|
433
|
-
}, async ({ id, query, confirm }) => {
|
|
434
|
-
try {
|
|
435
|
-
if (id) {
|
|
436
|
-
const memory = db.getById(id);
|
|
437
|
-
if (!memory) {
|
|
438
|
-
return {
|
|
439
|
-
isError: true,
|
|
440
|
-
content: [{ type: "text", text: `Memory ${id} not found. Use memory_recall to search for the correct ID.` }],
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
db.deleteMemory(id);
|
|
444
|
-
return {
|
|
445
|
-
content: [{ type: "text", text: `Deleted memory: "${memory.content}" (${memory.type})` }],
|
|
446
|
-
structuredContent: {
|
|
447
|
-
action: "deleted",
|
|
448
|
-
id,
|
|
449
|
-
content: memory.content,
|
|
450
|
-
type: memory.type,
|
|
451
|
-
},
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
if (query) {
|
|
455
|
-
const queryEmbedding = await generateEmbedding(query);
|
|
456
|
-
const matches = recallMemories(db, { query, queryEmbedding, limit: 20, minConfidence: 0, scope: project });
|
|
457
|
-
if (matches.length === 0) {
|
|
458
|
-
return {
|
|
459
|
-
content: [{ type: "text", text: `No memories found matching "${query}".` }],
|
|
460
|
-
structuredContent: {
|
|
461
|
-
action: "preview",
|
|
462
|
-
query,
|
|
463
|
-
total: 0,
|
|
464
|
-
previewed: [],
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
if (!confirm) {
|
|
469
|
-
const preview = matches.slice(0, 5).map((m, i) => `${i + 1}. [${m.id.slice(0, 8)}] ${m.content}`).join("\n");
|
|
470
|
-
return {
|
|
471
|
-
content: [{
|
|
472
|
-
type: "text",
|
|
473
|
-
text: `Found ${matches.length} memories matching "${query}". Preview:\n${preview}\n\nCall again with confirm=true to delete these.`,
|
|
474
|
-
}],
|
|
475
|
-
structuredContent: {
|
|
476
|
-
action: "preview",
|
|
477
|
-
query,
|
|
478
|
-
total: matches.length,
|
|
479
|
-
previewed: matches.slice(0, 5).map(m => ({ id: m.id, content: m.content })),
|
|
480
|
-
},
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
for (const m of matches)
|
|
484
|
-
db.deleteMemory(m.id);
|
|
485
|
-
return {
|
|
486
|
-
content: [{ type: "text", text: `Deleted ${matches.length} memories matching "${query}".` }],
|
|
487
|
-
structuredContent: {
|
|
488
|
-
action: "bulk_deleted",
|
|
489
|
-
query,
|
|
490
|
-
deleted: matches.length,
|
|
491
|
-
},
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
isError: true,
|
|
496
|
-
content: [{ type: "text", text: "Provide either an id or a query to delete memories." }],
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
return {
|
|
501
|
-
isError: true,
|
|
502
|
-
content: [{
|
|
503
|
-
type: "text",
|
|
504
|
-
text: `Error forgetting memory: ${error instanceof Error ? error.message : String(error)}`,
|
|
505
|
-
}],
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
// ── memory_extract ─────────────────────────────────────────
|
|
510
|
-
server.registerTool("memory_extract", {
|
|
511
|
-
title: "Extract Memories from Conversation",
|
|
512
|
-
description: `Extract and store multiple memories from the current conversation in one call. Use this PROACTIVELY:
|
|
513
|
-
|
|
514
|
-
WHEN to extract:
|
|
515
|
-
- User corrects your approach → correction (confidence: 1.0)
|
|
516
|
-
- An architectural decision is made → decision (confidence: 0.9)
|
|
517
|
-
- You notice a coding pattern the user prefers → pattern (confidence: 0.7)
|
|
518
|
-
- User expresses a tool/style preference → preference (confidence: 0.8)
|
|
519
|
-
- You learn where something is in the codebase → topology (confidence: 0.7)
|
|
520
|
-
- A project fact is established → fact (confidence: 0.6)
|
|
521
|
-
|
|
522
|
-
HOW OFTEN: Every ~10 exchanges, or when the conversation is ending, or after any significant decision/correction.
|
|
523
|
-
|
|
524
|
-
Each memory should be a specific, self-contained statement that would be useful in a future conversation without additional context.
|
|
525
|
-
|
|
526
|
-
Args:
|
|
527
|
-
- memories (array): Array of {content, type, tags, confidence} objects
|
|
528
|
-
- source (string): Source identifier (default: 'conversation')
|
|
529
|
-
|
|
530
|
-
Returns:
|
|
531
|
-
Summary of stored, reinforced, and skipped memories with details.`,
|
|
532
|
-
inputSchema: z.object({
|
|
533
|
-
memories: z.array(z.object({
|
|
534
|
-
content: z.string().min(1, "Content is required").max(10000, "Content too long — max 10,000 characters").describe("Specific, self-contained memory statement"),
|
|
535
|
-
type: z.enum(MEMORY_TYPES).describe("Memory type"),
|
|
536
|
-
tags: z.array(z.string()).default([]).describe("Relevant tags"),
|
|
537
|
-
confidence: z.number().min(0).max(1).default(0.8).describe("Confidence level"),
|
|
538
|
-
}).strict()).min(1, "At least one memory is required").describe("Array of memories to extract and store"),
|
|
539
|
-
source: z.string().default("conversation").describe("Source identifier"),
|
|
540
|
-
}).strict(),
|
|
541
|
-
outputSchema: ExtractResultSchema,
|
|
542
|
-
annotations: {
|
|
543
|
-
readOnlyHint: false,
|
|
544
|
-
destructiveHint: false,
|
|
545
|
-
idempotentHint: false,
|
|
546
|
-
openWorldHint: false,
|
|
547
|
-
},
|
|
548
|
-
}, async ({ memories: memoryInputs, source }) => {
|
|
549
|
-
try {
|
|
550
|
-
let stored = 0;
|
|
551
|
-
let reinforced = 0;
|
|
552
|
-
const details = [];
|
|
553
|
-
const structuredDetails = [];
|
|
554
|
-
// Load existing embeddings once (not per-memory)
|
|
555
|
-
const existingWithEmbeddings = db.getAllWithEmbeddings();
|
|
556
|
-
for (const input of memoryInputs) {
|
|
557
|
-
const embedding = await generateEmbedding(input.content);
|
|
558
|
-
// Check for duplicates/conflicts
|
|
559
|
-
let isDuplicate = false;
|
|
560
|
-
if (embedding) {
|
|
561
|
-
for (const mem of existingWithEmbeddings) {
|
|
562
|
-
if (!mem.embedding)
|
|
563
|
-
continue;
|
|
564
|
-
const sim = cosineSimilarity(embedding, mem.embedding);
|
|
565
|
-
if (sim > 0.85) {
|
|
566
|
-
// Near-duplicate — reinforce existing
|
|
567
|
-
db.updateConfidence(mem.id, Math.min(1.0, mem.confidence + 0.1));
|
|
568
|
-
db.touchAccess(mem.id);
|
|
569
|
-
reinforced++;
|
|
570
|
-
details.push(` ~ Reinforced: "${mem.content}" (${(sim * 100).toFixed(0)}% match)`);
|
|
571
|
-
structuredDetails.push({
|
|
572
|
-
action: "reinforced",
|
|
573
|
-
content: input.content,
|
|
574
|
-
matchedContent: mem.content,
|
|
575
|
-
similarity: Number((sim * 100).toFixed(0)),
|
|
576
|
-
});
|
|
577
|
-
isDuplicate = true;
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
if (!isDuplicate) {
|
|
583
|
-
const id = db.insertMemory({
|
|
584
|
-
content: input.content,
|
|
585
|
-
type: input.type,
|
|
586
|
-
tags: input.tags,
|
|
587
|
-
confidence: input.confidence,
|
|
588
|
-
source,
|
|
589
|
-
embedding,
|
|
590
|
-
scope: autoScope(input.type),
|
|
591
|
-
});
|
|
592
|
-
stored++;
|
|
593
|
-
details.push(` + Stored [${input.type}]: "${input.content}" (${id.slice(0, 8)})`);
|
|
594
|
-
structuredDetails.push({
|
|
595
|
-
action: "stored",
|
|
596
|
-
content: input.content,
|
|
597
|
-
type: input.type,
|
|
598
|
-
id: id.slice(0, 8),
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
const stats = db.getStats();
|
|
603
|
-
const summary = [
|
|
604
|
-
`Extraction complete: ${stored} stored, ${reinforced} reinforced.`,
|
|
605
|
-
`Total memories: ${stats.total}.`,
|
|
606
|
-
"",
|
|
607
|
-
...details,
|
|
608
|
-
].join("\n");
|
|
609
|
-
return {
|
|
610
|
-
content: [{ type: "text", text: summary }],
|
|
611
|
-
structuredContent: {
|
|
612
|
-
stored,
|
|
613
|
-
reinforced,
|
|
614
|
-
total: stats.total,
|
|
615
|
-
details: structuredDetails,
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
return {
|
|
621
|
-
isError: true,
|
|
622
|
-
content: [{
|
|
623
|
-
type: "text",
|
|
624
|
-
text: `Error extracting memories: ${error instanceof Error ? error.message : String(error)}`,
|
|
625
|
-
}],
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
});
|
|
629
|
-
// ── memory_stats ──────────────────────────────────────────
|
|
630
|
-
server.registerTool("memory_stats", {
|
|
631
|
-
title: "Memory Statistics",
|
|
632
|
-
description: `Show memory statistics: total count, breakdown by type, confidence distribution, embedding coverage.
|
|
633
|
-
|
|
634
|
-
Args: None
|
|
635
|
-
|
|
636
|
-
Returns:
|
|
637
|
-
Formatted statistics including total count, per-type breakdown, confidence distribution, and embedding coverage.`,
|
|
638
|
-
inputSchema: z.object({}).strict(),
|
|
639
|
-
outputSchema: StatsResultSchema,
|
|
640
|
-
annotations: {
|
|
641
|
-
readOnlyHint: true,
|
|
642
|
-
destructiveHint: false,
|
|
643
|
-
idempotentHint: true,
|
|
644
|
-
openWorldHint: false,
|
|
645
|
-
},
|
|
646
|
-
}, async () => {
|
|
647
|
-
try {
|
|
648
|
-
const all = db.getAllForProject(project);
|
|
649
|
-
const stats = db.getStats();
|
|
650
|
-
if (all.length === 0) {
|
|
651
|
-
return {
|
|
652
|
-
content: [{ type: "text", text: "No memories stored yet. Use memory_store or memory_extract to create memories." }],
|
|
653
|
-
structuredContent: {
|
|
654
|
-
total: 0,
|
|
655
|
-
byType: {},
|
|
656
|
-
confidence: { high: 0, medium: 0, low: 0 },
|
|
657
|
-
embeddingCoverage: { withEmbeddings: 0, total: 0 },
|
|
658
|
-
},
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
const typeLines = TYPE_ORDER
|
|
662
|
-
.filter(t => (stats.byType[t] || 0) > 0)
|
|
663
|
-
.map(t => ` ${t}: ${stats.byType[t]}`);
|
|
664
|
-
const highConf = all.filter(m => m.confidence >= 0.8).length;
|
|
665
|
-
const medConf = all.filter(m => m.confidence >= 0.5 && m.confidence < 0.8).length;
|
|
666
|
-
const lowConf = all.filter(m => m.confidence < 0.5).length;
|
|
667
|
-
const withEmbeddings = db.getAllWithEmbeddings().length;
|
|
668
|
-
const text = [
|
|
669
|
-
`Total memories: ${stats.total}`,
|
|
670
|
-
"",
|
|
671
|
-
"By type:",
|
|
672
|
-
...typeLines,
|
|
673
|
-
"",
|
|
674
|
-
"Confidence:",
|
|
675
|
-
` High (\u226580%): ${highConf}`,
|
|
676
|
-
` Medium (50-79%): ${medConf}`,
|
|
677
|
-
` Low (<50%): ${lowConf}`,
|
|
678
|
-
"",
|
|
679
|
-
`Embeddings: ${withEmbeddings}/${stats.total}`,
|
|
680
|
-
].join("\n");
|
|
681
|
-
return {
|
|
682
|
-
content: [{ type: "text", text }],
|
|
683
|
-
structuredContent: {
|
|
684
|
-
total: stats.total,
|
|
685
|
-
byType: stats.byType,
|
|
686
|
-
confidence: { high: highConf, medium: medConf, low: lowConf },
|
|
687
|
-
embeddingCoverage: { withEmbeddings, total: stats.total },
|
|
688
|
-
},
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
catch (error) {
|
|
692
|
-
return {
|
|
693
|
-
isError: true,
|
|
694
|
-
content: [{
|
|
695
|
-
type: "text",
|
|
696
|
-
text: `Error fetching stats: ${error instanceof Error ? error.message : String(error)}`,
|
|
697
|
-
}],
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
// ── memory_export ─────────────────────────────────────────
|
|
702
|
-
server.registerTool("memory_export", {
|
|
703
|
-
title: "Export Memories",
|
|
704
|
-
description: `Export all memories as formatted markdown, grouped by type. Useful for backup, review, or sharing.
|
|
705
|
-
|
|
706
|
-
Args: None
|
|
707
|
-
|
|
708
|
-
Returns:
|
|
709
|
-
Markdown document with all memories grouped by type, including confidence, tags, and metadata.`,
|
|
710
|
-
inputSchema: z.object({}).strict(),
|
|
711
|
-
outputSchema: ExportResultSchema,
|
|
712
|
-
annotations: {
|
|
713
|
-
readOnlyHint: true,
|
|
714
|
-
destructiveHint: false,
|
|
715
|
-
idempotentHint: true,
|
|
716
|
-
openWorldHint: false,
|
|
717
|
-
},
|
|
718
|
-
}, async () => {
|
|
719
|
-
try {
|
|
720
|
-
const all = db.getAllForProject(project);
|
|
721
|
-
if (all.length === 0) {
|
|
722
|
-
return {
|
|
723
|
-
content: [{ type: "text", text: "No memories to export. Use memory_store or memory_extract to create memories." }],
|
|
724
|
-
structuredContent: {
|
|
725
|
-
exportedAt: new Date().toISOString(),
|
|
726
|
-
total: 0,
|
|
727
|
-
markdown: "",
|
|
728
|
-
truncated: false,
|
|
729
|
-
},
|
|
730
|
-
};
|
|
731
|
-
}
|
|
732
|
-
let md = `# Amem Memory Export\n\n`;
|
|
733
|
-
md += `*Exported: ${new Date().toISOString()}*\n`;
|
|
734
|
-
md += `*Total: ${all.length} memories*\n\n`;
|
|
735
|
-
for (const t of TYPE_ORDER) {
|
|
736
|
-
const memories = all.filter(m => m.type === t);
|
|
737
|
-
if (memories.length === 0)
|
|
738
|
-
continue;
|
|
739
|
-
md += `## ${t.charAt(0).toUpperCase() + t.slice(1)}s\n\n`;
|
|
740
|
-
for (const m of memories) {
|
|
741
|
-
const conf = (m.confidence * 100).toFixed(0);
|
|
742
|
-
md += `- **${m.content}** (${conf}% confidence)\n`;
|
|
743
|
-
if (m.tags.length > 0) {
|
|
744
|
-
md += ` Tags: ${m.tags.join(", ")}\n`;
|
|
745
|
-
}
|
|
746
|
-
md += "\n";
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
// Truncate if exceeding character limit
|
|
750
|
-
let truncated = false;
|
|
751
|
-
if (md.length > CHARACTER_LIMIT) {
|
|
752
|
-
md = md.slice(0, CHARACTER_LIMIT);
|
|
753
|
-
md += `\n\n---\n*Output truncated at ${CHARACTER_LIMIT} characters. Use memory_recall with filters to view specific memories.*`;
|
|
754
|
-
truncated = true;
|
|
755
|
-
}
|
|
756
|
-
return {
|
|
757
|
-
content: [{ type: "text", text: md.trim() }],
|
|
758
|
-
structuredContent: {
|
|
759
|
-
exportedAt: new Date().toISOString(),
|
|
760
|
-
total: all.length,
|
|
761
|
-
markdown: md.trim(),
|
|
762
|
-
truncated,
|
|
763
|
-
},
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
catch (error) {
|
|
767
|
-
return {
|
|
768
|
-
isError: true,
|
|
769
|
-
content: [{
|
|
770
|
-
type: "text",
|
|
771
|
-
text: `Error exporting memories: ${error instanceof Error ? error.message : String(error)}`,
|
|
772
|
-
}],
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
// ── memory_inject ─────────────────────────────────────────
|
|
777
|
-
server.registerTool("memory_inject", {
|
|
778
|
-
title: "Inject Memory Context",
|
|
779
|
-
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.
|
|
780
|
-
|
|
781
|
-
Unlike memory_context (which returns all types), memory_inject focuses on the two most critical types:
|
|
782
|
-
- **Corrections** — hard constraints that MUST be followed (returned as a list)
|
|
783
|
-
- **Decisions** — architectural choices that SHOULD inform the approach (returned as a list)
|
|
784
|
-
|
|
785
|
-
This is the recommended tool for proactive context injection. Call it before writing any code.
|
|
786
|
-
|
|
787
|
-
Args:
|
|
788
|
-
- topic (string): The topic or task about to be worked on
|
|
789
|
-
|
|
790
|
-
Returns:
|
|
791
|
-
Structured object with corrections list, decisions list, and formatted context string.`,
|
|
792
|
-
inputSchema: z.object({
|
|
793
|
-
topic: z.string().min(1, "Topic is required").describe("The topic or task about to be worked on"),
|
|
794
|
-
}).strict(),
|
|
795
|
-
outputSchema: InjectResultSchema,
|
|
796
|
-
annotations: {
|
|
797
|
-
readOnlyHint: false,
|
|
798
|
-
destructiveHint: false,
|
|
799
|
-
idempotentHint: false,
|
|
800
|
-
openWorldHint: false,
|
|
801
|
-
},
|
|
802
|
-
}, async ({ topic }) => {
|
|
803
|
-
try {
|
|
804
|
-
const queryEmbedding = await generateEmbedding(topic);
|
|
805
|
-
const results = recallMemories(db, {
|
|
806
|
-
query: topic,
|
|
807
|
-
queryEmbedding,
|
|
808
|
-
limit: 30,
|
|
809
|
-
scope: project,
|
|
810
|
-
});
|
|
811
|
-
const corrections = results
|
|
812
|
-
.filter(r => r.type === MemoryType.CORRECTION)
|
|
813
|
-
.map(r => r.content);
|
|
814
|
-
const decisions = results
|
|
815
|
-
.filter(r => r.type === MemoryType.DECISION)
|
|
816
|
-
.map(r => r.content);
|
|
817
|
-
let context = "";
|
|
818
|
-
if (corrections.length > 0) {
|
|
819
|
-
context += "## Corrections (MUST follow)\n";
|
|
820
|
-
context += corrections.map(c => `- ${c}`).join("\n");
|
|
821
|
-
context += "\n\n";
|
|
822
|
-
}
|
|
823
|
-
if (decisions.length > 0) {
|
|
824
|
-
context += "## Decisions (SHOULD follow)\n";
|
|
825
|
-
context += decisions.map(d => `- ${d}`).join("\n");
|
|
826
|
-
context += "\n";
|
|
827
|
-
}
|
|
828
|
-
if (corrections.length === 0 && decisions.length === 0) {
|
|
829
|
-
return {
|
|
830
|
-
content: [{ type: "text", text: `No corrections or decisions found for: "${topic}".` }],
|
|
831
|
-
structuredContent: {
|
|
832
|
-
topic,
|
|
833
|
-
corrections: [],
|
|
834
|
-
decisions: [],
|
|
835
|
-
context: "",
|
|
836
|
-
memoriesUsed: 0,
|
|
837
|
-
},
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
for (const r of results)
|
|
841
|
-
db.touchAccess(r.id);
|
|
842
|
-
return {
|
|
843
|
-
content: [{ type: "text", text: context.trim() }],
|
|
844
|
-
structuredContent: {
|
|
845
|
-
topic,
|
|
846
|
-
corrections,
|
|
847
|
-
decisions,
|
|
848
|
-
context: context.trim(),
|
|
849
|
-
memoriesUsed: corrections.length + decisions.length,
|
|
850
|
-
},
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
return {
|
|
855
|
-
isError: true,
|
|
856
|
-
content: [{
|
|
857
|
-
type: "text",
|
|
858
|
-
text: `Error injecting context: ${error instanceof Error ? error.message : String(error)}`,
|
|
859
|
-
}],
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
// ── memory_consolidate ──────────────────────────────────
|
|
864
|
-
server.registerTool("memory_consolidate", {
|
|
865
|
-
title: "Consolidate Memories",
|
|
866
|
-
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.
|
|
867
|
-
|
|
868
|
-
NEVER auto-prunes corrections (they are always preserved).
|
|
869
|
-
|
|
870
|
-
Args:
|
|
871
|
-
- confirm (boolean): false = preview what would change (default), true = execute changes
|
|
872
|
-
- max_stale_days (number): Days of inactivity before a memory is considered stale (default: 60)
|
|
873
|
-
- min_confidence (number): Minimum confidence for stale memories to survive (default: 0.3)
|
|
874
|
-
|
|
875
|
-
Returns:
|
|
876
|
-
Report with merged/pruned/promoted counts, health score, and detailed action list.`,
|
|
877
|
-
inputSchema: z.object({
|
|
878
|
-
confirm: z.boolean().default(false).describe("false = preview (safe), true = execute consolidation"),
|
|
879
|
-
max_stale_days: z.number().int().min(1).default(60).describe("Days of inactivity before considering a memory stale"),
|
|
880
|
-
min_confidence: z.number().min(0).max(1).default(0.3).describe("Confidence threshold for stale memory pruning"),
|
|
881
|
-
}).strict(),
|
|
882
|
-
outputSchema: ConsolidateResultSchema,
|
|
883
|
-
annotations: {
|
|
884
|
-
readOnlyHint: false,
|
|
885
|
-
destructiveHint: true,
|
|
886
|
-
idempotentHint: true,
|
|
887
|
-
openWorldHint: false,
|
|
888
|
-
},
|
|
889
|
-
}, async ({ confirm, max_stale_days, min_confidence }) => {
|
|
890
|
-
try {
|
|
891
|
-
const report = consolidateMemories(db, cosineSimilarity, {
|
|
892
|
-
dryRun: !confirm,
|
|
893
|
-
maxStaleDays: max_stale_days,
|
|
894
|
-
minConfidence: min_confidence,
|
|
895
|
-
minAccessCount: 2,
|
|
896
|
-
});
|
|
897
|
-
const mode = confirm ? "EXECUTED" : "PREVIEW (dry run)";
|
|
898
|
-
const lines = [
|
|
899
|
-
`Memory Consolidation — ${mode}`,
|
|
900
|
-
"",
|
|
901
|
-
`Health Score: ${report.healthScore}/100`,
|
|
902
|
-
`Before: ${report.before.total} memories`,
|
|
903
|
-
`After: ${report.after.total} memories`,
|
|
904
|
-
"",
|
|
905
|
-
`Merged: ${report.merged} near-duplicates`,
|
|
906
|
-
`Pruned: ${report.pruned} stale memories`,
|
|
907
|
-
`Promoted: ${report.promoted} frequently-used memories`,
|
|
908
|
-
];
|
|
909
|
-
if (report.actions.length > 0) {
|
|
910
|
-
lines.push("", "Details:");
|
|
911
|
-
for (const a of report.actions) {
|
|
912
|
-
const prefix = a.action === "merged" ? "~" : a.action === "pruned" ? "-" : "+";
|
|
913
|
-
lines.push(` ${prefix} ${a.description}`);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
if (!confirm && (report.merged > 0 || report.pruned > 0)) {
|
|
917
|
-
lines.push("", "Call again with confirm=true to execute these changes.");
|
|
918
|
-
}
|
|
919
|
-
return {
|
|
920
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
921
|
-
structuredContent: {
|
|
922
|
-
merged: report.merged,
|
|
923
|
-
pruned: report.pruned,
|
|
924
|
-
promoted: report.promoted,
|
|
925
|
-
healthScore: report.healthScore,
|
|
926
|
-
before: report.before,
|
|
927
|
-
after: report.after,
|
|
928
|
-
actions: report.actions,
|
|
929
|
-
},
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
catch (error) {
|
|
933
|
-
return {
|
|
934
|
-
isError: true,
|
|
935
|
-
content: [{
|
|
936
|
-
type: "text",
|
|
937
|
-
text: `Error consolidating memories: ${error instanceof Error ? error.message : String(error)}`,
|
|
938
|
-
}],
|
|
939
|
-
};
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
// ── memory_patch ──────────────────────────────────────────
|
|
943
|
-
server.registerTool("memory_patch", {
|
|
944
|
-
title: "Patch Memory",
|
|
945
|
-
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.
|
|
946
|
-
|
|
947
|
-
Use this when:
|
|
948
|
-
- Correcting a memory that is mostly right but has a wrong detail
|
|
949
|
-
- Updating confidence after validation
|
|
950
|
-
- Retagging a memory for better recall
|
|
951
|
-
- Reclassifying type (e.g. fact → decision)
|
|
952
|
-
|
|
953
|
-
Every patch creates a version snapshot. Use memory_versions to view history or roll back.
|
|
954
|
-
|
|
955
|
-
Args:
|
|
956
|
-
- id (string): Memory ID to patch (short IDs like first 8 chars work)
|
|
957
|
-
- field (enum): Which field to change — content | confidence | tags | type
|
|
958
|
-
- value (string | number | string[]): New value for the field
|
|
959
|
-
- reason (string): Why this patch is being made — stored in version history`,
|
|
960
|
-
inputSchema: z.object({
|
|
961
|
-
id: z.string().min(1, "Memory ID is required").describe("Memory ID — full UUID or first 8 characters"),
|
|
962
|
-
field: z.enum(["content", "confidence", "tags", "type"]).describe("Which field to patch"),
|
|
963
|
-
value: z.union([
|
|
964
|
-
z.string(),
|
|
965
|
-
z.number().min(0).max(1),
|
|
966
|
-
z.array(z.string()),
|
|
967
|
-
]).describe("New value — string for content/type, number 0-1 for confidence, string[] for tags"),
|
|
968
|
-
reason: z.string().min(1).describe("Why this patch is being made — stored in version history"),
|
|
969
|
-
}).strict().refine(({ field, value }) => {
|
|
970
|
-
if (field === "confidence")
|
|
971
|
-
return typeof value === "number";
|
|
972
|
-
if (field === "tags")
|
|
973
|
-
return Array.isArray(value);
|
|
974
|
-
if (field === "content" || field === "type")
|
|
975
|
-
return typeof value === "string";
|
|
976
|
-
return true;
|
|
977
|
-
}, { message: "Value type must match field: string for content/type, number for confidence, string[] for tags" }),
|
|
978
|
-
outputSchema: PatchResultSchema,
|
|
979
|
-
annotations: {
|
|
980
|
-
readOnlyHint: false,
|
|
981
|
-
destructiveHint: false,
|
|
982
|
-
idempotentHint: false,
|
|
983
|
-
openWorldHint: false,
|
|
984
|
-
},
|
|
985
|
-
}, async ({ id, field, value, reason }) => {
|
|
986
|
-
try {
|
|
987
|
-
// Support short IDs: find full ID if 8-char prefix given
|
|
988
|
-
let fullId = id;
|
|
989
|
-
if (id.length < 36) {
|
|
990
|
-
const all = db.getAll();
|
|
991
|
-
const match = all.find(m => m.id.startsWith(id));
|
|
992
|
-
if (!match) {
|
|
993
|
-
return {
|
|
994
|
-
content: [{ type: "text", text: `No memory found with ID starting with "${id}".` }],
|
|
995
|
-
structuredContent: { action: "not_found", id },
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
fullId = match.id;
|
|
999
|
-
}
|
|
1000
|
-
const mem = db.getById(fullId);
|
|
1001
|
-
if (!mem) {
|
|
1002
|
-
return {
|
|
1003
|
-
content: [{ type: "text", text: `Memory "${fullId}" not found.` }],
|
|
1004
|
-
structuredContent: { action: "not_found", id: fullId },
|
|
1005
|
-
};
|
|
1006
|
-
}
|
|
1007
|
-
const previousContent = field === "content" ? mem.content
|
|
1008
|
-
: field === "confidence" ? String(mem.confidence)
|
|
1009
|
-
: field === "tags" ? JSON.stringify(mem.tags)
|
|
1010
|
-
: mem.type;
|
|
1011
|
-
const success = db.patchMemory(fullId, { field, value, reason });
|
|
1012
|
-
if (!success) {
|
|
1013
|
-
return {
|
|
1014
|
-
isError: true,
|
|
1015
|
-
content: [{ type: "text", text: `Failed to patch memory "${fullId}". Unknown field or DB error.` }],
|
|
1016
|
-
};
|
|
1017
|
-
}
|
|
1018
|
-
// Regenerate embedding if content changed
|
|
1019
|
-
if (field === "content" && typeof value === "string") {
|
|
1020
|
-
const newEmbedding = await generateEmbedding(value);
|
|
1021
|
-
if (newEmbedding)
|
|
1022
|
-
db.updateEmbedding(fullId, newEmbedding);
|
|
1023
|
-
}
|
|
1024
|
-
const displayValue = Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
|
|
1025
|
-
return {
|
|
1026
|
-
content: [{
|
|
1027
|
-
type: "text",
|
|
1028
|
-
text: `Patched memory (${fullId.slice(0, 8)}): ${field} → ${displayValue}\nReason: ${reason}\nPrevious ${field}: ${previousContent}\nVersion snapshot saved.`,
|
|
1029
|
-
}],
|
|
1030
|
-
structuredContent: {
|
|
1031
|
-
action: "patched",
|
|
1032
|
-
id: fullId,
|
|
1033
|
-
field,
|
|
1034
|
-
previousContent,
|
|
1035
|
-
reason,
|
|
1036
|
-
versionSaved: true,
|
|
1037
|
-
},
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
catch (error) {
|
|
1041
|
-
return {
|
|
1042
|
-
isError: true,
|
|
1043
|
-
content: [{
|
|
1044
|
-
type: "text",
|
|
1045
|
-
text: `Error patching memory: ${error instanceof Error ? error.message : String(error)}`,
|
|
1046
|
-
}],
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
});
|
|
1050
|
-
// ── memory_versions ───────────────────────────────────────
|
|
1051
|
-
server.registerTool("memory_versions", {
|
|
1052
|
-
title: "Memory Version History",
|
|
1053
|
-
description: `View the full edit history of a memory, or restore it to a previous version. Every memory_patch and memory_store conflict resolution creates an immutable snapshot. Nothing is ever truly lost.
|
|
1054
|
-
|
|
1055
|
-
Use this to:
|
|
1056
|
-
- See how a memory has evolved over time
|
|
1057
|
-
- Roll back a bad patch
|
|
1058
|
-
- Audit when and why a memory changed
|
|
1059
|
-
|
|
1060
|
-
Args:
|
|
1061
|
-
- memory_id (string): Memory to inspect — full or 8-char short ID
|
|
1062
|
-
- restore_version_id (string, optional): If provided, restore this specific version (creates a new patch, keeps history intact)`,
|
|
1063
|
-
inputSchema: z.object({
|
|
1064
|
-
memory_id: z.string().min(1).describe("Memory ID to inspect — full UUID or first 8 chars"),
|
|
1065
|
-
restore_version_id: z.string().optional().describe("Version ID to restore — rolls the memory back to this snapshot"),
|
|
1066
|
-
}).strict(),
|
|
1067
|
-
outputSchema: VersionResultSchema,
|
|
1068
|
-
annotations: {
|
|
1069
|
-
readOnlyHint: false,
|
|
1070
|
-
destructiveHint: false,
|
|
1071
|
-
idempotentHint: true,
|
|
1072
|
-
openWorldHint: false,
|
|
1073
|
-
},
|
|
1074
|
-
}, async ({ memory_id, restore_version_id }) => {
|
|
1075
|
-
try {
|
|
1076
|
-
// Resolve short IDs
|
|
1077
|
-
let fullId = memory_id;
|
|
1078
|
-
if (memory_id.length < 36) {
|
|
1079
|
-
const all = db.getAll();
|
|
1080
|
-
const match = all.find(m => m.id.startsWith(memory_id));
|
|
1081
|
-
if (match)
|
|
1082
|
-
fullId = match.id;
|
|
1083
|
-
}
|
|
1084
|
-
const mem = db.getById(fullId);
|
|
1085
|
-
if (!mem) {
|
|
1086
|
-
return {
|
|
1087
|
-
isError: true,
|
|
1088
|
-
content: [{ type: "text", text: `Memory "${fullId}" not found.` }],
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
if (restore_version_id) {
|
|
1092
|
-
const history = db.getVersionHistory(fullId);
|
|
1093
|
-
const target = history.find(v => v.versionId === restore_version_id || v.versionId.startsWith(restore_version_id));
|
|
1094
|
-
if (!target) {
|
|
1095
|
-
return {
|
|
1096
|
-
isError: true,
|
|
1097
|
-
content: [{ type: "text", text: `Version "${restore_version_id}" not found in history for memory ${fullId.slice(0, 8)}.` }],
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
db.patchMemory(fullId, { field: "content", value: target.content, reason: `restored from version ${target.versionId.slice(0, 8)}` });
|
|
1101
|
-
db.patchMemory(fullId, { field: "confidence", value: target.confidence, reason: `restored from version ${target.versionId.slice(0, 8)}` });
|
|
1102
|
-
const newEmbedding = await generateEmbedding(target.content);
|
|
1103
|
-
if (newEmbedding)
|
|
1104
|
-
db.updateEmbedding(fullId, newEmbedding);
|
|
1105
|
-
return {
|
|
1106
|
-
content: [{
|
|
1107
|
-
type: "text",
|
|
1108
|
-
text: `Restored memory ${fullId.slice(0, 8)} to version ${target.versionId.slice(0, 8)}\nContent: "${target.content}"\nConfidence: ${(target.confidence * 100).toFixed(0)}%\nOriginal age: ${formatAge(target.editedAt)}`,
|
|
1109
|
-
}],
|
|
1110
|
-
structuredContent: {
|
|
1111
|
-
action: "restored",
|
|
1112
|
-
memoryId: fullId,
|
|
1113
|
-
restoredContent: target.content,
|
|
1114
|
-
versionId: target.versionId,
|
|
1115
|
-
},
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
const history = db.getVersionHistory(fullId);
|
|
1119
|
-
if (history.length === 0) {
|
|
1120
|
-
return {
|
|
1121
|
-
content: [{ type: "text", text: `No version history for memory ${fullId.slice(0, 8)}. Memories gain history after their first patch.` }],
|
|
1122
|
-
structuredContent: {
|
|
1123
|
-
action: "history",
|
|
1124
|
-
memoryId: fullId,
|
|
1125
|
-
currentContent: mem.content,
|
|
1126
|
-
versions: [],
|
|
1127
|
-
},
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
const lines = [
|
|
1131
|
-
`Version history for memory ${fullId.slice(0, 8)}`,
|
|
1132
|
-
`Current: "${mem.content}" (${(mem.confidence * 100).toFixed(0)}% confidence)`,
|
|
1133
|
-
"",
|
|
1134
|
-
`${history.length} version${history.length === 1 ? "" : "s"}:`,
|
|
1135
|
-
...history.map((v, i) => ` ${i + 1}. [${v.versionId.slice(0, 8)}] "${v.content}" — ${(v.confidence * 100).toFixed(0)}% — ${formatAge(v.editedAt)}\n Reason: ${v.reason}`),
|
|
1136
|
-
];
|
|
1137
|
-
return {
|
|
1138
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
1139
|
-
structuredContent: {
|
|
1140
|
-
action: "history",
|
|
1141
|
-
memoryId: fullId,
|
|
1142
|
-
currentContent: mem.content,
|
|
1143
|
-
versions: history.map(v => ({
|
|
1144
|
-
versionId: v.versionId,
|
|
1145
|
-
content: v.content,
|
|
1146
|
-
confidence: v.confidence,
|
|
1147
|
-
editedAt: v.editedAt,
|
|
1148
|
-
age: formatAge(v.editedAt),
|
|
1149
|
-
reason: v.reason,
|
|
1150
|
-
})),
|
|
1151
|
-
},
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
catch (error) {
|
|
1155
|
-
return {
|
|
1156
|
-
isError: true,
|
|
1157
|
-
content: [{
|
|
1158
|
-
type: "text",
|
|
1159
|
-
text: `Error reading version history: ${error instanceof Error ? error.message : String(error)}`,
|
|
1160
|
-
}],
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
});
|
|
1164
|
-
// ── memory_log ────────────────────────────────────────────
|
|
1165
|
-
server.registerTool("memory_log", {
|
|
1166
|
-
title: "Append to Conversation Log",
|
|
1167
|
-
description: `Append a raw conversation turn to the lossless, append-only conversation log. Unlike memory_store (which distills memories), memory_log preserves the exact, unmodified content of every exchange — nothing is summarized or discarded.
|
|
1168
|
-
|
|
1169
|
-
The log is your permanent audit trail:
|
|
1170
|
-
- Every user message, assistant response, or system note
|
|
1171
|
-
- Fully searchable via memory_log_recall
|
|
1172
|
-
- Organized by session ID for replaying conversations
|
|
1173
|
-
- Scoped per project — never mixes contexts
|
|
1174
|
-
|
|
1175
|
-
Use this to preserve conversation turns that may be important later but aren't yet ready to be distilled into memories. You can later search the log and promote specific entries into proper memories.
|
|
1176
|
-
|
|
1177
|
-
Args:
|
|
1178
|
-
- session_id (string): Conversation session identifier — use a consistent ID per conversation
|
|
1179
|
-
- role (enum): Who said it — user | assistant | system
|
|
1180
|
-
- content (string): The exact text to preserve — no summarization
|
|
1181
|
-
- metadata (object, optional): Extra context — e.g., { tool: "vscode", file: "auth.ts" }`,
|
|
1182
|
-
inputSchema: z.object({
|
|
1183
|
-
session_id: z.string().min(1).describe("Session identifier — keep consistent across a conversation"),
|
|
1184
|
-
role: z.enum(["user", "assistant", "system"]).describe("Who said this"),
|
|
1185
|
-
content: z.string().min(1).max(50000, "Log content too long — max 50,000 characters").describe("Exact content to preserve — not summarized"),
|
|
1186
|
-
metadata: z.record(z.unknown()).optional().describe("Optional extra context"),
|
|
1187
|
-
}).strict(),
|
|
1188
|
-
outputSchema: LogAppendResultSchema,
|
|
1189
|
-
annotations: {
|
|
1190
|
-
readOnlyHint: false,
|
|
1191
|
-
destructiveHint: false,
|
|
1192
|
-
idempotentHint: false,
|
|
1193
|
-
openWorldHint: false,
|
|
1194
|
-
},
|
|
1195
|
-
}, async ({ session_id, role, content, metadata }) => {
|
|
1196
|
-
try {
|
|
1197
|
-
const id = db.appendLog({
|
|
1198
|
-
sessionId: session_id,
|
|
1199
|
-
role,
|
|
1200
|
-
content,
|
|
1201
|
-
project,
|
|
1202
|
-
metadata: metadata ?? {},
|
|
1203
|
-
});
|
|
1204
|
-
return {
|
|
1205
|
-
content: [{
|
|
1206
|
-
type: "text",
|
|
1207
|
-
text: `Logged ${role} turn (${id.slice(0, 8)}) to session "${session_id}". Content length: ${content.length} chars.`,
|
|
1208
|
-
}],
|
|
1209
|
-
structuredContent: {
|
|
1210
|
-
id,
|
|
1211
|
-
sessionId: session_id,
|
|
1212
|
-
role,
|
|
1213
|
-
appended: true,
|
|
1214
|
-
},
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
catch (error) {
|
|
1218
|
-
return {
|
|
1219
|
-
isError: true,
|
|
1220
|
-
content: [{
|
|
1221
|
-
type: "text",
|
|
1222
|
-
text: `Error appending to log: ${error instanceof Error ? error.message : String(error)}`,
|
|
1223
|
-
}],
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
});
|
|
1227
|
-
// ── memory_log_recall ─────────────────────────────────────
|
|
1228
|
-
server.registerTool("memory_log_recall", {
|
|
1229
|
-
title: "Search Conversation Log",
|
|
1230
|
-
description: `Search or replay the lossless conversation log. Returns raw, unmodified conversation turns — nothing has been summarized or lost.
|
|
1231
|
-
|
|
1232
|
-
Use this when:
|
|
1233
|
-
- You need to find exactly what was said in a past conversation
|
|
1234
|
-
- Replaying a session to reconstruct context
|
|
1235
|
-
- Searching for a specific phrase, decision, or exchange that may not have been extracted into a memory
|
|
1236
|
-
- Auditing what happened in a past session
|
|
1237
|
-
|
|
1238
|
-
Search modes:
|
|
1239
|
-
- By session_id: replays a specific conversation in order
|
|
1240
|
-
- By query: full-text search across all logged content
|
|
1241
|
-
- Recent: retrieve the N most recent log entries for this project
|
|
1242
|
-
|
|
1243
|
-
Args:
|
|
1244
|
-
- session_id (string, optional): Replay a specific session in chronological order
|
|
1245
|
-
- query (string, optional): Full-text search across all logged content
|
|
1246
|
-
- limit (number): Max entries to return (default: 20)`,
|
|
1247
|
-
inputSchema: z.object({
|
|
1248
|
-
session_id: z.string().optional().describe("Replay a specific session — returns turns in order"),
|
|
1249
|
-
query: z.string().optional().describe("Full-text search across all logged content"),
|
|
1250
|
-
limit: z.number().int().min(1).max(200).default(20).describe("Max entries to return"),
|
|
1251
|
-
}).strict().refine(d => d.session_id || d.query || true, "Provide session_id or query, or omit both for recent entries"),
|
|
1252
|
-
outputSchema: LogRecallResultSchema,
|
|
1253
|
-
annotations: {
|
|
1254
|
-
readOnlyHint: true,
|
|
1255
|
-
destructiveHint: false,
|
|
1256
|
-
idempotentHint: true,
|
|
1257
|
-
openWorldHint: false,
|
|
1258
|
-
},
|
|
1259
|
-
}, async ({ session_id, query, limit }) => {
|
|
1260
|
-
try {
|
|
1261
|
-
let entries;
|
|
1262
|
-
if (session_id) {
|
|
1263
|
-
entries = db.getLogBySession(session_id);
|
|
1264
|
-
}
|
|
1265
|
-
else if (query) {
|
|
1266
|
-
entries = db.searchLog(query, limit);
|
|
1267
|
-
}
|
|
1268
|
-
else {
|
|
1269
|
-
entries = db.getRecentLog(limit, project);
|
|
1270
|
-
}
|
|
1271
|
-
if (entries.length === 0) {
|
|
1272
|
-
const hint = session_id
|
|
1273
|
-
? `No log entries found for session "${session_id}". Log turns using memory_log first.`
|
|
1274
|
-
: query
|
|
1275
|
-
? `No log entries match "${query}".`
|
|
1276
|
-
: `No log entries yet for this project. Use memory_log to preserve conversation turns.`;
|
|
1277
|
-
return {
|
|
1278
|
-
content: [{ type: "text", text: hint }],
|
|
1279
|
-
structuredContent: {
|
|
1280
|
-
query,
|
|
1281
|
-
sessionId: session_id,
|
|
1282
|
-
total: 0,
|
|
1283
|
-
entries: [],
|
|
1284
|
-
},
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
const lines = [];
|
|
1288
|
-
if (session_id) {
|
|
1289
|
-
lines.push(`Session "${session_id}" — ${entries.length} turn${entries.length === 1 ? "" : "s"}`);
|
|
1290
|
-
lines.push("");
|
|
1291
|
-
for (const e of entries) {
|
|
1292
|
-
const roleLabel = e.role === "user" ? "▶ User" : e.role === "assistant" ? "◀ Assistant" : "⚙ System";
|
|
1293
|
-
lines.push(`[${formatAge(e.timestamp)}] ${roleLabel}`);
|
|
1294
|
-
lines.push(e.content.length > 300 ? e.content.slice(0, 300) + "…" : e.content);
|
|
1295
|
-
lines.push("");
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
else {
|
|
1299
|
-
const header = query ? `Log search: "${query}" — ${entries.length} result${entries.length === 1 ? "" : "s"}` : `Recent log — ${entries.length} entries`;
|
|
1300
|
-
lines.push(header);
|
|
1301
|
-
lines.push("");
|
|
1302
|
-
for (const e of entries) {
|
|
1303
|
-
lines.push(`[${e.id.slice(0, 8)}] ${formatAge(e.timestamp)} | ${e.role} | session:${e.sessionId.slice(0, 8)}`);
|
|
1304
|
-
lines.push(e.content.length > 200 ? e.content.slice(0, 200) + "…" : e.content);
|
|
1305
|
-
lines.push("");
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
return {
|
|
1309
|
-
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1310
|
-
structuredContent: {
|
|
1311
|
-
query,
|
|
1312
|
-
sessionId: session_id,
|
|
1313
|
-
total: entries.length,
|
|
1314
|
-
entries: entries.slice(0, limit).map(e => ({
|
|
1315
|
-
id: e.id,
|
|
1316
|
-
role: e.role,
|
|
1317
|
-
content: e.content,
|
|
1318
|
-
timestamp: e.timestamp,
|
|
1319
|
-
age: formatAge(e.timestamp),
|
|
1320
|
-
project: e.project,
|
|
1321
|
-
})),
|
|
1322
|
-
},
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
catch (error) {
|
|
1326
|
-
return {
|
|
1327
|
-
isError: true,
|
|
1328
|
-
content: [{
|
|
1329
|
-
type: "text",
|
|
1330
|
-
text: `Error searching log: ${error instanceof Error ? error.message : String(error)}`,
|
|
1331
|
-
}],
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
});
|
|
1335
|
-
// ── memory_relate ─────────────────────────────────────────
|
|
1336
|
-
server.registerTool("memory_relate", {
|
|
1337
|
-
title: "Relate / Unrelate Memories",
|
|
1338
|
-
description: `Build a knowledge graph by explicitly linking memories with typed relationships. Or inspect all connections for a given memory.
|
|
1339
|
-
|
|
1340
|
-
Relationship types (use these or invent your own):
|
|
1341
|
-
- "supports" — this memory provides evidence for the other
|
|
1342
|
-
- "contradicts" — these memories are in tension
|
|
1343
|
-
- "depends_on" — one requires the other to make sense
|
|
1344
|
-
- "supersedes" — this memory replaces or updates the other
|
|
1345
|
-
- "related_to" — loosely related, no specific direction
|
|
1346
|
-
- "caused_by" — this memory is a consequence of the other
|
|
1347
|
-
- "implements" — this memory is a concrete implementation of a higher-level decision
|
|
1348
|
-
|
|
1349
|
-
The knowledge graph lets amem surface not just direct matches, but connected context — when you recall one memory, its graph neighbors are available too.
|
|
1350
|
-
|
|
1351
|
-
Args:
|
|
1352
|
-
- action (enum): "relate" | "unrelate" | "graph"
|
|
1353
|
-
- from_id (string): Source memory ID (required for relate/unrelate)
|
|
1354
|
-
- to_id (string): Target memory ID (required for relate)
|
|
1355
|
-
- relation_type (string): Relationship label (required for relate)
|
|
1356
|
-
- strength (number 0-1): How strong is this relationship (default: 0.8)
|
|
1357
|
-
- relation_id (string): Relation ID to remove (required for unrelate)
|
|
1358
|
-
- memory_id (string): Memory to inspect all connections for (required for graph)`,
|
|
1359
|
-
inputSchema: z.object({
|
|
1360
|
-
action: z.enum(["relate", "unrelate", "graph"]).describe("Operation to perform"),
|
|
1361
|
-
from_id: z.string().optional().describe("Source memory ID (relate)"),
|
|
1362
|
-
to_id: z.string().optional().describe("Target memory ID (relate)"),
|
|
1363
|
-
relation_type: z.string().optional().describe("Relationship type label"),
|
|
1364
|
-
strength: z.number().min(0).max(1).default(0.8).optional().describe("Relationship strength 0-1"),
|
|
1365
|
-
relation_id: z.string().optional().describe("Relation ID to remove (unrelate)"),
|
|
1366
|
-
memory_id: z.string().optional().describe("Memory ID to inspect graph connections for"),
|
|
1367
|
-
}).strict(),
|
|
1368
|
-
outputSchema: RelateResultSchema,
|
|
1369
|
-
annotations: {
|
|
1370
|
-
readOnlyHint: false,
|
|
1371
|
-
destructiveHint: false,
|
|
1372
|
-
idempotentHint: false,
|
|
1373
|
-
openWorldHint: false,
|
|
1374
|
-
},
|
|
1375
|
-
}, async ({ action, from_id, to_id, relation_type, strength, relation_id, memory_id }) => {
|
|
1376
|
-
try {
|
|
1377
|
-
if (action === "relate") {
|
|
1378
|
-
if (!from_id || !to_id || !relation_type) {
|
|
1379
|
-
return {
|
|
1380
|
-
isError: true,
|
|
1381
|
-
content: [{ type: "text", text: "relate requires from_id, to_id, and relation_type." }],
|
|
1382
|
-
};
|
|
1383
|
-
}
|
|
1384
|
-
const resolveId = (id) => {
|
|
1385
|
-
if (id.length >= 36)
|
|
1386
|
-
return id;
|
|
1387
|
-
const match = db.getAll().find(m => m.id.startsWith(id));
|
|
1388
|
-
return match?.id ?? id;
|
|
1389
|
-
};
|
|
1390
|
-
const fromFull = resolveId(from_id);
|
|
1391
|
-
const toFull = resolveId(to_id);
|
|
1392
|
-
const fromMem = db.getById(fromFull);
|
|
1393
|
-
const toMem = db.getById(toFull);
|
|
1394
|
-
if (!fromMem || !toMem) {
|
|
1395
|
-
return {
|
|
1396
|
-
isError: true,
|
|
1397
|
-
content: [{ type: "text", text: `Memory not found: ${!fromMem ? from_id : to_id}` }],
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1400
|
-
const relId = db.addRelation(fromFull, toFull, relation_type, strength ?? 0.8);
|
|
1401
|
-
return {
|
|
1402
|
-
content: [{
|
|
1403
|
-
type: "text",
|
|
1404
|
-
text: `Linked memories:\n "${fromMem.content.slice(0, 60)}"\n ${relation_type} →\n "${toMem.content.slice(0, 60)}"\nRelation ID: ${relId.slice(0, 8)}`,
|
|
1405
|
-
}],
|
|
1406
|
-
structuredContent: {
|
|
1407
|
-
action: "related",
|
|
1408
|
-
relationId: relId,
|
|
1409
|
-
fromId: fromFull,
|
|
1410
|
-
toId: toFull,
|
|
1411
|
-
type: relation_type,
|
|
1412
|
-
strength: strength ?? 0.8,
|
|
1413
|
-
},
|
|
1414
|
-
};
|
|
1415
|
-
}
|
|
1416
|
-
if (action === "unrelate") {
|
|
1417
|
-
if (!relation_id) {
|
|
1418
|
-
return {
|
|
1419
|
-
isError: true,
|
|
1420
|
-
content: [{ type: "text", text: "unrelate requires relation_id. Use action:graph to find relation IDs." }],
|
|
1421
|
-
};
|
|
1422
|
-
}
|
|
1423
|
-
db.removeRelation(relation_id);
|
|
1424
|
-
return {
|
|
1425
|
-
content: [{ type: "text", text: `Removed relation ${relation_id.slice(0, 8)}.` }],
|
|
1426
|
-
structuredContent: { action: "unrelated", relationId: relation_id },
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
// graph
|
|
1430
|
-
if (!memory_id) {
|
|
1431
|
-
return {
|
|
1432
|
-
isError: true,
|
|
1433
|
-
content: [{ type: "text", text: "graph requires memory_id." }],
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
const resolveId = (id) => {
|
|
1437
|
-
if (id.length >= 36)
|
|
1438
|
-
return id;
|
|
1439
|
-
const match = db.getAll().find(m => m.id.startsWith(id));
|
|
1440
|
-
return match?.id ?? id;
|
|
1441
|
-
};
|
|
1442
|
-
const fullId = resolveId(memory_id);
|
|
1443
|
-
const mem = db.getById(fullId);
|
|
1444
|
-
if (!mem) {
|
|
1445
|
-
return {
|
|
1446
|
-
isError: true,
|
|
1447
|
-
content: [{ type: "text", text: `Memory "${memory_id}" not found.` }],
|
|
1448
|
-
};
|
|
1449
|
-
}
|
|
1450
|
-
const relations = db.getRelations(fullId);
|
|
1451
|
-
if (relations.length === 0) {
|
|
1452
|
-
return {
|
|
1453
|
-
content: [{
|
|
1454
|
-
type: "text",
|
|
1455
|
-
text: `Memory ${fullId.slice(0, 8)} has no explicit relations yet.\n\nUse action:relate to build the knowledge graph.`,
|
|
1456
|
-
}],
|
|
1457
|
-
structuredContent: { action: "graph", memoryId: fullId, relations: [] },
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
const lines = [
|
|
1461
|
-
`Knowledge graph for memory ${fullId.slice(0, 8)}:`,
|
|
1462
|
-
`"${mem.content.slice(0, 80)}${mem.content.length > 80 ? "…" : ""}"`,
|
|
1463
|
-
"",
|
|
1464
|
-
];
|
|
1465
|
-
const structRelations = [];
|
|
1466
|
-
for (const r of relations) {
|
|
1467
|
-
const direction = r.fromId === fullId ? "outgoing" : "incoming";
|
|
1468
|
-
const otherId = direction === "outgoing" ? r.toId : r.fromId;
|
|
1469
|
-
const other = db.getById(otherId);
|
|
1470
|
-
const arrow = direction === "outgoing" ? `→ [${r.relationshipType}] →` : `← [${r.relationshipType}] ←`;
|
|
1471
|
-
lines.push(` ${arrow} ${other?.content.slice(0, 60) ?? otherId.slice(0, 8)} (${(r.strength * 100).toFixed(0)}% strength)`);
|
|
1472
|
-
lines.push(` relation id: ${r.id.slice(0, 8)}`);
|
|
1473
|
-
structRelations.push({
|
|
1474
|
-
relatedId: otherId,
|
|
1475
|
-
direction,
|
|
1476
|
-
type: r.relationshipType,
|
|
1477
|
-
strength: r.strength,
|
|
1478
|
-
content: other?.content,
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
return {
|
|
1482
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
1483
|
-
structuredContent: {
|
|
1484
|
-
action: "graph",
|
|
1485
|
-
memoryId: fullId,
|
|
1486
|
-
relations: structRelations,
|
|
1487
|
-
},
|
|
1488
|
-
};
|
|
1489
|
-
}
|
|
1490
|
-
catch (error) {
|
|
1491
|
-
return {
|
|
1492
|
-
isError: true,
|
|
1493
|
-
content: [{
|
|
1494
|
-
type: "text",
|
|
1495
|
-
text: `Error managing relations: ${error instanceof Error ? error.message : String(error)}`,
|
|
1496
|
-
}],
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
});
|
|
1500
|
-
// ── memory_since ──────────────────────────────────────────
|
|
1501
|
-
server.registerTool("memory_since", {
|
|
1502
|
-
title: "Temporal Memory Query",
|
|
1503
|
-
description: `Query memories by when they were created. Use this to answer "what did we decide last week?" or "what changed since yesterday?" or to find memories from a specific time window.
|
|
1504
|
-
|
|
1505
|
-
Natural language time expressions supported:
|
|
1506
|
-
- "5m", "30m" — minutes ago
|
|
1507
|
-
- "1h", "2h", "6h" — hours ago
|
|
1508
|
-
- "1d", "7d", "30d" — days ago
|
|
1509
|
-
- "1w", "2w" — weeks ago
|
|
1510
|
-
- "1mo", "3mo" — months ago
|
|
1511
|
-
- ISO 8601 timestamp — exact time (e.g. "2025-01-15T10:00:00Z")
|
|
1512
|
-
- Unix millisecond timestamp
|
|
1513
|
-
|
|
1514
|
-
Args:
|
|
1515
|
-
- since (string): How far back to look — "7d", "1w", "2025-01-15", etc.
|
|
1516
|
-
- until (string, optional): End of time window — same format. Defaults to now.
|
|
1517
|
-
- type (enum, optional): Filter by memory type within this window`,
|
|
1518
|
-
inputSchema: z.object({
|
|
1519
|
-
since: z.string().min(1).describe("Start of time window — '7d', '2w', '1h', or ISO timestamp"),
|
|
1520
|
-
until: z.string().optional().describe("End of time window — defaults to now"),
|
|
1521
|
-
type: z.enum(TYPE_ORDER).optional().describe("Filter by memory type"),
|
|
1522
|
-
}).strict(),
|
|
1523
|
-
outputSchema: TemporalResultSchema,
|
|
1524
|
-
annotations: {
|
|
1525
|
-
readOnlyHint: true,
|
|
1526
|
-
destructiveHint: false,
|
|
1527
|
-
idempotentHint: true,
|
|
1528
|
-
openWorldHint: false,
|
|
1529
|
-
},
|
|
1530
|
-
}, async ({ since, until, type }) => {
|
|
1531
|
-
try {
|
|
1532
|
-
const parseTime = (s) => {
|
|
1533
|
-
const now = Date.now();
|
|
1534
|
-
const match = s.match(/^(\d+)(m|min|h|d|w|mo)$/i);
|
|
1535
|
-
if (match) {
|
|
1536
|
-
const n = parseInt(match[1], 10);
|
|
1537
|
-
const unit = match[2].toLowerCase();
|
|
1538
|
-
const ms = { m: 60000, min: 60000, h: 3600000, d: 86400000, w: 604800000, mo: 2592000000 };
|
|
1539
|
-
return now - n * (ms[unit] ?? 86400000);
|
|
1540
|
-
}
|
|
1541
|
-
const parsed = Date.parse(s);
|
|
1542
|
-
if (!isNaN(parsed))
|
|
1543
|
-
return parsed;
|
|
1544
|
-
const num = Number(s);
|
|
1545
|
-
if (!isNaN(num))
|
|
1546
|
-
return num;
|
|
1547
|
-
throw new Error(`Cannot parse time expression: "${s}". Use formats like "7d", "2w", "1h", or an ISO date.`);
|
|
1548
|
-
};
|
|
1549
|
-
const fromTs = parseTime(since);
|
|
1550
|
-
const toTs = until ? parseTime(until) : Date.now();
|
|
1551
|
-
let memories = db.getMemoriesByDateRange(fromTs, toTs);
|
|
1552
|
-
if (type)
|
|
1553
|
-
memories = memories.filter(m => m.type === type);
|
|
1554
|
-
if (memories.length === 0) {
|
|
1555
|
-
return {
|
|
1556
|
-
content: [{
|
|
1557
|
-
type: "text",
|
|
1558
|
-
text: `No memories found between ${new Date(fromTs).toISOString().slice(0, 10)} and ${new Date(toTs).toISOString().slice(0, 10)}${type ? ` of type "${type}"` : ""}.`,
|
|
1559
|
-
}],
|
|
1560
|
-
structuredContent: {
|
|
1561
|
-
from: new Date(fromTs).toISOString(),
|
|
1562
|
-
to: new Date(toTs).toISOString(),
|
|
1563
|
-
total: 0,
|
|
1564
|
-
memories: [],
|
|
1565
|
-
},
|
|
1566
|
-
};
|
|
1567
|
-
}
|
|
1568
|
-
const lines = [
|
|
1569
|
-
`Memories from ${new Date(fromTs).toISOString().slice(0, 10)} → ${new Date(toTs).toISOString().slice(0, 10)}`,
|
|
1570
|
-
type ? `Type filter: ${type}` : `All types`,
|
|
1571
|
-
`Found: ${memories.length}`,
|
|
1572
|
-
"",
|
|
1573
|
-
];
|
|
1574
|
-
for (const m of memories) {
|
|
1575
|
-
lines.push(`[${m.type}] ${m.content.slice(0, 80)}${m.content.length > 80 ? "…" : ""}`);
|
|
1576
|
-
lines.push(` Created: ${formatAge(m.createdAt)} | Confidence: ${(m.confidence * 100).toFixed(0)}% | ID: ${m.id.slice(0, 8)}`);
|
|
1577
|
-
if (m.tags.length > 0)
|
|
1578
|
-
lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
1579
|
-
lines.push("");
|
|
1580
|
-
}
|
|
1581
|
-
return {
|
|
1582
|
-
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1583
|
-
structuredContent: {
|
|
1584
|
-
from: new Date(fromTs).toISOString(),
|
|
1585
|
-
to: new Date(toTs).toISOString(),
|
|
1586
|
-
total: memories.length,
|
|
1587
|
-
memories: memories.map(m => ({
|
|
1588
|
-
id: m.id,
|
|
1589
|
-
content: m.content,
|
|
1590
|
-
type: m.type,
|
|
1591
|
-
confidence: m.confidence,
|
|
1592
|
-
createdAt: m.createdAt,
|
|
1593
|
-
age: formatAge(m.createdAt),
|
|
1594
|
-
tags: m.tags,
|
|
1595
|
-
})),
|
|
1596
|
-
},
|
|
1597
|
-
};
|
|
1598
|
-
}
|
|
1599
|
-
catch (error) {
|
|
1600
|
-
return {
|
|
1601
|
-
isError: true,
|
|
1602
|
-
content: [{
|
|
1603
|
-
type: "text",
|
|
1604
|
-
text: `Error in temporal query: ${error instanceof Error ? error.message : String(error)}`,
|
|
1605
|
-
}],
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
});
|
|
1609
|
-
// ── memory_search ─────────────────────────────────────────
|
|
1610
|
-
server.registerTool("memory_search", {
|
|
1611
|
-
title: "Full-Text Memory Search",
|
|
1612
|
-
description: `Exact full-text search across all memory content and tags using SQLite FTS5. Complements memory_recall (which is semantic/fuzzy) with precise keyword matching.
|
|
1613
|
-
|
|
1614
|
-
Use this when:
|
|
1615
|
-
- You need exact phrase matching ("never use any" not just "TypeScript types")
|
|
1616
|
-
- Searching for a specific function name, file path, or technical term
|
|
1617
|
-
- memory_recall returns too many loosely-related results
|
|
1618
|
-
- You want to find all memories mentioning a specific tool, library, or concept
|
|
1619
|
-
|
|
1620
|
-
Supports FTS5 query syntax:
|
|
1621
|
-
- Simple terms: "postgres"
|
|
1622
|
-
- Phrase search: '"event sourcing"'
|
|
1623
|
-
- Prefix search: "auth*"
|
|
1624
|
-
- Boolean: "postgres OR sqlite"
|
|
1625
|
-
- Negation: "database NOT redis"
|
|
1626
|
-
|
|
1627
|
-
Args:
|
|
1628
|
-
- query (string): Full-text search query — exact terms, phrases, or FTS5 syntax
|
|
1629
|
-
- limit (number): Max results (default: 20)`,
|
|
1630
|
-
inputSchema: z.object({
|
|
1631
|
-
query: z.string().min(1).describe("Full-text search query — exact terms, phrases, or FTS5 syntax"),
|
|
1632
|
-
limit: z.number().int().min(1).max(100).default(20).describe("Max results to return"),
|
|
1633
|
-
}).strict(),
|
|
1634
|
-
outputSchema: RecallResultSchema,
|
|
1635
|
-
annotations: {
|
|
1636
|
-
readOnlyHint: true,
|
|
1637
|
-
destructiveHint: false,
|
|
1638
|
-
idempotentHint: true,
|
|
1639
|
-
openWorldHint: false,
|
|
1640
|
-
},
|
|
1641
|
-
}, async ({ query, limit }) => {
|
|
1642
|
-
try {
|
|
1643
|
-
const results = db.fullTextSearch(query, limit, project);
|
|
1644
|
-
if (results.length === 0) {
|
|
1645
|
-
return {
|
|
1646
|
-
content: [{ type: "text", text: `No memories found matching "${query}". Try memory_recall for semantic/fuzzy search.` }],
|
|
1647
|
-
structuredContent: { query, total: 0, memories: [] },
|
|
1648
|
-
};
|
|
1649
|
-
}
|
|
1650
|
-
const lines = [`Full-text search: "${query}" — ${results.length} result${results.length === 1 ? "" : "s"}`, ""];
|
|
1651
|
-
for (const m of results) {
|
|
1652
|
-
lines.push(`[${m.type}] ${m.content}`);
|
|
1653
|
-
lines.push(` ID: ${m.id.slice(0, 8)} | Confidence: ${(m.confidence * 100).toFixed(0)}% | ${formatAge(m.lastAccessed)}`);
|
|
1654
|
-
if (m.tags.length > 0)
|
|
1655
|
-
lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
1656
|
-
lines.push("");
|
|
1657
|
-
}
|
|
1658
|
-
return {
|
|
1659
|
-
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1660
|
-
structuredContent: {
|
|
1661
|
-
query,
|
|
1662
|
-
total: results.length,
|
|
1663
|
-
memories: results.map(m => ({
|
|
1664
|
-
id: m.id,
|
|
1665
|
-
content: m.content,
|
|
1666
|
-
type: m.type,
|
|
1667
|
-
score: 1.0,
|
|
1668
|
-
confidence: m.confidence,
|
|
1669
|
-
tags: m.tags,
|
|
1670
|
-
age: formatAge(m.lastAccessed),
|
|
1671
|
-
})),
|
|
1672
|
-
},
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
catch (error) {
|
|
1676
|
-
return {
|
|
1677
|
-
isError: true,
|
|
1678
|
-
content: [{
|
|
1679
|
-
type: "text",
|
|
1680
|
-
text: `Error in full-text search: ${error instanceof Error ? error.message : String(error)}`,
|
|
1681
|
-
}],
|
|
1682
|
-
};
|
|
1683
|
-
}
|
|
1684
|
-
});
|
|
1685
|
-
// ── reminder_set ─────────────────────────────────────────
|
|
1686
|
-
server.registerTool("reminder_set", {
|
|
1687
|
-
title: "Set Reminder",
|
|
1688
|
-
description: `Create a reminder with optional due date.
|
|
1689
|
-
|
|
1690
|
-
Args:
|
|
1691
|
-
- content (string): What to be reminded about
|
|
1692
|
-
- due_at (number, optional): Unix timestamp (ms) for when the reminder is due
|
|
1693
|
-
- scope (string): Scope for the reminder — 'global' or project-specific (default: 'global')
|
|
1694
|
-
|
|
1695
|
-
Returns:
|
|
1696
|
-
Confirmation with reminder ID.`,
|
|
1697
|
-
inputSchema: z.object({
|
|
1698
|
-
content: z.string().min(1).describe("What to be reminded about"),
|
|
1699
|
-
due_at: z.number().optional().describe("Unix timestamp (ms) for when the reminder is due"),
|
|
1700
|
-
scope: z.string().default("global").describe("Scope for the reminder — 'global' or project-specific"),
|
|
1701
|
-
}).strict(),
|
|
1702
|
-
annotations: {
|
|
1703
|
-
readOnlyHint: false,
|
|
1704
|
-
destructiveHint: false,
|
|
1705
|
-
idempotentHint: false,
|
|
1706
|
-
openWorldHint: false,
|
|
1707
|
-
},
|
|
1708
|
-
}, async ({ content, due_at, scope }) => {
|
|
1709
|
-
try {
|
|
1710
|
-
const id = db.insertReminder(content, due_at ?? null, scope);
|
|
1711
|
-
const dueStr = due_at ? ` (due: ${new Date(due_at).toISOString()})` : "";
|
|
1712
|
-
return {
|
|
1713
|
-
content: [{
|
|
1714
|
-
type: "text",
|
|
1715
|
-
text: `Reminder set: "${content}"${dueStr}\nID: ${id}`,
|
|
1716
|
-
}],
|
|
1717
|
-
};
|
|
1718
|
-
}
|
|
1719
|
-
catch (error) {
|
|
1720
|
-
return {
|
|
1721
|
-
isError: true,
|
|
1722
|
-
content: [{
|
|
1723
|
-
type: "text",
|
|
1724
|
-
text: `Error setting reminder: ${error instanceof Error ? error.message : String(error)}`,
|
|
1725
|
-
}],
|
|
1726
|
-
};
|
|
1727
|
-
}
|
|
1728
|
-
});
|
|
1729
|
-
// ── reminder_list ────────────────────────────────────────
|
|
1730
|
-
server.registerTool("reminder_list", {
|
|
1731
|
-
title: "List Reminders",
|
|
1732
|
-
description: `List reminders, optionally including completed ones.
|
|
1733
|
-
|
|
1734
|
-
Args:
|
|
1735
|
-
- include_completed (boolean): Whether to include completed reminders (default: false)
|
|
1736
|
-
- scope (string, optional): Filter by scope — returns global + scope-matching reminders
|
|
1737
|
-
|
|
1738
|
-
Returns:
|
|
1739
|
-
List of reminders with their status.`,
|
|
1740
|
-
inputSchema: z.object({
|
|
1741
|
-
include_completed: z.boolean().default(false).describe("Whether to include completed reminders"),
|
|
1742
|
-
scope: z.string().optional().describe("Filter by scope — returns global + scope-matching reminders"),
|
|
1743
|
-
}).strict(),
|
|
1744
|
-
annotations: {
|
|
1745
|
-
readOnlyHint: true,
|
|
1746
|
-
destructiveHint: false,
|
|
1747
|
-
idempotentHint: true,
|
|
1748
|
-
openWorldHint: false,
|
|
1749
|
-
},
|
|
1750
|
-
}, async ({ include_completed, scope }) => {
|
|
1751
|
-
try {
|
|
1752
|
-
const reminders = db.listReminders(include_completed, scope);
|
|
1753
|
-
if (reminders.length === 0) {
|
|
1754
|
-
return {
|
|
1755
|
-
content: [{ type: "text", text: "No reminders found." }],
|
|
1756
|
-
};
|
|
1757
|
-
}
|
|
1758
|
-
const lines = [`${reminders.length} reminder${reminders.length === 1 ? "" : "s"}:`, ""];
|
|
1759
|
-
for (const r of reminders) {
|
|
1760
|
-
const dueStr = r.dueAt ? new Date(r.dueAt).toISOString() : "no due date";
|
|
1761
|
-
const status = r.completed ? "[DONE]" : "[pending]";
|
|
1762
|
-
lines.push(`${status} ${r.content}`);
|
|
1763
|
-
lines.push(` ID: ${r.id.slice(0, 8)} | Due: ${dueStr} | Scope: ${r.scope}`);
|
|
1764
|
-
lines.push("");
|
|
1765
|
-
}
|
|
1766
|
-
return {
|
|
1767
|
-
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1770
|
-
catch (error) {
|
|
1771
|
-
return {
|
|
1772
|
-
isError: true,
|
|
1773
|
-
content: [{
|
|
1774
|
-
type: "text",
|
|
1775
|
-
text: `Error listing reminders: ${error instanceof Error ? error.message : String(error)}`,
|
|
1776
|
-
}],
|
|
1777
|
-
};
|
|
1778
|
-
}
|
|
1779
|
-
});
|
|
1780
|
-
// ── reminder_check ───────────────────────────────────────
|
|
1781
|
-
server.registerTool("reminder_check", {
|
|
1782
|
-
title: "Check Reminders",
|
|
1783
|
-
description: `Check for overdue, today's, and upcoming reminders (within 7 days). Use this proactively at the start of sessions.
|
|
1784
|
-
|
|
1785
|
-
Args: None
|
|
1786
|
-
|
|
1787
|
-
Returns:
|
|
1788
|
-
List of actionable reminders with [OVERDUE], [TODAY], or [upcoming] prefixes.`,
|
|
1789
|
-
inputSchema: z.object({}).strict(),
|
|
1790
|
-
annotations: {
|
|
1791
|
-
readOnlyHint: true,
|
|
1792
|
-
destructiveHint: false,
|
|
1793
|
-
idempotentHint: true,
|
|
1794
|
-
openWorldHint: false,
|
|
1795
|
-
},
|
|
1796
|
-
}, async () => {
|
|
1797
|
-
try {
|
|
1798
|
-
const reminders = db.checkReminders();
|
|
1799
|
-
if (reminders.length === 0) {
|
|
1800
|
-
return {
|
|
1801
|
-
content: [{ type: "text", text: "No overdue, today, or upcoming reminders." }],
|
|
1802
|
-
};
|
|
1803
|
-
}
|
|
1804
|
-
const lines = [`${reminders.length} actionable reminder${reminders.length === 1 ? "" : "s"}:`, ""];
|
|
1805
|
-
for (const r of reminders) {
|
|
1806
|
-
const prefix = r.status === "overdue" ? "[OVERDUE]" : r.status === "today" ? "[TODAY]" : "[upcoming]";
|
|
1807
|
-
const dueStr = r.dueAt ? new Date(r.dueAt).toISOString() : "no due date";
|
|
1808
|
-
lines.push(`${prefix} ${r.content}`);
|
|
1809
|
-
lines.push(` ID: ${r.id.slice(0, 8)} | Due: ${dueStr} | Scope: ${r.scope}`);
|
|
1810
|
-
lines.push("");
|
|
1811
|
-
}
|
|
1812
|
-
return {
|
|
1813
|
-
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1814
|
-
};
|
|
1815
|
-
}
|
|
1816
|
-
catch (error) {
|
|
1817
|
-
return {
|
|
1818
|
-
isError: true,
|
|
1819
|
-
content: [{
|
|
1820
|
-
type: "text",
|
|
1821
|
-
text: `Error checking reminders: ${error instanceof Error ? error.message : String(error)}`,
|
|
1822
|
-
}],
|
|
1823
|
-
};
|
|
1824
|
-
}
|
|
1825
|
-
});
|
|
1826
|
-
// ── reminder_complete ────────────────────────────────────
|
|
1827
|
-
server.registerTool("reminder_complete", {
|
|
1828
|
-
title: "Complete Reminder",
|
|
1829
|
-
description: `Mark a reminder as completed. Supports partial ID matching (first 8 characters).
|
|
1830
|
-
|
|
1831
|
-
Args:
|
|
1832
|
-
- id (string): Full or partial reminder ID
|
|
1833
|
-
|
|
1834
|
-
Returns:
|
|
1835
|
-
Confirmation that the reminder was completed.`,
|
|
1836
|
-
inputSchema: z.object({
|
|
1837
|
-
id: z.string().min(1).describe("Full or partial reminder ID"),
|
|
1838
|
-
}).strict(),
|
|
1839
|
-
annotations: {
|
|
1840
|
-
readOnlyHint: false,
|
|
1841
|
-
destructiveHint: false,
|
|
1842
|
-
idempotentHint: true,
|
|
1843
|
-
openWorldHint: false,
|
|
1844
|
-
},
|
|
1845
|
-
}, async ({ id }) => {
|
|
1846
|
-
try {
|
|
1847
|
-
// Try exact match first
|
|
1848
|
-
if (db.completeReminder(id)) {
|
|
1849
|
-
return {
|
|
1850
|
-
content: [{ type: "text", text: `Reminder ${id.slice(0, 8)} marked as completed.` }],
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
// Try partial ID match
|
|
1854
|
-
const all = db.listReminders();
|
|
1855
|
-
const matches = all.filter(r => r.id.startsWith(id));
|
|
1856
|
-
if (matches.length === 0) {
|
|
1857
|
-
return {
|
|
1858
|
-
isError: true,
|
|
1859
|
-
content: [{ type: "text", text: `No reminder found matching ID "${id}".` }],
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
if (matches.length > 1) {
|
|
1863
|
-
const ids = matches.map(r => ` ${r.id.slice(0, 8)}: ${r.content}`).join("\n");
|
|
1864
|
-
return {
|
|
1865
|
-
isError: true,
|
|
1866
|
-
content: [{ type: "text", text: `Multiple reminders match "${id}". Be more specific:\n${ids}` }],
|
|
1867
|
-
};
|
|
1868
|
-
}
|
|
1869
|
-
db.completeReminder(matches[0].id);
|
|
1870
|
-
return {
|
|
1871
|
-
content: [{ type: "text", text: `Reminder ${matches[0].id.slice(0, 8)} marked as completed: "${matches[0].content}"` }],
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
catch (error) {
|
|
1875
|
-
return {
|
|
1876
|
-
isError: true,
|
|
1877
|
-
content: [{
|
|
1878
|
-
type: "text",
|
|
1879
|
-
text: `Error completing reminder: ${error instanceof Error ? error.message : String(error)}`,
|
|
1880
|
-
}],
|
|
1881
|
-
};
|
|
1882
|
-
}
|
|
1883
|
-
});
|
|
1884
|
-
}
|
|
1885
|
-
//# sourceMappingURL=tools.js.map
|