@aman_asmuei/amem 0.3.0 → 0.4.1
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 +272 -483
- package/dist/database.d.ts +51 -0
- package/dist/database.js +289 -0
- package/dist/database.js.map +1 -1
- package/dist/index.js +80 -7
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +321 -28
- package/dist/schemas.js +93 -0
- package/dist/schemas.js.map +1 -1
- package/dist/tools.js +787 -15
- package/dist/tools.js.map +1 -1
- package/package.json +5 -1
package/dist/tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MemoryType, recallMemories, detectConflict, consolidateMemories } from "./memory.js";
|
|
3
3
|
import { generateEmbedding, cosineSimilarity } from "./embeddings.js";
|
|
4
|
-
import { StoreResultSchema, RecallResultSchema, ContextResultSchema, ForgetResultSchema, ExtractResultSchema, StatsResultSchema, ExportResultSchema, InjectResultSchema, ConsolidateResultSchema, } from "./schemas.js";
|
|
4
|
+
import { StoreResultSchema, RecallResultSchema, ContextResultSchema, ForgetResultSchema, ExtractResultSchema, StatsResultSchema, ExportResultSchema, InjectResultSchema, ConsolidateResultSchema, PatchResultSchema, LogAppendResultSchema, LogRecallResultSchema, RelateResultSchema, VersionResultSchema, TemporalResultSchema, } from "./schemas.js";
|
|
5
5
|
const MEMORY_TYPES = Object.values(MemoryType);
|
|
6
6
|
const CHARACTER_LIMIT = 50_000;
|
|
7
7
|
export const TYPE_ORDER = ["correction", "decision", "pattern", "preference", "topology", "fact"];
|
|
@@ -39,7 +39,7 @@ Args:
|
|
|
39
39
|
Returns:
|
|
40
40
|
Confirmation with memory ID, or conflict detection if a similar memory exists.`,
|
|
41
41
|
inputSchema: z.object({
|
|
42
|
-
content: z.string().min(1, "Content is required").describe("The memory content — be specific and include context"),
|
|
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
43
|
type: z.enum(MEMORY_TYPES).describe("Memory type — corrections are highest priority"),
|
|
44
44
|
tags: z.array(z.string()).default([]).describe("Tags for filtering (e.g., ['typescript', 'auth', 'testing'])"),
|
|
45
45
|
confidence: z.number().min(0).max(1).default(0.8).describe("How confident is this memory (0-1). Corrections from user = 1.0"),
|
|
@@ -165,9 +165,9 @@ Returns:
|
|
|
165
165
|
}).strict(),
|
|
166
166
|
outputSchema: RecallResultSchema,
|
|
167
167
|
annotations: {
|
|
168
|
-
readOnlyHint:
|
|
168
|
+
readOnlyHint: false,
|
|
169
169
|
destructiveHint: false,
|
|
170
|
-
idempotentHint:
|
|
170
|
+
idempotentHint: false,
|
|
171
171
|
openWorldHint: false,
|
|
172
172
|
},
|
|
173
173
|
}, async ({ query, limit, type, tag, min_confidence }) => {
|
|
@@ -188,6 +188,11 @@ Returns:
|
|
|
188
188
|
if (results.length === 0) {
|
|
189
189
|
return {
|
|
190
190
|
content: [{ type: "text", text: `No memories found for: "${query}". Try broadening your search or using different keywords.` }],
|
|
191
|
+
structuredContent: {
|
|
192
|
+
query,
|
|
193
|
+
total: 0,
|
|
194
|
+
memories: [],
|
|
195
|
+
},
|
|
191
196
|
};
|
|
192
197
|
}
|
|
193
198
|
const memoriesData = results.map((r) => ({
|
|
@@ -243,9 +248,9 @@ Returns:
|
|
|
243
248
|
}).strict(),
|
|
244
249
|
outputSchema: ContextResultSchema,
|
|
245
250
|
annotations: {
|
|
246
|
-
readOnlyHint:
|
|
251
|
+
readOnlyHint: false,
|
|
247
252
|
destructiveHint: false,
|
|
248
|
-
idempotentHint:
|
|
253
|
+
idempotentHint: false,
|
|
249
254
|
openWorldHint: false,
|
|
250
255
|
},
|
|
251
256
|
}, async ({ topic, max_tokens }) => {
|
|
@@ -260,6 +265,11 @@ Returns:
|
|
|
260
265
|
if (results.length === 0) {
|
|
261
266
|
return {
|
|
262
267
|
content: [{ type: "text", text: `No context found for: "${topic}". Store some memories first using memory_store or memory_extract.` }],
|
|
268
|
+
structuredContent: {
|
|
269
|
+
topic,
|
|
270
|
+
groups: [],
|
|
271
|
+
memoriesUsed: 0,
|
|
272
|
+
},
|
|
263
273
|
};
|
|
264
274
|
}
|
|
265
275
|
const grouped = {};
|
|
@@ -374,6 +384,12 @@ Error Handling:
|
|
|
374
384
|
if (matches.length === 0) {
|
|
375
385
|
return {
|
|
376
386
|
content: [{ type: "text", text: `No memories found matching "${query}".` }],
|
|
387
|
+
structuredContent: {
|
|
388
|
+
action: "preview",
|
|
389
|
+
query,
|
|
390
|
+
total: 0,
|
|
391
|
+
previewed: [],
|
|
392
|
+
},
|
|
377
393
|
};
|
|
378
394
|
}
|
|
379
395
|
if (!confirm) {
|
|
@@ -387,7 +403,7 @@ Error Handling:
|
|
|
387
403
|
action: "preview",
|
|
388
404
|
query,
|
|
389
405
|
total: matches.length,
|
|
390
|
-
previewed: matches.slice(0, 5).map(m => ({ id: m.id
|
|
406
|
+
previewed: matches.slice(0, 5).map(m => ({ id: m.id, content: m.content })),
|
|
391
407
|
},
|
|
392
408
|
};
|
|
393
409
|
}
|
|
@@ -442,7 +458,7 @@ Returns:
|
|
|
442
458
|
Summary of stored, reinforced, and skipped memories with details.`,
|
|
443
459
|
inputSchema: z.object({
|
|
444
460
|
memories: z.array(z.object({
|
|
445
|
-
content: z.string().min(1, "Content is required").describe("Specific, self-contained memory statement"),
|
|
461
|
+
content: z.string().min(1, "Content is required").max(10000, "Content too long — max 10,000 characters").describe("Specific, self-contained memory statement"),
|
|
446
462
|
type: z.enum(MEMORY_TYPES).describe("Memory type"),
|
|
447
463
|
tags: z.array(z.string()).default([]).describe("Relevant tags"),
|
|
448
464
|
confidence: z.number().min(0).max(1).default(0.8).describe("Confidence level"),
|
|
@@ -462,13 +478,14 @@ Returns:
|
|
|
462
478
|
let reinforced = 0;
|
|
463
479
|
const details = [];
|
|
464
480
|
const structuredDetails = [];
|
|
481
|
+
// Load existing embeddings once (not per-memory)
|
|
482
|
+
const existingWithEmbeddings = db.getAllWithEmbeddings();
|
|
465
483
|
for (const input of memoryInputs) {
|
|
466
484
|
const embedding = await generateEmbedding(input.content);
|
|
467
485
|
// Check for duplicates/conflicts
|
|
468
486
|
let isDuplicate = false;
|
|
469
487
|
if (embedding) {
|
|
470
|
-
const
|
|
471
|
-
for (const mem of existing) {
|
|
488
|
+
for (const mem of existingWithEmbeddings) {
|
|
472
489
|
if (!mem.embedding)
|
|
473
490
|
continue;
|
|
474
491
|
const sim = cosineSimilarity(embedding, mem.embedding);
|
|
@@ -555,11 +572,17 @@ Returns:
|
|
|
555
572
|
},
|
|
556
573
|
}, async () => {
|
|
557
574
|
try {
|
|
575
|
+
const all = db.getAllForProject(project);
|
|
558
576
|
const stats = db.getStats();
|
|
559
|
-
|
|
560
|
-
if (stats.total === 0) {
|
|
577
|
+
if (all.length === 0) {
|
|
561
578
|
return {
|
|
562
579
|
content: [{ type: "text", text: "No memories stored yet. Use memory_store or memory_extract to create memories." }],
|
|
580
|
+
structuredContent: {
|
|
581
|
+
total: 0,
|
|
582
|
+
byType: {},
|
|
583
|
+
confidence: { high: 0, medium: 0, low: 0 },
|
|
584
|
+
embeddingCoverage: { withEmbeddings: 0, total: 0 },
|
|
585
|
+
},
|
|
563
586
|
};
|
|
564
587
|
}
|
|
565
588
|
const typeLines = TYPE_ORDER
|
|
@@ -621,10 +644,16 @@ Returns:
|
|
|
621
644
|
},
|
|
622
645
|
}, async () => {
|
|
623
646
|
try {
|
|
624
|
-
const all = db.
|
|
647
|
+
const all = db.getAllForProject(project);
|
|
625
648
|
if (all.length === 0) {
|
|
626
649
|
return {
|
|
627
650
|
content: [{ type: "text", text: "No memories to export. Use memory_store or memory_extract to create memories." }],
|
|
651
|
+
structuredContent: {
|
|
652
|
+
exportedAt: new Date().toISOString(),
|
|
653
|
+
total: 0,
|
|
654
|
+
markdown: "",
|
|
655
|
+
truncated: false,
|
|
656
|
+
},
|
|
628
657
|
};
|
|
629
658
|
}
|
|
630
659
|
let md = `# Amem Memory Export\n\n`;
|
|
@@ -692,9 +721,9 @@ Returns:
|
|
|
692
721
|
}).strict(),
|
|
693
722
|
outputSchema: InjectResultSchema,
|
|
694
723
|
annotations: {
|
|
695
|
-
readOnlyHint:
|
|
724
|
+
readOnlyHint: false,
|
|
696
725
|
destructiveHint: false,
|
|
697
|
-
idempotentHint:
|
|
726
|
+
idempotentHint: false,
|
|
698
727
|
openWorldHint: false,
|
|
699
728
|
},
|
|
700
729
|
}, async ({ topic }) => {
|
|
@@ -837,5 +866,748 @@ Returns:
|
|
|
837
866
|
};
|
|
838
867
|
}
|
|
839
868
|
});
|
|
869
|
+
// ── memory_patch ──────────────────────────────────────────
|
|
870
|
+
server.registerTool("memory_patch", {
|
|
871
|
+
title: "Patch Memory",
|
|
872
|
+
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.
|
|
873
|
+
|
|
874
|
+
Use this when:
|
|
875
|
+
- Correcting a memory that is mostly right but has a wrong detail
|
|
876
|
+
- Updating confidence after validation
|
|
877
|
+
- Retagging a memory for better recall
|
|
878
|
+
- Reclassifying type (e.g. fact → decision)
|
|
879
|
+
|
|
880
|
+
Every patch creates a version snapshot. Use memory_versions to view history or roll back.
|
|
881
|
+
|
|
882
|
+
Args:
|
|
883
|
+
- id (string): Memory ID to patch (short IDs like first 8 chars work)
|
|
884
|
+
- field (enum): Which field to change — content | confidence | tags | type
|
|
885
|
+
- value (string | number | string[]): New value for the field
|
|
886
|
+
- reason (string): Why this patch is being made — stored in version history`,
|
|
887
|
+
inputSchema: z.object({
|
|
888
|
+
id: z.string().min(1, "Memory ID is required").describe("Memory ID — full UUID or first 8 characters"),
|
|
889
|
+
field: z.enum(["content", "confidence", "tags", "type"]).describe("Which field to patch"),
|
|
890
|
+
value: z.union([
|
|
891
|
+
z.string(),
|
|
892
|
+
z.number().min(0).max(1),
|
|
893
|
+
z.array(z.string()),
|
|
894
|
+
]).describe("New value — string for content/type, number 0-1 for confidence, string[] for tags"),
|
|
895
|
+
reason: z.string().min(1).describe("Why this patch is being made — stored in version history"),
|
|
896
|
+
}).strict().refine(({ field, value }) => {
|
|
897
|
+
if (field === "confidence")
|
|
898
|
+
return typeof value === "number";
|
|
899
|
+
if (field === "tags")
|
|
900
|
+
return Array.isArray(value);
|
|
901
|
+
if (field === "content" || field === "type")
|
|
902
|
+
return typeof value === "string";
|
|
903
|
+
return true;
|
|
904
|
+
}, { message: "Value type must match field: string for content/type, number for confidence, string[] for tags" }),
|
|
905
|
+
outputSchema: PatchResultSchema,
|
|
906
|
+
annotations: {
|
|
907
|
+
readOnlyHint: false,
|
|
908
|
+
destructiveHint: false,
|
|
909
|
+
idempotentHint: false,
|
|
910
|
+
openWorldHint: false,
|
|
911
|
+
},
|
|
912
|
+
}, async ({ id, field, value, reason }) => {
|
|
913
|
+
try {
|
|
914
|
+
// Support short IDs: find full ID if 8-char prefix given
|
|
915
|
+
let fullId = id;
|
|
916
|
+
if (id.length < 36) {
|
|
917
|
+
const all = db.getAll();
|
|
918
|
+
const match = all.find(m => m.id.startsWith(id));
|
|
919
|
+
if (!match) {
|
|
920
|
+
return {
|
|
921
|
+
content: [{ type: "text", text: `No memory found with ID starting with "${id}".` }],
|
|
922
|
+
structuredContent: { action: "not_found", id },
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
fullId = match.id;
|
|
926
|
+
}
|
|
927
|
+
const mem = db.getById(fullId);
|
|
928
|
+
if (!mem) {
|
|
929
|
+
return {
|
|
930
|
+
content: [{ type: "text", text: `Memory "${fullId}" not found.` }],
|
|
931
|
+
structuredContent: { action: "not_found", id: fullId },
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const previousContent = field === "content" ? mem.content
|
|
935
|
+
: field === "confidence" ? String(mem.confidence)
|
|
936
|
+
: field === "tags" ? JSON.stringify(mem.tags)
|
|
937
|
+
: mem.type;
|
|
938
|
+
const success = db.patchMemory(fullId, { field, value, reason });
|
|
939
|
+
if (!success) {
|
|
940
|
+
return {
|
|
941
|
+
isError: true,
|
|
942
|
+
content: [{ type: "text", text: `Failed to patch memory "${fullId}". Unknown field or DB error.` }],
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
// Regenerate embedding if content changed
|
|
946
|
+
if (field === "content" && typeof value === "string") {
|
|
947
|
+
const newEmbedding = await generateEmbedding(value);
|
|
948
|
+
if (newEmbedding)
|
|
949
|
+
db.updateEmbedding(fullId, newEmbedding);
|
|
950
|
+
}
|
|
951
|
+
const displayValue = Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
|
|
952
|
+
return {
|
|
953
|
+
content: [{
|
|
954
|
+
type: "text",
|
|
955
|
+
text: `Patched memory (${fullId.slice(0, 8)}): ${field} → ${displayValue}\nReason: ${reason}\nPrevious ${field}: ${previousContent}\nVersion snapshot saved.`,
|
|
956
|
+
}],
|
|
957
|
+
structuredContent: {
|
|
958
|
+
action: "patched",
|
|
959
|
+
id: fullId,
|
|
960
|
+
field,
|
|
961
|
+
previousContent,
|
|
962
|
+
reason,
|
|
963
|
+
versionSaved: true,
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
catch (error) {
|
|
968
|
+
return {
|
|
969
|
+
isError: true,
|
|
970
|
+
content: [{
|
|
971
|
+
type: "text",
|
|
972
|
+
text: `Error patching memory: ${error instanceof Error ? error.message : String(error)}`,
|
|
973
|
+
}],
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
// ── memory_versions ───────────────────────────────────────
|
|
978
|
+
server.registerTool("memory_versions", {
|
|
979
|
+
title: "Memory Version History",
|
|
980
|
+
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.
|
|
981
|
+
|
|
982
|
+
Use this to:
|
|
983
|
+
- See how a memory has evolved over time
|
|
984
|
+
- Roll back a bad patch
|
|
985
|
+
- Audit when and why a memory changed
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
- memory_id (string): Memory to inspect — full or 8-char short ID
|
|
989
|
+
- restore_version_id (string, optional): If provided, restore this specific version (creates a new patch, keeps history intact)`,
|
|
990
|
+
inputSchema: z.object({
|
|
991
|
+
memory_id: z.string().min(1).describe("Memory ID to inspect — full UUID or first 8 chars"),
|
|
992
|
+
restore_version_id: z.string().optional().describe("Version ID to restore — rolls the memory back to this snapshot"),
|
|
993
|
+
}).strict(),
|
|
994
|
+
outputSchema: VersionResultSchema,
|
|
995
|
+
annotations: {
|
|
996
|
+
readOnlyHint: false,
|
|
997
|
+
destructiveHint: false,
|
|
998
|
+
idempotentHint: true,
|
|
999
|
+
openWorldHint: false,
|
|
1000
|
+
},
|
|
1001
|
+
}, async ({ memory_id, restore_version_id }) => {
|
|
1002
|
+
try {
|
|
1003
|
+
// Resolve short IDs
|
|
1004
|
+
let fullId = memory_id;
|
|
1005
|
+
if (memory_id.length < 36) {
|
|
1006
|
+
const all = db.getAll();
|
|
1007
|
+
const match = all.find(m => m.id.startsWith(memory_id));
|
|
1008
|
+
if (match)
|
|
1009
|
+
fullId = match.id;
|
|
1010
|
+
}
|
|
1011
|
+
const mem = db.getById(fullId);
|
|
1012
|
+
if (!mem) {
|
|
1013
|
+
return {
|
|
1014
|
+
isError: true,
|
|
1015
|
+
content: [{ type: "text", text: `Memory "${fullId}" not found.` }],
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
if (restore_version_id) {
|
|
1019
|
+
const history = db.getVersionHistory(fullId);
|
|
1020
|
+
const target = history.find(v => v.versionId === restore_version_id || v.versionId.startsWith(restore_version_id));
|
|
1021
|
+
if (!target) {
|
|
1022
|
+
return {
|
|
1023
|
+
isError: true,
|
|
1024
|
+
content: [{ type: "text", text: `Version "${restore_version_id}" not found in history for memory ${fullId.slice(0, 8)}.` }],
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
db.patchMemory(fullId, { field: "content", value: target.content, reason: `restored from version ${target.versionId.slice(0, 8)}` });
|
|
1028
|
+
db.patchMemory(fullId, { field: "confidence", value: target.confidence, reason: `restored from version ${target.versionId.slice(0, 8)}` });
|
|
1029
|
+
const newEmbedding = await generateEmbedding(target.content);
|
|
1030
|
+
if (newEmbedding)
|
|
1031
|
+
db.updateEmbedding(fullId, newEmbedding);
|
|
1032
|
+
return {
|
|
1033
|
+
content: [{
|
|
1034
|
+
type: "text",
|
|
1035
|
+
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)}`,
|
|
1036
|
+
}],
|
|
1037
|
+
structuredContent: {
|
|
1038
|
+
action: "restored",
|
|
1039
|
+
memoryId: fullId,
|
|
1040
|
+
restoredContent: target.content,
|
|
1041
|
+
versionId: target.versionId,
|
|
1042
|
+
},
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
const history = db.getVersionHistory(fullId);
|
|
1046
|
+
if (history.length === 0) {
|
|
1047
|
+
return {
|
|
1048
|
+
content: [{ type: "text", text: `No version history for memory ${fullId.slice(0, 8)}. Memories gain history after their first patch.` }],
|
|
1049
|
+
structuredContent: {
|
|
1050
|
+
action: "history",
|
|
1051
|
+
memoryId: fullId,
|
|
1052
|
+
currentContent: mem.content,
|
|
1053
|
+
versions: [],
|
|
1054
|
+
},
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
const lines = [
|
|
1058
|
+
`Version history for memory ${fullId.slice(0, 8)}`,
|
|
1059
|
+
`Current: "${mem.content}" (${(mem.confidence * 100).toFixed(0)}% confidence)`,
|
|
1060
|
+
"",
|
|
1061
|
+
`${history.length} version${history.length === 1 ? "" : "s"}:`,
|
|
1062
|
+
...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}`),
|
|
1063
|
+
];
|
|
1064
|
+
return {
|
|
1065
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1066
|
+
structuredContent: {
|
|
1067
|
+
action: "history",
|
|
1068
|
+
memoryId: fullId,
|
|
1069
|
+
currentContent: mem.content,
|
|
1070
|
+
versions: history.map(v => ({
|
|
1071
|
+
versionId: v.versionId,
|
|
1072
|
+
content: v.content,
|
|
1073
|
+
confidence: v.confidence,
|
|
1074
|
+
editedAt: v.editedAt,
|
|
1075
|
+
age: formatAge(v.editedAt),
|
|
1076
|
+
reason: v.reason,
|
|
1077
|
+
})),
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
return {
|
|
1083
|
+
isError: true,
|
|
1084
|
+
content: [{
|
|
1085
|
+
type: "text",
|
|
1086
|
+
text: `Error reading version history: ${error instanceof Error ? error.message : String(error)}`,
|
|
1087
|
+
}],
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
// ── memory_log ────────────────────────────────────────────
|
|
1092
|
+
server.registerTool("memory_log", {
|
|
1093
|
+
title: "Append to Conversation Log",
|
|
1094
|
+
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.
|
|
1095
|
+
|
|
1096
|
+
The log is your permanent audit trail:
|
|
1097
|
+
- Every user message, assistant response, or system note
|
|
1098
|
+
- Fully searchable via memory_log_recall
|
|
1099
|
+
- Organized by session ID for replaying conversations
|
|
1100
|
+
- Scoped per project — never mixes contexts
|
|
1101
|
+
|
|
1102
|
+
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.
|
|
1103
|
+
|
|
1104
|
+
Args:
|
|
1105
|
+
- session_id (string): Conversation session identifier — use a consistent ID per conversation
|
|
1106
|
+
- role (enum): Who said it — user | assistant | system
|
|
1107
|
+
- content (string): The exact text to preserve — no summarization
|
|
1108
|
+
- metadata (object, optional): Extra context — e.g., { tool: "vscode", file: "auth.ts" }`,
|
|
1109
|
+
inputSchema: z.object({
|
|
1110
|
+
session_id: z.string().min(1).describe("Session identifier — keep consistent across a conversation"),
|
|
1111
|
+
role: z.enum(["user", "assistant", "system"]).describe("Who said this"),
|
|
1112
|
+
content: z.string().min(1).max(50000, "Log content too long — max 50,000 characters").describe("Exact content to preserve — not summarized"),
|
|
1113
|
+
metadata: z.record(z.unknown()).optional().describe("Optional extra context"),
|
|
1114
|
+
}).strict(),
|
|
1115
|
+
outputSchema: LogAppendResultSchema,
|
|
1116
|
+
annotations: {
|
|
1117
|
+
readOnlyHint: false,
|
|
1118
|
+
destructiveHint: false,
|
|
1119
|
+
idempotentHint: false,
|
|
1120
|
+
openWorldHint: false,
|
|
1121
|
+
},
|
|
1122
|
+
}, async ({ session_id, role, content, metadata }) => {
|
|
1123
|
+
try {
|
|
1124
|
+
const id = db.appendLog({
|
|
1125
|
+
sessionId: session_id,
|
|
1126
|
+
role,
|
|
1127
|
+
content,
|
|
1128
|
+
project,
|
|
1129
|
+
metadata: metadata ?? {},
|
|
1130
|
+
});
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{
|
|
1133
|
+
type: "text",
|
|
1134
|
+
text: `Logged ${role} turn (${id.slice(0, 8)}) to session "${session_id}". Content length: ${content.length} chars.`,
|
|
1135
|
+
}],
|
|
1136
|
+
structuredContent: {
|
|
1137
|
+
id,
|
|
1138
|
+
sessionId: session_id,
|
|
1139
|
+
role,
|
|
1140
|
+
appended: true,
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
return {
|
|
1146
|
+
isError: true,
|
|
1147
|
+
content: [{
|
|
1148
|
+
type: "text",
|
|
1149
|
+
text: `Error appending to log: ${error instanceof Error ? error.message : String(error)}`,
|
|
1150
|
+
}],
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
// ── memory_log_recall ─────────────────────────────────────
|
|
1155
|
+
server.registerTool("memory_log_recall", {
|
|
1156
|
+
title: "Search Conversation Log",
|
|
1157
|
+
description: `Search or replay the lossless conversation log. Returns raw, unmodified conversation turns — nothing has been summarized or lost.
|
|
1158
|
+
|
|
1159
|
+
Use this when:
|
|
1160
|
+
- You need to find exactly what was said in a past conversation
|
|
1161
|
+
- Replaying a session to reconstruct context
|
|
1162
|
+
- Searching for a specific phrase, decision, or exchange that may not have been extracted into a memory
|
|
1163
|
+
- Auditing what happened in a past session
|
|
1164
|
+
|
|
1165
|
+
Search modes:
|
|
1166
|
+
- By session_id: replays a specific conversation in order
|
|
1167
|
+
- By query: full-text search across all logged content
|
|
1168
|
+
- Recent: retrieve the N most recent log entries for this project
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
- session_id (string, optional): Replay a specific session in chronological order
|
|
1172
|
+
- query (string, optional): Full-text search across all logged content
|
|
1173
|
+
- limit (number): Max entries to return (default: 20)`,
|
|
1174
|
+
inputSchema: z.object({
|
|
1175
|
+
session_id: z.string().optional().describe("Replay a specific session — returns turns in order"),
|
|
1176
|
+
query: z.string().optional().describe("Full-text search across all logged content"),
|
|
1177
|
+
limit: z.number().int().min(1).max(200).default(20).describe("Max entries to return"),
|
|
1178
|
+
}).strict().refine(d => d.session_id || d.query || true, "Provide session_id or query, or omit both for recent entries"),
|
|
1179
|
+
outputSchema: LogRecallResultSchema,
|
|
1180
|
+
annotations: {
|
|
1181
|
+
readOnlyHint: true,
|
|
1182
|
+
destructiveHint: false,
|
|
1183
|
+
idempotentHint: true,
|
|
1184
|
+
openWorldHint: false,
|
|
1185
|
+
},
|
|
1186
|
+
}, async ({ session_id, query, limit }) => {
|
|
1187
|
+
try {
|
|
1188
|
+
let entries;
|
|
1189
|
+
if (session_id) {
|
|
1190
|
+
entries = db.getLogBySession(session_id);
|
|
1191
|
+
}
|
|
1192
|
+
else if (query) {
|
|
1193
|
+
entries = db.searchLog(query, limit);
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
entries = db.getRecentLog(limit, project);
|
|
1197
|
+
}
|
|
1198
|
+
if (entries.length === 0) {
|
|
1199
|
+
const hint = session_id
|
|
1200
|
+
? `No log entries found for session "${session_id}". Log turns using memory_log first.`
|
|
1201
|
+
: query
|
|
1202
|
+
? `No log entries match "${query}".`
|
|
1203
|
+
: `No log entries yet for this project. Use memory_log to preserve conversation turns.`;
|
|
1204
|
+
return {
|
|
1205
|
+
content: [{ type: "text", text: hint }],
|
|
1206
|
+
structuredContent: {
|
|
1207
|
+
query,
|
|
1208
|
+
sessionId: session_id,
|
|
1209
|
+
total: 0,
|
|
1210
|
+
entries: [],
|
|
1211
|
+
},
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
const lines = [];
|
|
1215
|
+
if (session_id) {
|
|
1216
|
+
lines.push(`Session "${session_id}" — ${entries.length} turn${entries.length === 1 ? "" : "s"}`);
|
|
1217
|
+
lines.push("");
|
|
1218
|
+
for (const e of entries) {
|
|
1219
|
+
const roleLabel = e.role === "user" ? "▶ User" : e.role === "assistant" ? "◀ Assistant" : "⚙ System";
|
|
1220
|
+
lines.push(`[${formatAge(e.timestamp)}] ${roleLabel}`);
|
|
1221
|
+
lines.push(e.content.length > 300 ? e.content.slice(0, 300) + "…" : e.content);
|
|
1222
|
+
lines.push("");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
const header = query ? `Log search: "${query}" — ${entries.length} result${entries.length === 1 ? "" : "s"}` : `Recent log — ${entries.length} entries`;
|
|
1227
|
+
lines.push(header);
|
|
1228
|
+
lines.push("");
|
|
1229
|
+
for (const e of entries) {
|
|
1230
|
+
lines.push(`[${e.id.slice(0, 8)}] ${formatAge(e.timestamp)} | ${e.role} | session:${e.sessionId.slice(0, 8)}`);
|
|
1231
|
+
lines.push(e.content.length > 200 ? e.content.slice(0, 200) + "…" : e.content);
|
|
1232
|
+
lines.push("");
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1237
|
+
structuredContent: {
|
|
1238
|
+
query,
|
|
1239
|
+
sessionId: session_id,
|
|
1240
|
+
total: entries.length,
|
|
1241
|
+
entries: entries.slice(0, limit).map(e => ({
|
|
1242
|
+
id: e.id,
|
|
1243
|
+
role: e.role,
|
|
1244
|
+
content: e.content,
|
|
1245
|
+
timestamp: e.timestamp,
|
|
1246
|
+
age: formatAge(e.timestamp),
|
|
1247
|
+
project: e.project,
|
|
1248
|
+
})),
|
|
1249
|
+
},
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
catch (error) {
|
|
1253
|
+
return {
|
|
1254
|
+
isError: true,
|
|
1255
|
+
content: [{
|
|
1256
|
+
type: "text",
|
|
1257
|
+
text: `Error searching log: ${error instanceof Error ? error.message : String(error)}`,
|
|
1258
|
+
}],
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
});
|
|
1262
|
+
// ── memory_relate ─────────────────────────────────────────
|
|
1263
|
+
server.registerTool("memory_relate", {
|
|
1264
|
+
title: "Relate / Unrelate Memories",
|
|
1265
|
+
description: `Build a knowledge graph by explicitly linking memories with typed relationships. Or inspect all connections for a given memory.
|
|
1266
|
+
|
|
1267
|
+
Relationship types (use these or invent your own):
|
|
1268
|
+
- "supports" — this memory provides evidence for the other
|
|
1269
|
+
- "contradicts" — these memories are in tension
|
|
1270
|
+
- "depends_on" — one requires the other to make sense
|
|
1271
|
+
- "supersedes" — this memory replaces or updates the other
|
|
1272
|
+
- "related_to" — loosely related, no specific direction
|
|
1273
|
+
- "caused_by" — this memory is a consequence of the other
|
|
1274
|
+
- "implements" — this memory is a concrete implementation of a higher-level decision
|
|
1275
|
+
|
|
1276
|
+
The knowledge graph lets amem surface not just direct matches, but connected context — when you recall one memory, its graph neighbors are available too.
|
|
1277
|
+
|
|
1278
|
+
Args:
|
|
1279
|
+
- action (enum): "relate" | "unrelate" | "graph"
|
|
1280
|
+
- from_id (string): Source memory ID (required for relate/unrelate)
|
|
1281
|
+
- to_id (string): Target memory ID (required for relate)
|
|
1282
|
+
- relation_type (string): Relationship label (required for relate)
|
|
1283
|
+
- strength (number 0-1): How strong is this relationship (default: 0.8)
|
|
1284
|
+
- relation_id (string): Relation ID to remove (required for unrelate)
|
|
1285
|
+
- memory_id (string): Memory to inspect all connections for (required for graph)`,
|
|
1286
|
+
inputSchema: z.object({
|
|
1287
|
+
action: z.enum(["relate", "unrelate", "graph"]).describe("Operation to perform"),
|
|
1288
|
+
from_id: z.string().optional().describe("Source memory ID (relate)"),
|
|
1289
|
+
to_id: z.string().optional().describe("Target memory ID (relate)"),
|
|
1290
|
+
relation_type: z.string().optional().describe("Relationship type label"),
|
|
1291
|
+
strength: z.number().min(0).max(1).default(0.8).optional().describe("Relationship strength 0-1"),
|
|
1292
|
+
relation_id: z.string().optional().describe("Relation ID to remove (unrelate)"),
|
|
1293
|
+
memory_id: z.string().optional().describe("Memory ID to inspect graph connections for"),
|
|
1294
|
+
}).strict(),
|
|
1295
|
+
outputSchema: RelateResultSchema,
|
|
1296
|
+
annotations: {
|
|
1297
|
+
readOnlyHint: false,
|
|
1298
|
+
destructiveHint: false,
|
|
1299
|
+
idempotentHint: false,
|
|
1300
|
+
openWorldHint: false,
|
|
1301
|
+
},
|
|
1302
|
+
}, async ({ action, from_id, to_id, relation_type, strength, relation_id, memory_id }) => {
|
|
1303
|
+
try {
|
|
1304
|
+
if (action === "relate") {
|
|
1305
|
+
if (!from_id || !to_id || !relation_type) {
|
|
1306
|
+
return {
|
|
1307
|
+
isError: true,
|
|
1308
|
+
content: [{ type: "text", text: "relate requires from_id, to_id, and relation_type." }],
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const resolveId = (id) => {
|
|
1312
|
+
if (id.length >= 36)
|
|
1313
|
+
return id;
|
|
1314
|
+
const match = db.getAll().find(m => m.id.startsWith(id));
|
|
1315
|
+
return match?.id ?? id;
|
|
1316
|
+
};
|
|
1317
|
+
const fromFull = resolveId(from_id);
|
|
1318
|
+
const toFull = resolveId(to_id);
|
|
1319
|
+
const fromMem = db.getById(fromFull);
|
|
1320
|
+
const toMem = db.getById(toFull);
|
|
1321
|
+
if (!fromMem || !toMem) {
|
|
1322
|
+
return {
|
|
1323
|
+
isError: true,
|
|
1324
|
+
content: [{ type: "text", text: `Memory not found: ${!fromMem ? from_id : to_id}` }],
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const relId = db.addRelation(fromFull, toFull, relation_type, strength ?? 0.8);
|
|
1328
|
+
return {
|
|
1329
|
+
content: [{
|
|
1330
|
+
type: "text",
|
|
1331
|
+
text: `Linked memories:\n "${fromMem.content.slice(0, 60)}"\n ${relation_type} →\n "${toMem.content.slice(0, 60)}"\nRelation ID: ${relId.slice(0, 8)}`,
|
|
1332
|
+
}],
|
|
1333
|
+
structuredContent: {
|
|
1334
|
+
action: "related",
|
|
1335
|
+
relationId: relId,
|
|
1336
|
+
fromId: fromFull,
|
|
1337
|
+
toId: toFull,
|
|
1338
|
+
type: relation_type,
|
|
1339
|
+
strength: strength ?? 0.8,
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
if (action === "unrelate") {
|
|
1344
|
+
if (!relation_id) {
|
|
1345
|
+
return {
|
|
1346
|
+
isError: true,
|
|
1347
|
+
content: [{ type: "text", text: "unrelate requires relation_id. Use action:graph to find relation IDs." }],
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
db.removeRelation(relation_id);
|
|
1351
|
+
return {
|
|
1352
|
+
content: [{ type: "text", text: `Removed relation ${relation_id.slice(0, 8)}.` }],
|
|
1353
|
+
structuredContent: { action: "unrelated", relationId: relation_id },
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
// graph
|
|
1357
|
+
if (!memory_id) {
|
|
1358
|
+
return {
|
|
1359
|
+
isError: true,
|
|
1360
|
+
content: [{ type: "text", text: "graph requires memory_id." }],
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
const resolveId = (id) => {
|
|
1364
|
+
if (id.length >= 36)
|
|
1365
|
+
return id;
|
|
1366
|
+
const match = db.getAll().find(m => m.id.startsWith(id));
|
|
1367
|
+
return match?.id ?? id;
|
|
1368
|
+
};
|
|
1369
|
+
const fullId = resolveId(memory_id);
|
|
1370
|
+
const mem = db.getById(fullId);
|
|
1371
|
+
if (!mem) {
|
|
1372
|
+
return {
|
|
1373
|
+
isError: true,
|
|
1374
|
+
content: [{ type: "text", text: `Memory "${memory_id}" not found.` }],
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
const relations = db.getRelations(fullId);
|
|
1378
|
+
if (relations.length === 0) {
|
|
1379
|
+
return {
|
|
1380
|
+
content: [{
|
|
1381
|
+
type: "text",
|
|
1382
|
+
text: `Memory ${fullId.slice(0, 8)} has no explicit relations yet.\n\nUse action:relate to build the knowledge graph.`,
|
|
1383
|
+
}],
|
|
1384
|
+
structuredContent: { action: "graph", memoryId: fullId, relations: [] },
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const lines = [
|
|
1388
|
+
`Knowledge graph for memory ${fullId.slice(0, 8)}:`,
|
|
1389
|
+
`"${mem.content.slice(0, 80)}${mem.content.length > 80 ? "…" : ""}"`,
|
|
1390
|
+
"",
|
|
1391
|
+
];
|
|
1392
|
+
const structRelations = [];
|
|
1393
|
+
for (const r of relations) {
|
|
1394
|
+
const direction = r.fromId === fullId ? "outgoing" : "incoming";
|
|
1395
|
+
const otherId = direction === "outgoing" ? r.toId : r.fromId;
|
|
1396
|
+
const other = db.getById(otherId);
|
|
1397
|
+
const arrow = direction === "outgoing" ? `→ [${r.relationshipType}] →` : `← [${r.relationshipType}] ←`;
|
|
1398
|
+
lines.push(` ${arrow} ${other?.content.slice(0, 60) ?? otherId.slice(0, 8)} (${(r.strength * 100).toFixed(0)}% strength)`);
|
|
1399
|
+
lines.push(` relation id: ${r.id.slice(0, 8)}`);
|
|
1400
|
+
structRelations.push({
|
|
1401
|
+
relatedId: otherId,
|
|
1402
|
+
direction,
|
|
1403
|
+
type: r.relationshipType,
|
|
1404
|
+
strength: r.strength,
|
|
1405
|
+
content: other?.content,
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
1410
|
+
structuredContent: {
|
|
1411
|
+
action: "graph",
|
|
1412
|
+
memoryId: fullId,
|
|
1413
|
+
relations: structRelations,
|
|
1414
|
+
},
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
return {
|
|
1419
|
+
isError: true,
|
|
1420
|
+
content: [{
|
|
1421
|
+
type: "text",
|
|
1422
|
+
text: `Error managing relations: ${error instanceof Error ? error.message : String(error)}`,
|
|
1423
|
+
}],
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
// ── memory_since ──────────────────────────────────────────
|
|
1428
|
+
server.registerTool("memory_since", {
|
|
1429
|
+
title: "Temporal Memory Query",
|
|
1430
|
+
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.
|
|
1431
|
+
|
|
1432
|
+
Natural language time expressions supported:
|
|
1433
|
+
- "5m", "30m" — minutes ago
|
|
1434
|
+
- "1h", "2h", "6h" — hours ago
|
|
1435
|
+
- "1d", "7d", "30d" — days ago
|
|
1436
|
+
- "1w", "2w" — weeks ago
|
|
1437
|
+
- "1mo", "3mo" — months ago
|
|
1438
|
+
- ISO 8601 timestamp — exact time (e.g. "2025-01-15T10:00:00Z")
|
|
1439
|
+
- Unix millisecond timestamp
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
- since (string): How far back to look — "7d", "1w", "2025-01-15", etc.
|
|
1443
|
+
- until (string, optional): End of time window — same format. Defaults to now.
|
|
1444
|
+
- type (enum, optional): Filter by memory type within this window`,
|
|
1445
|
+
inputSchema: z.object({
|
|
1446
|
+
since: z.string().min(1).describe("Start of time window — '7d', '2w', '1h', or ISO timestamp"),
|
|
1447
|
+
until: z.string().optional().describe("End of time window — defaults to now"),
|
|
1448
|
+
type: z.enum(TYPE_ORDER).optional().describe("Filter by memory type"),
|
|
1449
|
+
}).strict(),
|
|
1450
|
+
outputSchema: TemporalResultSchema,
|
|
1451
|
+
annotations: {
|
|
1452
|
+
readOnlyHint: true,
|
|
1453
|
+
destructiveHint: false,
|
|
1454
|
+
idempotentHint: true,
|
|
1455
|
+
openWorldHint: false,
|
|
1456
|
+
},
|
|
1457
|
+
}, async ({ since, until, type }) => {
|
|
1458
|
+
try {
|
|
1459
|
+
const parseTime = (s) => {
|
|
1460
|
+
const now = Date.now();
|
|
1461
|
+
const match = s.match(/^(\d+)(m|min|h|d|w|mo)$/i);
|
|
1462
|
+
if (match) {
|
|
1463
|
+
const n = parseInt(match[1], 10);
|
|
1464
|
+
const unit = match[2].toLowerCase();
|
|
1465
|
+
const ms = { m: 60000, min: 60000, h: 3600000, d: 86400000, w: 604800000, mo: 2592000000 };
|
|
1466
|
+
return now - n * (ms[unit] ?? 86400000);
|
|
1467
|
+
}
|
|
1468
|
+
const parsed = Date.parse(s);
|
|
1469
|
+
if (!isNaN(parsed))
|
|
1470
|
+
return parsed;
|
|
1471
|
+
const num = Number(s);
|
|
1472
|
+
if (!isNaN(num))
|
|
1473
|
+
return num;
|
|
1474
|
+
throw new Error(`Cannot parse time expression: "${s}". Use formats like "7d", "2w", "1h", or an ISO date.`);
|
|
1475
|
+
};
|
|
1476
|
+
const fromTs = parseTime(since);
|
|
1477
|
+
const toTs = until ? parseTime(until) : Date.now();
|
|
1478
|
+
let memories = db.getMemoriesByDateRange(fromTs, toTs);
|
|
1479
|
+
if (type)
|
|
1480
|
+
memories = memories.filter(m => m.type === type);
|
|
1481
|
+
if (memories.length === 0) {
|
|
1482
|
+
return {
|
|
1483
|
+
content: [{
|
|
1484
|
+
type: "text",
|
|
1485
|
+
text: `No memories found between ${new Date(fromTs).toISOString().slice(0, 10)} and ${new Date(toTs).toISOString().slice(0, 10)}${type ? ` of type "${type}"` : ""}.`,
|
|
1486
|
+
}],
|
|
1487
|
+
structuredContent: {
|
|
1488
|
+
from: new Date(fromTs).toISOString(),
|
|
1489
|
+
to: new Date(toTs).toISOString(),
|
|
1490
|
+
total: 0,
|
|
1491
|
+
memories: [],
|
|
1492
|
+
},
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const lines = [
|
|
1496
|
+
`Memories from ${new Date(fromTs).toISOString().slice(0, 10)} → ${new Date(toTs).toISOString().slice(0, 10)}`,
|
|
1497
|
+
type ? `Type filter: ${type}` : `All types`,
|
|
1498
|
+
`Found: ${memories.length}`,
|
|
1499
|
+
"",
|
|
1500
|
+
];
|
|
1501
|
+
for (const m of memories) {
|
|
1502
|
+
lines.push(`[${m.type}] ${m.content.slice(0, 80)}${m.content.length > 80 ? "…" : ""}`);
|
|
1503
|
+
lines.push(` Created: ${formatAge(m.createdAt)} | Confidence: ${(m.confidence * 100).toFixed(0)}% | ID: ${m.id.slice(0, 8)}`);
|
|
1504
|
+
if (m.tags.length > 0)
|
|
1505
|
+
lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
1506
|
+
lines.push("");
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1510
|
+
structuredContent: {
|
|
1511
|
+
from: new Date(fromTs).toISOString(),
|
|
1512
|
+
to: new Date(toTs).toISOString(),
|
|
1513
|
+
total: memories.length,
|
|
1514
|
+
memories: memories.map(m => ({
|
|
1515
|
+
id: m.id,
|
|
1516
|
+
content: m.content,
|
|
1517
|
+
type: m.type,
|
|
1518
|
+
confidence: m.confidence,
|
|
1519
|
+
createdAt: m.createdAt,
|
|
1520
|
+
age: formatAge(m.createdAt),
|
|
1521
|
+
tags: m.tags,
|
|
1522
|
+
})),
|
|
1523
|
+
},
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
catch (error) {
|
|
1527
|
+
return {
|
|
1528
|
+
isError: true,
|
|
1529
|
+
content: [{
|
|
1530
|
+
type: "text",
|
|
1531
|
+
text: `Error in temporal query: ${error instanceof Error ? error.message : String(error)}`,
|
|
1532
|
+
}],
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
// ── memory_search ─────────────────────────────────────────
|
|
1537
|
+
server.registerTool("memory_search", {
|
|
1538
|
+
title: "Full-Text Memory Search",
|
|
1539
|
+
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.
|
|
1540
|
+
|
|
1541
|
+
Use this when:
|
|
1542
|
+
- You need exact phrase matching ("never use any" not just "TypeScript types")
|
|
1543
|
+
- Searching for a specific function name, file path, or technical term
|
|
1544
|
+
- memory_recall returns too many loosely-related results
|
|
1545
|
+
- You want to find all memories mentioning a specific tool, library, or concept
|
|
1546
|
+
|
|
1547
|
+
Supports FTS5 query syntax:
|
|
1548
|
+
- Simple terms: "postgres"
|
|
1549
|
+
- Phrase search: '"event sourcing"'
|
|
1550
|
+
- Prefix search: "auth*"
|
|
1551
|
+
- Boolean: "postgres OR sqlite"
|
|
1552
|
+
- Negation: "database NOT redis"
|
|
1553
|
+
|
|
1554
|
+
Args:
|
|
1555
|
+
- query (string): Full-text search query — exact terms, phrases, or FTS5 syntax
|
|
1556
|
+
- limit (number): Max results (default: 20)`,
|
|
1557
|
+
inputSchema: z.object({
|
|
1558
|
+
query: z.string().min(1).describe("Full-text search query — exact terms, phrases, or FTS5 syntax"),
|
|
1559
|
+
limit: z.number().int().min(1).max(100).default(20).describe("Max results to return"),
|
|
1560
|
+
}).strict(),
|
|
1561
|
+
outputSchema: RecallResultSchema,
|
|
1562
|
+
annotations: {
|
|
1563
|
+
readOnlyHint: true,
|
|
1564
|
+
destructiveHint: false,
|
|
1565
|
+
idempotentHint: true,
|
|
1566
|
+
openWorldHint: false,
|
|
1567
|
+
},
|
|
1568
|
+
}, async ({ query, limit }) => {
|
|
1569
|
+
try {
|
|
1570
|
+
const results = db.fullTextSearch(query, limit, project);
|
|
1571
|
+
if (results.length === 0) {
|
|
1572
|
+
return {
|
|
1573
|
+
content: [{ type: "text", text: `No memories found matching "${query}". Try memory_recall for semantic/fuzzy search.` }],
|
|
1574
|
+
structuredContent: { query, total: 0, memories: [] },
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const lines = [`Full-text search: "${query}" — ${results.length} result${results.length === 1 ? "" : "s"}`, ""];
|
|
1578
|
+
for (const m of results) {
|
|
1579
|
+
lines.push(`[${m.type}] ${m.content}`);
|
|
1580
|
+
lines.push(` ID: ${m.id.slice(0, 8)} | Confidence: ${(m.confidence * 100).toFixed(0)}% | ${formatAge(m.lastAccessed)}`);
|
|
1581
|
+
if (m.tags.length > 0)
|
|
1582
|
+
lines.push(` Tags: ${m.tags.join(", ")}`);
|
|
1583
|
+
lines.push("");
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
content: [{ type: "text", text: lines.join("\n").trim() }],
|
|
1587
|
+
structuredContent: {
|
|
1588
|
+
query,
|
|
1589
|
+
total: results.length,
|
|
1590
|
+
memories: results.map(m => ({
|
|
1591
|
+
id: m.id,
|
|
1592
|
+
content: m.content,
|
|
1593
|
+
type: m.type,
|
|
1594
|
+
score: 1.0,
|
|
1595
|
+
confidence: m.confidence,
|
|
1596
|
+
tags: m.tags,
|
|
1597
|
+
age: formatAge(m.lastAccessed),
|
|
1598
|
+
})),
|
|
1599
|
+
},
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
catch (error) {
|
|
1603
|
+
return {
|
|
1604
|
+
isError: true,
|
|
1605
|
+
content: [{
|
|
1606
|
+
type: "text",
|
|
1607
|
+
text: `Error in full-text search: ${error instanceof Error ? error.message : String(error)}`,
|
|
1608
|
+
}],
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
840
1612
|
}
|
|
841
1613
|
//# sourceMappingURL=tools.js.map
|