@raviolelabs/engram-mcp 0.4.5 → 0.5.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.
Files changed (205) hide show
  1. package/LICENSE +92 -18
  2. package/README.md +13 -5
  3. package/dist/cloud/auth.d.ts.map +1 -1
  4. package/dist/cloud/auth.js +3 -1
  5. package/dist/cloud/auth.js.map +1 -1
  6. package/dist/cloud/bridge-client.d.ts.map +1 -1
  7. package/dist/cloud/bridge-client.js +43 -2
  8. package/dist/cloud/bridge-client.js.map +1 -1
  9. package/dist/cloud/crypto.d.ts.map +1 -1
  10. package/dist/cloud/crypto.js +1 -3
  11. package/dist/cloud/crypto.js.map +1 -1
  12. package/dist/cloud/pairing.d.ts.map +1 -1
  13. package/dist/cloud/pairing.js.map +1 -1
  14. package/dist/cloud/transit-poller.d.ts.map +1 -1
  15. package/dist/cloud/transit-poller.js.map +1 -1
  16. package/dist/core/db/index.d.ts.map +1 -1
  17. package/dist/core/db/index.js +25 -1
  18. package/dist/core/db/index.js.map +1 -1
  19. package/dist/core/logger.d.ts.map +1 -1
  20. package/dist/core/logger.js.map +1 -1
  21. package/dist/core/security/uri-validator.d.ts.map +1 -1
  22. package/dist/core/security/uri-validator.js.map +1 -1
  23. package/dist/core/server/http.d.ts.map +1 -1
  24. package/dist/core/server/http.js +4 -2
  25. package/dist/core/server/http.js.map +1 -1
  26. package/dist/core/server/mcp-handler.d.ts.map +1 -1
  27. package/dist/core/server/mcp-handler.js +5 -5
  28. package/dist/core/server/mcp-handler.js.map +1 -1
  29. package/dist/core/server/mcp-http.d.ts.map +1 -1
  30. package/dist/core/server/mcp-http.js +1 -1
  31. package/dist/core/server/mcp-http.js.map +1 -1
  32. package/dist/core/server/websocket.d.ts.map +1 -1
  33. package/dist/core/server/websocket.js.map +1 -1
  34. package/dist/embeddings/index.js.map +1 -1
  35. package/dist/embeddings/providers/engram.d.ts.map +1 -1
  36. package/dist/embeddings/providers/engram.js +3 -1
  37. package/dist/embeddings/providers/engram.js.map +1 -1
  38. package/dist/ingest/jobs.d.ts.map +1 -1
  39. package/dist/ingest/jobs.js +2 -6
  40. package/dist/ingest/jobs.js.map +1 -1
  41. package/dist/mcp-server/tests/mcp-e2e.test.js.map +1 -1
  42. package/dist/memory/admin/tools.d.ts.map +1 -1
  43. package/dist/memory/admin/tools.js +4 -1
  44. package/dist/memory/admin/tools.js.map +1 -1
  45. package/dist/memory/core/signals.d.ts +71 -0
  46. package/dist/memory/core/signals.d.ts.map +1 -0
  47. package/dist/memory/core/signals.js +121 -0
  48. package/dist/memory/core/signals.js.map +1 -0
  49. package/dist/memory/core/source-registry.d.ts.map +1 -1
  50. package/dist/memory/core/source-registry.js.map +1 -1
  51. package/dist/memory/core/store.d.ts.map +1 -1
  52. package/dist/memory/core/store.js +58 -12
  53. package/dist/memory/core/store.js.map +1 -1
  54. package/dist/memory/modules/_custom/generic-module.d.ts.map +1 -1
  55. package/dist/memory/modules/_custom/generic-module.js.map +1 -1
  56. package/dist/memory/modules/_custom/tests/custom-types.test.js +9 -2
  57. package/dist/memory/modules/_custom/tests/custom-types.test.js.map +1 -1
  58. package/dist/memory/modules/_custom/tools.d.ts.map +1 -1
  59. package/dist/memory/modules/_custom/tools.js +3 -1
  60. package/dist/memory/modules/_custom/tools.js.map +1 -1
  61. package/dist/memory/modules/audio/tests/audio.test.js +12 -3
  62. package/dist/memory/modules/audio/tests/audio.test.js.map +1 -1
  63. package/dist/memory/modules/audio/tests/transcriber.test.js.map +1 -1
  64. package/dist/memory/modules/audio/tools.d.ts.map +1 -1
  65. package/dist/memory/modules/audio/tools.js +9 -2
  66. package/dist/memory/modules/audio/tools.js.map +1 -1
  67. package/dist/memory/modules/audio/transcriber.d.ts.map +1 -1
  68. package/dist/memory/modules/audio/transcriber.js +4 -2
  69. package/dist/memory/modules/audio/transcriber.js.map +1 -1
  70. package/dist/memory/modules/conversations/tests/conversations.test.js +6 -1
  71. package/dist/memory/modules/conversations/tests/conversations.test.js.map +1 -1
  72. package/dist/memory/modules/conversations/tools.d.ts.map +1 -1
  73. package/dist/memory/modules/conversations/tools.js +2 -2
  74. package/dist/memory/modules/conversations/tools.js.map +1 -1
  75. package/dist/memory/modules/drive/connector.d.ts.map +1 -1
  76. package/dist/memory/modules/drive/connector.js.map +1 -1
  77. package/dist/memory/modules/drive/oauth.d.ts.map +1 -1
  78. package/dist/memory/modules/drive/oauth.js.map +1 -1
  79. package/dist/memory/modules/drive/tools.d.ts.map +1 -1
  80. package/dist/memory/modules/drive/tools.js.map +1 -1
  81. package/dist/memory/modules/notes/tests/notes.test.js +6 -1
  82. package/dist/memory/modules/notes/tests/notes.test.js.map +1 -1
  83. package/dist/memory/modules/notes/tools.d.ts.map +1 -1
  84. package/dist/memory/modules/notes/tools.js +1 -1
  85. package/dist/memory/modules/notes/tools.js.map +1 -1
  86. package/dist/memory/modules/notion/connector.d.ts.map +1 -1
  87. package/dist/memory/modules/notion/connector.js +16 -8
  88. package/dist/memory/modules/notion/connector.js.map +1 -1
  89. package/dist/memory/modules/notion/oauth.d.ts.map +1 -1
  90. package/dist/memory/modules/notion/oauth.js.map +1 -1
  91. package/dist/memory/modules/notion/tools.d.ts.map +1 -1
  92. package/dist/memory/modules/notion/tools.js +1 -1
  93. package/dist/memory/modules/notion/tools.js.map +1 -1
  94. package/dist/memory/modules/obsidian/tests/obsidian.test.js +9 -2
  95. package/dist/memory/modules/obsidian/tests/obsidian.test.js.map +1 -1
  96. package/dist/memory/modules/obsidian/tools.d.ts.map +1 -1
  97. package/dist/memory/modules/obsidian/tools.js.map +1 -1
  98. package/dist/memory/modules/obsidian/vault-reader.js.map +1 -1
  99. package/dist/memory/modules/team/keystore.d.ts.map +1 -1
  100. package/dist/memory/modules/team/keystore.js +4 -1
  101. package/dist/memory/modules/team/keystore.js.map +1 -1
  102. package/dist/memory/modules/team/tools.d.ts.map +1 -1
  103. package/dist/memory/modules/team/tools.js +19 -13
  104. package/dist/memory/modules/team/tools.js.map +1 -1
  105. package/dist/memory/modules/youtube/tests/channel.test.js.map +1 -1
  106. package/dist/memory/modules/youtube/tests/transcript-fetcher.test.js.map +1 -1
  107. package/dist/memory/modules/youtube/tests/youtube.test.js +12 -3
  108. package/dist/memory/modules/youtube/tests/youtube.test.js.map +1 -1
  109. package/dist/memory/modules/youtube/tools.d.ts.map +1 -1
  110. package/dist/memory/modules/youtube/tools.js +14 -9
  111. package/dist/memory/modules/youtube/tools.js.map +1 -1
  112. package/dist/memory/modules/youtube/transcript-fetcher.js +4 -2
  113. package/dist/memory/modules/youtube/transcript-fetcher.js.map +1 -1
  114. package/dist/memory/modules/youtube/watcher.js +3 -1
  115. package/dist/memory/modules/youtube/watcher.js.map +1 -1
  116. package/dist/memory/public/tools.d.ts +1 -0
  117. package/dist/memory/public/tools.d.ts.map +1 -1
  118. package/dist/memory/public/tools.js +291 -28
  119. package/dist/memory/public/tools.js.map +1 -1
  120. package/dist/scripts/install.js +3 -1
  121. package/dist/scripts/install.js.map +1 -1
  122. package/dist/scripts/pair.js +2 -2
  123. package/dist/scripts/pair.js.map +1 -1
  124. package/dist/scripts/serve.js.map +1 -1
  125. package/dist/scripts/service.js.map +1 -1
  126. package/dist/server/api/daily.d.ts.map +1 -1
  127. package/dist/server/api/daily.js +3 -1
  128. package/dist/server/api/daily.js.map +1 -1
  129. package/dist/server/api/integrations.d.ts.map +1 -1
  130. package/dist/server/api/integrations.js +6 -2
  131. package/dist/server/api/integrations.js.map +1 -1
  132. package/dist/server/api/memories.d.ts.map +1 -1
  133. package/dist/server/api/memories.js +40 -5
  134. package/dist/server/api/memories.js.map +1 -1
  135. package/dist/server/api/settings.d.ts.map +1 -1
  136. package/dist/server/api/settings.js +1 -3
  137. package/dist/server/api/settings.js.map +1 -1
  138. package/dist/server/api/sources.d.ts.map +1 -1
  139. package/dist/server/api/sources.js.map +1 -1
  140. package/dist/server/api/sync-status.d.ts.map +1 -1
  141. package/dist/server/api/sync-status.js +1 -3
  142. package/dist/server/api/sync-status.js.map +1 -1
  143. package/dist/server/api/types.d.ts.map +1 -1
  144. package/dist/server/api/types.js.map +1 -1
  145. package/dist/server/api/version.d.ts.map +1 -1
  146. package/dist/server/api/version.js +6 -2
  147. package/dist/server/api/version.js.map +1 -1
  148. package/dist/sync/apply.d.ts.map +1 -1
  149. package/dist/sync/apply.js.map +1 -1
  150. package/dist/sync/cloud-saves.d.ts.map +1 -1
  151. package/dist/sync/cloud-saves.js +1 -3
  152. package/dist/sync/cloud-saves.js.map +1 -1
  153. package/dist/sync/ed25519.d.ts.map +1 -1
  154. package/dist/sync/ed25519.js +3 -8
  155. package/dist/sync/ed25519.js.map +1 -1
  156. package/dist/sync/recovery-setup.d.ts.map +1 -1
  157. package/dist/sync/recovery-setup.js.map +1 -1
  158. package/dist/sync/shamir.d.ts.map +1 -1
  159. package/dist/sync/shamir.js.map +1 -1
  160. package/dist/sync/team-sync.d.ts.map +1 -1
  161. package/dist/sync/team-sync.js +2 -2
  162. package/dist/sync/team-sync.js.map +1 -1
  163. package/dist/sync/tests/apply.test.d.ts.map +1 -1
  164. package/dist/sync/tests/apply.test.js +1 -3
  165. package/dist/sync/tests/apply.test.js.map +1 -1
  166. package/dist/sync/tests/two-device-sync.test.js +8 -8
  167. package/dist/sync/tests/two-device-sync.test.js.map +1 -1
  168. package/dist/tests/cloud-integration.test.js +1 -1
  169. package/dist/tests/cloud-integration.test.js.map +1 -1
  170. package/dist/tests/cloud-saves-integration.test.js +1 -1
  171. package/dist/tests/cloud-saves-integration.test.js.map +1 -1
  172. package/dist/tests/cloud-transit.test.js +1 -1
  173. package/dist/tests/cloud-transit.test.js.map +1 -1
  174. package/dist/tests/db.test.js +1 -3
  175. package/dist/tests/db.test.js.map +1 -1
  176. package/dist/tests/memory-store.test.js.map +1 -1
  177. package/dist/tests/module-registry.test.js +4 -4
  178. package/dist/tests/module-registry.test.js.map +1 -1
  179. package/dist/tests/public-tools.test.js +24 -8
  180. package/dist/tests/public-tools.test.js.map +1 -1
  181. package/dist/tests/scope-encryption.test.js.map +1 -1
  182. package/dist/tools/index.js +1 -1
  183. package/dist/tools/index.js.map +1 -1
  184. package/dist/vector/store.d.ts.map +1 -1
  185. package/dist/vector/store.js +1 -4
  186. package/dist/vector/store.js.map +1 -1
  187. package/dist/webapp/api/daily.d.ts.map +1 -1
  188. package/dist/webapp/api/daily.js +3 -1
  189. package/dist/webapp/api/daily.js.map +1 -1
  190. package/dist/webapp/api/memories.d.ts.map +1 -1
  191. package/dist/webapp/api/memories.js +4 -1
  192. package/dist/webapp/api/memories.js.map +1 -1
  193. package/dist/webapp/api/settings.d.ts.map +1 -1
  194. package/dist/webapp/api/settings.js +1 -3
  195. package/dist/webapp/api/settings.js.map +1 -1
  196. package/dist/webapp/api/sources.d.ts.map +1 -1
  197. package/dist/webapp/api/sources.js.map +1 -1
  198. package/dist/webapp/api/sync-status.d.ts.map +1 -1
  199. package/dist/webapp/api/sync-status.js +1 -3
  200. package/dist/webapp/api/sync-status.js.map +1 -1
  201. package/dist/webapp/api/types.d.ts.map +1 -1
  202. package/dist/webapp/api/types.js +2 -1
  203. package/dist/webapp/api/types.js.map +1 -1
  204. package/dist/webapp/tests/mcp-http.test.js.map +1 -1
  205. package/package.json +19 -14
