@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/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: true,
168
+ readOnlyHint: false,
169
169
  destructiveHint: false,
170
- idempotentHint: true,
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: true,
251
+ readOnlyHint: false,
247
252
  destructiveHint: false,
248
- idempotentHint: true,
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.slice(0, 8), content: m.content })),
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 existing = db.getAllWithEmbeddings();
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
- const all = db.getAll();
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.getAll();
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: true,
724
+ readOnlyHint: false,
696
725
  destructiveHint: false,
697
- idempotentHint: true,
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