exovault-mcp-server 1.1.0 → 1.2.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/db.d.ts +16 -0
- package/dist/db.js +34 -0
- package/dist/embedding-config.d.ts +1 -2
- package/dist/embedding-config.js +11 -10
- package/dist/extraction-prompt.js +4 -4
- package/dist/gateway-client.d.ts +51 -12
- package/dist/gateway-client.js +58 -10
- package/dist/index.js +145 -43
- package/dist/openai.d.ts +4 -2
- package/dist/openai.js +36 -32
- package/dist/rlm/actions.js +3 -2
- package/dist/rlm/verify.js +6 -6
- package/dist/rlm/writeback.js +10 -10
- package/dist/scripts/backfill-memory-embeddings.js +13 -30
- package/dist/tools/bm25.d.ts +6 -0
- package/dist/tools/bm25.js +10 -0
- package/dist/tools/confidence-dampen.d.ts +25 -0
- package/dist/tools/confidence-dampen.js +29 -0
- package/dist/tools/explore-graph.js +1 -1
- package/dist/tools/importance-boost.d.ts +25 -0
- package/dist/tools/importance-boost.js +29 -0
- package/dist/tools/read-memories.js +28 -1
- package/dist/tools/read-note.js +17 -1
- package/dist/tools/read-notes.js +28 -1
- package/dist/tools/recall.d.ts +24 -0
- package/dist/tools/recall.js +146 -0
- package/dist/tools/relevance.d.ts +13 -0
- package/dist/tools/relevance.js +17 -0
- package/dist/tools/rrf.d.ts +1 -1
- package/dist/tools/rrf.js +16 -1
- package/dist/tools/search-and-read.js +28 -1
- package/dist/tools/search-memories.d.ts +1 -0
- package/dist/tools/search-memories.js +117 -17
- package/dist/tools/search-notes.d.ts +10 -1
- package/dist/tools/search-notes.js +19 -9
- package/dist/tools/search.d.ts +27 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/semantic-search.js +29 -2
- package/dist/tools/update-memory.js +2 -2
- package/dist/tools/write-memory.js +2 -6
- package/package.json +2 -1
package/dist/db.d.ts
CHANGED
|
@@ -191,6 +191,22 @@ export declare function matchNotesByBlindTokens(supabase: SupabaseClient, tokenH
|
|
|
191
191
|
export declare function getNotesByIds(supabase: SupabaseClient, userId: string, noteIds: string[]): Promise<NoteRow[]>;
|
|
192
192
|
export declare function getNoteByImportSource(supabase: SupabaseClient, userId: string, vaultId: string, importSource: string, importSourceId: string): Promise<NoteRow | null>;
|
|
193
193
|
export declare function getMemoriesByIds(supabase: SupabaseClient, userId: string, memoryIds: string[]): Promise<MemoryRow[]>;
|
|
194
|
+
export interface MediaAttachmentRow {
|
|
195
|
+
id: string;
|
|
196
|
+
memory_id: string | null;
|
|
197
|
+
note_id: string | null;
|
|
198
|
+
modality: string;
|
|
199
|
+
mime_type: string;
|
|
200
|
+
file_name: string | null;
|
|
201
|
+
file_size_bytes: number;
|
|
202
|
+
embedding_status: string;
|
|
203
|
+
extracted_text: string | null;
|
|
204
|
+
extracted_text_iv: string | null;
|
|
205
|
+
extraction_status: string;
|
|
206
|
+
}
|
|
207
|
+
export declare function getAttachmentsForMemories(supabase: SupabaseClient, userId: string, memoryIds: string[]): Promise<MediaAttachmentRow[]>;
|
|
208
|
+
export declare function getAttachmentsForNotes(supabase: SupabaseClient, userId: string, noteIds: string[]): Promise<MediaAttachmentRow[]>;
|
|
209
|
+
export declare function formatAttachmentWithExtraction(att: MediaAttachmentRow, decryptedText: string | null, truncate?: number): string;
|
|
194
210
|
export interface ActiveAgentRow {
|
|
195
211
|
agentId: string;
|
|
196
212
|
modelId: string | null;
|
package/dist/db.js
CHANGED
|
@@ -372,6 +372,40 @@ export async function getMemoriesByIds(supabase, userId, memoryIds) {
|
|
|
372
372
|
throw new Error(sanitizeDbError("fetch memories by IDs", error.message));
|
|
373
373
|
return (data ?? []);
|
|
374
374
|
}
|
|
375
|
+
export async function getAttachmentsForMemories(supabase, userId, memoryIds) {
|
|
376
|
+
if (memoryIds.length === 0)
|
|
377
|
+
return [];
|
|
378
|
+
const { data, error } = await supabase
|
|
379
|
+
.from("media_attachments")
|
|
380
|
+
.select("id, memory_id, note_id, modality, mime_type, file_name, file_size_bytes, embedding_status, extracted_text, extracted_text_iv, extraction_status")
|
|
381
|
+
.eq("user_id", userId)
|
|
382
|
+
.in("memory_id", memoryIds);
|
|
383
|
+
if (error)
|
|
384
|
+
throw new Error(sanitizeDbError("fetch attachments", error.message));
|
|
385
|
+
return (data ?? []);
|
|
386
|
+
}
|
|
387
|
+
export async function getAttachmentsForNotes(supabase, userId, noteIds) {
|
|
388
|
+
if (noteIds.length === 0)
|
|
389
|
+
return [];
|
|
390
|
+
const { data, error } = await supabase
|
|
391
|
+
.from("media_attachments")
|
|
392
|
+
.select("id, memory_id, note_id, modality, mime_type, file_name, file_size_bytes, embedding_status, extracted_text, extracted_text_iv, extraction_status")
|
|
393
|
+
.eq("user_id", userId)
|
|
394
|
+
.in("note_id", noteIds);
|
|
395
|
+
if (error)
|
|
396
|
+
throw new Error(sanitizeDbError("fetch note attachments", error.message));
|
|
397
|
+
return (data ?? []);
|
|
398
|
+
}
|
|
399
|
+
export function formatAttachmentWithExtraction(att, decryptedText, truncate) {
|
|
400
|
+
let text = decryptedText;
|
|
401
|
+
if (text && truncate)
|
|
402
|
+
text = text.slice(0, truncate);
|
|
403
|
+
return [
|
|
404
|
+
`[${att.modality}] ${att.file_name ?? "unnamed"} (${att.mime_type}, ${att.file_size_bytes}B)`,
|
|
405
|
+
` embedding: ${att.embedding_status}, extraction: ${att.extraction_status}`,
|
|
406
|
+
text ? ` content: ${text}` : null,
|
|
407
|
+
].filter(Boolean).join("\n");
|
|
408
|
+
}
|
|
375
409
|
export async function getActiveAgents(supabase, userId, sinceDays = 30, limit = 20) {
|
|
376
410
|
const since = new Date();
|
|
377
411
|
since.setDate(since.getDate() - sinceDays);
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { McpContext } from "./auth.js";
|
|
2
2
|
export interface EmbeddingConfig {
|
|
3
3
|
apiKey: string;
|
|
4
|
-
baseUrl: string;
|
|
5
4
|
model: string;
|
|
6
5
|
}
|
|
7
6
|
/**
|
|
8
7
|
* Resolve embedding API configuration from env vars or MCP context.
|
|
9
|
-
* Priority: env vars > ctx.openaiApiKey > null.
|
|
8
|
+
* Priority: env vars > ctx.geminiApiKey > ctx.openaiApiKey > null.
|
|
10
9
|
*/
|
|
11
10
|
export declare function resolveEmbeddingConfig(ctx: McpContext): EmbeddingConfig | null;
|
package/dist/embedding-config.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
const
|
|
1
|
+
const DEFAULT_EMBEDDING_MODEL = "gemini-embedding-2-preview";
|
|
2
2
|
/**
|
|
3
3
|
* Resolve embedding API configuration from env vars or MCP context.
|
|
4
|
-
* Priority: env vars > ctx.openaiApiKey > null.
|
|
4
|
+
* Priority: env vars > ctx.geminiApiKey > ctx.openaiApiKey > null.
|
|
5
5
|
*/
|
|
6
6
|
export function resolveEmbeddingConfig(ctx) {
|
|
7
|
-
const envApiKey = process.env.
|
|
8
|
-
process.env.
|
|
7
|
+
const envApiKey = process.env.EXO_GEMINI_KEY ??
|
|
8
|
+
process.env.GEMINI_API_KEY ??
|
|
9
|
+
process.env.EMBEDDING_API_KEY;
|
|
9
10
|
if (envApiKey) {
|
|
10
11
|
return {
|
|
11
12
|
apiKey: envApiKey,
|
|
12
|
-
|
|
13
|
-
model: process.env.EMBEDDING_MODEL ?? OPENAI_EMBEDDING_MODEL,
|
|
13
|
+
model: process.env.EMBEDDING_MODEL ?? DEFAULT_EMBEDDING_MODEL,
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
// Fallback to MCP context keys
|
|
17
|
+
const ctxKey = ctx.geminiApiKey ?? ctx.openaiApiKey;
|
|
18
|
+
if (ctxKey && typeof ctxKey === "string") {
|
|
17
19
|
return {
|
|
18
|
-
apiKey:
|
|
19
|
-
|
|
20
|
-
model: OPENAI_EMBEDDING_MODEL,
|
|
20
|
+
apiKey: ctxKey,
|
|
21
|
+
model: DEFAULT_EMBEDDING_MODEL,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
return null;
|
|
@@ -74,10 +74,10 @@ function truncate(s, max) {
|
|
|
74
74
|
return undefined;
|
|
75
75
|
return s.length > max ? s.slice(0, max) : s;
|
|
76
76
|
}
|
|
77
|
-
const EXTRACTION_INSTRUCTIONS = `Extract durable knowledge from the activity log above. Return a JSON array, max 8 items.
|
|
78
|
-
Each item uses short keys: {c: content, t: type, i: importance(1-5), e: [entities], s: summary}
|
|
79
|
-
Types: fact, skill, preference, constraint, task
|
|
80
|
-
Only extract knowledge NOT already saved. Skip ephemeral info.
|
|
77
|
+
const EXTRACTION_INSTRUCTIONS = `Extract durable knowledge from the activity log above. Return a JSON array, max 8 items.
|
|
78
|
+
Each item uses short keys: {c: content, t: type, i: importance(1-5), e: [entities], s: summary}
|
|
79
|
+
Types: fact, skill, preference, constraint, task
|
|
80
|
+
Only extract knowledge NOT already saved. Skip ephemeral info.
|
|
81
81
|
Return [] if nothing worth extracting.`;
|
|
82
82
|
// ─── parseExtractionResult ────────────────────────────────────────────
|
|
83
83
|
/**
|
package/dist/gateway-client.d.ts
CHANGED
|
@@ -134,8 +134,28 @@ export declare class GatewayClient {
|
|
|
134
134
|
readNotes(noteIds: string[]): Promise<string>;
|
|
135
135
|
searchNotes(params: {
|
|
136
136
|
query: string;
|
|
137
|
+
topK?: number;
|
|
138
|
+
threshold?: number;
|
|
139
|
+
searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
|
|
140
|
+
diversity?: number;
|
|
137
141
|
vaultId?: string;
|
|
138
|
-
|
|
142
|
+
includeContent?: boolean;
|
|
143
|
+
compact?: boolean;
|
|
144
|
+
}): Promise<string>;
|
|
145
|
+
search(params: {
|
|
146
|
+
query: string;
|
|
147
|
+
topK?: number;
|
|
148
|
+
threshold?: number;
|
|
149
|
+
searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
|
|
150
|
+
diversity?: number;
|
|
151
|
+
vaultId?: string;
|
|
152
|
+
includeContent?: boolean;
|
|
153
|
+
compact?: boolean;
|
|
154
|
+
scope?: "all" | "memories" | "notes";
|
|
155
|
+
memoryType?: string;
|
|
156
|
+
entity?: string;
|
|
157
|
+
includeArchived?: boolean;
|
|
158
|
+
decayHalfLife?: number;
|
|
139
159
|
}): Promise<string>;
|
|
140
160
|
createNote(params: {
|
|
141
161
|
vaultId?: string;
|
|
@@ -151,17 +171,6 @@ export declare class GatewayClient {
|
|
|
151
171
|
tags?: string[];
|
|
152
172
|
}): Promise<string>;
|
|
153
173
|
deleteNote(noteId: string): Promise<string>;
|
|
154
|
-
semanticSearch(params: {
|
|
155
|
-
query: string;
|
|
156
|
-
topK?: number;
|
|
157
|
-
threshold?: number;
|
|
158
|
-
vaultId?: string;
|
|
159
|
-
}): Promise<string>;
|
|
160
|
-
searchAndRead(params: {
|
|
161
|
-
query: string;
|
|
162
|
-
maxNotes?: number;
|
|
163
|
-
vaultId?: string;
|
|
164
|
-
}): Promise<string>;
|
|
165
174
|
listFolders(params: {
|
|
166
175
|
vaultId?: string;
|
|
167
176
|
}): Promise<string>;
|
|
@@ -291,6 +300,16 @@ export declare class GatewayClient {
|
|
|
291
300
|
documentType: "soul" | "instructions" | "skills" | "checks";
|
|
292
301
|
vaultId?: string;
|
|
293
302
|
}): Promise<string>;
|
|
303
|
+
recall(params: {
|
|
304
|
+
mode: "temporal" | "topic" | "graph";
|
|
305
|
+
range?: string;
|
|
306
|
+
query?: string;
|
|
307
|
+
searchMode?: "auto" | "hybrid" | "bm25" | "semantic";
|
|
308
|
+
topK?: number;
|
|
309
|
+
nodeId?: string;
|
|
310
|
+
maxHops?: number;
|
|
311
|
+
vaultId?: string;
|
|
312
|
+
}): Promise<string>;
|
|
294
313
|
readDocs(params: {
|
|
295
314
|
slug?: string;
|
|
296
315
|
list?: boolean;
|
|
@@ -300,4 +319,24 @@ export declare class GatewayClient {
|
|
|
300
319
|
documentType: "instructions" | "skills" | "checks";
|
|
301
320
|
appendContent: string;
|
|
302
321
|
}): Promise<string>;
|
|
322
|
+
attachMedia(params: {
|
|
323
|
+
fileBytes: Uint8Array;
|
|
324
|
+
fileName: string;
|
|
325
|
+
mimeType: string;
|
|
326
|
+
memoryId?: string;
|
|
327
|
+
vaultId?: string;
|
|
328
|
+
}): Promise<string>;
|
|
329
|
+
downloadMedia(params: {
|
|
330
|
+
attachmentId: string;
|
|
331
|
+
}): Promise<{
|
|
332
|
+
attachmentId: string;
|
|
333
|
+
memoryId: string | null;
|
|
334
|
+
modality: string;
|
|
335
|
+
mimeType: string;
|
|
336
|
+
fileName: string | null;
|
|
337
|
+
fileSizeBytes: number;
|
|
338
|
+
embeddingStatus: string;
|
|
339
|
+
base64Content: string;
|
|
340
|
+
}>;
|
|
341
|
+
deleteMedia(attachmentId: string, vaultId?: string): Promise<string>;
|
|
303
342
|
}
|
package/dist/gateway-client.js
CHANGED
|
@@ -154,8 +154,12 @@ export class GatewayClient {
|
|
|
154
154
|
return JSON.stringify(result);
|
|
155
155
|
}
|
|
156
156
|
async searchNotes(params) {
|
|
157
|
-
const
|
|
158
|
-
return JSON.stringify(
|
|
157
|
+
const data = await this.request("POST", "/api/agent/search-notes", params);
|
|
158
|
+
return JSON.stringify(data, null, 2);
|
|
159
|
+
}
|
|
160
|
+
async search(params) {
|
|
161
|
+
const data = await this.request("POST", "/api/agent/search", params);
|
|
162
|
+
return JSON.stringify(data, null, 2);
|
|
159
163
|
}
|
|
160
164
|
async createNote(params) {
|
|
161
165
|
const result = await this.request("POST", "/api/agent/create-note", params);
|
|
@@ -169,14 +173,6 @@ export class GatewayClient {
|
|
|
169
173
|
const result = await this.request("POST", "/api/agent/delete-note", { noteId });
|
|
170
174
|
return JSON.stringify(result);
|
|
171
175
|
}
|
|
172
|
-
async semanticSearch(params) {
|
|
173
|
-
const result = await this.request("POST", "/api/agent/semantic-search", params);
|
|
174
|
-
return JSON.stringify(result);
|
|
175
|
-
}
|
|
176
|
-
async searchAndRead(params) {
|
|
177
|
-
const result = await this.request("POST", "/api/agent/search-and-read", params);
|
|
178
|
-
return JSON.stringify(result);
|
|
179
|
-
}
|
|
180
176
|
// ─── Folder operations ──────────────────────────────────────────────────
|
|
181
177
|
async listFolders(params) {
|
|
182
178
|
const result = await this.request("POST", "/api/agent/list-folders", params);
|
|
@@ -274,6 +270,11 @@ export class GatewayClient {
|
|
|
274
270
|
const result = await this.request("POST", "/api/agent/read-document", params);
|
|
275
271
|
return JSON.stringify(result);
|
|
276
272
|
}
|
|
273
|
+
// ─── Recall ──────────────────────────────────────────────────────────────
|
|
274
|
+
async recall(params) {
|
|
275
|
+
const result = await this.request("POST", "/api/agent/recall", params);
|
|
276
|
+
return JSON.stringify(result);
|
|
277
|
+
}
|
|
277
278
|
async readDocs(params) {
|
|
278
279
|
const result = await this.request("POST", "/api/agent/read-docs", params);
|
|
279
280
|
return JSON.stringify(result);
|
|
@@ -282,4 +283,51 @@ export class GatewayClient {
|
|
|
282
283
|
const result = await this.request("POST", "/api/agent/update-document", params);
|
|
283
284
|
return JSON.stringify(result);
|
|
284
285
|
}
|
|
286
|
+
// ─── Media operations ───────────────────────────────────────────────────────
|
|
287
|
+
async attachMedia(params) {
|
|
288
|
+
const url = `${this.baseUrl}/api/agent/attach-media`;
|
|
289
|
+
const headers = {
|
|
290
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
291
|
+
};
|
|
292
|
+
if (this.sessionRunId) {
|
|
293
|
+
headers["X-Agent-Run-Id"] = this.sessionRunId;
|
|
294
|
+
}
|
|
295
|
+
// Build multipart form data
|
|
296
|
+
const formData = new FormData();
|
|
297
|
+
const ab = params.fileBytes.buffer.slice(params.fileBytes.byteOffset, params.fileBytes.byteOffset + params.fileBytes.byteLength);
|
|
298
|
+
const blob = new Blob([ab], { type: params.mimeType });
|
|
299
|
+
formData.append("file", blob, params.fileName);
|
|
300
|
+
if (params.memoryId)
|
|
301
|
+
formData.append("memoryId", params.memoryId);
|
|
302
|
+
if (params.vaultId)
|
|
303
|
+
formData.append("vaultId", params.vaultId);
|
|
304
|
+
const response = await fetch(url, {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers,
|
|
307
|
+
body: formData,
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
let errorBody;
|
|
311
|
+
try {
|
|
312
|
+
errorBody = await response.json();
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
errorBody = null;
|
|
316
|
+
}
|
|
317
|
+
const message = errorBody?.error ?? `Gateway returned ${response.status}`;
|
|
318
|
+
throw new GatewayError(message, response.status, errorBody);
|
|
319
|
+
}
|
|
320
|
+
const result = await response.json();
|
|
321
|
+
return JSON.stringify(result);
|
|
322
|
+
}
|
|
323
|
+
async downloadMedia(params) {
|
|
324
|
+
return this.request("POST", "/api/agent/download-media", params);
|
|
325
|
+
}
|
|
326
|
+
async deleteMedia(attachmentId, vaultId) {
|
|
327
|
+
const body = { attachmentId };
|
|
328
|
+
if (vaultId)
|
|
329
|
+
body.vaultId = vaultId;
|
|
330
|
+
const res = await this.request("POST", "/api/agent/delete-media", body);
|
|
331
|
+
return JSON.stringify(res);
|
|
332
|
+
}
|
|
285
333
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
7
|
import { z } from "zod";
|
|
@@ -10,8 +12,6 @@ import { listNotes } from "./tools/list-notes.js";
|
|
|
10
12
|
import { readNote } from "./tools/read-note.js";
|
|
11
13
|
import { readNotes } from "./tools/read-notes.js";
|
|
12
14
|
import { searchNotes } from "./tools/search-notes.js";
|
|
13
|
-
import { semanticSearch } from "./tools/semantic-search.js";
|
|
14
|
-
import { searchAndRead } from "./tools/search-and-read.js";
|
|
15
15
|
import { createNote } from "./tools/create-note.js";
|
|
16
16
|
import { updateNote } from "./tools/update-note.js";
|
|
17
17
|
import { deleteNote } from "./tools/delete-note.js";
|
|
@@ -27,6 +27,8 @@ import { updateMemoryTool } from "./tools/update-memory.js";
|
|
|
27
27
|
import { cleanupMemories } from "./tools/cleanup-memories.js";
|
|
28
28
|
import { getLinks, addLink, removeLink } from "./tools/knowledge-links.js";
|
|
29
29
|
import { exploreGraph } from "./tools/explore-graph.js";
|
|
30
|
+
import { recall } from "./tools/recall.js";
|
|
31
|
+
import { search } from "./tools/search.js";
|
|
30
32
|
import { sendMessage, ackMessage, readMessages } from "./tools/agent-messages.js";
|
|
31
33
|
// Task tools are thin wrappers around memory tools — no separate agent-tasks import needed
|
|
32
34
|
import { resolveVaultId } from "./tools/resolve-vault-id.js";
|
|
@@ -50,6 +52,30 @@ const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "epis
|
|
|
50
52
|
const memoryTypeEnum = z.enum(MEMORY_TYPES);
|
|
51
53
|
/** Remind agents to checkpoint every N tool calls. */
|
|
52
54
|
const CHECKPOINT_REMINDER_INTERVAL = 20;
|
|
55
|
+
/** Max age (ms) of the hook session file to be considered valid. */
|
|
56
|
+
const HOOK_SESSION_MAX_AGE_MS = 120_000;
|
|
57
|
+
/**
|
|
58
|
+
* Read the session ID written by the SessionStart hook.
|
|
59
|
+
* Returns null if the file is missing, stale (>120s), or invalid.
|
|
60
|
+
*/
|
|
61
|
+
function readHookSessionId() {
|
|
62
|
+
try {
|
|
63
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
64
|
+
const filePath = join(home, ".exovault-mcp", "current-session.json");
|
|
65
|
+
const raw = readFileSync(filePath, "utf8");
|
|
66
|
+
const data = JSON.parse(raw);
|
|
67
|
+
if (data.sessionId &&
|
|
68
|
+
data.timestamp &&
|
|
69
|
+
Date.now() - data.timestamp < HOOK_SESSION_MAX_AGE_MS) {
|
|
70
|
+
process.stderr.write(`[exovault-mcp] Using hook session ID: ${data.sessionId}\n`);
|
|
71
|
+
return data.sessionId;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// File doesn't exist or is invalid — fall back to random UUID
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
53
79
|
async function main() {
|
|
54
80
|
// ─── Detect mode: gateway (agent key) or direct (Supabase) ─────────────
|
|
55
81
|
const config = await readConfig();
|
|
@@ -60,10 +86,10 @@ async function main() {
|
|
|
60
86
|
let allowedVaultIds;
|
|
61
87
|
let gwAgentType;
|
|
62
88
|
let gwAgentLabel;
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
const sessionRunId = randomUUID();
|
|
89
|
+
// Try to reuse the session ID written by the SessionStart hook so that
|
|
90
|
+
// MCP tool calls and hook-captured turns share the same dashboard session.
|
|
91
|
+
// Falls back to a random UUID if the hook file is missing or stale.
|
|
92
|
+
const sessionRunId = readHookSessionId() ?? randomUUID();
|
|
67
93
|
if (gwConfig) {
|
|
68
94
|
gw = new GatewayClient(gwConfig.apiUrl, gwConfig.agentKey, sessionRunId);
|
|
69
95
|
try {
|
|
@@ -344,7 +370,7 @@ async function main() {
|
|
|
344
370
|
if (gw) {
|
|
345
371
|
instructionLines.push("Running in gateway mode. Turn ingestion is automatic.");
|
|
346
372
|
}
|
|
347
|
-
instructionLines.push("", "## Tasks", "`create_task` (title, description, status, priority, assignedAgentId, doneWhen). `update_task` to change status. `list_tasks` to view.", "Set `doneWhen` for auto-detect completion. Tasks are memories with memoryType='task'.", "assignedAgentId: null=unassigned, 'any'=any agent, '<type>'=specific. Check assigned tasks at session start.", "", "## Messages", "Pending messages from users/agents appear in `session_start` and `context_checkpoint` responses under `pendingMessages`.", "**When you receive a message**: respond with `send_message(targetId: 'user', content: '...', parentMessageId: '<message.id>')` to reply in-thread.", "`ack_message(messageId)` to acknowledge without replying. `read_messages(agentId)` to fetch messages on demand.", "", "## Memory Protocol", "1. Scope to vaultId. 2. SEARCH FIRST before writing/answering. 3. externalWriteId for idempotency.", "4. Set importance+confidence (1-5). 5. Extract entities. 6. relatedMemoryIds for links. 7. supersededById for corrections.", "Types: fact, skill, preference, constraint, task, episodic, correction. Always set dedup:true, agentId, agentRunId.", "", "## Retrieval Tools (pick the right one)", "- `search_memories` — hybrid search. Use compact:true, then read_memories for full content.", "- `explore_graph` — **PREFERRED for deep retrieval**. query/nodeId → multi-hop graph map (nodes+edges). Then read_memories/read_note for full content. Zero LLM cost.", "- `
|
|
373
|
+
instructionLines.push("", "## Tasks", "`create_task` (title, description, status, priority, assignedAgentId, doneWhen). `update_task` to change status. `list_tasks` to view.", "Set `doneWhen` for auto-detect completion. Tasks are memories with memoryType='task'.", "assignedAgentId: null=unassigned, 'any'=any agent, '<type>'=specific. Check assigned tasks at session start.", "", "## Messages", "Pending messages from users/agents appear in `session_start` and `context_checkpoint` responses under `pendingMessages`.", "**When you receive a message**: respond with `send_message(targetId: 'user', content: '...', parentMessageId: '<message.id>')` to reply in-thread.", "`ack_message(messageId)` to acknowledge without replying. `read_messages(agentId)` to fetch messages on demand.", "", "## Memory Protocol", "1. Scope to vaultId. 2. SEARCH FIRST before writing/answering. 3. externalWriteId for idempotency.", "4. Set importance+confidence (1-5). 5. Extract entities. 6. relatedMemoryIds for links. 7. supersededById for corrections.", "Types: fact, skill, preference, constraint, task, episodic, correction. Always set dedup:true, agentId, agentRunId.", "", "## Retrieval Tools (pick the right one)", "- `search` — **universal search** across memories+notes with cross-type MMR. Set scope='all' (default), 'memories', or 'notes'.", "- `search_memories` — hybrid search memories only. Use compact:true, then read_memories for full content.", "- `search_notes` — hybrid search notes only. Use includeContent:true for full content, or read_notes for specific IDs.", "- `explore_graph` — **PREFERRED for deep retrieval**. query/nodeId → multi-hop graph map (nodes+edges). Then read_memories/read_note for full content. Zero LLM cost.", "- `get_links` / `get_related_memories` — single-hop link traversal.", "- `read_document` — vault docs (instructions, skills, checks).");
|
|
348
374
|
const instructionsText = instructionLines.join("\n");
|
|
349
375
|
// ── Guard: keep instructions ≤4,000 chars ─────────────────────────
|
|
350
376
|
const INSTRUCTION_CHAR_LIMIT = 4_000;
|
|
@@ -411,17 +437,22 @@ async function main() {
|
|
|
411
437
|
})));
|
|
412
438
|
// ─── search_notes ─────────────────────────────────────────────────────────
|
|
413
439
|
server.registerTool("search_notes", {
|
|
414
|
-
description: "Search notes
|
|
440
|
+
description: "Search notes using 4-signal hybrid scoring (semantic + BM25 + blind index + graph) with RRF fusion and MMR diversity. Gateway mode uses server-side pipeline; direct mode uses local Supabase queries. Use includeContent:true to get full note content inline, or follow up with read_notes for specific IDs. Consider `search` tool (scope='notes') for unified cross-type results.",
|
|
415
441
|
inputSchema: {
|
|
416
|
-
query: s(z.string().min(1).describe("Search query")),
|
|
442
|
+
query: s(z.string().min(1).describe("Search query (natural language or keywords)")),
|
|
443
|
+
topK: s(z.number().int().min(1).max(50).optional().describe("Max results (default 10)")),
|
|
444
|
+
threshold: s(z.number().min(0).max(1).optional().describe("Minimum similarity threshold 0-1 (default 0.5)")),
|
|
445
|
+
searchMode: s(z.enum(["auto", "hybrid", "bm25", "semantic"]).optional().describe("Search mode: auto (default), hybrid, bm25, or semantic")),
|
|
446
|
+
diversity: s(z.number().min(0).max(1).optional().describe("MMR diversity balance 0-1 (default 0.7). Higher = more relevance, lower = more diversity.")),
|
|
417
447
|
vaultId: s(z.string().uuid().optional().describe("Limit search to a specific vault")),
|
|
418
|
-
|
|
448
|
+
includeContent: s(z.boolean().optional().describe("Include full note content in results (default false, returns preview only)")),
|
|
449
|
+
compact: s(z.boolean().optional().describe("Return shorter previews (200 chars instead of 500)")),
|
|
419
450
|
},
|
|
420
451
|
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
421
|
-
const
|
|
452
|
+
const input = args;
|
|
422
453
|
return gw
|
|
423
|
-
? await gw.searchNotes({
|
|
424
|
-
: await searchNotes(ctx,
|
|
454
|
+
? await gw.searchNotes({ ...input, vaultId: resolveVault(input.vaultId) })
|
|
455
|
+
: await searchNotes(ctx, { ...input, vaultId: resolveVaultId(ctx, input.vaultId) });
|
|
425
456
|
})));
|
|
426
457
|
// ─── create_note ──────────────────────────────────────────────────────────
|
|
427
458
|
server.registerTool("create_note", {
|
|
@@ -522,35 +553,6 @@ async function main() {
|
|
|
522
553
|
const { noteIds } = args;
|
|
523
554
|
return gw ? await gw.readNotes(noteIds) : await readNotes(ctx, noteIds);
|
|
524
555
|
})));
|
|
525
|
-
// ─── semantic_search ────────────────────────────────────────────────────
|
|
526
|
-
server.registerTool("semantic_search", {
|
|
527
|
-
description: "Search notes by meaning using vector embeddings. Finds conceptually similar content even without exact keyword matches. Requires OpenAI API key in config.",
|
|
528
|
-
inputSchema: {
|
|
529
|
-
query: s(z.string().min(1).describe("Natural language search query")),
|
|
530
|
-
topK: s(z.number().int().min(1).max(50).optional().describe("Max results (default 10)")),
|
|
531
|
-
threshold: s(z.number().min(0).max(1).optional().describe("Minimum similarity threshold 0-1 (default 0.5)")),
|
|
532
|
-
vaultId: s(z.string().uuid().optional().describe("Vault to search within. Uses default vault if omitted.")),
|
|
533
|
-
},
|
|
534
|
-
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
535
|
-
const { query, topK, threshold, vaultId } = args;
|
|
536
|
-
return gw
|
|
537
|
-
? await gw.semanticSearch({ query, topK, threshold, vaultId: resolveVault(vaultId) })
|
|
538
|
-
: await semanticSearch(ctx, query, topK, threshold);
|
|
539
|
-
})));
|
|
540
|
-
// ─── search_and_read ────────────────────────────────────────────────────
|
|
541
|
-
server.registerTool("search_and_read", {
|
|
542
|
-
description: "Search notes using hybrid scoring (70% semantic + 30% keyword) and return the full content of top matches. Falls back to keyword-only if no OpenAI key, and to recency if no matches found. Best for gathering all relevant content on a topic.",
|
|
543
|
-
inputSchema: {
|
|
544
|
-
query: s(z.string().min(1).describe("Search query (natural language or keywords)")),
|
|
545
|
-
maxNotes: s(z.number().int().min(1).max(20).optional().describe("Max notes to return (default 5)")),
|
|
546
|
-
vaultId: s(z.string().uuid().optional().describe("Vault to search within. Uses default vault if omitted.")),
|
|
547
|
-
},
|
|
548
|
-
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
549
|
-
const { query, maxNotes, vaultId } = args;
|
|
550
|
-
return gw
|
|
551
|
-
? await gw.searchAndRead({ query, maxNotes, vaultId: resolveVault(vaultId) })
|
|
552
|
-
: await searchAndRead(ctx, query, maxNotes);
|
|
553
|
-
})));
|
|
554
556
|
// ─── write_memory ───────────────────────────────────────────────────────────
|
|
555
557
|
server.registerTool("write_memory", {
|
|
556
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.",
|
|
@@ -611,6 +613,31 @@ async function main() {
|
|
|
611
613
|
}
|
|
612
614
|
return await searchMemories(ctx, { ...input, vaultId: resolveVaultId(ctx, input.vaultId) });
|
|
613
615
|
})));
|
|
616
|
+
// ─── search (universal) ─────────────────────────────────────────────────────
|
|
617
|
+
server.registerTool("search", {
|
|
618
|
+
description: "Universal search across both memories and notes. Set scope to 'all' (default) for cross-type MMR-ranked results, 'memories' for memories only, or 'notes' for notes only. In gateway mode, delegates to the server for full cross-type MMR re-ranking. In direct mode, best-effort interleave.",
|
|
619
|
+
inputSchema: {
|
|
620
|
+
query: s(z.string().min(1).describe("Natural language search query")),
|
|
621
|
+
topK: s(z.number().int().min(1).max(50).optional().describe("Max results (default 10)")),
|
|
622
|
+
threshold: s(z.number().min(0).max(1).optional().describe("Similarity threshold 0-1 (default 0.5)")),
|
|
623
|
+
searchMode: s(z.enum(["auto", "hybrid", "bm25", "semantic"]).optional().describe("Search mode (default: auto)")),
|
|
624
|
+
diversity: s(z.number().min(0).max(1).optional().describe("MMR diversity balance 0-1 (default 0.7)")),
|
|
625
|
+
vaultId: s(z.string().uuid().optional().describe("Vault filter")),
|
|
626
|
+
includeContent: s(z.boolean().optional().describe("Include full note content (default false)")),
|
|
627
|
+
compact: s(z.boolean().optional().describe("Return shorter previews")),
|
|
628
|
+
scope: s(z.enum(["all", "memories", "notes"]).optional().describe("Search scope: all (default), memories, or notes")),
|
|
629
|
+
memoryType: s(z.string().optional().describe("Filter by memory type (memories scope only)")),
|
|
630
|
+
entity: s(z.string().optional().describe("Search by entity name (memories scope only)")),
|
|
631
|
+
includeArchived: s(z.boolean().optional().describe("Include archived memories (default false)")),
|
|
632
|
+
decayHalfLife: s(z.number().int().min(1).max(365).optional().describe("Temporal decay half-life in days (default 30)")),
|
|
633
|
+
},
|
|
634
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
635
|
+
const input = args;
|
|
636
|
+
if (gw) {
|
|
637
|
+
return await gw.search({ ...input, vaultId: resolveVault(input.vaultId) });
|
|
638
|
+
}
|
|
639
|
+
return await search(ctx, { ...input, vaultId: resolveVaultId(ctx, input.vaultId) });
|
|
640
|
+
})));
|
|
614
641
|
// ─── read_memories ──────────────────────────────────────────────────────────
|
|
615
642
|
server.registerTool("read_memories", {
|
|
616
643
|
description: "Read and decrypt full memory entries by IDs.",
|
|
@@ -689,7 +716,7 @@ async function main() {
|
|
|
689
716
|
})).optional(),
|
|
690
717
|
sourceNoteIds: z.array(z.string().uuid()).optional(),
|
|
691
718
|
supersededById: z.string().uuid().optional(),
|
|
692
|
-
})).max(50).describe("
|
|
719
|
+
})).max(50).default([]).describe("Memories to bulk-save (0-50). Defaults to empty array — omit or pass [] when only sessionSummary is needed.")),
|
|
693
720
|
sessionSummary: s(z.string().min(1).describe("REQUIRED. Narrative summary of what happened this session — what was discussed, decided, and accomplished. Saved as an episodic memory. Do NOT write tool call stats — describe the work in human terms.")),
|
|
694
721
|
vaultId: s(z.string().uuid().optional().describe("Vault/project scope for all memories. Required unless defaultVaultId is configured.")),
|
|
695
722
|
agentId: s(z.string().optional().describe("Agent identifier")),
|
|
@@ -1168,6 +1195,81 @@ async function main() {
|
|
|
1168
1195
|
}
|
|
1169
1196
|
return await readMessages(ctx, { ...input, agentId, vaultId: resolveVaultId(ctx, input.vaultId) });
|
|
1170
1197
|
})));
|
|
1198
|
+
// ─── recall ─────────────────────────────────────────────────────────────
|
|
1199
|
+
server.registerTool("recall", {
|
|
1200
|
+
description: "Deep context retrieval with three modes: " +
|
|
1201
|
+
"temporal (session timeline by date range), " +
|
|
1202
|
+
"topic (hybrid BM25+semantic search across memories), " +
|
|
1203
|
+
"graph (knowledge graph expansion from query or node). " +
|
|
1204
|
+
"Use session_start for basic warm-up; use recall for targeted deep retrieval mid-session.",
|
|
1205
|
+
inputSchema: {
|
|
1206
|
+
mode: s(z.enum(["temporal", "topic", "graph"]).describe("temporal: chronological session timeline by date range. " +
|
|
1207
|
+
"topic: hybrid BM25+semantic search across memories. " +
|
|
1208
|
+
"graph: knowledge graph expansion from query or node.")),
|
|
1209
|
+
range: s(z.string().optional().describe("Temporal mode: 'today', 'yesterday', 'last_week', 'last_3_days', 'last_N_days', or ISO date '2026-03-01'")),
|
|
1210
|
+
query: s(z.string().max(2000).optional().describe("Topic/graph mode: search query")),
|
|
1211
|
+
searchMode: s(z.enum(["auto", "hybrid", "bm25", "semantic"]).optional().describe("Topic mode: search strategy. Default: 'hybrid'")),
|
|
1212
|
+
topK: s(z.number().int().min(1).max(50).optional().describe("Topic mode: max results. Default: 10")),
|
|
1213
|
+
nodeId: s(z.string().uuid().optional().describe("Graph mode: start from this node")),
|
|
1214
|
+
maxHops: s(z.number().int().min(1).max(5).optional().describe("Graph mode: traversal depth. Default: 2")),
|
|
1215
|
+
vaultId: s(z.string().uuid().optional().describe("Scope to vault")),
|
|
1216
|
+
},
|
|
1217
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1218
|
+
const input = args;
|
|
1219
|
+
if (gw) {
|
|
1220
|
+
return await gw.recall({ ...input, vaultId: resolveVault(input.vaultId) });
|
|
1221
|
+
}
|
|
1222
|
+
return await recall(ctx, { ...input, vaultId: resolveVaultId(ctx, input.vaultId) });
|
|
1223
|
+
})));
|
|
1224
|
+
// ─── attach_media ──────────────────────────────────────────────────────────
|
|
1225
|
+
server.registerTool("attach_media", {
|
|
1226
|
+
description: "Attach a media file (image, PDF, audio, video) to a memory for multimodal embedding. The file is encrypted at rest and embedded via Gemini for cross-modal search. Accepts base64-encoded file content.\n\nSupported types: images (PNG, JPEG, WebP, GIF — 20MB), documents (PDF — 50MB), audio (MP3, WAV, OGG, WebM — 50MB), video (MP4, WebM, QuickTime — 20MB). SVG is not supported.\n\nPer-user storage quota applies (free tier: 100MB).",
|
|
1227
|
+
inputSchema: {
|
|
1228
|
+
fileBase64: s(z.string().min(1).describe("Base64-encoded file content")),
|
|
1229
|
+
fileName: s(z.string().min(1).describe("Original file name with extension")),
|
|
1230
|
+
mimeType: s(z.string().min(1).describe("MIME type (e.g. image/png, application/pdf, audio/mpeg, video/mp4)")),
|
|
1231
|
+
memoryId: s(z.string().uuid().optional().describe("Memory to attach this media to")),
|
|
1232
|
+
vaultId: s(z.string().uuid().optional().describe("Vault scope")),
|
|
1233
|
+
},
|
|
1234
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1235
|
+
const input = args;
|
|
1236
|
+
if (!gw)
|
|
1237
|
+
throw new Error("attach_media requires gateway mode (agent key)");
|
|
1238
|
+
const fileBytes = Buffer.from(input.fileBase64, "base64");
|
|
1239
|
+
return await gw.attachMedia({
|
|
1240
|
+
fileBytes: new Uint8Array(fileBytes),
|
|
1241
|
+
fileName: input.fileName,
|
|
1242
|
+
mimeType: input.mimeType,
|
|
1243
|
+
memoryId: input.memoryId,
|
|
1244
|
+
vaultId: resolveVault(input.vaultId),
|
|
1245
|
+
});
|
|
1246
|
+
})));
|
|
1247
|
+
// ─── download_media ───────────────────────────────────────────────────────
|
|
1248
|
+
server.registerTool("download_media", {
|
|
1249
|
+
description: "Download and decrypt a media attachment by ID. Returns base64-encoded file content with metadata.",
|
|
1250
|
+
inputSchema: {
|
|
1251
|
+
attachmentId: s(z.string().uuid().describe("Media attachment ID to download")),
|
|
1252
|
+
},
|
|
1253
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1254
|
+
const { attachmentId } = args;
|
|
1255
|
+
if (!gw)
|
|
1256
|
+
throw new Error("download_media requires gateway mode (agent key)");
|
|
1257
|
+
const result = await gw.downloadMedia({ attachmentId });
|
|
1258
|
+
return JSON.stringify(result);
|
|
1259
|
+
})));
|
|
1260
|
+
// ─── delete_media ───────────────────────────────────────────────────────
|
|
1261
|
+
server.registerTool("delete_media", {
|
|
1262
|
+
description: "Delete a media attachment and its embedding. Permanently removes the file from storage.",
|
|
1263
|
+
inputSchema: {
|
|
1264
|
+
attachmentId: s(z.string().uuid().describe("The attachment ID to delete")),
|
|
1265
|
+
vaultId: s(z.string().uuid().optional().describe("Optional vault scope")),
|
|
1266
|
+
},
|
|
1267
|
+
}, auto.wrap(wrapToolHandler(async (args) => {
|
|
1268
|
+
const { attachmentId, vaultId } = args;
|
|
1269
|
+
if (!gw)
|
|
1270
|
+
throw new Error("delete_media requires gateway mode (agent key)");
|
|
1271
|
+
return await gw.deleteMedia(attachmentId, vaultId);
|
|
1272
|
+
})));
|
|
1171
1273
|
// ─── Orphan recovery — flush crashed sessions from previous runs ─────────
|
|
1172
1274
|
try {
|
|
1173
1275
|
const orphans = await scanOrphanedBuffers(10);
|
package/dist/openai.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generates one query embedding using
|
|
2
|
+
* Generates one query embedding using Gemini.
|
|
3
|
+
* Used in direct mode for semantic search queries.
|
|
3
4
|
*/
|
|
4
5
|
export declare function generateQueryEmbedding(text: string, apiKey: string, options?: {
|
|
5
6
|
model?: string;
|
|
6
7
|
baseUrl?: string;
|
|
7
8
|
}): Promise<number[]>;
|
|
8
9
|
/**
|
|
9
|
-
* Generates multiple embeddings in one request
|
|
10
|
+
* Generates multiple embeddings in one request.
|
|
11
|
+
* Used in direct mode for bulk embedding operations.
|
|
10
12
|
*/
|
|
11
13
|
export declare function generateEmbeddings(values: string[], apiKey: string, options?: {
|
|
12
14
|
model?: string;
|