@@ -8,12 +8,12 @@ import { extractWikilinks } from '../core/wikilinks.js';
8
8
  const log = createLogger('public-tools');
9
9
  // ── Per-type weights for OSS recall calibration ───────────────────────────────
10
10
  const TYPE_WEIGHTS = {
11
- notes: 1.00, // user-curated, high signal
11
+ notes: 1.0, // user-curated, high signal
12
12
  conversations: 0.95, // recent, dialog context
13
- drive: 0.90, // structured documents
14
- notion: 0.90,
13
+ drive: 0.9, // structured documents
14
+ notion: 0.9,
15
15
  obsidian: 0.95, // user notes, high signal
16
- audio: 0.80, // transcripts can be noisy
16
+ audio: 0.8, // transcripts can be noisy
17
17
  youtube: 0.75, // longer, lower signal density
18
18
  };
19
19
  // Recency boost (exp decay, half-life ~180 days)
@@ -87,8 +87,7 @@ export function buildPublicTools(store, config) {
87
87
  }
88
88
  const contentBytes = Buffer.byteLength(content, 'utf-8');
89
89
  if (contentBytes > MAX_CONTENT_BYTES) {
90
- throw new Error(`content too large (${Math.round(contentBytes / 1024)} KB) — max ${MAX_CONTENT_BYTES / 1024} KB. ` +
91
- `Split into multiple remember() calls or use ingest() for large files.`);
90
+ throw new Error(`content too large (${Math.round(contentBytes / 1024)} KB) — max ${MAX_CONTENT_BYTES / 1024} KB. ` + `Split into multiple remember() calls or use ingest() for large files.`);
92
91
  }
