context-vault 3.4.4 → 3.4.5

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 (47) 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 +533 -11
  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 +24 -2
  10. package/dist/server.js.map +1 -1
  11. package/dist/tools/context-status.js +29 -28
  12. package/dist/tools/context-status.js.map +1 -1
  13. package/dist/tools/get-context.d.ts.map +1 -1
  14. package/dist/tools/get-context.js +22 -18
  15. package/dist/tools/get-context.js.map +1 -1
  16. package/dist/tools/list-context.d.ts.map +1 -1
  17. package/dist/tools/list-context.js +8 -8
  18. package/dist/tools/list-context.js.map +1 -1
  19. package/dist/tools/save-context.d.ts.map +1 -1
  20. package/dist/tools/save-context.js +37 -22
  21. package/dist/tools/save-context.js.map +1 -1
  22. package/dist/tools/session-start.d.ts.map +1 -1
  23. package/dist/tools/session-start.js +39 -5
  24. package/dist/tools/session-start.js.map +1 -1
  25. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  26. package/node_modules/@context-vault/core/dist/capture.js +11 -0
  27. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  28. package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
  29. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  30. package/node_modules/@context-vault/core/dist/config.js +20 -1
  31. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  32. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  33. package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
  34. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  35. package/node_modules/@context-vault/core/package.json +1 -1
  36. package/node_modules/@context-vault/core/src/capture.ts +9 -0
  37. package/node_modules/@context-vault/core/src/config.ts +22 -1
  38. package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
  39. package/package.json +2 -2
  40. package/scripts/prepack.js +17 -0
  41. package/src/helpers.ts +25 -0
  42. package/src/server.ts +25 -2
  43. package/src/tools/context-status.ts +30 -30
  44. package/src/tools/get-context.ts +23 -24
  45. package/src/tools/list-context.ts +8 -11
  46. package/src/tools/save-context.ts +39 -25
  47. package/src/tools/session-start.ts +36 -5
@@ -1,9 +1,30 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- import { homedir } from 'node:os';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import { DEFAULT_GROWTH_THRESHOLDS, DEFAULT_LIFECYCLE } from './constants.js';
5
5
  import type { VaultConfig } from './types.js';
6
6
 
