exovault-mcp-server 1.2.0 → 1.4.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/gateway-client.d.ts +25 -3
- package/dist/gateway-client.js +9 -6
- package/dist/index.js +30 -6
- 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/search-memories.d.ts +3 -0
- package/dist/tools/search-memories.js +57 -24
- package/dist/tools/search-notes.d.ts +9 -0
- package/dist/tools/search-notes.js +9 -0
- package/dist/tools/search.d.ts +8 -7
- package/dist/tools/search.js +11 -8
- package/dist/tools/view-media.d.ts +6 -0
- package/dist/tools/view-media.js +53 -0
- package/package.json +1 -1
package/dist/gateway-client.d.ts
CHANGED
|
@@ -62,8 +62,14 @@ export declare class GatewayClient {
|
|
|
62
62
|
includeArchived?: boolean;
|
|
63
63
|
entity?: string;
|
|
64
64
|
compact?: boolean;
|
|
65
|
+
decayHalfLife?: number;
|
|
66
|
+
diversity?: number;
|
|
67
|
+
searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
|
|
68
|
+
graphWeight?: number;
|
|
69
|
+
graphSeeds?: number;
|
|
70
|
+
graphMaxHops?: number;
|
|
65
71
|
}): Promise<string>;
|
|
66
|
-
readMemories(memoryIds: string[]): Promise<string>;
|
|
72
|
+
readMemories(memoryIds: string[], includeMediaContent?: boolean): Promise<string>;
|
|
67
73
|
updateMemory(params: {
|
|
68
74
|
memoryId: string;
|
|
69
75
|
content?: string;
|
|
@@ -130,8 +136,8 @@ export declare class GatewayClient {
|
|
|
130
136
|
limit?: number;
|
|
131
137
|
folderId?: string;
|
|
132
138
|
}): Promise<string>;
|
|
133
|
-
readNote(noteId: string): Promise<string>;
|
|
134
|
-
readNotes(noteIds: string[]): Promise<string>;
|
|
139
|
+
readNote(noteId: string, includeMediaContent?: boolean): Promise<string>;
|
|
140
|
+
readNotes(noteIds: string[], includeMediaContent?: boolean): Promise<string>;
|
|
135
141
|
searchNotes(params: {
|
|
136
142
|
query: string;
|
|
137
143
|
topK?: number;
|
|
@@ -338,5 +344,21 @@ export declare class GatewayClient {
|
|
|
338
344
|
embeddingStatus: string;
|
|
339
345
|
base64Content: string;
|
|
340
346
|
}>;
|
|
347
|
+
viewMedia(params: {
|
|
348
|
+
attachmentId: string;
|
|
349
|
+
}): Promise<{
|
|
350
|
+
attachmentId: string;
|
|
351
|
+
memoryId: string | null;
|
|
352
|
+
noteId: string | null;
|
|
353
|
+
modality: string;
|
|
354
|
+
mimeType: string;
|
|
355
|
+
fileName: string | null;
|
|
356
|
+
fileSizeBytes: number;
|
|
357
|
+
embeddingStatus: string;
|
|
358
|
+
extractionStatus: string;
|
|
359
|
+
extractedText: string | null;
|
|
360
|
+
base64Content: string | null;
|
|
361
|
+
contentNote: string | null;
|
|
362
|
+
}>;
|
|
341
363
|
deleteMedia(attachmentId: string, vaultId?: string): Promise<string>;
|
|
342
364
|
}
|
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) {
|
|
@@ -323,6 +323,9 @@ export class GatewayClient {
|
|
|
323
323
|
async downloadMedia(params) {
|
|
324
324
|
return this.request("POST", "/api/agent/download-media", params);
|
|
325
325
|
}
|
|
326
|
+
async viewMedia(params) {
|
|
327
|
+
return this.request("POST", "/api/agent/view-media", params);
|
|
328
|
+
}
|
|
326
329
|
async deleteMedia(attachmentId, vaultId) {
|
|
327
330
|
const body = { attachmentId };
|
|
328
331
|
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";
|
|
@@ -430,10 +431,11 @@ async function main() {
|
|
|
430
431
|
description: "Read the full decrypted content of a note, including title, tags, and vault name",
|
|
431
432
|
inputSchema: {
|
|
432
433
|
noteId: s(z.string().uuid().describe("The note ID to read")),
|
|
434
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
433
435
|
},
|
|
434
436
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
435
|
-
const { noteId } = args;
|
|
436
|
-
return gw ? await gw.readNote(noteId) : await readNote(ctx, noteId);
|
|
437
|
+
const { noteId, includeMediaContent } = args;
|
|
438
|
+
return gw ? await gw.readNote(noteId, includeMediaContent) : await readNote(ctx, noteId, includeMediaContent);
|
|
437
439
|
})));
|
|
438
440
|
// ─── search_notes ─────────────────────────────────────────────────────────
|
|
439
441
|
server.registerTool("search_notes", {
|
|
@@ -548,10 +550,11 @@ async function main() {
|
|
|
548
550
|
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
551
|
inputSchema: {
|
|
550
552
|
noteIds: s(z.array(z.string().uuid()).min(1).max(20).describe("Array of note IDs to read (max 20)")),
|
|
553
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
551
554
|
},
|
|
552
555
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
553
|
-
const { noteIds } = args;
|
|
554
|
-
return gw ? await gw.readNotes(noteIds) : await readNotes(ctx, noteIds);
|
|
556
|
+
const { noteIds, includeMediaContent } = args;
|
|
557
|
+
return gw ? await gw.readNotes(noteIds, includeMediaContent) : await readNotes(ctx, noteIds, includeMediaContent);
|
|
555
558
|
})));
|
|
556
559
|
// ─── write_memory ───────────────────────────────────────────────────────────
|
|
557
560
|
server.registerTool("write_memory", {
|
|
@@ -605,6 +608,9 @@ async function main() {
|
|
|
605
608
|
compact: s(z.boolean().optional().describe("Return truncated content previews (200 chars) instead of full content. Use read_memories for full content on specific IDs.")),
|
|
606
609
|
decayHalfLife: s(z.number().int().min(1).max(365).optional().describe("Temporal decay half-life in days (default 30). Older memories score lower unless importance >= 4.")),
|
|
607
610
|
diversity: s(z.number().min(0).max(1).optional().describe("MMR diversity balance 0-1 (default 0.7). Higher = more relevance, lower = more diversity.")),
|
|
611
|
+
graphWeight: s(z.number().min(0).max(1.5).optional().describe("RRF weight for graph signal (default 0.6). Set 0 to disable graph expansion.")),
|
|
612
|
+
graphSeeds: s(z.number().int().min(0).max(15).optional().describe("How many top results to use as graph expansion seeds (default 5)")),
|
|
613
|
+
graphMaxHops: s(z.number().int().min(1).max(2).optional().describe("Max hops for graph traversal (default 1)")),
|
|
608
614
|
},
|
|
609
615
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
610
616
|
const input = args;
|
|
@@ -643,10 +649,11 @@ async function main() {
|
|
|
643
649
|
description: "Read and decrypt full memory entries by IDs.",
|
|
644
650
|
inputSchema: {
|
|
645
651
|
memoryIds: s(z.array(z.string().uuid()).min(1).max(50).describe("Array of memory IDs to read")),
|
|
652
|
+
includeMediaContent: s(z.boolean().optional().describe("When true, includes base64 image content for images under 500KB. Gateway mode only. Default: false.")),
|
|
646
653
|
},
|
|
647
654
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
648
|
-
const { memoryIds } = args;
|
|
649
|
-
return gw ? await gw.readMemories(memoryIds) : await readMemories(ctx, memoryIds);
|
|
655
|
+
const { memoryIds, includeMediaContent } = args;
|
|
656
|
+
return gw ? await gw.readMemories(memoryIds, includeMediaContent) : await readMemories(ctx, memoryIds, includeMediaContent);
|
|
650
657
|
})));
|
|
651
658
|
// ─── archive_memory ─────────────────────────────────────────────────────────
|
|
652
659
|
server.registerTool("archive_memory", {
|
|
@@ -1257,6 +1264,23 @@ async function main() {
|
|
|
1257
1264
|
const result = await gw.downloadMedia({ attachmentId });
|
|
1258
1265
|
return JSON.stringify(result);
|
|
1259
1266
|
})));
|
|
1267
|
+
// ─── view_media ─────────────────────────────────────────────────────────
|
|
1268
|
+
server.registerTool("view_media", {
|
|
1269
|
+
description: "View a media attachment intelligently. Returns extracted text for all modalities, " +
|
|
1270
|
+
"plus base64 image content for images under 500KB. For non-image files (PDFs, audio, " +
|
|
1271
|
+
"video), returns the extracted text/transcription instead of raw bytes. " +
|
|
1272
|
+
"Use download_media if you need the actual file bytes regardless of size.",
|
|
1273
|
+
inputSchema: {
|
|
1274
|
+
attachmentId: s(z.string().uuid().describe("Media attachment ID to view")),
|
|
1275
|
+
},
|
|
1276
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1277
|
+
const { attachmentId } = args;
|
|
1278
|
+
if (gw) {
|
|
1279
|
+
const result = await gw.viewMedia({ attachmentId });
|
|
1280
|
+
return JSON.stringify(result);
|
|
1281
|
+
}
|
|
1282
|
+
return viewMedia(ctx, attachmentId);
|
|
1283
|
+
})));
|
|
1260
1284
|
// ─── delete_media ───────────────────────────────────────────────────────
|
|
1261
1285
|
server.registerTool("delete_media", {
|
|
1262
1286
|
description: "Delete a media attachment and its embedding. Permanently removes the file from storage.",
|
|
@@ -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));
|
|
@@ -11,4 +11,7 @@ export declare function searchMemories(ctx: McpContext, input: {
|
|
|
11
11
|
decayHalfLife?: number;
|
|
12
12
|
diversity?: number;
|
|
13
13
|
searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
|
|
14
|
+
graphWeight?: number;
|
|
15
|
+
graphSeeds?: number;
|
|
16
|
+
graphMaxHops?: number;
|
|
14
17
|
}): Promise<string>;
|
|
@@ -12,13 +12,27 @@ import { applyImportanceBoostBatch } from "./importance-boost.js";
|
|
|
12
12
|
import { applyConfidenceDampenBatch } from "./confidence-dampen.js";
|
|
13
13
|
const FALLBACK_CONTENT_PREVIEW_CHARS = 500;
|
|
14
14
|
const COMPACT_CONTENT_CHARS = 200;
|
|
15
|
+
function buildSignalsMap(lists) {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
for (const [signal, ids] of Object.entries(lists)) {
|
|
18
|
+
if (!ids)
|
|
19
|
+
continue;
|
|
20
|
+
for (const id of ids) {
|
|
21
|
+
const existing = map.get(id);
|
|
22
|
+
if (existing)
|
|
23
|
+
existing.push(signal);
|
|
24
|
+
else
|
|
25
|
+
map.set(id, [signal]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return map;
|
|
29
|
+
}
|
|
15
30
|
// RRF signal weights
|
|
16
31
|
const WEIGHT_SEMANTIC = 1.0;
|
|
17
32
|
const WEIGHT_BM25 = 0.9;
|
|
18
33
|
const WEIGHT_BLIND_INDEX = 0.4;
|
|
19
|
-
|
|
20
|
-
// Graph
|
|
21
|
-
const GRAPH_EXPAND_TOP_N = 5;
|
|
34
|
+
// WEIGHT_GRAPH and GRAPH_EXPAND_TOP_N are now configurable via input params (defaults applied inline)
|
|
35
|
+
// Graph relation weights for scoring neighbor relevance
|
|
22
36
|
const GRAPH_RELATION_WEIGHTS = {
|
|
23
37
|
refines: 1.0,
|
|
24
38
|
derived_from: 0.9,
|
|
@@ -90,6 +104,7 @@ export async function searchMemories(ctx, input) {
|
|
|
90
104
|
supersededById: m.superseded_by_id,
|
|
91
105
|
entities: m.entities,
|
|
92
106
|
updatedAt: m.updated_at,
|
|
107
|
+
signals: ["entity"],
|
|
93
108
|
...(attachmentLines.length > 0 ? { attachments: attachmentLines } : {}),
|
|
94
109
|
};
|
|
95
110
|
}));
|
|
@@ -187,30 +202,47 @@ export async function searchMemories(ctx, input) {
|
|
|
187
202
|
await Promise.all([semanticPromise, blindPromise, bm25Promise]);
|
|
188
203
|
// ── Signal 3: Graph expansion on top-N unique results from all signals ─
|
|
189
204
|
const graphIds = [];
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
const graphWeight = input.graphWeight ?? 0.6;
|
|
206
|
+
// Graph expansion — skip entirely when graphWeight=0
|
|
207
|
+
if (graphWeight > 0) {
|
|
208
|
+
const initialHits = new Set([...semanticIds, ...blindIds, ...bm25Ids]);
|
|
209
|
+
const graphSeedCount = input.graphSeeds ?? 5;
|
|
210
|
+
const seedIds = [...initialHits].slice(0, graphSeedCount);
|
|
211
|
+
if (seedIds.length > 0) {
|
|
212
|
+
try {
|
|
213
|
+
const graphMaxHops = input.graphMaxHops ?? 1;
|
|
214
|
+
const maxNodesPerSeed = graphMaxHops >= 2 ? 25 : 10;
|
|
215
|
+
const graphResults = await Promise.all(seedIds.map((id) => getKnowledgeGraph(ctx.supabase, ctx.userId, "memory", id, graphMaxHops, maxNodesPerSeed)));
|
|
216
|
+
// Collect neighbor memory IDs ranked by relation weight
|
|
217
|
+
const neighborScores = new Map();
|
|
218
|
+
for (const neighbors of graphResults) {
|
|
219
|
+
for (const n of neighbors) {
|
|
220
|
+
if (n.node_type !== "memory")
|
|
221
|
+
continue;
|
|
222
|
+
// Only exclude seed IDs — allow boosting of already-found results (aligned with gateway)
|
|
223
|
+
if (seedIds.includes(n.node_id))
|
|
224
|
+
continue;
|
|
225
|
+
const weight = GRAPH_RELATION_WEIGHTS[n.relation] ?? 0.5;
|
|
226
|
+
const current = neighborScores.get(n.node_id) ?? 0;
|
|
227
|
+
neighborScores.set(n.node_id, Math.max(current, weight));
|
|
228
|
+
}
|
|
204
229
|
}
|
|
230
|
+
graphIds.push(...Array.from(neighborScores.entries())
|
|
231
|
+
.sort((a, b) => b[1] - a[1])
|
|
232
|
+
.map(([id]) => id));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
process.stderr.write(`[search-memories] Graph expansion error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
205
236
|
}
|
|
206
|
-
graphIds.push(...Array.from(neighborScores.entries())
|
|
207
|
-
.sort((a, b) => b[1] - a[1])
|
|
208
|
-
.map(([id]) => id));
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
process.stderr.write(`[search-memories] Graph expansion error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
212
237
|
}
|
|
213
238
|
}
|
|
239
|
+
// ── Signal attribution ──────────────────────────────────────────────
|
|
240
|
+
const signalsMap = buildSignalsMap({
|
|
241
|
+
semantic: semanticIds,
|
|
242
|
+
bm25: bm25Ids,
|
|
243
|
+
blind: blindIds,
|
|
244
|
+
graph: graphIds,
|
|
245
|
+
});
|
|
214
246
|
// ── RRF Fusion + Temporal Decay ──────────────────────────────────────
|
|
215
247
|
let rankedIds = [];
|
|
216
248
|
let searchMode = "hybrid";
|
|
@@ -225,7 +257,7 @@ export async function searchMemories(ctx, input) {
|
|
|
225
257
|
if (blindIds.length > 0)
|
|
226
258
|
lists.push({ ids: blindIds, weight: WEIGHT_BLIND_INDEX });
|
|
227
259
|
if (graphIds.length > 0)
|
|
228
|
-
lists.push({ ids: graphIds, weight:
|
|
260
|
+
lists.push({ ids: graphIds, weight: graphWeight });
|
|
229
261
|
// Get scored candidates — fetch topK*2 for decay re-ranking headroom, with top-rank bonus
|
|
230
262
|
const rrfScored = fuseWithRRFScored(lists, 60, true).slice(0, topK * 2);
|
|
231
263
|
searchMode = effectiveSearchMode === "bm25" ? "bm25"
|
|
@@ -336,6 +368,7 @@ export async function searchMemories(ctx, input) {
|
|
|
336
368
|
supersededById: m.superseded_by_id,
|
|
337
369
|
entities: m.entities,
|
|
338
370
|
updatedAt: m.updated_at,
|
|
371
|
+
signals: signalsMap.get(m.id) ?? [],
|
|
339
372
|
...(attachmentLines.length > 0 ? { attachments: attachmentLines } : {}),
|
|
340
373
|
};
|
|
341
374
|
}));
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { McpContext } from "../auth.js";
|
|
2
|
+
/**
|
|
3
|
+
* Direct-mode note search: keyword matching via blind-index pre-filter (when mekHex
|
|
4
|
+
* is available) or full client-side scan. Scores by weighted term frequency
|
|
5
|
+
* (title 3x, tags 2x, content 1x).
|
|
6
|
+
*
|
|
7
|
+
* NOTE: `searchMode`, `threshold`, and `diversity` are accepted for API compatibility
|
|
8
|
+
* but are IGNORED in direct mode. Hybrid/semantic/graph search modes and MMR
|
|
9
|
+
* diversity re-ranking require gateway mode (POST /api/agent/search-notes).
|
|
10
|
+
*/
|
|
2
11
|
export declare function searchNotes(ctx: McpContext, input: {
|
|
3
12
|
query: string;
|
|
4
13
|
topK?: number;
|
|
@@ -42,6 +42,15 @@ function scoreNote(terms, title, tags, content) {
|
|
|
42
42
|
}
|
|
43
43
|
const COMPACT_PREVIEW_CHARS = 200;
|
|
44
44
|
const FULL_PREVIEW_CHARS = 500;
|
|
45
|
+
/**
|
|
46
|
+
* Direct-mode note search: keyword matching via blind-index pre-filter (when mekHex
|
|
47
|
+
* is available) or full client-side scan. Scores by weighted term frequency
|
|
48
|
+
* (title 3x, tags 2x, content 1x).
|
|
49
|
+
*
|
|
50
|
+
* NOTE: `searchMode`, `threshold`, and `diversity` are accepted for API compatibility
|
|
51
|
+
* but are IGNORED in direct mode. Hybrid/semantic/graph search modes and MMR
|
|
52
|
+
* diversity re-ranking require gateway mode (POST /api/agent/search-notes).
|
|
53
|
+
*/
|
|
45
54
|
export async function searchNotes(ctx, input) {
|
|
46
55
|
const topK = input.topK ?? 10;
|
|
47
56
|
const vaultId = input.vaultId;
|
package/dist/tools/search.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpContext } from "../auth.js";
|
|
2
|
+
import { GatewayClient } from "../gateway-client.js";
|
|
2
3
|
interface SearchParams {
|
|
3
4
|
query: string;
|
|
4
5
|
topK?: number;
|
|
@@ -7,7 +8,6 @@ interface SearchParams {
|
|
|
7
8
|
diversity?: number;
|
|
8
9
|
vaultId?: string;
|
|
9
10
|
includeContent?: boolean;
|
|
10
|
-
compact?: boolean;
|
|
11
11
|
scope?: "all" | "memories" | "notes";
|
|
12
12
|
memoryType?: string;
|
|
13
13
|
includeArchived?: boolean;
|
|
@@ -16,12 +16,13 @@ interface SearchParams {
|
|
|
16
16
|
/**
|
|
17
17
|
* Universal search tool — searches memories, notes, or both.
|
|
18
18
|
*
|
|
19
|
-
* In
|
|
20
|
-
*
|
|
19
|
+
* In gateway mode: delegates to POST /api/agent/search which performs full
|
|
20
|
+
* cross-type MMR re-ranking server-side (hybrid semantic + keyword, all modes).
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* In direct mode: best-effort — calls searchMemories and/or searchNotes
|
|
23
|
+
* independently and interleaves results (no cross-type MMR). searchMode,
|
|
24
|
+
* threshold, and diversity are passed through but only partially honoured
|
|
25
|
+
* (notes fall back to keyword+blind-index only; see searchNotes).
|
|
25
26
|
*/
|
|
26
|
-
export declare function search(ctx: McpContext, params: SearchParams): Promise<string>;
|
|
27
|
+
export declare function search(ctx: McpContext, params: SearchParams, gw?: GatewayClient): Promise<string>;
|
|
27
28
|
export {};
|
package/dist/tools/search.js
CHANGED
|
@@ -3,14 +3,19 @@ import { searchNotes } from "./search-notes.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Universal search tool — searches memories, notes, or both.
|
|
5
5
|
*
|
|
6
|
-
* In
|
|
7
|
-
*
|
|
6
|
+
* In gateway mode: delegates to POST /api/agent/search which performs full
|
|
7
|
+
* cross-type MMR re-ranking server-side (hybrid semantic + keyword, all modes).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* In direct mode: best-effort — calls searchMemories and/or searchNotes
|
|
10
|
+
* independently and interleaves results (no cross-type MMR). searchMode,
|
|
11
|
+
* threshold, and diversity are passed through but only partially honoured
|
|
12
|
+
* (notes fall back to keyword+blind-index only; see searchNotes).
|
|
12
13
|
*/
|
|
13
|
-
export async function search(ctx, params) {
|
|
14
|
+
export async function search(ctx, params, gw) {
|
|
15
|
+
// Gateway mode: full cross-type MMR re-ranking server-side
|
|
16
|
+
if (gw) {
|
|
17
|
+
return gw.search(params);
|
|
18
|
+
}
|
|
14
19
|
const scope = params.scope ?? "all";
|
|
15
20
|
const topK = params.topK ?? 10;
|
|
16
21
|
const searchMemoriesParams = {
|
|
@@ -20,7 +25,6 @@ export async function search(ctx, params) {
|
|
|
20
25
|
vaultId: params.vaultId,
|
|
21
26
|
memoryType: params.memoryType,
|
|
22
27
|
includeArchived: params.includeArchived,
|
|
23
|
-
compact: params.compact,
|
|
24
28
|
decayHalfLife: params.decayHalfLife,
|
|
25
29
|
diversity: params.diversity,
|
|
26
30
|
searchMode: params.searchMode,
|
|
@@ -33,7 +37,6 @@ export async function search(ctx, params) {
|
|
|
33
37
|
diversity: params.diversity,
|
|
34
38
|
vaultId: params.vaultId,
|
|
35
39
|
includeContent: params.includeContent,
|
|
36
|
-
compact: params.compact,
|
|
37
40
|
};
|
|
38
41
|
try {
|
|
39
42
|
if (scope === "memories") {
|
|
@@ -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
|
+
}
|