context-vault 2.4.1 → 2.4.2

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.
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * local-server.js — Local mode: serves app + vault API with no auth.
4
+ *
5
+ * Uses local SQLite vault. No authentication required.
6
+ * Usage: node local-server.js [--port 3141]
7
+ */
8
+
9
+ import { createServer } from "node:http";
10
+ import { createReadStream, existsSync, statSync, unlinkSync } from "node:fs";
11
+ import { join, resolve, dirname, extname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { homedir, platform } from "node:os";
14
+ import { execSync } from "node:child_process";
15
+ import { resolveConfig } from "@context-vault/core/core/config";
16
+ import { initDatabase, prepareStatements, insertVec, deleteVec } from "@context-vault/core/index/db";
17
+ import { embed } from "@context-vault/core/index/embed";
18
+ import { captureAndIndex, updateEntryFile } from "@context-vault/core/capture";
19
+ import { indexEntry } from "@context-vault/core/index";
20
+ import { hybridSearch } from "@context-vault/core/retrieve";
21
+ import { gatherVaultStatus } from "@context-vault/core/core/status";
22
+ import { normalizeKind } from "@context-vault/core/core/files";
23
+ import { categoryFor } from "@context-vault/core/core/categories";
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const LOCAL_ROOT = resolve(__dirname, "..");
27
+ const APP_DIST = resolve(LOCAL_ROOT, "..", "app", "dist");
28
+
29
+ const MIME = {
30
+ ".html": "text/html",
31
+ ".js": "application/javascript",
32
+ ".css": "text/css",
33
+ ".json": "application/json",
34
+ ".ico": "image/x-icon",
35
+ ".svg": "image/svg+xml",
36
+ };
37
+
38
+ function formatEntry(row) {
39
+ return {
40
+ id: row.id,
41
+ kind: row.kind,
42
+ category: row.category,
43
+ title: row.title || null,
44
+ body: row.body || null,
45
+ tags: row.tags ? JSON.parse(row.tags) : [],
46
+ meta: row.meta ? (typeof row.meta === "string" ? JSON.parse(row.meta) : row.meta) : {},
47
+ source: row.source || null,
48
+ identity_key: row.identity_key || null,
49
+ expires_at: row.expires_at || null,
50
+ created_at: row.created_at,
51
+ };
52
+ }
53
+
54
+ function validateEntry(data, { requireKind = true, requireBody = true } = {}) {
55
+ if (requireKind && !data.kind) return { error: "kind is required", status: 400 };
56
+ if (data.kind && !/^[a-z0-9-]+$/.test(data.kind)) return { error: "kind must be lowercase alphanumeric/hyphens", status: 400 };
57
+ if (requireBody && !data.body) return { error: "body is required", status: 400 };
58
+ if (data.body && data.body.length > 100 * 1024) return { error: "body max 100KB", status: 400 };
59
+ if (categoryFor(data.kind) === "entity" && !data.identity_key) return { error: `Entity kind "${data.kind}" requires identity_key`, status: 400 };
60
+ return null;
61
+ }
62
+
63
+ async function main() {
64
+ const portArg = process.argv.find((a) => a.startsWith("--port="));
65
+ const portVal = portArg ? portArg.split("=")[1] : process.argv[process.argv.indexOf("--port") + 1];
66
+ const port = parseInt(portVal || "3141", 10);
67
+
68
+ const config = resolveConfig();
69
+ config.vaultDirExists = existsSync(config.vaultDir);
70
+ let db = await initDatabase(config.dbPath);
71
+ let stmts = prepareStatements(db);
72
+
73
+ const state = {
74
+ db,
75
+ config,
76
+ stmts,
77
+ embed,
78
+ get ctx() {
79
+ return {
80
+ db: state.db,
81
+ config: state.config,
82
+ stmts: state.stmts,
83
+ embed: state.embed,
84
+ insertVec: (r, e) => insertVec(state.stmts, r, e),
85
+ deleteVec: (r) => deleteVec(state.stmts, r),
86
+ userId: null,
87
+ };
88
+ },
89
+ };
90
+
91
+ const server = createServer(async (req, res) => {
92
+ const url = req.url?.replace(/\?.*$/, "") || "/";
93
+
94
+ const json = (data, status = 200) => {
95
+ res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
96
+ res.end(JSON.stringify(data));
97
+ };
98
+
99
+ const readBody = () =>
100
+ new Promise((resolve) => {
101
+ let body = "";
102
+ req.on("data", (c) => (body += c));
103
+ req.on("end", () => {
104
+ try {
105
+ resolve(body ? JSON.parse(body) : null);
106
+ } catch {
107
+ resolve(null);
108
+ }
109
+ });
110
+ });
111
+
112
+ const ctx = state.ctx;
113
+
114
+ // ─── API: POST /api/local/browse — native folder picker dialog ────────────
115
+ if (url === "/api/local/browse" && req.method === "POST") {
116
+ const os = platform();
117
+ try {
118
+ let selected;
119
+ if (os === "darwin") {
120
+ selected = execSync(
121
+ `osascript -e 'POSIX path of (choose folder with prompt "Select vault folder")'`,
122
+ { encoding: "utf-8", timeout: 30000 }
123
+ ).trim();
124
+ } else if (os === "linux") {
125
+ selected = execSync(
126
+ `zenity --file-selection --directory --title="Select vault folder" 2>/dev/null`,
127
+ { encoding: "utf-8", timeout: 30000 }
128
+ ).trim();
129
+ } else {
130
+ return json({ error: "Folder picker not supported on this platform" }, 501);
131
+ }
132
+
133
+ if (selected) {
134
+ return json({ path: selected });
135
+ }
136
+ return json({ path: null, cancelled: true });
137
+ } catch {
138
+ // User cancelled the dialog or command failed
139
+ return json({ path: null, cancelled: true });
140
+ }
141
+ }
142
+
143
+ // ─── API: POST /api/local/connect — switch to a local vault folder ───────
144
+ if (url === "/api/local/connect" && req.method === "POST") {
145
+ const data = await readBody();
146
+ if (!data?.vaultDir?.trim()) return json({ error: "vaultDir is required", code: "INVALID_INPUT" }, 400);
147
+ let vaultPath = data.vaultDir.trim().replace(/^~/, homedir());
148
+ vaultPath = resolve(vaultPath);
149
+ if (!existsSync(vaultPath)) return json({ error: "Vault folder not found", code: "NOT_FOUND" }, 404);
150
+ if (!statSync(vaultPath).isDirectory()) return json({ error: "Path is not a directory", code: "INVALID_INPUT" }, 400);
151
+ try {
152
+ try { state.db.close(); } catch {}
153
+ const newConfig = { ...state.config, vaultDir: vaultPath, dbPath: join(vaultPath, ".context-vault.db"), vaultDirExists: true };
154
+ state.config = newConfig;
155
+ state.db = await initDatabase(newConfig.dbPath);
156
+ state.stmts = prepareStatements(state.db);
157
+ console.log(`[context-vault] Switched to vault: ${vaultPath}`);
158
+ return json({
159
+ userId: "local",
160
+ email: "local@localhost",
161
+ name: "Local",
162
+ tier: "free",
163
+ createdAt: new Date().toISOString(),
164
+ });
165
+ } catch (e) {
166
+ console.error(`[local-server] Connect error: ${e.message}`);
167
+ return json({ error: `Failed to connect: ${e.message}`, code: "CONNECT_FAILED" }, 500);
168
+ }
169
+ }
170
+
171
+ // ─── API: /api/me (local mode — no auth) ─────────────────────────────────
172
+ if (url === "/api/me" && req.method === "GET") {
173
+ return json({
174
+ userId: "local",
175
+ email: "local@localhost",
176
+ name: "Local",
177
+ tier: "free",
178
+ createdAt: new Date().toISOString(),
179
+ });
180
+ }
181
+
182
+ // ─── API: /api/billing/usage (local — unlimited) ─────────────────────────
183
+ if (url === "/api/billing/usage" && req.method === "GET") {
184
+ const status = gatherVaultStatus(ctx, {});
185
+ const total = status.kindCounts.reduce((s, k) => s + k.c, 0);
186
+ const storageMb = Math.round((status.dbSizeBytes / (1024 * 1024)) * 100) / 100;
187
+ return json({
188
+ tier: "free",
189
+ limits: { maxEntries: "unlimited", requestsPerDay: "unlimited", storageMb: 1024, exportEnabled: true },
190
+ usage: { requestsToday: 0, entriesUsed: total, storageMb },
191
+ });
192
+ }
193
+
194
+ // ─── API: /api/keys (local — empty) ──────────────────────────────────────
195
+ if (url === "/api/keys" && req.method === "GET") {
196
+ return json({ keys: [] });
197
+ }
198
+
199
+ // ─── API: /api/vault/status ─────────────────────────────────────────────
200
+ if (url === "/api/vault/status" && req.method === "GET") {
201
+ const status = gatherVaultStatus(ctx, {});
202
+ return json({
203
+ entries: {
204
+ total: status.kindCounts.reduce((s, k) => s + k.c, 0),
205
+ by_kind: Object.fromEntries(status.kindCounts.map((k) => [k.kind, k.c])),
206
+ by_category: Object.fromEntries(status.categoryCounts.map((k) => [k.category, k.c])),
207
+ },
208
+ files: { total: status.fileCount, directories: status.subdirs },
209
+ database: { size: status.dbSize, size_bytes: status.dbSizeBytes, stale_paths: status.staleCount, expired: status.expiredCount },
210
+ embeddings: status.embeddingStatus,
211
+ embed_model_available: status.embedModelAvailable,
212
+ health: status.errors.length === 0 && !status.stalePaths ? "ok" : "degraded",
213
+ errors: status.errors,
214
+ });
215
+ }
216
+
217
+ // ─── API: GET /api/vault/entries ────────────────────────────────────────
218
+ if (url.startsWith("/api/vault/entries") && req.method === "GET") {
219
+ const idMatch = url.match(/\/api\/vault\/entries\/([^/]+)$/);
220
+ if (idMatch) {
221
+ const entry = stmts.getEntryById.get(idMatch[1]);
222
+ if (!entry) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
223
+ return json(formatEntry(entry));
224
+ }
225
+ const u = new URL(req.url || "", "http://localhost");
226
+ const kind = u.searchParams.get("kind") || null;
227
+ const category = u.searchParams.get("category") || null;
228
+ const limit = Math.min(parseInt(u.searchParams.get("limit") || "20", 10) || 20, 100);
229
+ const offset = parseInt(u.searchParams.get("offset") || "0", 10) || 0;
230
+ const clauses = ["(expires_at IS NULL OR expires_at > datetime('now'))"];
231
+ const params = [];
232
+ if (kind) {
233
+ clauses.push("kind = ?");
234
+ params.push(normalizeKind(kind));
235
+ }
236
+ if (category) {
237
+ clauses.push("category = ?");
238
+ params.push(category);
239
+ }
240
+ const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
241
+ const total = ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...params).c;
242
+ const rows = ctx.db.prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
243
+ return json({ entries: rows.map(formatEntry), total, limit, offset });
244
+ }
245
+
246
+ // ─── API: POST /api/vault/entries ────────────────────────────────────────
247
+ if (url === "/api/vault/entries" && req.method === "POST") {
248
+ const data = await readBody();
249
+ if (!data) return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
250
+ const err = validateEntry(data);
251
+ if (err) return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
252
+ try {
253
+ const entry = await captureAndIndex(
254
+ ctx,
255
+ {
256
+ kind: data.kind,
257
+ title: data.title,
258
+ body: data.body,
259
+ meta: data.meta,
260
+ tags: data.tags,
261
+ source: data.source || "rest-api",
262
+ identity_key: data.identity_key,
263
+ expires_at: data.expires_at,
264
+ userId: null,
265
+ },
266
+ indexEntry
267
+ );
268
+ return json(formatEntry(stmts.getEntryById.get(entry.id)), 201);
269
+ } catch (e) {
270
+ console.error(`[local-server] Create error: ${e.message}`);
271
+ return json({ error: "Failed to create entry", code: "CREATE_FAILED" }, 500);
272
+ }
273
+ }
274
+
275
+ // ─── API: PUT /api/vault/entries/:id ─────────────────────────────────────
276
+ if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "PUT") {
277
+ const id = url.split("/").pop();
278
+ const data = await readBody();
279
+ if (!data) return json({ error: "Invalid JSON body", code: "INVALID_INPUT" }, 400);
280
+ const err = validateEntry(data, { requireKind: false, requireBody: false });
281
+ if (err) return json({ error: err.error, code: "INVALID_INPUT" }, err.status);
282
+ const existing = stmts.getEntryById.get(id);
283
+ if (!existing) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
284
+ try {
285
+ const entry = updateEntryFile(ctx, existing, {
286
+ title: data.title,
287
+ body: data.body,
288
+ tags: data.tags,
289
+ meta: data.meta,
290
+ source: data.source,
291
+ expires_at: data.expires_at,
292
+ });
293
+ await indexEntry(ctx, entry);
294
+ return json(formatEntry(stmts.getEntryById.get(id)));
295
+ } catch (e) {
296
+ console.error(`[local-server] Update error: ${e.message}`);
297
+ return json({ error: "Failed to update entry", code: "UPDATE_FAILED" }, 500);
298
+ }
299
+ }
300
+
301
+ // ─── API: DELETE /api/vault/entries/:id ──────────────────────────────────
302
+ if (url.match(/^\/api\/vault\/entries\/[^/]+$/) && req.method === "DELETE") {
303
+ const id = url.split("/").pop();
304
+ const entry = stmts.getEntryById.get(id);
305
+ if (!entry) return json({ error: "Entry not found", code: "NOT_FOUND" }, 404);
306
+ if (entry.file_path) {
307
+ try {
308
+ unlinkSync(entry.file_path);
309
+ } catch {}
310
+ }
311
+ const rowidResult = stmts.getRowid.get(id);
312
+ if (rowidResult?.rowid) {
313
+ try {
314
+ deleteVec(stmts, Number(rowidResult.rowid));
315
+ } catch {}
316
+ }
317
+ stmts.deleteEntry.run(id);
318
+ return json({ deleted: true, id, kind: entry.kind, title: entry.title || null });
319
+ }
320
+
321
+ // ─── API: POST /api/vault/search ─────────────────────────────────────────
322
+ if (url === "/api/vault/search" && req.method === "POST") {
323
+ const data = await readBody();
324
+ if (!data || !data.query?.trim()) return json({ error: "query is required", code: "INVALID_INPUT" }, 400);
325
+ const limit = Math.min(parseInt(data.limit || 20, 10) || 20, 100);
326
+ const offset = parseInt(data.offset || 0, 10) || 0;
327
+ try {
328
+ const results = await hybridSearch(ctx, data.query, {
329
+ kindFilter: data.kind ? normalizeKind(data.kind) : null,
330
+ categoryFilter: data.category || null,
331
+ limit,
332
+ offset,
333
+ decayDays: ctx.config.eventDecayDays || 30,
334
+ userIdFilter: undefined,
335
+ });
336
+ const formatted = results.map((row) => ({ ...formatEntry(row), score: Math.round(row.score * 1000) / 1000 }));
337
+ return json({ results: formatted, count: formatted.length, query: data.query });
338
+ } catch (e) {
339
+ console.error(`[local-server] Search error: ${e.message}`);
340
+ return json({ error: "Search failed", code: "SEARCH_FAILED" }, 500);
341
+ }
342
+ }
343
+
344
+ // ─── Static files ───────────────────────────────────────────────────────
345
+ const filePath = join(APP_DIST, url === "/" ? "index.html" : url.replace(/^\//, ""));
346
+ if (!filePath.startsWith(APP_DIST)) {
347
+ res.writeHead(403);
348
+ res.end();
349
+ return;
350
+ }
351
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
352
+ const fallback = join(APP_DIST, "index.html");
353
+ if (existsSync(fallback)) {
354
+ res.writeHead(200, { "Content-Type": "text/html" });
355
+ createReadStream(fallback).pipe(res);
356
+ return;
357
+ }
358
+ res.writeHead(404);
359
+ res.end();
360
+ return;
361
+ }
362
+ const type = MIME[extname(filePath)] || "application/octet-stream";
363
+ res.writeHead(200, { "Content-Type": type });
364
+ createReadStream(filePath).pipe(res);
365
+ });
366
+
367
+ server.listen(port, () => {
368
+ console.log(`[context-vault] Local mode: http://localhost:${port}`);
369
+ console.log(`[context-vault] Vault: ${config.vaultDir}`);
370
+ console.log(`[context-vault] No authentication required`);
371
+ });
372
+
373
+ process.on("SIGINT", () => {
374
+ try { state.db.close(); } catch {}
375
+ process.exit(0);
376
+ });
377
+ process.on("SIGTERM", () => {
378
+ try { state.db.close(); } catch {}
379
+ process.exit(0);
380
+ });
381
+ }
382
+
383
+ main().catch((e) => {
384
+ console.error(`[local-server] Fatal: ${e.message}`);
385
+ process.exit(1);
386
+ });
@@ -1,14 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * postinstall.js — Auto-rebuild native modules on install
4
+ * postinstall.js — Post-install setup for context-vault
5
5
  *
