context-vault 2.17.0 → 3.0.1

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 +783 -108
  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/files.ts +80 -0
  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} +27 -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/search.ts +285 -0
  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 -15
  79. package/src/{server/index.js → server.js} +8 -35
  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 +43 -75
  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 +118 -35
  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 +41 -21
  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 -97
  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 -236
  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 -72
  100. package/node_modules/@context-vault/core/src/core/files.js +0 -108
  101. package/node_modules/@context-vault/core/src/core/status.js +0 -350
  102. package/node_modules/@context-vault/core/src/index/db.js +0 -416
  103. package/node_modules/@context-vault/core/src/index/index.js +0 -522
  104. package/node_modules/@context-vault/core/src/index.js +0 -66
  105. package/node_modules/@context-vault/core/src/retrieve/index.js +0 -500
  106. package/node_modules/@context-vault/core/src/server/tools/submit-feedback.js +0 -55
  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
@@ -1,14 +1,20 @@
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";
9
16
  import * as listContext from "./tools/list-context.js";
10
17
  import * as deleteContext from "./tools/delete-context.js";
11
- import * as submitFeedback from "./tools/submit-feedback.js";
12
18
  import * as ingestUrl from "./tools/ingest-url.js";
13
19
  import * as contextStatus from "./tools/context-status.js";
14
20
  import * as clearContext from "./tools/clear-context.js";
