exovault-mcp-server 1.0.2 → 1.1.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/extraction-prompt.js +4 -4
- package/dist/rlm/verify.js +6 -6
- package/dist/rlm/writeback.js +10 -10
- package/dist/setup.js +88 -31
- package/hooks/capture-turn.js +175 -0
- package/package.json +2 -1
|
@@ -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/rlm/verify.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
2
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
|
-
const VERIFY_PROMPT = `You are a verification pass for a recursive retrieval controller.
|
|
4
|
-
|
|
5
|
-
Review the answer below and assess whether it fully addresses the original goal.
|
|
6
|
-
If the answer is incomplete or potentially wrong, explain what's missing.
|
|
7
|
-
If the answer is sufficient, confirm it.
|
|
8
|
-
|
|
3
|
+
const VERIFY_PROMPT = `You are a verification pass for a recursive retrieval controller.
|
|
4
|
+
|
|
5
|
+
Review the answer below and assess whether it fully addresses the original goal.
|
|
6
|
+
If the answer is incomplete or potentially wrong, explain what's missing.
|
|
7
|
+
If the answer is sufficient, confirm it.
|
|
8
|
+
|
|
9
9
|
Respond with JSON: { "verified": true/false, "revisedAnswer": "...", "confidence": 0.0-1.0, "reason": "..." }`;
|
|
10
10
|
/**
|
|
11
11
|
* Run a verification pass on the controller's answer.
|
package/dist/rlm/writeback.js
CHANGED
|
@@ -2,16 +2,16 @@ import { generateText } from "ai";
|
|
|
2
2
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
3
|
import { writeMemory } from "../tools/write-memory.js";
|
|
4
4
|
const ALLOWED_TYPES = new Set(["fact", "plan", "constraint", "skill"]);
|
|
5
|
-
const WRITEBACK_PROMPT = `You are a writeback policy for a knowledge base.
|
|
6
|
-
|
|
7
|
-
Given the goal and answer below, extract any durable knowledge artifacts worth storing as long-term memories.
|
|
8
|
-
|
|
9
|
-
Rules:
|
|
10
|
-
- Only extract genuinely new, durable insights — not trivial or transient information.
|
|
11
|
-
- Each artifact must have a memoryType of: fact, plan, constraint, or skill.
|
|
12
|
-
- Set importance (1-5) and confidence (1-5) appropriately.
|
|
13
|
-
- If nothing is worth storing, return an empty array.
|
|
14
|
-
|
|
5
|
+
const WRITEBACK_PROMPT = `You are a writeback policy for a knowledge base.
|
|
6
|
+
|
|
7
|
+
Given the goal and answer below, extract any durable knowledge artifacts worth storing as long-term memories.
|
|
8
|
+
|
|
9
|
+
Rules:
|
|
10
|
+
- Only extract genuinely new, durable insights — not trivial or transient information.
|
|
11
|
+
- Each artifact must have a memoryType of: fact, plan, constraint, or skill.
|
|
12
|
+
- Set importance (1-5) and confidence (1-5) appropriately.
|
|
13
|
+
- If nothing is worth storing, return an empty array.
|
|
14
|
+
|
|
15
15
|
Respond with JSON: { "artifacts": [{ "content": "...", "memoryType": "fact|plan|constraint|skill", "summary": "...", "importance": 1-5, "confidence": 1-5, "writeReason": "..." }] }`;
|
|
16
16
|
/**
|
|
17
17
|
* Extract durable artifacts from the controller's findings and write them as memories.
|
package/dist/setup.js
CHANGED
|
@@ -1,13 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createClient } from "@supabase/supabase-js";
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { homedir } from "node:os";
|
|
4
|
+
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { homedir, platform } from "node:os";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { deriveWrappingKey, unwrapMasterKey, importMasterKey, bufferToHex, } from "./crypto.js";
|
|
9
10
|
const CONFIG_DIR = join(homedir(), ".exovault-mcp");
|
|
10
11
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
const HOOKS_DIR = join(CONFIG_DIR, "hooks");
|
|
13
|
+
const IS_WINDOWS = platform() === "win32";
|
|
14
|
+
/** Returns platform-appropriate MCP server config snippet. */
|
|
15
|
+
function getMcpServerConfig() {
|
|
16
|
+
if (IS_WINDOWS) {
|
|
17
|
+
return {
|
|
18
|
+
mcpServers: {
|
|
19
|
+
exovault: {
|
|
20
|
+
command: "cmd",
|
|
21
|
+
args: ["/c", "npx", "-y", "exovault-mcp-server"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
mcpServers: {
|
|
28
|
+
exovault: {
|
|
29
|
+
command: "npx",
|
|
30
|
+
args: ["-y", "exovault-mcp-server"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Returns hook config snippet for turn capture. */
|
|
36
|
+
function getHooksConfig() {
|
|
37
|
+
const hookPath = "~/.exovault-mcp/hooks/capture-turn.js";
|
|
38
|
+
return {
|
|
39
|
+
hooks: {
|
|
40
|
+
UserPromptSubmit: [{
|
|
41
|
+
matcher: "",
|
|
42
|
+
hooks: [{
|
|
43
|
+
type: "command",
|
|
44
|
+
command: `node ${hookPath} user`,
|
|
45
|
+
timeout: 15,
|
|
46
|
+
}],
|
|
47
|
+
}],
|
|
48
|
+
Stop: [{
|
|
49
|
+
matcher: "",
|
|
50
|
+
hooks: [{
|
|
51
|
+
type: "command",
|
|
52
|
+
command: `node ${hookPath} assistant`,
|
|
53
|
+
timeout: 15,
|
|
54
|
+
}],
|
|
55
|
+
}],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Copies capture-turn.js hook to ~/.exovault-mcp/hooks/ */
|
|
60
|
+
async function installHook() {
|
|
61
|
+
try {
|
|
62
|
+
await mkdir(HOOKS_DIR, { recursive: true, mode: 0o700 });
|
|
63
|
+
// Resolve the bundled hook relative to this file's package
|
|
64
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
65
|
+
const srcHook = join(thisDir, "..", "hooks", "capture-turn.js");
|
|
66
|
+
const destHook = join(HOOKS_DIR, "capture-turn.js");
|
|
67
|
+
await copyFile(srcHook, destHook);
|
|
68
|
+
console.log(`Turn capture hook installed to: ${destHook}`);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.warn("Could not install turn capture hook:", err instanceof Error ? err.message : err);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Prints Claude Code config snippet + optional hook instructions. */
|
|
77
|
+
function printSetupInstructions(hookInstalled) {
|
|
78
|
+
console.log();
|
|
79
|
+
console.log("Add this to your project's .mcp.json or ~/.claude/settings.json:");
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(JSON.stringify(getMcpServerConfig(), null, 2));
|
|
82
|
+
if (hookInstalled) {
|
|
83
|
+
console.log();
|
|
84
|
+
console.log("Optional: Add turn capture hooks to .claude/settings.json for richer memory:");
|
|
85
|
+
console.log("(Captures full conversation context — not just MCP tool calls)");
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(JSON.stringify(getHooksConfig(), null, 2));
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
console.log("Setup complete! Restart Claude Code to connect.");
|
|
91
|
+
}
|
|
11
92
|
function printUsage() {
|
|
12
93
|
console.log("Usage:");
|
|
13
94
|
console.log(" exovault-mcp-server setup --agent-key <key> [--api-url <url>] (gateway mode)");
|
|
@@ -90,20 +171,8 @@ async function setupGatewayMode(agentKey, apiUrl) {
|
|
|
90
171
|
});
|
|
91
172
|
console.log();
|
|
92
173
|
console.log(`Config saved to: ${CONFIG_FILE}`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
console.log("Add this to your ~/.claude/settings.json (mcpServers section):");
|
|
96
|
-
console.log();
|
|
97
|
-
console.log(JSON.stringify({
|
|
98
|
-
mcpServers: {
|
|
99
|
-
exovault: {
|
|
100
|
-
command: "npx",
|
|
101
|
-
args: ["exovault-mcp-server"],
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
}, null, 2));
|
|
105
|
-
console.log();
|
|
106
|
-
console.log("Setup complete! Restart Claude Code to connect.");
|
|
174
|
+
const hookInstalled = await installHook();
|
|
175
|
+
printSetupInstructions(hookInstalled);
|
|
107
176
|
}
|
|
108
177
|
async function main() {
|
|
109
178
|
// Parse CLI args
|
|
@@ -237,20 +306,8 @@ async function main() {
|
|
|
237
306
|
rl3.close();
|
|
238
307
|
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
239
308
|
console.log(`Config saved to: ${CONFIG_FILE}`);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log("Add this to your ~/.claude/settings.json (mcpServers section):");
|
|
243
|
-
console.log();
|
|
244
|
-
console.log(JSON.stringify({
|
|
245
|
-
mcpServers: {
|
|
246
|
-
exovault: {
|
|
247
|
-
command: "npx",
|
|
248
|
-
args: ["exovault-mcp-server"],
|
|
249
|
-
},
|
|
250
|
-
},
|
|
251
|
-
}, null, 2));
|
|
252
|
-
console.log();
|
|
253
|
-
console.log("Setup complete! Restart Claude Code to connect.");
|
|
309
|
+
const hookInstalled = await installHook();
|
|
310
|
+
printSetupInstructions(hookInstalled);
|
|
254
311
|
}
|
|
255
312
|
catch (err) {
|
|
256
313
|
console.error("Setup failed:", err instanceof Error ? err.message : err);
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
3
|
+
/**
|
|
4
|
+
* ExoVault turn capture hook for Claude Code.
|
|
5
|
+
* Captures user prompts and assistant responses, POSTs to ingest-turn API.
|
|
6
|
+
*
|
|
7
|
+
* Usage (called by Claude Code hooks, not directly):
|
|
8
|
+
* echo '{"prompt":"hello"}' | node capture-turn.js user
|
|
9
|
+
* echo '{"last_assistant_message":"hi"}' | node capture-turn.js assistant
|
|
10
|
+
*
|
|
11
|
+
* Config resolution (first match wins):
|
|
12
|
+
* 1. EXOVAULT_AGENT_KEY / EXOVAULT_API_URL env vars
|
|
13
|
+
* 2. ~/.exovault-mcp/config.json (agentKey / apiUrl fields)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const path = require("path");
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
|
|
19
|
+
const MAX_CONTENT_LENGTH = 50_000;
|
|
20
|
+
const MIN_CONTENT_LENGTH = 5;
|
|
21
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
22
|
+
|
|
23
|
+
/** Default config file path — same as the MCP server. */
|
|
24
|
+
const CONFIG_PATH = path.join(
|
|
25
|
+
process.env.HOME || process.env.USERPROFILE || "~",
|
|
26
|
+
".exovault-mcp",
|
|
27
|
+
"config.json",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve agent key and API URL from env vars, falling back to config file.
|
|
32
|
+
* Returns { agentKey, apiUrl } or null if not configured.
|
|
33
|
+
*/
|
|
34
|
+
function resolveConfig() {
|
|
35
|
+
// 1. Env vars take priority
|
|
36
|
+
let agentKey = process.env.EXOVAULT_AGENT_KEY || "";
|
|
37
|
+
let apiUrl = process.env.EXOVAULT_API_URL || "";
|
|
38
|
+
|
|
39
|
+
// 2. Fall back to config file
|
|
40
|
+
if (!agentKey) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
43
|
+
const config = JSON.parse(raw);
|
|
44
|
+
if (!agentKey && config.agentKey) agentKey = config.agentKey;
|
|
45
|
+
if (!apiUrl && config.apiUrl) apiUrl = config.apiUrl;
|
|
46
|
+
} catch {
|
|
47
|
+
// Config file missing or invalid — that's fine
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!agentKey) return null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
agentKey,
|
|
55
|
+
apiUrl: (apiUrl || "https://exovault.co").replace(/\/+$/, ""),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract content from hook input based on role.
|
|
61
|
+
* Returns null if content should be skipped.
|
|
62
|
+
*/
|
|
63
|
+
function extractContent(input, role) {
|
|
64
|
+
if (role === "user") {
|
|
65
|
+
return input.prompt || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (role === "assistant") {
|
|
69
|
+
// Skip re-entry: stop_hook_active means Stop hook already fired
|
|
70
|
+
if (input.stop_hook_active) return null;
|
|
71
|
+
return input.last_assistant_message || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Prepare the ingest-turn request body.
|
|
79
|
+
*/
|
|
80
|
+
function buildRequestBody(content, role, sessionId) {
|
|
81
|
+
// Truncate oversized content
|
|
82
|
+
let trimmed = content;
|
|
83
|
+
if (trimmed.length > MAX_CONTENT_LENGTH) {
|
|
84
|
+
trimmed = trimmed.slice(0, MAX_CONTENT_LENGTH) + "\n[truncated]";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const body = {
|
|
88
|
+
content: trimmed,
|
|
89
|
+
role,
|
|
90
|
+
agentId: "claude_code",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (sessionId) {
|
|
94
|
+
body.agentRunId = sessionId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return body;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* POST a turn to the ExoVault ingest-turn API.
|
|
102
|
+
*/
|
|
103
|
+
async function postTurn(apiUrl, agentKey, body) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
await fetch(`${apiUrl}/api/agent/ingest-turn`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
Authorization: `Bearer ${agentKey}`,
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
signal: controller.signal,
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Main entry point — reads stdin, extracts content, POSTs to API.
|
|
124
|
+
*/
|
|
125
|
+
async function main() {
|
|
126
|
+
const role = process.argv[2];
|
|
127
|
+
if (role !== "user" && role !== "assistant") {
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const config = resolveConfig();
|
|
132
|
+
if (!config) {
|
|
133
|
+
process.exit(0); // Not configured — silently skip
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Read JSON from stdin
|
|
137
|
+
let data = "";
|
|
138
|
+
process.stdin.setEncoding("utf8");
|
|
139
|
+
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
142
|
+
process.stdin.on("end", resolve);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const input = JSON.parse(data);
|
|
146
|
+
const content = extractContent(input, role);
|
|
147
|
+
|
|
148
|
+
if (!content || content.length < MIN_CONTENT_LENGTH) {
|
|
149
|
+
process.exit(0); // Too short or empty — skip
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const body = buildRequestBody(content, role, input.session_id);
|
|
153
|
+
await postTurn(config.apiUrl, config.agentKey, body);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Export for testing, execute when run directly
|
|
157
|
+
if (typeof module !== "undefined") {
|
|
158
|
+
module.exports = {
|
|
159
|
+
extractContent,
|
|
160
|
+
buildRequestBody,
|
|
161
|
+
postTurn,
|
|
162
|
+
resolveConfig,
|
|
163
|
+
MAX_CONTENT_LENGTH,
|
|
164
|
+
MIN_CONTENT_LENGTH,
|
|
165
|
+
FETCH_TIMEOUT_MS,
|
|
166
|
+
CONFIG_PATH,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (require.main === module) {
|
|
171
|
+
main().then(
|
|
172
|
+
() => process.exit(0),
|
|
173
|
+
() => process.exit(0), // Silently swallow all errors
|
|
174
|
+
);
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "exovault-mcp-server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist/",
|
|
12
|
+
"hooks/",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|