6
- * Detects NODE_MODULE_VERSION mismatches and attempts a rebuild.
6
+ * 1. Detects NODE_MODULE_VERSION mismatches for native modules and rebuilds.
7
+ * 2. Installs @huggingface/transformers with --ignore-scripts to avoid sharp's
8
+ * broken install lifecycle in global contexts. Semantic search degrades
9
+ * gracefully if this step fails.
7
10
  */
8
11
 
9
12
  import { execSync } from "node:child_process";
13
+ import { existsSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const PKG_ROOT = join(__dirname, "..");
19
+ const NODE_MODULES = join(PKG_ROOT, "node_modules");
10
20
 
11
21
  async function main() {
22
+ // ── 1. Native-module rebuild ──────────────────────────────────────────
12
23
  let needsRebuild = false;
13
24
 
14
25
  try {
@@ -40,6 +51,28 @@ async function main() {
40
51
  console.error("[context-vault] Try manually: npm rebuild better-sqlite3 sqlite-vec");
41
52
  }
42
53
  }
54
+
55
+ // ── 2. Install @huggingface/transformers (optional) ───────────────────
56
+ // The transformers package depends on `sharp`, whose install script fails
57
+ // in global npm contexts. We install with --ignore-scripts to skip it —
58
+ // context-vault only uses text embeddings, not image processing.
59
+ // Check the package's own node_modules (not general import resolution,
60
+ // which may find it in the workspace during `npm install -g ./tarball`).
61
+ const transformersDir = join(NODE_MODULES, "@huggingface", "transformers");
62
+ if (!existsSync(transformersDir)) {
63
+ console.log("[context-vault] Installing embedding support (@huggingface/transformers)...");
64
+ try {
65
+ execSync("npm install --no-save --ignore-scripts @huggingface/transformers@^3.0.0", {
66
+ stdio: "inherit",
67
+ timeout: 120000,
68
+ cwd: PKG_ROOT,
69
+ });
70
+ console.log("[context-vault] Embedding support installed.");
71
+ } catch {
72
+ console.error("[context-vault] Warning: could not install @huggingface/transformers.");
73
+ console.error("[context-vault] Semantic search will be unavailable; full-text search still works.");
74
+ }
75
+ }
43
76
  }
44
77
 
45
78
  main().catch(() => {});
@@ -7,20 +7,22 @@
7
7
  * Replaces the Unix shell script in package.json "prepack".
8
8
  */
9
9
 
10
- import { cpSync, rmSync, mkdirSync } from "node:fs";
10
+ import { cpSync, rmSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
  const LOCAL_ROOT = join(__dirname, "..");
16
+ const NODE_MODULES = join(LOCAL_ROOT, "node_modules");
16
17
  const CORE_SRC = join(LOCAL_ROOT, "..", "core");
17
- const CORE_DEST = join(LOCAL_ROOT, "node_modules", "@context-vault", "core");
18
+ const CORE_DEST = join(NODE_MODULES, "@context-vault", "core");
18
19
 
19
- // Ensure target directory exists
20
- mkdirSync(join(LOCAL_ROOT, "node_modules", "@context-vault"), { recursive: true });
20
+ // Clean node_modules to prevent workspace deps from leaking into the tarball.
21
+ // Only @context-vault/core should be bundled.
22
+ rmSync(NODE_MODULES, { recursive: true, force: true });
21
23
 
22
- // Remove old copy if present
23
- rmSync(CORE_DEST, { recursive: true, force: true });
24
+ // Ensure target directory exists
25
+ mkdirSync(join(NODE_MODULES, "@context-vault"), { recursive: true });
24
26
 
25
27
  // Copy core package (dereference symlinks)
26
28
  cpSync(CORE_SRC, CORE_DEST, { recursive: true, dereference: true });
@@ -28,4 +30,15 @@ cpSync(CORE_SRC, CORE_DEST, { recursive: true, dereference: true });
28
30
  // Remove nested node_modules from the copy
29
31
  rmSync(join(CORE_DEST, "node_modules"), { recursive: true, force: true });
30
32
 
33
+ // Strip all dependencies from the bundled core's package.json.
34
+ // Core's deps (better-sqlite3, sqlite-vec, MCP SDK) are hoisted to
35
+ // context-vault's own dependencies. @huggingface/transformers is
36
+ // dynamically imported and installed via postinstall. Leaving them
37
+ // in the bundled core causes duplicate resolution that breaks native
38
+ // module install scripts in global npm contexts.
39
+ const corePkgPath = join(CORE_DEST, "package.json");
40
+ const corePkg = JSON.parse(readFileSync(corePkgPath, "utf8"));
41
+ delete corePkg.dependencies;
42
+ writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) + "\n");
43
+
31
44
  console.log("[prepack] Bundled @context-vault/core into node_modules");
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from "node:fs";
6
6
  import { join, dirname } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
