context-vault 3.4.4 → 3.5.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 (86) hide show
  1. package/assets/agent-rules.md +50 -0
  2. package/assets/setup-prompt.md +58 -0
  3. package/assets/skills/vault-setup/skill.md +81 -0
  4. package/bin/cli.js +753 -126
  5. package/dist/helpers.d.ts +2 -0
  6. package/dist/helpers.d.ts.map +1 -1
  7. package/dist/helpers.js +23 -0
  8. package/dist/helpers.js.map +1 -1
  9. package/dist/server.js +26 -2
  10. package/dist/server.js.map +1 -1
  11. package/dist/status.d.ts.map +1 -1
  12. package/dist/status.js +29 -0
  13. package/dist/status.js.map +1 -1
  14. package/dist/tools/context-status.d.ts.map +1 -1
  15. package/dist/tools/context-status.js +64 -29
  16. package/dist/tools/context-status.js.map +1 -1
  17. package/dist/tools/get-context.js +23 -18
  18. package/dist/tools/get-context.js.map +1 -1
  19. package/dist/tools/list-context.d.ts +2 -1
  20. package/dist/tools/list-context.d.ts.map +1 -1
  21. package/dist/tools/list-context.js +27 -10
  22. package/dist/tools/list-context.js.map +1 -1
  23. package/dist/tools/save-context.d.ts +2 -1
  24. package/dist/tools/save-context.d.ts.map +1 -1
  25. package/dist/tools/save-context.js +95 -26
  26. package/dist/tools/save-context.js.map +1 -1
  27. package/dist/tools/session-start.d.ts.map +1 -1
  28. package/dist/tools/session-start.js +230 -11
  29. package/dist/tools/session-start.js.map +1 -1
  30. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  31. package/node_modules/@context-vault/core/dist/capture.js +13 -0
  32. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  33. package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
  34. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  35. package/node_modules/@context-vault/core/dist/config.js +47 -2
  36. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  37. package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
  38. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  39. package/node_modules/@context-vault/core/dist/constants.js +13 -0
  40. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  41. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  42. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  43. package/node_modules/@context-vault/core/dist/db.js +73 -9
  44. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  45. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  46. package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
  47. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  48. package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
  49. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  50. package/node_modules/@context-vault/core/dist/index.js +58 -10
  51. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  52. package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
  53. package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
  54. package/node_modules/@context-vault/core/dist/indexing.js +22 -0
  55. package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
  56. package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
  57. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  58. package/node_modules/@context-vault/core/dist/main.js +3 -1
  59. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  60. package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
  61. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  62. package/node_modules/@context-vault/core/dist/search.js +82 -6
  63. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  64. package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
  65. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  66. package/node_modules/@context-vault/core/package.json +5 -1
  67. package/node_modules/@context-vault/core/src/capture.ts +11 -0
  68. package/node_modules/@context-vault/core/src/config.ts +40 -2
  69. package/node_modules/@context-vault/core/src/constants.ts +15 -0
  70. package/node_modules/@context-vault/core/src/db.ts +73 -9
  71. package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
  72. package/node_modules/@context-vault/core/src/index.ts +65 -11
  73. package/node_modules/@context-vault/core/src/indexing.ts +35 -0
  74. package/node_modules/@context-vault/core/src/main.ts +5 -0
  75. package/node_modules/@context-vault/core/src/search.ts +96 -6
  76. package/node_modules/@context-vault/core/src/types.ts +26 -0
  77. package/package.json +2 -2
  78. package/scripts/prepack.js +17 -0
  79. package/src/helpers.ts +25 -0
  80. package/src/server.ts +28 -2
  81. package/src/status.ts +35 -0
  82. package/src/tools/context-status.ts +65 -30
  83. package/src/tools/get-context.ts +24 -24
  84. package/src/tools/list-context.ts +25 -13
  85. package/src/tools/save-context.ts +106 -29
  86. package/src/tools/session-start.ts +257 -13
