@context-vault/core 2.17.1 → 3.0.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.
Files changed (101) hide show
  1. package/dist/capture.d.ts +21 -0
  2. package/dist/capture.d.ts.map +1 -0
  3. package/dist/capture.js +269 -0
  4. package/dist/capture.js.map +1 -0
  5. package/dist/categories.d.ts +6 -0
  6. package/dist/categories.d.ts.map +1 -0
  7. package/dist/categories.js +50 -0
  8. package/dist/categories.js.map +1 -0
  9. package/dist/config.d.ts +4 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +190 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/constants.d.ts +33 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +23 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/db.d.ts +13 -0
  18. package/dist/db.d.ts.map +1 -0
  19. package/dist/db.js +191 -0
  20. package/dist/db.js.map +1 -0
  21. package/dist/embed.d.ts +5 -0
  22. package/dist/embed.d.ts.map +1 -0
  23. package/dist/embed.js +78 -0
  24. package/dist/embed.js.map +1 -0
  25. package/dist/files.d.ts +13 -0
  26. package/dist/files.d.ts.map +1 -0
  27. package/dist/files.js +66 -0
  28. package/dist/files.js.map +1 -0
  29. package/dist/formatters.d.ts +8 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +18 -0
  32. package/dist/formatters.js.map +1 -0
  33. package/dist/frontmatter.d.ts +12 -0
  34. package/dist/frontmatter.d.ts.map +1 -0
  35. package/dist/frontmatter.js +101 -0
  36. package/dist/frontmatter.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +297 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/ingest-url.d.ts +20 -0
  42. package/dist/ingest-url.d.ts.map +1 -0
  43. package/dist/ingest-url.js +113 -0
  44. package/dist/ingest-url.js.map +1 -0
  45. package/dist/main.d.ts +14 -0
  46. package/dist/main.d.ts.map +1 -0
  47. package/dist/main.js +25 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/search.d.ts +18 -0
  50. package/dist/search.d.ts.map +1 -0
  51. package/dist/search.js +238 -0
  52. package/dist/search.js.map +1 -0
  53. package/dist/types.d.ts +176 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +66 -16
  58. package/src/capture.ts +308 -0
  59. package/src/categories.ts +54 -0
  60. package/src/{core/config.js → config.ts} +34 -33
  61. package/src/{constants.js → constants.ts} +6 -3
  62. package/src/db.ts +229 -0
  63. package/src/{index/embed.js → embed.ts} +10 -35
  64. package/src/{core/files.js → files.ts} +15 -20
  65. package/src/{capture/formatters.js → formatters.ts} +13 -11
  66. package/src/{core/frontmatter.js → frontmatter.ts} +26 -33
  67. package/src/index.ts +351 -0
  68. package/src/ingest-url.ts +99 -0
  69. package/src/main.ts +111 -0
  70. package/src/{retrieve/index.js → search.ts} +62 -150
  71. package/src/types.ts +166 -0
  72. package/src/capture/file-ops.js +0 -99
  73. package/src/capture/import-pipeline.js +0 -46
  74. package/src/capture/importers.js +0 -387
  75. package/src/capture/index.js +0 -250
  76. package/src/capture/ingest-url.js +0 -252
  77. package/src/consolidation/index.js +0 -112
  78. package/src/core/categories.js +0 -73
  79. package/src/core/error-log.js +0 -54
  80. package/src/core/linking.js +0 -161
  81. package/src/core/migrate-dirs.js +0 -196
  82. package/src/core/status.js +0 -350
  83. package/src/core/telemetry.js +0 -90
  84. package/src/core/temporal.js +0 -146
  85. package/src/index/db.js +0 -586
  86. package/src/index/index.js +0 -583
  87. package/src/index.js +0 -71
  88. package/src/server/helpers.js +0 -44
  89. package/src/server/tools/clear-context.js +0 -47
  90. package/src/server/tools/context-status.js +0 -182
  91. package/src/server/tools/create-snapshot.js +0 -200
  92. package/src/server/tools/delete-context.js +0 -60
  93. package/src/server/tools/get-context.js +0 -765
  94. package/src/server/tools/ingest-project.js +0 -244
  95. package/src/server/tools/ingest-url.js +0 -88
  96. package/src/server/tools/list-buckets.js +0 -116
  97. package/src/server/tools/list-context.js +0 -163
  98. package/src/server/tools/save-context.js +0 -632
  99. package/src/server/tools/session-start.js +0 -285
  100. package/src/server/tools.js +0 -172
  101. package/src/sync/sync.js +0 -235
