context-vault 3.4.5 → 3.5.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 (73) hide show
  1. package/bin/cli.js +489 -299
  2. package/dist/server.js +2 -0
  3. package/dist/server.js.map +1 -1
  4. package/dist/status.d.ts.map +1 -1
  5. package/dist/status.js +29 -0
  6. package/dist/status.js.map +1 -1
  7. package/dist/tools/context-status.d.ts.map +1 -1
  8. package/dist/tools/context-status.js +39 -5
  9. package/dist/tools/context-status.js.map +1 -1
  10. package/dist/tools/get-context.d.ts.map +1 -1
  11. package/dist/tools/get-context.js +1 -0
  12. package/dist/tools/get-context.js.map +1 -1
  13. package/dist/tools/list-context.d.ts +2 -1
  14. package/dist/tools/list-context.d.ts.map +1 -1
  15. package/dist/tools/list-context.js +22 -5
  16. package/dist/tools/list-context.js.map +1 -1
  17. package/dist/tools/save-context.d.ts +2 -1
  18. package/dist/tools/save-context.d.ts.map +1 -1
  19. package/dist/tools/save-context.js +58 -4
  20. package/dist/tools/save-context.js.map +1 -1
  21. package/dist/tools/session-start.d.ts.map +1 -1
  22. package/dist/tools/session-start.js +192 -7
  23. package/dist/tools/session-start.js.map +1 -1
  24. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  25. package/node_modules/@context-vault/core/dist/capture.js +2 -0
  26. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  27. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  28. package/node_modules/@context-vault/core/dist/config.js +27 -1
  29. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  30. package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
  31. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  32. package/node_modules/@context-vault/core/dist/constants.js +13 -0
  33. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  34. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  35. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  36. package/node_modules/@context-vault/core/dist/db.js +73 -9
  37. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  38. package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
  39. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  40. package/node_modules/@context-vault/core/dist/index.js +58 -10
  41. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  42. package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
  43. package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/indexing.js +22 -0
  45. package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
  47. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  48. package/node_modules/@context-vault/core/dist/main.js +3 -1
  49. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  50. package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
  51. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  52. package/node_modules/@context-vault/core/dist/search.js +82 -6
  53. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  54. package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
  55. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  56. package/node_modules/@context-vault/core/package.json +5 -1
  57. package/node_modules/@context-vault/core/src/capture.ts +2 -0
  58. package/node_modules/@context-vault/core/src/config.ts +18 -1
  59. package/node_modules/@context-vault/core/src/constants.ts +15 -0
  60. package/node_modules/@context-vault/core/src/db.ts +73 -9
  61. package/node_modules/@context-vault/core/src/index.ts +65 -11
  62. package/node_modules/@context-vault/core/src/indexing.ts +35 -0
  63. package/node_modules/@context-vault/core/src/main.ts +5 -0
  64. package/node_modules/@context-vault/core/src/search.ts +96 -6
  65. package/node_modules/@context-vault/core/src/types.ts +26 -0
  66. package/package.json +2 -2
  67. package/src/server.ts +3 -0
  68. package/src/status.ts +35 -0
  69. package/src/tools/context-status.ts +40 -5
  70. package/src/tools/get-context.ts +1 -0
  71. package/src/tools/list-context.ts +20 -5
  72. package/src/tools/save-context.ts +67 -4
  73. package/src/tools/session-start.ts +222 -9
package/src/status.ts CHANGED
@@ -148,6 +148,20 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
148
148
  errors.push(`Archived count failed: ${(e as Error).message}`);
149
149
  }
150
150
 
