context-vault 3.4.3 → 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 (67) 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 +52 -12
  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 +2 -1
  14. package/dist/tools/get-context.d.ts.map +1 -1
  15. package/dist/tools/get-context.js +44 -20
  16. package/dist/tools/get-context.js.map +1 -1
  17. package/dist/tools/list-context.d.ts.map +1 -1
  18. package/dist/tools/list-context.js +8 -8
  19. package/dist/tools/list-context.js.map +1 -1
  20. package/dist/tools/save-context.d.ts +2 -1
  21. package/dist/tools/save-context.d.ts.map +1 -1
  22. package/dist/tools/save-context.js +100 -24
  23. package/dist/tools/save-context.js.map +1 -1
  24. package/dist/tools/session-start.d.ts.map +1 -1
  25. package/dist/tools/session-start.js +39 -5
  26. package/dist/tools/session-start.js.map +1 -1
  27. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  28. package/node_modules/@context-vault/core/dist/capture.js +11 -0
  29. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  30. package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
  31. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  32. package/node_modules/@context-vault/core/dist/config.js +20 -1
  33. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  34. package/node_modules/@context-vault/core/dist/context.d.ts +34 -0
  35. package/node_modules/@context-vault/core/dist/context.d.ts.map +1 -0
  36. package/node_modules/@context-vault/core/dist/context.js +55 -0
  37. package/node_modules/@context-vault/core/dist/context.js.map +1 -0
  38. package/node_modules/@context-vault/core/dist/db.d.ts +3 -1
  39. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  40. package/node_modules/@context-vault/core/dist/db.js +29 -2
  41. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  42. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  43. package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
  44. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  45. package/node_modules/@context-vault/core/dist/search.d.ts +1 -0
  46. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  47. package/node_modules/@context-vault/core/dist/search.js +57 -3
  48. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  49. package/node_modules/@context-vault/core/dist/types.d.ts +6 -0
  50. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  51. package/node_modules/@context-vault/core/package.json +5 -1
  52. package/node_modules/@context-vault/core/src/capture.ts +9 -0
  53. package/node_modules/@context-vault/core/src/config.ts +22 -1
  54. package/node_modules/@context-vault/core/src/context.ts +65 -0
  55. package/node_modules/@context-vault/core/src/db.ts +29 -2
  56. package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
  57. package/node_modules/@context-vault/core/src/search.ts +54 -2
  58. package/node_modules/@context-vault/core/src/types.ts +6 -0
  59. package/package.json +2 -2
  60. package/scripts/prepack.js +17 -0
  61. package/src/helpers.ts +25 -0
  62. package/src/server.ts +57 -11
  63. package/src/tools/context-status.ts +30 -30
  64. package/src/tools/get-context.ts +48 -25
  65. package/src/tools/list-context.ts +8 -11
  66. package/src/tools/save-context.ts +101 -26
  67. package/src/tools/session-start.ts +36 -5
