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.
Files changed (94) hide show
  1. package/bin/cli.js +263 -414
  2. package/dist/error-log.d.ts +2 -0
  3. package/dist/error-log.d.ts.map +1 -1
  4. package/dist/error-log.js +31 -1
  5. package/dist/error-log.js.map +1 -1
  6. package/dist/register-tools.d.ts.map +1 -1
  7. package/dist/register-tools.js +4 -0
  8. package/dist/register-tools.js.map +1 -1
  9. package/dist/server.js +23 -426
  10. package/dist/server.js.map +1 -1
  11. package/dist/status.d.ts.map +1 -1
  12. package/dist/status.js +17 -0
  13. package/dist/status.js.map +1 -1
  14. package/dist/tools/context-status.d.ts.map +1 -1
  15. package/dist/tools/context-status.js +26 -1
  16. package/dist/tools/context-status.js.map +1 -1
  17. package/dist/tools/delete-context.d.ts +1 -1
  18. package/dist/tools/delete-context.d.ts.map +1 -1
  19. package/dist/tools/delete-context.js +15 -2
  20. package/dist/tools/delete-context.js.map +1 -1
  21. package/dist/tools/get-context.d.ts.map +1 -1
  22. package/dist/tools/get-context.js +3 -2
  23. package/dist/tools/get-context.js.map +1 -1
  24. package/dist/tools/list-context.d.ts +7 -15
  25. package/dist/tools/list-context.d.ts.map +1 -1
  26. package/dist/tools/list-context.js +570 -111
  27. package/dist/tools/list-context.js.map +1 -1
  28. package/dist/tools/publish-to-team.js +1 -1
  29. package/dist/tools/publish-to-team.js.map +1 -1
  30. package/dist/tools/save-context.js +2 -2
  31. package/dist/tools/save-context.js.map +1 -1
  32. package/dist/tools/session-start.d.ts +20 -7
  33. package/dist/tools/session-start.d.ts.map +1 -1
  34. package/dist/tools/session-start.js +406 -439
  35. package/dist/tools/session-start.js.map +1 -1
  36. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  37. package/node_modules/@context-vault/core/dist/capture.js +4 -0
  38. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  39. package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -1
  40. package/node_modules/@context-vault/core/dist/categories.js +8 -0
  41. package/node_modules/@context-vault/core/dist/categories.js.map +1 -1
  42. package/node_modules/@context-vault/core/dist/compact.d.ts +38 -0
  43. package/node_modules/@context-vault/core/dist/compact.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/compact.js +127 -0
  45. package/node_modules/@context-vault/core/dist/compact.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  47. package/node_modules/@context-vault/core/dist/config.js +12 -0
  48. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  49. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  50. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  51. package/node_modules/@context-vault/core/dist/db.js +40 -4
  52. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  53. package/node_modules/@context-vault/core/dist/main.d.ts +6 -2
  54. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  55. package/node_modules/@context-vault/core/dist/main.js +5 -1
  56. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  57. package/node_modules/@context-vault/core/dist/search.d.ts +13 -1
  58. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  59. package/node_modules/@context-vault/core/dist/search.js +50 -5
  60. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  61. package/node_modules/@context-vault/core/dist/tier-analysis.d.ts +36 -0
  62. package/node_modules/@context-vault/core/dist/tier-analysis.d.ts.map +1 -0
  63. package/node_modules/@context-vault/core/dist/tier-analysis.js +227 -0
  64. package/node_modules/@context-vault/core/dist/tier-analysis.js.map +1 -0
  65. package/node_modules/@context-vault/core/dist/types.d.ts +12 -0
  66. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  67. package/node_modules/@context-vault/core/dist/watch.d.ts +21 -0
  68. package/node_modules/@context-vault/core/dist/watch.d.ts.map +1 -0
  69. package/node_modules/@context-vault/core/dist/watch.js +230 -0
  70. package/node_modules/@context-vault/core/dist/watch.js.map +1 -0
  71. package/node_modules/@context-vault/core/package.json +13 -1
  72. package/node_modules/@context-vault/core/src/capture.ts +4 -0
  73. package/node_modules/@context-vault/core/src/categories.ts +8 -0
  74. package/node_modules/@context-vault/core/src/compact.ts +183 -0
  75. package/node_modules/@context-vault/core/src/config.ts +8 -0
  76. package/node_modules/@context-vault/core/src/db.ts +40 -4
  77. package/node_modules/@context-vault/core/src/main.ts +10 -0
  78. package/node_modules/@context-vault/core/src/search.ts +55 -4
  79. package/node_modules/@context-vault/core/src/tier-analysis.ts +299 -0
  80. package/node_modules/@context-vault/core/src/types.ts +10 -0
  81. package/node_modules/@context-vault/core/src/watch.ts +269 -0
  82. package/package.json +2 -2
  83. package/scripts/postinstall.js +26 -1
  84. package/src/error-log.ts +30 -0
  85. package/src/register-tools.ts +4 -0
  86. package/src/server.ts +23 -423
  87. package/src/status.ts +17 -0
  88. package/src/tools/context-status.ts +30 -1
  89. package/src/tools/delete-context.ts +10 -5
  90. package/src/tools/get-context.ts +3 -2
  91. package/src/tools/list-context.ts +620 -119
  92. package/src/tools/publish-to-team.ts +1 -1
  93. package/src/tools/save-context.ts +2 -2
  94. 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
- (db as any).pragma('wal_checkpoint(TRUNCATE)');
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
- // Auto-update check (and apply for daemon mode)
644
- const updateCheck = async () => {
645
- const result = await autoUpdate(useHttp);
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 ULID id. Removes the file from disk and cleans up the search index.';
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 ULID to delete'),
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
- ctx.deleteVec(Number(rowidResult.rowid));
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) {
@@ -324,7 +324,7 @@ export const inputSchema = {
324
324
  .number()
325
325
  .optional()
326
326
  .describe(
327
- 'Skeleton mode: top pivot_count entries by relevance are returned with full body. Remaining entries are returned as skeletons (title + tags + first ~100 chars of body). Default: 2. Set to 0 to skeleton all results, or a high number to disable.'
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 ? ' `skeleton`' : '';
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)})`