93
92
  const contentHash = createHash('sha256').update(content).digest('hex');
94
93
  // Idempotency: check for existing memory with same content_hash + type
@@ -352,7 +351,7 @@ export function buildPublicTools(store, config) {
352
351
  'WHEN: "what else is connected to this?", building a memory graph, or exploring a topic cluster.',
353
352
  'Use recall instead of relate when starting from a query string.',
354
353
  'ANTI-LOOP: if returns empty, this memory has no semantic neighbors above the threshold — DO NOT retry.',
355
- 'Try recall with the memory\'s tags instead.',
354
+ "Try recall with the memory's tags instead.",
356
355
  'RETURNS: array of { id, type, score, snippet, title }.',
357
356
  ].join(' '),
358
357
  inputSchema: {
@@ -670,7 +669,12 @@ export function buildPublicTools(store, config) {
670
669
  config: { mimeType: meta.mimeType },
671
670
  });
672
671
  if (driveAlreadyExists) {
673
- return { watched: true, already_watching: true, source_id: sourceId, display_name: meta.name };
672
+ return {
673
+ watched: true,
674
+ already_watching: true,
675
+ source_id: sourceId,
676
+ display_name: meta.name,
677
+ };
674
678
  }
675
679
  const content = await downloadFileContent(targetId, meta.mimeType, config);