@@ -1,196 +0,0 @@
1
- /**
2
- * migrate-dirs.js — Rename plural vault directories to singular
3
- *
4
- * After context-vault >= 2.18.0, kindToDir() returns singular names.
5
- * Existing vaults still have plural dirs (e.g. knowledge/decisions/).
6
- * This module plans and executes the rename/merge migration.
7
- *
8
- * Architecture: pure planning function + I/O execution function.
9
- */
10
-
11
- import {
12
- existsSync,
13
- readdirSync,
14
- mkdirSync,
15
- renameSync,
16
- copyFileSync,
17
- rmSync,
18
- statSync,
19
- } from "node:fs";
20
- import { join, basename } from "node:path";
21
-
22
- /**
23
- * Complete plural→singular mapping for vault directory names.
24
- * Covers the old PLURAL_MAP from files.js plus extended kinds seen in the wild.
25
- *
26
- * @type {Record<string, string>}
27
- */
28
- export const PLURAL_TO_SINGULAR = {
29
- // From old PLURAL_MAP in files.js
30
- insights: "insight",
31
- decisions: "decision",
32
- patterns: "pattern",
33
- statuses: "status",
34
- analyses: "analysis",
35
- contacts: "contact",
36
- projects: "project",
37
- tools: "tool",
38
- sources: "source",
39
- conversations: "conversation",
40
- messages: "message",
41
- sessions: "session",
42
- logs: "log",
43
- feedbacks: "feedback",
44
- // Extended kinds from categories.js + observed in vaults
45
- notes: "note",
46
- prompts: "prompt",
47
- documents: "document",
48
- references: "reference",
49
- tasks: "task",
50
- buckets: "bucket",
51
- architectures: "architecture",
52
- briefs: "brief",
53
- companies: "company",
54
- discoveries: "discovery",
55
- events: "event",
56
- ideas: "idea",
57
- issues: "issue",
58
- agents: "agent",
59
- "session-summaries": "session-summary",
60
- "session-reviews": "session-review",
61
- "user-prompts": "user-prompt",
62
- };
63
-
64
- /**
65
- * Category directory names that are scanned for plural kind subdirectories.
66
- */
67
- const CATEGORY_DIRS = ["knowledge", "entities", "events"];
68
-
69
- /**
70
- * Count .md files recursively in a directory.
71
- *
72
- * @param {string} dir
73
- * @returns {number}
74
- */
75
- function countMdFiles(dir) {
76
- let count = 0;
77
- try {
78
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
79
- if (entry.isDirectory()) {
80
- count += countMdFiles(join(dir, entry.name));
81
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
82
- count++;
83
- }
84
- }
85
- } catch {
86
- // ignore unreadable dirs
87
- }
88
- return count;
89
- }
90
-
91
- /**
92
- * Plan migration operations: walk the vault and detect plural dirs that need renaming.
93
- * Pure I/O read-only — does not modify any files.
94
- *
95
- * @param {string} vaultDir - Absolute path to vault root
96
- * @returns {MigrationOp[]} Array of planned operations
97
- *
98
- * @typedef {{ action: 'rename'|'merge', pluralDir: string, singularDir: string, pluralName: string, singularName: string, fileCount: number }} MigrationOp
99
- */
100
- export function planMigration(vaultDir) {
101
- const ops = [];
102
-
103
- for (const catName of CATEGORY_DIRS) {
104
- const catDir = join(vaultDir, catName);
105
- if (!existsSync(catDir) || !statSync(catDir).isDirectory()) continue;
106
-
107
- let entries;
108
- try {
109
- entries = readdirSync(catDir, { withFileTypes: true });
110
- } catch {
111
- continue;
112
- }
113
-
114
- for (const entry of entries) {
115
- if (!entry.isDirectory()) continue;
116
- const dirName = entry.name;
117
- const singular = PLURAL_TO_SINGULAR[dirName];
118
-
119
- // Not a known plural — skip (might already be singular or unknown kind)
120
- if (!singular) continue;
121
-
122
- const pluralDir = join(catDir, dirName);
123
- const singularDir = join(catDir, singular);
124
-
125
- // Guard: plural and singular are the same (shouldn't happen but be safe)
126
- if (pluralDir === singularDir) continue;
127
-
128
- const fileCount = countMdFiles(pluralDir);
129
- const singularExists = existsSync(singularDir);
130
-
131
- ops.push({
132
- action: singularExists ? "merge" : "rename",
133
- pluralDir,
134
- singularDir,
135
- pluralName: dirName,
136
- singularName: singular,
137
- fileCount,
138
- });
139
- }
140
- }
141
-
142
- return ops;
143
- }
144
-
145
- /**
146
- * Copy all files and subdirectories from src into dst (non-overwriting).
147
- * Mirrors `cp -rn src/* dst/`.
148
- *
149
- * @param {string} src
150
- * @param {string} dst
151
- */
152
- function mergeDir(src, dst) {
153
- mkdirSync(dst, { recursive: true });
154
- for (const entry of readdirSync(src, { withFileTypes: true })) {
155
- const srcPath = join(src, entry.name);
156
- const dstPath = join(dst, entry.name);
157
- if (entry.isDirectory()) {
158
- mergeDir(srcPath, dstPath);
159
- } else if (entry.isFile()) {
160
- if (!existsSync(dstPath)) {
161
- copyFileSync(srcPath, dstPath);
162
- }
163
- }
164
- }
165
- }
166
-
167
- /**
168
- * Execute migration operations. Renames or merges plural dirs into singular dirs.
169
- * Safe to call multiple times — already-renamed dirs produce no-op ops from planMigration.
170
- *
171
- * @param {MigrationOp[]} ops - Operations from planMigration()
172
- * @returns {{ renamed: number, merged: number, errors: string[] }}
173
- */
174
- export function executeMigration(ops) {
175
- let renamed = 0;
176
- let merged = 0;
177
- const errors = [];
178
-
179
- for (const op of ops) {
180
- try {
181
- if (op.action === "rename") {
182
- renameSync(op.pluralDir, op.singularDir);
183
- renamed++;
184
- } else {
185
- // merge: copy files from plural into singular, then remove plural
186
- mergeDir(op.pluralDir, op.singularDir);
187
- rmSync(op.pluralDir, { recursive: true, force: true });
188
- merged++;
189
- }
190
- } catch (e) {
191
- errors.push(`${op.pluralName} → ${op.singularName}: ${e.message}`);
192
- }
193
- }
194
-
195
- return { renamed, merged, errors };
196
- }
@@ -1,350 +0,0 @@
1
- /**
2
- * status.js — Vault status/diagnostics data gathering
3
- */
4
-
5
- import { existsSync, readdirSync, statSync } from "node:fs";
6
- import { join } from "node:path";
7
- import { walkDir } from "./files.js";
8
- import { isEmbedAvailable } from "../index/embed.js";
9
- import { KIND_STALENESS_DAYS } from "./categories.js";
10
-
11
- /**
12
- * Gather raw vault status data for formatting by consumers.
13
- *
14
- * @param {import('../server/types.js').BaseCtx} ctx
15
- * @param {{ userId?: string }} opts — optional userId for per-user stats
16
- * @returns {{ fileCount, subdirs, kindCounts, dbSize, stalePaths, resolvedFrom, embeddingStatus, errors }}
17
- */
18
- export function gatherVaultStatus(ctx, opts = {}) {
19
- const { db, config } = ctx;
20
- const { userId } = opts;
21
- const errors = [];
22
-
23
- // Build user filter clause for DB queries
24
- const hasUser = userId !== undefined;
25
- const userWhere = hasUser ? "WHERE user_id = ?" : "";
26
- const userAnd = hasUser ? "AND user_id = ?" : "";
27
- const userParams = hasUser ? [userId] : [];
28
-
29
- // Count files in vault subdirs (auto-discover)
30
- let fileCount = 0;
31
- const subdirs = [];
32
- try {
33
- if (existsSync(config.vaultDir)) {
34
- for (const d of readdirSync(config.vaultDir, { withFileTypes: true })) {
35
- if (d.isDirectory()) {
36
- const dir = join(config.vaultDir, d.name);
37
- const count = walkDir(dir).length;
38
- fileCount += count;
39
- if (count > 0) subdirs.push({ name: d.name, count });
40
- }
41
- }
42
- }
43
- } catch (e) {
44
- errors.push(`File scan failed: ${e.message}`);
45
- }
46
-
47
- // Count DB rows by kind
48
- let kindCounts = [];
49
- try {
50
- kindCounts = db
51
- .prepare(
52
- `SELECT kind, COUNT(*) as c FROM vault ${userWhere} GROUP BY kind`,
53
- )
54
- .all(...userParams);
55
- } catch (e) {
56
- errors.push(`Kind count query failed: ${e.message}`);
57
- }
58
-
59
- // Count DB rows by category
60
- let categoryCounts = [];
61
- try {
62
- categoryCounts = db
63
- .prepare(
64
- `SELECT category, COUNT(*) as c FROM vault ${userWhere} GROUP BY category`,
65
- )
66
- .all(...userParams);
67
- } catch (e) {
68
- errors.push(`Category count query failed: ${e.message}`);
69
- }
70
-
71
- // DB file size
72
- let dbSize = "n/a";
73
- let dbSizeBytes = 0;
74
- try {
75
- if (existsSync(config.dbPath)) {
76
- dbSizeBytes = statSync(config.dbPath).size;
77
- dbSize =
78
- dbSizeBytes > 1024 * 1024
79
- ? `${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB`
80
- : `${(dbSizeBytes / 1024).toFixed(1)}KB`;
81
- }
82
- } catch (e) {
83
- errors.push(`DB size check failed: ${e.message}`);
84
- }
85
-
86
- // Check for stale paths (count all mismatches, not just a sample)
87
- let stalePaths = false;
88
- let staleCount = 0;
89
- try {
90
- const result = db
91
- .prepare(
92
- `SELECT COUNT(*) as c FROM vault WHERE file_path NOT LIKE ? || '%' ${userAnd}`,
93
- )
94
- .get(config.vaultDir, ...userParams);
95
- staleCount = result.c;
96
- stalePaths = staleCount > 0;
97
- } catch (e) {
98
- errors.push(`Stale path check failed: ${e.message}`);
99
- }
100
-
101
- // Count expired entries pending pruning
102
- let expiredCount = 0;
103
- try {
104
- expiredCount = db
105
- .prepare(
106
- `SELECT COUNT(*) as c FROM vault WHERE expires_at IS NOT NULL AND expires_at <= datetime('now') ${userAnd}`,
107
- )
108
- .get(...userParams).c;
109
- } catch (e) {
110
- errors.push(`Expired count failed: ${e.message}`);
111
- }
112
-
113
- // Count event-category entries
114
- let eventCount = 0;
115
- try {
116
- eventCount = db
117
- .prepare(
118
- `SELECT COUNT(*) as c FROM vault WHERE category = 'event' ${userAnd}`,
119
- )
120
- .get(...userParams).c;
121
- } catch (e) {
122
- errors.push(`Event count failed: ${e.message}`);
123
- }
124
-
125
- // Count event entries without expires_at
126
- let eventsWithoutTtlCount = 0;
127
- try {
128
- eventsWithoutTtlCount = db
129
- .prepare(
130
- `SELECT COUNT(*) as c FROM vault WHERE category = 'event' AND expires_at IS NULL ${userAnd}`,
131
- )
132
- .get(...userParams).c;
133
- } catch (e) {
134
- errors.push(`Events without TTL count failed: ${e.message}`);
135
- }
136
-
137
- // Embedding/vector status
138
- let embeddingStatus = null;
139
- try {
140
- const total = db
141
- .prepare(`SELECT COUNT(*) as c FROM vault ${userWhere}`)
142
- .get(...userParams).c;
143
- const indexed = db
144
- .prepare(
145
- `SELECT COUNT(*) as c FROM vault WHERE rowid IN (SELECT rowid FROM vault_vec) ${userAnd}`,
146
- )
147
- .get(...userParams).c;
148
- embeddingStatus = { indexed, total, missing: total - indexed };
149
- } catch (e) {
150
- errors.push(`Embedding status check failed: ${e.message}`);
151
- }
152
-
153
- // Embedding model availability
154
- const embedModelAvailable = isEmbedAvailable();
155
-
156
- // Count auto-captured feedback entries (written by tracked() on unhandled errors)
157
- let autoCapturedFeedbackCount = 0;
158
- try {
159
- autoCapturedFeedbackCount = db
160
- .prepare(
161
- `SELECT COUNT(*) as c FROM vault WHERE kind = 'feedback' AND tags LIKE '%"auto-captured"%' ${userAnd}`,
162
- )
163
- .get(...userParams).c;
164
- } catch (e) {
165
- errors.push(`Auto-captured feedback count failed: ${e.message}`);
166
- }
167
-
168
- // Stale knowledge entries — kinds with a threshold, not updated within N days
169
- let staleKnowledge = [];
170
- try {
171
- const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
172
- if (stalenessKinds.length > 0) {
173
- const kindClauses = stalenessKinds
174
- .map(
175
- ([kind, days]) =>
176
- `(kind = '${kind}' AND COALESCE(updated_at, created_at) <= datetime('now', '-${days} days'))`,
177
- )
178
- .join(" OR ");
179
- staleKnowledge = db
180
- .prepare(
181
- `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')) ${userAnd} ORDER BY last_updated ASC LIMIT 10`,
182
- )
183
- .all(...userParams);
184
- }
185
- } catch (e) {
186
- errors.push(`Stale knowledge check failed: ${e.message}`);
187
- }
188
-
189
- return {
190
- fileCount,
191
- subdirs,
192
- kindCounts,
193
- categoryCounts,
194
- dbSize,
195
- dbSizeBytes,
196
- stalePaths,
197
- staleCount,
198
- expiredCount,
199
- eventCount,
200
- eventsWithoutTtlCount,
201
- embeddingStatus,
202
- embedModelAvailable,
203
- autoCapturedFeedbackCount,
204
- staleKnowledge,
205
- resolvedFrom: config.resolvedFrom,
206
- errors,
207
- };
208
- }
209
-
210
- /**
211
- * Compute growth warnings based on vault status and configured thresholds.
212
- *
213
- * @param {object} status — result of gatherVaultStatus()
214
- * @param {object} thresholds — from config.thresholds
215
- * @returns {{ warnings: Array, hasCritical: boolean, hasWarnings: boolean, actions: string[], kindBreakdown: Array }}
216
- */
217
- export function computeGrowthWarnings(status, thresholds) {
218
- if (!thresholds)
219
- return {
220
- warnings: [],
221
- hasCritical: false,
222
- hasWarnings: false,
223
- actions: [],
224
- kindBreakdown: [],
225
- };
226
-
227
- const t = thresholds;
228
- const warnings = [];
229
- const actions = [];
230
-
231
- const total = status.embeddingStatus?.total ?? 0;
232
- const {
233
- eventCount = 0,
234
- eventsWithoutTtlCount = 0,
235
- expiredCount = 0,
236
- dbSizeBytes = 0,
237
- } = status;
238
-
239
- let totalExceeded = false;
240
-
241
- if (t.totalEntries?.critical != null && total >= t.totalEntries.critical) {
242
- totalExceeded = true;
243
- warnings.push({
244
- level: "critical",
245
- message: `Total entries: ${total.toLocaleString()} (exceeds critical limit of ${t.totalEntries.critical.toLocaleString()})`,
246
- });
247
- } else if (t.totalEntries?.warn != null && total >= t.totalEntries.warn) {
248
- totalExceeded = true;
249
- warnings.push({
250
- level: "warn",
251
- message: `Total entries: ${total.toLocaleString()} (exceeds recommended ${t.totalEntries.warn.toLocaleString()})`,
252
- });
253
- }
254
-
255
- if (
256
- t.eventEntries?.critical != null &&
257
- eventCount >= t.eventEntries.critical
258
- ) {
259
- warnings.push({
260
- level: "critical",
261
- message: `Event entries: ${eventCount.toLocaleString()} (exceeds critical limit of ${t.eventEntries.critical.toLocaleString()})`,
262
- });
263
- } else if (
264
- t.eventEntries?.warn != null &&
265
- eventCount >= t.eventEntries.warn
266
- ) {
267
- const ttlNote =
268
- eventsWithoutTtlCount > 0
269
- ? ` (${eventsWithoutTtlCount.toLocaleString()} without TTL)`
270
- : "";
271
- warnings.push({
272
- level: "warn",
273
- message: `Event entries: ${eventCount.toLocaleString()}${ttlNote} (exceeds recommended ${t.eventEntries.warn.toLocaleString()})`,
274
- });
275
- }
276
-
277
- if (
278
- t.vaultSizeBytes?.critical != null &&
279
- dbSizeBytes >= t.vaultSizeBytes.critical
280
- ) {
281
- warnings.push({
282
- level: "critical",
283
- message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds critical limit of ${(t.vaultSizeBytes.critical / 1024 / 1024).toFixed(0)}MB)`,
284
- });
285
- } else if (
286
- t.vaultSizeBytes?.warn != null &&
287
- dbSizeBytes >= t.vaultSizeBytes.warn
288
- ) {
289
- warnings.push({
290
- level: "warn",
291
- message: `Database size: ${(dbSizeBytes / 1024 / 1024).toFixed(1)}MB (exceeds recommended ${(t.vaultSizeBytes.warn / 1024 / 1024).toFixed(0)}MB)`,
292
- });
293
- }
294
-
295
- if (
296
- t.eventsWithoutTtl?.warn != null &&
297
- eventsWithoutTtlCount >= t.eventsWithoutTtl.warn
298
- ) {
299
- warnings.push({
300
- level: "warn",
301
- message: `Event entries without expires_at: ${eventsWithoutTtlCount.toLocaleString()} (exceeds recommended ${t.eventsWithoutTtl.warn.toLocaleString()})`,
302
- });
303
- }
304
-
305
- const hasCritical = warnings.some((w) => w.level === "critical");
306
-
307
- if (expiredCount > 0) {
308
- actions.push(
309
- `Run \`context-vault prune\` to remove ${expiredCount} expired event entr${expiredCount === 1 ? "y" : "ies"}`,
310
- );
311
- }
312
- const eventThresholdExceeded =
313
- eventCount >= (t.eventEntries?.warn ?? Infinity);
314
- const ttlThresholdExceeded =
315
- eventsWithoutTtlCount >= (t.eventsWithoutTtl?.warn ?? Infinity);
316
- if (
317
- eventsWithoutTtlCount > 0 &&
318
- (eventThresholdExceeded || ttlThresholdExceeded)
319
- ) {
320
- actions.push(
321
- "Add `expires_at` to event/session entries to enable automatic cleanup",
322
- );
323
- }
324
- if (total >= (t.totalEntries?.warn ?? Infinity)) {
325
- actions.push("Consider archiving events older than 90 days");
326
- }
327
-
328
- const kindBreakdown = totalExceeded
329
- ? buildKindBreakdown(status.kindCounts, total)
330
- : [];
331
-
332
- return {
333
- warnings,
334
- hasCritical,
335
- hasWarnings: warnings.length > 0,
336
- actions,
337
- kindBreakdown,
338
- };
339
- }
340
-
341
- function buildKindBreakdown(kindCounts, total) {
342
- if (!kindCounts?.length || total === 0) return [];
343
- return [...kindCounts]
344
- .sort((a, b) => b.c - a.c)
345
- .map(({ kind, c }) => ({
346
- kind,
347
- count: c,
348
- pct: Math.round((c / total) * 100),
349
- }));
350
- }
@@ -1,90 +0,0 @@
1
- import { existsSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { API_URL, MARKETING_URL, GITHUB_ISSUES_URL } from "../constants.js";
4
-
5
- const TELEMETRY_ENDPOINT = `${API_URL}/telemetry`;
6
- const NOTICE_MARKER = ".telemetry-notice-shown";
7
- const FEEDBACK_PROMPT_MARKER = ".feedback-prompt-shown";
8
-
9
- export function isTelemetryEnabled(config) {
10
- const envVal = process.env.CONTEXT_VAULT_TELEMETRY;
11
- if (envVal !== undefined) return envVal === "1" || envVal === "true";
12
- return config?.telemetry === true;
13
- }
14
-
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) {
21
- if (!isTelemetryEnabled(config)) return;
22
-
23
- const event = {
24
- event: payload.event,
25
- code: payload.code || null,
26
- tool: payload.tool || null,
27
- cv_version: payload.cv_version,
28
- node_version: process.version,
29
- platform: process.platform,
30
- arch: process.arch,
31
- ts: new Date().toISOString(),
32
- };
33
-
34
- fetch(TELEMETRY_ENDPOINT, {
35
- method: "POST",
36
- headers: { "Content-Type": "application/json" },
37
- body: JSON.stringify(event),
38
- signal: AbortSignal.timeout(5000),
39
- }).catch(() => {});
40
- }
41
-
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) {
47
- try {
48
- const markerPath = join(dataDir, NOTICE_MARKER);
49
- if (existsSync(markerPath)) return;
50
- writeFileSync(markerPath, new Date().toISOString() + "\n");
51
- } catch {
52
- return;
53
- }
54
-
55
- const lines = [
56
- "[context-vault] Telemetry: disabled by default.",
57
- "[context-vault] To help improve context-vault, you can opt in to anonymous error reporting.",
58
- "[context-vault] Reports contain only: event type, error code, tool name, version, node version, platform, arch, timestamp.",
59
- "[context-vault] No vault content, file paths, or personal data is ever sent.",
60
- '[context-vault] Opt in: set "telemetry": true in ~/.context-mcp/config.json or set CONTEXT_VAULT_TELEMETRY=1.',
61
- `[context-vault] Full payload schema: ${MARKETING_URL}/telemetry`,
62
- ];
63
- for (const line of lines) {
64
- process.stderr.write(line + "\n");
65
- }
66
- }
67
-
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) {
74
- try {
75
- const markerPath = join(dataDir, FEEDBACK_PROMPT_MARKER);
76
- if (existsSync(markerPath)) return;
77
- writeFileSync(markerPath, new Date().toISOString() + "\n");
78
- } catch {
79
- return;
80
- }
81
-
82
- const lines = [
83
- "[context-vault] First entry saved — nice work!",
84
- "[context-vault] Got feedback, a bug, or a feature request?",
85
- `[context-vault] Open an issue: ${GITHUB_ISSUES_URL}`,
86
- ];
87
- for (const line of lines) {
88
- process.stderr.write(line + "\n");
89
- }
90
- }