context-vault 3.13.0 → 3.16.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/bin/cli.js +263 -414
- package/dist/error-log.d.ts +2 -0
- package/dist/error-log.d.ts.map +1 -1
- package/dist/error-log.js +31 -1
- package/dist/error-log.js.map +1 -1
- package/dist/register-tools.d.ts.map +1 -1
- package/dist/register-tools.js +4 -0
- package/dist/register-tools.js.map +1 -1
- package/dist/server.js +23 -426
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +17 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +26 -1
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/delete-context.d.ts +1 -1
- package/dist/tools/delete-context.d.ts.map +1 -1
- package/dist/tools/delete-context.js +15 -2
- package/dist/tools/delete-context.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +3 -2
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +7 -15
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +570 -111
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/publish-to-team.js +1 -1
- package/dist/tools/publish-to-team.js.map +1 -1
- package/dist/tools/save-context.js +2 -2
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts +20 -7
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +406 -439
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +4 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/categories.js +8 -0
- package/node_modules/@context-vault/core/dist/categories.js.map +1 -1
- package/node_modules/@context-vault/core/dist/compact.d.ts +38 -0
- package/node_modules/@context-vault/core/dist/compact.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/compact.js +127 -0
- package/node_modules/@context-vault/core/dist/compact.js.map +1 -0
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +12 -0
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +40 -4
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/main.d.ts +6 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +5 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +13 -1
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +50 -5
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts +36 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js +227 -0
- package/node_modules/@context-vault/core/dist/tier-analysis.js.map +1 -0
- package/node_modules/@context-vault/core/dist/types.d.ts +12 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/watch.d.ts +21 -0
- package/node_modules/@context-vault/core/dist/watch.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/watch.js +230 -0
- package/node_modules/@context-vault/core/dist/watch.js.map +1 -0
- package/node_modules/@context-vault/core/package.json +13 -1
- package/node_modules/@context-vault/core/src/capture.ts +4 -0
- package/node_modules/@context-vault/core/src/categories.ts +8 -0
- package/node_modules/@context-vault/core/src/compact.ts +183 -0
- package/node_modules/@context-vault/core/src/config.ts +8 -0
- package/node_modules/@context-vault/core/src/db.ts +40 -4
- package/node_modules/@context-vault/core/src/main.ts +10 -0
- package/node_modules/@context-vault/core/src/search.ts +55 -4
- package/node_modules/@context-vault/core/src/tier-analysis.ts +299 -0
- package/node_modules/@context-vault/core/src/types.ts +10 -0
- package/node_modules/@context-vault/core/src/watch.ts +269 -0
- package/package.json +2 -2
- package/scripts/postinstall.js +26 -1
- package/src/error-log.ts +30 -0
- package/src/register-tools.ts +4 -0
- package/src/server.ts +23 -423
- package/src/status.ts +17 -0
- package/src/tools/context-status.ts +30 -1
- package/src/tools/delete-context.ts +10 -5
- package/src/tools/get-context.ts +3 -2
- package/src/tools/list-context.ts +620 -119
- package/src/tools/publish-to-team.ts +1 -1
- package/src/tools/save-context.ts +2 -2
- package/src/tools/session-start.ts +444 -484
package/src/server.ts
CHANGED
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
|
|
9
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
10
6
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
11
7
|
import { join, dirname } from 'node:path';
|
|
12
8
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -31,240 +27,9 @@ import {
|
|
|
31
27
|
} from '@context-vault/core/db';
|
|
32
28
|
import { registerTools } from './register-tools.js';
|
|
33
29
|
import { pruneExpired } from '@context-vault/core/index';
|
|
30
|
+
import { startWatcher } from '@context-vault/core/watch';
|
|
34
31
|
import { setSessionId } from '@context-vault/core/search';
|
|
35
32
|
|
|
36
|
-
const DAEMON_PORT = 3377;
|
|
37
|
-
const PID_PATH = join(homedir(), '.context-mcp', 'daemon.pid');
|
|
38
|
-
|
|
39
|
-
async function tryAutoDaemon(): Promise<void> {
|
|
40
|
-
// Check if daemon is already running
|
|
41
|
-
if (existsSync(PID_PATH)) {
|
|
42
|
-
try {
|
|
43
|
-
const { pid, port } = JSON.parse(readFileSync(PID_PATH, 'utf-8'));
|
|
44
|
-
process.kill(pid, 0); // throws if dead
|
|
45
|
-
const res = await fetch(`http://localhost:${port}/health`);
|
|
46
|
-
if (res.ok) return; // daemon is healthy, nothing to do
|
|
47
|
-
} catch {
|
|
48
|
-
// stale PID file or unhealthy, continue to start daemon
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const { spawn, execFileSync } = await import('node:child_process');
|
|
53
|
-
|
|
54
|
-
// Spawn daemon process
|
|
55
|
-
const serverPath = join(__dirname, 'server.js');
|
|
56
|
-
const child = spawn(process.execPath, [serverPath, '--http', '--port', String(DAEMON_PORT)], {
|
|
57
|
-
detached: true,
|
|
58
|
-
stdio: 'ignore',
|
|
59
|
-
env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning' },
|
|
60
|
-
});
|
|
61
|
-
child.unref();
|
|
62
|
-
|
|
63
|
-
// Wait for daemon to be healthy
|
|
64
|
-
const deadline = Date.now() + 5000;
|
|
65
|
-
let healthy = false;
|
|
66
|
-
while (Date.now() < deadline) {
|
|
67
|
-
try {
|
|
68
|
-
const res = await fetch(`http://localhost:${DAEMON_PORT}/health`);
|
|
69
|
-
if (res.ok) { healthy = true; break; }
|
|
70
|
-
} catch {}
|
|
71
|
-
await new Promise(r => setTimeout(r, 200));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (!healthy) {
|
|
75
|
-
console.error('[context-vault] Auto-daemon failed to start, continuing in stdio mode');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Reconfigure Claude Code to use HTTP transport
|
|
80
|
-
const env = { ...process.env };
|
|
81
|
-
delete (env as Record<string, string | undefined>).CLAUDECODE;
|
|
82
|
-
try {
|
|
83
|
-
execFileSync('claude', ['mcp', 'remove', 'context-vault', '-s', 'user'], { stdio: 'pipe', env });
|
|
84
|
-
} catch {}
|
|
85
|
-
try {
|
|
86
|
-
execFileSync('claude', [
|
|
87
|
-
'mcp', 'add', '-s', 'user', '--transport', 'http',
|
|
88
|
-
'context-vault', `http://localhost:${DAEMON_PORT}/mcp`,
|
|
89
|
-
], { stdio: 'pipe', env });
|
|
90
|
-
console.error(`[context-vault] Daemon started on port ${DAEMON_PORT}. New sessions will use shared HTTP mode.`);
|
|
91
|
-
} catch {
|
|
92
|
-
console.error('[context-vault] Daemon started but could not reconfigure Claude Code');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function selfCheck(port: number): Promise<void> {
|
|
97
|
-
const { execFileSync } = await import('node:child_process');
|
|
98
|
-
const env = { ...process.env };
|
|
99
|
-
delete (env as Record<string, string | undefined>).CLAUDECODE;
|
|
100
|
-
|
|
101
|
-
// 1. Validate LaunchAgent plist on macOS (correct node/server paths)
|
|
102
|
-
if (process.platform === 'darwin') {
|
|
103
|
-
const plistPath = join(homedir(), 'Library', 'LaunchAgents', 'com.context-vault.daemon.plist');
|
|
104
|
-
if (existsSync(plistPath)) {
|
|
105
|
-
try {
|
|
106
|
-
const plist = readFileSync(plistPath, 'utf-8');
|
|
107
|
-
const currentNode = process.execPath;
|
|
108
|
-
const currentServer = join(__dirname, 'server.js');
|
|
109
|
-
if (!plist.includes(currentNode) || !plist.includes(currentServer)) {
|
|
110
|
-
console.error('[context-vault] Self-heal: LaunchAgent has stale paths, rewriting...');
|
|
111
|
-
const vaultDirIdx = process.argv.indexOf('--vault-dir');
|
|
112
|
-
const vaultDir = vaultDirIdx !== -1 ? process.argv[vaultDirIdx + 1] : undefined;
|
|
113
|
-
const progArgs = [currentNode, currentServer, '--http', '--port', String(port)];
|
|
114
|
-
if (vaultDir) progArgs.push('--vault-dir', vaultDir);
|
|
115
|
-
const logPath = join(homedir(), '.context-mcp', 'daemon.log');
|
|
116
|
-
const newPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
117
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
118
|
-
<plist version="1.0">
|
|
119
|
-
<dict>
|
|
120
|
-
<key>Label</key>
|
|
121
|
-
<string>com.context-vault.daemon</string>
|
|
122
|
-
<key>ProgramArguments</key>
|
|
123
|
-
<array>
|
|
124
|
-
${progArgs.map(a => ` <string>${a}</string>`).join('\n')}
|
|
125
|
-
</array>
|
|
126
|
-
<key>RunAtLoad</key>
|
|
127
|
-
<true/>
|
|
128
|
-
<key>KeepAlive</key>
|
|
129
|
-
<dict>
|
|
130
|
-
<key>SuccessfulExit</key>
|
|
131
|
-
<false/>
|
|
132
|
-
</dict>
|
|
133
|
-
<key>StandardErrorPath</key>
|
|
134
|
-
<string>${logPath}</string>
|
|
135
|
-
<key>StandardOutPath</key>
|
|
136
|
-
<string>/dev/null</string>
|
|
137
|
-
<key>EnvironmentVariables</key>
|
|
138
|
-
<dict>
|
|
139
|
-
<key>NODE_OPTIONS</key>
|
|
140
|
-
<string>--no-warnings=ExperimentalWarning</string>
|
|
141
|
-
<key>CONTEXT_VAULT_NO_DAEMON</key>
|
|
142
|
-
<string>1</string>
|
|
143
|
-
</dict>
|
|
144
|
-
<key>ThrottleInterval</key>
|
|
145
|
-
<integer>5</integer>
|
|
146
|
-
</dict>
|
|
147
|
-
</plist>`;
|
|
148
|
-
writeFileSync(plistPath, newPlist);
|
|
149
|
-
// Reload the agent so launchd picks up new paths
|
|
150
|
-
try { execFileSync('launchctl', ['unload', plistPath], { stdio: 'pipe' }); } catch {}
|
|
151
|
-
try { execFileSync('launchctl', ['load', '-w', plistPath], { stdio: 'pipe' }); } catch {}
|
|
152
|
-
console.error('[context-vault] Self-heal: LaunchAgent updated with current paths');
|
|
153
|
-
}
|
|
154
|
-
} catch (e) {
|
|
155
|
-
console.error(`[context-vault] LaunchAgent check failed: ${(e as Error).message}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 2. Validate Claude Code MCP config points to this daemon
|
|
161
|
-
try {
|
|
162
|
-
const result = execFileSync('claude', ['mcp', 'list'], {
|
|
163
|
-
encoding: 'utf-8',
|
|
164
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
165
|
-
env,
|
|
166
|
-
timeout: 5000,
|
|
167
|
-
});
|
|
168
|
-
if (result.includes('context-vault') && !result.includes(`localhost:${port}`)) {
|
|
169
|
-
console.error('[context-vault] Self-heal: Claude Code not pointing to this daemon, reconfiguring...');
|
|
170
|
-
try { execFileSync('claude', ['mcp', 'remove', 'context-vault', '-s', 'user'], { stdio: 'pipe', env }); } catch {}
|
|
171
|
-
execFileSync('claude', [
|
|
172
|
-
'mcp', 'add', '-s', 'user', '--transport', 'http',
|
|
173
|
-
'context-vault', `http://localhost:${port}/mcp`,
|
|
174
|
-
], { stdio: 'pipe', env });
|
|
175
|
-
console.error('[context-vault] Self-heal: Claude Code reconfigured');
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// claude CLI not available or check failed, skip
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Auto-update: check npm for a newer version. In daemon (HTTP) mode,
|
|
184
|
-
* install the update and gracefully restart. In stdio mode, just log.
|
|
185
|
-
*/
|
|
186
|
-
async function autoUpdate(isDaemon: boolean): Promise<string | null> {
|
|
187
|
-
const { execSync, spawn: spawnProc } = await import('node:child_process');
|
|
188
|
-
// Use a non-blocking npm check to avoid event loop stalls during reindex
|
|
189
|
-
const latest = await new Promise<string>((resolve, reject) => {
|
|
190
|
-
const child = spawnProc('npm', ['view', 'context-vault', 'version'], {
|
|
191
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
192
|
-
timeout: 10000,
|
|
193
|
-
});
|
|
194
|
-
let out = '';
|
|
195
|
-
child.stdout?.on('data', (d: Buffer) => { out += d.toString(); });
|
|
196
|
-
child.on('close', (code: number | null) => {
|
|
197
|
-
if (code === 0 && out.trim()) resolve(out.trim());
|
|
198
|
-
else reject(new Error(`npm view failed (code ${code})`));
|
|
199
|
-
});
|
|
200
|
-
child.on('error', reject);
|
|
201
|
-
}).catch(() => null as string | null) as string;
|
|
202
|
-
|
|
203
|
-
if (!latest) return null; // offline or registry unreachable
|
|
204
|
-
if (latest === pkg.version) return latest;
|
|
205
|
-
|
|
206
|
-
console.error(`[context-vault] Update available: v${pkg.version} -> v${latest}`);
|
|
207
|
-
|
|
208
|
-
if (!isDaemon) {
|
|
209
|
-
console.error('[context-vault] Run: context-vault update');
|
|
210
|
-
return latest;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Daemon mode: auto-install and restart
|
|
214
|
-
console.error(`[context-vault] Auto-updating to v${latest}...`);
|
|
215
|
-
try {
|
|
216
|
-
execSync('npm install -g context-vault@latest', {
|
|
217
|
-
encoding: 'utf-8',
|
|
218
|
-
timeout: 120000,
|
|
219
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
220
|
-
});
|
|
221
|
-
console.error(`[context-vault] Installed v${latest}. Restarting daemon...`);
|
|
222
|
-
|
|
223
|
-
// Find our own server.js path in the updated install
|
|
224
|
-
const newBin = execSync('which context-vault', {
|
|
225
|
-
encoding: 'utf-8',
|
|
226
|
-
timeout: 5000,
|
|
227
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
228
|
-
}).trim();
|
|
229
|
-
|
|
230
|
-
// Resolve the actual package root from the binary
|
|
231
|
-
const { readlinkSync } = await import('node:fs');
|
|
232
|
-
const { resolve: resolvePath } = await import('node:path');
|
|
233
|
-
let binTarget = newBin;
|
|
234
|
-
try { binTarget = readlinkSync(newBin); } catch {}
|
|
235
|
-
const newPkgRoot = resolvePath(dirname(binTarget), '..');
|
|
236
|
-
const newServerJs = join(newPkgRoot, 'dist', 'server.js');
|
|
237
|
-
|
|
238
|
-
// Spawn the new version as a replacement daemon
|
|
239
|
-
const portIdx = process.argv.indexOf('--port');
|
|
240
|
-
const port = portIdx !== -1 ? process.argv[portIdx + 1] : '3377';
|
|
241
|
-
const args = [newServerJs, '--http', '--port', port];
|
|
242
|
-
|
|
243
|
-
// Pass through vault-dir if specified
|
|
244
|
-
const vaultIdx = process.argv.indexOf('--vault-dir');
|
|
245
|
-
if (vaultIdx !== -1 && process.argv[vaultIdx + 1]) {
|
|
246
|
-
args.push('--vault-dir', process.argv[vaultIdx + 1]);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const { spawn } = await import('node:child_process');
|
|
250
|
-
const child = spawn(process.execPath, args, {
|
|
251
|
-
detached: true,
|
|
252
|
-
stdio: 'ignore',
|
|
253
|
-
env: { ...process.env, NODE_OPTIONS: '--no-warnings=ExperimentalWarning', CONTEXT_VAULT_NO_DAEMON: '1' },
|
|
254
|
-
});
|
|
255
|
-
child.unref();
|
|
256
|
-
|
|
257
|
-
// Give the new process a moment to bind the port, then exit
|
|
258
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
259
|
-
console.error(`[context-vault] New daemon spawned (PID ${child.pid}). Old daemon exiting.`);
|
|
260
|
-
process.exit(0);
|
|
261
|
-
} catch (e) {
|
|
262
|
-
console.error(`[context-vault] Auto-update failed: ${(e as Error).message}`);
|
|
263
|
-
console.error('[context-vault] Run manually: context-vault update');
|
|
264
|
-
}
|
|
265
|
-
return latest;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
33
|
async function main(): Promise<void> {
|
|
269
34
|
let phase = 'CONFIG';
|
|
270
35
|
let db: import('node:sqlite').DatabaseSync | undefined;
|
|
@@ -375,6 +140,24 @@ async function main(): Promise<void> {
|
|
|
375
140
|
);
|
|
376
141
|
}
|
|
377
142
|
|
|
143
|
+
if (config.watch?.enabled === true && config.vaultDirExists) {
|
|
144
|
+
try {
|
|
145
|
+
const vaultWatcher = startWatcher(ctx, {
|
|
146
|
+
vaultDir: config.watch?.path || config.vaultDir,
|
|
147
|
+
debounceMs: config.watch?.debounceMs ?? 500,
|
|
148
|
+
indexingConfig: config.indexing,
|
|
149
|
+
dataDir: config.dataDir,
|
|
150
|
+
onError: (err) => console.error(`[context-vault] Watcher: ${err.message}`),
|
|
151
|
+
});
|
|
152
|
+
// Expose markSelfWrite on ctx so save_context can suppress re-indexing
|
|
153
|
+
(ctx as any).markSelfWrite = vaultWatcher.markSelfWrite;
|
|
154
|
+
process.on('exit', () => vaultWatcher.close());
|
|
155
|
+
console.error('[context-vault] Filesystem watcher active (opt-in via config)');
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.error(`[context-vault] Watcher skipped: ${(err as Error).message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
378
161
|
phase = 'SERVER';
|
|
379
162
|
|
|
380
163
|
const CONFIG_CACHE_TTL_MS = 30_000;
|
|
@@ -407,15 +190,13 @@ async function main(): Promise<void> {
|
|
|
407
190
|
return s;
|
|
408
191
|
}
|
|
409
192
|
|
|
410
|
-
const server = createServer();
|
|
411
|
-
|
|
412
193
|
function closeDb(): void {
|
|
413
194
|
try {
|
|
414
195
|
if ((db as any).inTransaction) {
|
|
415
196
|
console.error('[context-vault] Rolling back active transaction...');
|
|
416
197
|
db!.exec('ROLLBACK');
|
|
417
198
|
}
|
|
418
|
-
|
|
199
|
+
db!.exec('PRAGMA wal_checkpoint(TRUNCATE)');
|
|
419
200
|
db!.close();
|
|
420
201
|
console.error('[context-vault] Database closed cleanly.');
|
|
421
202
|
} catch (shutdownErr) {
|
|
@@ -426,7 +207,6 @@ async function main(): Promise<void> {
|
|
|
426
207
|
|
|
427
208
|
function shutdown(signal: string): void {
|
|
428
209
|
console.error(`[context-vault] Received ${signal}, shutting down...`);
|
|
429
|
-
try { unlinkSync(join(homedir(), '.context-mcp', 'daemon.pid')); } catch {}
|
|
430
210
|
|
|
431
211
|
if (ctx.activeOps.count > 0) {
|
|
432
212
|
console.error(
|
|
@@ -462,196 +242,16 @@ async function main(): Promise<void> {
|
|
|
462
242
|
const capMb = Math.round(MAX_RSS_BYTES / 1024 / 1024);
|
|
463
243
|
console.error(`[context-vault] WATCHDOG: RSS ${rssMb}MB exceeds ${capMb}MB limit. Shutting down to protect system resources.`);
|
|
464
244
|
console.error(`[context-vault] Adjust limit with CONTEXT_VAULT_MAX_RSS_MB env var, or run 'context-vault reindex' manually.`);
|
|
465
|
-
try { unlinkSync(PID_PATH); } catch {}
|
|
466
245
|
process.exit(137);
|
|
467
246
|
}
|
|
468
247
|
}, 5_000);
|
|
469
248
|
rssWatchdog.unref();
|
|
470
249
|
|
|
471
250
|
phase = 'CONNECTED';
|
|
472
|
-
let latestKnownVersion: string | null = null;
|
|
473
|
-
|
|
474
|
-
const useHttp = process.argv.includes('--http');
|
|
475
|
-
|
|
476
|
-
if (useHttp) {
|
|
477
|
-
const portIdx = process.argv.indexOf('--port');
|
|
478
|
-
const port = portIdx !== -1 && process.argv[portIdx + 1]
|
|
479
|
-
? parseInt(process.argv[portIdx + 1], 10)
|
|
480
|
-
: 3377;
|
|
481
|
-
|
|
482
|
-
const app = createMcpExpressApp();
|
|
483
|
-
const transports: Record<string, StreamableHTTPServerTransport> = {};
|
|
484
|
-
|
|
485
|
-
app.get('/health', (_req: IncomingMessage, res: ServerResponse) => {
|
|
486
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
487
|
-
res.end(JSON.stringify({
|
|
488
|
-
ok: true,
|
|
489
|
-
version: pkg.version,
|
|
490
|
-
latestVersion: latestKnownVersion,
|
|
491
|
-
updateAvailable: latestKnownVersion ? latestKnownVersion !== pkg.version : null,
|
|
492
|
-
pid: process.pid,
|
|
493
|
-
uptime: process.uptime(),
|
|
494
|
-
sessions: Object.keys(transports).length,
|
|
495
|
-
}));
|
|
496
|
-
});
|
|
497
|
-
|
|
498
|
-
function createTransport(): StreamableHTTPServerTransport {
|
|
499
|
-
const transport = new StreamableHTTPServerTransport({
|
|
500
|
-
sessionIdGenerator: () => randomUUID(),
|
|
501
|
-
onsessioninitialized: (sid: string) => {
|
|
502
|
-
transports[sid] = transport;
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
transport.onclose = () => {
|
|
506
|
-
const sid = transport.sessionId;
|
|
507
|
-
if (sid && transports[sid]) {
|
|
508
|
-
delete transports[sid];
|
|
509
|
-
}
|
|
510
|
-
};
|
|
511
|
-
return transport;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
app.post('/mcp', async (req: IncomingMessage & { body?: unknown }, res: ServerResponse) => {
|
|
515
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
516
|
-
try {
|
|
517
|
-
let transport: StreamableHTTPServerTransport;
|
|
518
|
-
if (sessionId && transports[sessionId]) {
|
|
519
|
-
transport = transports[sessionId];
|
|
520
|
-
} else if (isInitializeRequest((req as any).body)) {
|
|
521
|
-
// Allow (re-)initialization with or without a stale session ID.
|
|
522
|
-
// Covers: first connect, reconnect after daemon restart.
|
|
523
|
-
transport = createTransport();
|
|
524
|
-
const sessionServer = createServer();
|
|
525
|
-
await sessionServer.connect(transport);
|
|
526
|
-
await transport.handleRequest(req, res, (req as any).body);
|
|
527
|
-
return;
|
|
528
|
-
} else if (sessionId) {
|
|
529
|
-
// Stale session (e.g., daemon restarted). Claude Code's MCP client
|
|
530
|
-
// does not auto-reinitialize on 404, so we recover transparently:
|
|
531
|
-
// create a new transport reusing the stale session ID, force it into
|
|
532
|
-
// initialized state, and handle the request as if nothing happened.
|
|
533
|
-
console.error(`[context-vault] Recovering stale session ${sessionId.slice(0, 8)}...`);
|
|
534
|
-
|
|
535
|
-
transport = new StreamableHTTPServerTransport({
|
|
536
|
-
sessionIdGenerator: () => sessionId,
|
|
537
|
-
onsessioninitialized: (sid: string) => {
|
|
538
|
-
transports[sid] = transport;
|
|
539
|
-
},
|
|
540
|
-
});
|
|
541
|
-
transport.onclose = () => {
|
|
542
|
-
if (transports[sessionId]) delete transports[sessionId];
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
const sessionServer = createServer();
|
|
546
|
-
await sessionServer.connect(transport);
|
|
547
|
-
|
|
548
|
-
// Force transport into initialized state, bypassing the initialize
|
|
549
|
-
// handshake. The inner WebStandardStreamableHTTPServerTransport holds
|
|
550
|
-
// the _initialized flag and sessionId.
|
|
551
|
-
const inner = (transport as any)._webStandardTransport;
|
|
552
|
-
inner._initialized = true;
|
|
553
|
-
inner.sessionId = sessionId;
|
|
554
|
-
transports[sessionId] = transport;
|
|
555
|
-
|
|
556
|
-
// Fall through to handleRequest below
|
|
557
|
-
} else {
|
|
558
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
559
|
-
res.end(JSON.stringify({
|
|
560
|
-
jsonrpc: '2.0',
|
|
561
|
-
error: { code: -32000, message: 'Bad Request: No valid session ID' },
|
|
562
|
-
id: null,
|
|
563
|
-
}));
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
await transport.handleRequest(req, res, (req as any).body);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
console.error('[context-vault] HTTP error:', error);
|
|
569
|
-
if (!res.headersSent) {
|
|
570
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
571
|
-
res.end(JSON.stringify({
|
|
572
|
-
jsonrpc: '2.0',
|
|
573
|
-
error: { code: -32603, message: 'Internal server error' },
|
|
574
|
-
id: null,
|
|
575
|
-
}));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
app.get('/mcp', async (req: IncomingMessage, res: ServerResponse) => {
|
|
581
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
582
|
-
if (!sessionId || !transports[sessionId]) {
|
|
583
|
-
res.writeHead(404);
|
|
584
|
-
res.end('Session not found');
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
await transports[sessionId].handleRequest(req, res);
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
app.delete('/mcp', async (req: IncomingMessage, res: ServerResponse) => {
|
|
591
|
-
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
592
|
-
if (!sessionId || !transports[sessionId]) {
|
|
593
|
-
res.writeHead(404);
|
|
594
|
-
res.end('Session not found');
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
await transports[sessionId].handleRequest(req, res);
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
app.listen(port, () => {
|
|
601
|
-
console.error(`[context-vault] Serving on http://localhost:${port}/mcp`);
|
|
602
|
-
const pidDir = join(homedir(), '.context-mcp');
|
|
603
|
-
mkdirSync(pidDir, { recursive: true });
|
|
604
|
-
writeFileSync(join(pidDir, 'daemon.pid'), JSON.stringify({ pid: process.pid, port }));
|
|
605
|
-
|
|
606
|
-
// Self-healing: validate and repair infrastructure on startup
|
|
607
|
-
selfCheck(port).catch(() => {});
|
|
608
|
-
|
|
609
|
-
// Periodic health monitor: validate DB, vault, and PID file every 5 minutes
|
|
610
|
-
setInterval(() => {
|
|
611
|
-
try {
|
|
612
|
-
// Verify DB is accessible
|
|
613
|
-
ctx.db.exec('SELECT 1');
|
|
614
|
-
// Verify PID file is correct
|
|
615
|
-
const pidData = existsSync(PID_PATH)
|
|
616
|
-
? JSON.parse(readFileSync(PID_PATH, 'utf-8'))
|
|
617
|
-
: null;
|
|
618
|
-
if (!pidData || pidData.pid !== process.pid || pidData.port !== port) {
|
|
619
|
-
writeFileSync(PID_PATH, JSON.stringify({ pid: process.pid, port }));
|
|
620
|
-
console.error('[context-vault] Self-heal: repaired stale PID file');
|
|
621
|
-
}
|
|
622
|
-
// Verify vault directory
|
|
623
|
-
if (!existsSync(ctx.config.vaultDir)) {
|
|
624
|
-
console.error(`[context-vault] Warning: vault directory missing: ${ctx.config.vaultDir}`);
|
|
625
|
-
}
|
|
626
|
-
} catch (e) {
|
|
627
|
-
console.error(`[context-vault] Health check failed: ${(e as Error).message}`);
|
|
628
|
-
}
|
|
629
|
-
}, 5 * 60 * 1000);
|
|
630
|
-
});
|
|
631
|
-
} else {
|
|
632
|
-
const transport = new StdioServerTransport();
|
|
633
|
-
await server.connect(transport);
|
|
634
|
-
|
|
635
|
-
// Auto-daemonize: if no daemon is running, spawn one in the background
|
|
636
|
-
// and reconfigure Claude Code to use HTTP. Next session onwards, all
|
|
637
|
-
// sessions share the single daemon process. This session stays on stdio.
|
|
638
|
-
if (!process.env.CONTEXT_VAULT_NO_DAEMON) {
|
|
639
|
-
setTimeout(() => tryAutoDaemon().catch(() => {}), 2000);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
251
|
|
|
643
|
-
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
if (result) latestKnownVersion = result;
|
|
647
|
-
};
|
|
648
|
-
setTimeout(() => updateCheck().catch((e) => {
|
|
649
|
-
console.error(`[context-vault] Update check failed: ${(e as Error).message}`);
|
|
650
|
-
}), 5000);
|
|
651
|
-
// Re-check daily for long-running daemons
|
|
652
|
-
if (useHttp) {
|
|
653
|
-
setInterval(() => updateCheck().catch(() => {}), 24 * 60 * 60 * 1000);
|
|
654
|
-
}
|
|
252
|
+
const server = createServer();
|
|
253
|
+
const transport = new StdioServerTransport();
|
|
254
|
+
await server.connect(transport);
|
|
655
255
|
} catch (rawErr) {
|
|
656
256
|
const err = rawErr as Error;
|
|
657
257
|
const dataDir = config?.dataDir || join(homedir(), '.context-mcp');
|
package/src/status.ts
CHANGED
|
@@ -162,6 +162,21 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
|
|
|
162
162
|
errors.push(`Indexing stats failed: ${(e as Error).message}`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
let ftsRowCount: number | null = null;
|
|
166
|
+
try {
|
|
167
|
+
ftsRowCount = (db.prepare('SELECT COUNT(*) as c FROM vault_fts').get() as { c: number } | undefined)?.c ?? 0;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
errors.push(`FTS row count failed: ${(e as Error).message}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let coRetrievalPairCount = 0;
|
|
173
|
+
try {
|
|
174
|
+
coRetrievalPairCount =
|
|
175
|
+
(db.prepare('SELECT COUNT(*) as c FROM co_retrievals').get() as { c: number } | undefined)?.c ?? 0;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
errors.push(`Co-retrieval count failed: ${(e as Error).message}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
165
180
|
let staleKnowledge: unknown[] = [];
|
|
166
181
|
try {
|
|
167
182
|
const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
|
|
@@ -219,6 +234,8 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
|
|
|
219
234
|
archivedCount,
|
|
220
235
|
staleKnowledge,
|
|
221
236
|
indexingStats,
|
|
237
|
+
ftsRowCount,
|
|
238
|
+
coRetrievalPairCount,
|
|
222
239
|
recallStats,
|
|
223
240
|
resolvedFrom: config.resolvedFrom,
|
|
224
241
|
errors,
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { gatherVaultStatus, computeGrowthWarnings } from '../status.js';
|
|
4
4
|
import { gatherRecallSummary } from '../stats/recall.js';
|
|
5
|
-
import { errorLogPath, errorLogCount } from '../error-log.js';
|
|
5
|
+
import { errorLogPath, errorLogCount, embedRelatedLogTail } from '../error-log.js';
|
|
6
6
|
import { getAutoMemory } from '../auto-memory.js';
|
|
7
7
|
import { ok, err, kindIcon } from '../helpers.js';
|
|
8
8
|
import type { LocalCtx, ToolResult } from '../types.js';
|
|
@@ -104,6 +104,12 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
|
|
|
104
104
|
const pct = ix.total > 0 ? Math.round((ix.indexed / ix.total) * 100) : 100;
|
|
105
105
|
lines.push(`| **Indexed entries** | ${ix.indexed}/${ix.total} (${pct}%) |`);
|
|
106
106
|
}
|
|
107
|
+
if (status.ftsRowCount != null) {
|
|
108
|
+
lines.push(`| **Search index rows** | ${status.ftsRowCount.toLocaleString()} |`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(
|
|
111
|
+
`| **Related entry pairs** | ${(status.coRetrievalPairCount ?? 0).toLocaleString()} |`
|
|
112
|
+
);
|
|
107
113
|
|
|
108
114
|
// Indexed kinds as compact table
|
|
109
115
|
lines.push(``, `### Entries by Kind`);
|
|
@@ -219,6 +225,29 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
|
|
|
219
225
|
lines.push(`Use save_context to refresh or add expires_at to retire stale entries.`);
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
const embedLogHints = embedRelatedLogTail(config.dataDir);
|
|
229
|
+
if (embedLogHints.length > 0) {
|
|
230
|
+
lines.push(``, `### Embedding hints (error.log tail)`);
|
|
231
|
+
lines.push(`_Lines mentioning embeddings — see \`docs/large-vaults.md\` for triage._`);
|
|
232
|
+
for (const ln of embedLogHints) {
|
|
233
|
+
lines.push(`- ${ln.length > 240 ? ln.slice(0, 237) + '...' : ln}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const healthSnapshot = {
|
|
238
|
+
total_entries: status.embeddingStatus?.total ?? null,
|
|
239
|
+
searchable: status.embeddingStatus?.indexed ?? null,
|
|
240
|
+
pending_indexing: status.embeddingStatus?.missing ?? null,
|
|
241
|
+
search_index_rows: status.ftsRowCount ?? null,
|
|
242
|
+
related_pairs: status.coRetrievalPairCount ?? 0,
|
|
243
|
+
database_size_bytes: status.dbSizeBytes ?? null,
|
|
244
|
+
files_on_disk: status.fileCount,
|
|
245
|
+
};
|
|
246
|
+
lines.push(``, `### Vault Health Snapshot`);
|
|
247
|
+
lines.push('```json');
|
|
248
|
+
lines.push(JSON.stringify(healthSnapshot, null, 2));
|
|
249
|
+
lines.push('```');
|
|
250
|
+
|
|
222
251
|
// Error log
|
|
223
252
|
const logPath = errorLogPath(config.dataDir);
|
|
224
253
|
const logCount = errorLogCount(config.dataDir);
|
|
@@ -6,10 +6,10 @@ import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
|
|
|
6
6
|
export const name = 'delete_context';
|
|
7
7
|
|
|
8
8
|
export const description =
|
|
9
|
-
'Delete an entry from your vault by its
|
|
9
|
+
'Delete an entry from your vault by its ID. Removes the file from disk and cleans up the search index.';
|
|
10
10
|
|
|
11
11
|
export const inputSchema = {
|
|
12
|
-
id: z.string().describe('The entry
|
|
12
|
+
id: z.string().describe('The entry ID to delete'),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export async function handler(
|
|
@@ -27,12 +27,17 @@ export async function handler(
|
|
|
27
27
|
// Delete DB record first — if this fails, the file stays and no orphan is created
|
|
28
28
|
const rowidResult = ctx.stmts.getRowid.get(id);
|
|
29
29
|
if (rowidResult?.rowid) {
|
|
30
|
-
try {
|
|
31
|
-
|
|
32
|
-
} catch {}
|
|
30
|
+
try { ctx.deleteVec(Number(rowidResult.rowid)); } catch {}
|
|
31
|
+
try { ctx.deleteCtxVec(Number(rowidResult.rowid)); } catch {}
|
|
33
32
|
}
|
|
34
33
|
ctx.stmts.deleteEntry.run(id);
|
|
35
34
|
|
|
35
|
+
// Clean up access_log and co_retrievals references
|
|
36
|
+
try { ctx.db.prepare(`DELETE FROM access_log WHERE entry_id = ?`).run(id); } catch {}
|
|
37
|
+
try {
|
|
38
|
+
ctx.db.prepare(`DELETE FROM co_retrievals WHERE entry_a = ? OR entry_b = ?`).run(id, id);
|
|
39
|
+
} catch {}
|
|
40
|
+
|
|
36
41
|
// Delete file from disk after successful DB delete
|
|
37
42
|
let fileWarning = null;
|
|
38
43
|
if (entry.file_path) {
|
package/src/tools/get-context.ts
CHANGED
|
@@ -324,7 +324,7 @@ export const inputSchema = {
|
|
|
324
324
|
.number()
|
|
325
325
|
.optional()
|
|
326
326
|
.describe(
|
|
327
|
-
'
|
|
327
|
+
'Number of top results to return with full body text. Lower-ranked results are returned as summaries (title + tags + first ~100 chars). Default: 2. Set to 0 to summarize all results, or a high number to return all with full body.'
|
|
328
328
|
),
|
|
329
329
|
include_ephemeral: z
|
|
330
330
|
.boolean()
|
|
@@ -503,6 +503,7 @@ export async function handler(
|
|
|
503
503
|
includeSuperseeded: include_superseded ?? false,
|
|
504
504
|
includeEphemeral: include_ephemeral ?? false,
|
|
505
505
|
contextEmbedding,
|
|
506
|
+
trackMeta: { query, sessionGoal: typeof context === 'string' ? context : context ? JSON.stringify(context) : undefined },
|
|
506
507
|
});
|
|
507
508
|
|
|
508
509
|
// Post-filter by tags if provided, then apply requested limit
|
|
@@ -724,7 +725,7 @@ export async function handler(
|
|
|
724
725
|
const entryTags = r.tags ? JSON.parse(r.tags) : [];
|
|
725
726
|
const tagStr = entryTags.length ? entryTags.join(', ') : '';
|
|
726
727
|
const icon = kindIcon(r.kind);
|
|
727
|
-
const skeletonLabel = isSkeleton ? ' `
|
|
728
|
+
const skeletonLabel = isSkeleton ? ' `[summary]`' : '';
|
|
728
729
|
const tierLabel = r.tier ? `**${r.tier}**` : '';
|
|
729
730
|
const dateStr = r.updated_at && r.updated_at !== r.created_at
|
|
730
731
|
? `${fmtDate(r.created_at)} (upd ${fmtDate(r.updated_at)})`
|