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
@@ -0,0 +1,285 @@
1
+ import type { BaseCtx, SearchResult, SearchOptions, VaultEntry } from "./types.js";
2
+
3
+ const NEAR_DUP_THRESHOLD = 0.92;
4
+ const RRF_K = 60;
5
+
6
+ export function recencyDecayScore(updatedAt: string | null | undefined, decayRate = 0.05): number {
7
+ if (updatedAt == null) return 0.5;
8
+ const ageDays = (Date.now() - new Date(updatedAt).getTime()) / 86400000;
9
+ return Math.exp(-decayRate * ageDays);
10
+ }
11
+
12
+ export function dotProduct(a: Float32Array, b: Float32Array): number {
13
+ let sum = 0;
14
+ for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
15
+ return sum;
16
+ }
17
+
18
+ export function buildFtsQuery(query: string): string | null {
19
+ const words = query
20
+ .split(/[\s-]+/)
21
+ .map((w) => w.replace(/[*"():^~{}]/g, ""))
22
+ .filter((w) => w.length > 0);
23
+ if (!words.length) return null;
24
+ if (words.length === 1) return `"${words[0]}"`;
25
+ const phrase = `"${words.join(" ")}"`;
26
+ const near = `NEAR(${words.map((w) => `"${w}"`).join(" ")}, 10)`;
27
+ const and = words.map((w) => `"${w}"`).join(" AND ");
28
+ return `${phrase} OR ${near} OR ${and}`;
29
+ }
30
+
31
+ export function recencyBoost(createdAt: string, category: string, decayDays = 30): number {
32
+ if (category !== "event") return 1.0;
33
+ const ageDays = (Date.now() - new Date(createdAt).getTime()) / 86400000;
34
+ return 1 / (1 + ageDays / decayDays);
35
+ }
36
+
37
+ export function buildFilterClauses({
38
+ categoryFilter,
39
+ excludeEvents = false,
40
+ since,
41
+ until,
42
+ includeSuperseeded = false,
43
+ }: {
44
+ categoryFilter?: string | null;
45
+ excludeEvents?: boolean;
46
+ since?: string | null;
47
+ until?: string | null;
48
+ includeSuperseeded?: boolean;
49
+ }): { clauses: string[]; params: unknown[] } {
50
+ const clauses: string[] = [];
51
+ const params: unknown[] = [];
52
+ if (categoryFilter) {
53
+ clauses.push("e.category = ?");
54
+ params.push(categoryFilter);
55
+ }
56
+ if (excludeEvents && !categoryFilter) {
57
+ clauses.push("e.category != 'event'");
58
+ }
59
+ if (since) {
60
+ clauses.push("e.created_at >= ?");
61
+ params.push(since);
62
+ }
63
+ if (until) {
64
+ clauses.push("e.created_at <= ?");
65
+ params.push(until);
66
+ }
67
+ clauses.push("(e.expires_at IS NULL OR e.expires_at > datetime('now'))");
68
+ if (!includeSuperseeded) {
69
+ clauses.push("e.superseded_by IS NULL");
70
+ }
71
+ return { clauses, params };
72
+ }
73
+
74
+ export function reciprocalRankFusion(
75
+ rankedLists: string[][],
76
+ k: number = RRF_K,
77
+ ): Map<string, number> {
78
+ const scores = new Map<string, number>();
79
+ for (const list of rankedLists) {
80
+ for (let rank = 0; rank < list.length; rank++) {
81
+ const id = list[rank];
82
+ scores.set(id, (scores.get(id) ?? 0) + 1 / (k + rank + 1));
83
+ }
84
+ }
85
+ return scores;
86
+ }
87
+
88
+ export async function hybridSearch(
89
+ ctx: BaseCtx,
90
+ query: string,
91
+ opts: SearchOptions = {},
92
+ ): Promise<SearchResult[]> {
93
+ const {
94
+ kindFilter = null,
95
+ categoryFilter = null,
96
+ excludeEvents = false,
97
+ since = null,
98
+ until = null,
99
+ limit = 20,
100
+ offset = 0,
101
+ decayDays = 30,
102
+ includeSuperseeded = false,
103
+ } = opts;
104
+
105
+ const rowMap = new Map<string, VaultEntry>();
106
+ const idToRowid = new Map<string, number>();
107
+ let queryVec: Float32Array | null = null;
108
+
109
+ const extraFilters = buildFilterClauses({
110
+ categoryFilter,
111
+ excludeEvents,
112
+ since,
113
+ until,
114
+ includeSuperseeded,
115
+ });
116
+
117
+ const ftsRankedIds: string[] = [];
118
+
119
+ const ftsQuery = buildFtsQuery(query);
120
+ if (ftsQuery) {
121
+ try {
122
+ const whereParts = ["vault_fts MATCH ?"];
123
+ const ftsParams: unknown[] = [ftsQuery];
124
+
125
+ if (kindFilter) {
126
+ whereParts.push("e.kind = ?");
127
+ ftsParams.push(kindFilter);
128
+ }
129
+ whereParts.push(...extraFilters.clauses);
130
+ ftsParams.push(...extraFilters.params);
131
+
132
+ const ftsSQL = `SELECT e.*, rank FROM vault_fts f JOIN vault e ON f.rowid = e.rowid WHERE ${whereParts.join(" AND ")} ORDER BY rank LIMIT 15`;
133
+ // @ts-expect-error -- node:sqlite types are overly strict for dynamic SQL params
134
+ const rows = ctx.db.prepare(ftsSQL).all(...ftsParams) as unknown as (VaultEntry & { rank: number })[];
135
+
136
+ for (const { rank: _rank, ...row } of rows) {
137
+ ftsRankedIds.push(row.id);
138
+ if (!rowMap.has(row.id)) rowMap.set(row.id, row);
139
+ }
140
+ } catch (err) {
141
+ if (!(err as Error).message?.includes("fts5: syntax error")) {
142
+ console.error(`[retrieve] FTS search error: ${(err as Error).message}`);
143
+ }
144
+ }
145
+ }
146
+
147
+ const vecRankedIds: string[] = [];
148
+ const vecSimMap = new Map<string, number>();
149
+
150
+ try {
151
+ const vecCount = (ctx.db.prepare("SELECT COUNT(*) as c FROM vault_vec").get() as { c: number }).c;
152
+ if (vecCount > 0) {
153
+ queryVec = await ctx.embed(query);
154
+ if (queryVec) {
155
+ const vecLimit = kindFilter ? 30 : 15;
156
+ const vecRows = ctx.db
157
+ .prepare(
158
+ `SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
159
+ )
160
+ .all(queryVec, vecLimit) as { rowid: number; distance: number }[];
161
+
162
+ if (vecRows.length) {
163
+ const rowids = vecRows.map((vr) => vr.rowid);
164
+ const placeholders = rowids.map(() => "?").join(",");
165
+ const hydrated = ctx.db
166
+ .prepare(
167
+ `SELECT rowid, * FROM vault WHERE rowid IN (${placeholders})`,
168
+ )
169
+ .all(...rowids) as unknown as (VaultEntry & { rowid: number })[];
170
+
171
+ const byRowid = new Map<number, VaultEntry & { rowid: number }>();
172
+ for (const row of hydrated) byRowid.set(row.rowid, row);
173
+
174
+ for (const vr of vecRows) {
175
+ const row = byRowid.get(vr.rowid);
176
+ if (!row) continue;
177
+ if (kindFilter && row.kind !== kindFilter) continue;
178
+ if (categoryFilter && row.category !== categoryFilter) continue;
179
+ if (excludeEvents && row.category === "event") continue;
180
+ if (since && row.created_at < since) continue;
181
+ if (until && row.created_at > until) continue;
182
+ if (row.expires_at && new Date(row.expires_at) <= new Date())
183
+ continue;
184
+
185
+ const { rowid: _rowid, ...cleanRow } = row;
186
+ idToRowid.set(cleanRow.id, Number(row.rowid));
187
+
188
+ const vecSim = Math.max(0, 1 - vr.distance / 2);
189
+ vecSimMap.set(cleanRow.id, vecSim);
190
+ vecRankedIds.push(cleanRow.id);
191
+
192
+ if (!rowMap.has(cleanRow.id)) rowMap.set(cleanRow.id, cleanRow);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ } catch (err) {
198
+ if (!(err as Error).message?.includes("no such table")) {
199
+ console.error(`[retrieve] Vector search error: ${(err as Error).message}`);
200
+ }
201
+ }
202
+
203
+ if (rowMap.size === 0) return [];
204
+
205
+ const rrfScores = reciprocalRankFusion([ftsRankedIds, vecRankedIds]);
206
+
207
+ for (const [id, entry] of rowMap) {
208
+ const boost = recencyBoost(entry.created_at, entry.category, decayDays);
209
+ rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost);
210
+ }
211
+
212
+ const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
213
+ ...entry,
214
+ score: rrfScores.get(entry.id) ?? 0,
215
+ }));
216
+ candidates.sort((a, b) => b.score - a.score);
217
+
218
+ const embeddingMap = new Map<string, Float32Array>();
219
+ if (queryVec && idToRowid.size > 0) {
220
+ const rowidToId = new Map<number, string>();
221
+ for (const [id, rowid] of idToRowid) rowidToId.set(rowid, id);
222
+
223
+ const rowidsToFetch = [...idToRowid.values()];
224
+ try {
225
+ const placeholders = rowidsToFetch.map(() => "?").join(",");
226
+ const vecData = ctx.db
227
+ .prepare(
228
+ `SELECT rowid, embedding FROM vault_vec WHERE rowid IN (${placeholders})`,
229
+ )
230
+ .all(...rowidsToFetch) as { rowid: number; embedding: Buffer }[];
231
+ for (const row of vecData) {
232
+ const id = rowidToId.get(Number(row.rowid));
233
+ const buf = row.embedding;
234
+ if (id && buf) {
235
+ embeddingMap.set(
236
+ id,
237
+ new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4),
238
+ );
239
+ }
240
+ }
241
+ } catch {
242
+ // Embeddings unavailable
243
+ }
244
+ }
245
+
246
+ if (queryVec && embeddingMap.size > 0) {
247
+ const selected: SearchResult[] = [];
248
+ const selectedVecs: Float32Array[] = [];
249
+ for (const candidate of candidates) {
250
+ if (selected.length >= offset + limit) break;
251
+ const vec = embeddingMap.get(candidate.id);
252
+ if (vec && selectedVecs.length > 0) {
253
+ let maxSim = 0;
254
+ for (const sv of selectedVecs) {
255
+ const sim = dotProduct(sv, vec);
256
+ if (sim > maxSim) maxSim = sim;
257
+ }
258
+ if (maxSim > NEAR_DUP_THRESHOLD) continue;
259
+ }
260
+ selected.push(candidate);
261
+ if (vec) selectedVecs.push(vec);
262
+ }
263
+ const dedupedPage = selected.slice(offset, offset + limit);
264
+ trackAccess(ctx, dedupedPage);
265
+ return dedupedPage;
266
+ }
267
+
268
+ const finalPage = candidates.slice(offset, offset + limit);
269
+ trackAccess(ctx, finalPage);
270
+ return finalPage;
271
+ }
272
+
273
+ function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
274
+ if (!entries.length) return;
275
+ try {
276
+ const placeholders = entries.map(() => "?").join(",");
277
+ ctx.db
278
+ .prepare(
279
+ `UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE id IN (${placeholders})`,
280
+ )
281
+ .run(...entries.map((e) => e.id));
282
+ } catch {
283
+ // Non-fatal
284
+ }
285
+ }
@@ -0,0 +1,166 @@
1
+ import type { DatabaseSync, StatementSync } from "node:sqlite";
2
+
3
+ export interface VaultConfig {
4
+ vaultDir: string;
5
+ dataDir: string;
6
+ dbPath: string;
7
+ devDir: string;
8
+ eventDecayDays: number;
9
+ thresholds: GrowthThresholds;
10
+ telemetry: boolean;
11
+ resolvedFrom: string;
12
+ configPath?: string;
13
+ vaultDirExists?: boolean;
14
+ recall: RecallConfig;
15
+ consolidation: ConsolidationConfig;
16
+ lifecycle: Record<string, { archiveAfterDays?: number }>;
17
+ }
18
+
19
+ export interface RecallConfig {
20
+ maxResults: number;
21
+ maxOutputBytes: number;
22
+ minRelevanceScore: number;
23
+ excludeKinds: string[];
24
+ excludeCategories: string[];
25
+ bodyTruncateChars: number;
26
+ }
27
+
28
+ export interface ConsolidationConfig {
29
+ tagThreshold: number;
30
+ maxAgeDays: number;
31
+ autoConsolidate: boolean;
32
+ }
33
+
34
+ export interface GrowthThresholds {
35
+ totalEntries: { warn: number; critical: number };
36
+ eventEntries: { warn: number; critical: number };
37
+ vaultSizeBytes: { warn: number; critical: number };
38
+ eventsWithoutTtl: { warn: number };
39
+ }
40
+
41
+ export interface PreparedStatements {
42
+ insertEntry: StatementSync;
43
+ updateEntry: StatementSync;
44
+ deleteEntry: StatementSync;
45
+ getRowid: StatementSync;
46
+ getRowidByPath: StatementSync;
47
+ getEntryById: StatementSync;
48
+ getByIdentityKey: StatementSync;
49
+ upsertByIdentityKey: StatementSync;
50
+ updateSourceFiles: StatementSync;
51
+ updateRelatedTo: StatementSync;
52
+ insertVecStmt: StatementSync;
53
+ deleteVecStmt: StatementSync;
54
+ updateSupersededBy: StatementSync;
55
+ clearSupersededByRef: StatementSync;
56
+ }
57
+
58
+ export interface VaultEntry {
59
+ id: string;
60
+ kind: string;
61
+ category: string;
62
+ title: string | null;
63
+ body: string;
64
+ meta: string | null;
65
+ tags: string | null;
66
+ source: string | null;
67
+ file_path: string | null;
68
+ identity_key: string | null;
69
+ expires_at: string | null;
70
+ superseded_by: string | null;
71
+ created_at: string;
72
+ updated_at: string | null;
73
+ hit_count: number;
74
+ last_accessed_at: string | null;
75
+ source_files: string | null;
76
+ tier: string;
77
+ related_to: string | null;
78
+ rowid?: number;
79
+ }
80
+
81
+ export interface SearchResult extends VaultEntry {
82
+ score: number;
83
+ stale?: boolean;
84
+ stale_reason?: string;
85
+ }
86
+
87
+ export interface CaptureInput {
88
+ kind: string;
89
+ title?: string | null;
90
+ body: string;
91
+ meta?: Record<string, unknown> | null;
92
+ tags?: string[] | null;
93
+ source?: string | null;
94
+ folder?: string | null;
95
+ identity_key?: string | null;
96
+ expires_at?: string | null;
97
+ supersedes?: string[] | null;
98
+ related_to?: string[] | null;
99
+ source_files?: Array<{ path: string; hash: string }> | null;
100
+ tier?: string | null;
101
+ }
102
+
103
+ export interface CaptureResult {
104
+ id: string;
105
+ filePath: string;
106
+ kind: string;
107
+ category: string;
108
+ title: string | null;
109
+ body: string;
110
+ meta: Record<string, unknown> | undefined;
111
+ tags: string[] | null;
112
+ source: string | null;
113
+ createdAt: string;
114
+ updatedAt: string;
115
+ identity_key: string | null;
116
+ expires_at: string | null;
117
+ supersedes: string[] | null;
118
+ related_to: string[] | null;
119
+ source_files: Array<{ path: string; hash: string }> | null;
120
+ tier: string | null;
121
+ }
122
+
123
+ export interface IndexEntryInput {
124
+ id: string;
125
+ kind: string;
126
+ category: string;
127
+ title: string | null;
128
+ body: string;
129
+ meta: Record<string, unknown> | undefined;
130
+ tags: string[] | null;
131
+ source: string | null;
132
+ filePath: string;
133
+ createdAt: string;
134
+ identity_key: string | null;
135
+ expires_at: string | null;
136
+ source_files: Array<{ path: string; hash: string }> | null;
137
+ tier: string | null;
138
+ }
139
+
140
+ export interface ReindexStats {
141
+ added: number;
142
+ updated: number;
143
+ removed: number;
144
+ unchanged: number;
145
+ }
146
+
147
+ export interface BaseCtx {
148
+ db: DatabaseSync;
149
+ config: VaultConfig;
150
+ stmts: PreparedStatements;
151
+ embed: (text: string) => Promise<Float32Array | null>;
152
+ insertVec: (rowid: number, embedding: Float32Array) => void;
153
+ deleteVec: (rowid: number) => void;
154
+ }
155
+
156
+ export interface SearchOptions {
157
+ kindFilter?: string | null;
158
+ categoryFilter?: string | null;
159
+ excludeEvents?: boolean;
160
+ since?: string | null;
161
+ until?: string | null;
162
+ limit?: number;
163
+ offset?: number;
164
+ decayDays?: number;
165
+ includeSuperseeded?: boolean;
166
+ }
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.17.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
7
7
  "context-vault": "bin/cli.js",
8
- "context-mcp": "bin/cli.js"
8
+ "context-mcp": "bin/cli.js",
9
+ "memory": "bin/cli.js"
9
10
  },
10
- "main": "src/server/index.js",
11
+ "main": "src/server.js",
11
12
  "exports": {
12
- ".": "./src/server/index.js",
13
+ ".": "./src/server.js",
13
14
  "./cli": "./bin/cli.js"
14
15
  },
15
16
  "scripts": {
@@ -26,7 +27,7 @@
26
27
  ],
27
28
  "license": "MIT",
28
29
  "engines": {
29
- "node": ">=24"
30
+ "node": ">=22"
30
31
  },
31
32
  "author": "Felix Hellstrom",
32
33
  "repository": {
@@ -56,8 +57,12 @@
56
57
  "@context-vault/core"
57
58
  ],
58
59
  "dependencies": {
59
- "@context-vault/core": "^2.17.0",
60
+ "@context-vault/core": "^3.0.0",
60
61
  "@modelcontextprotocol/sdk": "^1.26.0",
62
+ "adm-zip": "^0.5.16",
61
63
  "sqlite-vec": "^0.1.0"
62
- }
64
+ },
65
+ "bundleDependencies": [
66
+ "@context-vault/core"
67
+ ]
63
68
  }
@@ -56,7 +56,7 @@ async function main() {
56
56
  // `npx context-vault serve` instead, so skip writing the launcher.
57
57
  const isNpx = PKG_ROOT.includes("/_npx/") || PKG_ROOT.includes("\\_npx\\");
58
58
  if (!isNpx) {
59
- const SERVER_ABS = join(PKG_ROOT, "src", "server", "index.js");
59
+ const SERVER_ABS = join(PKG_ROOT, "src", "server.js");
60
60
  const DATA_DIR = join(homedir(), ".context-mcp");
61
61
  const LAUNCHER = join(DATA_DIR, "server.mjs");
62
62
  mkdirSync(DATA_DIR, { recursive: true });
@@ -8,20 +8,12 @@ import {
8
8
  } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
- const MAX_LOG_SIZE = 1024 * 1024; // 1MB
11
+ const MAX_LOG_SIZE = 1024 * 1024;
12
12
 
13
13
  export function errorLogPath(dataDir) {
14
14
  return join(dataDir, "error.log");
15
15
  }
16
16
 
17
- /**
18
- * Append a structured JSON entry to the startup error log.
19
- * Rotates the file if it exceeds MAX_LOG_SIZE.
20
- * Never throws — logging failures must not mask the original error.
21
- *
22
- * @param {string} dataDir
23
- * @param {object} entry
24
- */
25
17
  export function appendErrorLog(dataDir, entry) {
26
18
  try {
27
19
  mkdirSync(dataDir, { recursive: true });
@@ -35,12 +27,6 @@ export function appendErrorLog(dataDir, entry) {
35
27
  }
36
28
  }
37
29
 
38
- /**
39
- * Return number of log lines in the error log, or 0 if absent.
40
- *
41
- * @param {string} dataDir
42
- * @returns {number}
43
- */
44
30
  export function errorLogCount(dataDir) {
45
31
  try {
46
32
  const logPath = errorLogPath(dataDir);
@@ -1,8 +1,11 @@
1
- /**
2
- * helpers.js Shared MCP response helpers and validation
3
- */
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
4
 
5
- import pkg from "../../package.json" with { type: "json" };
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const pkg = JSON.parse(
7
+ readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8"),
8
+ );
6
9
 
7
10
  export function ok(text) {
8
11
  return { content: [{ type: "text", text }] };
@@ -42,3 +45,5 @@ export function ensureValidKind(kind) {
42
45
  }
43
46
  return null;
44
47
  }
48
+
49
+ export { pkg };
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
+ }