context-vault 2.8.12 → 2.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -14,6 +14,7 @@ import { createInterface } from "node:readline";
14
14
  import {
15
15
  existsSync,
16
16
  statSync,
17
+ readdirSync,
17
18
  readFileSync,
18
19
  writeFileSync,
19
20
  mkdirSync,
@@ -455,6 +456,9 @@ async function runSetup() {
455
456
  if (create.toLowerCase() !== "n") {
456
457
  mkdirSync(resolvedVaultDir, { recursive: true });
457
458
  console.log(` ${green("+")} Created ${resolvedVaultDir}`);
459
+ } else {
460
+ console.log(red("\n Setup cancelled — vault directory is required."));
461
+ process.exit(1);
458
462
  }
459
463
  }
460
464
 
@@ -506,16 +510,45 @@ async function runSetup() {
506
510
  const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
507
511
  let frame = 0;
508
512
  const start = Date.now();
513
+ const modelDir = join(homedir(), ".context-mcp", "models");
509
514
  const spinner = setInterval(() => {
510
515
  const elapsed = ((Date.now() - start) / 1000).toFixed(0);
516
+ let downloadedMB = "?";
517
+ try {
518
+ const files = readdirSync(modelDir, {
519
+ recursive: true,
520
+ withFileTypes: true,
521
+ });
522
+ const totalBytes = files
523
+ .filter((f) => f.isFile())
524
+ .reduce(
525
+ (sum, f) =>
526
+ sum + statSync(join(f.parentPath ?? f.path, f.name)).size,
527
+ 0,
528
+ );
529
+ downloadedMB = (totalBytes / 1024 / 1024).toFixed(1);
530
+ } catch {}
511
531
  process.stdout.write(
512
- `\r ${spinnerFrames[frame++ % spinnerFrames.length]} Downloading... ${dim(`${elapsed}s`)}`,
532
+ `\r ${spinnerFrames[frame++ % spinnerFrames.length]} Downloading... ${downloadedMB} MB / ~22 MB ${dim(`${elapsed}s`)}`,
513
533
  );
514
534
  }, 100);
515
535
 
516
536
  try {
517
537
  const { embed } = await import("@context-vault/core/index/embed");
518
- await embed("warmup");
538
+ let timeoutHandle;
539
+ const timeout = new Promise((_, reject) => {
540
+ timeoutHandle = setTimeout(
541
+ () =>
542
+ reject(
543
+ Object.assign(new Error("Download timed out after 90s"), {
544
+ code: "ETIMEDOUT",
545
+ }),
546
+ ),
547
+ 90_000,
548
+ );
549
+ });
550
+ await Promise.race([embed("warmup"), timeout]);
551
+ clearTimeout(timeoutHandle);
519
552
 
520
553
  clearInterval(spinner);
521
554
  process.stdout.write(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.8.12",
3
+ "version": "2.8.14",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -14,38 +14,51 @@ let extractor = null;
14
14
  /** @type {null | true | false} null = unknown, true = working, false = failed */
15
15
  let embedAvailable = null;
16
16
 
17
+ /** Shared promise for in-flight initialization — prevents concurrent loads */
18
+ let loadingPromise = null;
19
+
17
20
  async function ensurePipeline() {
18
21
  if (embedAvailable === false) return null;
19
22
  if (extractor) return extractor;
23
+ if (loadingPromise) return loadingPromise;
20
24
 
21
- try {
22
- // Dynamic import — @huggingface/transformers is optional (its transitive
23
- // dep `sharp` can fail to install on some platforms). When missing, the
24
- // server still works with full-text search only.
25
- const { pipeline, env } = await import("@huggingface/transformers");
25
+ loadingPromise = (async () => {
26
+ try {
27
+ // Dynamic import @huggingface/transformers is optional (its transitive
28
+ // dep `sharp` can fail to install on some platforms). When missing, the
29
+ // server still works with full-text search only.
30
+ const { pipeline, env } = await import("@huggingface/transformers");
26
31
 
27
- // Redirect model cache to ~/.context-mcp/models/ so it works when the
28
- // package is installed globally in a root-owned directory (e.g. /usr/lib/node_modules/).
29
- const modelCacheDir = join(homedir(), ".context-mcp", "models");
30
- mkdirSync(modelCacheDir, { recursive: true });
31
- env.cacheDir = modelCacheDir;
32
+ // Redirect model cache to ~/.context-mcp/models/ so it works when the
33
+ // package is installed globally in a root-owned directory (e.g. /usr/lib/node_modules/).
34
+ const modelCacheDir = join(homedir(), ".context-mcp", "models");
35
+ mkdirSync(modelCacheDir, { recursive: true });
36
+ env.cacheDir = modelCacheDir;
32
37
 
33
- console.error(
34
- "[context-vault] Loading embedding model (first run may download ~22MB)...",
35
- );
36
- extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
37
- embedAvailable = true;
38
- return extractor;
39
- } catch (e) {
40
- embedAvailable = false;
41
- console.error(
42
- `[context-vault] Failed to load embedding model: ${e.message}`,
43
- );
44
- console.error(
45
- `[context-vault] Semantic search disabled. Full-text search still works.`,
46
- );
47
- return null;
48
- }
38
+ console.error(
39
+ "[context-vault] Loading embedding model (first run may download ~22MB)...",
40
+ );
41
+ extractor = await pipeline(
42
+ "feature-extraction",
43
+ "Xenova/all-MiniLM-L6-v2",
44
+ );
45
+ embedAvailable = true;
46
+ return extractor;
47
+ } catch (e) {
48
+ embedAvailable = false;
49
+ console.error(
50
+ `[context-vault] Failed to load embedding model: ${e.message}`,
51
+ );
52
+ console.error(
53
+ `[context-vault] Semantic search disabled. Full-text search still works.`,
54
+ );
55
+ return null;
56
+ } finally {
57
+ loadingPromise = null;
58
+ }
59
+ })();
60
+
61
+ return loadingPromise;
49
62
  }
50
63
 
51
64
  export async function embed(text) {
@@ -57,6 +70,7 @@ export async function embed(text) {
57
70
  if (!result?.data?.length) {
58
71
  extractor = null;
59
72
  embedAvailable = null;
73
+ loadingPromise = null;
60
74
  throw new Error("Embedding pipeline returned empty result");
61
75
  }
62
76
  return new Float32Array(result.data);
@@ -76,6 +90,7 @@ export async function embedBatch(texts) {
76
90
  if (!result?.data?.length) {
77
91
  extractor = null;
78
92
  embedAvailable = null;
93
+ loadingPromise = null;
79
94
  throw new Error("Embedding pipeline returned empty result");
80
95
  }
81
96
  const dim = result.data.length / texts.length;
@@ -93,6 +108,7 @@ export async function embedBatch(texts) {
93
108
  export function resetEmbedPipeline() {
94
109
  extractor = null;
95
110
  embedAvailable = null;
111
+ loadingPromise = null;
96
112
  }
97
113
 
98
114
  /** Check if embedding is currently available. */
@@ -307,11 +307,16 @@ export async function reindex(ctx, opts = {}) {
307
307
  created,
308
308
  );
309
309
  if (result.changes > 0) {
310
- const rowid = ctx.stmts.getRowid.get(id).rowid;
311
- const embeddingText = [parsed.title, parsed.body]
312
- .filter(Boolean)
313
- .join(" ");
314
- pendingEmbeds.push({ rowid, text: embeddingText });
310
+ const rowidResult = ctx.stmts.getRowid.get(id);
311
+ if (rowidResult?.rowid) {
312
+ const embeddingText = [parsed.title, parsed.body]
313
+ .filter(Boolean)
314
+ .join(" ");
315
+ pendingEmbeds.push({
316
+ rowid: rowidResult.rowid,
317
+ text: embeddingText,
318
+ });
319
+ }
315
320
  stats.added++;
316
321
  } else {
317
322
  stats.unchanged++;
@@ -157,9 +157,9 @@ export async function hybridSearch(
157
157
  : 15;
158
158
  const vecRows = ctx.db
159
159
  .prepare(
160
- `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ${vecLimit}`,
160
+ `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
161
161
  )
162
- .all(queryVec);
162
+ .all(queryVec, vecLimit);
163
163
 
164
164
  if (vecRows.length) {
165
165
  // Batch hydration: single query instead of N+1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.8.12",
3
+ "version": "2.8.14",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  "@context-vault/core"
56
56
  ],
57
57
  "dependencies": {
58
- "@context-vault/core": "^2.8.12",
58
+ "@context-vault/core": "^2.8.14",
59
59
  "@modelcontextprotocol/sdk": "^1.26.0",
60
60
  "better-sqlite3": "^12.6.2",
61
61
  "sqlite-vec": "^0.1.0"