@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.
- package/LICENSE +92 -18
- package/README.md +13 -5
- package/dist/cloud/auth.d.ts.map +1 -1
- package/dist/cloud/auth.js +3 -1
- package/dist/cloud/auth.js.map +1 -1
- package/dist/cloud/bridge-client.d.ts.map +1 -1
- package/dist/cloud/bridge-client.js +43 -2
- package/dist/cloud/bridge-client.js.map +1 -1
- package/dist/cloud/crypto.d.ts.map +1 -1
- package/dist/cloud/crypto.js +1 -3
- package/dist/cloud/crypto.js.map +1 -1
- package/dist/cloud/pairing.d.ts.map +1 -1
- package/dist/cloud/pairing.js.map +1 -1
- package/dist/cloud/transit-poller.d.ts.map +1 -1
- package/dist/cloud/transit-poller.js.map +1 -1
- package/dist/core/db/index.d.ts.map +1 -1
- package/dist/core/db/index.js +25 -1
- package/dist/core/db/index.js.map +1 -1
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js.map +1 -1
- package/dist/core/security/uri-validator.d.ts.map +1 -1
- package/dist/core/security/uri-validator.js.map +1 -1
- package/dist/core/server/http.d.ts.map +1 -1
- package/dist/core/server/http.js +4 -2
- package/dist/core/server/http.js.map +1 -1
- package/dist/core/server/mcp-handler.d.ts.map +1 -1
- package/dist/core/server/mcp-handler.js +5 -5
- package/dist/core/server/mcp-handler.js.map +1 -1
- package/dist/core/server/mcp-http.d.ts.map +1 -1
- package/dist/core/server/mcp-http.js +1 -1
- package/dist/core/server/mcp-http.js.map +1 -1
- package/dist/core/server/websocket.d.ts.map +1 -1
- package/dist/core/server/websocket.js.map +1 -1
- package/dist/embeddings/index.js.map +1 -1
- package/dist/embeddings/providers/engram.d.ts.map +1 -1
- package/dist/embeddings/providers/engram.js +3 -1
- package/dist/embeddings/providers/engram.js.map +1 -1
- package/dist/ingest/jobs.d.ts.map +1 -1
- package/dist/ingest/jobs.js +2 -6
- package/dist/ingest/jobs.js.map +1 -1
- package/dist/mcp-server/tests/mcp-e2e.test.js.map +1 -1
- package/dist/memory/admin/tools.d.ts.map +1 -1
- package/dist/memory/admin/tools.js +4 -1
- package/dist/memory/admin/tools.js.map +1 -1
- package/dist/memory/core/signals.d.ts +71 -0
- package/dist/memory/core/signals.d.ts.map +1 -0
- package/dist/memory/core/signals.js +121 -0
- package/dist/memory/core/signals.js.map +1 -0
- package/dist/memory/core/source-registry.d.ts.map +1 -1
- package/dist/memory/core/source-registry.js.map +1 -1
- package/dist/memory/core/store.d.ts.map +1 -1
- package/dist/memory/core/store.js +58 -12
- package/dist/memory/core/store.js.map +1 -1
- package/dist/memory/modules/_custom/generic-module.d.ts.map +1 -1
- package/dist/memory/modules/_custom/generic-module.js.map +1 -1
- package/dist/memory/modules/_custom/tests/custom-types.test.js +9 -2
- package/dist/memory/modules/_custom/tests/custom-types.test.js.map +1 -1
- package/dist/memory/modules/_custom/tools.d.ts.map +1 -1
- package/dist/memory/modules/_custom/tools.js +3 -1
- package/dist/memory/modules/_custom/tools.js.map +1 -1
- package/dist/memory/modules/audio/tests/audio.test.js +12 -3
- package/dist/memory/modules/audio/tests/audio.test.js.map +1 -1
- package/dist/memory/modules/audio/tests/transcriber.test.js.map +1 -1
- package/dist/memory/modules/audio/tools.d.ts.map +1 -1
- package/dist/memory/modules/audio/tools.js +9 -2
- package/dist/memory/modules/audio/tools.js.map +1 -1
- package/dist/memory/modules/audio/transcriber.d.ts.map +1 -1
- package/dist/memory/modules/audio/transcriber.js +4 -2
- package/dist/memory/modules/audio/transcriber.js.map +1 -1
- package/dist/memory/modules/conversations/tests/conversations.test.js +6 -1
- package/dist/memory/modules/conversations/tests/conversations.test.js.map +1 -1
- package/dist/memory/modules/conversations/tools.d.ts.map +1 -1
- package/dist/memory/modules/conversations/tools.js +2 -2
- package/dist/memory/modules/conversations/tools.js.map +1 -1
- package/dist/memory/modules/drive/connector.d.ts.map +1 -1
- package/dist/memory/modules/drive/connector.js.map +1 -1
- package/dist/memory/modules/drive/oauth.d.ts.map +1 -1
- package/dist/memory/modules/drive/oauth.js.map +1 -1
- package/dist/memory/modules/drive/tools.d.ts.map +1 -1
- package/dist/memory/modules/drive/tools.js.map +1 -1
- package/dist/memory/modules/notes/tests/notes.test.js +6 -1
- package/dist/memory/modules/notes/tests/notes.test.js.map +1 -1
- package/dist/memory/modules/notes/tools.d.ts.map +1 -1
- package/dist/memory/modules/notes/tools.js +1 -1
- package/dist/memory/modules/notes/tools.js.map +1 -1
- package/dist/memory/modules/notion/connector.d.ts.map +1 -1
- package/dist/memory/modules/notion/connector.js +16 -8
- package/dist/memory/modules/notion/connector.js.map +1 -1
- package/dist/memory/modules/notion/oauth.d.ts.map +1 -1
- package/dist/memory/modules/notion/oauth.js.map +1 -1
- package/dist/memory/modules/notion/tools.d.ts.map +1 -1
- package/dist/memory/modules/notion/tools.js +1 -1
- package/dist/memory/modules/notion/tools.js.map +1 -1
- package/dist/memory/modules/obsidian/tests/obsidian.test.js +9 -2
- package/dist/memory/modules/obsidian/tests/obsidian.test.js.map +1 -1
- package/dist/memory/modules/obsidian/tools.d.ts.map +1 -1
- package/dist/memory/modules/obsidian/tools.js.map +1 -1
- package/dist/memory/modules/obsidian/vault-reader.js.map +1 -1
- package/dist/memory/modules/team/keystore.d.ts.map +1 -1
- package/dist/memory/modules/team/keystore.js +4 -1
- package/dist/memory/modules/team/keystore.js.map +1 -1
- package/dist/memory/modules/team/tools.d.ts.map +1 -1
- package/dist/memory/modules/team/tools.js +19 -13
- package/dist/memory/modules/team/tools.js.map +1 -1
- package/dist/memory/modules/youtube/tests/channel.test.js.map +1 -1
- package/dist/memory/modules/youtube/tests/transcript-fetcher.test.js.map +1 -1
- package/dist/memory/modules/youtube/tests/youtube.test.js +12 -3
- package/dist/memory/modules/youtube/tests/youtube.test.js.map +1 -1
- package/dist/memory/modules/youtube/tools.d.ts.map +1 -1
- package/dist/memory/modules/youtube/tools.js +14 -9
- package/dist/memory/modules/youtube/tools.js.map +1 -1
- package/dist/memory/modules/youtube/transcript-fetcher.js +4 -2
- package/dist/memory/modules/youtube/transcript-fetcher.js.map +1 -1
- package/dist/memory/modules/youtube/watcher.js +3 -1
- package/dist/memory/modules/youtube/watcher.js.map +1 -1
- package/dist/memory/public/tools.d.ts +1 -0
- package/dist/memory/public/tools.d.ts.map +1 -1
- package/dist/memory/public/tools.js +291 -28
- package/dist/memory/public/tools.js.map +1 -1
- package/dist/scripts/install.js +3 -1
- package/dist/scripts/install.js.map +1 -1
- package/dist/scripts/pair.js +2 -2
- package/dist/scripts/pair.js.map +1 -1
- package/dist/scripts/serve.js.map +1 -1
- package/dist/scripts/service.js.map +1 -1
- package/dist/server/api/daily.d.ts.map +1 -1
- package/dist/server/api/daily.js +3 -1
- package/dist/server/api/daily.js.map +1 -1
- package/dist/server/api/integrations.d.ts.map +1 -1
- package/dist/server/api/integrations.js +6 -2
- package/dist/server/api/integrations.js.map +1 -1
- package/dist/server/api/memories.d.ts.map +1 -1
- package/dist/server/api/memories.js +40 -5
- package/dist/server/api/memories.js.map +1 -1
- package/dist/server/api/settings.d.ts.map +1 -1
- package/dist/server/api/settings.js +1 -3
- package/dist/server/api/settings.js.map +1 -1
- package/dist/server/api/sources.d.ts.map +1 -1
- package/dist/server/api/sources.js.map +1 -1
- package/dist/server/api/sync-status.d.ts.map +1 -1
- package/dist/server/api/sync-status.js +1 -3
- package/dist/server/api/sync-status.js.map +1 -1
- package/dist/server/api/types.d.ts.map +1 -1
- package/dist/server/api/types.js.map +1 -1
- package/dist/server/api/version.d.ts.map +1 -1
- package/dist/server/api/version.js +6 -2
- package/dist/server/api/version.js.map +1 -1
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js.map +1 -1
- package/dist/sync/cloud-saves.d.ts.map +1 -1
- package/dist/sync/cloud-saves.js +1 -3
- package/dist/sync/cloud-saves.js.map +1 -1
- package/dist/sync/ed25519.d.ts.map +1 -1
- package/dist/sync/ed25519.js +3 -8
- package/dist/sync/ed25519.js.map +1 -1
- package/dist/sync/recovery-setup.d.ts.map +1 -1
- package/dist/sync/recovery-setup.js.map +1 -1
- package/dist/sync/shamir.d.ts.map +1 -1
- package/dist/sync/shamir.js.map +1 -1
- package/dist/sync/team-sync.d.ts.map +1 -1
- package/dist/sync/team-sync.js +2 -2
- package/dist/sync/team-sync.js.map +1 -1
- package/dist/sync/tests/apply.test.d.ts.map +1 -1
- package/dist/sync/tests/apply.test.js +1 -3
- package/dist/sync/tests/apply.test.js.map +1 -1
- package/dist/sync/tests/two-device-sync.test.js +8 -8
- package/dist/sync/tests/two-device-sync.test.js.map +1 -1
- package/dist/tests/cloud-integration.test.js +1 -1
- package/dist/tests/cloud-integration.test.js.map +1 -1
- package/dist/tests/cloud-saves-integration.test.js +1 -1
- package/dist/tests/cloud-saves-integration.test.js.map +1 -1
- package/dist/tests/cloud-transit.test.js +1 -1
- package/dist/tests/cloud-transit.test.js.map +1 -1
- package/dist/tests/db.test.js +1 -3
- package/dist/tests/db.test.js.map +1 -1
- package/dist/tests/memory-store.test.js.map +1 -1
- package/dist/tests/module-registry.test.js +4 -4
- package/dist/tests/module-registry.test.js.map +1 -1
- package/dist/tests/public-tools.test.js +24 -8
- package/dist/tests/public-tools.test.js.map +1 -1
- package/dist/tests/scope-encryption.test.js.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/vector/store.d.ts.map +1 -1
- package/dist/vector/store.js +1 -4
- package/dist/vector/store.js.map +1 -1
- package/dist/webapp/api/daily.d.ts.map +1 -1
- package/dist/webapp/api/daily.js +3 -1
- package/dist/webapp/api/daily.js.map +1 -1
- package/dist/webapp/api/memories.d.ts.map +1 -1
- package/dist/webapp/api/memories.js +4 -1
- package/dist/webapp/api/memories.js.map +1 -1
- package/dist/webapp/api/settings.d.ts.map +1 -1
- package/dist/webapp/api/settings.js +1 -3
- package/dist/webapp/api/settings.js.map +1 -1
- package/dist/webapp/api/sources.d.ts.map +1 -1
- package/dist/webapp/api/sources.js.map +1 -1
- package/dist/webapp/api/sync-status.d.ts.map +1 -1
- package/dist/webapp/api/sync-status.js +1 -3
- package/dist/webapp/api/sync-status.js.map +1 -1
- package/dist/webapp/api/types.d.ts.map +1 -1
- package/dist/webapp/api/types.js +2 -1
- package/dist/webapp/api/types.js.map +1 -1
- package/dist/webapp/tests/mcp-http.test.js.map +1 -1
- 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.
|
|
11
|
+
notes: 1.0, // user-curated, high signal
|
|
12
12
|
conversations: 0.95, // recent, dialog context
|
|
13
|
-
drive: 0.
|
|
14
|
-
notion: 0.
|
|
13
|
+
drive: 0.9, // structured documents
|
|
14
|
+
notion: 0.9,
|
|
15
15
|
obsidian: 0.95, // user notes, high signal
|
|
16
|
-
audio: 0.
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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: {
|
|
933
|
-
|
|
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) => ({
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ||
|
|
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 =
|
|
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
|
|
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: {
|
|
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
|
|
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
|