@@ -2,12 +2,13 @@ import { z } from 'zod';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { readFileSync, existsSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
- import { hybridSearch } from '@context-vault/core/search';
5
+ import { hybridSearch, trackAccess } from '@context-vault/core/search';
6
6
  import { categoryFor } from '@context-vault/core/categories';
7
7
  import { normalizeKind } from '@context-vault/core/files';
8
+ import { parseContextParam } from '@context-vault/core/context';
8
9
  import { resolveTemporalParams } from '../temporal.js';
9
10
  import { collectLinkedEntries } from '../linking.js';
10
- import { ok, err, errWithHint } from '../helpers.js';
11
+ import { ok, err, errWithHint, kindIcon, fmtDate } from '../helpers.js';
11
12
  import { isEmbedAvailable } from '@context-vault/core/embed';
12
13
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
13
14
 
@@ -346,6 +347,12 @@ export const inputSchema = {
346
347
  .describe(
347
348
  'When true with identity_key, return a clear "not found" instead of falling through to semantic search on miss. Default: false.'
348
349
  ),
350
+ context: z
351
+ .any()
352
+ .optional()
353
+ .describe(
354
+ 'Current context for contextual reinstatement. Boosts entries that were saved in a similar context. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "debugging token expiry" }) or a free-text string. Entries saved with matching encoding_context will rank higher.'
355
+ ),
349
356
  };
350
357
 
351
358
  export async function handler(
@@ -369,6 +376,7 @@ export async function handler(
369
376
  follow_links,
370
377
  body_limit,
371
378
  strict,
379
+ context,
372
380
  }: Record<string, any>,
373
381
  ctx: LocalCtx,
374
382
  { ensureIndexed, reindexFailed }: SharedCtx
@@ -413,6 +421,7 @@ export async function handler(
413
421
  if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
414
422
  const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key) as any;
415
423
  if (match) {
424
+ trackAccess(ctx, [{ ...match, score: 1 }]);
416
425
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
417
426
  const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
418
427
  const relPath =
@@ -452,6 +461,17 @@ export async function handler(
452
461
  ? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
453
462
  : effectiveLimit;
454
463
 
464
+ // Generate context embedding for contextual reinstatement boosting
465
+ let contextEmbedding: Float32Array | null = null;
466
+ const parsedCtx = parseContextParam(context);
467
+ if (parsedCtx?.text) {
468
+ try {
469
+ contextEmbedding = await ctx.embed(parsedCtx.text);
470
+ } catch {
471
+ // Non-fatal: proceed without context boosting
472
+ }
473
+ }
474
+
455
475
  let filtered: any[];
456
476
  if (hasQuery) {
457
477
  // Hybrid search mode
@@ -465,6 +485,7 @@ export async function handler(
465
485
  decayDays: config.eventDecayDays || 30,
466
486
  includeSuperseeded: include_superseded ?? false,
467
487
  includeEphemeral: include_ephemeral ?? false,
488
+ contextEmbedding,
468
489
  });
469
490
 
470
491
  // Post-filter by tags if provided, then apply requested limit
@@ -532,6 +553,9 @@ export async function handler(
532
553
 
533
554
  // Add score field for consistent output
534
555
  for (const r of filtered) r.score = 0;
556
+
557
+ // Track access for filter-only results (hybrid search tracks its own)
558
+ trackAccess(ctx, filtered);
535
559
  }
536
560
 
537
561
  // Brief score boost: briefs rank slightly higher so consolidated snapshots
@@ -609,23 +633,24 @@ export async function handler(
609
633
  const r = filtered[i];
610
634
  const isSkeleton = i >= effectivePivot;
611
635
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
612
- const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
613
- const relPath =
614
- r.file_path && config.vaultDir
615
- ? r.file_path.replace(config.vaultDir + '/', '')
616
- : r.file_path || 'n/a';
617
- const skeletonLabel = isSkeleton ? ' skeleton' : '';
618
- lines.push(
619
- `### [${i + 1}/${filtered.length}] ${r.title || '(untitled)'} [${r.kind}/${r.category}]${skeletonLabel}`
620
- );
621
- const dateStr =
622
- r.updated_at && r.updated_at !== r.created_at
623
- ? `${r.created_at} (updated ${r.updated_at})`
624
- : r.created_at || '';
625
- const tierStr = r.tier ? ` · tier: ${r.tier}` : '';
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);
626
643
  lines.push(
627
- `${r.score.toFixed(3)} · ${tagStr} · ${relPath} · ${dateStr} · skeleton: ${isSkeleton}${tierStr} · id: \`${r.id}\``
644
+ `### ${icon} ${r.title || '(untitled)'}${skeletonLabel}`
628
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}\``);
629
654
  const stalenessResult = checkStaleness(r);
630
655
  if (stalenessResult) {
631
656
  r.stale = true;
@@ -667,15 +692,13 @@ export async function handler(
667
692
  if (uniqueLinked.length > 0) {
668
693
  lines.push(`## Linked Entries (${uniqueLinked.length} via related_to)\n`);
669
694
  for (const r of uniqueLinked) {
670
- const direction = forward.some((f: any) => f.id === r.id) ? '→ forward' : '← backlink';
695
+ const direction = forward.some((f: any) => f.id === r.id) ? '→' : '←';
671
696
  const entryTags = r.tags ? JSON.parse(r.tags) : [];
672
- const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
673
- const relPath =
674
- r.file_path && config.vaultDir
675
- ? r.file_path.replace(config.vaultDir + '/', '')
676
- : r.file_path || 'n/a';
677
- lines.push(`### ${r.title || '(untitled)'} [${r.kind}/${r.category}] ${direction}`);
678
- 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}\``);
679
702
  lines.push(truncateBody(r.body, body_limit ?? 300));
680
703
  lines.push('');
681
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) {
@@ -3,7 +3,8 @@ import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
3
3
  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
- import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
6
+ import { parseContextParam } from '@context-vault/core/context';
7
+ import { ok, err, errWithHint, ensureVaultExists, ensureValidKind, kindIcon } from '../helpers.js';
7
8
  import { maybeShowFeedbackPrompt } from '../telemetry.js';
8
9
  import { validateRelatedTo } from '../linking.js';
9
10
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
@@ -81,13 +82,13 @@ async function findSimilar(
81
82
  }
82
83
 
83
84
  function formatSimilarWarning(similar: any[]): string {
84
- const lines = ['', '⚠ Similar entries already exist:'];
85
+ const lines = ['', '### ⚠ Similar entries'];
85
86
  for (const e of similar) {
86
- const score = e.score.toFixed(2);
87
- const title = e.title ? `"${e.title}"` : '(no title)';
88
- 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}\``);
89
90
  }
90
- 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._');
91
92
  return lines.join('\n');
92
93
  }
93
94
 
@@ -141,13 +142,12 @@ export function buildConflictCandidates(similarEntries: any[]): any[] {
141
142
  }
142
143
 
143
144
  function formatConflictSuggestions(candidates: any[]): string {
144
- const lines = ['', '── Conflict Resolution Suggestions ──'];
145
+ const lines = ['', '### Conflict Resolution'];
145
146
  for (const c of candidates) {
146
- const titleDisplay = c.title ? `"${c.title}"` : '(no title)';
147
- lines.push(
148
- ` [${c.suggested_action}] ${titleDisplay} (${(c.score * 100).toFixed(0)}%) id: ${c.id}`
149
- );
150
- 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}`);
151
151
  }
152
152
  return lines.join('\n');
153
153
  }
@@ -310,6 +310,12 @@ export const inputSchema = {
310
310
  .describe(
311
311
  'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).'
312
312
  ),
313
+ encoding_context: z
314
+ .any()
315
+ .optional()
316
+ .describe(
317
+ '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
+ ),
313
319
  };
314
320
 
315
321
  export async function handler(
@@ -331,6 +337,7 @@ export async function handler(
331
337
  similarity_threshold,
332
338
  tier,
333
339
  conflict_resolution,
340
+ encoding_context,
334
341
  }: Record<string, any>,
335
342
  ctx: LocalCtx,
336
343
  { ensureIndexed }: SharedCtx
@@ -388,13 +395,21 @@ export async function handler(
388
395
  );
389
396
  }
390
397
 
398
+ // Merge encoding context into meta for update path
399
+ const updateParsedCtx = parseContextParam(encoding_context);
400
+ let updateMeta = meta;
401
+ if (updateParsedCtx) {
402
+ updateMeta = { ...(meta || {}) };
403
+ updateMeta.encoding_context = updateParsedCtx.structured || updateParsedCtx.text;
404
+ }
405
+
391
406
  let entry;
392
407
  try {
393
408
  entry = updateEntryFile(ctx, existing, {
394
409
  title,
395
410
  body,
396
411
  tags,
397
- meta,
412
+ meta: updateMeta,
398
413
  source,
399
414
  expires_at,
400
415
  supersedes,
@@ -409,6 +424,24 @@ export async function handler(
409
424
  'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
410
425
  );
411
426
  }
427
+
428
+ // Store context embedding for updated entry
429
+ if (updateParsedCtx?.text) {
430
+ try {
431
+ const ctxEmbed = await ctx.embed(updateParsedCtx.text);
432
+ if (ctxEmbed) {
433
+ const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
434
+ if (rowidResult?.rowid) {
435
+ const rowid = Number(rowidResult.rowid);
436
+ try { ctx.deleteCtxVec(rowid); } catch {}
437
+ ctx.insertCtxVec(rowid, ctxEmbed);
438
+ }
439
+ }
440
+ } catch (e) {
441
+ console.warn(`[context-vault] Context embedding update failed: ${(e as Error).message}`);
442
+ }
443
+ }
444
+
412
445
  if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
413
446
  ctx.stmts.updateRelatedTo.run(JSON.stringify(entry.related_to), entry.id);
414
447
  } else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
@@ -502,6 +535,15 @@ export async function handler(
502
535
 
503
536
  const mergedMeta = { ...(meta || {}) };
504
537
  if (folder) mergedMeta.folder = folder;
538
+
539
+ // Merge encoding context into meta for persistence
540
+ const parsedCtx = parseContextParam(encoding_context);
541
+ if (parsedCtx?.structured) {
542
+ mergedMeta.encoding_context = parsedCtx.structured;
543
+ } else if (parsedCtx?.text) {
544
+ mergedMeta.encoding_context = parsedCtx.text;
545
+ }
546
+
505
547
  const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
506
548
 
507
549
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
@@ -538,6 +580,24 @@ export async function handler(
538
580
  );
539
581
  }
540
582
 
583
+ // Store context embedding in vault_ctx_vec for contextual reinstatement
584
+ if (parsedCtx?.text && entry) {
585
+ try {
586
+ const ctxEmbedding = await ctx.embed(parsedCtx.text);
587
+ if (ctxEmbedding) {
588
+ const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
589
+ if (rowidResult?.rowid) {
590
+ const rowid = Number(rowidResult.rowid);
591
+ try { ctx.deleteCtxVec(rowid); } catch {}
592
+ ctx.insertCtxVec(rowid, ctxEmbedding);
593
+ }
594
+ }
595
+ } catch (e) {
596
+ // Non-fatal: context embedding failure should not block the save
597
+ console.warn(`[context-vault] Context embedding failed: ${(e as Error).message}`);
598
+ }
599
+ }
600
+
541
601
  if (ctx.config?.dataDir) {
542
602
  maybeShowFeedbackPrompt(ctx.config.dataDir);
543
603
  }
@@ -545,11 +605,19 @@ export async function handler(
545
605
  const relPath = entry.filePath
546
606
  ? entry.filePath.replace(config.vaultDir + '/', '')
547
607
  : entry.filePath;
548
- const parts = [`✓ Saved ${normalizedKind} → ${relPath}`, ` id: ${entry.id}`];
549
- if (title) parts.push(` title: ${title}`);
550
- if (tags?.length) parts.push(` tags: ${tags.join(', ')}`);
551
- parts.push(` tier: ${effectiveTier}`);
552
- 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
+ }
553
621
  const hasBucketTag = (tags || []).some(
554
622
  (t: any) => typeof t === 'string' && t.startsWith('bucket:')
555
623
  );
@@ -563,18 +631,25 @@ export async function handler(
563
631
  (t: any) => typeof t === 'string' && t.startsWith('bucket:')
564
632
  );
565
633
  for (const bt of bucketTags) {
566
- const bucketUserClause = '';
567
- const bucketParams = false ? [bt] : [bt];
568
634
  const exists = ctx.db
569
635
  .prepare(
570
- `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`
571
637
  )
572
- .get(...bucketParams);
638
+ .get(bt);
573
639
  if (!exists) {
574
- parts.push(
575
- ``,
576
- `_Note: bucket '${bt}' is not registered. Use save_context(kind: "bucket", identity_key: "${bt}") to register it._`
577
- );
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
+ }
578
653
  }
579
654
  }
580
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'}_`