676
680
  if (content) {
@@ -690,7 +694,12 @@ export function buildPublicTools(store, config) {
690
694
  display_name: meta.title,
691
695
  });
692
696
  if (notionAlreadyExists) {
693
- return { watched: true, already_watching: true, source_id: sourceId, display_name: meta.title };
697
+ return {
698
+ watched: true,
699
+ already_watching: true,
700
+ source_id: sourceId,
701
+ display_name: meta.title,
702
+ };
694
703
  }
695
704
  const content = await fetchPageText(meta.id);
696
705
  const item = buildNotionItem({ metadata: meta, content, embeddingModel });
@@ -710,7 +719,12 @@ export function buildPublicTools(store, config) {
710
719
  });
711
720
  if (ytAlreadyExists) {
712
721
  const existingSource = sourceRegistry.get(sourceId);
713
- return { watched: true, already_watching: true, source_id: sourceId, display_name: existingSource?.display_name ?? channelName };
722
+ return {
723
+ watched: true,
724
+ already_watching: true,
725
+ source_id: sourceId,
726
+ display_name: existingSource?.display_name ?? channelName,
727
+ };
714
728
  }
715
729
  return { watched: true, source_id: sourceId, display_name: channelName };
716
730
  }
@@ -726,7 +740,12 @@ export function buildPublicTools(store, config) {
726
740
  config: { vault_path: vaultPath },
727
741
  });
728
742
  if (obsidianAlreadyExists) {
729
- return { watched: true, already_watching: true, source_id: sourceId, display_name: path.default.basename(vaultPath) };
743
+ return {
744
+ watched: true,
745
+ already_watching: true,
746
+ source_id: sourceId,
747
+ display_name: path.default.basename(vaultPath),
748
+ };
730
749
  }
731
750
  const files = await readVault(vaultPath);
732
751
  for (const file of files) {
@@ -735,7 +754,12 @@ export function buildPublicTools(store, config) {
735
754
  await store.insert(item);
736
755
  }
737
756
  sourceRegistry.recordSync(sourceId, new Date().toISOString());
738
- return { watched: true, source_id: sourceId, display_name: path.default.basename(vaultPath), files_indexed: files.length };
757
+ return {
758
+ watched: true,
759
+ source_id: sourceId,
760
+ display_name: path.default.basename(vaultPath),
761
+ files_indexed: files.length,
762
+ };
739
763
  }
