exovault-mcp-server 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/db.d.ts +2 -0
- package/dist/db.js +28 -17
- package/dist/embedding-config.js +1 -1
- package/dist/episodic-headline.d.ts +5 -2
- package/dist/episodic-headline.js +15 -7
- package/dist/gateway-client.d.ts +23 -3
- package/dist/gateway-client.js +13 -6
- package/dist/index.js +45 -10
- package/dist/scripts/backfill-memory-embeddings.js +1 -1
- package/dist/session-buffer.js +29 -11
- package/dist/tools/read-memories.d.ts +1 -1
- package/dist/tools/read-memories.js +1 -1
- package/dist/tools/read-note.d.ts +1 -1
- package/dist/tools/read-note.js +1 -1
- package/dist/tools/read-notes.d.ts +1 -1
- package/dist/tools/read-notes.js +1 -1
- package/dist/tools/update-memory.d.ts +1 -0
- package/dist/tools/update-memory.js +2 -0
- package/dist/tools/view-media.d.ts +6 -0
- package/dist/tools/view-media.js +53 -0
- package/package.json +1 -1
package/dist/db.d.ts
CHANGED
|
@@ -97,6 +97,8 @@ export declare function getMemories(supabase: SupabaseClient, userId: string, op
|
|
|
97
97
|
includeArchived?: boolean;
|
|
98
98
|
limit?: number;
|
|
99
99
|
orderBy?: "updated_at" | "importance";
|
|
100
|
+
/** Use lightweight columns (no encrypted content/summary). Default: false. */
|
|
101
|
+
lightweight?: boolean;
|
|
100
102
|
}): Promise<MemoryRow[]>;
|
|
101
103
|
export declare function getMemory(supabase: SupabaseClient, userId: string, memoryId: string): Promise<MemoryRow | null>;
|
|
102
104
|
export declare function getMemoryByExternalWriteId(supabase: SupabaseClient, userId: string, externalWriteId: string): Promise<MemoryRow | null>;
|
package/dist/db.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { sanitizeDbError } from "./error-sanitizer.js";
|
|
2
|
+
// ─── Column lists (explicit to avoid SELECT * egress overhead) ───────────────
|
|
3
|
+
const VAULT_COLUMNS = "id, user_id, encrypted_name, name_iv, icon, color, sort_order, created_at, updated_at";
|
|
4
|
+
const NOTE_COLUMNS = "id, user_id, vault_id, encrypted_title, title_iv, encrypted_content, content_iv, content_type, encrypted_tags, tags_iv, embedding_status, import_source, import_source_id, is_trashed, created_at, updated_at";
|
|
5
|
+
const LINK_COLUMNS = "id, user_id, source_type, source_id, target_type, target_id, relation_type, encrypted_label, label_iv, is_pending, pending_target_hash, created_at, created_by";
|
|
6
|
+
const MESSAGE_COLUMNS = "id, user_id, vault_id, sender_type, sender_id, target_type, target_id, category, priority, subject, encrypted_content, content_iv, status, delivered_at, acknowledged_at, parent_message_id, thread_id, expires_at, metadata, created_at, updated_at";
|
|
2
7
|
// ─── Query helpers ────────────────────────────────────────────────────────────
|
|
3
8
|
export async function getVaults(supabase, userId) {
|
|
4
9
|
const { data, error } = await supabase
|
|
5
10
|
.from("vaults")
|
|
6
|
-
.select(
|
|
11
|
+
.select(VAULT_COLUMNS)
|
|
7
12
|
.eq("user_id", userId)
|
|
8
13
|
.order("sort_order", { ascending: true });
|
|
9
14
|
if (error)
|
|
@@ -13,7 +18,7 @@ export async function getVaults(supabase, userId) {
|
|
|
13
18
|
export async function getNotes(supabase, userId, vaultId, limit = 20) {
|
|
14
19
|
let query = supabase
|
|
15
20
|
.from("notes")
|
|
16
|
-
.select(
|
|
21
|
+
.select(NOTE_COLUMNS)
|
|
17
22
|
.eq("user_id", userId)
|
|
18
23
|
.eq("is_trashed", false)
|
|
19
24
|
.order("updated_at", { ascending: false })
|
|
@@ -29,7 +34,7 @@ export async function getNotes(supabase, userId, vaultId, limit = 20) {
|
|
|
29
34
|
export async function getNote(supabase, userId, noteId) {
|
|
30
35
|
const { data, error } = await supabase
|
|
31
36
|
.from("notes")
|
|
32
|
-
.select(
|
|
37
|
+
.select(NOTE_COLUMNS)
|
|
33
38
|
.eq("id", noteId)
|
|
34
39
|
.eq("user_id", userId)
|
|
35
40
|
.single();
|
|
@@ -43,7 +48,7 @@ export async function getNote(supabase, userId, noteId) {
|
|
|
43
48
|
export async function getVault(supabase, userId, vaultId) {
|
|
44
49
|
const { data, error } = await supabase
|
|
45
50
|
.from("vaults")
|
|
46
|
-
.select(
|
|
51
|
+
.select(VAULT_COLUMNS)
|
|
47
52
|
.eq("id", vaultId)
|
|
48
53
|
.eq("user_id", userId)
|
|
49
54
|
.single();
|
|
@@ -102,9 +107,15 @@ export async function getMemories(supabase, userId, options) {
|
|
|
102
107
|
const includeArchived = options?.includeArchived ?? false;
|
|
103
108
|
const limit = options?.limit ?? 50;
|
|
104
109
|
const orderBy = options?.orderBy ?? "updated_at";
|
|
110
|
+
// Lightweight mode: select only metadata columns (no encrypted content/summary) to reduce egress.
|
|
111
|
+
// Supabase's TS type parser can't handle long literal strings, so we build the query separately.
|
|
112
|
+
const columns = options?.lightweight
|
|
113
|
+
? "id,user_id,vault_id,memory_type,agent_id,agent_type,model_id,agent_run_id,importance,confidence,access_count,is_archived,entities,superseded_by_id,created_at,updated_at"
|
|
114
|
+
: "*";
|
|
105
115
|
let query = supabase
|
|
106
116
|
.from("memories")
|
|
107
|
-
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
+
.select(columns)
|
|
108
119
|
.eq("user_id", userId)
|
|
109
120
|
.order(orderBy, { ascending: false })
|
|
110
121
|
.limit(limit);
|
|
@@ -339,7 +350,7 @@ export async function getNotesByIds(supabase, userId, noteIds) {
|
|
|
339
350
|
return [];
|
|
340
351
|
const { data, error } = await supabase
|
|
341
352
|
.from("notes")
|
|
342
|
-
.select(
|
|
353
|
+
.select(NOTE_COLUMNS)
|
|
343
354
|
.eq("user_id", userId)
|
|
344
355
|
.eq("is_trashed", false)
|
|
345
356
|
.in("id", noteIds);
|
|
@@ -350,7 +361,7 @@ export async function getNotesByIds(supabase, userId, noteIds) {
|
|
|
350
361
|
export async function getNoteByImportSource(supabase, userId, vaultId, importSource, importSourceId) {
|
|
351
362
|
const { data, error } = await supabase
|
|
352
363
|
.from("notes")
|
|
353
|
-
.select(
|
|
364
|
+
.select(NOTE_COLUMNS)
|
|
354
365
|
.eq("user_id", userId)
|
|
355
366
|
.eq("vault_id", vaultId)
|
|
356
367
|
.eq("import_source", importSource)
|
|
@@ -583,7 +594,7 @@ export async function insertKnowledgeLink(supabase, data) {
|
|
|
583
594
|
pending_target_hash: data.pending_target_hash ?? null,
|
|
584
595
|
created_by: data.created_by ?? null,
|
|
585
596
|
})
|
|
586
|
-
.select()
|
|
597
|
+
.select(LINK_COLUMNS)
|
|
587
598
|
.single();
|
|
588
599
|
if (error)
|
|
589
600
|
throw new Error(sanitizeDbError("insert knowledge link", error.message));
|
|
@@ -604,7 +615,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
604
615
|
if (direction === "outgoing") {
|
|
605
616
|
const { data, error } = await supabase
|
|
606
617
|
.from("knowledge_links")
|
|
607
|
-
.select()
|
|
618
|
+
.select(LINK_COLUMNS)
|
|
608
619
|
.eq("user_id", userId)
|
|
609
620
|
.eq("source_type", nodeType)
|
|
610
621
|
.eq("source_id", nodeId)
|
|
@@ -617,7 +628,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
617
628
|
if (direction === "incoming") {
|
|
618
629
|
const { data, error } = await supabase
|
|
619
630
|
.from("knowledge_links")
|
|
620
|
-
.select()
|
|
631
|
+
.select(LINK_COLUMNS)
|
|
621
632
|
.eq("user_id", userId)
|
|
622
633
|
.eq("target_type", nodeType)
|
|
623
634
|
.eq("target_id", nodeId)
|
|
@@ -631,7 +642,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
631
642
|
const [outgoing, incoming] = await Promise.all([
|
|
632
643
|
supabase
|
|
633
644
|
.from("knowledge_links")
|
|
634
|
-
.select()
|
|
645
|
+
.select(LINK_COLUMNS)
|
|
635
646
|
.eq("user_id", userId)
|
|
636
647
|
.eq("source_type", nodeType)
|
|
637
648
|
.eq("source_id", nodeId)
|
|
@@ -639,7 +650,7 @@ export async function getLinksForNode(supabase, userId, nodeType, nodeId, option
|
|
|
639
650
|
.limit(limit),
|
|
640
651
|
supabase
|
|
641
652
|
.from("knowledge_links")
|
|
642
|
-
.select()
|
|
653
|
+
.select(LINK_COLUMNS)
|
|
643
654
|
.eq("user_id", userId)
|
|
644
655
|
.eq("target_type", nodeType)
|
|
645
656
|
.eq("target_id", nodeId)
|
|
@@ -724,7 +735,7 @@ export async function resolvePendingLinks(supabase, userId, hashToNoteId) {
|
|
|
724
735
|
// Fetch all pending links for this user
|
|
725
736
|
const { data: pending, error } = await supabase
|
|
726
737
|
.from("knowledge_links")
|
|
727
|
-
.select()
|
|
738
|
+
.select(LINK_COLUMNS)
|
|
728
739
|
.eq("user_id", userId)
|
|
729
740
|
.eq("is_pending", true)
|
|
730
741
|
.limit(10_000);
|
|
@@ -786,7 +797,7 @@ export async function insertAgentMessage(supabase, data) {
|
|
|
786
797
|
parent_message_id: data.parent_message_id ?? null,
|
|
787
798
|
thread_id: data.thread_id ?? null,
|
|
788
799
|
})
|
|
789
|
-
.select(
|
|
800
|
+
.select(MESSAGE_COLUMNS)
|
|
790
801
|
.single();
|
|
791
802
|
if (error)
|
|
792
803
|
throw new Error(sanitizeDbError("insert agent message", error.message));
|
|
@@ -799,7 +810,7 @@ export async function insertAgentMessage(supabase, data) {
|
|
|
799
810
|
export async function getPendingMessages(supabase, userId, targetId, limit = 5) {
|
|
800
811
|
const { data, error } = await supabase
|
|
801
812
|
.from("agent_messages")
|
|
802
|
-
.select(
|
|
813
|
+
.select(MESSAGE_COLUMNS)
|
|
803
814
|
.eq("user_id", userId)
|
|
804
815
|
.eq("status", "pending")
|
|
805
816
|
.or(`target_id.eq.${targetId},target_id.eq.*`)
|
|
@@ -819,7 +830,7 @@ export async function getAgentMessages(supabase, userId, targetId, options) {
|
|
|
819
830
|
const includeBroadcast = options?.includeBroadcast ?? true;
|
|
820
831
|
let query = supabase
|
|
821
832
|
.from("agent_messages")
|
|
822
|
-
.select(
|
|
833
|
+
.select(MESSAGE_COLUMNS)
|
|
823
834
|
.eq("user_id", userId)
|
|
824
835
|
.order("priority", { ascending: false })
|
|
825
836
|
.order("created_at", { ascending: false })
|
|
@@ -890,7 +901,7 @@ export async function resolveThreadId(supabase, userId, parentMessageId) {
|
|
|
890
901
|
export async function getThread(supabase, userId, threadId) {
|
|
891
902
|
const { data, error } = await supabase
|
|
892
903
|
.from("agent_messages")
|
|
893
|
-
.select(
|
|
904
|
+
.select(MESSAGE_COLUMNS)
|
|
894
905
|
.eq("user_id", userId)
|
|
895
906
|
.eq("thread_id", threadId)
|
|
896
907
|
.order("created_at", { ascending: true });
|
package/dist/embedding-config.js
CHANGED
|
@@ -10,7 +10,7 @@ export function resolveEmbeddingConfig(ctx) {
|
|
|
10
10
|
if (envApiKey) {
|
|
11
11
|
return {
|
|
12
12
|
apiKey: envApiKey,
|
|
13
|
-
model:
|
|
13
|
+
model: DEFAULT_EMBEDDING_MODEL, // Hardcoded — EMBEDDING_MODEL is for OpenAI text
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
// Fallback to MCP context keys
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extracts a short headline (max 200 chars) from episodic memory content.
|
|
3
|
-
*
|
|
4
|
-
* first
|
|
3
|
+
*
|
|
4
|
+
* New format: first line is the session title, followed by details.
|
|
5
|
+
* Legacy format: "Session: Xm, agent=Y." prefix is stripped.
|
|
6
|
+
*
|
|
7
|
+
* Pure function, no LLM calls.
|
|
5
8
|
*/
|
|
6
9
|
export declare function generateEpisodicHeadline(content: string): string;
|
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extracts a short headline (max 200 chars) from episodic memory content.
|
|
3
|
-
*
|
|
4
|
-
* first
|
|
3
|
+
*
|
|
4
|
+
* New format: first line is the session title, followed by details.
|
|
5
|
+
* Legacy format: "Session: Xm, agent=Y." prefix is stripped.
|
|
6
|
+
*
|
|
7
|
+
* Pure function, no LLM calls.
|
|
5
8
|
*/
|
|
6
9
|
const MAX_HEADLINE_CHARS = 200;
|
|
7
|
-
//
|
|
10
|
+
// Legacy prefix — matches "Session: <duration>, agent=<name>." (with optional newlines after)
|
|
8
11
|
const SESSION_PREFIX_RE = /^Session:\s*\S+,\s*agent=\S+\.\s*/;
|
|
9
12
|
export function generateEpisodicHeadline(content) {
|
|
10
13
|
if (!content)
|
|
11
14
|
return "";
|
|
12
|
-
// Strip
|
|
15
|
+
// Strip legacy metadata prefix if present
|
|
13
16
|
const body = content.replace(SESSION_PREFIX_RE, "").trim();
|
|
14
17
|
// If stripping left nothing, return original content
|
|
15
18
|
if (!body)
|
|
16
19
|
return content;
|
|
17
|
-
//
|
|
18
|
-
|
|
20
|
+
// First line is the session title — use it as headline if it fits
|
|
21
|
+
const firstLineEnd = body.indexOf("\n");
|
|
22
|
+
const firstLine = firstLineEnd > 0 ? body.slice(0, firstLineEnd).trim() : body.trim();
|
|
23
|
+
// If first line is short enough and not a "Explored:/Outcomes:" detail line, use it directly
|
|
24
|
+
if (firstLine.length <= MAX_HEADLINE_CHARS && !firstLine.startsWith("Explored:") && !firstLine.startsWith("Outcomes:")) {
|
|
25
|
+
return firstLine;
|
|
26
|
+
}
|
|
27
|
+
// Fallback: take first 1-2 sentences from body
|
|
19
28
|
const sentences = splitIntoSentences(body);
|
|
20
29
|
// Take enough sentences to fill ~200 chars
|
|
21
30
|
let headline = "";
|
|
@@ -24,7 +33,6 @@ export function generateEpisodicHeadline(content) {
|
|
|
24
33
|
continue;
|
|
25
34
|
const candidate = headline ? `${headline} ${sentence}` : sentence;
|
|
26
35
|
if (candidate.length > MAX_HEADLINE_CHARS) {
|
|
27
|
-
// If we have nothing yet, take this sentence (will be clipped)
|
|
28
36
|
if (!headline)
|
|
29
37
|
headline = sentence;
|
|
30
38
|
break;
|
package/dist/gateway-client.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export declare class GatewayClient {
|
|
|
69
69
|
graphSeeds?: number;
|
|
70
70
|
graphMaxHops?: number;
|
|
71
71
|
}): Promise<string>;
|
|
72
|
-
readMemories(memoryIds: string[]): Promise<string>;
|
|
72
|
+
readMemories(memoryIds: string[], includeMediaContent?: boolean): Promise<string>;
|
|
73
73
|
updateMemory(params: {
|
|
74
74
|
memoryId: string;
|
|
75
75
|
content?: string;
|
|
@@ -136,8 +136,8 @@ export declare class GatewayClient {
|
|
|
136
136
|
limit?: number;
|
|
137
137
|
folderId?: string;
|
|
138
138
|
}): Promise<string>;
|
|
139
|
-
readNote(noteId: string): Promise<string>;
|
|
140
|
-
readNotes(noteIds: string[]): Promise<string>;
|
|
139
|
+
readNote(noteId: string, includeMediaContent?: boolean): Promise<string>;
|
|
140
|
+
readNotes(noteIds: string[], includeMediaContent?: boolean): Promise<string>;
|
|
141
141
|
searchNotes(params: {
|
|
142
142
|
query: string;
|
|
143
143
|
topK?: number;
|
|
@@ -195,6 +195,10 @@ export declare class GatewayClient {
|
|
|
195
195
|
icon?: string;
|
|
196
196
|
color?: string;
|
|
197
197
|
}): Promise<string>;
|
|
198
|
+
deleteVault(params: {
|
|
199
|
+
vaultId: string;
|
|
200
|
+
confirm: true;
|
|
201
|
+
}): Promise<string>;
|
|
198
202
|
createTask(params: {
|
|
199
203
|
title: string;
|
|
200
204
|
description?: string;
|
|
@@ -344,5 +348,21 @@ export declare class GatewayClient {
|
|
|
344
348
|
embeddingStatus: string;
|
|
345
349
|
base64Content: string;
|
|
346
350
|
}>;
|
|
351
|
+
viewMedia(params: {
|
|
352
|
+
attachmentId: string;
|
|
353
|
+
}): Promise<{
|
|
354
|
+
attachmentId: string;
|
|
355
|
+
memoryId: string | null;
|
|
356
|
+
noteId: string | null;
|
|
357
|
+
modality: string;
|
|
358
|
+
mimeType: string;
|
|
359
|
+
fileName: string | null;
|
|
360
|
+
fileSizeBytes: number;
|
|
361
|
+
embeddingStatus: string;
|
|
362
|
+
extractionStatus: string;
|
|
363
|
+
extractedText: string | null;
|
|
364
|
+
base64Content: string | null;
|
|
365
|
+
contentNote: string | null;
|
|
366
|
+
}>;
|
|
347
367
|
deleteMedia(attachmentId: string, vaultId?: string): Promise<string>;
|
|
348
368
|
}
|
package/dist/gateway-client.js
CHANGED
|
@@ -102,8 +102,8 @@ export class GatewayClient {
|
|
|
102
102
|
const result = await this.request("POST", "/api/agent/search-memories", params);
|
|
103
103
|
return JSON.stringify(result);
|
|
104
104
|
}
|
|
105
|
-
async readMemories(memoryIds) {
|
|
106
|
-
const result = await this.request("POST", "/api/agent/read-memories", { memoryIds });
|
|
105
|
+
async readMemories(memoryIds, includeMediaContent) {
|
|
106
|
+
const result = await this.request("POST", "/api/agent/read-memories", { memoryIds, includeMediaContent });
|
|
107
107
|
return JSON.stringify(result);
|
|
108
108
|
}
|
|
109
109
|
async updateMemory(params) {
|
|
@@ -145,12 +145,12 @@ export class GatewayClient {
|
|
|
145
145
|
const result = await this.request("POST", "/api/agent/list-notes", params);
|
|
146
146
|
return JSON.stringify(result);
|
|
147
147
|
}
|
|
148
|
-
async readNote(noteId) {
|
|
149
|
-
const result = await this.request("POST", "/api/agent/read-note", { noteId });
|
|
148
|
+
async readNote(noteId, includeMediaContent) {
|
|
149
|
+
const result = await this.request("POST", "/api/agent/read-note", { noteId, includeMediaContent });
|
|
150
150
|
return JSON.stringify(result);
|
|
151
151
|
}
|
|
152
|
-
async readNotes(noteIds) {
|
|
153
|
-
const result = await this.request("POST", "/api/agent/read-notes", { noteIds });
|
|
152
|
+
async readNotes(noteIds, includeMediaContent) {
|
|
153
|
+
const result = await this.request("POST", "/api/agent/read-notes", { noteIds, includeMediaContent });
|
|
154
154
|
return JSON.stringify(result);
|
|
155
155
|
}
|
|
156
156
|
async searchNotes(params) {
|
|
@@ -191,6 +191,10 @@ export class GatewayClient {
|
|
|
191
191
|
const result = await this.request("POST", "/api/agent/create-vault", params);
|
|
192
192
|
return JSON.stringify(result);
|
|
193
193
|
}
|
|
194
|
+
async deleteVault(params) {
|
|
195
|
+
const result = await this.request("POST", "/api/agent/delete-vault", params);
|
|
196
|
+
return JSON.stringify(result);
|
|
197
|
+
}
|
|
194
198
|
// ─── Task operations ─────────────────────────────────────────────────────
|
|
195
199
|
async createTask(params) {
|
|
196
200
|
const result = await this.request("POST", "/api/agent/create-task", params);
|
|
@@ -323,6 +327,9 @@ export class GatewayClient {
|
|
|
323
327
|
async downloadMedia(params) {
|
|
324
328
|
return this.request("POST", "/api/agent/download-media", params);
|
|
325
329
|
}
|
|
330
|
+
async viewMedia(params) {
|
|
331
|
+
return this.request("POST", "/api/agent/view-media", params);
|
|
332
|
+
}
|
|
326
333
|
async deleteMedia(attachmentId, vaultId) {
|
|
327
334
|
const body = { attachmentId };
|
|
328
335
|
if (vaultId)
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { exploreGraph } from "./tools/explore-graph.js";
|
|
|
30
30
|
import { recall } from "./tools/recall.js";
|
|
31
31
|
import { search } from "./tools/search.js";
|
|
32
32
|
import { sendMessage, ackMessage, readMessages } from "./tools/agent-messages.js";
|
|
33
|
+
import { viewMedia } from "./tools/view-media.js";
|
|
33
34
|
// Task tools are thin wrappers around memory tools — no separate agent-tasks import needed
|
|
34
35
|
import { resolveVaultId } from "./tools/resolve-vault-id.js";
|
|
35
36
|
import { GatewayClient } from "./gateway-client.js";
|
|
@@ -48,7 +49,7 @@ import { scanOrphanedBuffers, deleteBuffer as deleteBufferFile } from "./buffer-
|
|
|
48
49
|
import { flushSession } from "./session-flush.js";
|
|
49
50
|
import { coerceSchema } from "./coerce-params.js";
|
|
50
51
|
const s = (schema) => coerceSchema(schema);
|
|
51
|
-
const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction"];
|
|
52
|
+
const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction", "decision"];
|
|
52
53
|
const memoryTypeEnum = z.enum(MEMORY_TYPES);
|
|
53
54
|
/** Remind agents to checkpoint every N tool calls. */
|
|
54
55
|
const CHECKPOINT_REMINDER_INTERVAL = 20;
|
|
@@ -351,7 +352,7 @@ async function main() {
|
|
|
351
352
|
" b. **WRITE on these triggers** — call `write_memory` IMMEDIATELY when:",
|
|
352
353
|
" - User states a preference, rule, or convention → `preference` or `constraint`",
|
|
353
354
|
" - You discover a non-obvious fact about the domain/project/topic → `fact` (importance 3-5)",
|
|
354
|
-
" - A decision is made (by user or jointly) → `
|
|
355
|
+
" - A decision is made (by user or jointly) → `decision` (importance 4-5, include inputs, reasoning, alternatives, outcome)",
|
|
355
356
|
" - You solve a problem or learn a procedure → `skill`",
|
|
356
357
|
" - You hit a surprising gotcha or limitation → `skill` (importance 4)",
|
|
357
358
|
" - Previous knowledge turns out wrong → `correction` (set supersededById)",
|
|
@@ -411,6 +412,20 @@ async function main() {
|
|
|
411
412
|
? await gw.createVault({ name, icon, color })
|
|
412
413
|
: await createVault(ctx, name, { icon, color });
|
|
413
414
|
})));
|
|
415
|
+
// ─── delete_vault ──────────────────────────────────────────────────────────
|
|
416
|
+
server.registerTool("delete_vault", {
|
|
417
|
+
description: "Permanently delete a vault and all its notes/folders. Memories are preserved (vault_id set to null). Requires admin scope and confirm: true. Cannot delete the default vault.",
|
|
418
|
+
inputSchema: {
|
|
419
|
+
vaultId: s(z.string().uuid().describe("The vault ID to delete")),
|
|
420
|
+
confirm: s(z.literal(true).describe("Must be true to confirm deletion")),
|
|
421
|
+
},
|
|
422
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
423
|
+
const { vaultId, confirm } = args;
|
|
424
|
+
if (!gw) {
|
|
425
|
+
return JSON.stringify({ success: false, error: "delete_vault requires gateway mode" });
|
|
426
|
+
}
|
|
427
|
+
return await gw.deleteVault({ vaultId, confirm });
|
|
428
|
+
})));
|
|
414
429
|
// ─── list_notes ───────────────────────────────────────────────────────────
|
|
415
430
|
server.registerTool("list_notes", {
|
|
416
431
|
description: "List notes with titles, tags, and content previews. Optionally filter by vault or folder.",
|
|
@@ -430,10 +445,11 @@ async function main() {
|
|
|
430
445
|
description: "Read the full decrypted content of a note, including title, tags, and vault name",
|
|
431
446
|
inputSchema: {
|
|
432
447
|
noteId: s(z.string().uuid().describe("The note ID to read")),
|
|
448
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
433
449
|
},
|
|
434
450
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
435
|
-
const { noteId } = args;
|
|
436
|
-
return gw ? await gw.readNote(noteId) : await readNote(ctx, noteId);
|
|
451
|
+
const { noteId, includeMediaContent } = args;
|
|
452
|
+
return gw ? await gw.readNote(noteId, includeMediaContent) : await readNote(ctx, noteId, includeMediaContent);
|
|
437
453
|
})));
|
|
438
454
|
// ─── search_notes ─────────────────────────────────────────────────────────
|
|
439
455
|
server.registerTool("search_notes", {
|
|
@@ -548,17 +564,18 @@ async function main() {
|
|
|
548
564
|
description: "Read the full decrypted content of multiple notes at once. Returns all notes with their titles, content, tags, and vault names. Reports any IDs that were not found.",
|
|
549
565
|
inputSchema: {
|
|
550
566
|
noteIds: s(z.array(z.string().uuid()).min(1).max(20).describe("Array of note IDs to read (max 20)")),
|
|
567
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
551
568
|
},
|
|
552
569
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
553
|
-
const { noteIds } = args;
|
|
554
|
-
return gw ? await gw.readNotes(noteIds) : await readNotes(ctx, noteIds);
|
|
570
|
+
const { noteIds, includeMediaContent } = args;
|
|
571
|
+
return gw ? await gw.readNotes(noteIds, includeMediaContent) : await readNotes(ctx, noteIds, includeMediaContent);
|
|
555
572
|
})));
|
|
556
573
|
// ─── write_memory ───────────────────────────────────────────────────────────
|
|
557
574
|
server.registerTool("write_memory", {
|
|
558
|
-
description: "Create or upsert a durable memory entry.\n\nMemory types: fact (durable knowledge, importance 3-5), skill (procedures/how-tos, 3-5), preference (user style/choices, 2-4), constraint (hard rules/limits, 4-5), task (active work items, 2-4), episodic (session summaries, 1-3), correction (superseded knowledge — always set supersededById, 3-5).\n\nImportance scale: 5=critical, 4=important, 3=standard (default), 2=supplementary, 1=low-value. Confidence scale: 5=verified, 4=observed multiple times, 3=reasonable inference (default), 2=uncertain, 1=speculative.\n\nRelationship fields: relatedMemoryIds links to related memories (derived_from, contradicts, refines, part_of, supersedes). sourceNoteIds links to source notes. supersededById points to the memory this one replaces. entities array enables cross-linking.\n\nServer dedup (dedup: true): >92% similarity = skip, >80% = supersede old, <80% = create new.",
|
|
575
|
+
description: "Create or upsert a durable memory entry.\n\nMemory types: fact (durable knowledge, importance 3-5), skill (procedures/how-tos, 3-5), preference (user style/choices, 2-4), constraint (hard rules/limits, 4-5), task (active work items, 2-4), episodic (session summaries, 1-3), correction (superseded knowledge — always set supersededById, 3-5), decision (significant choices made — include inputs, reasoning, alternatives considered, outcome; importance 4-5).\n\nImportance scale: 5=critical, 4=important, 3=standard (default), 2=supplementary, 1=low-value. Confidence scale: 5=verified, 4=observed multiple times, 3=reasonable inference (default), 2=uncertain, 1=speculative.\n\nRelationship fields: relatedMemoryIds links to related memories (derived_from, contradicts, refines, part_of, supersedes). sourceNoteIds links to source notes. supersededById points to the memory this one replaces. entities array enables cross-linking.\n\nServer dedup (dedup: true): >92% similarity = skip, >80% = supersede old, <80% = create new.",
|
|
559
576
|
inputSchema: {
|
|
560
577
|
content: s(z.string().min(1).max(1_000_000).describe("Memory content in plain text")),
|
|
561
|
-
memoryType: s(memoryTypeEnum.optional().describe("Type: fact, skill, preference, constraint, task, episodic, correction")),
|
|
578
|
+
memoryType: s(memoryTypeEnum.optional().describe("Type: fact, skill, preference, constraint, task, episodic, correction, decision")),
|
|
562
579
|
summary: s(z.string().max(500).optional().describe("Optional short summary")),
|
|
563
580
|
vaultId: s(z.string().uuid().optional().describe("Vault/project scope. Required unless defaultVaultId is configured for this MCP session.")),
|
|
564
581
|
importance: s(z.number().int().min(1).max(5).optional().describe("Importance from 1 to 5")),
|
|
@@ -646,10 +663,11 @@ async function main() {
|
|
|
646
663
|
description: "Read and decrypt full memory entries by IDs.",
|
|
647
664
|
inputSchema: {
|
|
648
665
|
memoryIds: s(z.array(z.string().uuid()).min(1).max(50).describe("Array of memory IDs to read")),
|
|
666
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
649
667
|
},
|
|
650
668
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
651
|
-
const { memoryIds } = args;
|
|
652
|
-
return gw ? await gw.readMemories(memoryIds) : await readMemories(ctx, memoryIds);
|
|
669
|
+
const { memoryIds, includeMediaContent } = args;
|
|
670
|
+
return gw ? await gw.readMemories(memoryIds, includeMediaContent) : await readMemories(ctx, memoryIds, includeMediaContent);
|
|
653
671
|
})));
|
|
654
672
|
// ─── archive_memory ─────────────────────────────────────────────────────────
|
|
655
673
|
server.registerTool("archive_memory", {
|
|
@@ -1260,6 +1278,23 @@ async function main() {
|
|
|
1260
1278
|
const result = await gw.downloadMedia({ attachmentId });
|
|
1261
1279
|
return JSON.stringify(result);
|
|
1262
1280
|
})));
|
|
1281
|
+
// ─── view_media ─────────────────────────────────────────────────────────
|
|
1282
|
+
server.registerTool("view_media", {
|
|
1283
|
+
description: "View a media attachment intelligently. Returns extracted text for all modalities, " +
|
|
1284
|
+
"plus base64 image content for images under 500KB. For non-image files (PDFs, audio, " +
|
|
1285
|
+
"video), returns the extracted text/transcription instead of raw bytes. " +
|
|
1286
|
+
"Use download_media if you need the actual file bytes regardless of size.",
|
|
1287
|
+
inputSchema: {
|
|
1288
|
+
attachmentId: s(z.string().uuid().describe("Media attachment ID to view")),
|
|
1289
|
+
},
|
|
1290
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1291
|
+
const { attachmentId } = args;
|
|
1292
|
+
if (gw) {
|
|
1293
|
+
const result = await gw.viewMedia({ attachmentId });
|
|
1294
|
+
return JSON.stringify(result);
|
|
1295
|
+
}
|
|
1296
|
+
return viewMedia(ctx, attachmentId);
|
|
1297
|
+
})));
|
|
1263
1298
|
// ─── delete_media ───────────────────────────────────────────────────────
|
|
1264
1299
|
server.registerTool("delete_media", {
|
|
1265
1300
|
description: "Delete a media attachment and its embedding. Permanently removes the file from storage.",
|
|
@@ -67,7 +67,7 @@ async function main() {
|
|
|
67
67
|
}
|
|
68
68
|
const embeddingConfig = {
|
|
69
69
|
apiKey,
|
|
70
|
-
model:
|
|
70
|
+
model: DEFAULT_EMBEDDING_MODEL, // Hardcoded — EMBEDDING_MODEL is for OpenAI text
|
|
71
71
|
};
|
|
72
72
|
const candidates = await loadBackfillCandidates(ctx, limit);
|
|
73
73
|
if (candidates.length === 0) {
|
package/dist/session-buffer.js
CHANGED
|
@@ -159,11 +159,6 @@ export function buildTurnContent(data) {
|
|
|
159
159
|
* using tool call metadata (inputs + outputs).
|
|
160
160
|
*/
|
|
161
161
|
export function buildRuleBasedEpisodic(data) {
|
|
162
|
-
const durationMs = Date.now() - new Date(data.startedAt).getTime();
|
|
163
|
-
const durationMin = Math.round(durationMs / 60_000);
|
|
164
|
-
const lines = [
|
|
165
|
-
`Session: ${durationMin}min, agent=${data.agentId}.`,
|
|
166
|
-
];
|
|
167
162
|
// Extract search topics
|
|
168
163
|
const searchTopics = [...new Set(data.activities
|
|
169
164
|
.filter((a) => a.category === "search" && a.inputSummary)
|
|
@@ -173,6 +168,9 @@ export function buildRuleBasedEpisodic(data) {
|
|
|
173
168
|
const readTopics = extractReadTopics(data.activities);
|
|
174
169
|
// Extract write actions with context
|
|
175
170
|
const writeActions = extractWriteActions(data.activities);
|
|
171
|
+
// Derive a session title from topics/actions
|
|
172
|
+
const title = deriveSessionTitle(searchTopics, readTopics, writeActions);
|
|
173
|
+
const lines = [title];
|
|
176
174
|
if (searchTopics.length > 0 || readTopics.length > 0) {
|
|
177
175
|
const explored = [];
|
|
178
176
|
if (searchTopics.length > 0)
|
|
@@ -186,6 +184,28 @@ export function buildRuleBasedEpisodic(data) {
|
|
|
186
184
|
}
|
|
187
185
|
return lines.join("\n");
|
|
188
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Derive a short session title from extracted topics and actions.
|
|
189
|
+
* Returns a concise phrase (no trailing period) summarizing the session focus.
|
|
190
|
+
*/
|
|
191
|
+
function deriveSessionTitle(searchTopics, readTopics, writeActions) {
|
|
192
|
+
const allTopics = [...searchTopics, ...readTopics];
|
|
193
|
+
if (allTopics.length > 0) {
|
|
194
|
+
const topicStr = allTopics.slice(0, 2).join(" and ");
|
|
195
|
+
return capitalizeAndClean(topicStr);
|
|
196
|
+
}
|
|
197
|
+
if (writeActions.length > 0) {
|
|
198
|
+
return capitalizeAndClean(writeActions[0]);
|
|
199
|
+
}
|
|
200
|
+
return "Session activity";
|
|
201
|
+
}
|
|
202
|
+
/** Capitalize first letter and strip trailing sentence punctuation. */
|
|
203
|
+
function capitalizeAndClean(phrase) {
|
|
204
|
+
const cleaned = phrase.trim().replace(/[.!?]+$/, "");
|
|
205
|
+
if (!cleaned)
|
|
206
|
+
return "Session activity";
|
|
207
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
208
|
+
}
|
|
189
209
|
/**
|
|
190
210
|
* Extract what was read/explored during the session.
|
|
191
211
|
* Provides context about what the agent was looking at — note titles,
|
|
@@ -453,12 +473,10 @@ export function buildTranscriptEpisodic(data) {
|
|
|
453
473
|
if (data.transcript.length < MIN_TRANSCRIPT_ENTRIES_FOR_EPISODIC) {
|
|
454
474
|
return null;
|
|
455
475
|
}
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
"",
|
|
461
|
-
];
|
|
476
|
+
// Derive title from transcript entries — use first entry as session focus
|
|
477
|
+
const firstContext = data.transcript[0]?.context ?? "Session activity";
|
|
478
|
+
const title = firstContext.charAt(0).toUpperCase() + firstContext.slice(1);
|
|
479
|
+
const lines = [title, ""];
|
|
462
480
|
for (const entry of data.transcript) {
|
|
463
481
|
lines.push(`- ${entry.context}`);
|
|
464
482
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { McpContext } from "../auth.js";
|
|
2
|
-
export declare function readMemories(ctx: McpContext, memoryIds: string[]): Promise<string>;
|
|
2
|
+
export declare function readMemories(ctx: McpContext, memoryIds: string[], _includeMediaContent?: boolean): Promise<string>;
|
|
@@ -2,7 +2,7 @@ import { getMemoriesByIds, touchMemories, getAttachmentsForMemories, formatAttac
|
|
|
2
2
|
import { decrypt } from "../crypto.js";
|
|
3
3
|
import { logMcpUsageEvent } from "../usage.js";
|
|
4
4
|
import { decryptMemoryFields } from "./decrypt-helpers.js";
|
|
5
|
-
export async function readMemories(ctx, memoryIds) {
|
|
5
|
+
export async function readMemories(ctx, memoryIds, _includeMediaContent) {
|
|
6
6
|
const memories = await getMemoriesByIds(ctx.supabase, ctx.userId, memoryIds);
|
|
7
7
|
const foundIds = new Set(memories.map((m) => m.id));
|
|
8
8
|
const notFound = memoryIds.filter((id) => !foundIds.has(id));
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { McpContext } from "../auth.js";
|
|
2
|
-
export declare function readNote(ctx: McpContext, noteId: string): Promise<string>;
|
|
2
|
+
export declare function readNote(ctx: McpContext, noteId: string, _includeMediaContent?: boolean): Promise<string>;
|
package/dist/tools/read-note.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getNote, getVault, getAttachmentsForNotes, formatAttachmentWithExtracti
|
|
|
2
2
|
import { decrypt } from "../crypto.js";
|
|
3
3
|
import { logMcpUsageEvent } from "../usage.js";
|
|
4
4
|
import { decryptNoteFields } from "./decrypt-helpers.js";
|
|
5
|
-
export async function readNote(ctx, noteId) {
|
|
5
|
+
export async function readNote(ctx, noteId, _includeMediaContent) {
|
|
6
6
|
const note = await getNote(ctx.supabase, ctx.userId, noteId);
|
|
7
7
|
if (!note) {
|
|
8
8
|
return JSON.stringify({ error: "Note not found" });
|
|
@@ -3,4 +3,4 @@ import type { McpContext } from "../auth.js";
|
|
|
3
3
|
* Batch-reads multiple notes by ID. Returns full decrypted content for each,
|
|
4
4
|
* plus a list of any IDs that were not found.
|
|
5
5
|
*/
|
|
6
|
-
export declare function readNotes(ctx: McpContext, noteIds: string[]): Promise<string>;
|
|
6
|
+
export declare function readNotes(ctx: McpContext, noteIds: string[], _includeMediaContent?: boolean): Promise<string>;
|
package/dist/tools/read-notes.js
CHANGED
|
@@ -5,7 +5,7 @@ import { decryptNoteFields } from "./decrypt-helpers.js";
|
|
|
5
5
|
* Batch-reads multiple notes by ID. Returns full decrypted content for each,
|
|
6
6
|
* plus a list of any IDs that were not found.
|
|
7
7
|
*/
|
|
8
|
-
export async function readNotes(ctx, noteIds) {
|
|
8
|
+
export async function readNotes(ctx, noteIds, _includeMediaContent) {
|
|
9
9
|
const notes = await getNotesByIds(ctx.supabase, ctx.userId, noteIds);
|
|
10
10
|
// Build a set of found IDs to report missing ones
|
|
11
11
|
const foundIds = new Set(notes.map((n) => n.id));
|
|
@@ -14,6 +14,7 @@ export declare function updateMemoryTool(ctx: McpContext, input: {
|
|
|
14
14
|
relatedMemoryIds?: MemoryRelation[];
|
|
15
15
|
sourceNoteIds?: string[];
|
|
16
16
|
isArchived?: boolean;
|
|
17
|
+
supersededById?: string;
|
|
17
18
|
metadata?: Record<string, unknown>;
|
|
18
19
|
}): Promise<string>;
|
|
19
20
|
export {};
|
|
@@ -72,6 +72,8 @@ export async function updateMemoryTool(ctx, input) {
|
|
|
72
72
|
updates.source_note_ids = input.sourceNoteIds;
|
|
73
73
|
if (input.isArchived !== undefined)
|
|
74
74
|
updates.is_archived = input.isArchived;
|
|
75
|
+
if (input.supersededById !== undefined)
|
|
76
|
+
updates.superseded_by_id = input.supersededById;
|
|
75
77
|
// Merge metadata (task-specific fields like taskStatus, assignedAgentId)
|
|
76
78
|
if (input.metadata !== undefined) {
|
|
77
79
|
const existingMeta = (existing.metadata ?? {});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View a media attachment intelligently.
|
|
3
|
+
* Direct mode: returns extracted text only (no storage access).
|
|
4
|
+
*/
|
|
5
|
+
export async function viewMedia(ctx, attachmentId) {
|
|
6
|
+
const { data, error } = await ctx.supabase
|
|
7
|
+
.from("media_attachments")
|
|
8
|
+
.select("id, memory_id, note_id, vault_id, modality, mime_type, file_name, file_size_bytes, embedding_status, extraction_status, extracted_text, extracted_text_iv")
|
|
9
|
+
.eq("id", attachmentId)
|
|
10
|
+
.eq("user_id", ctx.userId)
|
|
11
|
+
.maybeSingle();
|
|
12
|
+
if (error)
|
|
13
|
+
return JSON.stringify({ error: "Failed to fetch attachment" });
|
|
14
|
+
if (!data)
|
|
15
|
+
return JSON.stringify({ error: "Attachment not found" });
|
|
16
|
+
let extractedText = null;
|
|
17
|
+
if (data.extracted_text && data.extracted_text_iv && ctx.masterKey) {
|
|
18
|
+
const { decrypt } = await import("../crypto.js");
|
|
19
|
+
try {
|
|
20
|
+
extractedText = await decrypt(data.extracted_text, data.extracted_text_iv, ctx.masterKey);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
extractedText = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let contentNote = null;
|
|
27
|
+
if (data.modality === "image") {
|
|
28
|
+
contentNote = "base64 unavailable in direct mode — use gateway mode or download_media";
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const label = data.modality.charAt(0).toUpperCase() + data.modality.slice(1);
|
|
32
|
+
const verb = (data.modality === "audio" || data.modality === "video") ? "transcription" : "extracted text";
|
|
33
|
+
contentNote = `${label} — returning ${verb}. Use download_media for raw bytes.`;
|
|
34
|
+
}
|
|
35
|
+
if (!extractedText && data.extraction_status !== "completed") {
|
|
36
|
+
const note = `Extraction ${data.extraction_status} — no text content available yet.`;
|
|
37
|
+
contentNote = contentNote ? `${contentNote} ${note}` : note;
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
attachmentId: data.id,
|
|
41
|
+
memoryId: data.memory_id,
|
|
42
|
+
noteId: data.note_id,
|
|
43
|
+
modality: data.modality,
|
|
44
|
+
mimeType: data.mime_type,
|
|
45
|
+
fileName: data.file_name,
|
|
46
|
+
fileSizeBytes: data.file_size_bytes,
|
|
47
|
+
embeddingStatus: data.embedding_status,
|
|
48
|
+
extractionStatus: data.extraction_status,
|
|
49
|
+
extractedText,
|
|
50
|
+
base64Content: null,
|
|
51
|
+
contentNote,
|
|
52
|
+
});
|
|
53
|
+
}
|