context-vault 3.12.0 → 3.16.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 (89) hide show
  1. package/bin/cli.js +283 -12
  2. package/dist/error-log.d.ts +2 -0
  3. package/dist/error-log.d.ts.map +1 -1
  4. package/dist/error-log.js +31 -1
  5. package/dist/error-log.js.map +1 -1
  6. package/dist/server.js +32 -2
  7. package/dist/server.js.map +1 -1
  8. package/dist/status.d.ts.map +1 -1
  9. package/dist/status.js +17 -0
  10. package/dist/status.js.map +1 -1
  11. package/dist/tools/context-status.d.ts.map +1 -1
  12. package/dist/tools/context-status.js +26 -1
  13. package/dist/tools/context-status.js.map +1 -1
  14. package/dist/tools/delete-context.d.ts +1 -1
  15. package/dist/tools/delete-context.d.ts.map +1 -1
  16. package/dist/tools/delete-context.js +15 -2
  17. package/dist/tools/delete-context.js.map +1 -1
  18. package/dist/tools/get-context.d.ts.map +1 -1
  19. package/dist/tools/get-context.js +3 -2
  20. package/dist/tools/get-context.js.map +1 -1
  21. package/dist/tools/list-context.d.ts +7 -15
  22. package/dist/tools/list-context.d.ts.map +1 -1
  23. package/dist/tools/list-context.js +570 -111
  24. package/dist/tools/list-context.js.map +1 -1
  25. package/dist/tools/publish-to-team.js +1 -1
  26. package/dist/tools/publish-to-team.js.map +1 -1
  27. package/dist/tools/save-context.js +2 -2
  28. package/dist/tools/save-context.js.map +1 -1
  29. package/dist/tools/session-start.d.ts +20 -7
  30. package/dist/tools/session-start.d.ts.map +1 -1
  31. package/dist/tools/session-start.js +406 -439
  32. package/dist/tools/session-start.js.map +1 -1
  33. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  34. package/node_modules/@context-vault/core/dist/capture.js +4 -0
  35. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  36. package/node_modules/@context-vault/core/dist/categories.d.ts.map +1 -1
  37. package/node_modules/@context-vault/core/dist/categories.js +8 -0
  38. package/node_modules/@context-vault/core/dist/categories.js.map +1 -1
  39. package/node_modules/@context-vault/core/dist/compact.d.ts +38 -0
  40. package/node_modules/@context-vault/core/dist/compact.d.ts.map +1 -0
  41. package/node_modules/@context-vault/core/dist/compact.js +127 -0
  42. package/node_modules/@context-vault/core/dist/compact.js.map +1 -0
  43. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  44. package/node_modules/@context-vault/core/dist/config.js +12 -0
  45. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  46. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  47. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  48. package/node_modules/@context-vault/core/dist/db.js +40 -4
  49. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  50. package/node_modules/@context-vault/core/dist/main.d.ts +6 -2
  51. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  52. package/node_modules/@context-vault/core/dist/main.js +5 -1
  53. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  54. package/node_modules/@context-vault/core/dist/search.d.ts +13 -1
  55. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  56. package/node_modules/@context-vault/core/dist/search.js +50 -5
  57. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  58. package/node_modules/@context-vault/core/dist/tier-analysis.d.ts +36 -0
  59. package/node_modules/@context-vault/core/dist/tier-analysis.d.ts.map +1 -0
  60. package/node_modules/@context-vault/core/dist/tier-analysis.js +227 -0
  61. package/node_modules/@context-vault/core/dist/tier-analysis.js.map +1 -0
  62. package/node_modules/@context-vault/core/dist/types.d.ts +12 -0
  63. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  64. package/node_modules/@context-vault/core/dist/watch.d.ts +21 -0
  65. package/node_modules/@context-vault/core/dist/watch.d.ts.map +1 -0
  66. package/node_modules/@context-vault/core/dist/watch.js +230 -0
  67. package/node_modules/@context-vault/core/dist/watch.js.map +1 -0
  68. package/node_modules/@context-vault/core/package.json +13 -1
  69. package/node_modules/@context-vault/core/src/capture.ts +4 -0
  70. package/node_modules/@context-vault/core/src/categories.ts +8 -0
  71. package/node_modules/@context-vault/core/src/compact.ts +183 -0
  72. package/node_modules/@context-vault/core/src/config.ts +8 -0
  73. package/node_modules/@context-vault/core/src/db.ts +40 -4
  74. package/node_modules/@context-vault/core/src/main.ts +10 -0
  75. package/node_modules/@context-vault/core/src/search.ts +55 -4
  76. package/node_modules/@context-vault/core/src/tier-analysis.ts +299 -0
  77. package/node_modules/@context-vault/core/src/types.ts +10 -0
  78. package/node_modules/@context-vault/core/src/watch.ts +269 -0
  79. package/package.json +2 -2
  80. package/src/error-log.ts +30 -0
  81. package/src/server.ts +31 -2
  82. package/src/status.ts +17 -0
  83. package/src/tools/context-status.ts +30 -1
  84. package/src/tools/delete-context.ts +10 -5
  85. package/src/tools/get-context.ts +3 -2
  86. package/src/tools/list-context.ts +620 -119
  87. package/src/tools/publish-to-team.ts +1 -1
  88. package/src/tools/save-context.ts +2 -2
  89. package/src/tools/session-start.ts +444 -484
