exovault-mcp-server 1.4.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 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
- .select("*")
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 });
@@ -10,7 +10,7 @@ export function resolveEmbeddingConfig(ctx) {
10
10
  if (envApiKey) {
11
11
  return {
12
12
  apiKey: envApiKey,
13
- model: process.env.EMBEDDING_MODEL ?? DEFAULT_EMBEDDING_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
- * Strips the "Session: Xm, agent=Y." metadata prefix and returns the
4
- * first 1-2 substantive sentences. Pure function, no LLM calls.
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
- * Strips the "Session: Xm, agent=Y." metadata prefix and returns the
4
- * first 1-2 substantive sentences. Pure function, no LLM calls.
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
- // Matches "Session: <duration>, agent=<name>." prefix (with optional newlines after)
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 the metadata prefix
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
- // For rule-based format ("Explored: ...\nOutcomes: ..."), join lines
18
- // For LLM narrative, take first 1-2 sentences
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;
@@ -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;
@@ -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);
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import { scanOrphanedBuffers, deleteBuffer as deleteBufferFile } from "./buffer-
49
49
  import { flushSession } from "./session-flush.js";
50
50
  import { coerceSchema } from "./coerce-params.js";
51
51
  const s = (schema) => coerceSchema(schema);
52
- const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction"];
52
+ const MEMORY_TYPES = ["fact", "skill", "preference", "constraint", "task", "episodic", "correction", "decision"];
53
53
  const memoryTypeEnum = z.enum(MEMORY_TYPES);
54
54
  /** Remind agents to checkpoint every N tool calls. */
55
55
  const CHECKPOINT_REMINDER_INTERVAL = 20;
@@ -352,7 +352,7 @@ async function main() {
352
352
  " b. **WRITE on these triggers** — call `write_memory` IMMEDIATELY when:",
353
353
  " - User states a preference, rule, or convention → `preference` or `constraint`",
354
354
  " - You discover a non-obvious fact about the domain/project/topic → `fact` (importance 3-5)",
355
- " - A decision is made (by user or jointly) → `fact` (importance 4-5)",
355
+ " - A decision is made (by user or jointly) → `decision` (importance 4-5, include inputs, reasoning, alternatives, outcome)",
356
356
  " - You solve a problem or learn a procedure → `skill`",
357
357
  " - You hit a surprising gotcha or limitation → `skill` (importance 4)",
358
358
  " - Previous knowledge turns out wrong → `correction` (set supersededById)",
@@ -412,6 +412,20 @@ async function main() {
412
412
  ? await gw.createVault({ name, icon, color })
413
413
  : await createVault(ctx, name, { icon, color });
414
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
+ })));
415
429
  // ─── list_notes ───────────────────────────────────────────────────────────
416
430
  server.registerTool("list_notes", {
417
431
  description: "List notes with titles, tags, and content previews. Optionally filter by vault or folder.",
@@ -558,10 +572,10 @@ async function main() {
558
572
  })));
559
573
  // ─── write_memory ───────────────────────────────────────────────────────────
560
574
  server.registerTool("write_memory", {
561
- 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.",
562
576
  inputSchema: {
563
577
  content: s(z.string().min(1).max(1_000_000).describe("Memory content in plain text")),
564
- 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")),
565
579
  summary: s(z.string().max(500).optional().describe("Optional short summary")),
566
580
  vaultId: s(z.string().uuid().optional().describe("Vault/project scope. Required unless defaultVaultId is configured for this MCP session.")),
567
581
  importance: s(z.number().int().min(1).max(5).optional().describe("Importance from 1 to 5")),
@@ -67,7 +67,7 @@ async function main() {
67
67
  }
68
68
  const embeddingConfig = {
69
69
  apiKey,
70
- model: process.env.EMBEDDING_MODEL ?? DEFAULT_EMBEDDING_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) {
@@ -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
- const durationMs = Date.now() - new Date(data.startedAt).getTime();
457
- const durationMin = Math.round(durationMs / 60_000);
458
- const lines = [
459
- `Session: ${durationMin}min, agent=${data.agentId}.`,
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
  }
@@ -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 ?? {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exovault-mcp-server",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for ExoVault — read, search, and manage encrypted notes from Claude Code",
6
6
  "main": "dist/index.js",