151
+ let indexingStats: { indexed: number; unindexed: number; total: number; byKind: Array<{ kind: string; indexed: number; total: number }> } | null = null;
152
+ try {
153
+ const totalRow = db.prepare('SELECT COUNT(*) as c FROM vault').get() as { c: number } | undefined;
154
+ const indexedRow = db.prepare('SELECT COUNT(*) as c FROM vault WHERE indexed = 1').get() as { c: number } | undefined;
155
+ const total = totalRow?.c ?? 0;
156
+ const indexed = indexedRow?.c ?? 0;
157
+ const byKind = db.prepare(
158
+ 'SELECT kind, COUNT(*) as total, SUM(CASE WHEN indexed = 1 THEN 1 ELSE 0 END) as indexed FROM vault GROUP BY kind'
159
+ ).all() as Array<{ kind: string; total: number; indexed: number }>;
160
+ indexingStats = { indexed, unindexed: total - indexed, total, byKind };
161
+ } catch (e) {
162
+ errors.push(`Indexing stats failed: ${(e as Error).message}`);
163
+ }
164
+
151
165
  let staleKnowledge: unknown[] = [];
152
166
  try {
153
167
  const stalenessKinds = Object.entries(KIND_STALENESS_DAYS);
@@ -168,6 +182,25 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
168
182
  errors.push(`Stale knowledge check failed: ${(e as Error).message}`);
169
183
  }
170
184
 
185
+ let recallStats: { totalRecalls: number; entriesRecalled: number; avgRecallCount: number; maxRecallCount: number; topRecalled: Array<{ title: string; kind: string; recall_count: number; recall_sessions: number }> } | null = null;
186
+ try {
187
+ const totals = db.prepare(
188
+ `SELECT SUM(recall_count) as total_recalls, COUNT(CASE WHEN recall_count > 0 THEN 1 END) as entries_recalled, AVG(CASE WHEN recall_count > 0 THEN recall_count END) as avg_recall, MAX(recall_count) as max_recall FROM vault`
189
+ ).get() as any;
190
+ const topRecalled = db.prepare(
191
+ `SELECT title, kind, recall_count, recall_sessions FROM vault WHERE recall_count > 0 ORDER BY recall_count DESC LIMIT 5`
192
+ ).all() as any[];
193
+ recallStats = {
194
+ totalRecalls: totals?.total_recalls ?? 0,
195
+ entriesRecalled: totals?.entries_recalled ?? 0,
196
+ avgRecallCount: Math.round((totals?.avg_recall ?? 0) * 10) / 10,
197
+ maxRecallCount: totals?.max_recall ?? 0,
198
+ topRecalled,
199
+ };
200
+ } catch (e) {
201
+ errors.push(`Recall stats failed: ${(e as Error).message}`);
202
+ }
203
+
171
204
  return {
172
205
  fileCount,
173
206
  subdirs,
@@ -185,6 +218,8 @@ export function gatherVaultStatus(ctx: LocalCtx, opts: Record<string, unknown> =
185
218
  autoCapturedFeedbackCount,
186
219
  archivedCount,
187
220
  staleKnowledge,
221
+ indexingStats,
222
+ recallStats,
188
223
  resolvedFrom: config.resolvedFrom,
189
224
  errors,
190
225
  };
@@ -55,13 +55,31 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
55
55
  lines.push(`| **Expired** | ${status.expiredCount} pending prune |`);
56
56
  }
57
57
 
58
+ // Selective indexing stats
59
+ if (status.indexingStats) {
60
+ const ix = status.indexingStats;
61
+ const pct = ix.total > 0 ? Math.round((ix.indexed / ix.total) * 100) : 100;
62
+ lines.push(`| **Indexed entries** | ${ix.indexed}/${ix.total} (${pct}%) |`);
63
+ }
64
+
58
65
  // Indexed kinds as compact table
