@context-vault/core 2.15.0 → 2.17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.15.0",
3
+ "version": "2.17.0",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
package/src/constants.js CHANGED
@@ -14,8 +14,13 @@ export const MAX_SOURCE_LENGTH = 200;
14
14
  export const MAX_IDENTITY_KEY_LENGTH = 200;
15
15
 
16
16
  export const DEFAULT_GROWTH_THRESHOLDS = {
17
- totalEntries: { warn: 1000, critical: 5000 },
18
- eventEntries: { warn: 500, critical: 2000 },
17
+ totalEntries: { warn: 2000, critical: 5000 },
18
+ eventEntries: { warn: 1000, critical: 3000 },
19
19
  vaultSizeBytes: { warn: 50 * 1024 * 1024, critical: 200 * 1024 * 1024 },
20
20
  eventsWithoutTtl: { warn: 200 },
21
21
  };
22
+
23
+ export const DEFAULT_LIFECYCLE = {
24
+ event: { archiveAfterDays: 90 },
25
+ ephemeral: { archiveAfterDays: 30 },
26
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { DEFAULT_GROWTH_THRESHOLDS } from "../constants.js";
4
+ import { DEFAULT_GROWTH_THRESHOLDS, DEFAULT_LIFECYCLE } from "../constants.js";
5
5
 
6
6
  export function parseArgs(argv) {
7
7
  const args = {};
@@ -27,7 +27,7 @@ export function resolveConfig() {
27
27
  join(HOME, ".context-mcp"),
28
28
  );
29
29
  const config = {
30
- vaultDir: join(HOME, "vault"),
30
+ vaultDir: join(HOME, ".vault"),
31
31
  dataDir,
32
32
  dbPath: join(dataDir, "vault.db"),
33
33
  devDir: join(HOME, "dev"),
@@ -48,6 +48,7 @@ export function resolveConfig() {
48
48
  maxAgeDays: 7,
49
49
  autoConsolidate: false,
50
50
  },
51
+ lifecycle: structuredClone(DEFAULT_LIFECYCLE),
51
52
  };
52
53
 
53
54
  const configPath = join(dataDir, "config.json");
@@ -62,6 +63,12 @@ export function resolveConfig() {
62
63
  if (fc.dbPath) config.dbPath = fc.dbPath;
63
64
  if (fc.devDir) config.devDir = fc.devDir;
64
65
  if (fc.eventDecayDays != null) config.eventDecayDays = fc.eventDecayDays;
66
+ if (fc.growthWarningThreshold != null) {
67
+ config.thresholds.totalEntries = {
68
+ ...config.thresholds.totalEntries,
69
+ warn: Number(fc.growthWarningThreshold),
70
+ };
71
+ }
65
72
  if (fc.thresholds) {
66
73
  const t = fc.thresholds;
67
74
  if (t.totalEntries)
@@ -212,7 +212,7 @@ export function gatherVaultStatus(ctx, opts = {}) {
212
212
  *
213
213
  * @param {object} status — result of gatherVaultStatus()
214
214
  * @param {object} thresholds — from config.thresholds
215
- * @returns {{ warnings: Array, hasCritical: boolean, hasWarnings: boolean, actions: string[] }}
215
+ * @returns {{ warnings: Array, hasCritical: boolean, hasWarnings: boolean, actions: string[], kindBreakdown: Array }}
216
216
  */
217
217
  export function computeGrowthWarnings(status, thresholds) {
218
218
  if (!thresholds)
@@ -221,6 +221,7 @@ export function computeGrowthWarnings(status, thresholds) {
221
221
  hasCritical: false,
222
222
  hasWarnings: false,
223
223
  actions: [],
224
+ kindBreakdown: [],
224
225
  };
225
226
 
226
227
  const t = thresholds;
@@ -235,12 +236,16 @@ export function computeGrowthWarnings(status, thresholds) {
235
236
  dbSizeBytes = 0,
236
237
  } = status;
237
238
 
239
+ let totalExceeded = false;
240
+
238
241
  if (t.totalEntries?.critical != null && total >= t.totalEntries.critical) {
242
+ totalExceeded = true;
239
243
  warnings.push({
240
244
  level: "critical",
241
245
  message: `Total entries: ${total.toLocaleString()} (exceeds critical limit of ${t.totalEntries.critical.toLocaleString()})`,
242
246
  });
243
247
  } else if (t.totalEntries?.warn != null && total >= t.totalEntries.warn) {
248
+ totalExceeded = true;
244
249
  warnings.push({
245
250
  level: "warn",
246
251
  message: `Total entries: ${total.toLocaleString()} (exceeds recommended ${t.totalEntries.warn.toLocaleString()})`,
@@ -320,5 +325,26 @@ export function computeGrowthWarnings(status, thresholds) {
320
325
  actions.push("Consider archiving events older than 90 days");
321
326
  }
322
327
 
323
- return { warnings, hasCritical, hasWarnings: warnings.length > 0, actions };
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
+ }));
324
350
  }
@@ -196,18 +196,20 @@ export async function indexEntry(
196
196
  );
197
197
  }
198
198
 
199
- // Embeddings are always generated from plaintext (before encryption)
200
- const embeddingText = [title, body].filter(Boolean).join(" ");
201
- const embedding = await ctx.embed(embeddingText);
199
+ // Skip embedding generation for event entries they are excluded from
200
+ // default semantic search and don't need vector representations
201
+ if (cat !== "event") {
202
+ const embeddingText = [title, body].filter(Boolean).join(" ");
203
+ const embedding = await ctx.embed(embeddingText);
202
204
 
203
- // Upsert vec: delete old if exists, then insert new (skip if embedding unavailable)
204
- if (embedding) {
205
- try {
206
- ctx.deleteVec(rowid);
207
- } catch {
208
- /* no-op if not found */
205
+ if (embedding) {
206
+ try {
207
+ ctx.deleteVec(rowid);
208
+ } catch {
209
+ /* no-op if not found */
210
+ }
211
+ ctx.insertVec(rowid, embedding);
209
212
  }
210
- ctx.insertVec(rowid, embedding);
211
213
  }
212
214
  }
213
215
 
@@ -370,15 +372,17 @@ export async function reindex(ctx, opts = {}) {
370
372
  fmMeta.updated || created,
371
373
  );
372
374
  if (result.changes > 0) {
373
- const rowidResult = ctx.stmts.getRowid.get(id);
374
- if (rowidResult?.rowid) {
375
- const embeddingText = [parsed.title, parsed.body]
376
- .filter(Boolean)
377
- .join(" ");
378
- pendingEmbeds.push({
379
- rowid: rowidResult.rowid,
380
- text: embeddingText,
381
- });
375
+ if (category !== "event") {
376
+ const rowidResult = ctx.stmts.getRowid.get(id);
377
+ if (rowidResult?.rowid) {
378
+ const embeddingText = [parsed.title, parsed.body]
379
+ .filter(Boolean)
380
+ .join(" ");
381
+ pendingEmbeds.push({
382
+ rowid: rowidResult.rowid,
383
+ text: embeddingText,
384
+ });
385
+ }
382
386
  }
383
387
  stats.added++;
384
388
  } else {
@@ -407,7 +411,7 @@ export async function reindex(ctx, opts = {}) {
407
411
  );
408
412
 
409
413
  // Queue re-embed if title or body changed (vector ops deferred to Phase 2)
410
- if (bodyChanged || titleChanged) {
414
+ if ((bodyChanged || titleChanged) && category !== "event") {
411
415
  const rowid = ctx.stmts.getRowid.get(existing.id)?.rowid;
412
416
  if (rowid) {
413
417
  const embeddingText = [parsed.title, parsed.body]
@@ -74,6 +74,7 @@ export function recencyBoost(createdAt, category, decayDays = 30) {
74
74
  */
75
75
  export function buildFilterClauses({
76
76
  categoryFilter,
77
+ excludeEvents = false,
77
78
  since,
78
79
  until,
79
80
  userIdFilter,
@@ -94,6 +95,9 @@ export function buildFilterClauses({
94
95
  clauses.push("e.category = ?");
95
96
  params.push(categoryFilter);
96
97
  }
98
+ if (excludeEvents && !categoryFilter) {
99
+ clauses.push("e.category != 'event'");
100
+ }
97
101
  if (since) {
98
102
  clauses.push("e.created_at >= ?");
99
103
  params.push(since);
@@ -242,6 +246,7 @@ export async function hybridSearch(
242
246
  {
243
247
  kindFilter = null,
244
248
  categoryFilter = null,
249
+ excludeEvents = false,
245
250
  since = null,
246
251
  until = null,
247
252
  limit = 20,
@@ -258,6 +263,7 @@ export async function hybridSearch(
258
263
 
259
264
  const extraFilters = buildFilterClauses({
260
265
  categoryFilter,
266
+ excludeEvents,
261
267
  since,
262
268
  until,
263
269
  userIdFilter,
@@ -340,6 +346,7 @@ export async function hybridSearch(
340
346
  if (teamIdFilter && row.team_id !== teamIdFilter) continue;
341
347
  if (kindFilter && row.kind !== kindFilter) continue;
342
348
  if (categoryFilter && row.category !== categoryFilter) continue;
349
+ if (excludeEvents && row.category === "event") continue;
343
350
  if (since && row.created_at < since) continue;
344
351
  if (until && row.created_at > until) continue;
345
352
  if (row.expires_at && new Date(row.expires_at) <= new Date())
@@ -146,6 +146,13 @@ export function handler(_args, ctx) {
146
146
  for (const w of growth.warnings) {
147
147
  lines.push(` ${w.message}`);
148
148
  }
149
+ if (growth.kindBreakdown.length) {
150
+ lines.push("");
151
+ lines.push(" Breakdown by kind:");
152
+ for (const { kind, count, pct } of growth.kindBreakdown) {
153
+ lines.push(` ${kind}: ${count.toLocaleString()} (${pct}%)`);
154
+ }
155
+ }
149
156
  if (growth.actions.length) {
150
157
  lines.push("", "Suggested growth actions:");
151
158
  for (const a of growth.actions) {
@@ -316,6 +316,12 @@ export const inputSchema = {
316
316
  .describe(
317
317
  "If true, include ephemeral tier entries in results. Default: false — only working and durable tiers are returned.",
318
318
  ),
319
+ include_events: z
320
+ .boolean()
321
+ .optional()
322
+ .describe(
323
+ "If true, include event category entries in semantic search results. Default: false — events are excluded from query-based search but remain accessible via category/tag filters.",
324
+ ),
319
325
  };
320
326
 
321
327
  /**
@@ -339,6 +345,7 @@ export async function handler(
339
345
  max_tokens,
340
346
  pivot_count,
341
347
  include_ephemeral,
348
+ include_events,
342
349
  },
343
350
  ctx,
344
351
  { ensureIndexed, reindexFailed },
@@ -347,6 +354,7 @@ export async function handler(
347
354
  const userId = ctx.userId !== undefined ? ctx.userId : undefined;
348
355
 
349
356
  const hasQuery = query?.trim();
357
+ const shouldExcludeEvents = hasQuery && !include_events && !category;
350
358
  // Expand buckets to bucket: prefixed tags and merge with explicit tags
351
359
  const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
352
360
  const effectiveTags = [...(tags ?? []), ...bucketTags];
@@ -413,6 +421,7 @@ export async function handler(
413
421
  const sorted = await hybridSearch(ctx, query, {
414
422
  kindFilter,
415
423
  categoryFilter: category || null,
424
+ excludeEvents: shouldExcludeEvents,
416
425
  since: effectiveSince,
417
426
  until: effectiveUntil,
418
427
  limit: fetchLimit,
@@ -490,6 +499,11 @@ export async function handler(
490
499
  filtered = filtered.filter((r) => r.tier !== "ephemeral");
491
500
  }
492
501
 
502
+ // Event category filter: exclude events from semantic search by default
503
+ if (shouldExcludeEvents) {
504
+ filtered = filtered.filter((r) => r.category !== "event");
505
+ }
506
+
493
507
  if (!filtered.length) {
494
508
  if (autoWindowed) {
495
509
  const days = config.eventDecayDays || 30;