exovault-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +41 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +236 -0
- package/dist/auth.js.map +1 -0
- package/dist/auto-session.d.ts +39 -0
- package/dist/auto-session.d.ts.map +1 -0
- package/dist/auto-session.js +128 -0
- package/dist/auto-session.js.map +1 -0
- package/dist/buffer-persistence.d.ts +35 -0
- package/dist/buffer-persistence.d.ts.map +1 -0
- package/dist/buffer-persistence.js +110 -0
- package/dist/buffer-persistence.js.map +1 -0
- package/dist/coerce-params.d.ts +36 -0
- package/dist/coerce-params.d.ts.map +1 -0
- package/dist/coerce-params.js +120 -0
- package/dist/coerce-params.js.map +1 -0
- package/dist/crypto.d.ts +39 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +119 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db.d.ts +350 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +866 -0
- package/dist/db.js.map +1 -0
- package/dist/embedding-config.d.ts +11 -0
- package/dist/embedding-config.d.ts.map +1 -0
- package/dist/embedding-config.js +24 -0
- package/dist/embedding-config.js.map +1 -0
- package/dist/entity-extraction.d.ts +22 -0
- package/dist/entity-extraction.d.ts.map +1 -0
- package/dist/entity-extraction.js +140 -0
- package/dist/entity-extraction.js.map +1 -0
- package/dist/episodic-headline.d.ts +6 -0
- package/dist/episodic-headline.d.ts.map +1 -0
- package/dist/episodic-headline.js +62 -0
- package/dist/episodic-headline.js.map +1 -0
- package/dist/error-sanitizer.d.ts +20 -0
- package/dist/error-sanitizer.d.ts.map +1 -0
- package/dist/error-sanitizer.js +54 -0
- package/dist/error-sanitizer.js.map +1 -0
- package/dist/extraction-budget.d.ts +39 -0
- package/dist/extraction-budget.d.ts.map +1 -0
- package/dist/extraction-budget.js +122 -0
- package/dist/extraction-budget.js.map +1 -0
- package/dist/extraction-llm.d.ts +22 -0
- package/dist/extraction-llm.d.ts.map +1 -0
- package/dist/extraction-llm.js +32 -0
- package/dist/extraction-llm.js.map +1 -0
- package/dist/extraction-prompt.d.ts +40 -0
- package/dist/extraction-prompt.d.ts.map +1 -0
- package/dist/extraction-prompt.js +176 -0
- package/dist/extraction-prompt.js.map +1 -0
- package/dist/gateway-client.d.ts +303 -0
- package/dist/gateway-client.d.ts.map +1 -0
- package/dist/gateway-client.js +285 -0
- package/dist/gateway-client.js.map +1 -0
- package/dist/gateway-init.d.ts +32 -0
- package/dist/gateway-init.d.ts.map +1 -0
- package/dist/gateway-init.js +71 -0
- package/dist/gateway-init.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1242 -0
- package/dist/index.js.map +1 -0
- package/dist/infer-task-status.d.ts +7 -0
- package/dist/infer-task-status.d.ts.map +1 -0
- package/dist/infer-task-status.js +23 -0
- package/dist/infer-task-status.js.map +1 -0
- package/dist/normalize-agent-id.d.ts +21 -0
- package/dist/normalize-agent-id.d.ts.map +1 -0
- package/dist/normalize-agent-id.js +54 -0
- package/dist/normalize-agent-id.js.map +1 -0
- package/dist/openai.d.ts +14 -0
- package/dist/openai.d.ts.map +1 -0
- package/dist/openai.js +43 -0
- package/dist/openai.js.map +1 -0
- package/dist/rlm/actions.d.ts +31 -0
- package/dist/rlm/actions.d.ts.map +1 -0
- package/dist/rlm/actions.js +241 -0
- package/dist/rlm/actions.js.map +1 -0
- package/dist/rlm/benchmark.d.ts +2 -0
- package/dist/rlm/benchmark.d.ts.map +1 -0
- package/dist/rlm/benchmark.js +215 -0
- package/dist/rlm/benchmark.js.map +1 -0
- package/dist/rlm/execute.d.ts +13 -0
- package/dist/rlm/execute.d.ts.map +1 -0
- package/dist/rlm/execute.js +366 -0
- package/dist/rlm/execute.js.map +1 -0
- package/dist/rlm/index.d.ts +6 -0
- package/dist/rlm/index.d.ts.map +1 -0
- package/dist/rlm/index.js +147 -0
- package/dist/rlm/index.js.map +1 -0
- package/dist/rlm/profiles.d.ts +9 -0
- package/dist/rlm/profiles.d.ts.map +1 -0
- package/dist/rlm/profiles.js +46 -0
- package/dist/rlm/profiles.js.map +1 -0
- package/dist/rlm/types.d.ts +98 -0
- package/dist/rlm/types.d.ts.map +1 -0
- package/dist/rlm/types.js +6 -0
- package/dist/rlm/types.js.map +1 -0
- package/dist/rlm/verify.d.ts +13 -0
- package/dist/rlm/verify.d.ts.map +1 -0
- package/dist/rlm/verify.js +58 -0
- package/dist/rlm/verify.js.map +1 -0
- package/dist/rlm/writeback.d.ts +7 -0
- package/dist/rlm/writeback.d.ts.map +1 -0
- package/dist/rlm/writeback.js +77 -0
- package/dist/rlm/writeback.js.map +1 -0
- package/dist/scripts/backfill-memory-embeddings.d.ts +2 -0
- package/dist/scripts/backfill-memory-embeddings.d.ts.map +1 -0
- package/dist/scripts/backfill-memory-embeddings.js +153 -0
- package/dist/scripts/backfill-memory-embeddings.js.map +1 -0
- package/dist/session-buffer.d.ts +104 -0
- package/dist/session-buffer.d.ts.map +1 -0
- package/dist/session-buffer.js +466 -0
- package/dist/session-buffer.js.map +1 -0
- package/dist/session-dedup.d.ts +30 -0
- package/dist/session-dedup.d.ts.map +1 -0
- package/dist/session-dedup.js +67 -0
- package/dist/session-dedup.js.map +1 -0
- package/dist/session-flush.d.ts +81 -0
- package/dist/session-flush.d.ts.map +1 -0
- package/dist/session-flush.js +169 -0
- package/dist/session-flush.js.map +1 -0
- package/dist/session-lifecycle.d.ts +72 -0
- package/dist/session-lifecycle.d.ts.map +1 -0
- package/dist/session-lifecycle.js +247 -0
- package/dist/session-lifecycle.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +260 -0
- package/dist/setup.js.map +1 -0
- package/dist/stopwords.d.ts +2 -0
- package/dist/stopwords.d.ts.map +1 -0
- package/dist/stopwords.js +20 -0
- package/dist/stopwords.js.map +1 -0
- package/dist/strip-html.d.ts +5 -0
- package/dist/strip-html.d.ts.map +1 -0
- package/dist/strip-html.js +35 -0
- package/dist/strip-html.js.map +1 -0
- package/dist/task-completion-flush.d.ts +36 -0
- package/dist/task-completion-flush.d.ts.map +1 -0
- package/dist/task-completion-flush.js +97 -0
- package/dist/task-completion-flush.js.map +1 -0
- package/dist/task-lifecycle-types.d.ts +13 -0
- package/dist/task-lifecycle-types.d.ts.map +1 -0
- package/dist/task-lifecycle-types.js +12 -0
- package/dist/task-lifecycle-types.js.map +1 -0
- package/dist/task-lifecycle.d.ts +78 -0
- package/dist/task-lifecycle.d.ts.map +1 -0
- package/dist/task-lifecycle.js +256 -0
- package/dist/task-lifecycle.js.map +1 -0
- package/dist/tools/agent-messages.d.ts +26 -0
- package/dist/tools/agent-messages.d.ts.map +1 -0
- package/dist/tools/agent-messages.js +123 -0
- package/dist/tools/agent-messages.js.map +1 -0
- package/dist/tools/agent-tasks.d.ts +24 -0
- package/dist/tools/agent-tasks.d.ts.map +1 -0
- package/dist/tools/agent-tasks.js +162 -0
- package/dist/tools/agent-tasks.js.map +1 -0
- package/dist/tools/archive-memory.d.ts +2 -0
- package/dist/tools/archive-memory.d.ts.map +1 -0
- package/dist/tools/archive-memory.js +19 -0
- package/dist/tools/archive-memory.js.map +1 -0
- package/dist/tools/blind-index.d.ts +29 -0
- package/dist/tools/blind-index.d.ts.map +1 -0
- package/dist/tools/blind-index.js +53 -0
- package/dist/tools/blind-index.js.map +1 -0
- package/dist/tools/cleanup-memories.d.ts +44 -0
- package/dist/tools/cleanup-memories.d.ts.map +1 -0
- package/dist/tools/cleanup-memories.js +126 -0
- package/dist/tools/cleanup-memories.js.map +1 -0
- package/dist/tools/context-checkpoint.d.ts +28 -0
- package/dist/tools/context-checkpoint.d.ts.map +1 -0
- package/dist/tools/context-checkpoint.js +140 -0
- package/dist/tools/context-checkpoint.js.map +1 -0
- package/dist/tools/context-profiles.d.ts +67 -0
- package/dist/tools/context-profiles.d.ts.map +1 -0
- package/dist/tools/context-profiles.js +30 -0
- package/dist/tools/context-profiles.js.map +1 -0
- package/dist/tools/create-note.d.ts +2 -0
- package/dist/tools/create-note.d.ts.map +1 -0
- package/dist/tools/create-note.js +60 -0
- package/dist/tools/create-note.js.map +1 -0
- package/dist/tools/create-vault.d.ts +5 -0
- package/dist/tools/create-vault.d.ts.map +1 -0
- package/dist/tools/create-vault.js +121 -0
- package/dist/tools/create-vault.js.map +1 -0
- package/dist/tools/decrypt-helpers.d.ts +31 -0
- package/dist/tools/decrypt-helpers.d.ts.map +1 -0
- package/dist/tools/decrypt-helpers.js +33 -0
- package/dist/tools/decrypt-helpers.js.map +1 -0
- package/dist/tools/delete-note.d.ts +2 -0
- package/dist/tools/delete-note.d.ts.map +1 -0
- package/dist/tools/delete-note.js +21 -0
- package/dist/tools/delete-note.js.map +1 -0
- package/dist/tools/explore-graph.d.ts +11 -0
- package/dist/tools/explore-graph.d.ts.map +1 -0
- package/dist/tools/explore-graph.js +169 -0
- package/dist/tools/explore-graph.js.map +1 -0
- package/dist/tools/get-related-memories.d.ts +2 -0
- package/dist/tools/get-related-memories.d.ts.map +1 -0
- package/dist/tools/get-related-memories.js +59 -0
- package/dist/tools/get-related-memories.js.map +1 -0
- package/dist/tools/knowledge-links.d.ts +17 -0
- package/dist/tools/knowledge-links.d.ts.map +1 -0
- package/dist/tools/knowledge-links.js +102 -0
- package/dist/tools/knowledge-links.js.map +1 -0
- package/dist/tools/list-active-agents.d.ts +5 -0
- package/dist/tools/list-active-agents.d.ts.map +1 -0
- package/dist/tools/list-active-agents.js +15 -0
- package/dist/tools/list-active-agents.js.map +1 -0
- package/dist/tools/list-notes.d.ts +2 -0
- package/dist/tools/list-notes.d.ts.map +1 -0
- package/dist/tools/list-notes.js +19 -0
- package/dist/tools/list-notes.js.map +1 -0
- package/dist/tools/list-vaults.d.ts +2 -0
- package/dist/tools/list-vaults.d.ts.map +1 -0
- package/dist/tools/list-vaults.js +19 -0
- package/dist/tools/list-vaults.js.map +1 -0
- package/dist/tools/mmr.d.ts +18 -0
- package/dist/tools/mmr.d.ts.map +1 -0
- package/dist/tools/mmr.js +67 -0
- package/dist/tools/mmr.js.map +1 -0
- package/dist/tools/read-memories.d.ts +2 -0
- package/dist/tools/read-memories.d.ts.map +1 -0
- package/dist/tools/read-memories.js +46 -0
- package/dist/tools/read-memories.js.map +1 -0
- package/dist/tools/read-note.d.ts +2 -0
- package/dist/tools/read-note.d.ts.map +1 -0
- package/dist/tools/read-note.js +35 -0
- package/dist/tools/read-note.js.map +1 -0
- package/dist/tools/read-notes.d.ts +6 -0
- package/dist/tools/read-notes.d.ts.map +1 -0
- package/dist/tools/read-notes.js +45 -0
- package/dist/tools/read-notes.js.map +1 -0
- package/dist/tools/resolve-vault-id.d.ts +6 -0
- package/dist/tools/resolve-vault-id.d.ts.map +1 -0
- package/dist/tools/resolve-vault-id.js +7 -0
- package/dist/tools/resolve-vault-id.js.map +1 -0
- package/dist/tools/rrf.d.ts +28 -0
- package/dist/tools/rrf.d.ts.map +1 -0
- package/dist/tools/rrf.js +19 -0
- package/dist/tools/rrf.js.map +1 -0
- package/dist/tools/search-and-read.d.ts +11 -0
- package/dist/tools/search-and-read.d.ts.map +1 -0
- package/dist/tools/search-and-read.js +208 -0
- package/dist/tools/search-and-read.js.map +1 -0
- package/dist/tools/search-memories.d.ts +13 -0
- package/dist/tools/search-memories.d.ts.map +1 -0
- package/dist/tools/search-memories.js +272 -0
- package/dist/tools/search-memories.js.map +1 -0
- package/dist/tools/search-notes.d.ts +2 -0
- package/dist/tools/search-notes.d.ts.map +1 -0
- package/dist/tools/search-notes.js +94 -0
- package/dist/tools/search-notes.js.map +1 -0
- package/dist/tools/semantic-search.d.ts +7 -0
- package/dist/tools/semantic-search.d.ts.map +1 -0
- package/dist/tools/semantic-search.js +85 -0
- package/dist/tools/semantic-search.js.map +1 -0
- package/dist/tools/session-start.d.ts +24 -0
- package/dist/tools/session-start.d.ts.map +1 -0
- package/dist/tools/session-start.js +256 -0
- package/dist/tools/session-start.js.map +1 -0
- package/dist/tools/stale-tasks.d.ts +22 -0
- package/dist/tools/stale-tasks.d.ts.map +1 -0
- package/dist/tools/stale-tasks.js +39 -0
- package/dist/tools/stale-tasks.js.map +1 -0
- package/dist/tools/temporal-decay.d.ts +21 -0
- package/dist/tools/temporal-decay.d.ts.map +1 -0
- package/dist/tools/temporal-decay.js +32 -0
- package/dist/tools/temporal-decay.js.map +1 -0
- package/dist/tools/update-memory.d.ts +19 -0
- package/dist/tools/update-memory.d.ts.map +1 -0
- package/dist/tools/update-memory.js +230 -0
- package/dist/tools/update-memory.js.map +1 -0
- package/dist/tools/update-note.d.ts +2 -0
- package/dist/tools/update-note.d.ts.map +1 -0
- package/dist/tools/update-note.js +79 -0
- package/dist/tools/update-note.js.map +1 -0
- package/dist/tools/vault-instruction-template.d.ts +17 -0
- package/dist/tools/vault-instruction-template.d.ts.map +1 -0
- package/dist/tools/vault-instruction-template.js +77 -0
- package/dist/tools/vault-instruction-template.js.map +1 -0
- package/dist/tools/wiki-link-sync.d.ts +34 -0
- package/dist/tools/wiki-link-sync.d.ts.map +1 -0
- package/dist/tools/wiki-link-sync.js +132 -0
- package/dist/tools/wiki-link-sync.js.map +1 -0
- package/dist/tools/wrap-tool-handler.d.ts +8 -0
- package/dist/tools/wrap-tool-handler.d.ts.map +1 -0
- package/dist/tools/wrap-tool-handler.js +32 -0
- package/dist/tools/wrap-tool-handler.js.map +1 -0
- package/dist/tools/write-memory.d.ts +34 -0
- package/dist/tools/write-memory.d.ts.map +1 -0
- package/dist/tools/write-memory.js +359 -0
- package/dist/tools/write-memory.js.map +1 -0
- package/dist/usage.d.ts +11 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +38 -0
- package/dist/usage.js.map +1 -0
- package/dist/wiki-link-parser.d.ts +27 -0
- package/dist/wiki-link-parser.d.ts.map +1 -0
- package/dist/wiki-link-parser.js +93 -0
- package/dist/wiki-link-parser.js.map +1 -0
- package/package.json +38 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
import { sanitizeDbError } from "./error-sanitizer.js";
|
|
2
|
+
// ─── Query helpers ────────────────────────────────────────────────────────────
|
|
3
|
+
export async function getVaults(supabase, userId) {
|
|
4
|
+
const { data, error } = await supabase
|
|
5
|
+
.from("vaults")
|
|
6
|
+
.select("*")
|
|
7
|
+
.eq("user_id", userId)
|
|
8
|
+
.order("sort_order", { ascending: true });
|
|
9
|
+
if (error)
|
|
10
|
+
throw new Error(sanitizeDbError("fetch vaults", error.message));
|
|
11
|
+
return data ?? [];
|
|
12
|
+
}
|
|
13
|
+
export async function getNotes(supabase, userId, vaultId, limit = 20) {
|
|
14
|
+
let query = supabase
|
|
15
|
+
.from("notes")
|
|
16
|
+
.select("*")
|
|
17
|
+
.eq("user_id", userId)
|
|
18
|
+
.eq("is_trashed", false)
|
|
19
|
+
.order("updated_at", { ascending: false })
|
|
20
|
+
.limit(limit);
|
|
21
|
+
if (vaultId) {
|
|
22
|
+
query = query.eq("vault_id", vaultId);
|
|
23
|
+
}
|
|
24
|
+
const { data, error } = await query;
|
|
25
|
+
if (error)
|
|
26
|
+
throw new Error(sanitizeDbError("fetch notes", error.message));
|
|
27
|
+
return data ?? [];
|
|
28
|
+
}
|
|
29
|
+
export async function getNote(supabase, userId, noteId) {
|
|
30
|
+
const { data, error } = await supabase
|
|
31
|
+
.from("notes")
|
|
32
|
+
.select("*")
|
|
33
|
+
.eq("id", noteId)
|
|
34
|
+
.eq("user_id", userId)
|
|
35
|
+
.single();
|
|
36
|
+
if (error) {
|
|
37
|
+
if (error.code === "PGRST116")
|
|
38
|
+
return null; // not found
|
|
39
|
+
throw new Error(sanitizeDbError("fetch note", error.message));
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
export async function getVault(supabase, userId, vaultId) {
|
|
44
|
+
const { data, error } = await supabase
|
|
45
|
+
.from("vaults")
|
|
46
|
+
.select("*")
|
|
47
|
+
.eq("id", vaultId)
|
|
48
|
+
.eq("user_id", userId)
|
|
49
|
+
.single();
|
|
50
|
+
if (error) {
|
|
51
|
+
if (error.code === "PGRST116")
|
|
52
|
+
return null;
|
|
53
|
+
throw new Error(sanitizeDbError("fetch vault", error.message));
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
export async function countNotesByVault(supabase, userId) {
|
|
58
|
+
const { data, error } = await supabase
|
|
59
|
+
.from("notes")
|
|
60
|
+
.select("vault_id")
|
|
61
|
+
.eq("user_id", userId)
|
|
62
|
+
.eq("is_trashed", false)
|
|
63
|
+
.limit(10_000);
|
|
64
|
+
if (error)
|
|
65
|
+
throw new Error(sanitizeDbError("count notes", error.message));
|
|
66
|
+
const counts = {};
|
|
67
|
+
for (const row of data ?? []) {
|
|
68
|
+
counts[row.vault_id] = (counts[row.vault_id] ?? 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
return counts;
|
|
71
|
+
}
|
|
72
|
+
export async function insertNote(supabase, data) {
|
|
73
|
+
const { data: row, error } = await supabase
|
|
74
|
+
.from("notes")
|
|
75
|
+
.insert(data)
|
|
76
|
+
.select("id")
|
|
77
|
+
.single();
|
|
78
|
+
if (error)
|
|
79
|
+
throw new Error(sanitizeDbError("insert note", error.message));
|
|
80
|
+
return row;
|
|
81
|
+
}
|
|
82
|
+
export async function updateNote(supabase, noteId, userId, updates) {
|
|
83
|
+
const { error } = await supabase
|
|
84
|
+
.from("notes")
|
|
85
|
+
.update({ ...updates, updated_at: new Date().toISOString() })
|
|
86
|
+
.eq("id", noteId)
|
|
87
|
+
.eq("user_id", userId);
|
|
88
|
+
if (error)
|
|
89
|
+
throw new Error(sanitizeDbError("update note", error.message));
|
|
90
|
+
}
|
|
91
|
+
export async function deleteNote(supabase, noteId, userId) {
|
|
92
|
+
const { error } = await supabase
|
|
93
|
+
.from("notes")
|
|
94
|
+
.delete()
|
|
95
|
+
.eq("id", noteId)
|
|
96
|
+
.eq("user_id", userId);
|
|
97
|
+
if (error)
|
|
98
|
+
throw new Error(sanitizeDbError("delete note", error.message));
|
|
99
|
+
}
|
|
100
|
+
// ─── Memory helpers ───────────────────────────────────────────────────────────
|
|
101
|
+
export async function getMemories(supabase, userId, options) {
|
|
102
|
+
const includeArchived = options?.includeArchived ?? false;
|
|
103
|
+
const limit = options?.limit ?? 50;
|
|
104
|
+
const orderBy = options?.orderBy ?? "updated_at";
|
|
105
|
+
let query = supabase
|
|
106
|
+
.from("memories")
|
|
107
|
+
.select("*")
|
|
108
|
+
.eq("user_id", userId)
|
|
109
|
+
.order(orderBy, { ascending: false })
|
|
110
|
+
.limit(limit);
|
|
111
|
+
if (!includeArchived)
|
|
112
|
+
query = query.eq("is_archived", false);
|
|
113
|
+
if (options?.vaultId)
|
|
114
|
+
query = query.eq("vault_id", options.vaultId);
|
|
115
|
+
if (options?.memoryType)
|
|
116
|
+
query = query.eq("memory_type", options.memoryType);
|
|
117
|
+
if (options?.minImportance)
|
|
118
|
+
query = query.gte("importance", options.minImportance);
|
|
119
|
+
const { data, error } = await query;
|
|
120
|
+
if (error)
|
|
121
|
+
throw new Error(sanitizeDbError("fetch memories", error.message));
|
|
122
|
+
return (data ?? []);
|
|
123
|
+
}
|
|
124
|
+
export async function getMemory(supabase, userId, memoryId) {
|
|
125
|
+
const { data, error } = await supabase
|
|
126
|
+
.from("memories")
|
|
127
|
+
.select("*")
|
|
128
|
+
.eq("id", memoryId)
|
|
129
|
+
.eq("user_id", userId)
|
|
130
|
+
.single();
|
|
131
|
+
if (error) {
|
|
132
|
+
if (error.code === "PGRST116")
|
|
133
|
+
return null;
|
|
134
|
+
throw new Error(sanitizeDbError("fetch memory", error.message));
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
export async function getMemoryByExternalWriteId(supabase, userId, externalWriteId) {
|
|
139
|
+
const { data, error } = await supabase
|
|
140
|
+
.from("memories")
|
|
141
|
+
.select("*")
|
|
142
|
+
.eq("user_id", userId)
|
|
143
|
+
.eq("external_write_id", externalWriteId)
|
|
144
|
+
.limit(1);
|
|
145
|
+
if (error)
|
|
146
|
+
throw new Error(sanitizeDbError("fetch memory by externalWriteId", error.message));
|
|
147
|
+
return (data && data.length > 0 ? data[0] : null);
|
|
148
|
+
}
|
|
149
|
+
export async function insertMemory(supabase, data) {
|
|
150
|
+
const { data: row, error } = await supabase
|
|
151
|
+
.from("memories")
|
|
152
|
+
.insert({
|
|
153
|
+
...data,
|
|
154
|
+
indexing_status: data.indexing_status ?? "pending",
|
|
155
|
+
importance: data.importance ?? 3,
|
|
156
|
+
confidence: data.confidence ?? 3,
|
|
157
|
+
})
|
|
158
|
+
.select("*")
|
|
159
|
+
.single();
|
|
160
|
+
if (error)
|
|
161
|
+
throw new Error(sanitizeDbError("insert memory", error.message));
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
164
|
+
export async function updateMemory(supabase, memoryId, userId, updates) {
|
|
165
|
+
const { data, error } = await supabase
|
|
166
|
+
.from("memories")
|
|
167
|
+
.update({ ...updates, updated_at: new Date().toISOString() })
|
|
168
|
+
.eq("id", memoryId)
|
|
169
|
+
.eq("user_id", userId)
|
|
170
|
+
.select("*")
|
|
171
|
+
.single();
|
|
172
|
+
if (error)
|
|
173
|
+
throw new Error(sanitizeDbError("update memory", error.message));
|
|
174
|
+
return data;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Update last_accessed_at for a batch of memory IDs.
|
|
178
|
+
* Fire-and-forget — errors are silently ignored so this never blocks the caller.
|
|
179
|
+
*
|
|
180
|
+
* Tries RPC `increment_memory_access` for atomic counter increment.
|
|
181
|
+
* Falls back to simple timestamp update if RPC doesn't exist yet.
|
|
182
|
+
*/
|
|
183
|
+
export async function touchMemories(supabase, userId, memoryIds) {
|
|
184
|
+
if (memoryIds.length === 0)
|
|
185
|
+
return;
|
|
186
|
+
try {
|
|
187
|
+
const now = new Date().toISOString();
|
|
188
|
+
await Promise.all(memoryIds.map(async (id) => {
|
|
189
|
+
const { error: rpcError } = await supabase.rpc("increment_memory_access", {
|
|
190
|
+
p_memory_id: id,
|
|
191
|
+
p_user_id: userId,
|
|
192
|
+
p_accessed_at: now,
|
|
193
|
+
});
|
|
194
|
+
if (rpcError) {
|
|
195
|
+
// RPC doesn't exist yet — just update timestamp
|
|
196
|
+
await supabase
|
|
197
|
+
.from("memories")
|
|
198
|
+
.update({ last_accessed_at: now })
|
|
199
|
+
.eq("id", id)
|
|
200
|
+
.eq("user_id", userId);
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Fire-and-forget — never fail the parent operation
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export async function archiveMemory(supabase, memoryId, userId, archived) {
|
|
209
|
+
const { error } = await supabase
|
|
210
|
+
.from("memories")
|
|
211
|
+
.update({ is_archived: archived, updated_at: new Date().toISOString() })
|
|
212
|
+
.eq("id", memoryId)
|
|
213
|
+
.eq("user_id", userId);
|
|
214
|
+
if (error)
|
|
215
|
+
throw new Error(sanitizeDbError("archive memory", error.message));
|
|
216
|
+
}
|
|
217
|
+
export async function deleteMemory(supabase, memoryId, userId) {
|
|
218
|
+
const { error } = await supabase
|
|
219
|
+
.from("memories")
|
|
220
|
+
.delete()
|
|
221
|
+
.eq("id", memoryId)
|
|
222
|
+
.eq("user_id", userId);
|
|
223
|
+
if (error)
|
|
224
|
+
throw new Error(sanitizeDbError("delete memory", error.message));
|
|
225
|
+
}
|
|
226
|
+
export async function replaceMemoryEmbeddings(supabase, input) {
|
|
227
|
+
const { error: deleteError } = await supabase
|
|
228
|
+
.from("memory_embeddings")
|
|
229
|
+
.delete()
|
|
230
|
+
.eq("memory_id", input.memoryId)
|
|
231
|
+
.eq("user_id", input.userId);
|
|
232
|
+
if (deleteError) {
|
|
233
|
+
throw new Error(sanitizeDbError("delete memory embeddings", deleteError.message));
|
|
234
|
+
}
|
|
235
|
+
if (input.rows.length === 0)
|
|
236
|
+
return;
|
|
237
|
+
const { error: insertError } = await supabase
|
|
238
|
+
.from("memory_embeddings")
|
|
239
|
+
.insert(input.rows.map((row) => ({
|
|
240
|
+
memory_id: input.memoryId,
|
|
241
|
+
user_id: input.userId,
|
|
242
|
+
chunk_index: row.chunkIndex,
|
|
243
|
+
embedding: row.embedding,
|
|
244
|
+
content_hash: input.contentHash,
|
|
245
|
+
embedding_model: input.embeddingModel,
|
|
246
|
+
token_count: row.tokenCount ?? null,
|
|
247
|
+
})));
|
|
248
|
+
if (insertError) {
|
|
249
|
+
throw new Error(sanitizeDbError("insert memory embeddings", insertError.message));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Fetch embeddings for a batch of memory IDs.
|
|
254
|
+
* Returns one embedding per memory (chunk_index = 0 preferred).
|
|
255
|
+
*/
|
|
256
|
+
export async function getEmbeddingsForMemories(supabase, userId, memoryIds) {
|
|
257
|
+
if (memoryIds.length === 0)
|
|
258
|
+
return [];
|
|
259
|
+
const { data, error } = await supabase
|
|
260
|
+
.from("memory_embeddings")
|
|
261
|
+
.select("memory_id, embedding")
|
|
262
|
+
.eq("user_id", userId)
|
|
263
|
+
.in("memory_id", memoryIds)
|
|
264
|
+
.order("chunk_index", { ascending: true });
|
|
265
|
+
if (error)
|
|
266
|
+
throw new Error(sanitizeDbError("fetch memory embeddings", error.message));
|
|
267
|
+
// Keep only the first embedding per memory (chunk_index=0)
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
const result = [];
|
|
270
|
+
for (const row of data ?? []) {
|
|
271
|
+
if (!seen.has(row.memory_id)) {
|
|
272
|
+
seen.add(row.memory_id);
|
|
273
|
+
result.push({ memory_id: row.memory_id, embedding: row.embedding });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
export async function matchNoteEmbeddings(supabase, embedding, userId, threshold = 0.5, count = 20) {
|
|
279
|
+
const { data, error } = await supabase.rpc("match_note_embeddings", {
|
|
280
|
+
query_embedding: JSON.stringify(embedding),
|
|
281
|
+
match_threshold: threshold,
|
|
282
|
+
match_count: count,
|
|
283
|
+
p_user_id: userId,
|
|
284
|
+
});
|
|
285
|
+
if (error)
|
|
286
|
+
throw new Error(sanitizeDbError("match note embeddings", error.message));
|
|
287
|
+
return (data ?? []);
|
|
288
|
+
}
|
|
289
|
+
export async function matchMemoryEmbeddings(supabase, embedding, userId, threshold = 0.5, count = 20) {
|
|
290
|
+
const { data, error } = await supabase.rpc("match_memory_embeddings", {
|
|
291
|
+
query_embedding: JSON.stringify(embedding),
|
|
292
|
+
match_threshold: threshold,
|
|
293
|
+
match_count: count,
|
|
294
|
+
p_user_id: userId,
|
|
295
|
+
});
|
|
296
|
+
if (error)
|
|
297
|
+
throw new Error(sanitizeDbError("match memory embeddings", error.message));
|
|
298
|
+
return (data ?? []);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Searches memories by HMAC-hashed blind tokens (privacy-preserving keyword search).
|
|
302
|
+
* Returns memory IDs ranked by number of matching tokens.
|
|
303
|
+
*/
|
|
304
|
+
export async function matchMemoriesByBlindTokens(supabase, tokenHashes, userId, vaultId, count = 20, includeArchived = false) {
|
|
305
|
+
if (tokenHashes.length === 0)
|
|
306
|
+
return [];
|
|
307
|
+
const { data, error } = await supabase.rpc("match_memories_by_blind_tokens", {
|
|
308
|
+
p_user_id: userId,
|
|
309
|
+
p_token_hashes: tokenHashes,
|
|
310
|
+
p_vault_id: vaultId ?? null,
|
|
311
|
+
p_match_count: count,
|
|
312
|
+
p_include_archived: includeArchived,
|
|
313
|
+
});
|
|
314
|
+
if (error)
|
|
315
|
+
throw new Error(sanitizeDbError("match memories by blind tokens", error.message));
|
|
316
|
+
return (data ?? []);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Searches notes by HMAC-hashed blind tokens (privacy-preserving keyword search).
|
|
320
|
+
* Returns note IDs ranked by number of matching tokens.
|
|
321
|
+
*/
|
|
322
|
+
export async function matchNotesByBlindTokens(supabase, tokenHashes, userId, vaultId, count = 20, includeTrashed = false) {
|
|
323
|
+
if (tokenHashes.length === 0)
|
|
324
|
+
return [];
|
|
325
|
+
const { data, error } = await supabase.rpc("match_notes_by_blind_tokens", {
|
|
326
|
+
p_user_id: userId,
|
|
327
|
+
p_token_hashes: tokenHashes,
|
|
328
|
+
p_vault_id: vaultId ?? null,
|
|
329
|
+
p_match_count: count,
|
|
330
|
+
p_include_trashed: includeTrashed,
|
|
331
|
+
});
|
|
332
|
+
if (error)
|
|
333
|
+
throw new Error(sanitizeDbError("match notes by blind tokens", error.message));
|
|
334
|
+
return (data ?? []);
|
|
335
|
+
}
|
|
336
|
+
// ─── Batch fetch helper ──────────────────────────────────────────────────────
|
|
337
|
+
export async function getNotesByIds(supabase, userId, noteIds) {
|
|
338
|
+
if (noteIds.length === 0)
|
|
339
|
+
return [];
|
|
340
|
+
const { data, error } = await supabase
|
|
341
|
+
.from("notes")
|
|
342
|
+
.select("*")
|
|
343
|
+
.eq("user_id", userId)
|
|
344
|
+
.eq("is_trashed", false)
|
|
345
|
+
.in("id", noteIds);
|
|
346
|
+
if (error)
|
|
347
|
+
throw new Error(sanitizeDbError("fetch notes by IDs", error.message));
|
|
348
|
+
return data ?? [];
|
|
349
|
+
}
|
|
350
|
+
export async function getNoteByImportSource(supabase, userId, vaultId, importSource, importSourceId) {
|
|
351
|
+
const { data, error } = await supabase
|
|
352
|
+
.from("notes")
|
|
353
|
+
.select("*")
|
|
354
|
+
.eq("user_id", userId)
|
|
355
|
+
.eq("vault_id", vaultId)
|
|
356
|
+
.eq("import_source", importSource)
|
|
357
|
+
.eq("import_source_id", importSourceId)
|
|
358
|
+
.limit(1);
|
|
359
|
+
if (error)
|
|
360
|
+
throw new Error(sanitizeDbError("fetch note by import source", error.message));
|
|
361
|
+
return (data && data.length > 0 ? data[0] : null);
|
|
362
|
+
}
|
|
363
|
+
export async function getMemoriesByIds(supabase, userId, memoryIds) {
|
|
364
|
+
if (memoryIds.length === 0)
|
|
365
|
+
return [];
|
|
366
|
+
const { data, error } = await supabase
|
|
367
|
+
.from("memories")
|
|
368
|
+
.select("*")
|
|
369
|
+
.eq("user_id", userId)
|
|
370
|
+
.in("id", memoryIds);
|
|
371
|
+
if (error)
|
|
372
|
+
throw new Error(sanitizeDbError("fetch memories by IDs", error.message));
|
|
373
|
+
return (data ?? []);
|
|
374
|
+
}
|
|
375
|
+
export async function getActiveAgents(supabase, userId, sinceDays = 30, limit = 20) {
|
|
376
|
+
const since = new Date();
|
|
377
|
+
since.setDate(since.getDate() - sinceDays);
|
|
378
|
+
const { data, error } = await supabase
|
|
379
|
+
.from("memories")
|
|
380
|
+
.select("agent_id, model_id, updated_at")
|
|
381
|
+
.eq("user_id", userId)
|
|
382
|
+
.eq("is_archived", false)
|
|
383
|
+
.gte("updated_at", since.toISOString())
|
|
384
|
+
.limit(10_000);
|
|
385
|
+
if (error)
|
|
386
|
+
throw new Error(sanitizeDbError("fetch active agents", error.message));
|
|
387
|
+
// Aggregate in JS since Supabase client doesn't support GROUP BY
|
|
388
|
+
const agentMap = new Map();
|
|
389
|
+
for (const row of data ?? []) {
|
|
390
|
+
const key = `${row.agent_id ?? "unknown"}::${row.model_id ?? "unknown"}`;
|
|
391
|
+
const existing = agentMap.get(key);
|
|
392
|
+
if (existing) {
|
|
393
|
+
existing.count++;
|
|
394
|
+
if (row.updated_at > existing.lastActiveAt) {
|
|
395
|
+
existing.lastActiveAt = row.updated_at;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
agentMap.set(key, {
|
|
400
|
+
modelId: row.model_id,
|
|
401
|
+
count: 1,
|
|
402
|
+
lastActiveAt: row.updated_at,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const agents = [];
|
|
407
|
+
for (const [key, value] of agentMap) {
|
|
408
|
+
const agentId = key.split("::")[0];
|
|
409
|
+
agents.push({
|
|
410
|
+
agentId,
|
|
411
|
+
modelId: value.modelId,
|
|
412
|
+
memoryCount: value.count,
|
|
413
|
+
lastActiveAt: value.lastActiveAt,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
agents.sort((a, b) => b.lastActiveAt.localeCompare(a.lastActiveAt));
|
|
417
|
+
return agents.slice(0, limit);
|
|
418
|
+
}
|
|
419
|
+
export async function getMemoryHealthStats(supabase, userId, vaultId) {
|
|
420
|
+
let query = supabase
|
|
421
|
+
.from("memories")
|
|
422
|
+
.select("indexing_status, is_archived")
|
|
423
|
+
.eq("user_id", userId);
|
|
424
|
+
if (vaultId)
|
|
425
|
+
query = query.eq("vault_id", vaultId);
|
|
426
|
+
query = query.limit(10_000);
|
|
427
|
+
const { data, error } = await query;
|
|
428
|
+
if (error)
|
|
429
|
+
throw new Error(sanitizeDbError("fetch memory health", error.message));
|
|
430
|
+
const rows = (data ?? []);
|
|
431
|
+
const stats = { total: rows.length, ready: 0, pending: 0, failed: 0, archived: 0 };
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
if (row.is_archived) {
|
|
434
|
+
stats.archived++;
|
|
435
|
+
}
|
|
436
|
+
else if (row.indexing_status === "ready") {
|
|
437
|
+
stats.ready++;
|
|
438
|
+
}
|
|
439
|
+
else if (row.indexing_status === "pending") {
|
|
440
|
+
stats.pending++;
|
|
441
|
+
}
|
|
442
|
+
else if (row.indexing_status === "failed") {
|
|
443
|
+
stats.failed++;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return stats;
|
|
447
|
+
}
|
|
448
|
+
// ─── Entity + relationship helpers ──────────────────────────────────────────
|
|
449
|
+
export async function searchMemoriesByEntity(supabase, userId, entity, limit = 20) {
|
|
450
|
+
const { data, error } = await supabase
|
|
451
|
+
.from("memories")
|
|
452
|
+
.select("*")
|
|
453
|
+
.eq("user_id", userId)
|
|
454
|
+
.contains("entities", JSON.stringify([entity]))
|
|
455
|
+
.order("updated_at", { ascending: false })
|
|
456
|
+
.limit(limit);
|
|
457
|
+
if (error)
|
|
458
|
+
throw new Error(sanitizeDbError("search memories by entity", error.message));
|
|
459
|
+
return (data ?? []);
|
|
460
|
+
}
|
|
461
|
+
export async function getRelatedMemoriesById(supabase, userId, memoryId, limit = 20) {
|
|
462
|
+
// Fetch the source memory
|
|
463
|
+
const source = await getMemory(supabase, userId, memoryId);
|
|
464
|
+
if (!source)
|
|
465
|
+
return { source: null, related: [], linkedNotes: [], entityMatches: [], supersessionChain: [] };
|
|
466
|
+
// Query knowledge_links for this memory (both directions)
|
|
467
|
+
const links = await getLinksForNode(supabase, userId, "memory", memoryId, {
|
|
468
|
+
direction: "both",
|
|
469
|
+
limit: limit * 2,
|
|
470
|
+
});
|
|
471
|
+
// Separate linked memory IDs and linked note IDs from knowledge_links
|
|
472
|
+
const linkedMemoryIds = new Set();
|
|
473
|
+
const linkedNoteIds = new Set();
|
|
474
|
+
for (const link of links) {
|
|
475
|
+
const otherId = link.source_id === memoryId ? link.target_id : link.source_id;
|
|
476
|
+
const otherType = link.source_id === memoryId ? link.target_type : link.source_type;
|
|
477
|
+
if (otherType === "memory" && otherId !== memoryId) {
|
|
478
|
+
linkedMemoryIds.add(otherId);
|
|
479
|
+
}
|
|
480
|
+
else if (otherType === "note") {
|
|
481
|
+
linkedNoteIds.add(otherId);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Fallback: also check JSONB related_memory_ids (for links not yet migrated)
|
|
485
|
+
for (const r of source.related_memory_ids ?? []) {
|
|
486
|
+
linkedMemoryIds.add(r.memoryId);
|
|
487
|
+
}
|
|
488
|
+
// Fallback: also check JSONB source_note_ids
|
|
489
|
+
for (const noteId of source.source_note_ids ?? []) {
|
|
490
|
+
linkedNoteIds.add(noteId);
|
|
491
|
+
}
|
|
492
|
+
const related = linkedMemoryIds.size > 0
|
|
493
|
+
? await getMemoriesByIds(supabase, userId, Array.from(linkedMemoryIds))
|
|
494
|
+
: [];
|
|
495
|
+
// Fetch linked notes
|
|
496
|
+
const linkedNotes = linkedNoteIds.size > 0
|
|
497
|
+
? await getNotesByIds(supabase, userId, Array.from(linkedNoteIds))
|
|
498
|
+
: [];
|
|
499
|
+
// Entity-based matches (excluding self and already-related)
|
|
500
|
+
const entities = source.entities ?? [];
|
|
501
|
+
let entityMatches = [];
|
|
502
|
+
if (entities.length > 0) {
|
|
503
|
+
const allEntityResults = [];
|
|
504
|
+
for (const entity of entities.slice(0, 5)) {
|
|
505
|
+
const matches = await searchMemoriesByEntity(supabase, userId, entity, limit);
|
|
506
|
+
allEntityResults.push(...matches);
|
|
507
|
+
}
|
|
508
|
+
const seen = new Set([memoryId, ...linkedMemoryIds]);
|
|
509
|
+
entityMatches = allEntityResults
|
|
510
|
+
.filter((m) => {
|
|
511
|
+
if (seen.has(m.id))
|
|
512
|
+
return false;
|
|
513
|
+
seen.add(m.id);
|
|
514
|
+
return true;
|
|
515
|
+
})
|
|
516
|
+
.slice(0, limit);
|
|
517
|
+
}
|
|
518
|
+
// Supersession chain: follow superseded_by_id forward, and look for memories that point to this one
|
|
519
|
+
const supersessionChain = [];
|
|
520
|
+
if (source.superseded_by_id) {
|
|
521
|
+
const successor = await getMemory(supabase, userId, source.superseded_by_id);
|
|
522
|
+
if (successor)
|
|
523
|
+
supersessionChain.push(successor);
|
|
524
|
+
}
|
|
525
|
+
const { data: predecessors, error: predError } = await supabase
|
|
526
|
+
.from("memories")
|
|
527
|
+
.select("*")
|
|
528
|
+
.eq("user_id", userId)
|
|
529
|
+
.eq("superseded_by_id", memoryId)
|
|
530
|
+
.limit(limit);
|
|
531
|
+
if (!predError && predecessors) {
|
|
532
|
+
supersessionChain.push(...predecessors);
|
|
533
|
+
}
|
|
534
|
+
return { source, related, linkedNotes, entityMatches, supersessionChain };
|
|
535
|
+
}
|
|
536
|
+
export async function insertKnowledgeLink(supabase, data) {
|
|
537
|
+
const { data: row, error } = await supabase
|
|
538
|
+
.from("knowledge_links")
|
|
539
|
+
.insert({
|
|
540
|
+
user_id: data.user_id,
|
|
541
|
+
source_type: data.source_type,
|
|
542
|
+
source_id: data.source_id,
|
|
543
|
+
target_type: data.target_type,
|
|
544
|
+
target_id: data.target_id,
|
|
545
|
+
relation_type: data.relation_type,
|
|
546
|
+
encrypted_label: data.encrypted_label ?? null,
|
|
547
|
+
label_iv: data.label_iv ?? null,
|
|
548
|
+
is_pending: data.is_pending ?? false,
|
|
549
|
+
pending_target_hash: data.pending_target_hash ?? null,
|
|
550
|
+
created_by: data.created_by ?? null,
|
|
551
|
+
})
|
|
552
|
+
.select()
|
|
553
|
+
.single();
|
|
554
|
+
if (error)
|
|
555
|
+
throw new Error(sanitizeDbError("insert knowledge link", error.message));
|
|
556
|
+
return row;
|
|
557
|
+
}
|
|
558
|
+
export async function deleteKnowledgeLink(supabase, linkId, userId) {
|
|
559
|
+
const { error } = await supabase
|
|
560
|
+
.from("knowledge_links")
|
|
561
|
+
.delete()
|
|
562
|
+
.eq("id", linkId)
|
|
563
|
+
.eq("user_id", userId);
|
|
564
|
+
if (error)
|
|
565
|
+
throw new Error(sanitizeDbError("delete knowledge link", error.message));
|
|
566
|
+
}
|
|
567
|
+
export async function getLinksForNode(supabase, userId, nodeType, nodeId, options) {
|
|
568
|
+
const direction = options?.direction ?? "both";
|
|
569
|
+
const limit = options?.limit ?? 100;
|
|
570
|
+
if (direction === "outgoing") {
|
|
571
|
+
const { data, error } = await supabase
|
|
572
|
+
.from("knowledge_links")
|
|
573
|
+
.select()
|
|
574
|
+
.eq("user_id", userId)
|
|
575
|
+
.eq("source_type", nodeType)
|
|
576
|
+
.eq("source_id", nodeId)
|
|
577
|
+
.order("created_at", { ascending: false })
|
|
578
|
+
.limit(limit);
|
|
579
|
+
if (error)
|
|
580
|
+
throw new Error(sanitizeDbError("fetch outgoing links", error.message));
|
|
581
|
+
return (data ?? []);
|
|
582
|
+
}
|
|
583
|
+
if (direction === "incoming") {
|
|
584
|
+
const { data, error } = await supabase
|
|
585
|
+
.from("knowledge_links")
|
|
586
|
+
.select()
|
|
587
|
+
.eq("user_id", userId)
|
|
588
|
+
.eq("target_type", nodeType)
|
|
589
|
+
.eq("target_id", nodeId)
|
|
590
|
+
.order("created_at", { ascending: false })
|
|
591
|
+
.limit(limit);
|
|
592
|
+
if (error)
|
|
593
|
+
throw new Error(sanitizeDbError("fetch incoming links", error.message));
|
|
594
|
+
return (data ?? []);
|
|
595
|
+
}
|
|
596
|
+
// Both directions — two queries merged
|
|
597
|
+
const [outgoing, incoming] = await Promise.all([
|
|
598
|
+
supabase
|
|
599
|
+
.from("knowledge_links")
|
|
600
|
+
.select()
|
|
601
|
+
.eq("user_id", userId)
|
|
602
|
+
.eq("source_type", nodeType)
|
|
603
|
+
.eq("source_id", nodeId)
|
|
604
|
+
.order("created_at", { ascending: false })
|
|
605
|
+
.limit(limit),
|
|
606
|
+
supabase
|
|
607
|
+
.from("knowledge_links")
|
|
608
|
+
.select()
|
|
609
|
+
.eq("user_id", userId)
|
|
610
|
+
.eq("target_type", nodeType)
|
|
611
|
+
.eq("target_id", nodeId)
|
|
612
|
+
.order("created_at", { ascending: false })
|
|
613
|
+
.limit(limit),
|
|
614
|
+
]);
|
|
615
|
+
if (outgoing.error)
|
|
616
|
+
throw new Error(sanitizeDbError("fetch outgoing links", outgoing.error.message));
|
|
617
|
+
if (incoming.error)
|
|
618
|
+
throw new Error(sanitizeDbError("fetch incoming links", incoming.error.message));
|
|
619
|
+
// Deduplicate by link ID (a link could appear in both if source and target are same type)
|
|
620
|
+
const seen = new Set();
|
|
621
|
+
const merged = [];
|
|
622
|
+
for (const row of [...(outgoing.data ?? []), ...(incoming.data ?? [])]) {
|
|
623
|
+
const link = row;
|
|
624
|
+
if (!seen.has(link.id)) {
|
|
625
|
+
seen.add(link.id);
|
|
626
|
+
merged.push(link);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return merged.slice(0, limit);
|
|
630
|
+
}
|
|
631
|
+
export async function getBacklinks(supabase, userId, nodeType, nodeId, limit = 50) {
|
|
632
|
+
return getLinksForNode(supabase, userId, nodeType, nodeId, {
|
|
633
|
+
direction: "incoming",
|
|
634
|
+
limit,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
export async function deleteLinksForNode(supabase, userId, nodeType, nodeId) {
|
|
638
|
+
// Delete outgoing links
|
|
639
|
+
const { error: outErr } = await supabase
|
|
640
|
+
.from("knowledge_links")
|
|
641
|
+
.delete()
|
|
642
|
+
.eq("user_id", userId)
|
|
643
|
+
.eq("source_type", nodeType)
|
|
644
|
+
.eq("source_id", nodeId);
|
|
645
|
+
if (outErr)
|
|
646
|
+
throw new Error(sanitizeDbError("delete outgoing links", outErr.message));
|
|
647
|
+
// Delete incoming links
|
|
648
|
+
const { error: inErr } = await supabase
|
|
649
|
+
.from("knowledge_links")
|
|
650
|
+
.delete()
|
|
651
|
+
.eq("user_id", userId)
|
|
652
|
+
.eq("target_type", nodeType)
|
|
653
|
+
.eq("target_id", nodeId);
|
|
654
|
+
if (inErr)
|
|
655
|
+
throw new Error(sanitizeDbError("delete incoming links", inErr.message));
|
|
656
|
+
}
|
|
657
|
+
export async function bulkInsertKnowledgeLinks(supabase, links) {
|
|
658
|
+
if (links.length === 0)
|
|
659
|
+
return 0;
|
|
660
|
+
const rows = links.map((l) => ({
|
|
661
|
+
user_id: l.user_id,
|
|
662
|
+
source_type: l.source_type,
|
|
663
|
+
source_id: l.source_id,
|
|
664
|
+
target_type: l.target_type,
|
|
665
|
+
target_id: l.target_id,
|
|
666
|
+
relation_type: l.relation_type,
|
|
667
|
+
encrypted_label: l.encrypted_label ?? null,
|
|
668
|
+
label_iv: l.label_iv ?? null,
|
|
669
|
+
is_pending: l.is_pending ?? false,
|
|
670
|
+
pending_target_hash: l.pending_target_hash ?? null,
|
|
671
|
+
created_by: l.created_by ?? null,
|
|
672
|
+
}));
|
|
673
|
+
// Use upsert with ignoreDuplicates to handle unique constraint violations
|
|
674
|
+
const { data, error } = await supabase
|
|
675
|
+
.from("knowledge_links")
|
|
676
|
+
.upsert(rows, { onConflict: "user_id,source_type,source_id,target_type,target_id,relation_type", ignoreDuplicates: true })
|
|
677
|
+
.select("id");
|
|
678
|
+
if (error)
|
|
679
|
+
throw new Error(sanitizeDbError("bulk insert knowledge links", error.message));
|
|
680
|
+
return data?.length ?? 0;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Resolves pending wiki-links by matching stored SHA-256 hashes against
|
|
684
|
+
* candidate note names. Caller provides a map of hash → noteId.
|
|
685
|
+
* Use `hashPendingTarget()` from crypto.ts to build the map keys.
|
|
686
|
+
*/
|
|
687
|
+
export async function resolvePendingLinks(supabase, userId, hashToNoteId) {
|
|
688
|
+
if (hashToNoteId.size === 0)
|
|
689
|
+
return 0;
|
|
690
|
+
// Fetch all pending links for this user
|
|
691
|
+
const { data: pending, error } = await supabase
|
|
692
|
+
.from("knowledge_links")
|
|
693
|
+
.select()
|
|
694
|
+
.eq("user_id", userId)
|
|
695
|
+
.eq("is_pending", true)
|
|
696
|
+
.limit(10_000);
|
|
697
|
+
if (error)
|
|
698
|
+
throw new Error(sanitizeDbError("fetch pending links", error.message));
|
|
699
|
+
if (!pending || pending.length === 0)
|
|
700
|
+
return 0;
|
|
701
|
+
let resolved = 0;
|
|
702
|
+
for (const link of pending) {
|
|
703
|
+
if (!link.pending_target_hash)
|
|
704
|
+
continue;
|
|
705
|
+
const noteId = hashToNoteId.get(link.pending_target_hash);
|
|
706
|
+
if (!noteId)
|
|
707
|
+
continue;
|
|
708
|
+
const { error: updateErr } = await supabase
|
|
709
|
+
.from("knowledge_links")
|
|
710
|
+
.update({
|
|
711
|
+
target_id: noteId,
|
|
712
|
+
target_type: "note",
|
|
713
|
+
is_pending: false,
|
|
714
|
+
pending_target_hash: null,
|
|
715
|
+
})
|
|
716
|
+
.eq("id", link.id)
|
|
717
|
+
.eq("user_id", userId);
|
|
718
|
+
if (!updateErr)
|
|
719
|
+
resolved++;
|
|
720
|
+
}
|
|
721
|
+
return resolved;
|
|
722
|
+
}
|
|
723
|
+
export async function getKnowledgeGraph(supabase, userId, nodeType, nodeId, maxHops = 2, maxNodes = 50) {
|
|
724
|
+
const { data, error } = await supabase.rpc("get_knowledge_graph", {
|
|
725
|
+
p_user_id: userId,
|
|
726
|
+
p_node_type: nodeType,
|
|
727
|
+
p_node_id: nodeId,
|
|
728
|
+
p_max_hops: maxHops,
|
|
729
|
+
p_max_nodes: maxNodes,
|
|
730
|
+
});
|
|
731
|
+
if (error)
|
|
732
|
+
throw new Error(sanitizeDbError("traverse knowledge graph", error.message));
|
|
733
|
+
return (data ?? []);
|
|
734
|
+
}
|
|
735
|
+
export async function insertAgentMessage(supabase, data) {
|
|
736
|
+
const { data: row, error } = await supabase
|
|
737
|
+
.from("agent_messages")
|
|
738
|
+
.insert({
|
|
739
|
+
user_id: data.user_id,
|
|
740
|
+
vault_id: data.vault_id ?? null,
|
|
741
|
+
sender_type: data.sender_type,
|
|
742
|
+
sender_id: data.sender_id,
|
|
743
|
+
target_type: data.target_type,
|
|
744
|
+
target_id: data.target_id,
|
|
745
|
+
category: data.category ?? "info",
|
|
746
|
+
priority: data.priority ?? 3,
|
|
747
|
+
subject: data.subject ?? null,
|
|
748
|
+
encrypted_content: data.encrypted_content,
|
|
749
|
+
content_iv: data.content_iv,
|
|
750
|
+
expires_at: data.expires_at ?? undefined,
|
|
751
|
+
metadata: data.metadata ?? null,
|
|
752
|
+
parent_message_id: data.parent_message_id ?? null,
|
|
753
|
+
thread_id: data.thread_id ?? null,
|
|
754
|
+
})
|
|
755
|
+
.select("*")
|
|
756
|
+
.single();
|
|
757
|
+
if (error)
|
|
758
|
+
throw new Error(sanitizeDbError("insert agent message", error.message));
|
|
759
|
+
return row;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Fetch pending messages for a target agent (or broadcast).
|
|
763
|
+
* Returns priority-sorted, non-expired messages.
|
|
764
|
+
*/
|
|
765
|
+
export async function getPendingMessages(supabase, userId, targetId, limit = 5) {
|
|
766
|
+
const { data, error } = await supabase
|
|
767
|
+
.from("agent_messages")
|
|
768
|
+
.select("*")
|
|
769
|
+
.eq("user_id", userId)
|
|
770
|
+
.eq("status", "pending")
|
|
771
|
+
.or(`target_id.eq.${targetId},target_id.eq.*`)
|
|
772
|
+
.gte("expires_at", new Date().toISOString())
|
|
773
|
+
.order("priority", { ascending: false })
|
|
774
|
+
.order("created_at", { ascending: true })
|
|
775
|
+
.limit(limit);
|
|
776
|
+
if (error)
|
|
777
|
+
throw new Error(sanitizeDbError("fetch pending messages", error.message));
|
|
778
|
+
return (data ?? []);
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Fetch messages with flexible filtering.
|
|
782
|
+
*/
|
|
783
|
+
export async function getAgentMessages(supabase, userId, targetId, options) {
|
|
784
|
+
const limit = options?.limit ?? 20;
|
|
785
|
+
const includeBroadcast = options?.includeBroadcast ?? true;
|
|
786
|
+
let query = supabase
|
|
787
|
+
.from("agent_messages")
|
|
788
|
+
.select("*")
|
|
789
|
+
.eq("user_id", userId)
|
|
790
|
+
.order("priority", { ascending: false })
|
|
791
|
+
.order("created_at", { ascending: false })
|
|
792
|
+
.limit(limit);
|
|
793
|
+
if (includeBroadcast) {
|
|
794
|
+
query = query.or(`target_id.eq.${targetId},target_id.eq.*`);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
query = query.eq("target_id", targetId);
|
|
798
|
+
}
|
|
799
|
+
if (options?.status)
|
|
800
|
+
query = query.eq("status", options.status);
|
|
801
|
+
if (options?.category)
|
|
802
|
+
query = query.eq("category", options.category);
|
|
803
|
+
if (options?.vaultId)
|
|
804
|
+
query = query.eq("vault_id", options.vaultId);
|
|
805
|
+
const { data, error } = await query;
|
|
806
|
+
if (error)
|
|
807
|
+
throw new Error(sanitizeDbError("fetch agent messages", error.message));
|
|
808
|
+
return (data ?? []);
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Bulk-update message status and optional extra fields (e.g. delivered_at, acknowledged_at).
|
|
812
|
+
*/
|
|
813
|
+
export async function bulkUpdateMessageStatus(supabase, messageIds, userId, status, extraFields) {
|
|
814
|
+
if (messageIds.length === 0)
|
|
815
|
+
return;
|
|
816
|
+
const { error } = await supabase
|
|
817
|
+
.from("agent_messages")
|
|
818
|
+
.update({
|
|
819
|
+
status,
|
|
820
|
+
updated_at: new Date().toISOString(),
|
|
821
|
+
...extraFields,
|
|
822
|
+
})
|
|
823
|
+
.eq("user_id", userId)
|
|
824
|
+
.in("id", messageIds);
|
|
825
|
+
if (error)
|
|
826
|
+
throw new Error(sanitizeDbError("bulk update message status", error.message));
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Resolve thread ID for a reply. If the parent has a thread_id, use it.
|
|
830
|
+
* If not, the parent becomes the thread root (set parent's thread_id = parent.id).
|
|
831
|
+
* Returns the resolved thread_id.
|
|
832
|
+
*/
|
|
833
|
+
export async function resolveThreadId(supabase, userId, parentMessageId) {
|
|
834
|
+
const { data: parent, error } = await supabase
|
|
835
|
+
.from("agent_messages")
|
|
836
|
+
.select("id, thread_id")
|
|
837
|
+
.eq("id", parentMessageId)
|
|
838
|
+
.eq("user_id", userId)
|
|
839
|
+
.single();
|
|
840
|
+
if (error || !parent)
|
|
841
|
+
throw new Error("Parent message not found");
|
|
842
|
+
if (parent.thread_id) {
|
|
843
|
+
return parent.thread_id;
|
|
844
|
+
}
|
|
845
|
+
// Parent becomes the thread root
|
|
846
|
+
await supabase
|
|
847
|
+
.from("agent_messages")
|
|
848
|
+
.update({ thread_id: parent.id, updated_at: new Date().toISOString() })
|
|
849
|
+
.eq("id", parent.id)
|
|
850
|
+
.eq("user_id", userId);
|
|
851
|
+
return parent.id;
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Fetch all messages in a thread, ordered chronologically.
|
|
855
|
+
*/
|
|
856
|
+
export async function getThread(supabase, userId, threadId) {
|
|
857
|
+
const { data, error } = await supabase
|
|
858
|
+
.from("agent_messages")
|
|
859
|
+
.select("*")
|
|
860
|
+
.eq("user_id", userId)
|
|
861
|
+
.eq("thread_id", threadId)
|
|
862
|
+
.order("created_at", { ascending: true });
|
|
863
|
+
if (error)
|
|
864
|
+
throw new Error(sanitizeDbError("fetch thread", error.message));
|
|
865
|
+
return (data ?? []);
|
|
866
|
+
}
|