context-vault 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.4.2",
3
+ "version": "2.6.0",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "bin/",
21
21
  "src/",
22
22
  "scripts/",
23
+ "app-dist/",
23
24
  "README.md",
24
25
  "LICENSE"
25
26
  ],
@@ -30,11 +31,11 @@
30
31
  "author": "Felix Hellstrom",
31
32
  "repository": {
32
33
  "type": "git",
33
- "url": "git+https://github.com/fellanH/context-mcp.git"
34
+ "url": "git+https://github.com/fellanH/context-vault.git"
34
35
  },
35
- "homepage": "https://github.com/fellanH/context-mcp",
36
+ "homepage": "https://github.com/fellanH/context-vault",
36
37
  "bugs": {
37
- "url": "https://github.com/fellanH/context-mcp/issues"
38
+ "url": "https://github.com/fellanH/context-vault/issues"
38
39
  },
39
40
  "keywords": [
40
41
  "mcp",
@@ -55,7 +56,7 @@
55
56
  "@context-vault/core"
56
57
  ],
57
58
  "dependencies": {
58
- "@context-vault/core": "^2.4.2",
59
+ "@context-vault/core": "^2.5.1",
59
60
  "@modelcontextprotocol/sdk": "^1.26.0",
60
61
  "better-sqlite3": "^12.6.2",
61
62
  "sqlite-vec": "^0.1.0"
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { createServer } from "node:http";
10
- import { createReadStream, existsSync, statSync, unlinkSync } from "node:fs";
10
+ import { createReadStream, existsSync, statSync, unlinkSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
11
  import { join, resolve, dirname, extname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { homedir, platform } from "node:os";
@@ -21,10 +21,18 @@ import { hybridSearch } from "@context-vault/core/retrieve";
21
21
  import { gatherVaultStatus } from "@context-vault/core/core/status";
22
22
  import { normalizeKind } from "@context-vault/core/core/files";
23
23
  import { categoryFor } from "@context-vault/core/core/categories";
24
+ import { parseFile } from "@context-vault/core/capture/importers";
25
+ import { importEntries } from "@context-vault/core/capture/import-pipeline";
26
+ import { ingestUrl } from "@context-vault/core/capture/ingest-url";
27
+ import { buildLocalManifest, fetchRemoteManifest, computeSyncPlan, executeSync } from "@context-vault/core/sync";
24
28
 
25
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
30
  const LOCAL_ROOT = resolve(__dirname, "..");
27
- const APP_DIST = resolve(LOCAL_ROOT, "..", "app", "dist");
31
+
32
+ // Try bundled path first (npm install), then workspace path (local dev)
33
+ const bundledDist = resolve(LOCAL_ROOT, "app-dist");
34
+ const workspaceDist = resolve(LOCAL_ROOT, "..", "app", "dist");
35
+ const APP_DIST = existsSync(join(bundledDist, "index.html")) ? bundledDist : workspaceDist;
28
36
 
29
37
  const MIME = {
30
38
  ".html": "text/html",
@@ -91,8 +99,24 @@ async function main() {
91
99
  const server = createServer(async (req, res) => {
92
100
  const url = req.url?.replace(/\?.*$/, "") || "/";
93
101
 
102
+ // ─── CORS Preflight ───────────────────────────────────────────────────────
103
+ if (req.method === "OPTIONS") {
104
+ res.writeHead(204, {
105
+ "Access-Control-Allow-Origin": "*",
106
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
107
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
108
+ });
109
+ res.end();
110
+ return;
111
+ }
112
+
94
113
  const json = (data, status = 200) => {
95
- res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
114
+ res.writeHead(status, {
115
+ "Content-Type": "application/json",
116
+ "Access-Control-Allow-Origin": "*",
117
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
118
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
119
+ });
96
120
  res.end(JSON.stringify(data));
97
121
  };
98
122
 
@@ -341,6 +365,183 @@ async function main() {
341
365
  }
342
366
  }
343
367
 
368
+ // ─── API: POST /api/vault/import/bulk — Bulk import entries ─────────────
369
+ if (url === "/api/vault/import/bulk" && req.method === "POST") {
370
+ const data = await readBody();
371
+ if (!data || !Array.isArray(data.entries)) {
372
+ return json({ error: "Invalid body — expected { entries: [...] }", code: "INVALID_INPUT" }, 400);
373
+ }
374
+ if (data.entries.length > 500) {
375
+ return json({ error: "Maximum 500 entries per request", code: "LIMIT_EXCEEDED" }, 400);
376
+ }
377
+
378
+ const result = await importEntries(ctx, data.entries, { source: "bulk-import" });
379
+ return json({ imported: result.imported, failed: result.failed, errors: result.errors.slice(0, 10).map((e) => e.error) });
380
+ }
381
+
382
+ // ─── API: POST /api/vault/import/file — Import from file content ────────
383
+ if (url === "/api/vault/import/file" && req.method === "POST") {
384
+ const data = await readBody();
385
+ if (!data?.filename || !data?.content) {
386
+ return json({ error: "filename and content are required", code: "INVALID_INPUT" }, 400);
387
+ }
388
+
389
+ const entries = parseFile(data.filename, data.content, { kind: data.kind, source: data.source || "file-import" });
390
+ if (!entries.length) return json({ imported: 0, failed: 0, errors: ["No entries parsed from file"] });
391
+
392
+ const result = await importEntries(ctx, entries, { source: data.source || "file-import" });
393
+ return json({ imported: result.imported, failed: result.failed, errors: result.errors.slice(0, 10).map((e) => e.error) });
394
+ }
395
+
396
+ // ─── API: GET /api/vault/export — Export all entries ─────────────────────
397
+ if (url.startsWith("/api/vault/export") && req.method === "GET") {
398
+ const u = new URL(req.url || "", "http://localhost");
399
+ const format = u.searchParams.get("format") || "json";
400
+
401
+ const rows = ctx.db.prepare(
402
+ "SELECT * FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC"
403
+ ).all();
404
+
405
+ const entries = rows.map(formatEntry);
406
+
407
+ if (format === "csv") {
408
+ const headers = ["id", "kind", "category", "title", "body", "tags", "source", "identity_key", "expires_at", "created_at"];
409
+ const csvLines = [headers.join(",")];
410
+ for (const e of entries) {
411
+ const row = headers.map((h) => {
412
+ let val = e[h];
413
+ if (Array.isArray(val)) val = val.join(", ");
414
+ if (val == null) val = "";
415
+ val = String(val);
416
+ if (val.includes(",") || val.includes('"') || val.includes("\n")) {
417
+ val = '"' + val.replace(/"/g, '""') + '"';
418
+ }
419
+ return val;
420
+ });
421
+ csvLines.push(row.join(","));
422
+ }
423
+ res.writeHead(200, { "Content-Type": "text/csv", "Access-Control-Allow-Origin": "*" });
424
+ res.end(csvLines.join("\n"));
425
+ return;
426
+ }
427
+
428
+ return json({ entries, total: entries.length, exported_at: new Date().toISOString() });
429
+ }
430
+
431
+ // ─── API: POST /api/vault/ingest — Fetch URL and save as entry ──────────
432
+ if (url === "/api/vault/ingest" && req.method === "POST") {
433
+ const data = await readBody();
434
+ if (!data?.url) return json({ error: "url is required", code: "INVALID_INPUT" }, 400);
435
+
436
+ try {
437
+ const entry = await ingestUrl(data.url, { kind: data.kind, tags: data.tags });
438
+ const result = await captureAndIndex(ctx, entry, indexEntry);
439
+ return json(formatEntry(ctx.stmts.getEntryById.get(result.id)), 201);
440
+ } catch (e) {
441
+ return json({ error: `Ingestion failed: ${e.message}`, code: "INGEST_FAILED" }, 500);
442
+ }
443
+ }
444
+
445
+ // ─── API: GET /api/vault/manifest — Lightweight entry list for sync ─────
446
+ if (url === "/api/vault/manifest" && req.method === "GET") {
447
+ const rows = ctx.db.prepare(
448
+ "SELECT id, kind, title, created_at FROM vault WHERE (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY created_at DESC"
449
+ ).all();
450
+ return json({ entries: rows.map((r) => ({ id: r.id, kind: r.kind, title: r.title || null, created_at: r.created_at })) });
451
+ }
452
+
453
+ // ─── API: GET /api/local/link — Get link status ─────────────────────────
454
+ if (url === "/api/local/link" && req.method === "GET") {
455
+ const dataDir = join(homedir(), ".context-mcp");
456
+ const configPath = join(dataDir, "config.json");
457
+ let storedConfig = {};
458
+ if (existsSync(configPath)) {
459
+ try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
460
+ }
461
+ return json({
462
+ linked: !!storedConfig.apiKey,
463
+ email: storedConfig.email || null,
464
+ hostedUrl: storedConfig.hostedUrl || null,
465
+ linkedAt: storedConfig.linkedAt || null,
466
+ tier: storedConfig.tier || null,
467
+ });
468
+ }
469
+
470
+ // ─── API: POST /api/local/link — Link/unlink hosted account ─────────────
471
+ if (url === "/api/local/link" && req.method === "POST") {
472
+ const data = await readBody();
473
+ const dataDir = join(homedir(), ".context-mcp");
474
+ const configPath = join(dataDir, "config.json");
475
+
476
+ let storedConfig = {};
477
+ if (existsSync(configPath)) {
478
+ try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
479
+ }
480
+
481
+ if (!data?.apiKey) {
482
+ // Unlink
483
+ delete storedConfig.apiKey;
484
+ delete storedConfig.hostedUrl;
485
+ delete storedConfig.userId;
486
+ delete storedConfig.email;
487
+ delete storedConfig.linkedAt;
488
+ delete storedConfig.tier;
489
+ writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
490
+ return json({ linked: false });
491
+ }
492
+
493
+ const hostedUrl = data.hostedUrl || "https://api.context-vault.com";
494
+ try {
495
+ const response = await fetch(`${hostedUrl}/api/me`, {
496
+ headers: { Authorization: `Bearer ${data.apiKey}` },
497
+ });
498
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
499
+ const user = await response.json();
500
+
501
+ storedConfig.apiKey = data.apiKey;
502
+ storedConfig.hostedUrl = hostedUrl;
503
+ storedConfig.userId = user.userId || user.id;
504
+ storedConfig.email = user.email;
505
+ storedConfig.tier = user.tier || "free";
506
+ storedConfig.linkedAt = new Date().toISOString();
507
+
508
+ mkdirSync(dataDir, { recursive: true });
509
+ writeFileSync(configPath, JSON.stringify(storedConfig, null, 2) + "\n");
510
+
511
+ return json({ linked: true, email: user.email, tier: user.tier || "free" });
512
+ } catch (e) {
513
+ return json({ error: `Verification failed: ${e.message}`, code: "AUTH_FAILED" }, 401);
514
+ }
515
+ }
516
+
517
+ // ─── API: POST /api/local/sync — Bidirectional sync ─────────────────────
518
+ if (url === "/api/local/sync" && req.method === "POST") {
519
+ const dataDir = join(homedir(), ".context-mcp");
520
+ const configPath = join(dataDir, "config.json");
521
+ let storedConfig = {};
522
+ if (existsSync(configPath)) {
523
+ try { storedConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {}
524
+ }
525
+
526
+ if (!storedConfig.apiKey) {
527
+ return json({ error: "Not linked. Use link endpoint first.", code: "NOT_LINKED" }, 400);
528
+ }
529
+
530
+ try {
531
+ const local = buildLocalManifest(ctx);
532
+ const remote = await fetchRemoteManifest(storedConfig.hostedUrl, storedConfig.apiKey);
533
+ const plan = computeSyncPlan(local, remote);
534
+ const result = await executeSync(ctx, {
535
+ hostedUrl: storedConfig.hostedUrl,
536
+ apiKey: storedConfig.apiKey,
537
+ plan,
538
+ });
539
+ return json(result);
540
+ } catch (e) {
541
+ return json({ error: `Sync failed: ${e.message}`, code: "SYNC_FAILED" }, 500);
542
+ }
543
+ }
544
+
344
545
  // ─── Static files ───────────────────────────────────────────────────────
345
546
  const filePath = join(APP_DIST, url === "/" ? "index.html" : url.replace(/^\//, ""));
346
547
  if (!filePath.startsWith(APP_DIST)) {
@@ -7,7 +7,7 @@
7
7
  * Replaces the Unix shell script in package.json "prepack".
8
8
  */
9
9
 
10
- import { cpSync, rmSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { cpSync, rmSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
11
11
  import { join, dirname } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
 
@@ -16,6 +16,8 @@ const LOCAL_ROOT = join(__dirname, "..");
16
16
  const NODE_MODULES = join(LOCAL_ROOT, "node_modules");
17
17
  const CORE_SRC = join(LOCAL_ROOT, "..", "core");
18
18
  const CORE_DEST = join(NODE_MODULES, "@context-vault", "core");
19
+ const APP_SRC = join(LOCAL_ROOT, "..", "app", "dist");
20
+ const APP_DEST = join(LOCAL_ROOT, "app-dist");
19
21
 
20
22
  // Clean node_modules to prevent workspace deps from leaking into the tarball.
21
23
  // Only @context-vault/core should be bundled.
@@ -42,3 +44,13 @@ delete corePkg.dependencies;
42
44
  writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) + "\n");
43
45
 
44
46
  console.log("[prepack] Bundled @context-vault/core into node_modules");
47
+
48
+ // Copy pre-built web dashboard into app-dist/
49
+ if (!existsSync(join(APP_SRC, "index.html"))) {
50
+ console.error("[prepack] ERROR: Web dashboard not built. Run: npm run build --workspace=packages/app");
51
+ process.exit(1);
52
+ }
53
+
54
+ rmSync(APP_DEST, { recursive: true, force: true });
55
+ cpSync(APP_SRC, APP_DEST, { recursive: true });
56
+ console.log("[prepack] Bundled web dashboard into app-dist/");