59
- lines.push(``, `### Indexed`);
66
+ lines.push(``, `### Entries by Kind`);
60
67
  if (status.kindCounts.length) {
61
- lines.push('| Kind | Count |');
62
- lines.push('|---|---|');
63
- for (const { kind, c } of status.kindCounts) {
64
- lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} |`);
68
+ if (status.indexingStats?.byKind?.length) {
69
+ lines.push('| Kind | Total | Indexed |');
70
+ lines.push('|---|---|---|');
71
+ const byKindMap = new Map(status.indexingStats.byKind.map((k: any) => [k.kind, k]));
72
+ for (const { kind, c } of status.kindCounts) {
73
+ const kStats = byKindMap.get(kind) as { indexed?: number } | undefined;
74
+ const idxCount = kStats?.indexed ?? c;
75
+ lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} | ${idxCount} |`);
76
+ }
77
+ } else {
78
+ lines.push('| Kind | Count |');
79
+ lines.push('|---|---|');
80
+ for (const { kind, c } of status.kindCounts) {
81
+ lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} |`);
82
+ }
65
83
  }
66
84
  } else {
67
85
  lines.push(`_(empty vault)_`);
@@ -79,6 +97,23 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
79
97
  for (const { name, count } of status.subdirs) lines.push(`- \`${name}/\` ${count} files`);
80
98
  }
81
99
 
100
+ if (status.recallStats && status.recallStats.totalRecalls > 0) {
101
+ const rs = status.recallStats;
102
+ lines.push(``, `### Recall Frequency`);
103
+ lines.push(`| Metric | Value |`);
104
+ lines.push(`|---|---|`);
105
+ lines.push(`| **Total recalls** | ${rs.totalRecalls} |`);
106
+ lines.push(`| **Entries recalled** | ${rs.entriesRecalled} |`);
107
+ lines.push(`| **Avg recalls/entry** | ${rs.avgRecallCount} |`);
108
+ lines.push(`| **Max recalls** | ${rs.maxRecallCount} |`);
109
+ if (rs.topRecalled?.length) {
110
+ lines.push(``, `**Top recalled:**`);
111
+ for (const t of rs.topRecalled) {
112
+ lines.push(`- "${t.title || '(untitled)'}" (\`${t.kind}\`): ${t.recall_count} recalls, ${t.recall_sessions} sessions`);
113
+ }
114
+ }
115
+ }
116
+
82
117
  if (status.stalePaths) {
83
118
  lines.push(``);
84
119
  lines.push(`### ⚠ Stale Paths`);
@@ -526,6 +526,7 @@ export async function handler(
526
526
  if (!include_superseded) {
527
527
  clauses.push('superseded_by IS NULL');
528
528
  }
529
+ clauses.push('indexed = 1');
529
530
  const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
530
531
  params.push(fetchLimit);
531
532
  let rows;
@@ -18,10 +18,11 @@ export const inputSchema = {
18
18
  until: z.string().optional().describe('ISO date, return entries created before this'),
19
19
  limit: z.number().optional().describe('Max results to return (default 20, max 100)'),
20
20
  offset: z.number().optional().describe('Skip first N results for pagination'),
21
+ include_unindexed: z.boolean().optional().describe('If true, include entries that are stored but not indexed for search. Default: false.'),
21
22
  };
22
23
 
23
24
  export async function handler(
24
- { kind, category, tags, since, until, limit, offset }: Record<string, any>,
25
+ { kind, category, tags, since, until, limit, offset, include_unindexed }: Record<string, any>,
25
26
  ctx: LocalCtx,
26
27
  { ensureIndexed, reindexFailed }: SharedCtx
27
28
  ): Promise<ToolResult> {
@@ -63,6 +64,9 @@ export async function handler(
63
64
  params.push(until);
64
65
  }
65
66
  clauses.push("(expires_at IS NULL OR expires_at > datetime('now'))");
67
+ if (!include_unindexed) {
68
+ clauses.push('indexed = 1');
69
+ }
66
70
 
67
71
  const where = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
68
72
  const effectiveLimit = Math.min(limit || 20, 100);
@@ -81,7 +85,7 @@ export async function handler(
81
85
  params.push(fetchLimit, effectiveOffset);
82
86
  rows = ctx.db
83
87
  .prepare(
84
- `SELECT id, title, kind, category, tags, created_at, updated_at, SUBSTR(body, 1, 120) as preview FROM vault ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
88
+ `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 ?`
85
89
  )
86
90
  .all(...params) as any[];
87
91
  } catch (e) {
@@ -124,15 +128,26 @@ export async function handler(
124
128
  `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
125
129
  );
126
130
  }
127
- lines.push('| | Title | Kind | Tags | Date | ID |');
128
- lines.push('|---|---|---|---|---|---|');
131
+ if (include_unindexed) {
132
+ lines.push('| | Title | Kind | Tags | Recalls | Idx | Date | ID |');
133
+ lines.push('|---|---|---|---|---|---|---|---|');
134
+ } else {
135
+ lines.push('| | Title | Kind | Tags | Recalls | Date | ID |');
136
+ lines.push('|---|---|---|---|---|---|---|');
137
+ }
129
138
  for (const r of filtered) {
130
139
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
131
140
  const tagStr = entryTags.length ? entryTags.join(', ') : '';
132
141
  const date = fmtDate(r.updated_at && r.updated_at !== r.created_at ? r.updated_at : r.created_at);
133
142
  const icon = kindIcon(r.kind);
134
143
  const title = (r.title || '(untitled)').replace(/\|/g, '\\|');
135
- lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${date} | \`${r.id}\` |`);
144
+ const recalls = r.recall_count ?? 0;
145
+ if (include_unindexed) {
146
+ const idxStr = r.indexed ? 'Y' : 'N';
147
+ lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${idxStr} | ${date} | \`${r.id}\` |`);
148
+ } else {
149
+ lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${recalls} | ${date} | \`${r.id}\` |`);
150
+ }
136
151
  }