740
764
  default:
741
765
  throw new Error(`Unknown source_type: ${sourceType}`);
@@ -903,7 +927,11 @@ export function buildPublicTools(store, config) {
903
927
  flow.waitForCallback.then(() => ({ connected: true })),
904
928
  new Promise((resolve) => setTimeout(() => resolve({ timeout: true }), 300_000)),
905
929
  ]);
906
- return { auth_url: flow.authUrl, instructions: 'Open auth_url in your browser and authorize. Then confirm here.', ...result };
930
+ return {
931
+ auth_url: flow.authUrl,
932
+ instructions: 'Open auth_url in your browser and authorize. Then confirm here.',
933
+ ...result,
934
+ };
907
935
  }
908
936
  catch (e) {
909
937
  const msg = e instanceof Error ? e.message : String(e);
@@ -929,8 +957,15 @@ export function buildPublicTools(store, config) {
929
957
  inputSchema: {
930
958
  type: 'object',
931
959
  properties: {
932
- query: { type: 'string', description: 'Google Drive search query (e.g. "name contains \'report\'".' },
933
- limit: { type: 'number', default: 25, description: 'Max files to return (default 25, max 100).' },
960
+ query: {
961
+ type: 'string',
962
+ description: 'Google Drive search query (e.g. "name contains \'report\'".',
963
+ },
964
+ limit: {
965
+ type: 'number',
966
+ default: 25,
967
+ description: 'Max files to return (default 25, max 100).',
968
+ },
934
969
  },
935
970
  },
936
971
  handler: async (args) => {
@@ -994,10 +1029,17 @@ export function buildPublicTools(store, config) {
994
1029
  try {
995
1030
  const flow = await startNotionOAuthFlow(config);
996
1031
  const result = await Promise.race([
997
- flow.waitForCallback.then((t) => ({ connected: true, workspace: t.workspace_name })),
1032
+ flow.waitForCallback.then((t) => ({
1033
+ connected: true,
1034
+ workspace: t.workspace_name,
1035
+ })),
998
1036
  new Promise((resolve) => setTimeout(() => resolve({ timeout: true }), 300_000)),
999
1037
  ]);
1000
- return { auth_url: flow.authUrl, instructions: 'Open auth_url in your browser and authorize. Then confirm here.', ...result };
1038
+ return {
1039
+ auth_url: flow.authUrl,
1040
+ instructions: 'Open auth_url in your browser and authorize. Then confirm here.',
1041
+ ...result,
1042
+ };
1001
1043
  }
1002
1044
  catch (e) {
1003
1045
  const msg = e instanceof Error ? e.message : String(e);
@@ -1031,7 +1073,11 @@ export function buildPublicTools(store, config) {
1031
1073
  const { searchPages } = await import('../modules/notion/connector.js');
1032
1074
  const { isNotionConnected } = await import('../modules/notion/oauth.js');
1033
1075
  if (!isNotionConnected())
1034
- return { error: 'notion_not_connected', message: 'Notion is not connected.', hint: 'Call connect_notion first to authenticate with Notion.' };
1076
+ return {
1077
+ error: 'notion_not_connected',
1078
+ message: 'Notion is not connected.',
1079
+ hint: 'Call connect_notion first to authenticate with Notion.',
1080
+ };
1035
1081
  return await searchPages(args.query ?? '', args.limit ?? 25);
1036
1082
  },
1037
1083
  },
@@ -1041,7 +1087,7 @@ export function buildPublicTools(store, config) {
1041
1087
  description: [
1042
1088
  'Bulk-import a YouTube playlist (public URL). Imports all videos as memory items.',
1043
1089
  'SLOW: can take 1-30 minutes depending on playlist size.',
1044
- 'ANTI-LOOP: DO NOT call twice for the same playlist — duplicates are deduped but the API hammering wastes the user\'s YouTube quota.',
1090
+ "ANTI-LOOP: DO NOT call twice for the same playlist — duplicates are deduped but the API hammering wastes the user's YouTube quota.",
1045
1091
  'For individual YouTube videos, use ingest(youtube_url) instead.',
1046
1092
  'Poll get_ingest_status(job_id) to track progress.',
1047
1093
  'RETURNS: { job_id?, status, imported?, errors? } — large playlists run as async job.',
@@ -1118,7 +1164,10 @@ export function buildPublicTools(store, config) {
1118
1164
  return [];
1119
1165
  }
1120
1166
  }));
1121
- let candidates = allResults.flat().sort((a, b) => b.score - a.score).slice(0, limit);
1167
+ let candidates = allResults
1168
+ .flat()
1169
+ .sort((a, b) => b.score - a.score)
1170
+ .slice(0, limit);
1122
1171
  // Apply lookback filter if specified
1123
1172
  if (lookbackDays !== undefined) {
1124
1173
  const cutoff = Date.now() - lookbackDays * 24 * 60 * 60 * 1000;
@@ -1326,7 +1375,10 @@ export function buildPublicTools(store, config) {
1326
1375
  return [];
1327
1376
  }
1328
1377
  }));
1329
- let candidates = allResults.flat().sort((a, b) => b.score - a.score).slice(0, 50);
1378
+ let candidates = allResults
1379
+ .flat()
1380
+ .sort((a, b) => b.score - a.score)
1381
+ .slice(0, 50);
1330
1382
  // Apply lookback filter
1331
1383
  candidates = candidates.filter((c) => Date.parse(c.memory.properties.created_at) >= cutoff);
1332
1384
  if (candidates.length === 0) {
@@ -1438,6 +1490,206 @@ export function buildPublicTools(store, config) {
1438
1490
  return { deleted: typeName };
1439
1491
  },
1440
1492
  },
1493
+ // ── skip / unskip / pin / unpin / set_importance ─────────────────────────
1494
+ // Recall-signal tools. Cheap writes to the memories table — no embedding,
1495
+ // no chunking, no vector index touch. Used by users (via dashboard) and by
1496
+ // agents to teach the system what to surface vs hide vs preserve forever.
1497
+ {
1498
+ name: 'skip',
1499
+ description: [
1500
+ 'Mark a memory as "not useful right now" — multiplies its skip_penalty by 0.2.',
1501
+ "WHEN: a recall result is genuinely irrelevant and the agent shouldn't surface it again on similar queries.",
1502
+ 'NOT for deletion — the memory stays in storage and unskip() restores full rank.',
1503
+ 'IDEMPOTENT: calling twice multiplies penalty again (0.2 → 0.04). Use sparingly.',
1504
+ 'RETURNS: { id, skip_penalty: <new value> }.',
1505
+ ].join(' '),
1506
+ inputSchema: {
1507
+ type: 'object',
1508
+ properties: { id: { type: 'string' } },
1509
+ required: ['id'],
1510
+ },
1511
+ handler: async (args) => {
1512
+ const id = args.id;
1513
+ const { getDb } = await import('../../db/index.js');
1514
+ const db = getDb();
1515
+ const result = db
1516
+ .prepare(`UPDATE memories SET skip_penalty = MAX(0.001, skip_penalty * 0.2) WHERE id = ?`)
1517
+ .run(id);
1518
+ if (result.changes === 0)
1519
+ return { error: 'not_found', id };
1520
+ const row = db.prepare(`SELECT skip_penalty FROM memories WHERE id = ?`).get(id);
1521
+ return { id, skip_penalty: row?.skip_penalty };
1522
+ },
1523
+ },
1524
+ {
1525
+ name: 'unskip',
1526
+ description: [
1527
+ 'Restore a previously skipped memory to full recall rank (skip_penalty = 1.0).',
1528
+ 'WHEN: the user corrects a wrongly skipped item, or you realise a "not useful" call was a mistake.',
1529
+ 'IDEMPOTENT: returns success even if the memory was never skipped.',
1530
+ 'RETURNS: { id, skip_penalty: 1.0 }.',
1531
+ ].join(' '),
1532
+ inputSchema: {
1533
+ type: 'object',
1534
+ properties: { id: { type: 'string' } },
1535
+ required: ['id'],
1536
+ },
1537
+ handler: async (args) => {
1538
+ const id = args.id;
1539
+ const { getDb } = await import('../../db/index.js');
1540
+ const r = getDb().prepare(`UPDATE memories SET skip_penalty = 1.0 WHERE id = ?`).run(id);
1541
+ if (r.changes === 0)
1542
+ return { error: 'not_found', id };
1543
+ return { id, skip_penalty: 1.0 };
1544
+ },
1545
+ },
1546
+ {
1547
+ name: 'pin',
1548
+ description: [
1549
+ 'Pin a memory — exempts it from time-decay forever (until unpinned).',
1550
+ 'WHEN: critical preference, identity fact, or a piece of context that must never fade.',
1551
+ 'Pinned memories ignore the importance × half-life decay curve.',
1552
+ 'IDEMPOTENT: returns {pinned: true} regardless of prior state.',
1553
+ 'RETURNS: { id, pinned: true }.',
1554
+ ].join(' '),
1555
+ inputSchema: {
1556
+ type: 'object',
1557
+ properties: { id: { type: 'string' } },
1558
+ required: ['id'],
1559
+ },
1560
+ handler: async (args) => {
1561
+ const id = args.id;
1562
+ const { getDb } = await import('../../db/index.js');
1563
+ const r = getDb().prepare(`UPDATE memories SET pinned = 1 WHERE id = ?`).run(id);
1564
+ if (r.changes === 0)
1565
+ return { error: 'not_found', id };
1566
+ return { id, pinned: true };
1567
+ },
1568
+ },
1569
+ {
1570
+ name: 'unpin',
1571
+ description: [
1572
+ 'Remove the pin from a memory — it resumes normal time-decay based on its importance.',
1573
+ 'IDEMPOTENT: returns {pinned: false} regardless of prior state.',
1574
+ 'RETURNS: { id, pinned: false }.',
1575
+ ].join(' '),
1576
+ inputSchema: {
1577
+ type: 'object',
1578
+ properties: { id: { type: 'string' } },
1579
+ required: ['id'],
1580
+ },
1581
+ handler: async (args) => {
1582
+ const id = args.id;
1583
+ const { getDb } = await import('../../db/index.js');
1584
+ const r = getDb().prepare(`UPDATE memories SET pinned = 0 WHERE id = ?`).run(id);
1585
+ if (r.changes === 0)
1586
+ return { error: 'not_found', id };
1587
+ return { id, pinned: false };
1588
+ },
1589
+ },
1590
+ {
1591
+ name: 'set_importance',
1592
+ description: [
1593
+ "Set a memory's importance level — affects decay half-life (high=90d, medium=30d, low=14d) and recall ranking.",
1594
+ 'WHEN: user marks something as critical, or the agent decides a memory deserves more/less prominence.',
1595
+ 'Default importance is set automatically at remember() time based on intent classification (preferences + corrections → high).',
1596
+ 'IDEMPOTENT.',
1597
+ 'RETURNS: { id, importance }.',
1598
+ ].join(' '),
1599
+ inputSchema: {
1600
+ type: 'object',
1601
+ properties: {
1602
+ id: { type: 'string' },
1603
+ level: { type: 'string', enum: ['high', 'medium', 'low'] },
1604
+ },
1605
+ required: ['id', 'level'],
1606
+ },
1607
+ handler: async (args) => {
1608
+ const id = args.id;
1609
+ const level = args.level;
1610
+ if (!['high', 'medium', 'low'].includes(level)) {
1611
+ return { error: 'invalid_level', message: 'level must be high|medium|low' };
1612
+ }
1613
+ const { getDb } = await import('../../db/index.js');
1614
+ const r = getDb().prepare(`UPDATE memories SET importance = ? WHERE id = ?`).run(level, id);
1615
+ if (r.changes === 0)
1616
+ return { error: 'not_found', id };
1617
+ return { id, importance: level };
1618
+ },
1619
+ },
1620
+ // ── recall_chain ────────────────────────────────────────────────────────
1621
+ // Graph traversal over wikilinks + related_ids — our answer to Neo4j-backed
1622
+ // graph memory features in competing products. No external graph DB needed
1623
+ // because we already store the edges in the memory row's related_ids array.
1624
+ {
1625
+ name: 'recall_chain',
1626
+ description: [
1627
+ 'Traverse the memory graph from a starting memory id, following wikilinks + related_ids up to `depth` hops.',
1628
+ 'WHEN: "show me the chain of reasoning that led here", "what memories are connected to this decision?".',
1629
+ 'Returns memories grouped by hop distance — direct neighbors at depth 1, their neighbors at depth 2, etc.',
1630
+ 'ANTI-LOOP: depth is capped at 4 (silently). Memories are deduplicated across hops.',
1631
+ 'RETURNS: { root: id, chain: [{ depth, memories: [{ id, type, title, score }] }] }.',
1632
+ ].join(' '),
1633
+ inputSchema: {
1634
+ type: 'object',
1635
+ properties: {
1636
+ id: { type: 'string', description: 'Starting memory id.' },
1637
+ depth: { type: 'number', default: 2, description: 'Max hop distance (capped at 4).' },
1638
+ limit_per_hop: { type: 'number', default: 10 },
1639
+ },
1640
+ required: ['id'],
1641
+ },
1642
+ handler: async (args) => {
1643
+ const rootId = args.id;
1644
+ const maxDepth = Math.min(4, Math.max(1, args.depth ?? 2));
1645
+ const limitPerHop = Math.max(1, args.limit_per_hop ?? 10);
1646
+ const root = store.getById(rootId);
1647
+ if (!root)
1648
+ return { error: 'not_found', id: rootId };
1649
+ const visited = new Set([rootId]);
1650
+ const chain = [];
1651
+ let frontier = [rootId];
1652
+ for (let d = 1; d <= maxDepth; d++) {
1653
+ const nextFrontier = [];
1654
+ for (const fid of frontier) {
1655
+ const m = store.getById(fid);
1656
+ if (!m)
1657
+ continue;
1658
+ for (const wl of m.wikilinks ?? []) {
1659
+ const targetId = wl;
1660
+ if (!visited.has(targetId)) {
1661
+ visited.add(targetId);
1662
+ nextFrontier.push({ id: targetId, via: 'wikilink' });
1663
+ }
1664
+ }
1665
+ for (const rid of m.related_ids ?? []) {
1666
+ if (!visited.has(rid)) {
1667
+ visited.add(rid);
1668
+ nextFrontier.push({ id: rid, via: 'related' });
1669
+ }
1670
+ }
1671
+ }
1672
+ if (nextFrontier.length === 0)
1673
+ break;
1674
+ const hopMemories = [];
1675
+ for (const f of nextFrontier.slice(0, limitPerHop)) {
1676
+ const m = store.getById(f.id);
1677
+ if (!m)
1678
+ continue;
1679
+ hopMemories.push({ id: m.id, type: m.type, title: m.properties.title, via: f.via });
1680
+ }
1681
+ if (hopMemories.length === 0)
1682
+ break;
1683
+ chain.push({ depth: d, memories: hopMemories });
1684
+ frontier = hopMemories.map((m) => m.id);
1685
+ }
1686
+ return {
1687
+ root: { id: root.id, type: root.type, title: root.properties.title },
1688
+ total_reached: visited.size - 1,
1689
+ chain,
1690
+ };
1691
+ },
1692
+ },
1441
1693
  ];
