context-vault 3.5.0 → 3.6.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.
@@ -0,0 +1,288 @@
1
+ import { z } from 'zod';
2
+ import { captureAndIndex } from '@context-vault/core/capture';
3
+ import { execSync } from 'node:child_process';
4
+ import { ok, ensureVaultExists } from '../helpers.js';
5
+ const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
6
+ const MAX_ENTRIES_PER_SESSION = 5;
7
+ const PROMPT_TEMPLATE = `## Session End: Knowledge Capture
8
+
9
+ Before this session ends, take a moment to capture what was learned.
10
+
11
+ **Answer these questions (skip any that don't apply):**
12
+
13
+ 1. **What was accomplished?**
14
+ List the key outcomes of this session.
15
+
16
+ 2. **What was learned?** (most important)
17
+ Non-obvious findings, gotchas, or discoveries that would save time in a future session.
18
+ Skip anything derivable from reading the code or git history.
19
+
20
+ 3. **What decisions were made and why?**
21
+ Architectural choices, trade-offs, or scope decisions with their rationale.
22
+
23
+ 4. **What would save time for the next session?**
24
+ Blockers, context that took a while to build, or shortcuts discovered.
25
+
26
+ **Then call \`session_end\` again with your summary and any discrete discoveries:**
27
+
28
+ \`\`\`
29
+ session_end({
30
+ summary: "your summary text",
31
+ discoveries: [
32
+ { title: "Short descriptive title", body: "What was learned and why it matters", kind: "insight" },
33
+ { title: "Decision: chose X over Y", body: "Rationale and trade-offs", kind: "decision" }
34
+ ]
35
+ })
36
+ \`\`\``;
37
+ function detectProject() {
38
+ try {
39
+ const remote = execSync('git remote get-url origin 2>/dev/null', {
40
+ encoding: 'utf-8',
41
+ timeout: 3000,
42
+ stdio: ['pipe', 'pipe', 'pipe'],
43
+ }).trim();
44
+ if (remote) {
45
+ const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
46
+ if (match)
47
+ return match[1];
48
+ }
49
+ }
50
+ catch { }
51
+ try {
52
+ const cwd = process.cwd();
53
+ const parts = cwd.split(/[/\\]/);
54
+ return parts[parts.length - 1];
55
+ }
56
+ catch { }
57
+ return null;
58
+ }
59
+ function classifyDiscovery(body) {
60
+ const lower = body.toLowerCase();
61
+ if (/chose|decided|picked|went with|trade.?off|alternative/i.test(lower))
62
+ return 'architectural';
63
+ if (/bug|fix|error|crash|broke|regression/i.test(lower))
64
+ return 'bugfix';
65
+ if (/pattern|convention|approach|technique/i.test(lower))
66
+ return 'pattern';
67
+ if (/api|endpoint|library|framework|dependency/i.test(lower))
68
+ return 'integration';
69
+ return 'general';
70
+ }
71
+ function extractInsightsFromSummary(summary) {
72
+ const insights = [];
73
+ const sections = summary.split(/\n(?=#+\s|(?:\d+\.|\*|-)\s+\*\*)/);
74
+ for (const section of sections) {
75
+ const lower = section.toLowerCase();
76
+ if (/\blearned\b|\bdiscover|\bgotcha|\bnon.?obvious|\bsurpris|\bunexpect|\bworkaround/.test(lower)) {
77
+ const lines = section.split('\n').filter(l => l.trim());
78
+ const bullets = lines.filter(l => /^\s*[-*]\s/.test(l));
79
+ if (bullets.length > 0) {
80
+ for (const bullet of bullets) {
81
+ const text = bullet.replace(/^\s*[-*]\s+/, '').trim();
82
+ if (text.length > 20) {
83
+ insights.push({
84
+ title: text.length > 120 ? text.slice(0, 117) + '...' : text,
85
+ body: text,
86
+ kind: 'insight',
87
+ });
88
+ }
89
+ }
90
+ }
91
+ else if (section.trim().length > 30) {
92
+ const firstLine = lines[0]?.replace(/^#+\s*/, '').trim() || 'Session insight';
93
+ insights.push({
94
+ title: firstLine.length > 120 ? firstLine.slice(0, 117) + '...' : firstLine,
95
+ body: section.trim(),
96
+ kind: 'insight',
97
+ });
98
+ }
99
+ }
100
+ if (/\bdecision|\bdecided|\bchose|\btrade.?off/.test(lower)) {
101
+ const lines = section.split('\n').filter(l => l.trim());
102
+ const bullets = lines.filter(l => /^\s*[-*]\s/.test(l));
103
+ if (bullets.length > 0) {
104
+ for (const bullet of bullets) {
105
+ const text = bullet.replace(/^\s*[-*]\s+/, '').trim();
106
+ if (text.length > 20) {
107
+ insights.push({
108
+ title: text.length > 120 ? text.slice(0, 117) + '...' : text,
109
+ body: text,
110
+ kind: 'decision',
111
+ });
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return insights.slice(0, MAX_ENTRIES_PER_SESSION);
118
+ }
119
+ async function deduplicateAndSave(entries, ctx) {
120
+ const saved = [];
121
+ const skipped = [];
122
+ for (const entry of entries) {
123
+ const embeddingText = [entry.title, entry.body].filter(Boolean).join(' ');
124
+ let isDuplicate = false;
125
+ try {
126
+ const embedding = await ctx.embed(embeddingText);
127
+ if (embedding) {
128
+ const vecCount = ctx.db.prepare('SELECT COUNT(*) as c FROM vault_vec').get()?.c ?? 0;
129
+ if (vecCount > 0) {
130
+ const vecRows = ctx.db
131
+ .prepare('SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT 3')
132
+ .all(embedding, 3);
133
+ for (const vr of vecRows) {
134
+ const similarity = Math.max(0, 1 - vr.distance / 2);
135
+ if (similarity >= DEFAULT_SIMILARITY_THRESHOLD) {
136
+ const row = ctx.db
137
+ .prepare('SELECT id, title FROM vault WHERE rowid = ?')
138
+ .get(vr.rowid);
139
+ if (row) {
140
+ isDuplicate = true;
141
+ skipped.push({
142
+ title: entry.title,
143
+ reason: `similar to "${row.title || row.id}" (${(similarity * 100).toFixed(0)}%)`,
144
+ });
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ catch { }
153
+ if (isDuplicate)
154
+ continue;
155
+ try {
156
+ const result = await captureAndIndex(ctx, {
157
+ kind: entry.kind,
158
+ title: entry.title,
159
+ body: entry.body,
160
+ tags: entry.tags,
161
+ tier: entry.tier,
162
+ meta: entry.meta,
163
+ source: 'session-end',
164
+ });
165
+ saved.push({ id: result.id, title: entry.title, kind: entry.kind });
166
+ }
167
+ catch (e) {
168
+ skipped.push({
169
+ title: entry.title,
170
+ reason: `save failed: ${e.message}`,
171
+ });
172
+ }
173
+ }
174
+ return { saved, skipped };
175
+ }
176
+ export const name = 'session_end';
177
+ export const description = 'End-of-session knowledge capture. Call when a session ends to extract and save insights, decisions, and learnings. If called without a summary, returns a prompt template to guide the agent through knowledge capture.';
178
+ export const inputSchema = {
179
+ summary: z
180
+ .string()
181
+ .optional()
182
+ .describe('Session summary text covering what was accomplished, learned, decided, and what would help next time.'),
183
+ discoveries: z
184
+ .array(z.object({
185
+ title: z.string().describe('Short descriptive title for this discovery'),
186
+ body: z.string().describe('What was learned and why it matters'),
187
+ kind: z
188
+ .enum(['insight', 'decision', 'pattern', 'reference'])
189
+ .optional()
190
+ .describe('Entry kind (default: insight)'),
191
+ }))
192
+ .optional()
193
+ .describe('Explicit discoveries to save. Each becomes a vault entry with deduplication.'),
194
+ project: z
195
+ .string()
196
+ .optional()
197
+ .describe('Project name for bucket tagging. Auto-detected from git remote if not provided.'),
198
+ };
199
+ export async function handler({ summary, discoveries, project }, ctx, { ensureIndexed }) {
200
+ const { config } = ctx;
201
+ const vaultErr = ensureVaultExists(config);
202
+ if (vaultErr)
203
+ return vaultErr;
204
+ if (!summary && (!discoveries || discoveries.length === 0)) {
205
+ return ok(PROMPT_TEMPLATE);
206
+ }
207
+ await ensureIndexed({ blocking: false });
208
+ const effectiveProject = project?.trim() || detectProject();
209
+ const bucketTag = effectiveProject ? `bucket:${effectiveProject}` : null;
210
+ const baseTags = ['auto-session', ...(bucketTag ? [bucketTag] : [])];
211
+ const entriesToSave = [];
212
+ if (discoveries?.length) {
213
+ for (const d of discoveries.slice(0, MAX_ENTRIES_PER_SESSION)) {
214
+ const kind = d.kind || 'insight';
215
+ entriesToSave.push({
216
+ title: d.title,
217
+ body: d.body,
218
+ kind,
219
+ tags: [...baseTags],
220
+ tier: kind === 'decision' ? 'durable' : 'working',
221
+ meta: {
222
+ discovery_type: classifyDiscovery(d.body),
223
+ source: 'session-end-explicit',
224
+ },
225
+ });
226
+ }
227
+ }
228
+ if (summary) {
229
+ const extracted = extractInsightsFromSummary(summary);
230
+ const remainingSlots = MAX_ENTRIES_PER_SESSION - entriesToSave.length;
231
+ for (const e of extracted.slice(0, remainingSlots)) {
232
+ entriesToSave.push({
233
+ title: e.title,
234
+ body: e.body,
235
+ kind: e.kind,
236
+ tags: [...baseTags],
237
+ tier: e.kind === 'decision' ? 'durable' : 'working',
238
+ meta: {
239
+ discovery_type: classifyDiscovery(e.body),
240
+ source: 'session-end-extracted',
241
+ },
242
+ });
243
+ }
244
+ entriesToSave.push({
245
+ title: `Session: ${effectiveProject || 'unknown'} ${new Date().toISOString().slice(0, 10)}`,
246
+ body: summary,
247
+ kind: 'session',
248
+ tags: [...baseTags],
249
+ tier: 'ephemeral',
250
+ meta: {
251
+ source: 'session-end',
252
+ discoveries_explicit: discoveries?.length ?? 0,
253
+ discoveries_extracted: extractInsightsFromSummary(summary).length,
254
+ },
255
+ });
256
+ }
257
+ if (entriesToSave.length === 0) {
258
+ return ok('No save-worthy content found in the provided summary. Session recorded without vault entries.');
259
+ }
260
+ const { saved, skipped } = await deduplicateAndSave(entriesToSave, ctx);
261
+ const lines = ['## Session End Summary\n'];
262
+ if (saved.length > 0) {
263
+ lines.push(`### Saved (${saved.length})\n`);
264
+ for (const s of saved) {
265
+ lines.push(`- **${s.title}** (\`${s.kind}\`) \`${s.id}\``);
266
+ }
267
+ lines.push('');
268
+ }
269
+ if (skipped.length > 0) {
270
+ lines.push(`### Skipped (${skipped.length})\n`);
271
+ for (const s of skipped) {
272
+ lines.push(`- **${s.title}**: ${s.reason}`);
273
+ }
274
+ lines.push('');
275
+ }
276
+ if (saved.length === 0 && skipped.length > 0) {
277
+ lines.push('_All discoveries matched existing entries. No new entries created._');
278
+ }
279
+ const result = ok(lines.join('\n'));
280
+ result._meta = {
281
+ project: effectiveProject,
282
+ saved_count: saved.length,
283
+ skipped_count: skipped.length,
284
+ saved_ids: saved.map(s => s.id),
285
+ };
286
+ return result;
287
+ }
288
+ //# sourceMappingURL=session-end.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-end.js","sourceRoot":"","sources":["../../src/tools/session-end.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "type": "module",
5
5
  "description": "Pure local engine: capture, index, search, and utilities for context-vault",
6
6
  "main": "dist/main.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -63,7 +63,7 @@
63
63
  "@context-vault/core"
64
64
  ],
65
65
  "dependencies": {
66
- "@context-vault/core": "^3.5.0",
66
+ "@context-vault/core": "^3.6.0",
67
67
  "@modelcontextprotocol/sdk": "^1.26.0",
68
68
  "adm-zip": "^0.5.16",
69
69
  "sqlite-vec": "^0.1.0"
@@ -21,6 +21,7 @@ import * as createSnapshot from './tools/create-snapshot.js';
21
21
  import * as sessionStart from './tools/session-start.js';
22
22
  import * as listBuckets from './tools/list-buckets.js';
23
23
  import * as ingestProject from './tools/ingest-project.js';
24
+ import * as sessionEnd from './tools/session-end.js';
24
25
 
25
26
  const toolModules = [
26
27
  getContext,
@@ -33,6 +34,7 @@ const toolModules = [
33
34
  clearContext,
34
35
  createSnapshot,
35
36
  sessionStart,
37
+ sessionEnd,
36
38
  listBuckets,
37
39
  ];
38
40
 
@@ -14,6 +14,47 @@ function relativeTime(ts: number): string {
14
14
  return `${hrs} hour${hrs === 1 ? '' : 's'} ago`;
15
15
  }
16
16
 
17
+ interface LearningRate {
18
+ saved7d: number;
19
+ saved30d: number;
20
+ sessions30d: number;
21
+ savesPerSession: string;
22
+ recalls30d: number;
23
+ recallToSaveRatio: string;
24
+ }
25
+
26
+ function computeLearningRate(ctx: LocalCtx): LearningRate | null {
27
+ try {
28
+ const saved7d = (ctx.db.prepare(
29
+ `SELECT COUNT(*) as c FROM vault WHERE created_at >= datetime('now', '-7 days') AND kind NOT IN ('bucket', 'session', 'brief')`
30
+ ).get() as any)?.c ?? 0;
31
+
32
+ const saved30d = (ctx.db.prepare(
33
+ `SELECT COUNT(*) as c FROM vault WHERE created_at >= datetime('now', '-30 days') AND kind NOT IN ('bucket', 'session', 'brief')`
34
+ ).get() as any)?.c ?? 0;
35
+
36
+ const sessions30d = (ctx.db.prepare(
37
+ `SELECT COUNT(*) as c FROM vault WHERE created_at >= datetime('now', '-30 days') AND kind = 'session'`
38
+ ).get() as any)?.c ?? 0;
39
+
40
+ const savesPerSession = sessions30d > 0
41
+ ? (saved30d / sessions30d).toFixed(1)
42
+ : '0.0';
43
+
44
+ const recalls30d = (ctx.db.prepare(
45
+ `SELECT SUM(recall_count) as c FROM vault WHERE last_recalled_at >= datetime('now', '-30 days')`
46
+ ).get() as any)?.c ?? 0;
47
+
48
+ const recallToSaveRatio = saved30d > 0
49
+ ? `${(recalls30d / saved30d).toFixed(1)}:1`
50
+ : recalls30d > 0 ? `${recalls30d}:0 (all recall, no save)` : 'n/a';
51
+
52
+ return { saved7d, saved30d, sessions30d, savesPerSession, recalls30d, recallToSaveRatio };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
17
58
  export const name = 'context_status';
18
59
 
19
60
  export const description =
@@ -114,6 +155,30 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
114
155
  }
115
156
  }
116
157
 
158
+ const learningRate = computeLearningRate(ctx);
159
+ if (learningRate) {
160
+ lines.push(``, `### Learning Rate`);
161
+ lines.push(`| Metric | Value |`);
162
+ lines.push(`|---|---|`);
163
+ lines.push(`| **Saved (7 days)** | ${learningRate.saved7d} |`);
164
+ lines.push(`| **Saved (30 days)** | ${learningRate.saved30d} |`);
165
+ lines.push(`| **Sessions (30 days)** | ${learningRate.sessions30d} |`);
166
+ if (learningRate.sessions30d > 0) {
167
+ lines.push(`| **Saves per session** | ${learningRate.savesPerSession} |`);
168
+ }
169
+ if (learningRate.recalls30d > 0 || learningRate.saved30d > 0) {
170
+ lines.push(`| **Recalls (30 days)** | ${learningRate.recalls30d} |`);
171
+ lines.push(`| **Recall:save ratio** | ${learningRate.recallToSaveRatio} |`);
172
+ }
173
+ if (learningRate.saved30d === 0) {
174
+ lines.push('');
175
+ lines.push('_No entries saved in 30 days. Knowledge may be getting lost between sessions._');
176
+ } else if (learningRate.sessions30d > 0 && learningRate.savesPerSession === '0.0') {
177
+ lines.push('');
178
+ lines.push('_Very low save rate. Consider using `session_end` to capture learnings._');
179
+ }
180
+ }
181
+
117
182
  if (status.stalePaths) {
118
183
  lines.push(``);
119
184
  lines.push(`### ⚠ Stale Paths`);
@@ -243,6 +243,22 @@ function checkStaleness(entry: any): { stale: boolean; stale_reason: string } |
243
243
  return null;
244
244
  }
245
245
 
246
+ const ERROR_TERMS = /\b(error|bug|fix|debug|failing|broken|crash|exception|issue|wrong|unexpected|stacktrace|traceback)\b/i;
247
+
248
+ function generateSaveHint(query: string | undefined, resultCount: number): string | null {
249
+ if (!query) return null;
250
+
251
+ if (ERROR_TERMS.test(query)) {
252
+ return 'If you solve this, save the root cause and fix as an insight.';
253
+ }
254
+
255
+ if (resultCount === 0) {
256
+ return 'No existing entries for this topic. If you learn something useful, save it for future sessions.';
257
+ }
258
+
259
+ return null;
260
+ }
261
+
246
262
  export const name = 'get_context';
247
263
 
248
264
  export const description =
@@ -743,6 +759,12 @@ export async function handler(
743
759
  if (consolidationSuggestions.length > 0) {
744
760
  meta.consolidation_suggestions = consolidationSuggestions;
745
761
  }
762
+
763
+ const saveHint = generateSaveHint(query, filtered.length);
764
+ if (saveHint) {
765
+ meta.save_hint = saveHint;
766
+ }
767
+
746
768
  result._meta = meta;
747
769
  return result;
748
770
  }
@@ -249,6 +249,27 @@ function validateSaveInput({
249
249
  return null;
250
250
  }
251
251
 
252
+ function enrichDecisionMeta(
253
+ mergedMeta: Record<string, any>,
254
+ title: string | undefined,
255
+ body: string | undefined
256
+ ): void {
257
+ const combined = [title, body].filter(Boolean).join(' ').toLowerCase();
258
+
259
+ if (/\b(architect|infra|schema|database|api|stack|deploy|migration)\b/.test(combined)) {
260
+ mergedMeta.decision_type = 'architectural';
261
+ } else if (/\b(scope|feature|requirement|priority|roadmap|milestone)\b/.test(combined)) {
262
+ mergedMeta.decision_type = 'product';
263
+ } else if (/\b(convention|style|naming|lint|format|pattern)\b/.test(combined)) {
264
+ mergedMeta.decision_type = 'convention';
265
+ } else {
266
+ mergedMeta.decision_type = 'general';
267
+ }
268
+
269
+ mergedMeta.alternatives_noted = /\b(alternative|instead of|over|rather than|compared to|vs\.?|versus|option|considered)\b/i.test(combined);
270
+ mergedMeta.has_rationale = /\b(because|reason|rationale|why|trade.?off|benefit|downside|pro|con)\b/i.test(combined);
271
+ }
272
+
252
273
  export const name = 'save_context';
253
274
 
254
275
  export const description =
@@ -587,6 +608,10 @@ export async function handler(
587
608
  mergedMeta.encoding_context = parsedCtx.text;
588
609
  }
589
610
 
611
+ if (normalizedKind === 'decision') {
612
+ enrichDecisionMeta(mergedMeta, title, body);
613
+ }
614
+
590
615
  const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
591
616
 
592
617
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);