context-vault 2.17.1 → 3.0.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.
Files changed (110) hide show
  1. package/bin/cli.js +795 -71
  2. package/node_modules/@context-vault/core/dist/capture.d.ts +21 -0
  3. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -0
  4. package/node_modules/@context-vault/core/dist/capture.js +269 -0
  5. package/node_modules/@context-vault/core/dist/capture.js.map +1 -0
  6. package/node_modules/@context-vault/core/dist/categories.d.ts +6 -0
  7. package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -0
  8. package/node_modules/@context-vault/core/dist/categories.js +50 -0
  9. package/node_modules/@context-vault/core/dist/categories.js.map +1 -0
  10. package/node_modules/@context-vault/core/dist/config.d.ts +4 -0
  11. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -0
  12. package/node_modules/@context-vault/core/dist/config.js +190 -0
  13. package/node_modules/@context-vault/core/dist/config.js.map +1 -0
  14. package/node_modules/@context-vault/core/dist/constants.d.ts +33 -0
  15. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -0
  16. package/node_modules/@context-vault/core/dist/constants.js +23 -0
  17. package/node_modules/@context-vault/core/dist/constants.js.map +1 -0
  18. package/node_modules/@context-vault/core/dist/db.d.ts +13 -0
  19. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -0
  20. package/node_modules/@context-vault/core/dist/db.js +191 -0
  21. package/node_modules/@context-vault/core/dist/db.js.map +1 -0
  22. package/node_modules/@context-vault/core/dist/embed.d.ts +5 -0
  23. package/node_modules/@context-vault/core/dist/embed.d.ts.map +1 -0
  24. package/node_modules/@context-vault/core/dist/embed.js +78 -0
  25. package/node_modules/@context-vault/core/dist/embed.js.map +1 -0
  26. package/node_modules/@context-vault/core/dist/files.d.ts +13 -0
  27. package/node_modules/@context-vault/core/dist/files.d.ts.map +1 -0
  28. package/node_modules/@context-vault/core/dist/files.js +66 -0
  29. package/node_modules/@context-vault/core/dist/files.js.map +1 -0
  30. package/node_modules/@context-vault/core/dist/formatters.d.ts +8 -0
  31. package/node_modules/@context-vault/core/dist/formatters.d.ts.map +1 -0
  32. package/node_modules/@context-vault/core/dist/formatters.js +18 -0
  33. package/node_modules/@context-vault/core/dist/formatters.js.map +1 -0
  34. package/node_modules/@context-vault/core/dist/frontmatter.d.ts +12 -0
  35. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -0
  36. package/node_modules/@context-vault/core/dist/frontmatter.js +101 -0
  37. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -0
  38. package/node_modules/@context-vault/core/dist/index.d.ts +10 -0
  39. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -0
  40. package/node_modules/@context-vault/core/dist/index.js +297 -0
  41. package/node_modules/@context-vault/core/dist/index.js.map +1 -0
  42. package/node_modules/@context-vault/core/dist/ingest-url.d.ts +20 -0
  43. package/node_modules/@context-vault/core/dist/ingest-url.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/ingest-url.js +113 -0
  45. package/node_modules/@context-vault/core/dist/ingest-url.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/main.d.ts +14 -0
  47. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -0
  48. package/node_modules/@context-vault/core/dist/main.js +25 -0
  49. package/node_modules/@context-vault/core/dist/main.js.map +1 -0
  50. package/node_modules/@context-vault/core/dist/search.d.ts +18 -0
  51. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -0
  52. package/node_modules/@context-vault/core/dist/search.js +238 -0
  53. package/node_modules/@context-vault/core/dist/search.js.map +1 -0
  54. package/node_modules/@context-vault/core/dist/types.d.ts +176 -0
  55. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -0
  56. package/node_modules/@context-vault/core/dist/types.js +2 -0
  57. package/node_modules/@context-vault/core/dist/types.js.map +1 -0
  58. package/node_modules/@context-vault/core/package.json +66 -16
  59. package/node_modules/@context-vault/core/src/capture.ts +308 -0
  60. package/node_modules/@context-vault/core/src/categories.ts +54 -0
  61. package/node_modules/@context-vault/core/src/{core/config.js → config.ts} +34 -33
  62. package/node_modules/@context-vault/core/src/{constants.js → constants.ts} +6 -3
  63. package/node_modules/@context-vault/core/src/db.ts +229 -0
  64. package/node_modules/@context-vault/core/src/{index/embed.js → embed.ts} +10 -35
  65. package/node_modules/@context-vault/core/src/{core/files.js → files.ts} +15 -20
  66. package/node_modules/@context-vault/core/src/{capture/formatters.js → formatters.ts} +13 -11
  67. package/node_modules/@context-vault/core/src/{core/frontmatter.js → frontmatter.ts} +26 -33
  68. package/node_modules/@context-vault/core/src/index.ts +351 -0
  69. package/node_modules/@context-vault/core/src/ingest-url.ts +99 -0
  70. package/node_modules/@context-vault/core/src/main.ts +111 -0
  71. package/node_modules/@context-vault/core/src/{retrieve/index.js → search.ts} +62 -150
  72. package/node_modules/@context-vault/core/src/types.ts +166 -0
  73. package/package.json +12 -7
  74. package/scripts/postinstall.js +1 -1
  75. package/{node_modules/@context-vault/core/src/core → src}/error-log.js +1 -15
  76. package/{node_modules/@context-vault/core/src/server → src}/helpers.js +9 -4
  77. package/src/linking.js +100 -0
  78. package/{node_modules/@context-vault/core/src/server/tools.js → src/register-tools.js} +14 -13
  79. package/src/{server/index.js → server.js} +10 -38
  80. package/src/status.js +235 -0
  81. package/{node_modules/@context-vault/core/src/core → src}/telemetry.js +9 -19
  82. package/src/temporal.js +97 -0
  83. package/{node_modules/@context-vault/core/src/server → src}/tools/context-status.js +3 -4
  84. package/{node_modules/@context-vault/core/src/server → src}/tools/create-snapshot.js +6 -7
  85. package/{node_modules/@context-vault/core/src/server → src}/tools/delete-context.js +0 -2
  86. package/{node_modules/@context-vault/core/src/server → src}/tools/get-context.js +17 -21
  87. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-project.js +5 -6
  88. package/{node_modules/@context-vault/core/src/server → src}/tools/ingest-url.js +3 -4
  89. package/{node_modules/@context-vault/core/src/server → src}/tools/list-buckets.js +4 -5
  90. package/{node_modules/@context-vault/core/src/server → src}/tools/list-context.js +3 -6
  91. package/{node_modules/@context-vault/core/src/server → src}/tools/save-context.js +17 -20
  92. package/{node_modules/@context-vault/core/src/server → src}/tools/session-start.js +9 -16
  93. package/node_modules/@context-vault/core/src/capture/file-ops.js +0 -99
  94. package/node_modules/@context-vault/core/src/capture/import-pipeline.js +0 -46
  95. package/node_modules/@context-vault/core/src/capture/importers.js +0 -387
  96. package/node_modules/@context-vault/core/src/capture/index.js +0 -250
  97. package/node_modules/@context-vault/core/src/capture/ingest-url.js +0 -252
  98. package/node_modules/@context-vault/core/src/consolidation/index.js +0 -112
  99. package/node_modules/@context-vault/core/src/core/categories.js +0 -73
  100. package/node_modules/@context-vault/core/src/core/linking.js +0 -161
  101. package/node_modules/@context-vault/core/src/core/migrate-dirs.js +0 -196
  102. package/node_modules/@context-vault/core/src/core/status.js +0 -350
  103. package/node_modules/@context-vault/core/src/core/temporal.js +0 -146
  104. package/node_modules/@context-vault/core/src/index/db.js +0 -586
  105. package/node_modules/@context-vault/core/src/index/index.js +0 -583
  106. package/node_modules/@context-vault/core/src/index.js +0 -71
  107. package/node_modules/@context-vault/core/src/sync/sync.js +0 -235
  108. package/src/hooks/post-tool-call.mjs +0 -62
  109. package/src/hooks/session-end.mjs +0 -492
  110. /package/{node_modules/@context-vault/core/src/server → src}/tools/clear-context.js +0 -0