@@ -1,127 +1,586 @@
1
1
  import { z } from 'zod';
2
- import { normalizeKind } from '@context-vault/core/files';
3
- import { categoryFor } from '@context-vault/core/categories';
4
- import { ok, errWithHint, kindIcon, fmtDate } from '../helpers.js';
5
- import { resolveTemporalParams } from '../temporal.js';
6
- export const name = 'list_context';
7
- export const description = 'Browse vault entries without a search query. Returns id, title, kind, category, tags, created_at, updated_at. Use get_context with a query for semantic search. Use this to browse by tags or find recent entries.';
2
+ import { execSync } from 'node:child_process';
3
+ import { readdirSync } from 'node:fs';
4
+ import { ok, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
5
+ import { getAutoMemory } from '../auto-memory.js';
6
+ import { getRemoteClient, getTeamId, getPublicVaults } from '../remote.js';
7
+ const DEFAULT_MAX_TOKENS = 4000;
8
+ const RECENT_DAYS = 7;
9
+ const MAX_BODY_PER_ENTRY = 400;
10
+ const PRIORITY_KINDS = ['decision', 'insight', 'pattern'];
11
+ const SESSION_SUMMARY_KIND = 'session';
12
+ /**
13
+ * Build a search context string from auto-memory entries.
14
+ * Used to boost vault retrieval relevance.
15
+ */
16
+ function buildAutoMemoryContext(entries) {
17
+ if (entries.length === 0)
18
+ return '';
19
+ const parts = entries
20
+ .map((e) => {
21
+ const desc = e.description ? `: ${e.description}` : '';
22
+ // Include a snippet of the body for richer context
23
+ const bodySnippet = e.body ? ` -- ${e.body.slice(0, 200)}` : '';
24
+ return `${e.name}${desc}${bodySnippet}`;
25
+ });
26
+ return parts.join('. ');
27
+ }
28
+ export const name = 'session_start';
29
+ export const description = 'Auto-assemble a context brief for the current project on session start. Pulls recent entries, last session summary, and active decisions/blockers into a token-budgeted capsule formatted for agent consumption.';
8
30
  export const inputSchema = {
9
- kind: z.string().optional().describe("Filter by kind (e.g. 'insight', 'decision', 'pattern')"),
10
- category: z.enum(['knowledge', 'entity', 'event']).optional().describe('Filter by category'),
11
- tags: z.array(z.string()).optional().describe('Filter by tags (entries must match at least one)'),
12
- since: z.string().optional().describe('ISO date, return entries created after this'),
13
- until: z.string().optional().describe('ISO date, return entries created before this'),
14
- limit: z.number().optional().describe('Max results to return (default 20, max 100)'),
15
- offset: z.number().optional().describe('Skip first N results for pagination'),
16
- include_unindexed: z.boolean().optional().describe('If true, include entries that are stored but not indexed for search. Default: false.'),
31
+ project: z
32
+ .string()
33
+ .optional()
34
+ .describe('Project name or tag to scope the brief. Auto-detected from cwd/git remote if not provided.'),
35
+ max_tokens: z
36
+ .number()
37
+ .optional()
38
+ .describe('Token budget for the capsule (rough estimate: 1 token ~ 4 chars). Default: 4000.'),
39
+ buckets: z
40
+ .array(z.string())
41
+ .optional()
42
+ .describe("Bucket names to scope the session brief. Each name expands to a 'bucket:<name>' tag filter. When provided, the brief only includes entries from these buckets."),
43
+ auto_memory_path: z
44
+ .string()
45
+ .optional()
46
+ .describe("Explicit path to the Claude Code auto-memory directory. Overrides auto-detection. If not provided, session_start attempts to detect ~/.claude/projects/-<project-key>/memory/ from cwd."),
17
47
  };
18
- export async function handler({ kind, category, tags, since, until, limit, offset, include_unindexed }, ctx, { ensureIndexed, reindexFailed }) {
48
+ function detectProject() {
49
+ try {
50
+ const remote = execSync('git remote get-url origin 2>/dev/null', {
51
+ encoding: 'utf-8',
52
+ timeout: 3000,
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ }).trim();
55
+ if (remote) {
56
+ const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
57
+ if (match)
58
+ return match[1];
59
+ }
60
+ }
61
+ catch { }
62
+ try {
63
+ const cwd = process.cwd();
64
+ const parts = cwd.split(/[/\\]/);
65
+ return parts[parts.length - 1];
66
+ }
67
+ catch { }
68
+ return null;
69
+ }
70
+ function truncateBody(body, maxLen = MAX_BODY_PER_ENTRY) {
71
+ if (!body)
72
+ return '(no body)';
73
+ if (body.length <= maxLen)
74
+ return body;
75
+ return body.slice(0, maxLen) + '...';
76
+ }
77
+ function estimateTokens(text) {
78
+ return Math.ceil((text || '').length / 4);
79
+ }
80
+ function formatEntry(entry) {
81
+ const tags = entry.tags ? JSON.parse(entry.tags) : [];
82
+ const tagStr = tags.length ? tags.join(', ') : '';
83
+ const date = fmtDate(entry.updated_at || entry.created_at);
84
+ const icon = kindIcon(entry.kind);
85
+ const meta = [`\`${entry.kind}\``, tagStr, date].filter(Boolean).join(' · ');
86
+ return [
87
+ `- ${icon} **${entry.title || '(untitled)'}**`,
88
+ ` ${meta} · \`${entry.id}\``,
89
+ ` ${truncateBody(entry.body).replace(/\n+/g, ' ').trim()}`,
90
+ ].join('\n');
91
+ }
92
+ export async function handler({ project, max_tokens, buckets, auto_memory_path }, ctx, { ensureIndexed }) {
19
93
  const { config } = ctx;
94
+ const vaultErr = ensureVaultExists(config);
95
+ if (vaultErr)
96
+ return vaultErr;
20
97
  await ensureIndexed();
21
- const resolved = resolveTemporalParams({ since, until });
22
- since = resolved.since;
23
- until = resolved.until;
24
- const kindFilter = kind ? normalizeKind(kind) : null;
25
- const effectiveCategory = category || (kindFilter ? categoryFor(kindFilter) : null);
26
- let effectiveSince = since || null;
27
- let autoWindowed = false;
28
- if (effectiveCategory === 'event' && !since && !until) {
29
- const decayMs = (config.eventDecayDays || 30) * 86400000;
30
- effectiveSince = new Date(Date.now() - decayMs).toISOString();
31
- autoWindowed = true;
32
- }
33
- const clauses = [];
34
- const params = [];
35
- if (kindFilter) {
36
- clauses.push('kind = ?');
37
- params.push(kindFilter);
98
+ // Sanity check: compare DB entries vs disk files
99
+ let indexWarning = '';
100
+ try {
101
+ const dbCount = ctx.db.prepare('SELECT COUNT(*) as cnt FROM vault').get()?.cnt ?? 0;
102
+ let diskCount = 0;
103
+ const walk = (dir, depth = 0) => {
104
+ if (depth > 3 || diskCount >= 100)
105
+ return;
106
+ try {
107
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
108
+ if (diskCount >= 100)
109
+ return;
110
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '_archive') {
111
+ walk(`${dir}/${entry.name}`, depth + 1);
112
+ }
113
+ else if (entry.name.endsWith('.md')) {
114
+ diskCount++;
115
+ }
116
+ }
117
+ }
118
+ catch { }
119
+ };
120
+ walk(config.vaultDir);
121
+ if (diskCount >= 100 && dbCount < diskCount / 10) {
122
+ 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`;
123
+ }
38
124
  }
39
- if (category) {
40
- clauses.push('category = ?');
41
- params.push(category);
125
+ catch { }
126
+ const effectiveProject = project?.trim() || detectProject();
127
+ const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
128
+ const bucketTags = buckets?.length ? buckets.map((b) => `bucket:${b}`) : [];
129
+ const effectiveTags = bucketTags.length ? bucketTags : effectiveProject ? [effectiveProject] : [];
130
+ const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
131
+ // Auto-detect Claude Code auto-memory (explicit path overrides auto-detection)
132
+ const autoMemory = getAutoMemory(auto_memory_path);
133
+ const autoMemoryContext = buildAutoMemoryContext(autoMemory.entries);
134
+ const topicsExtracted = autoMemory.entries.length > 0
135
+ ? extractKeywords(autoMemoryContext).slice(0, 20)
136
+ : [];
137
+ const sections = [];
138
+ let tokensUsed = 0;
139
+ sections.push(`# Session Brief${effectiveProject ? ` -- ${effectiveProject}` : ''}`);
140
+ const bucketsLabel = buckets?.length ? ` | buckets: ${buckets.join(', ')}` : '';
141
+ const autoMemoryLabel = autoMemory.detected
142
+ ? ` | auto-memory: ${autoMemory.entries.length} entries detected, used as search context`
143
+ : '';
144
+ sections.push(`_Generated ${new Date().toISOString().slice(0, 10)} | budget: ${tokenBudget} tokens${bucketsLabel}${autoMemoryLabel}_\n`);
145
+ tokensUsed += estimateTokens(sections.join('\n'));
146
+ const lastSession = queryLastSession(ctx, effectiveTags);
147
+ if (lastSession) {
148
+ const sessionBlock = ['## Last Session Summary', truncateBody(lastSession.body, 600), ''].join('\n');
149
+ const sessionTokens = estimateTokens(sessionBlock);
150
+ if (tokensUsed + sessionTokens <= tokenBudget) {
151
+ sections.push(sessionBlock);
152
+ tokensUsed += sessionTokens;
153
+ }
42
154
  }
43
- if (effectiveSince) {
44
- clauses.push('created_at >= ?');
45
- params.push(effectiveSince);
155
+ // Hot entries: always loaded regardless of project/bucket scope
156
+ const hotEntries = queryHotEntries(ctx);
157
+ if (hotEntries.length > 0) {
158
+ const header = '## Hot Entries (frequently accessed)\n';
159
+ const headerTokens = estimateTokens(header);
160
+ if (tokensUsed + headerTokens <= tokenBudget) {
161
+ const entryLines = [];
162
+ tokensUsed += headerTokens;
163
+ for (const entry of hotEntries) {
164
+ const line = formatEntry(entry);
165
+ const lineTokens = estimateTokens(line);
166
+ if (tokensUsed + lineTokens > tokenBudget)
167
+ break;
168
+ entryLines.push(line);
169
+ tokensUsed += lineTokens;
170
+ }
171
+ if (entryLines.length > 0) {
172
+ sections.push(header + entryLines.join('\n') + '\n');
173
+ }
174
+ }
46
175
  }
47
- if (until) {
48
- clauses.push('created_at <= ?');
49
- params.push(until);
176
+ // When auto-memory context is available, boost decisions query with FTS
177
+ const decisions = queryByKinds(ctx, PRIORITY_KINDS, sinceDate, effectiveTags, autoMemoryContext);
178
+ if (decisions.length > 0) {
179
+ const header = '## Active Decisions, Insights & Patterns\n';
180
+ const headerTokens = estimateTokens(header);
181
+ if (tokensUsed + headerTokens <= tokenBudget) {
182
+ const entryLines = [];
183
+ tokensUsed += headerTokens;
184
+ for (const entry of decisions) {
185
+ const line = formatEntry(entry);
186
+ const lineTokens = estimateTokens(line);
187
+ if (tokensUsed + lineTokens > tokenBudget)
188
+ break;
189
+ entryLines.push(line);
190
+ tokensUsed += lineTokens;
191
+ }
192
+ if (entryLines.length > 0) {
193
+ sections.push(header + entryLines.join('\n') + '\n');
194
+ }
195
+ }
50
196
  }
51
- clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
52
- if (!include_unindexed) {
53
- clauses.push('indexed = 1');
54
- }
55
- const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
56
- const effectiveLimit = Math.min(limit || 20, 100);
57
- const effectiveOffset = offset || 0;
58
- // When tag-filtering, over-fetch to compensate for post-filter reduction
59
- const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
60
- const countParams = [...params];
61
- let total;
62
- let rows;
63
- try {
64
- total =
65
- ctx.db.prepare(`SELECT COUNT(*) as c FROM vault ${where}`).get(...countParams)?.c ??
66
- 0;
67
- params.push(fetchLimit, effectiveOffset);
68
- rows = ctx.db
69
- .prepare(`SELECT id, title, kind, category, tags, created_at, updated_at, indexed, recall_count, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
70
- .all(...params);
71
- }
72
- catch (e) {
73
- return errWithHint(e instanceof Error ? e.message : String(e), 'DB_ERROR', 'context-vault list_context DB_ERROR. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.');
74
- }
75
- // Post-filter by tags if provided, then apply requested limit
76
- const filtered = tags?.length
77
- ? rows
78
- .filter((r) => {
79
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
80
- return tags.some((t) => entryTags.includes(t));
81
- })
82
- .slice(0, effectiveLimit)
83
- : rows;
84
- if (!filtered.length) {
85
- if (autoWindowed) {
86
- const days = config.eventDecayDays || 30;
87
- return ok(`No entries found matching the given filters in events (last ${days} days).\nTry with \`since: "YYYY-MM-DD"\` to search older events.`);
197
+ const seenIdsForDurable = new Set([
198
+ ...decisions.map((d) => d.id),
199
+ ...(lastSession ? [lastSession.id] : []),
200
+ ]);
201
+ const durables = queryDurable(ctx, effectiveTags, seenIdsForDurable);
202
+ if (durables.length > 0) {
203
+ const header = '## Foundational Decisions\n';
204
+ const headerTokens = estimateTokens(header);
205
+ if (tokensUsed + headerTokens <= tokenBudget) {
206
+ const entryLines = [];
207
+ tokensUsed += headerTokens;
208
+ for (const entry of durables) {
209
+ const line = formatEntry(entry);
210
+ const lineTokens = estimateTokens(line);
211
+ if (tokensUsed + lineTokens > tokenBudget)
212
+ break;
213
+ entryLines.push(line);
214
+ tokensUsed += lineTokens;
215
+ }
216
+ if (entryLines.length > 0) {
217
+ sections.push(header + entryLines.join('\n') + '\n');
218
+ }
88
219
  }
89
- return ok('No entries found matching the given filters.');
90
- }
91
- const lines = [];
92
- if (reindexFailed)
93
- lines.push(`> **Warning:** Auto-reindex failed. Results may be stale. Run \`context-vault reindex\` to fix.\n`);
94
- lines.push(`## Vault Entries (${filtered.length} shown, ${total} total)\n`);
95
- if (autoWindowed) {
96
- const days = config.eventDecayDays || 30;
97
- lines.push(`> Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`);
98
- }
99
- if (include_unindexed) {
100
- lines.push('| | Title | Kind | Tags | Recalls | Idx | Date | ID |');
101
- lines.push('|---|---|---|---|---|---|---|---|');
102
- }
103
- else {
104
- lines.push('| | Title | Kind | Tags | Recalls | Date | ID |');
105
- lines.push('|---|---|---|---|---|---|---|');
106
- }
107
- for (const r of filtered) {
108
- const entryTags = r.tags ? JSON.parse(r.tags) : [];
109
- const tagStr = entryTags.length ? entryTags.join(', ') : '';
110
- const date = fmtDate(r.updated_at && r.updated_at !== r.created_at ? r.updated_at : r.created_at);
111
- const icon = kindIcon(r.kind);
112
- const title = (r.title || '(untitled)').replace(/\|/g, '\\|');
113
- const recalls = r.recall_count ?? 0;
114
- if (include_unindexed) {
115
- const idxStr = r.indexed ? 'Y' : 'N';
116
- lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${idxStr} | ${date} | \`${r.id}\` |`);
220
+ }
221
+ const recent = queryRecent(ctx, sinceDate, effectiveTags);
222
+ const seenIds = new Set(decisions.map((d) => d.id));
223
+ if (lastSession)
224
+ seenIds.add(lastSession.id);
225
+ durables.forEach((d) => seenIds.add(d.id));
226
+ const deduped = recent.filter((r) => !seenIds.has(r.id));
227
+ if (deduped.length > 0) {
228
+ const header = `## Recent Entries (last ${RECENT_DAYS} days)\n`;
229
+ const headerTokens = estimateTokens(header);
230
+ if (tokensUsed + headerTokens <= tokenBudget) {
231
+ const entryLines = [];
232
+ tokensUsed += headerTokens;
233
+ for (const entry of deduped) {
234
+ const line = formatEntry(entry);
235
+ const lineTokens = estimateTokens(line);
236
+ if (tokensUsed + lineTokens > tokenBudget)
237
+ break;
238
+ entryLines.push(line);
239
+ tokensUsed += lineTokens;
240
+ }
241
+ if (entryLines.length > 0) {
242
+ sections.push(header + entryLines.join('\n') + '\n');
243
+ }
244
+ }
245
+ }
246
+ // Remote entries: pull recent from hosted API if configured
247
+ const remoteClient = getRemoteClient(ctx.config);
248
+ let remoteCount = 0;
249
+ if (remoteClient && tokensUsed < tokenBudget) {
250
+ try {
251
+ const seenIds = new Set([
252
+ ...decisions.map((d) => d.id),
253
+ ...deduped.map((d) => d.id),
254
+ ...(lastSession ? [lastSession.id] : []),
255
+ ]);
256
+ const remoteTags = effectiveTags.length ? effectiveTags : undefined;
257
+ const remoteResults = await remoteClient.search({
258
+ tags: remoteTags,
259
+ limit: 10,
260
+ since: sinceDate,
261
+ });
262
+ const uniqueRemote = remoteResults.filter((r) => !seenIds.has(r.id));
263
+ if (uniqueRemote.length > 0) {
264
+ const header = '## Remote Entries\n';
265
+ const headerTokens = estimateTokens(header);
266
+ if (tokensUsed + headerTokens <= tokenBudget) {
267
+ const entryLines = [];
268
+ tokensUsed += headerTokens;
269
+ for (const entry of uniqueRemote) {
270
+ const line = formatEntry(entry);
271
+ const lineTokens = estimateTokens(line);
272
+ if (tokensUsed + lineTokens > tokenBudget)
273
+ break;
274
+ entryLines.push(line);
275
+ tokensUsed += lineTokens;
276
+ remoteCount++;
277
+ }
278
+ if (entryLines.length > 0) {
279
+ sections.push(header + entryLines.join('\n') + '\n');
280
+ }
281
+ }
282
+ }
283
+ }
284
+ catch (e) {
285
+ console.warn(`[context-vault] Remote session_start failed: ${e.message}`);
286
+ }
287
+ }
288
+ // Team vault entries: include team knowledge in brief if teamId is configured
289
+ let teamCount = 0;
290
+ const teamId = getTeamId(ctx.config);
291
+ if (remoteClient && teamId && tokensUsed < tokenBudget) {
292
+ try {
293
+ const allSeenIds = new Set([
294
+ ...decisions.map((d) => d.id),
295
+ ...deduped.map((d) => d.id),
296
+ ...(lastSession ? [lastSession.id] : []),
297
+ ]);
298
+ const teamResults = await remoteClient.teamSearch(teamId, {
299
+ tags: effectiveTags.length ? effectiveTags : undefined,
300
+ limit: 10,
301
+ since: sinceDate,
302
+ });
303
+ const uniqueTeam = teamResults.filter((r) => !allSeenIds.has(r.id));
304
+ if (uniqueTeam.length > 0) {
305
+ const header = '## Team Knowledge\n';
306
+ const headerTokens = estimateTokens(header);
307
+ if (tokensUsed + headerTokens <= tokenBudget) {
308
+ const entryLines = [];
309
+ tokensUsed += headerTokens;
310
+ for (const entry of uniqueTeam) {
311
+ const line = formatEntry(entry) + ' `[team]`';
312
+ const lineTokens = estimateTokens(line);
313
+ if (tokensUsed + lineTokens > tokenBudget)
314
+ break;
315
+ entryLines.push(line);
316
+ tokensUsed += lineTokens;
317
+ teamCount++;
318
+ }
319
+ if (entryLines.length > 0) {
320
+ sections.push(header + entryLines.join('\n') + '\n');
321
+ }
322
+ }
323
+ }
324
+ }
325
+ catch (e) {
326
+ console.warn(`[context-vault] Team session_start failed: ${e.message}`);
327
+ }
328
+ }
329
+ // Public vault entries: include public knowledge if publicVaults are configured
330
+ let publicCount = 0;
331
+ const publicVaultSlugs = getPublicVaults(ctx.config);
332
+ if (remoteClient && publicVaultSlugs.length > 0 && tokensUsed < tokenBudget) {
333
+ try {
334
+ const allPublicSeenIds = new Set([
335
+ ...decisions.map((d) => d.id),
336
+ ...deduped.map((d) => d.id),
337
+ ...(lastSession ? [lastSession.id] : []),
338
+ ]);
339
+ const publicSearches = publicVaultSlugs.map(slug => remoteClient.publicSearch(slug, {
340
+ tags: effectiveTags.length ? effectiveTags : undefined,
341
+ limit: 5,
342
+ since: sinceDate,
343
+ }).catch(() => []));
344
+ const allPublicResults = await Promise.all(publicSearches);
345
+ const flatPublic = allPublicResults.flat().filter((r) => !allPublicSeenIds.has(r.id));
346
+ if (flatPublic.length > 0) {
347
+ const header = '## Public Knowledge\n';
348
+ const headerTokens = estimateTokens(header);
349
+ if (tokensUsed + headerTokens <= tokenBudget) {
350
+ const entryLines = [];
351
+ tokensUsed += headerTokens;
352
+ for (const entry of flatPublic) {
353
+ const slug = entry.vault_slug || 'public';
354
+ const line = formatEntry(entry) + ` \`[public:${slug}]\``;
355
+ const lineTokens = estimateTokens(line);
356
+ if (tokensUsed + lineTokens > tokenBudget)
357
+ break;
358
+ entryLines.push(line);
359
+ tokensUsed += lineTokens;
360
+ publicCount++;
361
+ }
362
+ if (entryLines.length > 0) {
363
+ sections.push(header + entryLines.join('\n') + '\n');
364
+ }
365
+ }
366
+ }
117
367
  }
118
- else {
119
- lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${date} | \`${r.id}\` |`);
368
+ catch (e) {
369
+ console.warn(`[context-vault] Public vault session_start failed: ${e.message}`);
120
370
  }
121
371
  }
122
- if (effectiveOffset + effectiveLimit < total) {
123
- lines.push(`\n_Page ${Math.floor(effectiveOffset / effectiveLimit) + 1}. Use offset: ${effectiveOffset + effectiveLimit} for next page._`);
372
+ const totalEntries = (lastSession ? 1 : 0) +
373
+ decisions.length +
374
+ durables.length +
375
+ deduped.filter((_d) => {
376
+ return true;
377
+ }).length +
378
+ remoteCount +
379
+ teamCount +
380
+ publicCount;
381
+ if (indexWarning) {
382
+ sections.push(indexWarning);
383
+ }
384
+ sections.push('---');
385
+ sections.push(`_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || 'unscoped'}_`);
386
+ const result = ok(sections.join('\n'));
387
+ result._meta = {
388
+ project: effectiveProject || null,
389
+ buckets: buckets || null,
390
+ tokens_used: tokensUsed,
391
+ tokens_budget: tokenBudget,
392
+ sections: {
393
+ last_session: lastSession ? 1 : 0,
394
+ hot_entries: hotEntries.length,
395
+ decisions: decisions.length,
396
+ foundational: durables.length,
397
+ recent: deduped.length,
398
+ },
399
+ auto_memory: {
400
+ detected: autoMemory.detected,
401
+ path: autoMemory.path,
402
+ entries: autoMemory.entries.length,
403
+ lines_used: autoMemory.linesUsed,
404
+ topics_extracted: topicsExtracted,
405
+ },
406
+ };
407
+ return result;
408
+ }
409
+ function queryHotEntries(ctx) {
410
+ try {
411
+ return ctx.db
412
+ .prepare(`SELECT * FROM vault
413
+ WHERE heat_tier = 'hot'
414
+ AND superseded_by IS NULL
415
+ AND (expires_at IS NULL OR expires_at > datetime('now'))
416
+ AND indexed = 1
417
+ ORDER BY last_accessed_at DESC
418
+ LIMIT 10`)
419
+ .all();
420
+ }
421
+ catch {
422
+ return [];
423
+ }
424
+ }
425
+ function queryLastSession(ctx, effectiveTags) {
426
+ const clauses = [`kind = '${SESSION_SUMMARY_KIND}'`];
427
+ const params = [];
428
+ if (false) {
429
+ }
430
+ clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
431
+ clauses.push('superseded_by IS NULL');
432
+ const where = `WHERE ${clauses.join(' AND ')}`;
433
+ const rows = ctx.db
434
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 5`)
435
+ .all(...params);
436
+ if (effectiveTags.length) {
437
+ const match = rows.find((r) => {
438
+ const tags = r.tags ? JSON.parse(r.tags) : [];
439
+ return effectiveTags.some((t) => tags.includes(t));
440
+ });
441
+ if (match)
442
+ return match;
443
+ }
444
+ return rows[0] || null;
445
+ }
446
+ function queryByKinds(ctx, kinds, since, effectiveTags, autoMemoryContext = '') {
447
+ const kindPlaceholders = kinds.map(() => '?').join(',');
448
+ const clauses = [`kind IN (${kindPlaceholders})`];
449
+ const params = [...kinds];
450
+ clauses.push('created_at >= ?');
451
+ params.push(since);
452
+ if (false) {
453
+ }
454
+ clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
455
+ clauses.push('superseded_by IS NULL');
456
+ const where = `WHERE ${clauses.join(' AND ')}`;
457
+ let rows = ctx.db
458
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
459
+ .all(...params);
460
+ if (effectiveTags.length) {
461
+ const tagged = rows.filter((r) => {
462
+ const tags = r.tags ? JSON.parse(r.tags) : [];
463
+ return effectiveTags.some((t) => tags.includes(t));
464
+ });
465
+ if (tagged.length > 0)
466
+ rows = tagged;
467
+ }
468
+ // When auto-memory context is available, boost results by FTS relevance
469
+ // to surface vault entries that match what the user's auto-memory says they care about
470
+ if (autoMemoryContext && rows.length > 1) {
471
+ rows = boostByAutoMemory(ctx, rows, autoMemoryContext);
472
+ }
473
+ return rows;
474
+ }
475
+ /**
476
+ * Re-rank vault entries by FTS relevance to auto-memory context.
477
+ * Entries matching auto-memory topics float to the top while preserving
478
+ * all original entries (non-matching ones keep their original order at the end).
479
+ */
480
+ function boostByAutoMemory(ctx, rows, context) {
481
+ // Extract meaningful keywords from auto-memory context for FTS
482
+ const keywords = extractKeywords(context);
483
+ if (keywords.length === 0)
484
+ return rows;
485
+ // Build FTS query from auto-memory keywords
486
+ const ftsTerms = keywords.slice(0, 10).map((k) => `"${k}"`).join(' OR ');
487
+ const rowIds = new Set(rows.map((r) => r.id));
488
+ try {
489
+ const ftsRows = ctx.db
490
+ .prepare(`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`)
491
+ .all(ftsTerms);
492
+ // Build a boost map: entries matching auto-memory context get priority
493
+ const boostMap = new Map();
494
+ for (const fr of ftsRows) {
495
+ if (rowIds.has(fr.id)) {
496
+ boostMap.set(fr.id, fr.rank);
497
+ }
498
+ }
499
+ if (boostMap.size === 0)
500
+ return rows;
501
+ // Sort: boosted entries first (by FTS rank), then unboosted in original order
502
+ const boosted = rows.filter((r) => boostMap.has(r.id));
503
+ const unboosted = rows.filter((r) => !boostMap.has(r.id));
504
+ boosted.sort((a, b) => (boostMap.get(a.id) ?? 0) - (boostMap.get(b.id) ?? 0));
505
+ return [...boosted, ...unboosted];
506
+ }
507
+ catch {
508
+ // FTS errors are non-fatal; return original order
509
+ return rows;
510
+ }
511
+ }
512
+ /**
513
+ * Extract meaningful keywords from auto-memory context text.
514
+ * Filters out common stop words and short tokens.
515
+ */
516
+ function extractKeywords(text) {
517
+ const stopWords = new Set([
518
+ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
519
+ 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
520
+ 'should', 'may', 'might', 'shall', 'can', 'need', 'must', 'ought',
521
+ 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'for', 'with',
522
+ 'from', 'into', 'during', 'before', 'after', 'above', 'below',
523
+ 'to', 'of', 'in', 'on', 'at', 'by', 'about', 'between', 'through',
524
+ 'this', 'that', 'these', 'those', 'it', 'its', 'they', 'them',
525
+ 'their', 'we', 'our', 'you', 'your', 'he', 'she', 'him', 'her',
526
+ 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other',
527
+ 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very',
528
+ 'just', 'also', 'now', 'then', 'here', 'there', 'when', 'where',
529
+ 'how', 'what', 'which', 'who', 'whom', 'why', 'if', 'because',
530
+ 'as', 'until', 'while', 'use', 'using', 'used',
531
+ ]);
532
+ const words = text
533
+ .toLowerCase()
534
+ .replace(/[^a-z0-9\s-]/g, ' ')
535
+ .split(/\s+/)
536
+ .filter((w) => w.length >= 3 && !stopWords.has(w));
537
+ // Deduplicate while preserving order
538
+ const seen = new Set();
539
+ return words.filter((w) => {
540
+ if (seen.has(w))
541
+ return false;
542
+ seen.add(w);
543
+ return true;
544
+ });
545
+ }
546
+ function queryDurable(ctx, effectiveTags, excludeIds) {
547
+ const clauses = [
548
+ "tier = 'durable'",
549
+ "kind IN ('decision', 'pattern', 'architecture', 'reference')",
550
+ "(expires_at IS NULL OR expires_at > datetime('now'))",
551
+ "superseded_by IS NULL",
552
+ ];
553
+ const rows = ctx.db
554
+ .prepare(`SELECT * FROM vault WHERE ${clauses.join(' AND ')} ORDER BY created_at DESC LIMIT 20`)
555
+ .all();
556
+ let filtered = rows.filter((r) => !excludeIds.has(r.id));
557
+ if (effectiveTags.length) {
558
+ filtered = filtered.filter((r) => {
559
+ const tags = r.tags ? JSON.parse(r.tags) : [];
560
+ return effectiveTags.some((t) => tags.includes(t));
561
+ });
562
+ }
563
+ return filtered;
564
+ }
565
+ function queryRecent(ctx, since, effectiveTags) {
566
+ const clauses = ['created_at >= ?'];
567
+ const params = [since];
568
+ if (false) {
569
+ }
570
+ clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
571
+ clauses.push('superseded_by IS NULL');
572
+ const where = `WHERE ${clauses.join(' AND ')}`;
573
+ const rows = ctx.db
574
+ .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
575
+ .all(...params);
576
+ if (effectiveTags.length) {
577
+ const tagged = rows.filter((r) => {
578
+ const tags = r.tags ? JSON.parse(r.tags) : [];
579
+ return effectiveTags.some((t) => tags.includes(t));
580
+ });
581
+ if (tagged.length > 0)
582
+ return tagged;
124
583
  }
125
- return ok(lines.join('\n'));
584
+ return rows;
126
585
  }
127
586
  //# sourceMappingURL=list-context.js.map