1442
1694
  }
1443
1695
  // ── Heavy-op detection ────────────────────────────────────────────────────────
@@ -1514,7 +1766,7 @@ async function handleAsyncIngest(uri, forceType, titleOverride, tagsOverride, st
1514
1766
  return { job_id: jobId, status: 'pending', estimated_ms: estimatedMs };
1515
1767
  }
1516
1768
  // ── Ingest routing helper ─────────────────────────────────────────────────────
1517
- async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, config) {
1769
+ export async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, config) {
1518
1770
  const embeddingModel = `${config.embeddings.provider}/${config.embeddings.model}`;
1519
1771
  // Normalise: bare absolute paths → file:// URI
1520
1772
  let normalUri = uri;
@@ -1549,7 +1801,9 @@ async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, c
1549
1801
  const { buildYoutubeItem } = await import('../modules/youtube/ingest.js');
1550
1802
  const transcript = await fetchTranscript(normalUri, config.youtube);
1551
1803
  // Fail fast if transcript is empty — do not create an empty memory
1552
- if (!transcript.full_text || transcript.full_text.trim() === '' || transcript.segments.length === 0) {
1804
+ if (!transcript.full_text ||
1805
+ transcript.full_text.trim() === '' ||
1806
+ transcript.segments.length === 0) {
1553
1807
  throw new Error(`Could not fetch transcript — video may have no captions or yt-dlp is unavailable (video_id: ${transcript.video_id})`);
1554
1808
  }
1555
1809
  const item = buildYoutubeItem({ transcript, embeddingModel });
@@ -1683,10 +1937,11 @@ async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, c
1683
1937
  let extractionError;
1684
1938
  try {
1685
1939
  const buffer = await readFile(filePath);
1686
- const { PDFParse } = await import('pdf-parse');
1940
+ const { PDFParse } = (await import('pdf-parse'));
1687
1941
  const parser = new PDFParse({ data: buffer, verbosity: 0 });
1688
1942
  const result = await parser.getText();
1689
- content = result.text.trim() || `[PDF] ${title} — no extractable text (possibly scanned image PDF)`;
1943
+ content =
1944
+ result.text.trim() || `[PDF] ${title} — no extractable text (possibly scanned image PDF)`;
1690
1945
  }
1691
1946
  catch (e) {
1692
1947
  extractionFailed = true;
@@ -1703,11 +1958,16 @@ async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, c
1703
1958
  content_hash: createHash('sha256').update(content).digest('hex'),
1704
1959
  properties: {
1705
1960
  title,
1706
- tags: extractionFailed ? [...(tagsOverride ?? []), 'pdf_extraction_failed'] : tagsOverride,
1961
+ tags: extractionFailed
1962
+ ? [...(tagsOverride ?? []), 'pdf_extraction_failed']
1963
+ : tagsOverride,
1707
1964
  created_at: now,
1708
1965
  ingested_at: now,
1709
1966
  source_url: normalUri,
1710
- custom: { pdf_path: filePath, extraction_status: extractionFailed ? 'failed' : 'complete' },
1967
+ custom: {
1968
+ pdf_path: filePath,
1969
+ extraction_status: extractionFailed ? 'failed' : 'complete',
1970
+ },
1711
1971
  },
1712
1972
  wikilinks,
1713
1973
  related_ids: [],
@@ -1790,7 +2050,10 @@ async function routeIngest(uri, forceType, titleOverride, tagsOverride, store, c
1790
2050
  if (titleMatch)
1791
2051
  autoTitle = titleMatch[1].trim();
1792
2052
  // Naive text extraction: strip tags
1793
- content = html.replace(/<[^>]+>/g, ' ').replace(/\s{2,}/g, ' ').slice(0, 5000);
2053
+ content = html
2054
+ .replace(/<[^>]+>/g, ' ')
2055
+ .replace(/\s{2,}/g, ' ')
2056
+ .slice(0, 5000);
1794
2057
  }
1795
2058
  catch {
1796
2059
  // fetch failed — just store URL as note