137
152
 
138
153
  if (effectiveOffset + effectiveLimit < total) {
@@ -1,9 +1,13 @@
1
1
  import { z } from 'zod';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { resolve, basename, join } from 'node:path';
4
+ import { homedir } from 'node:os';
2
5
  import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
3
6
  import { indexEntry } from '@context-vault/core/index';
4
7
  import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
5
- import { normalizeKind } from '@context-vault/core/files';
8
+ import { normalizeKind, kindToPath } from '@context-vault/core/files';
6
9
  import { parseContextParam } from '@context-vault/core/context';
10
+ import { shouldIndex } from '@context-vault/core/indexing';
7
11
  import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
8
12
  import { maybeShowFeedbackPrompt } from '../telemetry.js';
9
13
  import { validateRelatedTo } from '../linking.js';
@@ -23,6 +27,34 @@ const DEFAULT_SIMILARITY_THRESHOLD = 0.85;
23
27
  const SKIP_THRESHOLD = 0.95;
24
28
  const UPDATE_THRESHOLD = 0.85;
25
29
 
30
+ function isDualWriteEnabled(config: { dataDir: string }): boolean {
31
+ try {
32
+ const configPath = join(config.dataDir, 'config.json');
33
+ if (!existsSync(configPath)) return true;
34
+ const fc = JSON.parse(readFileSync(configPath, 'utf-8'));
35
+ if (fc.dualWrite && fc.dualWrite.enabled === false) return false;
36
+ return true;
37
+ } catch {
38
+ return true;
39
+ }
40
+ }
41
+
42
+ function dualWriteLocal(entryFilePath: string, kind: string): void {
43
+ const cwd = process.cwd();
44
+ const home = homedir();
45
+ if (!cwd || cwd === '/' || cwd === home) return;
46
+
47
+ try {
48
+ const content = readFileSync(entryFilePath, 'utf-8');
49
+ const localDir = resolve(cwd, '.context', kindToPath(kind));
50
+ mkdirSync(localDir, { recursive: true });
51
+ const filename = basename(entryFilePath);
52
+ writeFileSync(join(localDir, filename), content);
53
+ } catch (e) {
54
+ console.warn(`[context-vault] Dual-write to .context/ failed: ${(e as Error).message}`);
55
+ }
56
+ }
57
+
26
58
  async function findSimilar(
27
59
  ctx: LocalCtx,
28
60
  embedding: any,
@@ -316,6 +348,12 @@ export const inputSchema = {
316
348
  .describe(
317
349
  'Encoding context for contextual reinstatement. Captures the situation when this entry was created, enabling context-aware retrieval boosting. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "implementing JWT" }) or a free-text string describing the current context.'
318
350
  ),
351
+ indexed: z
352
+ .boolean()
353
+ .optional()
354
+ .describe(
355
+ 'Whether to index this entry for search (generate embeddings + FTS). Default: auto-determined by indexing config. Set to false to store file + metadata only, skipping embedding generation. Set to true to force indexing regardless of config rules.'
356
+ ),
319
357
  };
320
358
 
321
359
  export async function handler(
@@ -338,6 +376,7 @@ export async function handler(
338
376
  tier,
339
377
  conflict_resolution,
340
378
  encoding_context,
379
+ indexed,
341
380
  }: Record<string, any>,
342
381
  ctx: LocalCtx,
343
382
  { ensureIndexed }: SharedCtx
@@ -447,6 +486,10 @@ export async function handler(
447
486
  } else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
448
487
  ctx.stmts.updateRelatedTo.run(null, entry.id);
449
488
  }
489
+ if (isDualWriteEnabled(config) && entry.filePath) {
490
+ dualWriteLocal(entry.filePath, entry.kind);
491
+ }
492
+
450
493
  const relPath = entry.filePath
451
494
  ? entry.filePath.replace(config.vaultDir + '/', '')
452
495
  : entry.filePath;
@@ -548,7 +591,17 @@ export async function handler(
548
591
 
549
592
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
550
593
 
551
- const embeddingToReuse = category === 'knowledge' ? queryEmbedding : null;
594
+ const effectiveIndexed = shouldIndex(
595
+ {
596
+ kind: normalizedKind,
597
+ category,
598
+ bodyLength: body?.length ?? 0,
599
+ explicitIndexed: indexed,
600
+ },
601
+ config.indexing
602
+ );
603
+
604
+ const embeddingToReuse = category === 'knowledge' && effectiveIndexed ? queryEmbedding : null;
552
605
 
553
606
  let entry;
554
607
  try {
@@ -567,8 +620,8 @@ export async function handler(
567
620
  supersedes,
568
621
  related_to,
569
622
  source_files,
570
-
571
623
  tier: effectiveTier,
624
+ indexed: effectiveIndexed,
572
625
  },
573
626
  embeddingToReuse
574
627
  );
@@ -581,7 +634,7 @@ export async function handler(
581
634
  }
582
635
 
583
636
  // Store context embedding in vault_ctx_vec for contextual reinstatement
584
- if (parsedCtx?.text && entry) {
637
+ if (parsedCtx?.text && entry && effectiveIndexed) {
585
638
  try {
586
639
  const ctxEmbedding = await ctx.embed(parsedCtx.text);
587
640
  if (ctxEmbedding) {
@@ -598,6 +651,10 @@ export async function handler(
598
651
  }
599
652
  }
600
653
 
654
+ if (isDualWriteEnabled(config) && entry.filePath) {
655
+ dualWriteLocal(entry.filePath, normalizedKind);
656
+ }
657
+
601
658
  if (ctx.config?.dataDir) {
602
659
  maybeShowFeedbackPrompt(ctx.config.dataDir);
603
660
  }
@@ -612,6 +669,12 @@ export async function handler(
612
669
  `\`${normalizedKind}\` · **${effectiveTier}**${tags?.length ? ` · ${tags.join(', ')}` : ''}`,
613
670
  `\`${entry.id}\` → ${relPath}`,
614
671
  ];
672
+ if (!effectiveIndexed) {
673
+ parts.push(
674
+ '',
675
+ '_Note: this entry is stored but not indexed (no embeddings/FTS). It will not appear in search results. Use `include_unindexed: true` in list_context to browse unindexed entries._'
676
+ );
677
+ }
615
678
  if (effectiveTier === 'ephemeral') {
616
679
  parts.push(
617
680
  '',
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { execSync } from 'node:child_process';
3
- import { readdirSync } from 'node:fs';
3
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { homedir } from 'node:os';
4
6
  import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
5
7
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
6
8
 
@@ -10,6 +12,115 @@ const MAX_BODY_PER_ENTRY = 400;
10
12
  const PRIORITY_KINDS = ['decision', 'insight', 'pattern'];
11
13
  const SESSION_SUMMARY_KIND = 'session';
12
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
+
13
124
  export const name = 'session_start';
14
125
 
15
126
  export const description =
@@ -123,13 +234,23 @@ export async function handler(
123
234
 
124
235
  const sinceDate = new Date(Date.now() - RECENT_DAYS * 86400000).toISOString();
125
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
+
126
244
  const sections = [];
127
245
  let tokensUsed = 0;
128
246
 
129
- sections.push(`# Session Brief${effectiveProject ? ` ${effectiveProject}` : ''}`);
247
+ sections.push(`# Session Brief${effectiveProject ? ` -- ${effectiveProject}` : ''}`);
130
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
+ : '';
131
252
  sections.push(
132
- `_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`
133
254
  );
134
255
  tokensUsed += estimateTokens(sections.join('\n'));
135
256
 
@@ -145,12 +266,13 @@ export async function handler(
145
266
  }
146
267
  }
147
268
 
269
+ // When auto-memory context is available, boost decisions query with FTS
148
270
  const decisions = queryByKinds(
149
271
  ctx,
150
272
  PRIORITY_KINDS,
151
273
  sinceDate,
152
-
153
- effectiveTags
274
+ effectiveTags,
275
+ autoMemoryContext
154
276
  );
155
277
  if (decisions.length > 0) {
156
278
  const header = '## Active Decisions, Insights & Patterns\n';
@@ -222,6 +344,11 @@ export async function handler(
222
344
  decisions: decisions.length,
223
345
  recent: deduped.length,
224
346
  },
347
+ auto_memory: {
348
+ detected: autoMemory.detected,
349
+ entries: autoMemory.entries.length,
350
+ lines_used: autoMemory.linesUsed,
351
+ },
225
352
  };
226
353
  return result;
227
354
  }
@@ -254,7 +381,8 @@ function queryByKinds(
254
381
  ctx: LocalCtx,
255
382
  kinds: string[],
256
383
  since: string,
257
- effectiveTags: string[]
384
+ effectiveTags: string[],
385
+ autoMemoryContext = ''
258
386
  ): any[] {
259
387
  const kindPlaceholders = kinds.map(() => '?').join(',');
260
388
  const clauses = [`kind IN (${kindPlaceholders})`];
@@ -269,20 +397,105 @@ function queryByKinds(
269
397
  clauses.push('superseded_by IS NULL');
270
398
 
271
399
  const where = `WHERE ${clauses.join(' AND ')}`;
272
- const rows = ctx.db
400
+ let rows = ctx.db
273
401
  .prepare(`SELECT * FROM vault ${where} ORDER BY created_at DESC LIMIT 50`)
274
- .all(...params);
402
+ .all(...params) as any[];
275
403
 
276
404
  if (effectiveTags.length) {
277
405
  const tagged = rows.filter((r: any) => {
278
406
  const tags = r.tags ? JSON.parse(r.tags) : [];
279
407
  return effectiveTags.some((t: string) => tags.includes(t));
280
408
  });
281
- 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);
282
416
  }
417
+
283
418
  return rows;
284
419
  }
285
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
+
286
499
  function queryRecent(ctx: LocalCtx, since: string, effectiveTags: string[]): any[] {
287
500
  const clauses = ['created_at >= ?'];
288
501
  const params = [since];