package/src/linking.js ADDED
@@ -0,0 +1,100 @@
1
+ import type { DatabaseSync } from "node:sqlite";
2
+
3
+ export function parseRelatedTo(raw: string | null | undefined): string[] {
4
+ if (!raw) return [];
5
+ try {
6
+ const parsed = JSON.parse(raw);
7
+ if (!Array.isArray(parsed)) return [];
8
+ return parsed.filter((id: unknown) => typeof id === "string" && (id as string).trim());
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ export function resolveLinks(
15
+ db: DatabaseSync,
16
+ ids: string[],
17
+ ): Record<string, unknown>[] {
18
+ if (!ids.length) return [];
19
+ const unique = [...new Set(ids)];
20
+ const placeholders = unique.map(() => "?").join(",");
21
+ try {
22
+ return db
23
+ .prepare(
24
+ `SELECT * FROM vault
25
+ WHERE id IN (${placeholders})
26
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
27
+ AND superseded_by IS NULL`,
28
+ )
29
+ .all(...unique) as unknown as Record<string, unknown>[];
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ export function resolveBacklinks(
36
+ db: DatabaseSync,
37
+ entryId: string,
38
+ ): Record<string, unknown>[] {
39
+ if (!entryId) return [];
40
+ const likePattern = `%"${entryId}"%`;
41
+ try {
42
+ return db
43
+ .prepare(
44
+ `SELECT * FROM vault
45
+ WHERE related_to LIKE ?
46
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
47
+ AND superseded_by IS NULL`,
48
+ )
49
+ .all(likePattern) as unknown as Record<string, unknown>[];
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ export function collectLinkedEntries(
56
+ db: DatabaseSync,
57
+ primaryEntries: Record<string, unknown>[],
58
+ ): { forward: Record<string, unknown>[]; backward: Record<string, unknown>[] } {
59
+ const primaryIds = new Set(primaryEntries.map((e) => e.id as string));
60
+
61
+ const forwardIds: string[] = [];
62
+ for (const entry of primaryEntries) {
63
+ const ids = parseRelatedTo(entry.related_to as string);
64
+ for (const id of ids) {
65
+ if (!primaryIds.has(id)) forwardIds.push(id);
66
+ }
67
+ }
68
+ const forwardEntries = resolveLinks(db, forwardIds).filter(
69
+ (e) => !primaryIds.has(e.id as string),
70
+ );
71
+
72
+ const backwardSeen = new Set<string>();
73
+ const backwardEntries: Record<string, unknown>[] = [];
74
+ for (const entry of primaryEntries) {
75
+ const backlinks = resolveBacklinks(db, entry.id as string);
76
+ for (const bl of backlinks) {
77
+ if (!primaryIds.has(bl.id as string) && !backwardSeen.has(bl.id as string)) {
78
+ backwardSeen.add(bl.id as string);
79
+ backwardEntries.push(bl);
80
+ }
81
+ }
82
+ }
83
+
84
+ return { forward: forwardEntries, backward: backwardEntries };
85
+ }
86
+
87
+ export function validateRelatedTo(relatedTo: unknown): string | null {
88
+ if (relatedTo === undefined || relatedTo === null) return null;
89
+ if (!Array.isArray(relatedTo))
90
+ return "related_to must be an array of entry IDs";
91
+ for (const id of relatedTo) {
92
+ if (typeof id !== "string" || !id.trim()) {
93
+ return "each related_to entry must be a non-empty string ID";
94
+ }
95
+ if (id.length > 32) {
96
+ return `related_to ID too long (max 32 chars): "${id.slice(0, 32)}..."`;
97
+ }
98
+ }
99
+ return null;
100
+ }
@@ -1,8 +1,15 @@
1
- import { reindex } from "../index/index.js";
2
- import { captureAndIndex } from "../capture/index.js";
1
+ import { reindex } from "@context-vault/core/index";
2
+ import { captureAndIndex } from "@context-vault/core/capture";
3
3
  import { err } from "./helpers.js";
4
- import { sendTelemetryEvent } from "../core/telemetry.js";
5
- import pkg from "../../package.json" with { type: "json" };
4
+ import { sendTelemetryEvent } from "./telemetry.js";
5
+ import { readFileSync } from "node:fs";
6
+ import { join, dirname } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const pkg = JSON.parse(
11
+ readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
12
+ );
6
13
 
7
14
  import * as getContext from "./tools/get-context.js";
8
15
  import * as saveContext from "./tools/save-context.js";
@@ -33,8 +40,6 @@ const toolModules = [
33
40
  const TOOL_TIMEOUT_MS = 60_000;
34
41
 
35
42
  export function registerTools(server, ctx) {
36
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
37
-
38
43
  function tracked(handler, toolName) {
39
44
  return async (...args) => {
40
45
  if (ctx.activeOps) ctx.activeOps.count++;
@@ -55,8 +60,6 @@ export function registerTools(server, ctx) {
55
60
  return result;
56
61
  } catch (e) {
57
62
  if (e.message === "TOOL_TIMEOUT") {
58
- // Suppress any late rejection from the still-running handler to
59
- // prevent unhandled promise rejection warnings in the host process.
60
63
  handlerPromise?.catch(() => {});
61
64
  if (ctx.toolStats) {
62
65
  ctx.toolStats.errors++;
@@ -105,7 +108,7 @@ export function registerTools(server, ctx) {
105
108
  auto: true,
106
109
  },
107
110
  });
108
- } catch {} // never block on feedback capture
111
+ } catch {}
109
112
  throw e;
110
113
  } finally {
111
114
  clearTimeout(timer);
@@ -114,8 +117,7 @@ export function registerTools(server, ctx) {
114
117
  };
115
118
  }
116
119
 
117
- // In hosted mode, skip reindex — DB is always in sync via writeEntry→indexEntry
118
- let reindexDone = userId !== undefined ? true : false;
120
+ let reindexDone = false;
119
121
  let reindexPromise = null;
120
122
  let reindexAttempts = 0;
121
123
  let reindexFailed = false;
@@ -124,7 +126,6 @@ export function registerTools(server, ctx) {
124
126
  async function ensureIndexed() {
125
127
  if (reindexDone) return;
126
128
  if (reindexPromise) return reindexPromise;
127
- // Assign promise synchronously to prevent concurrent calls from both entering reindex()
128
129
  const promise = reindex(ctx, { fullSync: true })
129
130
  .then((stats) => {
130
131
  reindexDone = true;
@@ -147,7 +148,7 @@ export function registerTools(server, ctx) {
147
148
  reindexDone = true;
148
149
  reindexFailed = true;
149
150
  } else {
150
- reindexPromise = null; // Allow retry on next tool call
151
+ reindexPromise = null;
151
152
  }
152
153
  });
153
154
  reindexPromise = promise;
@@ -18,24 +18,19 @@ const pkg = JSON.parse(
18
18
  readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
19
19
  );
20
20
 
21
- import { resolveConfig } from "@context-vault/core/core/config";
22
- import { appendErrorLog } from "@context-vault/core/core/error-log";
23
- import {
24
- sendTelemetryEvent,
25
- maybeShowTelemetryNotice,
26
- } from "@context-vault/core/core/telemetry";
27
- import { embed } from "@context-vault/core/index/embed";
21
+ import { resolveConfig } from "@context-vault/core/config";
22
+ import { appendErrorLog } from "./error-log.js";
23
+ import { sendTelemetryEvent, maybeShowTelemetryNotice } from "./telemetry.js";
24
+ import { embed } from "@context-vault/core/embed";
28
25
  import {
29
26
  initDatabase,
30
27
  NativeModuleError,
31
28
  prepareStatements,
32
29
  insertVec,
33
30
  deleteVec,
34
- } from "@context-vault/core/index/db";
35
- import { registerTools } from "@context-vault/core/server/tools";
36
- import { pruneExpired } from "@context-vault/core/index/index";
37
-
38
- // ─── Phased Startup ─────────────────────────────────────────────────────────
31
+ } from "@context-vault/core/db";
32
+ import { registerTools } from "./register-tools.js";
33
+ import { pruneExpired } from "@context-vault/core/index";
39
34
 
40
35
  async function main() {
41
36
  let phase = "CONFIG";
@@ -43,16 +38,13 @@ async function main() {
43
38
  let config;
44
39
 
45
40
  try {
46
- // ── Phase: CONFIG ────────────────────────────────────────────────────────
47
41
  config = resolveConfig();
48
42
 
49
- // ── Phase: DIRS ──────────────────────────────────────────────────────────
50
43
  phase = "DIRS";
51
44
  mkdirSync(config.dataDir, { recursive: true });
52
45
  mkdirSync(config.vaultDir, { recursive: true });
53
46
  maybeShowTelemetryNotice(config.dataDir);
54
47
 
55
- // Verify vault directory is writable (catch permission issues early)
56
48
  try {
57
49
  const probe = join(config.vaultDir, ".write-probe");
58
50
  writeFileSync(probe, "");
@@ -68,7 +60,6 @@ async function main() {
68
60
  process.exit(1);
69
61
  }
70
62
 
71
- // Write .context-mcp marker (non-fatal)
72
63
  try {
73
64
  const markerPath = join(config.vaultDir, ".context-mcp");
74
65
  const markerData = existsSync(markerPath)
@@ -93,7 +84,6 @@ async function main() {
93
84
 
94
85
  config.vaultDirExists = existsSync(config.vaultDir);
95
86
 
96
- // Startup diagnostics
97
87
  console.error(`[context-vault] Vault: ${config.vaultDir}`);
98
88
  console.error(`[context-vault] Database: ${config.dbPath}`);
99
89
  console.error(`[context-vault] Dev dir: ${config.devDir}`);
@@ -101,11 +91,9 @@ async function main() {
101
91
  console.error(`[context-vault] WARNING: Vault directory not found!`);
102
92
  }
103
93
 
104
- // ── Phase: DB ────────────────────────────────────────────────────────────
105
94
  phase = "DB";
106
- const mode = config.hostedUrl ? "hosted" : "local";
107
- db = await initDatabase(config.dbPath, { mode });
108
- const stmts = prepareStatements(db, mode);
95
+ db = await initDatabase(config.dbPath);
96
+ const stmts = prepareStatements(db);
109
97
 
110
98
  const ctx = {
111
99
  db,
@@ -118,7 +106,6 @@ async function main() {
118
106
  toolStats: { ok: 0, errors: 0, lastError: null },
119
107
  };
120
108
 
121
- // ── Phase: PRUNE ─────────────────────────────────────────────────────────
122
109
  try {
123
110
  const pruned = await pruneExpired(ctx);
124
111
  if (pruned > 0) {
@@ -132,16 +119,12 @@ async function main() {
132
119
  );
133
120
  }
134
121
 
135
- // ── Phase: SERVER ────────────────────────────────────────────────────────
136
122
  phase = "SERVER";
137
123
  const server = new McpServer(
138
124
  { name: "context-vault", version: pkg.version },
139
125
  { capabilities: { tools: {} } },
140
126
  );
141
127
 
142
- // Hot-reload config.json on every tool call (Option C from #144).
143
- // resolveConfig() re-reads the small file each time — negligible I/O
144
- // compared to DB queries and embedding operations that follow.
145
128
  let lastVaultDir = config.vaultDir;
146
129
  Object.defineProperty(ctx, "config", {
147
130
  get() {
@@ -160,7 +143,6 @@ async function main() {
160
143
 
161
144
  registerTools(server, ctx);
162
145
 
163
- // ── Graceful Shutdown ────────────────────────────────────────────────────
164
146
  function closeDb() {
165
147
  try {
166
148
  if (db.inTransaction) {
@@ -189,7 +171,6 @@ async function main() {
189
171
  closeDb();
190
172
  }
191
173
  }, 100);
192
- // Force shutdown after 5 seconds even if ops are still running
193
174
  setTimeout(() => {
194
175
  clearInterval(check);
195
176
  console.error(
@@ -204,12 +185,10 @@ async function main() {
204
185
  process.on("SIGINT", () => shutdown("SIGINT"));
205
186
  process.on("SIGTERM", () => shutdown("SIGTERM"));
206
187
 
207
- // ── Phase: CONNECTED ─────────────────────────────────────────────────────
208
188
  phase = "CONNECTED";
209
189
  const transport = new StdioServerTransport();
210
190
  await server.connect(transport);
211
191
 
212
- // ── Non-blocking Update Check ────────────────────────────────────────────
213
192
  setTimeout(() => {
214
193
  import("node:child_process")
215
194
  .then(({ execSync }) => {
@@ -251,7 +230,6 @@ async function main() {
251
230
  });
252
231
 
253
232
  if (err instanceof NativeModuleError) {
254
- // Boxed diagnostic for native module mismatch
255
233
  console.error("");
256
234
  console.error(
257
235
  "╔══════════════════════════════════════════════════════════════╗",
@@ -269,7 +247,7 @@ async function main() {
269
247
  console.error(` Node.js version: ${process.version}`);
270
248
  console.error(` Error log: ${join(dataDir, "error.log")}`);
271
249
  console.error("");
272
- process.exit(78); // EX_CONFIG
250
+ process.exit(78);
273
251
  }
274
252
 
275
253
  console.error(
@@ -285,12 +263,6 @@ async function main() {
285
263
  }
286
264
  }
287
265
 
288
- // ─── Top-level Safety Net ────────────────────────────────────────────────────
289
- // Catch any errors that escape the main() try/catch (e.g. thrown in MCP
290
- // transport callbacks or in unrelated async chains). Claude Code surfaces
291
- // stderr when a server exits unexpectedly, so every message written here will
292
- // be visible to the user.
293
-
294
266
  process.on("uncaughtException", (err) => {
295
267
  const dataDir = join(homedir(), ".context-mcp");
296
268
  const logEntry = {
package/src/status.js ADDED
@@ -0,0 +1,235 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { walkDir } from "@context-vault/core/files";
4
+ import { isEmbedAvailable } from "@context-vault/core/embed";
5
+ import { KIND_STALENESS_DAYS } from "@context-vault/core/categories";
6
+
7
+ function countArchivedEntries(vaultDir) {
8
+ const archRoot = join(vaultDir, "_archive");
9
+ if (!existsSync(archRoot)) return 0;
10
+ try {
11
+ return walkDir(archRoot).length;
12
+ } catch {
13
+ return 0;
14
+ }
15
+ }
16
+
17
+ export function gatherVaultStatus(ctx, opts = {}) {
18
+ const { db, config } = ctx;
19
+ const errors = [];
20
+
21
+ let fileCount = 0;
22
+ const subdirs = [];
23
+ try {
24
+ if (existsSync(config.vaultDir)) {
25
+ for (const d of readdirSync(config.vaultDir, { withFileTypes: true })) {
26
+ if (d.isDirectory()) {
27
+ const dir = join(config.vaultDir, d.name);
28
+ const count = walkDir(dir).length;
29
+ fileCount += count;
30
+ if (count > 0) subdirs.push({ name: d.name, count });
31
+ }
32
+ }
33
+ }
34
+ } catch (e) {
35
+ errors.push(`File scan failed: ${e.message}`);
36
+ }
37
+
38
+ let kindCounts = [];
39
+ try {
40
+ kindCounts = db
41
+ .prepare(`SELECT kind, COUNT(*) as c FROM vault GROUP BY kind`)
42
+ .all();
43
+ } catch (e) {
44
+ errors.push(`Kind count query failed: ${e.message}`);
45
+ }
46
+
47
+ let categoryCounts = [];
48
+ try {
49
+ categoryCounts = db
50
+ .prepare(`SELECT category, COUNT(*) as c FROM vault GROUP BY category`)
51
+ .all();
52
+ } catch (e) {
53
+ errors.push(`Category count query failed: ${e.message}`);
54
+ }
55
+
56
+ let dbSize = "n/a";
57
+ let dbSizeBytes = 0;
58
+ try {
59
+ if (existsSync(config.dbPath)) {
60
+ dbSizeBytes = statSync(config.dbPath).size;
61
+ dbSize =
62
+ dbSizeBytes > 1024 * 1024
63
+ ? `${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB`
64
+ : `${(dbSizeBytes / 1024).toFixed(1)}KB`;
65
+ }
66
+ } catch (e) {
67
+ errors.push(`DB size check failed: ${e.message}`);
68
+ }
69
+
70
+ let stalePaths = false;
71
+ let staleCount = 0;
72
+ try {
73
+ const result = db
74
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%'`)
75
+ .get(config.vaultDir);
76
+ staleCount = result.c;
77
+ stalePaths = staleCount > 0;
78
+ } catch (e) {
79
+ errors.push(`Stale path check failed: ${e.message}`);
80
+ }
81
+
82
+ let expiredCount = 0;
83
+ try {
84
+ expiredCount = db
85
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now')`)
86
+ .get().c;
87
+ } catch (e) {
88
+ errors.push(`Expired count failed: ${e.message}`);
89
+ }
90
+
91
+ let eventCount = 0;
92
+ try {
93
+ eventCount = db
94
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE category = 'event'`)
95
+ .get().c;
96
+ } catch (e) {
97
+ errors.push(`Event count failed: ${e.message}`);
98
+ }
99
+
100
+ let eventsWithoutTtlCount = 0;
101
+ try {
102
+ eventsWithoutTtlCount = db
103
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE category = 'event' AND expires_at IS NULL`)
104
+ .get().c;
105
+ } catch (e) {
106
+ errors.push(`Events without TTL count failed: ${e.message}`);
107
+ }
108
+
109
+ let embeddingStatus = null;
110
+ try {
111
+ const total = db.prepare(`SELECT COUNT(*) as c FROM vault`).get().c;
112
+ const indexed = db
113
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec)`)
114
+ .get().c;
115
+ embeddingStatus = { indexed, total, missing: total - indexed };
116
+ } catch (e) {
117
+ errors.push(`Embedding status check failed: ${e.message}`);
118
+ }
119
+
120
+ const embedModelAvailable = isEmbedAvailable();
121
+
122
+ let autoCapturedFeedbackCount = 0;
123
+ try {
124
+ autoCapturedFeedbackCount = db
125
+ .prepare(`SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%'`)
126
+ .get().c;
127
+ } catch (e) {
128
+ errors.push(`Auto-captured feedback count failed: ${e.message}`);
129
+ }
130
+
131
+ let archivedCount = 0;
132
+ try {
133
+ archivedCount = countArchivedEntries(config.vaultDir);
134
+ } catch (e) {
135
+ errors.push(`Archived count failed: ${e.message}`);
136
+ }
137
+
138
+ let staleKnowledge = [];
139
+ try {
140
+ const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
141
+ if (stalenessKinds.length > 0) {
142
+ const kindClauses = stalenessKinds
143
+ .map(
144
+ ([kind, days]) =>
145
+ `(kind = '${kind}' AND COALESCE(updated_at, created_at) <= datetime('now', '-${days} days'))`,
146
+ )
147
+ .join(" OR ");
148
+ staleKnowledge = db
149
+ .prepare(
150
+ `SELECT kind, title, COALESCE(updated_at, created_at) as last_updated FROM vault WHERE category = 'knowledge' AND (${kindClauses}) AND (expires_at IS NULL OR expires_at > datetime('now')) ORDER BY last_updated ASC LIMIT 10`,
151
+ )
152
+ .all();
153
+ }
154
+ } catch (e) {
155
+ errors.push(`Stale knowledge check failed: ${e.message}`);
156
+ }
157
+
158
+ return {
159
+ fileCount,
160
+ subdirs,
161
+ kindCounts,
162
+ categoryCounts,
163
+ dbSize,
164
+ dbSizeBytes,
165
+ stalePaths,
166
+ staleCount,
167
+ expiredCount,
168
+ eventCount,
169
+ eventsWithoutTtlCount,
170
+ embeddingStatus,
171
+ embedModelAvailable,
172
+ autoCapturedFeedbackCount,
173
+ archivedCount,
174
+ staleKnowledge,
175
+ resolvedFrom: config.resolvedFrom,
176
+ errors,
177
+ };
178
+ }
179
+
180
+ export function computeGrowthWarnings(status, thresholds) {
181
+ if (!thresholds)
182
+ return { warnings: [], hasCritical: false, hasWarnings: false, actions: [], kindBreakdown: [] };
183
+
184
+ const t = thresholds;
185
+ const warnings = [];
186
+ const actions = [];
187
+
188
+ const total = status.embeddingStatus?.total ?? 0;
189
+ const { eventCount = 0, eventsWithoutTtlCount = 0, expiredCount = 0, dbSizeBytes = 0 } = status;
190
+
191
+ let totalExceeded = false;
192
+
193
+ if (t.totalEntries?.critical != null && total >= t.totalEntries.critical) {
194
+ totalExceeded = true;
195
+ warnings.push({ level: "critical", message: `Total entries: ${total.toLocaleString()} (exceeds critical limit of ${t.totalEntries.critical.toLocaleString()})` });
196
+ } else if (t.totalEntries?.warn != null && total >= t.totalEntries.warn) {
197
+ totalExceeded = true;
198
+ warnings.push({ level: "warn", message: `Total entries: ${total.toLocaleString()} (exceeds recommended ${t.totalEntries.warn.toLocaleString()})` });
199
+ }
200
+
201
+ if (t.eventEntries?.critical != null && eventCount >= t.eventEntries.critical) {
202
+ warnings.push({ level: "critical", message: `Event entries: ${eventCount.toLocaleString()} (exceeds critical limit of ${t.eventEntries.critical.toLocaleString()})` });
203
+ } else if (t.eventEntries?.warn != null && eventCount >= t.eventEntries.warn) {
204
+ const ttlNote = eventsWithoutTtlCount > 0 ? ` (${eventsWithoutTtlCount.toLocaleString()} without TTL)` : "";
205
+ warnings.push({ level: "warn", message: `Event entries: ${eventCount.toLocaleString()}${ttlNote} (exceeds recommended ${t.eventEntries.warn.toLocaleString()})` });
206
+ }
207
+
208
+ if (t.vaultSizeBytes?.critical != null && dbSizeBytes >= t.vaultSizeBytes.critical) {
209
+ warnings.push({ level: "critical", message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds critical limit of ${(t.vaultSizeBytes.critical / 1024 / 1024).toFixed(0)}MB)` });
210
+ } else if (t.vaultSizeBytes?.warn != null && dbSizeBytes >= t.vaultSizeBytes.warn) {
211
+ warnings.push({ level: "warn", message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds recommended ${(t.vaultSizeBytes.warn / 1024 / 1024).toFixed(0)}MB)` });
212
+ }
213
+
214
+ if (t.eventsWithoutTtl?.warn != null && eventsWithoutTtlCount >= t.eventsWithoutTtl.warn) {
215
+ warnings.push({ level: "warn", message: `Event entries without expires_at: ${eventsWithoutTtlCount.toLocaleString()} (exceeds recommended ${t.eventsWithoutTtl.warn.toLocaleString()})` });
216
+ }
217
+
218
+ const hasCritical = warnings.some((w) => w.level === "critical");
219
+
220
+ if (expiredCount > 0) {
221
+ actions.push(`Run \`context-vault prune\` to remove ${expiredCount} expired event entr${expiredCount === 1 ? "y" : "ies"}`);
222
+ }
223
+ if (eventsWithoutTtlCount > 0 && (eventCount >= (t.eventEntries?.warn ?? Infinity) || eventsWithoutTtlCount >= (t.eventsWithoutTtl?.warn ?? Infinity))) {
224
+ actions.push("Add `expires_at` to event/session entries to enable automatic cleanup");
225
+ }
226
+ if (total >= (t.totalEntries?.warn ?? Infinity)) {
227
+ actions.push("Run `context-vault archive` to move old ephemeral/event entries to _archive/");
228
+ }
229
+
230
+ const kindBreakdown = totalExceeded && status.kindCounts?.length
231
+ ? [...status.kindCounts].sort((a, b) => b.c - a.c).map(({ kind, c }) => ({ kind, count: c, pct: Math.round((c / total) * 100) }))
232
+ : [];
233
+
234
+ return { warnings, hasCritical, hasWarnings: warnings.length > 0, actions, kindBreakdown };
235
+ }
@@ -1,23 +1,22 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "../constants.js";
3
+ import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "@context-vault/core/constants";
4
+ import type { VaultConfig } from "@context-vault/core/types";
4
5
 
5
6
  const TELEMETRY_ENDPOINT = `${API_URL}/telemetry`;
6
7
  const NOTICE_MARKER = ".telemetry-notice-shown";
7
8
  const FEEDBACK_PROMPT_MARKER = ".feedback-prompt-shown";
8
9
 
9
- export function isTelemetryEnabled(config) {
10
+ export function isTelemetryEnabled(config: VaultConfig | undefined): boolean {
10
11
  const envVal = process.env.CONTEXT_VAULT_TELEMETRY;
11
12
  if (envVal !== undefined) return envVal === "1" || envVal === "true";
12
13
  return config?.telemetry === true;
13
14
  }
14
15
 
15
- /**
16
- * Fire-and-forget telemetry event. Never throws, never blocks.
17
- * Payload contains only: event, code, tool, cv_version, node_version, platform, arch, ts.
18
- * No message text, stack traces, vault content, file paths, or user identifiers.
19
- */
20
- export function sendTelemetryEvent(config, payload) {
16
+ export function sendTelemetryEvent(
17
+ config: VaultConfig | undefined,
18
+ payload: { event: string; code?: string | null; tool?: string | null; cv_version: string },
19
+ ): void {
21
20
  if (!isTelemetryEnabled(config)) return;
22
21
 
23
22
  const event = {
@@ -39,11 +38,7 @@ export function sendTelemetryEvent(config, payload) {
39
38
  }).catch(() => {});
40
39
  }
41
40
 
42
- /**
43
- * Print the one-time telemetry notice to stderr.
44
- * Uses a marker file in dataDir to ensure it's only shown once.
45
- */
46
- export function maybeShowTelemetryNotice(dataDir) {
41
+ export function maybeShowTelemetryNotice(dataDir: string): void {
47
42
  try {
48
43
  const markerPath = join(dataDir, NOTICE_MARKER);
49
44
  if (existsSync(markerPath)) return;
@@ -65,12 +60,7 @@ export function maybeShowTelemetryNotice(dataDir) {
65
60
  }
66
61
  }
67
62
 
68
- /**
69
- * Print a one-time feedback prompt after the user's first successful save.
70
- * Uses a marker file in dataDir to ensure it's only shown once.
71
- * Never throws, never blocks.
72
- */
73
- export function maybeShowFeedbackPrompt(dataDir) {
63
+ export function maybeShowFeedbackPrompt(dataDir: string): void {
74
64
  try {
75
65
  const markerPath = join(dataDir, FEEDBACK_PROMPT_MARKER);
76
66
  if (existsSync(markerPath)) return;