@@ -19,33 +19,46 @@ import { registerTools } from "@context-vault/core/server/tools";
19
19
  async function main() {
20
20
  let phase = "CONFIG";
21
21
  let db;
22
+ let config;
22
23
 
23
24
  try {
24
25
  // ── Phase: CONFIG ────────────────────────────────────────────────────────
25
- const config = resolveConfig();
26
+ config = resolveConfig();
26
27
 
27
28
  // ── Phase: DIRS ──────────────────────────────────────────────────────────
28
29
  phase = "DIRS";
29
30
  mkdirSync(config.dataDir, { recursive: true });
30
31
  mkdirSync(config.vaultDir, { recursive: true });
31
32
 
33
+ // Verify vault directory is writable (catch permission issues early)
34
+ try {
35
+ const probe = join(config.vaultDir, ".write-probe");
36
+ writeFileSync(probe, "");
37
+ unlinkSync(probe);
38
+ } catch (writeErr) {
39
+ console.error(`[context-vault] FATAL: Vault directory is not writable: ${config.vaultDir}`);
40
+ console.error(`[context-vault] ${writeErr.message}`);
41
+ console.error(`[context-vault] Fix permissions: chmod u+w "${config.vaultDir}"`);
42
+ process.exit(1);
43
+ }
44
+
32
45
  // Write .context-mcp marker (non-fatal)
33
46
  try {
34
47
  const markerPath = join(config.vaultDir, ".context-mcp");
35
48
  const markerData = existsSync(markerPath) ? JSON.parse(readFileSync(markerPath, "utf-8")) : {};
36
49
  writeFileSync(markerPath, JSON.stringify({ created: markerData.created || new Date().toISOString(), version: pkg.version }, null, 2) + "\n");
37
50
  } catch (markerErr) {
38
- console.error(`[context-mcp] Warning: could not write marker file: ${markerErr.message}`);
51
+ console.error(`[context-vault] Warning: could not write marker file: ${markerErr.message}`);
39
52
  }
40
53
 
41
54
  config.vaultDirExists = existsSync(config.vaultDir);
42
55
 
43
56
  // Startup diagnostics
44
- console.error(`[context-mcp] Vault: ${config.vaultDir}`);
45
- console.error(`[context-mcp] Database: ${config.dbPath}`);
46
- console.error(`[context-mcp] Dev dir: ${config.devDir}`);
57
+ console.error(`[context-vault] Vault: ${config.vaultDir}`);
58
+ console.error(`[context-vault] Database: ${config.dbPath}`);
59
+ console.error(`[context-vault] Dev dir: ${config.devDir}`);
47
60
  if (!config.vaultDirExists) {
48
- console.error(`[context-mcp] WARNING: Vault directory not found!`);
61
+ console.error(`[context-vault] WARNING: Vault directory not found!`);
49
62
  }
50
63
 
51
64
  // ── Phase: DB ────────────────────────────────────────────────────────────
@@ -60,33 +73,55 @@ async function main() {
60
73
  embed,
61
74
  insertVec: (rowid, embedding) => insertVec(stmts, rowid, embedding),
62
75
  deleteVec: (rowid) => deleteVec(stmts, rowid),
76
+ activeOps: { count: 0 },
63
77
  };
64
78
 
65
79
  // ── Phase: SERVER ────────────────────────────────────────────────────────
66
80
  phase = "SERVER";
67
81
  const server = new McpServer(
68
- { name: "context-mcp", version: pkg.version },
82
+ { name: "context-vault", version: pkg.version },
69
83
  { capabilities: { tools: {} } }
70
84
  );
71
85
 
72
86
  registerTools(server, ctx);
73
87
 
74
88
  // ── Graceful Shutdown ────────────────────────────────────────────────────
75
- function shutdown(signal) {
76
- console.error(`[context-mcp] Received ${signal}, shutting down...`);
89
+ function closeDb() {
77
90
  try {
78
91
  if (db.inTransaction) {
79
- console.error("[context-mcp] Rolling back active transaction...");
92
+ console.error("[context-vault] Rolling back active transaction...");
80
93
  db.exec("ROLLBACK");
81
94
  }
82
95
  db.pragma("wal_checkpoint(TRUNCATE)");
83
96
  db.close();
84
- console.error("[context-mcp] Database closed cleanly.");
97
+ console.error("[context-vault] Database closed cleanly.");
85
98
  } catch (shutdownErr) {
86
- console.error(`[context-mcp] Shutdown error: ${shutdownErr.message}`);
99
+ console.error(`[context-vault] Shutdown error: ${shutdownErr.message}`);
87
100
  }
88
101
  process.exit(0);
89
102
  }
103
+
104
+ function shutdown(signal) {
105
+ console.error(`[context-vault] Received ${signal}, shutting down...`);
106
+
107
+ if (ctx.activeOps.count > 0) {
108
+ console.error(`[context-vault] Waiting for ${ctx.activeOps.count} in-flight operation(s)...`);
109
+ const check = setInterval(() => {
110
+ if (ctx.activeOps.count === 0) {
111
+ clearInterval(check);
112
+ closeDb();
113
+ }
114
+ }, 100);
115
+ // Force shutdown after 5 seconds even if ops are still running
116
+ setTimeout(() => {
117
+ clearInterval(check);
118
+ console.error(`[context-vault] Force shutdown — ${ctx.activeOps.count} operation(s) still running`);
119
+ closeDb();
120
+ }, 5000);
121
+ } else {
122
+ closeDb();
123
+ }
124
+ }
90
125
  process.on("SIGINT", () => shutdown("SIGINT"));
91
126
  process.on("SIGTERM", () => shutdown("SIGTERM"));
92
127
 
@@ -105,7 +140,7 @@ async function main() {
105
140
  stdio: ["pipe", "pipe", "pipe"],
106
141
  }).trim();
107
142
  if (latest && latest !== pkg.version) {
108
- console.error(`[context-mcp] Update available: v${pkg.version} → v${latest}. Run: context-mcp update`);
143
+ console.error(`[context-vault] Update available: v${pkg.version} → v${latest}. Run: context-vault update`);
109
144
  }
110
145
  } catch {}
111
146
  }).catch(() => {});