7
+ /**
8
+ * Guard against writes to the real config file during test runs.
9
+ * Set CONTEXT_VAULT_TEST=1 in test helpers to activate.
10
+ *
11
+ * Allows writes if the target path is under a temp directory (tests with
12
+ * HOME overridden to a temp dir). Blocks writes to non-temp paths.
13
+ */
14
+ export function assertNotTestMode(targetPath: string): void {
15
+ if (process.env.CONTEXT_VAULT_TEST !== '1') return;
16
+ const resolved = resolve(targetPath);
17
+ const tmp = tmpdir();
18
+ // Allow writes to temp directories (tests with HOME isolation)
19
+ if (resolved.startsWith(tmp) || resolved.startsWith('/tmp/') || resolved.startsWith('/var/folders/')) {
20
+ return;
21
+ }
22
+ throw new Error(
23
+ `[context-vault] Refusing to write to real config in test mode (${targetPath}). ` +
24
+ 'Set HOME or CONTEXT_VAULT_DATA_DIR to a temp directory.'
25
+ );
26
+ }
27
+
7
28
  export function parseArgs(argv: string[]): Record<string, string | number> {
8
29
  const args: Record<string, string | number> = {};
9
30
  for (let i = 2; i < argv.length; i++) {
@@ -59,6 +59,8 @@ export function parseFrontmatter(text: string): {
59
59
  const RESERVED_FM_KEYS = new Set([
60
60
  'id',
61
61
  'title',
62
+ 'kind',
63
+ 'tier',
62
64
  'tags',
63
65
  'source',
64
66
  'created',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "3.4.4",
3
+ "version": "3.4.5",
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.4.4",
66
+ "@context-vault/core": "^3.4.5",
67
67
  "@modelcontextprotocol/sdk": "^1.26.0",
68
68
  "adm-zip": "^0.5.16",
69
69
  "sqlite-vec": "^0.1.0"
@@ -35,3 +35,20 @@ delete corePkg.dependencies;
35
35
  writeFileSync(corePkgPath, JSON.stringify(corePkg, null, 2) + '\n');
36
36
 
37
37
  console.log('[prepack] Bundled @context-vault/core into node_modules');
38
+
39
+ // Copy monorepo-level assets (agent-rules.md, setup-prompt.md) into local assets/
40
+ // so they ship with the npm package.
41
+ const MONOREPO_ASSETS = join(LOCAL_ROOT, '..', '..', 'assets');
42
+ const LOCAL_ASSETS = join(LOCAL_ROOT, 'assets');
43
+ const ASSET_FILES = ['agent-rules.md', 'setup-prompt.md'];
44
+
45
+ for (const file of ASSET_FILES) {
46
+ const src = join(MONOREPO_ASSETS, file);
47
+ const dest = join(LOCAL_ASSETS, file);
48
+ try {
49
+ cpSync(src, dest);
50
+ console.log(`[prepack] Copied ${file} to assets/`);
51
+ } catch (e) {
52
+ console.warn(`[prepack] Warning: could not copy ${file}: ${e.message}`);
53
+ }
54
+ }
package/src/helpers.ts CHANGED
@@ -58,4 +58,29 @@ export function ensureValidKind(kind: string): ToolResult | null {
58
58
  return null;
59
59
  }
60
60
 
61
+ const KIND_ICONS: Record<string, string> = {
62
+ insight: '◆',
63
+ decision: '◇',
64
+ pattern: '◈',
65
+ reference: '▸',
66
+ event: '○',
67
+ session: '◎',
68
+ brief: '▪',
69
+ bucket: '▫',
70
+ contact: '●',
71
+ project: '■',
72
+ tool: '▹',
73
+ source: '►',
74
+ feedback: '◉',
75
+ };
76
+
77
+ export function kindIcon(kind: string): string {
78
+ return KIND_ICONS[kind] || '·';
79
+ }
80
+
81
+ export function fmtDate(date: string | null | undefined): string {
82
+ if (!date) return '';
83
+ return date.split('T')[0];
84
+ }
85
+
61
86
  export { pkg };
package/src/server.ts CHANGED
@@ -7,9 +7,9 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
7
7
  import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
9
9
  import type { IncomingMessage, ServerResponse } from 'node:http';
10
- import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync } from 'node:fs';
10
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, readdirSync } from 'node:fs';
11
11
  import { join, dirname } from 'node:path';
12
- import { homedir } from 'node:os';
12
+ import { homedir, tmpdir } from 'node:os';
13
13
  import { fileURLToPath } from 'node:url';
14
14
 
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -312,6 +312,29 @@ async function main(): Promise<void> {
312
312
 
313
313
  config.vaultDirExists = existsSync(config.vaultDir);
314
314
 
315
+ // Validate vaultDir sanity
316
+ const osTmp = tmpdir();
317
+ const isTempPath = config.vaultDir.startsWith(osTmp) ||
318
+ config.vaultDir.startsWith('/tmp/') ||
319
+ config.vaultDir.startsWith('/var/folders/');
320
+ if (isTempPath) {
321
+ console.error(`[context-vault] WARNING: vaultDir points to a temp directory: ${config.vaultDir}`);
322
+ console.error(`[context-vault] This is likely from a test run that overwrote ~/.context-mcp/config.json`);
323
+ console.error(`[context-vault] Fix: run 'context-vault reconnect' or 'context-vault setup'`);
324
+ }
325
+
326
+ if (config.vaultDirExists) {
327
+ try {
328
+ const entries = readdirSync(config.vaultDir);
329
+ const hasMdFiles = entries.some(f => f.endsWith('.md'));
330
+ const hasMarker = entries.includes('.context-mcp');
331
+ if (!hasMdFiles && hasMarker) {
332
+ console.error(`[context-vault] WARNING: vaultDir has no markdown files but has a marker file`);
333
+ console.error(`[context-vault] The vault may be misconfigured. Run 'context-vault reconnect'`);
334
+ }
335
+ } catch {}
336
+ }
337
+
315
338
  console.error(`[context-vault] Vault: ${config.vaultDir}`);
316
339
  console.error(`[context-vault] Database: ${config.dbPath}`);
317
340
  console.error(`[context-vault] Dev dir: ${config.devDir}`);
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { gatherVaultStatus, computeGrowthWarnings } from '../status.js';
4
4
  import { errorLogPath, errorLogCount } from '../error-log.js';
5
- import { ok, err } from '../helpers.js';
5
+ import { ok, err, kindIcon } from '../helpers.js';
6
6
  import type { LocalCtx, ToolResult } from '../types.js';
7
7
 
8
8
  function relativeTime(ts: number): string {
@@ -30,53 +30,53 @@ export function handler(_args: Record<string, any>, ctx: LocalCtx): ToolResult {
30
30
  const hasIssues = status.stalePaths || (status.embeddingStatus?.missing ?? 0) > 0;
31
31
  const healthIcon = hasIssues ? '⚠' : '✓';
32
32
 
33
+ const schemaVersion = (ctx.db.prepare('PRAGMA user_version').get() as any)?.user_version ?? 'unknown';
34
+ const embedPct = status.embeddingStatus
35
+ ? (status.embeddingStatus.total > 0 ? Math.round((status.embeddingStatus.indexed / status.embeddingStatus.total) * 100) : 100)
36
+ : null;
37
+ const embedStr = status.embeddingStatus
38
+ ? `${status.embeddingStatus.indexed}/${status.embeddingStatus.total} (${embedPct}%)`
39
+ : 'n/a';
40
+ const modelStr = status.embedModelAvailable === false ? '⚠ unavailable' : status.embedModelAvailable === true ? '✓ loaded' : 'unknown';
41
+
33
42
  const lines = [
34
- `## ${healthIcon} Vault Status (connected)`,
43
+ `## ${healthIcon} Vault Dashboard`,
35
44
  ``,
36
- `Vault: ${config.vaultDir} (${config.vaultDirExists ? status.fileCount + ' files' : 'missing'})`,
37
- `Database: ${config.dbPath} (${status.dbSize})`,
38
- `Dev dir: ${config.devDir}`,
39
- `Data dir: ${config.dataDir}`,
40
- `Config: ${config.configPath}`,
41
- `Resolved via: ${status.resolvedFrom}`,
42
- `Schema: v9 (updated_at, superseded_by)`,
45
+ `| | |`,
46
+ `|---|---|`,
47
+ `| **Vault** | ${config.vaultDir} (${config.vaultDirExists ? status.fileCount + ' files' : '⚠ missing'}) |`,
48
+ `| **Database** | ${config.dbPath} (${status.dbSize}) |`,
49
+ `| **Schema** | v${schemaVersion} |`,
50
+ `| **Embeddings** | ${embedStr} |`,
51
+ `| **Model** | ${modelStr} |`,
52
+ `| **Event decay** | ${config.eventDecayDays} days |`,
43
53
  ];
44
-
45
- if (status.embeddingStatus) {
46
- const { indexed, total, missing } = status.embeddingStatus;
47
- const pct = total > 0 ? Math.round((indexed / total) * 100) : 100;
48
- lines.push(`Embeddings: ${indexed}/${total} (${pct}%)`);
49
- }
50
- if (status.embedModelAvailable === false) {
51
- lines.push(`Embed model: unavailable (semantic search disabled, FTS still works)`);
52
- } else if (status.embedModelAvailable === true) {
53
- lines.push(`Embed model: loaded`);
54
- }
55
- lines.push(`Decay: ${config.eventDecayDays} days (event recency window)`);
56
54
  if (status.expiredCount > 0) {
57
- lines.push(
58
- `Expired: ${status.expiredCount} entries pending prune (run \`context-vault prune\` to remove now)`
59
- );
55
+ lines.push(`| **Expired** | ${status.expiredCount} pending prune |`);
60
56
  }
61
57
 
58
+ // Indexed kinds as compact table
62
59
  lines.push(``, `### Indexed`);
63
-
64
60
  if (status.kindCounts.length) {
65
- for (const { kind, c } of status.kindCounts) lines.push(`- ${c} ${kind}s`);
61
+ lines.push('| Kind | Count |');
62
+ lines.push('|---|---|');
63
+ for (const { kind, c } of status.kindCounts) {
64
+ lines.push(`| ${kindIcon(kind)} \`${kind}\` | ${c} |`);
65
+ }
66
66
  } else {
67
- lines.push(`- (empty)`);
67
+ lines.push(`_(empty vault)_`);
68
68
  }
69
69
 
70
70
  if (status.categoryCounts.length) {
71
71
  lines.push(``);
72
72
  lines.push(`### Categories`);
73
- for (const { category, c } of status.categoryCounts) lines.push(`- ${category}: ${c}`);
73
+ for (const { category, c } of status.categoryCounts) lines.push(`- **${category}**: ${c}`);
74
74
  }
75
75
 
76
76
  if (status.subdirs.length) {
77
77
  lines.push(``);
78
- lines.push(`### Disk Directories`);
79
- for (const { name, count } of status.subdirs) lines.push(`- ${name}/: ${count} files`);
78
+ lines.push(`### Disk`);
79
+ for (const { name, count } of status.subdirs) lines.push(`- \`${name}/\` ${count} files`);
80
80
  }
81
81
 
82
82
  if (status.stalePaths) {
@@ -8,7 +8,7 @@ import { normalizeKind } from '@context-vault/core/files';
8
8
  import { parseContextParam } from '@context-vault/core/context';
9
9
  import { resolveTemporalParams } from '../temporal.js';
10
10
  import { collectLinkedEntries } from '../linking.js';
11
- import { ok, err, errWithHint } from '../helpers.js';
11
+ import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
12
12
  import { isEmbedAvailable } from '@context-vault/core/embed';
13
13
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
14
14
 
@@ -633,23 +633,24 @@ export async function handler(
633
633
  const r = filtered[i];
634
634
  const isSkeleton = i >= effectivePivot;
635
635
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
636
- const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
637
- const relPath =
638
- r.file_path && config.vaultDir
639
- ? r.file_path.replace(config.vaultDir + '/', '')
640
- : r.file_path || 'n/a';
641
- const skeletonLabel = isSkeleton ? ' skeleton' : '';
636
+ const tagStr = entryTags.length ? entryTags.join(', ') : '';
637
+ const icon = kindIcon(r.kind);
638
+ const skeletonLabel = isSkeleton ? ' `skeleton`' : '';
639
+ const tierLabel = r.tier ? `**${r.tier}**` : '';
640
+ const dateStr = r.updated_at && r.updated_at !== r.created_at
641
+ ? `${fmtDate(r.created_at)} (upd ${fmtDate(r.updated_at)})`
642
+ : fmtDate(r.created_at);
642
643
  lines.push(
643
- `### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
644
- );
645
- const dateStr =
646
- r.updated_at && r.updated_at !== r.created_at
647
- ? `${r.created_at} (updated ${r.updated_at})`
648
- : r.created_at || '';
649
- const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
650
- lines.push(
651
- `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
644
+ `### ${icon} ${r.title || '(untitled)'}${skeletonLabel}`
652
645
  );
646
+ const meta = [
647
+ `\`${r.score.toFixed(2)}\``,
648
+ `\`${r.kind}\``,
649
+ tierLabel,
650
+ tagStr,
651
+ dateStr,
652
+ ].filter(Boolean).join(' · ');
653
+ lines.push(`${meta} \nid: \`${r.id}\``);
653
654
  const stalenessResult = checkStaleness(r);
654
655
  if (stalenessResult) {
655
656
  r.stale = true;
@@ -691,15 +692,13 @@ export async function handler(
691
692
  if (uniqueLinked.length > 0) {
692
693
  lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
693
694
  for (const r of uniqueLinked) {
694
- const direction = forward.some((f: any) => f.id === r.id) ? '→ forward' : '← backlink';
695
+ const direction = forward.some((f: any) => f.id === r.id) ? '→' : '←';
695
696
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
696
- const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
697
- const relPath =
698
- r.file_path && config.vaultDir
699
- ? r.file_path.replace(config.vaultDir + '/', '')
700
- : r.file_path || 'n/a';
701
- lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
702
- lines.push(`${tagStr} · ${relPath} · id: \`${r.id}\``);
697
+ const tagStr = entryTags.length ? entryTags.join(', ') : '';
698
+ const icon = kindIcon(r.kind);
699
+ lines.push(`### ${icon} ${r.title || '(untitled)'} ${direction}`);
700
+ const meta = [`\`${r.kind}\``, tagStr].filter(Boolean).join(' · ');
701
+ lines.push(`${meta} \nid: \`${r.id}\``);
703
702
  lines.push(truncateBody(r.body, body_limit ?? 300));
704
703
  lines.push('');
705
704
  }
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { normalizeKind } from '@context-vault/core/files';
3
3
  import { categoryFor } from '@context-vault/core/categories';
4
- import { ok, err, errWithHint } from '../helpers.js';
4
+ import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
5
5
  import { resolveTemporalParams } from '../temporal.js';
6
6
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
7
7
 
@@ -124,18 +124,15 @@ export async function handler(
124
124
  `> ℹ Event search limited to last ${days} days. Use \`since\` parameter for older results.\n`
125
125
  );
126
126
  }
127
+ lines.push('| | Title | Kind | Tags | Date | ID |');
128
+ lines.push('|---|---|---|---|---|---|');
127
129
  for (const r of filtered) {
128
130
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
129
- const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
130
- const dateStr =
131
- r.updated_at && r.updated_at !== r.created_at
132
- ? `${r.created_at} (updated ${r.updated_at})`
133
- : r.created_at;
134
- lines.push(
135
- `- **${r.title || '(untitled)'}** [${r.kind}/${r.category}] — ${tagStr} — ${dateStr} — \`${r.id}\``
136
- );
137
- if (r.preview)
138
- lines.push(` ${r.preview.replace(/\n+/g, ' ').trim()}${r.preview.length >= 120 ? '…' : ''}`);
131
+ const tagStr = entryTags.length ? entryTags.join(', ') : '';
132
+ const date = fmtDate(r.updated_at && r.updated_at !== r.created_at ? r.updated_at : r.created_at);
133
+ const icon = kindIcon(r.kind);
134
+ const title = (r.title || '(untitled)').replace(/\|/g, '\\|');
135
+ lines.push(`| ${icon} | **${title}** | \`${r.kind}\` | ${tagStr} | ${date} | \`${r.id}\` |`);
139
136
  }
140
137
 
141
138
  if (effectiveOffset + effectiveLimit < total) {
@@ -4,7 +4,7 @@ import { indexEntry } from '@context-vault/core/index';
4
4
  import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
5
5
  import { normalizeKind } from '@context-vault/core/files';
6
6
  import { parseContextParam } from '@context-vault/core/context';
7
- import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
7
+ import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
8
8
  import { maybeShowFeedbackPrompt } from '../telemetry.js';
9
9
  import { validateRelatedTo } from '../linking.js';
10
10
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
@@ -82,13 +82,13 @@ async function findSimilar(
82
82
  }
83
83
 
84
84
  function formatSimilarWarning(similar: any[]): string {
85
- const lines = ['', '⚠ Similar entries already exist:'];
85
+ const lines = ['', '### ⚠ Similar entries'];
86
86
  for (const e of similar) {
87
- const score = e.score.toFixed(2);
88
- const title = e.title ? `"${e.title}"` : '(no title)';
89
- lines.push(` - ${title} (${score}) id: ${e.id}`);
87
+ const score = (e.score * 100).toFixed(0);
88
+ const title = e.title ? `**${e.title}**` : '(no title)';
89
+ lines.push(`- ${title} \`${score}%\` · \`${e.id}\``);
90
90
  }
91
- lines.push(' Consider using `id: <existing>` in save_context to update instead.');
91
+ lines.push('_Use `id` param to update instead of creating a duplicate._');
92
92
  return lines.join('\n');
93
93
  }
94
94
 
@@ -142,13 +142,12 @@ export function buildConflictCandidates(similarEntries: any[]): any[] {
142
142
  }
143
143
 
144
144
  function formatConflictSuggestions(candidates: any[]): string {
145
- const lines = ['', '── Conflict Resolution Suggestions ──'];
145
+ const lines = ['', '### Conflict Resolution'];
146
146
  for (const c of candidates) {
147
- const titleDisplay = c.title ? `"${c.title}"` : '(no title)';
148
- lines.push(
149
- ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) id: ${c.id}`
150
- );
151
- lines.push(` ${c.reasoning_context}`);
147
+ const titleDisplay = c.title ? `**${c.title}**` : '(no title)';
148
+ const actionIcon = c.suggested_action === 'SKIP' ? '⊘' : c.suggested_action === 'UPDATE' ? '↻' : '+';
149
+ lines.push(`${actionIcon} **${c.suggested_action}** ${titleDisplay} \`${(c.score * 100).toFixed(0)}%\` · \`${c.id}\``);
150
+ lines.push(` ${c.reasoning_context}`);
152
151
  }
153
152
  return lines.join('\n');
154
153
  }
@@ -606,11 +605,19 @@ export async function handler(
606
605
  const relPath = entry.filePath
607
606
  ? entry.filePath.replace(config.vaultDir + '/', '')
608
607
  : entry.filePath;
609
- const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
610
- if (title) parts.push(` title: ${title}`);
611
- if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
612
- parts.push(` tier: ${effectiveTier}`);
613
- parts.push('', '_Use this id to update or delete later._');
608
+ const icon = kindIcon(normalizedKind);
609
+ const parts = [
610
+ `## Saved`,
611
+ `${icon} **${title || '(untitled)'}**`,
612
+ `\`${normalizedKind}\` · **${effectiveTier}**${tags?.length ? ` · ${tags.join(', ')}` : ''}`,
613
+ `\`${entry.id}\` → ${relPath}`,
614
+ ];
615
+ if (effectiveTier === 'ephemeral') {
616
+ parts.push(
617
+ '',
618
+ '_Note: ephemeral entries are excluded from default search. Use `include_ephemeral: true` in get_context to find them._'
619
+ );
620
+ }
614
621
  const hasBucketTag = (tags || []).some(
615
622
  (t: any) => typeof t === 'string' && t.startsWith('bucket:')
616
623
  );
@@ -624,18 +631,25 @@ export async function handler(
624
631
  (t: any) => typeof t === 'string' && t.startsWith('bucket:')
625
632
  );
626
633
  for (const bt of bucketTags) {
627
- const bucketUserClause = '';
628
- const bucketParams = false ? [bt] : [bt];
629
634
  const exists = ctx.db
630
635
  .prepare(
631
- `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? ${bucketUserClause} LIMIT 1`
636
+ `SELECT 1 FROM vault WHERE kind = 'bucket' AND identity_key = ? LIMIT 1`
632
637
  )
633
- .get(...bucketParams);
638
+ .get(bt);
634
639
  if (!exists) {
635
- parts.push(
636
- ``,
637
- `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`
638
- );
640
+ // Auto-register the bucket silently
641
+ const bucketName = bt.replace(/^bucket:/, '');
642
+ try {
643
+ await captureAndIndex(ctx, {
644
+ kind: 'bucket',
645
+ title: bucketName,
646
+ body: `Bucket for project: ${bucketName}`,
647
+ tags: [bt],
648
+ identity_key: bt,
649
+ });
650
+ } catch {
651
+ // Non-fatal: bucket registration failure should not block the save
652
+ }
639
653
  }
640
654
  }
641
655
  if (similarEntries.length) {
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { execSync } from 'node:child_process';
3
- import { ok, err, ensureVaultExists } from '../helpers.js';
3
+ import { readdirSync } from 'node:fs';
4
+ import { ok, err, ensureVaultExists, kindIcon, fmtDate } from '../helpers.js';
4
5
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
5
6
 
6
7
  const DEFAULT_MAX_TOKENS = 4000;
@@ -67,11 +68,13 @@ function estimateTokens(text: string | null | undefined): number {
67
68
 
68
69
  function formatEntry(entry: any): string {
69
70
  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';
71
+ const tagStr = tags.length ? tags.join(', ') : '';
72
+ const date = fmtDate(entry.updated_at || entry.created_at);
73
+ const icon = kindIcon(entry.kind);
74
+ const meta = [`\`${entry.kind}\``, tagStr, date].filter(Boolean).join(' · ');
72
75
  return [
73
- `- **${entry.title || '(untitled)'}** [${entry.kind}]`,
74
- ` tags: ${tagStr} | ${date} | id: \`${entry.id}\``,
76
+ `- ${icon} **${entry.title || '(untitled)'}**`,
77
+ ` ${meta} · \`${entry.id}\``,
75
78
  ` ${truncateBody(entry.body).replace(/\n+/g, ' ').trim()}`,
76
79
  ].join('\n');
77
80
  }
@@ -88,6 +91,30 @@ export async function handler(
88
91
 
89
92
  await ensureIndexed();
90
93
 
94
+ // Sanity check: compare DB entries vs disk files
95
+ let indexWarning = '';
96
+ try {
97
+ const dbCount = (ctx.db.prepare('SELECT COUNT(*) as cnt FROM vault').get() as any)?.cnt ?? 0;
98
+ let diskCount = 0;
99
+ const walk = (dir: string, depth = 0) => {
100
+ if (depth > 3 || diskCount >= 100) return;
101
+ try {
102
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
103
+ if (diskCount >= 100) return;
104
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== '_archive') {
105
+ walk(`${dir}/${entry.name}`, depth + 1);
106
+ } else if (entry.name.endsWith('.md')) {
107
+ diskCount++;
108
+ }
109
+ }
110
+ } catch {}
111
+ };
112
+ walk(config.vaultDir);
113
+ if (diskCount >= 100 && dbCount < diskCount / 10) {
114
+ 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`;
115
+ }
116
+ } catch {}
117
+
91
118
  const effectiveProject = project?.trim() || detectProject();
92
119
  const tokenBudget = max_tokens || DEFAULT_MAX_TOKENS;
93
120
 
@@ -175,6 +202,10 @@ export async function handler(
175
202
  return true;
176
203
  }).length;
177
204
 
205
+ if (indexWarning) {
206
+ sections.push(indexWarning);
207
+ }
208
+
178
209
  sections.push('---');
179
210
  sections.push(
180
211
  `_${tokensUsed} / ${tokenBudget} tokens used | project: ${effectiveProject || 'unscoped'}_`