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