@@ -1,6 +1,9 @@
1
1
  import { z } from 'zod';
2
2
  import { execSync } from 'node:child_process';
3
- import { ok, err, ensureVaultExists } from '../helpers.js';
3
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { homedir } from 'node:os';
6
+ import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
4
7
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
5
8
 
6
9
  const DEFAULT_MAX_TOKENS = 4000;
@@ -9,6 +12,115 @@ const MAX_BODY_PER_ENTRY = 400;
9
12
  const PRIORITY_KINDS = ['decision', 'insight', 'pattern'];
10
13
  const SESSION_SUMMARY_KIND = 'session';
11
14
 
15
+ interface AutoMemoryEntry {
16
+ file: string;
17
+ name: string;
18
+ description: string;
19
+ type: string;
20
+ body: string;
21
+ }
22
+
23
+ interface AutoMemoryResult {
24
+ detected: boolean;
25
+ path: string | null;
26
+ entries: AutoMemoryEntry[];
27
+ linesUsed: number;
28
+ }
29
+
30
+ /**
31
+ * Detect the Claude Code auto-memory directory for the current project.
32
+ * Convention: ~/.claude/projects/-<cwd-with-slashes-replaced-by-dashes>/memory/
33
+ */
34
+ function detectAutoMemoryPath(): string | null {
35
+ try {
36
+ const cwd = process.cwd();
37
+ // Claude Code project key: absolute path with / replaced by -, leading - kept
38
+ const projectKey = cwd.replace(/\//g, '-');
39
+ const memoryDir = join(homedir(), '.claude', 'projects', projectKey, 'memory');
40
+ const memoryIndex = join(memoryDir, 'MEMORY.md');
41
+ if (existsSync(memoryIndex)) return memoryDir;
42
+ } catch {}
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Parse YAML-ish frontmatter from a memory file.
48
+ * Returns { name, description, type } and the body after frontmatter.
49
+ */
50
+ function parseMemoryFile(content: string): { name: string; description: string; type: string; body: string } {
51
+ const result = { name: '', description: '', type: '', body: content };
52
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
53
+ if (!fmMatch) return result;
54
+
55
+ const frontmatter = fmMatch[1];
56
+ result.body = fmMatch[2].trim();
57
+
58
+ for (const line of frontmatter.split('\n')) {
59
+ const kv = line.match(/^(\w+)\s*:\s*(.+)$/);
60
+ if (!kv) continue;
61
+ const [, key, val] = kv;
62
+ if (key === 'name') result.name = val.trim();
63
+ else if (key === 'description') result.description = val.trim();
64
+ else if (key === 'type') result.type = val.trim();
65
+ }
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Read and parse all auto-memory entries from a memory directory.
71
+ */
72
+ function readAutoMemory(memoryDir: string): AutoMemoryResult {
73
+ const indexPath = join(memoryDir, 'MEMORY.md');
74
+ let linesUsed = 0;
75
+
76
+ try {
77
+ const indexContent = readFileSync(indexPath, 'utf-8');
78
+ linesUsed = indexContent.split('\n').length;
79
+ } catch {
80
+ return { detected: true, path: memoryDir, entries: [], linesUsed: 0 };
81
+ }
82
+
83
+ const entries: AutoMemoryEntry[] = [];
84
+
85
+ try {
86
+ const files = readdirSync(memoryDir).filter(
87
+ (f) => f.endsWith('.md') && f !== 'MEMORY.md'
88
+ );
89
+
90
+ for (const file of files) {
91
+ try {
92
+ const content = readFileSync(join(memoryDir, file), 'utf-8');
93
+ const parsed = parseMemoryFile(content);
94
+ entries.push({
95
+ file,
96
+ name: parsed.name || file.replace('.md', ''),
97
+ description: parsed.description,
98
+ type: parsed.type,
99
+ body: parsed.body,
100
+ });
101
+ } catch {}
102
+ }
103
+ } catch {}
104
+
105
+ return { detected: true, path: memoryDir, entries, linesUsed };
106
+ }
107
+
108
+ /**
109
+ * Build a search context string from auto-memory entries.
110
+ * Used to boost vault retrieval relevance.
111
+ */
112
+ function buildAutoMemoryContext(entries: AutoMemoryEntry[]): string {
113
+ if (entries.length === 0) return '';
114
+ const parts = entries
115
+ .map((e) => {
116
+ const desc = e.description ? `: ${e.description}` : '';
117
+ // Include a snippet of the body for richer context
118
+ const bodySnippet = e.body ? ` -- ${e.body.slice(0, 200)}` : '';
119
+ return `${e.name}${desc}${bodySnippet}`;
120
+ });
121
+ return parts.join('. ');
122
+ }
123
+
12
124
  export const name = 'session_start';
13
125
 
14
126
  export const description =
@@ -67,11 +179,13 @@ function estimateTokens(text: string | null | undefined): number {
67
179
 
68
180
  function formatEntry(entry: any): string {
69
181
  const tags = entry.tags ? JSON.parse(entry.tags) : [];
70
- const tagStr = tags.length ? tags.join(', ') : 'none';
71
- const date = entry.updated_at || entry.created_at || 'unknown';
182
+ const tagStr = tags.length ? tags.join(', ') : '';
183
+ const date = fmtDate(entry.updated_at || entry.created_at);
184
+ const icon = kindIcon(entry.kind);
185
+ const meta = [`\`${entry.kind}\``, tagStr, date].filter(Boolean).join(' · ');
72
186
  return [
73
- `- **${entry.title || '(untitled)'}** [${entry.kind}]`,
74
- ` tags: ${tagStr} | ${date} | id: \`${entry.id}\``,
187
+ `- ${icon} **${entry.title || '(untitled)'}**`,
188
+ ` ${meta} · \`${entry.id}\``,
75
189
  ` ${truncateBody(entry.body).replace(/\n+/g, ' ').trim()}`,
76
190
  ].join('\n');
77
191
  }
@@ -88,6 +202,30 @@ export async function handler(
88
202
 
89
203
  await ensureIndexed();
90
204
 
205
+ // Sanity check: compare DB entries vs disk files
206
+ let indexWarning = '';
207
+ try {
208
+ const dbCount = (ctx.db.prepare('SELECT COUNT(*) as cnt FROM vault').get() as any)?.cnt ?? 0;
209
+ let diskCount = 0;
210
+ const walk = (dir: string, depth = 0) => {
211
+ if (depth > 3 || diskCount >= 100) return;
212
+ try {
213
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
214
+ if (diskCount >= 100) return;
215
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '_archive') {
216
+ walk(`${dir}/${entry.name}`, depth + 1);
217
+ } else if (entry.name.endsWith('.md')) {
218
+ diskCount++;
219
+ }
220
+ }
221
+ } catch {}
222
+ };
223
+ walk(config.vaultDir);
224
+ if (diskCount >= 100 && dbCount < diskCount / 10) {
225
+ indexWarning = `\n> **WARNING:** Vault has significantly more files on disk (~${diskCount}+) than indexed entries (${dbCount}). The search index may be out of sync. Run \`context-vault reconnect\` to fix.\n`;
226
+ }
227
+ } catch {}
228
+
91
229
  const effectiveProject = project?.trim() || detectProject();
92
230
  const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
93
231
 
@@ -96,13 +234,23 @@ export async function handler(
96
234
 
97
235
  const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
98
236
 
237
+ // Auto-detect Claude Code auto-memory
238
+ const autoMemoryPath = detectAutoMemoryPath();
239
+ const autoMemory: AutoMemoryResult = autoMemoryPath
240
+ ? readAutoMemory(autoMemoryPath)
241
+ : { detected: false, path: null, entries: [], linesUsed: 0 };
242
+ const autoMemoryContext = buildAutoMemoryContext(autoMemory.entries);
243
+
99
244
  const sections = [];
100
245
  let tokensUsed = 0;
101
246
 
102
- sections.push(`# Session Brief${effectiveProject ? ` ${effectiveProject}` : ''}`);
247
+ sections.push(`# Session Brief${effectiveProject ? ` -- ${effectiveProject}` : ''}`);
103
248
  const bucketsLabel = buckets?.length ? ` | buckets: ${buckets.join(', ')}` : '';
249
+ const autoMemoryLabel = autoMemory.detected
250
+ ? ` | auto-memory: ${autoMemory.entries.length} entries detected, used as search context`
251
+ : '';
104
252
  sections.push(
105
- `_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}_\n`
253
+ `_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}${autoMemoryLabel}_\n`
106
254
  );
107
255
  tokensUsed += estimateTokens(sections.join('\n'));
108
256
 
@@ -118,12 +266,13 @@ export async function handler(
118
266
  }
119
267
  }
120
268
 
269
+ // When auto-memory context is available, boost decisions query with FTS
121
270
  const decisions = queryByKinds(
122
271
  ctx,
123
272
  PRIORITY_KINDS,
124
273
  sinceDate,
125
-
126
- effectiveTags
274
+ effectiveTags,
275
+ autoMemoryContext
127
276
  );
128
277
  if (decisions.length > 0) {
129
278
  const header = '## Active Decisions, Insights & Patterns\n';
@@ -175,6 +324,10 @@ export async function handler(
175
324
  return true;
176
325
  }).length;
177
326
 
327
+ if (indexWarning) {
328
+ sections.push(indexWarning);
329
+ }
330
+
178
331
  sections.push('---');
179
332
  sections.push(
180
333
  `_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || 'unscoped'}_`
@@ -191,6 +344,11 @@ export async function handler(
191
344
  decisions: decisions.length,
192
345
  recent: deduped.length,
193
346
  },
347
+ auto_memory: {
348
+ detected: autoMemory.detected,
349
+ entries: autoMemory.entries.length,
350
+ lines_used: autoMemory.linesUsed,
351
+ },
194
352
  };
195
353
  return result;
196
354
  }
@@ -223,7 +381,8 @@ function queryByKinds(
223
381
  ctx: LocalCtx,
224
382
  kinds: string[],
225
383
  since: string,
226
- effectiveTags: string[]
384
+ effectiveTags: string[],
385
+ autoMemoryContext = ''
227
386
  ): any[] {
228
387
  const kindPlaceholders = kinds.map(() => '?').join(',');
229
388
  const clauses = [`kind IN (${kindPlaceholders})`];
@@ -238,20 +397,105 @@ function queryByKinds(
238
397
  clauses.push('superseded_by IS NULL');
239
398
 
240
399
  const where = `WHERE ${clauses.join(' AND ')}`;
241
- const rows = ctx.db
400
+ let rows = ctx.db
242
401
  .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
243
- .all(...params);
402
+ .all(...params) as any[];
244
403
 
245
404
  if (effectiveTags.length) {
246
405
  const tagged = rows.filter((r: any) => {
247
406
  const tags = r.tags ? JSON.parse(r.tags) : [];
248
407
  return effectiveTags.some((t: string) => tags.includes(t));
249
408
  });
250
- if (tagged.length > 0) return tagged;
409
+ if (tagged.length > 0) rows = tagged;
410
+ }
411
+
412
+ // When auto-memory context is available, boost results by FTS relevance
413
+ // to surface vault entries that match what the user's auto-memory says they care about
414
+ if (autoMemoryContext && rows.length > 1) {
415
+ rows = boostByAutoMemory(ctx, rows, autoMemoryContext);
251
416
  }
417
+
252
418
  return rows;
253
419
  }
254
420
 
421
+ /**
422
+ * Re-rank vault entries by FTS relevance to auto-memory context.
423
+ * Entries matching auto-memory topics float to the top while preserving
424
+ * all original entries (non-matching ones keep their original order at the end).
425
+ */
426
+ function boostByAutoMemory(ctx: LocalCtx, rows: any[], context: string): any[] {
427
+ // Extract meaningful keywords from auto-memory context for FTS
428
+ const keywords = extractKeywords(context);
429
+ if (keywords.length === 0) return rows;
430
+
431
+ // Build FTS query from auto-memory keywords
432
+ const ftsTerms = keywords.slice(0, 10).map((k) => `"${k}"`).join(' OR ');
433
+ const rowIds = new Set(rows.map((r: any) => r.id));
434
+
435
+ try {
436
+ const ftsRows = ctx.db
437
+ .prepare(
438
+ `SELECT e.id, rank FROM vault_fts f JOIN vault e ON f.rowid = e.rowid WHERE vault_fts MATCH ? ORDER BY rank LIMIT 50`
439
+ )
440
+ .all(ftsTerms) as { id: string; rank: number }[];
441
+
442
+ // Build a boost map: entries matching auto-memory context get priority
443
+ const boostMap = new Map<string, number>();
444
+ for (const fr of ftsRows) {
445
+ if (rowIds.has(fr.id)) {
446
+ boostMap.set(fr.id, fr.rank);
447
+ }
448
+ }
449
+
450
+ if (boostMap.size === 0) return rows;
451
+
452
+ // Sort: boosted entries first (by FTS rank), then unboosted in original order
453
+ const boosted = rows.filter((r: any) => boostMap.has(r.id));
454
+ const unboosted = rows.filter((r: any) => !boostMap.has(r.id));
455
+ boosted.sort((a: any, b: any) => (boostMap.get(a.id) ?? 0) - (boostMap.get(b.id) ?? 0));
456
+ return [...boosted, ...unboosted];
457
+ } catch {
458
+ // FTS errors are non-fatal; return original order
459
+ return rows;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Extract meaningful keywords from auto-memory context text.
465
+ * Filters out common stop words and short tokens.
466
+ */
467
+ function extractKeywords(text: string): string[] {
468
+ const stopWords = new Set([
469
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
470
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
471
+ 'should', 'may', 'might', 'shall', 'can', 'need', 'must', 'ought',
472
+ 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'for', 'with',
473
+ 'from', 'into', 'during', 'before', 'after', 'above', 'below',
474
+ 'to', 'of', 'in', 'on', 'at', 'by', 'about', 'between', 'through',
475
+ 'this', 'that', 'these', 'those', 'it', 'its', 'they', 'them',
476
+ 'their', 'we', 'our', 'you', 'your', 'he', 'she', 'him', 'her',
477
+ 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other',
478
+ 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very',
479
+ 'just', 'also', 'now', 'then', 'here', 'there', 'when', 'where',
480
+ 'how', 'what', 'which', 'who', 'whom', 'why', 'if', 'because',
481
+ 'as', 'until', 'while', 'use', 'using', 'used',
482
+ ]);
483
+
484
+ const words = text
485
+ .toLowerCase()
486
+ .replace(/[^a-z0-9\s-]/g, ' ')
487
+ .split(/\s+/)
488
+ .filter((w) => w.length >= 3 && !stopWords.has(w));
489
+
490
+ // Deduplicate while preserving order
491
+ const seen = new Set<string>();
492
+ return words.filter((w) => {
493
+ if (seen.has(w)) return false;
494
+ seen.add(w);
495
+ return true;
496
+ });
497
+ }
498
+
255
499
  function queryRecent(ctx: LocalCtx, since: string, effectiveTags: string[]): any[] {
256
500
  const clauses = ['created_at >= ?'];
257
501
  const params = [since];