@@ -116,7 +151,7 @@ async function main() {
116
151
  // Boxed diagnostic for native module mismatch
117
152
  console.error("");
118
153
  console.error("╔══════════════════════════════════════════════════════════════╗");
119
- console.error("║ context-mcp: Native Module Error ║");
154
+ console.error("║ context-vault: Native Module Error ║");
120
155
  console.error("╚══════════════════════════════════════════════════════════════╝");
121
156
  console.error("");
122
157
  console.error(err.message);
@@ -127,15 +162,15 @@ async function main() {
127
162
  process.exit(78); // EX_CONFIG
128
163
  }
129
164
 
130
- console.error(`[context-mcp] Fatal error during ${phase} phase: ${err.message}`);
165
+ console.error(`[context-vault] Fatal error during ${phase} phase: ${err.message}`);
131
166
  if (phase === "DB") {
132
- console.error(`[context-mcp] Try deleting the DB file and restarting: rm "${err.dbPath || "vault.db"}"`);
167
+ console.error(`[context-vault] Try deleting the DB file and restarting: rm "${config?.dbPath || "vault.db"}"`);
133
168
  }
134
169
  process.exit(1);
135
170
  }
136
171
  }
137
172
 
138
173
  main().catch((err) => {
139
- console.error(`[context-mcp] Unexpected fatal error: ${err.message}`);
174
+ console.error(`[context-vault] Unexpected fatal error: ${err.message}`);
140
175
  process.exit(1);
141
176
  });