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.
- package/README.md +438 -0
- package/bin/cli.js +365 -69
- package/node_modules/@context-vault/core/package.json +13 -11
- package/node_modules/@context-vault/core/src/core/config.js +8 -8
- package/node_modules/@context-vault/core/src/index/db.js +5 -9
- package/node_modules/@context-vault/core/src/index/embed.js +14 -10
- package/node_modules/@context-vault/core/src/server/tools.js +50 -19
- package/package.json +34 -7
- package/scripts/local-server.js +386 -0
- package/scripts/postinstall.js +35 -2
- package/scripts/prepack.js +19 -6
- package/src/server/index.js +53 -18
|
@@ -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
|
+
});
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* postinstall.js —
|
|
4
|
+
* postinstall.js — Post-install setup for context-vault
|
|
5
5
|
*
|
|
6
|
-
* Detects NODE_MODULE_VERSION mismatches
|
|
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(() => {});
|
package/scripts/prepack.js
CHANGED
|
@@ -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(
|
|
18
|
+
const CORE_DEST = join(NODE_MODULES, "@context-vault", "core");
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
|
|
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
|
-
//
|
|
23
|
-
|
|
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");
|
package/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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-
|
|
45
|
-
console.error(`[context-
|
|
46
|
-
console.error(`[context-
|
|
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-
|
|
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-
|
|
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
|
|
76
|
-
console.error(`[context-mcp] Received ${signal}, shutting down...`);
|
|
89
|
+
function closeDb() {
|
|
77
90
|
try {
|
|
78
91
|
if (db.inTransaction) {
|
|
79
|
-
console.error("[context-
|
|
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-
|
|
97
|
+
console.error("[context-vault] Database closed cleanly.");
|
|
85
98
|
} catch (shutdownErr) {
|
|
86
|
-
console.error(`[context-
|
|
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-
|
|
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-
|
|
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-
|
|
165
|
+
console.error(`[context-vault] Fatal error during ${phase} phase: ${err.message}`);
|
|
131
166
|
if (phase === "DB") {
|
|
132
|
-
console.error(`[context-
|
|
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-
|
|
174
|
+
console.error(`[context-vault] Unexpected fatal error: ${err.message}`);
|
|
140
175
|
process.exit(1);
|
|
141
176
|
});
|