@@ -22,7 +28,6 @@ const toolModules = [
22
28
  saveContext,
23
29
  listContext,
24
30
  deleteContext,
25
- submitFeedback,
26
31
  ingestUrl,
27
32
  ingestProject,
28
33
  contextStatus,
@@ -35,8 +40,6 @@ const toolModules = [
35
40
  const TOOL_TIMEOUT_MS = 60_000;
36
41
 
37
42
  export function registerTools(server, ctx) {
38
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
39
-
40
43
  function tracked(handler, toolName) {
41
44
  return async (...args) => {
42
45
  if (ctx.activeOps) ctx.activeOps.count++;
@@ -57,8 +60,6 @@ export function registerTools(server, ctx) {
57
60
  return result;
58
61
  } catch (e) {
59
62
  if (e.message === "TOOL_TIMEOUT") {
60
- // Suppress any late rejection from the still-running handler to
61
- // prevent unhandled promise rejection warnings in the host process.
62
63
  handlerPromise?.catch(() => {});
63
64
  if (ctx.toolStats) {
64
65
  ctx.toolStats.errors++;
@@ -107,7 +108,7 @@ export function registerTools(server, ctx) {
107
108
  auto: true,
108
109
  },
109
110
  });
110
- } catch {} // never block on feedback capture
111
+ } catch {}
111
112
  throw e;
112
113
  } finally {
113
114
  clearTimeout(timer);
@@ -116,8 +117,7 @@ export function registerTools(server, ctx) {
116
117
  };
117
118
  }
118
119
 
119
- // In hosted mode, skip reindex — DB is always in sync via writeEntry→indexEntry
120
- let reindexDone = userId !== undefined ? true : false;
120
+ let reindexDone = false;
121
121
  let reindexPromise = null;
122
122
  let reindexAttempts = 0;
123
123
  let reindexFailed = false;
@@ -126,7 +126,6 @@ export function registerTools(server, ctx) {
126
126
  async function ensureIndexed() {
127
127
  if (reindexDone) return;
128
128
  if (reindexPromise) return reindexPromise;
129
- // Assign promise synchronously to prevent concurrent calls from both entering reindex()
130
129
  const promise = reindex(ctx, { fullSync: true })
131
130
  .then((stats) => {
132
131
  reindexDone = true;
@@ -149,7 +148,7 @@ export function registerTools(server, ctx) {
149
148
  reindexDone = true;
150
149
  reindexFailed = true;
151
150
  } else {
152
- reindexPromise = null; // Allow retry on next tool call
151
+ reindexPromise = null;
153
152
  }
154
153
  });
155
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,7 +91,6 @@ 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
95
  db = await initDatabase(config.dbPath);
107
96
  const stmts = prepareStatements(db);
@@ -117,7 +106,6 @@ async function main() {
117
106
  toolStats: { ok: 0, errors: 0, lastError: null },
118
107
  };
119
108
 
120
- // ── Phase: PRUNE ─────────────────────────────────────────────────────────
121
109
  try {
122
110
  const pruned = await pruneExpired(ctx);
123
111
  if (pruned > 0) {
@@ -131,16 +119,12 @@ async function main() {
131
119
  );
132
120
  }
133
121
 
134
- // ── Phase: SERVER ────────────────────────────────────────────────────────
135
122
  phase = "SERVER";
136
123
  const server = new McpServer(
137
124
  { name: "context-vault", version: pkg.version },
138
125
  { capabilities: { tools: {} } },
139
126
  );
140
127
 
141
- // Hot-reload config.json on every tool call (Option C from #144).
142
- // resolveConfig() re-reads the small file each time — negligible I/O
143
- // compared to DB queries and embedding operations that follow.
144
128
  let lastVaultDir = config.vaultDir;
145
129
  Object.defineProperty(ctx, "config", {
146
130
  get() {
@@ -159,7 +143,6 @@ async function main() {
159
143
 
160
144
  registerTools(server, ctx);
161
145
 
162
- // ── Graceful Shutdown ────────────────────────────────────────────────────
163
146
  function closeDb() {
164
147
  try {
165
148
  if (db.inTransaction) {
@@ -188,7 +171,6 @@ async function main() {
188
171
  closeDb();
189
172
  }
190
173
  }, 100);
191
- // Force shutdown after 5 seconds even if ops are still running
192
174
  setTimeout(() => {
193
175
  clearInterval(check);
194
176
  console.error(
@@ -203,12 +185,10 @@ async function main() {
203
185
  process.on("SIGINT", () => shutdown("SIGINT"));
204
186
  process.on("SIGTERM", () => shutdown("SIGTERM"));
205
187
 
206
- // ── Phase: CONNECTED ─────────────────────────────────────────────────────
207
188
  phase = "CONNECTED";
208
189
  const transport = new StdioServerTransport();
209
190
  await server.connect(transport);
210
191
 
211
- // ── Non-blocking Update Check ────────────────────────────────────────────
212
192
  setTimeout(() => {
213
193
  import("node:child_process")
214
194
  .then(({ execSync }) => {
@@ -250,7 +230,6 @@ async function main() {
250
230
  });
251
231
 
252
232
  if (err instanceof NativeModuleError) {
253
- // Boxed diagnostic for native module mismatch
254
233
  console.error("");
255
234
  console.error(
256
235
  "╔══════════════════════════════════════════════════════════════╗",
@@ -268,7 +247,7 @@ async function main() {
268
247
  console.error(` Node.js version: ${process.version}`);
269
248
  console.error(` Error log: ${join(dataDir, "error.log")}`);
270
249
  console.error("");
271
- process.exit(78); // EX_CONFIG
250
+ process.exit(78);
272
251
  }
273
252
 
274
253
  console.error(
@@ -284,12 +263,6 @@ async function main() {
284
263
  }
285
264
  }
286
265
 
287
- // ─── Top-level Safety Net ────────────────────────────────────────────────────
288
- // Catch any errors that escape the main() try/catch (e.g. thrown in MCP
289
- // transport callbacks or in unrelated async chains). Claude Code surfaces
290
- // stderr when a server exits unexpectedly, so every message written here will
291
- // be visible to the user.
292
-
293
266
  process.on("uncaughtException", (err) => {
294
267
  const dataDir = join(homedir(), ".context-mcp");
295
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;
@@ -0,0 +1,97 @@
1
+ const SHORTCUT_RE = /^last[_ ](\d+)[_ ](day|days|week|weeks|month|months)$/i;
2
+
3
+ function startOfToday(now: Date): Date {
4
+ const d = new Date(now);
5
+ d.setUTCHours(0, 0, 0, 0);
6
+ return d;
7
+ }
8
+
9
+ export function resolveTemporalShortcut(
10
+ role: "since" | "until",
11
+ value: string,
12
+ now: Date = new Date(),
13
+ ): string {
14
+ if (!value || typeof value !== "string") return value;
15
+ const trimmed = value.trim().toLowerCase().replace(/\s+/g, "_");
16
+
17
+ if (trimmed === "today") {
18
+ const start = startOfToday(now);
19
+ if (role === "until") {
20
+ const end = new Date(start);
21
+ end.setUTCDate(end.getUTCDate() + 1);
22
+ return end.toISOString();
23
+ }
24
+ return start.toISOString();
25
+ }
26
+
27
+ if (trimmed === "yesterday") {
28
+ const todayStart = startOfToday(now);
29
+ const yesterdayStart = new Date(todayStart);
30
+ yesterdayStart.setUTCDate(yesterdayStart.getUTCDate() - 1);
31
+ if (role === "since") return yesterdayStart.toISOString();
32
+ return todayStart.toISOString();
33
+ }
34
+
35
+ if (trimmed === "this_week") {
36
+ const todayStart = startOfToday(now);
37
+ const dayOfWeek = todayStart.getUTCDay();
38
+ const daysFromMonday = (dayOfWeek + 6) % 7;
39
+ const monday = new Date(todayStart);
40
+ monday.setUTCDate(monday.getUTCDate() - daysFromMonday);
41
+ if (role === "since") return monday.toISOString();
42
+ const endOfToday = new Date(todayStart);
43
+ endOfToday.setUTCDate(endOfToday.getUTCDate() + 1);
44
+ return endOfToday.toISOString();
45
+ }
46
+
47
+ if (trimmed === "this_month") {
48
+ const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
49
+ if (role === "since") return d.toISOString();
50
+ const endOfMonth = new Date(
51
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1),
52
+ );
53
+ return endOfMonth.toISOString();
54
+ }
55
+
56
+ const m = SHORTCUT_RE.exec(trimmed);
57
+ if (m) {
58
+ const n = parseInt(m[1], 10);
59
+ const unit = m[2].replace(/s$/, "");
60
+ let ms: number;
61
+ if (unit === "day") ms = n * 86400000;
62
+ else if (unit === "week") ms = n * 7 * 86400000;
63
+ else ms = n * 30 * 86400000;
64
+ const target = new Date(now.getTime() - ms);
65
+ target.setUTCHours(0, 0, 0, 0);
66
+ return target.toISOString();
67
+ }
68
+
69
+ return value;
70
+ }
71
+
72
+ export function resolveTemporalParams(
73
+ params: { since?: string; until?: string },
74
+ now: Date = new Date(),
75
+ ): { since: string | undefined; until: string | undefined } {
76
+ let { since, until } = params;
77
+
78
+ if (
79
+ since?.trim().toLowerCase() === "yesterday" &&
80
+ (until === undefined || until === null)
81
+ ) {
82
+ since = resolveTemporalShortcut("since", since, now);
83
+ until = resolveTemporalShortcut("until", "yesterday", now);
84
+ return { since, until };
85
+ }
86
+
87
+ return {
88
+ since:
89
+ since !== undefined
90
+ ? resolveTemporalShortcut("since", since, now)
91
+ : since,
92
+ until:
93
+ until !== undefined
94
+ ? resolveTemporalShortcut("until", until, now)
95
+ : until,
96
+ };
97
+ }
@@ -1,5 +1,5 @@
1
- import { gatherVaultStatus, computeGrowthWarnings } from "../../core/status.js";
2
- import { errorLogPath, errorLogCount } from "../../core/error-log.js";
1
+ import { gatherVaultStatus, computeGrowthWarnings } from "../status.js";
2
+ import { errorLogPath, errorLogCount } from "../error-log.js";
3
3
  import { ok } from "../helpers.js";
4
4
 
5
5
  function relativeTime(ts) {
@@ -24,9 +24,8 @@ export const inputSchema = {};
24
24
  */
25
25
  export function handler(_args, ctx) {
26
26
  const { config } = ctx;
27
- const userId = ctx.userId !== undefined ? ctx.userId : undefined;
28
27
 
29
- const status = gatherVaultStatus(ctx, { userId });
28
+ const status = gatherVaultStatus(ctx);
30
29
 
31
30
  const hasIssues = status.stalePaths || status.embeddingStatus?.missing > 0;
32
31
  const healthIcon = hasIssues ? "⚠" : "✓";