context-vault 3.4.4 → 3.5.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 (86) 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 +753 -126
  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 +26 -2
  10. package/dist/server.js.map +1 -1
  11. package/dist/status.d.ts.map +1 -1
  12. package/dist/status.js +29 -0
  13. package/dist/status.js.map +1 -1
  14. package/dist/tools/context-status.d.ts.map +1 -1
  15. package/dist/tools/context-status.js +64 -29
  16. package/dist/tools/context-status.js.map +1 -1
  17. package/dist/tools/get-context.js +23 -18
  18. package/dist/tools/get-context.js.map +1 -1
  19. package/dist/tools/list-context.d.ts +2 -1
  20. package/dist/tools/list-context.d.ts.map +1 -1
  21. package/dist/tools/list-context.js +27 -10
  22. package/dist/tools/list-context.js.map +1 -1
  23. package/dist/tools/save-context.d.ts +2 -1
  24. package/dist/tools/save-context.d.ts.map +1 -1
  25. package/dist/tools/save-context.js +95 -26
  26. package/dist/tools/save-context.js.map +1 -1
  27. package/dist/tools/session-start.d.ts.map +1 -1
  28. package/dist/tools/session-start.js +230 -11
  29. package/dist/tools/session-start.js.map +1 -1
  30. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  31. package/node_modules/@context-vault/core/dist/capture.js +13 -0
  32. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  33. package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
  34. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  35. package/node_modules/@context-vault/core/dist/config.js +47 -2
  36. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  37. package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
  38. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  39. package/node_modules/@context-vault/core/dist/constants.js +13 -0
  40. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  41. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  42. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  43. package/node_modules/@context-vault/core/dist/db.js +73 -9
  44. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  45. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  46. package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
  47. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  48. package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
  49. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  50. package/node_modules/@context-vault/core/dist/index.js +58 -10
  51. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  52. package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
  53. package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
  54. package/node_modules/@context-vault/core/dist/indexing.js +22 -0
  55. package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
  56. package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
  57. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  58. package/node_modules/@context-vault/core/dist/main.js +3 -1
  59. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  60. package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
  61. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  62. package/node_modules/@context-vault/core/dist/search.js +82 -6
  63. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  64. package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
  65. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  66. package/node_modules/@context-vault/core/package.json +5 -1
  67. package/node_modules/@context-vault/core/src/capture.ts +11 -0
  68. package/node_modules/@context-vault/core/src/config.ts +40 -2
  69. package/node_modules/@context-vault/core/src/constants.ts +15 -0
  70. package/node_modules/@context-vault/core/src/db.ts +73 -9
  71. package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
  72. package/node_modules/@context-vault/core/src/index.ts +65 -11
  73. package/node_modules/@context-vault/core/src/indexing.ts +35 -0
  74. package/node_modules/@context-vault/core/src/main.ts +5 -0
  75. package/node_modules/@context-vault/core/src/search.ts +96 -6
  76. package/node_modules/@context-vault/core/src/types.ts +26 -0
  77. package/package.json +2 -2
  78. package/scripts/prepack.js +17 -0
  79. package/src/helpers.ts +25 -0
  80. package/src/server.ts +28 -2
  81. package/src/status.ts +35 -0
  82. package/src/tools/context-status.ts +65 -30
  83. package/src/tools/get-context.ts +24 -24
  84. package/src/tools/list-context.ts +25 -13
  85. package/src/tools/save-context.ts +106 -29
  86. package/src/tools/session-start.ts +257 -13
package/bin/cli.js CHANGED
@@ -1,5 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // Suppress Node.js ExperimentalWarning for built-in SQLite (used by context-vault)
4
+ const originalEmit = process.emit;
5
+ process.emit = function (name, data, ...args) {
6
+ if (name === 'warning' && typeof data === 'object' && data?.name === 'ExperimentalWarning' &&
7
+ typeof data?.message === 'string' && data.message.includes('SQLite')) {
8
+ return false;
9
+ }
10
+ return originalEmit.call(process, name, data, ...args);
11
+ };
12
+
3
13
  // Node.js version guard — must run before any ESM imports
4
14
  const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
5
15
  if (nodeVersion < 22) {
@@ -47,6 +57,7 @@ import { homedir, platform } from 'node:os';
47
57
  import { execSync, execFile, execFileSync, spawn } from 'node:child_process';
48
58
  import { fileURLToPath } from 'node:url';
49
59
  import { APP_URL, API_URL, MARKETING_URL } from '@context-vault/core/constants';
60
+ import { assertNotTestMode } from '@context-vault/core/config';
50
61
 
51
62
  const __filename = fileURLToPath(import.meta.url);
52
63
  const __dirname = dirname(__filename);
@@ -241,12 +252,16 @@ const TOOLS = [
241
252
  name: 'Claude Code',
242
253
  detect: () => commandExistsAsync('claude'),
243
254
  configType: 'cli',
255
+ rulesPath: join(HOME, '.claude', 'rules', 'context-vault.md'),
256
+ rulesMethod: 'write',
244
257
  },
245
258
  {
246
259
  id: 'codex',
247
260
  name: 'Codex',
248
261
  detect: () => commandExistsAsync('codex'),
249
262
  configType: 'cli',
263
+ rulesPath: null,
264
+ rulesMethod: null,
250
265
  },
251
266
  {
252
267
  id: 'claude-desktop',
@@ -255,6 +270,8 @@ const TOOLS = [
255
270
  configType: 'json',
256
271
  configPath: join(appDataDir(), 'Claude', 'claude_desktop_config.json'),
257
272
  configKey: 'mcpServers',
273
+ rulesPath: null,
274
+ rulesMethod: null,
258
275
  },
259
276
  {
260
277
  id: 'cursor',
@@ -263,6 +280,8 @@ const TOOLS = [
263
280
  configType: 'json',
264
281
  configPath: join(HOME, '.cursor', 'mcp.json'),
265
282
  configKey: 'mcpServers',
283
+ rulesPath: join(HOME, '.cursor', 'rules', 'context-vault.mdc'),
284
+ rulesMethod: 'write',
266
285
  },
267
286
  {
268
287
  id: 'windsurf',
@@ -275,6 +294,8 @@ const TOOLS = [
275
294
  : join(HOME, '.codeium', 'windsurf', 'mcp_config.json');
276
295
  },
277
296
  configKey: 'mcpServers',
297
+ rulesPath: join(HOME, '.windsurfrules'),
298
+ rulesMethod: 'append',
278
299
  },
279
300
  {
280
301
  id: 'antigravity',
@@ -283,6 +304,8 @@ const TOOLS = [
283
304
  configType: 'json',
284
305
  configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'),
285
306
  configKey: 'mcpServers',
307
+ rulesPath: null,
308
+ rulesMethod: null,
286
309
  },
287
310
  {
288
311
  id: 'cline',
@@ -296,6 +319,8 @@ const TOOLS = [
296
319
  'cline_mcp_settings.json'
297
320
  ),
298
321
  configKey: 'mcpServers',
322
+ rulesPath: null,
323
+ rulesMethod: null,
299
324
  },
300
325
  {
301
326
  id: 'roo-code',
@@ -309,6 +334,8 @@ const TOOLS = [
309
334
  'cline_mcp_settings.json'
310
335
  ),
311
336
  configKey: 'mcpServers',
337
+ rulesPath: null,
338
+ rulesMethod: null,
312
339
  },
313
340
  ];
314
341
 
@@ -353,12 +380,14 @@ ${bold('Commands:')}
353
380
  ${cyan('hooks')} install|uninstall Install or remove Claude Code memory hook
354
381
  ${cyan('claude')} install|uninstall Alias for hooks install|uninstall
355
382
  ${cyan('skills')} install Install bundled Claude Code skills
383
+ ${cyan('rules')} install Install agent rules for detected AI tools
356
384
  ${cyan('health')} Quick health check — vault, DB, entry count
357
385
  ${cyan('status')} Show vault diagnostics
358
386
  ${cyan('doctor')} Diagnose and repair common issues
359
387
  ${cyan('debug')} Generate AI-pasteable debug report
360
388
  ${cyan('daemon')} start|stop|status Run vault as a shared HTTP daemon (one process, all sessions)
361
389
  ${cyan('restart')} Stop running MCP server processes (client auto-restarts)
390
+ ${cyan('reconnect')} Fix vault path, kill stale servers, re-register MCP, reindex
362
391
  ${cyan('search')} Search vault entries from CLI
363
392
  ${cyan('save')} Save an entry to the vault from CLI
364
393
  ${cyan('import')} <path> Import entries from file, directory, or .zip archive
@@ -545,7 +574,7 @@ async function runSetup() {
545
574
  }
546
575
  } catch {}
547
576
 
548
- console.log(`\n ${dim('[2/2]')}${bold(' Configuring tools...\n')}`);
577
+ console.log(`\n ${dim('[2/3]')}${bold(' Configuring tools...\n')}`);
549
578
  for (const tool of selected) {
550
579
  try {
551
580
  if (tool.configType === 'cli' && tool.id === 'codex') {
@@ -561,6 +590,40 @@ async function runSetup() {
561
590
  }
562
591
  }
563
592
 
593
+ // Offer rules installation for users who previously skipped or used an older version
594
+ console.log(`\n ${dim('[3/3]')}${bold(' Agent rules...\n')}`);
595
+ const rulesContent = loadAgentRules();
596
+ if (rulesContent && !flags.has('--no-rules')) {
597
+ const missingRules = selected.filter((t) => {
598
+ const p = getRulesPathForTool(t);
599
+ return p && !existsSync(p);
600
+ });
601
+ if (missingRules.length > 0) {
602
+ console.log(dim(' Agent rules teach your AI when to save knowledge automatically.'));
603
+ console.log(dim(' No rules file detected for: ' + missingRules.map((t) => t.name).join(', ')));
604
+ console.log();
605
+ const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
606
+ if (rulesAnswer.toLowerCase() !== 'n') {
607
+ for (const tool of missingRules) {
608
+ try {
609
+ const ok = installAgentRulesForTool(tool, rulesContent);
610
+ const rulesPath = getRulesPathForTool(tool);
611
+ if (ok) {
612
+ console.log(` ${green('+')} ${tool.name} — agent rules installed`);
613
+ if (rulesPath) console.log(` ${dim(rulesPath)}`);
614
+ }
615
+ } catch (e) {
616
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
617
+ }
618
+ }
619
+ } else {
620
+ console.log(dim(' Skipped — install later: context-vault rules install'));
621
+ }
622
+ } else {
623
+ console.log(dim(' Agent rules already installed — skipping.'));
624
+ }
625
+ }
626
+
564
627
  console.log();
565
628
  console.log(green(' ✓ Tool configs updated.'));
566
629
  console.log(dim(' Restart your AI tools to apply the changes.'));
@@ -572,7 +635,7 @@ async function runSetup() {
572
635
  }
573
636
 
574
637
  // Detect tools
575
- console.log(dim(` [1/6]`) + bold(' Detecting tools...\n'));
638
+ console.log(dim(` [1/7]`) + bold(' Detecting tools...\n'));
576
639
  verbose(userLevel, 'Scanning for AI tools on this machine.');
577
640
  if (userLevel === 'beginner') console.log();
578
641
  const { detected, results: detectionResults } = await detectAllTools();
@@ -643,8 +706,21 @@ async function runSetup() {
643
706
  }
644
707
  }
645
708
 
709
+ // Fast path for new users: recommended defaults
710
+ let useRecommendedDefaults = false;
711
+ const existingConfigForFastPath = join(HOME, '.context-mcp', 'config.json');
712
+ const isNewInstall = !existsSync(existingConfigForFastPath);
713
+ if (isNewInstall && !isNonInteractive) {
714
+ console.log(dim(' Install with recommended settings?'));
715
+ console.log(dim(' Vault in default location, all hooks, skills, and rules installed.'));
716
+ console.log();
717
+ const fastAnswer = await prompt(' Install with recommended settings? (Y/n):', 'Y');
718
+ useRecommendedDefaults = fastAnswer.toLowerCase() !== 'n';
719
+ if (useRecommendedDefaults) console.log();
720
+ }
721
+
646
722
  // Vault directory (content files)
647
- console.log(dim(` [2/6]`) + bold(' Configuring vault...\n'));
723
+ console.log(dim(` [2/7]`) + bold(' Configuring vault...\n'));
648
724
  verbose(userLevel, 'Your vault is a folder of plain markdown files — you own it.');
649
725
  if (userLevel === 'beginner') console.log();
650
726
 
@@ -664,7 +740,7 @@ async function runSetup() {
664
740
  }
665
741
  }
666
742
 
667
- if (!getFlag('--vault-dir') && !isNonInteractive) {
743
+ if (!getFlag('--vault-dir') && !isNonInteractive && !useRecommendedDefaults) {
668
744
  const existingVaults = scanForVaults();
669
745
  if (existingVaults.length === 1) {
670
746
  console.log(
@@ -696,9 +772,16 @@ async function runSetup() {
696
772
  }
697
773
  console.log();
698
774
  }
775
+ } else if (!getFlag('--vault-dir') && useRecommendedDefaults) {
776
+ // Fast path: still use detected vaults if found
777
+ const existingVaults = scanForVaults();
778
+ if (existingVaults.length >= 1) {
779
+ defaultVaultDir = existingVaults[0].path;
780
+ console.log(` ${green('+')} Using existing vault at ${defaultVaultDir}`);
781
+ }
699
782
  }
700
783
 
701
- const vaultDir = isNonInteractive
784
+ const vaultDir = (isNonInteractive || useRecommendedDefaults)
702
785
  ? defaultVaultDir
703
786
  : await prompt(` Vault directory:`, defaultVaultDir);
704
787
  let resolvedVaultDir = resolve(vaultDir);
@@ -710,7 +793,7 @@ async function runSetup() {
710
793
  console.error(dim(` Remove or rename the file, then run setup again.\n`));
711
794
  process.exit(1);
712
795
  }
713
- } else if (isNonInteractive) {
796
+ } else if (isNonInteractive || useRecommendedDefaults) {
714
797
  mkdirSync(resolvedVaultDir, { recursive: true });
715
798
  console.log(`\n ${green('+')} Created ${resolvedVaultDir}`);
716
799
  } else {
@@ -786,41 +869,18 @@ async function runSetup() {
786
869
  vaultConfig.devDir = join(HOME, 'dev');
787
870
  vaultConfig.mode = 'local';
788
871
 
789
- // Telemetry opt-in
790
- console.log(`\n ${dim('[3/6]')}${bold(' Anonymous error reporting\n')}`);
791
- verbose(userLevel, 'Entirely optional — works identically either way.\n');
792
- console.log(dim(' When enabled, unhandled errors send a minimal event (type, tool name,'));
793
- console.log(dim(' version, platform) to help diagnose issues. No vault content,'));
794
- console.log(dim(' file paths, or personal data is ever sent. Off by default.'));
795
- console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
796
- console.log();
797
-
798
- let telemetryEnabled = vaultConfig.telemetry === true;
799
- if (!isNonInteractive) {
800
- const defaultChoice = telemetryEnabled ? 'Y' : 'n';
801
- const telemetryAnswer = await prompt(
802
- ` Enable anonymous error reporting? (y/N):`,
803
- defaultChoice
804
- );
805
- telemetryEnabled =
806
- telemetryAnswer.toLowerCase() === 'y' || telemetryAnswer.toLowerCase() === 'yes';
807
- }
808
- vaultConfig.telemetry = telemetryEnabled;
809
- console.log(
810
- ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
811
- );
812
-
872
+ assertNotTestMode(configPath);
813
873
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
814
874
  console.log(`\n ${green('+')} Wrote ${configPath}`);
815
875
 
816
876
  // Pre-download embedding model with spinner (skip with --skip-embeddings)
817
877
  const skipEmbeddings = flags.has('--skip-embeddings');
818
878
  if (skipEmbeddings) {
819
- console.log(`\n ${dim('[4/6]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
879
+ console.log(`\n ${dim('[3/7]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
820
880
  console.log(dim(' FTS-only mode — full-text search works, semantic search disabled.'));
821
881
  console.log(dim(' To enable later: context-vault setup (without --skip-embeddings)'));
822
882
  } else {
823
- console.log(`\n ${dim('[4/6]')}${bold(' Downloading embedding model...')}`);
883
+ console.log(`\n ${dim('[3/7]')}${bold(' Downloading embedding model...')}`);
824
884
  verbose(userLevel, 'Enables meaning-based search. ~22MB download, runs fully offline.');
825
885
  console.log(dim(' all-MiniLM-L6-v2 (~22MB, one-time download)'));
826
886
  console.log(dim(` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`));
@@ -902,12 +962,11 @@ async function runSetup() {
902
962
  } catch {}
903
963
  }
904
964
 
905
- // Configure each tool — pass vault dir as arg if non-default
906
- console.log(`\n ${dim('[5/6]')}${bold(' Configuring tools...\n')}`);
965
+ // Configure each tool — always pass vault dir explicitly to prevent config drift
966
+ console.log(`\n ${dim('[4/7]')}${bold(' Configuring tools...\n')}`);
907
967
  verbose(userLevel, 'Writing config so your AI tool can find your vault.\n');
908
968
  const results = [];
909
- const defaultVDir = join(HOME, 'vault');
910
- const customVaultDir = resolvedVaultDir !== resolve(defaultVDir) ? resolvedVaultDir : null;
969
+ const customVaultDir = resolvedVaultDir;
911
970
 
912
971
  for (const tool of selected) {
913
972
  try {
@@ -926,103 +985,107 @@ async function runSetup() {
926
985
  }
927
986
  }
928
987
 
929
- // Claude Code hooks (opt-in)
988
+ // Claude Code extras: hooks, skills, rules (bundled into one step)
989
+ console.log(`\n ${dim('[5/7]')}${bold(' Extras...\n')}`);
930
990
  const claudeConfigured = results.some((r) => r.ok && r.tool.id === 'claude-code');
931
991
  const hookFlag = flags.has('--hooks');
992
+ const configuredTools = results.filter((r) => r.ok).map((r) => r.tool);
993
+ const installedRulesPaths = [];
994
+
932
995
  if (claudeConfigured) {
933
- // 1. Recall hook (UserPromptSubmit)
934
- let installHook = hookFlag;
935
- if (!hookFlag && !isNonInteractive) {
996
+ // Bundled hooks prompt: one Y/n for all three hooks
997
+ let installHooks = hookFlag || useRecommendedDefaults;
998
+ if (!hookFlag && !isNonInteractive && !useRecommendedDefaults) {
999
+ console.log(dim(' Install Claude Code hooks? (recommended)'));
1000
+ console.log(dim(' Memory recall, session capture, and auto-capture.'));
936
1001
  console.log();
937
- console.log(dim(' Claude Code detected install memory hook?'));
938
- console.log(dim(' Searches your vault on every prompt and injects relevant entries'));
939
- console.log(dim(" as additional context alongside Claude's native memory."));
940
- console.log();
941
- const answer = await prompt(' Install Claude Code memory hook? (y/N):', 'N');
942
- installHook = answer.toLowerCase() === 'y';
1002
+ const answer = await prompt(' Install Claude Code hooks? (Y/n):', 'Y');
1003
+ installHooks = answer.toLowerCase() !== 'n';
943
1004
  }
944
- if (installHook) {
1005
+ if (installHooks) {
945
1006
  try {
946
- const installed = installClaudeHook();
947
- if (installed) {
948
- console.log(`\n ${green('+')} Memory hook installed`);
949
- }
1007
+ const hookInstalled = installClaudeHook();
1008
+ if (hookInstalled) console.log(` ${green('+')} Memory recall hook installed`);
950
1009
  } catch (e) {
951
- console.log(`\n ${red('x')} Hook install failed: ${e.message}`);
1010
+ console.log(` ${red('x')} Memory hook failed: ${e.message}`);
952
1011
  }
953
-
954
- // 2. Session capture hook (SessionEnd) — only offer if recall hook was installed
955
- if (!isNonInteractive) {
956
- console.log();
957
- console.log(dim(' Auto-save session summaries when Claude Code exits?'));
958
- console.log(dim(' Captures files touched, tools used, and decisions made per session.'));
959
- console.log();
960
- const captureAnswer = await prompt(' Install session capture hook? (Y/n):', 'Y');
961
- if (captureAnswer.toLowerCase() !== 'n') {
962
- try {
963
- const captureInstalled = installSessionCaptureHook();
964
- if (captureInstalled) {
965
- console.log(` ${green('+')} Session capture hook installed`);
966
- }
967
- } catch (e) {
968
- console.log(` ${red('x')} Session capture hook failed: ${e.message}`);
969
- }
970
- }
971
- } else if (hookFlag) {
972
- try {
973
- installSessionCaptureHook();
974
- } catch {}
1012
+ try {
1013
+ const captureInstalled = installSessionCaptureHook();
1014
+ if (captureInstalled) console.log(` ${green('+')} Session capture hook installed`);
1015
+ } catch (e) {
1016
+ console.log(` ${red('x')} Session capture hook failed: ${e.message}`);
975
1017
  }
976
-
977
- // 3. Auto-capture hook (PostToolCall) — only offer if recall hook was installed
978
- if (!isNonInteractive) {
979
- console.log();
980
- console.log(dim(' Passively log tool calls for richer session summaries?'));
981
- console.log(dim(' Records tool names and file paths after each tool call (lightweight).'));
982
- console.log();
983
- const autoCaptureAnswer = await prompt(' Install auto-capture hook? (Y/n):', 'Y');
984
- if (autoCaptureAnswer.toLowerCase() !== 'n') {
985
- try {
986
- const acInstalled = installPostToolCallHook();
987
- if (acInstalled) {
988
- console.log(` ${green('+')} Auto-capture hook installed`);
989
- }
990
- } catch (e) {
991
- console.log(` ${red('x')} Auto-capture hook failed: ${e.message}`);
992
- }
993
- }
994
- } else if (hookFlag) {
995
- try {
996
- installPostToolCallHook();
997
- } catch {}
1018
+ try {
1019
+ const acInstalled = installPostToolCallHook();
1020
+ if (acInstalled) console.log(` ${green('+')} Auto-capture hook installed`);
1021
+ } catch (e) {
1022
+ console.log(` ${red('x')} Auto-capture hook failed: ${e.message}`);
998
1023
  }
999
- } else if (!isNonInteractive && !hookFlag) {
1000
- console.log(dim(` Skipped install later: context-vault hooks install`));
1024
+ } else {
1025
+ console.log(dim(` Hooks skipped. Install later: context-vault hooks install`));
1001
1026
  }
1002
- }
1003
1027
 
1004
- // Claude Code skills (opt-in)
1005
- if (claudeConfigured && !isNonInteractive) {
1006
- console.log();
1007
- console.log(dim(' Install Claude Code skills? (recommended)'));
1008
- console.log(dim(' compile-context compile vault entries into a project brief'));
1009
- console.log();
1010
- const skillAnswer = await prompt(' Install Claude Code skills? (Y/n):', 'Y');
1011
- const installSkillsFlag = skillAnswer.toLowerCase() !== 'n';
1028
+ // Skills (bundled, no separate prompt unless not using fast path)
1029
+ let installSkillsFlag = useRecommendedDefaults || isNonInteractive;
1030
+ if (!isNonInteractive && !useRecommendedDefaults) {
1031
+ console.log();
1032
+ console.log(dim(' Install Claude Code skills? (recommended)'));
1033
+ console.log(dim(' compile-context, vault-setup'));
1034
+ console.log();
1035
+ const skillAnswer = await prompt(' Install Claude Code skills? (Y/n):', 'Y');
1036
+ installSkillsFlag = skillAnswer.toLowerCase() !== 'n';
1037
+ }
1012
1038
  if (installSkillsFlag) {
1013
1039
  try {
1014
1040
  const names = installSkills();
1015
- if (names.length > 0) {
1016
- for (const name of names) {
1017
- console.log(`\n ${green('+')} ${name} skill installed`);
1018
- }
1041
+ for (const name of names) {
1042
+ console.log(` ${green('+')} ${name} skill installed`);
1019
1043
  }
1020
1044
  } catch (e) {
1021
- console.log(`\n ${red('x')} Skills install failed: ${e.message}`);
1045
+ console.log(` ${red('x')} Skills install failed: ${e.message}`);
1046
+ }
1047
+ } else {
1048
+ console.log(dim(` Skills skipped. Install later: context-vault skills install`));
1049
+ }
1050
+ }
1051
+
1052
+ // Agent rules installation
1053
+ if (configuredTools.length > 0 && !flags.has('--no-rules')) {
1054
+ let installRules = isNonInteractive || useRecommendedDefaults;
1055
+ if (!isNonInteractive && !useRecommendedDefaults) {
1056
+ console.log();
1057
+ console.log(dim(' Install agent rules? (recommended)'));
1058
+ console.log(dim(' Teaches your AI agent when and how to save knowledge to the vault.'));
1059
+ console.log();
1060
+ const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
1061
+ installRules = rulesAnswer.toLowerCase() !== 'n';
1062
+ }
1063
+ if (installRules) {
1064
+ const rulesContent = loadAgentRules();
1065
+ if (rulesContent) {
1066
+ for (const tool of configuredTools) {
1067
+ try {
1068
+ const installed = installAgentRulesForTool(tool, rulesContent);
1069
+ const rulesPath = getRulesPathForTool(tool);
1070
+ if (installed) {
1071
+ console.log(` ${green('+')} ${tool.name} agent rules installed`);
1072
+ if (rulesPath) {
1073
+ console.log(` ${dim(rulesPath)}`);
1074
+ installedRulesPaths.push({ tool: tool.name, path: rulesPath });
1075
+ }
1076
+ }
1077
+ } catch (e) {
1078
+ console.log(` ${red('x')} ${tool.name} rules: ${e.message}`);
1079
+ }
1080
+ }
1081
+ } else {
1082
+ console.log(dim(' Agent rules file not found in package.'));
1022
1083
  }
1023
1084
  } else {
1024
- console.log(dim(` Skipped install later: context-vault skills install`));
1085
+ console.log(dim(' Rules skipped. Install later: context-vault rules install'));
1025
1086
  }
1087
+ } else if (flags.has('--no-rules')) {
1088
+ console.log(dim(' Agent rules skipped (--no-rules)'));
1026
1089
  }
1027
1090
 
1028
1091
  // Seed entry
@@ -1033,8 +1096,36 @@ async function runSetup() {
1033
1096
  );
1034
1097
  }
1035
1098
 
1099
+ // Telemetry opt-in (moved to end, after user has seen value)
1100
+ console.log(`\n ${dim('[6/7]')}${bold(' Anonymous error reporting\n')}`);
1101
+ verbose(userLevel, 'Entirely optional. Works identically either way.\n');
1102
+ console.log(dim(' When enabled, unhandled errors send a minimal event (type, tool name,'));
1103
+ console.log(dim(' version, platform) to help diagnose issues. No vault content,'));
1104
+ console.log(dim(' file paths, or personal data is ever sent. Off by default.'));
1105
+ console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
1106
+ console.log();
1107
+
1108
+ let telemetryEnabled = vaultConfig.telemetry === true;
1109
+ if (!isNonInteractive && !useRecommendedDefaults) {
1110
+ const defaultChoice = telemetryEnabled ? 'Y' : 'n';
1111
+ const telemetryAnswer = await prompt(
1112
+ ` Enable anonymous error reporting? (y/N):`,
1113
+ defaultChoice
1114
+ );
1115
+ telemetryEnabled =
1116
+ telemetryAnswer.toLowerCase() === 'y' || telemetryAnswer.toLowerCase() === 'yes';
1117
+ }
1118
+ vaultConfig.telemetry = telemetryEnabled;
1119
+ console.log(
1120
+ ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
1121
+ );
1122
+
1123
+ // Re-write config with telemetry setting
1124
+ assertNotTestMode(configPath);
1125
+ writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1126
+
1036
1127
  // Health check
1037
- console.log(`\n ${dim('[6/6]')}${bold(' Health check...')}\n`);
1128
+ console.log(`\n ${dim('[7/7]')}${bold(' Health check...')}\n`);
1038
1129
  verbose(userLevel, 'Verifying vault, config, and database are accessible.\n');
1039
1130
  const okResults = results.filter((r) => r.ok);
1040
1131
 
@@ -1128,6 +1219,21 @@ async function runSetup() {
1128
1219
  : []),
1129
1220
  ];
1130
1221
  }
1222
+ if (installedRulesPaths.length > 0) {
1223
+ boxLines.push(``, ` ${bold('Agent rules installed:')}`);
1224
+ for (const { path } of installedRulesPaths) {
1225
+ boxLines.push(` ${dim(path)}`);
1226
+ }
1227
+ boxLines.push(
1228
+ ``,
1229
+ ` ${dim(`View: ${cli} rules show`)}`,
1230
+ ` ${dim(`Remove: ${cli} uninstall`)}`,
1231
+ ` ${dim(`Skip: ${cli} setup --no-rules`)}`
1232
+ );
1233
+ }
1234
+ if (claudeConfigured) {
1235
+ boxLines.push(``, ` ${dim('Personalize: run /vault-setup in your next session')}`);
1236
+ }
1131
1237
  const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
1132
1238
  const pad = (s) => s + ' '.repeat(Math.max(0, innerWidth - s.length));
1133
1239
  console.log();
@@ -1368,8 +1474,8 @@ function configureJsonTool(tool, vaultDir) {
1368
1474
  function createSeedEntries(vaultDir) {
1369
1475
  let created = 0;
1370
1476
 
1371
- // Entry 1: Getting started (improved)
1372
- const insightDir = join(vaultDir, 'knowledge', 'insights');
1477
+ // Entry 1: Getting started
1478
+ const insightDir = join(vaultDir, 'knowledge', 'insight');
1373
1479
  const insightPath = join(insightDir, 'getting-started.md');
1374
1480
  if (!existsSync(insightPath)) {
1375
1481
  mkdirSync(insightDir, { recursive: true });
@@ -1379,6 +1485,9 @@ function createSeedEntries(vaultDir) {
1379
1485
  insightPath,
1380
1486
  `---
1381
1487
  id: ${id1}
1488
+ title: Getting started with your context vault
1489
+ kind: insight
1490
+ tier: durable
1382
1491
  tags: ["getting-started", "vault"]
1383
1492
  source: context-vault-setup
1384
1493
  created: ${now}
@@ -1402,7 +1511,7 @@ ${insightPath}
1402
1511
  }
1403
1512
 
1404
1513
  // Entry 2: Example decision
1405
- const decisionDir = join(vaultDir, 'knowledge', 'decisions');
1514
+ const decisionDir = join(vaultDir, 'knowledge', 'decision');
1406
1515
  const decisionPath = join(decisionDir, 'example-local-first-data.md');
1407
1516
  if (!existsSync(decisionPath)) {
1408
1517
  mkdirSync(decisionDir, { recursive: true });
@@ -1412,6 +1521,9 @@ ${insightPath}
1412
1521
  decisionPath,
1413
1522
  `---
1414
1523
  id: ${id2}
1524
+ title: Use local-first data storage over cloud databases
1525
+ kind: decision
1526
+ tier: durable
1415
1527
  tags: ["example", "architecture"]
1416
1528
  source: context-vault-setup
1417
1529
  created: ${now}
@@ -1584,6 +1696,7 @@ async function runConnect() {
1584
1696
  modeConfig.mode = 'hosted';
1585
1697
  modeConfig.hostedUrl = hostedUrl;
1586
1698
  mkdirSync(join(HOME, '.context-mcp'), { recursive: true });
1699
+ assertNotTestMode(modeConfigPath);
1587
1700
  writeFileSync(modeConfigPath, JSON.stringify(modeConfig, null, 2) + '\n');
1588
1701
 
1589
1702
  console.log();
@@ -1696,6 +1809,7 @@ async function runSwitch() {
1696
1809
  if (target === 'local') {
1697
1810
  vaultConfig.mode = 'local';
1698
1811
  mkdirSync(dataDir, { recursive: true });
1812
+ assertNotTestMode(configPath);
1699
1813
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1700
1814
 
1701
1815
  console.log();
@@ -1756,6 +1870,7 @@ async function runSwitch() {
1756
1870
  vaultConfig.hostedUrl = hostedUrl;
1757
1871
  vaultConfig.apiKey = apiKey;
1758
1872
  mkdirSync(dataDir, { recursive: true });
1873
+ assertNotTestMode(configPath);
1759
1874
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1760
1875
 
1761
1876
  for (const tool of detected) {
@@ -1780,7 +1895,11 @@ async function runSwitch() {
1780
1895
  }
1781
1896
 
1782
1897
  async function runReindex() {
1783
- console.log(dim('Loading vault...'));
1898
+ const dryRun = flags.has('--dry-run');
1899
+ const kindIdx = args.indexOf('--kind');
1900
+ const kindFilter = kindIdx !== -1 && args[kindIdx + 1] ? args[kindIdx + 1] : null;
1901
+
1902
+ console.log(dim(dryRun ? 'Analyzing vault (dry run)...' : 'Loading vault...'));
1784
1903
 
1785
1904
  const { resolveConfig } = await import('@context-vault/core/config');
1786
1905
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
@@ -1806,14 +1925,34 @@ async function runReindex() {
1806
1925
  deleteVec: (r) => deleteVec(stmts, r),
1807
1926
  };
1808
1927
 
1809
- const stats = await reindex(ctx, { fullSync: true });
1928
+ const reindexOpts = {
1929
+ fullSync: true,
1930
+ indexingConfig: config.indexing,
1931
+ dryRun,
1932
+ kindFilter,
1933
+ };
1934
+
1935
+ const stats = await reindex(ctx, reindexOpts);
1810
1936
 
1811
1937
  db.close();
1812
- console.log(green('✓ Reindex complete'));
1813
- console.log(` ${green('+')} ${stats.added} added`);
1814
- console.log(` ${yellow('~')} ${stats.updated} updated`);
1815
- console.log(` ${red('-')} ${stats.removed} removed`);
1816
- console.log(` ${dim('·')} ${stats.unchanged} unchanged`);
1938
+
1939
+ if (dryRun) {
1940
+ console.log(yellow('Dry run results (no changes made):'));
1941
+ console.log(` Would index: ${stats.added}`);
1942
+ console.log(` Would skip: ${stats.skippedIndexing ?? 0}`);
1943
+ } else {
1944
+ console.log(green('✓ Reindex complete'));
1945
+ console.log(` ${green('+')} ${stats.added} added`);
1946
+ console.log(` ${yellow('~')} ${stats.updated} updated`);
1947
+ console.log(` ${red('-')} ${stats.removed} removed`);
1948
+ console.log(` ${dim('·')} ${stats.unchanged} unchanged`);
1949
+ if (stats.skippedIndexing) {
1950
+ console.log(` ${dim('○')} ${stats.skippedIndexing} skipped indexing`);
1951
+ }
1952
+ if (stats.embeddingsCleared) {
1953
+ console.log(` ${dim('⊘')} ${stats.embeddingsCleared} embeddings cleared`);
1954
+ }
1955
+ }
1817
1956
  }
1818
1957
 
1819
1958
  async function runMigrateDirs() {
@@ -2131,6 +2270,11 @@ async function runStatus() {
2131
2270
  }
2132
2271
 
2133
2272
  const status = gatherVaultStatus({ db, config });
2273
+ let schemaVersion = 'unknown';
2274
+ try {
2275
+ const row = db.prepare('PRAGMA user_version').get();
2276
+ schemaVersion = String(row?.user_version ?? 'unknown');
2277
+ } catch {}
2134
2278
 
2135
2279
  db.close();
2136
2280
 
@@ -2148,7 +2292,7 @@ async function runStatus() {
2148
2292
  ` Config: ${config.configPath} ${dim(`(${existsSync(config.configPath) ? 'exists' : 'missing'})`)}`
2149
2293
  );
2150
2294
  console.log(` Resolved: ${status.resolvedFrom}`);
2151
- console.log(` Schema: v7 (teams)`);
2295
+ console.log(` Schema: v${schemaVersion}`);
2152
2296
 
2153
2297
  if (status.kindCounts.length) {
2154
2298
  const BAR_WIDTH = 20;
@@ -2159,7 +2303,9 @@ async function runStatus() {
2159
2303
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
2160
2304
  const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
2161
2305
  const countStr = String(c).padStart(4);
2162
- console.log(` ${dim(bar)} ${countStr} ${kind}s`);
2306
+ const IRREGULAR_PLURALS = { activity: 'activities', inbox: 'inboxes', index: 'indexes', match: 'matches' };
2307
+ const plural = IRREGULAR_PLURALS[kind] || (kind.endsWith('s') ? kind : kind + 's');
2308
+ console.log(` ${dim(bar)} ${countStr} ${plural}`);
2163
2309
  }
2164
2310
  } else {
2165
2311
  console.log(`\n ${dim('(empty — no entries indexed)')}`);
@@ -2325,6 +2471,34 @@ async function runUninstall() {
2325
2471
  console.log(` ${green('+')} Removed installed skills`);
2326
2472
  }
2327
2473
 
2474
+ // Remove agent rules files
2475
+ const claudeRulesPath = join(HOME, '.claude', 'rules', 'context-vault.md');
2476
+ const cursorRulesPath = join(HOME, '.cursor', 'rules', 'context-vault.mdc');
2477
+ const windsurfRulesPath = join(HOME, '.windsurfrules');
2478
+
2479
+ if (existsSync(claudeRulesPath)) {
2480
+ unlinkSync(claudeRulesPath);
2481
+ console.log(` ${green('+')} Removed agent rules (Claude Code: ${claudeRulesPath})`);
2482
+ }
2483
+ if (existsSync(cursorRulesPath)) {
2484
+ unlinkSync(cursorRulesPath);
2485
+ console.log(` ${green('+')} Removed agent rules (Cursor: ${cursorRulesPath})`);
2486
+ }
2487
+ if (existsSync(windsurfRulesPath)) {
2488
+ const content = readFileSync(windsurfRulesPath, 'utf-8');
2489
+ if (content.includes(RULES_DELIMITER_START)) {
2490
+ const cleaned = content
2491
+ .replace(new RegExp(`\n?${RULES_DELIMITER_START}[\\s\\S]*?${RULES_DELIMITER_END}\n?`, 'g'), '\n')
2492
+ .trim();
2493
+ if (cleaned) {
2494
+ writeFileSync(windsurfRulesPath, cleaned + '\n');
2495
+ } else {
2496
+ unlinkSync(windsurfRulesPath);
2497
+ }
2498
+ console.log(` ${green('+')} Removed agent rules section from ${windsurfRulesPath}`);
2499
+ }
2500
+ }
2501
+
2328
2502
  // Optionally remove data directory
2329
2503
  const dataDir = join(HOME, '.context-mcp');
2330
2504
  if (existsSync(dataDir)) {
@@ -3648,6 +3822,78 @@ async function runSessionEnd() {
3648
3822
  meta: { session_id: session_id ?? null, cwd, message_count },
3649
3823
  });
3650
3824
  console.log(`context-vault session captured — id: ${entry.id}`);
3825
+
3826
+ // ── Auto-insight extraction ──────────────────────────────────────────────
3827
+ const aiConfig = config.autoInsights ?? { enabled: true, patterns: ['★ Insight'], minChars: 50, maxPerSession: 5, tier: 'working' };
3828
+ if (aiConfig.enabled !== false) {
3829
+ try {
3830
+ const patterns = aiConfig.patterns ?? ['★ Insight'];
3831
+ const minChars = aiConfig.minChars ?? 50;
3832
+ const maxInsights = aiConfig.maxPerSession ?? 5;
3833
+ const defaultTier = aiConfig.tier ?? 'working';
3834
+
3835
+ // Build regex for all configured patterns
3836
+ const escapedPatterns = patterns.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
3837
+ const patternRe = new RegExp(
3838
+ `(?:${escapedPatterns.join('|')})[─\\s]*\`?\\n([\\s\\S]*?)\\n\`?─{10,}`,
3839
+ 'g'
3840
+ );
3841
+
3842
+ const insightBlocks = [];
3843
+ for (const turn of turns) {
3844
+ if (turn.role !== 'assistant') continue;
3845
+ const text = extractText(turn);
3846
+ if (!text) continue;
3847
+ for (const m of text.matchAll(patternRe)) {
3848
+ const insightBody = m[1].trim();
3849
+ if (insightBody.length >= minChars && insightBlocks.length < maxInsights) {
3850
+ insightBlocks.push(insightBody);
3851
+ }
3852
+ }
3853
+ }
3854
+
3855
+ if (insightBlocks.length > 0) {
3856
+ // Check existing auto-insight entries for dedup (by title, lightweight)
3857
+ const existingTitles = new Set();
3858
+ try {
3859
+ const rows = db.prepare(
3860
+ `SELECT title FROM entries WHERE tags LIKE '%auto-insight%' ORDER BY created_at DESC LIMIT 100`
3861
+ ).all();
3862
+ for (const r of rows) {
3863
+ if (r.title) existingTitles.add(r.title.toLowerCase());
3864
+ }
3865
+ } catch {}
3866
+
3867
+ let savedCount = 0;
3868
+ for (const insightBody of insightBlocks) {
3869
+ const boldMatch = insightBody.match(/\*\*(.+?)\*\*/);
3870
+ const firstLine = insightBody.split('\n')[0].replace(/\*\*/g, '').trim();
3871
+ const insightTitle = boldMatch ? boldMatch[1].slice(0, 80) : firstLine.slice(0, 80);
3872
+
3873
+ // Skip near-duplicates by title
3874
+ if (existingTitles.has(insightTitle.toLowerCase())) continue;
3875
+
3876
+ const insightTags = ['auto-insight', 'session-insight', `bucket:${project}`];
3877
+ await captureAndIndex(ctx, {
3878
+ kind: 'insight',
3879
+ title: insightTitle,
3880
+ body: insightBody,
3881
+ tags: insightTags,
3882
+ source: `claude-code session ${new Date().toISOString().slice(0, 10)}`,
3883
+ tier: defaultTier,
3884
+ meta: { auto_extracted: true, session_id: session_id ?? null },
3885
+ });
3886
+ existingTitles.add(insightTitle.toLowerCase());
3887
+ savedCount++;
3888
+ }
3889
+ if (savedCount > 0) {
3890
+ console.log(`context-vault auto-insights — ${savedCount} insight${savedCount === 1 ? '' : 's'} saved`);
3891
+ }
3892
+ }
3893
+ } catch {
3894
+ // Auto-insight extraction is best-effort
3895
+ }
3896
+ }
3651
3897
  } catch {
3652
3898
  // fail silently — never block session end
3653
3899
  } finally {
@@ -3936,6 +4182,73 @@ function installSkills() {
3936
4182
  return installed;
3937
4183
  }
3938
4184
 
4185
+ const RULES_DELIMITER_START = '<!-- context-vault agent rules -->';
4186
+ const RULES_DELIMITER_END = '<!-- /context-vault agent rules -->';
4187
+
4188
+ /**
4189
+ * Load agent-rules.md from the assets directory.
4190
+ * Returns the file content or null if not found.
4191
+ */
4192
+ function loadAgentRules() {
4193
+ const rulesPath = join(ROOT, 'assets', 'agent-rules.md');
4194
+ if (!existsSync(rulesPath)) return null;
4195
+ return readFileSync(rulesPath, 'utf-8');
4196
+ }
4197
+
4198
+ /**
4199
+ * Return the path where agent rules are/would be installed for a given tool.
4200
+ * Returns null for tools with no rules install path.
4201
+ */
4202
+ function getRulesPathForTool(tool) {
4203
+ return tool.rulesPath || null;
4204
+ }
4205
+
4206
+ /**
4207
+ * Install agent rules for a specific tool.
4208
+ * Uses tool.rulesPath and tool.rulesMethod from the TOOLS array.
4209
+ * - 'write' method: writes the file directly (Claude Code, Cursor)
4210
+ * - 'append' method: appends with delimiter markers (Windsurf)
4211
+ * Returns true if installed/updated, false if already up to date or skipped.
4212
+ */
4213
+ function installAgentRulesForTool(tool, rulesContent) {
4214
+ const rulesPath = tool.rulesPath;
4215
+ if (!rulesPath) return false;
4216
+
4217
+ if (tool.rulesMethod === 'write') {
4218
+ if (existsSync(rulesPath)) {
4219
+ const existing = readFileSync(rulesPath, 'utf-8');
4220
+ if (existing.trim() === rulesContent.trim()) return false;
4221
+ }
4222
+ mkdirSync(dirname(rulesPath), { recursive: true });
4223
+ writeFileSync(rulesPath, rulesContent);
4224
+ return true;
4225
+ }
4226
+
4227
+ if (tool.rulesMethod === 'append') {
4228
+ const delimited = `\n${RULES_DELIMITER_START}\n${rulesContent}\n${RULES_DELIMITER_END}\n`;
4229
+ if (existsSync(rulesPath)) {
4230
+ const existing = readFileSync(rulesPath, 'utf-8');
4231
+ if (existing.includes(RULES_DELIMITER_START)) {
4232
+ const delimiterRegex = new RegExp(
4233
+ `\n?${RULES_DELIMITER_START}[\\s\\S]*?${RULES_DELIMITER_END}\n?`,
4234
+ 'g'
4235
+ );
4236
+ const existingSection = existing.match(delimiterRegex)?.[0] || '';
4237
+ if (existingSection.includes(rulesContent.trim())) return false;
4238
+ const cleaned = existing.replace(delimiterRegex, '');
4239
+ writeFileSync(rulesPath, cleaned + delimited);
4240
+ return true;
4241
+ }
4242
+ writeFileSync(rulesPath, existing + delimited);
4243
+ } else {
4244
+ writeFileSync(rulesPath, delimited.trimStart());
4245
+ }
4246
+ return true;
4247
+ }
4248
+
4249
+ return false;
4250
+ }
4251
+
3939
4252
  /** Returns the path to Claude Code's global settings.json */
3940
4253
  function claudeSettingsPath() {
3941
4254
  return join(HOME, '.claude', 'settings.json');
@@ -4291,6 +4604,160 @@ ${bold('Commands:')}
4291
4604
 
4292
4605
  ${bold('Bundled skills:')}
4293
4606
  ${cyan('compile-context')} Compile vault entries into a project brief using create_snapshot
4607
+ ${cyan('vault-setup')} Agent-assisted vault customization (run /vault-setup)
4608
+ `);
4609
+ }
4610
+ }
4611
+
4612
+ async function runRules() {
4613
+ const sub = args[1];
4614
+
4615
+ if (sub === 'install') {
4616
+ console.log();
4617
+ const rulesContent = loadAgentRules();
4618
+ if (!rulesContent) {
4619
+ console.log(` ${yellow('!')} Agent rules file not found in package.\n`);
4620
+ process.exit(1);
4621
+ }
4622
+
4623
+ const { detected } = await detectAllTools();
4624
+ if (detected.length === 0) {
4625
+ console.log(` ${yellow('!')} No supported tools detected.\n`);
4626
+ process.exit(1);
4627
+ }
4628
+
4629
+ let installed = 0;
4630
+ for (const tool of detected) {
4631
+ try {
4632
+ const ok = installAgentRulesForTool(tool, rulesContent);
4633
+ const rulesPath = getRulesPathForTool(tool);
4634
+ if (ok) {
4635
+ console.log(` ${green('+')} ${tool.name} — agent rules installed`);
4636
+ if (rulesPath) console.log(` ${dim(rulesPath)}`);
4637
+ installed++;
4638
+ } else {
4639
+ const hasPath = !!rulesPath;
4640
+ const alreadyExists = hasPath && existsSync(rulesPath);
4641
+ if (alreadyExists) {
4642
+ console.log(` ${dim('-')} ${tool.name} — already installed`);
4643
+ } else if (hasPath) {
4644
+ console.log(` ${dim('-')} ${tool.name} — skipped (up to date)`);
4645
+ } else {
4646
+ console.log(` ${dim('-')} ${tool.name} — not supported`);
4647
+ }
4648
+ }
4649
+ } catch (e) {
4650
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
4651
+ }
4652
+ }
4653
+
4654
+ console.log();
4655
+ if (installed > 0) {
4656
+ console.log(dim(' Rules teach your AI agent when to save knowledge automatically.'));
4657
+ console.log(dim(' Restart your AI tools to apply.'));
4658
+ console.log(dim(` View: context-vault rules show`));
4659
+ console.log(dim(` Remove: context-vault uninstall`));
4660
+ }
4661
+ console.log();
4662
+ } else if (sub === 'show') {
4663
+ const { detected } = await detectAllTools();
4664
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4665
+ if (toolsWithRules.length === 0) {
4666
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4667
+ process.exit(1);
4668
+ }
4669
+ let anyShown = false;
4670
+ for (const tool of toolsWithRules) {
4671
+ const rulesPath = getRulesPathForTool(tool);
4672
+ if (!rulesPath || !existsSync(rulesPath)) {
4673
+ console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4674
+ console.log(dim(` Run: context-vault rules install`));
4675
+ continue;
4676
+ }
4677
+ if (anyShown) console.log(dim(' ' + '─'.repeat(40)));
4678
+ console.log(`\n ${dim(`${tool.name}: ${rulesPath}`)}\n`);
4679
+ console.log(readFileSync(rulesPath, 'utf-8'));
4680
+ anyShown = true;
4681
+ }
4682
+ if (!anyShown) {
4683
+ console.log(dim(`\n Run: context-vault rules install\n`));
4684
+ process.exit(1);
4685
+ }
4686
+ } else if (sub === 'path') {
4687
+ const { detected } = await detectAllTools();
4688
+ const supportedTools = detected.filter((t) => getRulesPathForTool(t));
4689
+ if (supportedTools.length === 0) {
4690
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4691
+ process.exit(1);
4692
+ }
4693
+ console.log();
4694
+ for (const tool of supportedTools) {
4695
+ const p = getRulesPathForTool(tool);
4696
+ const installed = existsSync(p);
4697
+ console.log(` ${tool.name}: ${p} ${installed ? green('(installed)') : dim('(not installed)')}`);
4698
+ }
4699
+ console.log();
4700
+ } else if (sub === 'diff') {
4701
+ const bundled = loadAgentRules();
4702
+ if (!bundled) {
4703
+ console.log(`\n ${yellow('!')} Agent rules file not found in package.\n`);
4704
+ process.exit(1);
4705
+ }
4706
+ const { detected } = await detectAllTools();
4707
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4708
+ if (toolsWithRules.length === 0) {
4709
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4710
+ process.exit(1);
4711
+ }
4712
+ for (const tool of toolsWithRules) {
4713
+ const rulesPath = getRulesPathForTool(tool);
4714
+ if (!rulesPath || !existsSync(rulesPath)) {
4715
+ console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4716
+ console.log(dim(` Run: context-vault rules install`));
4717
+ continue;
4718
+ }
4719
+ const installed = readFileSync(rulesPath, 'utf-8');
4720
+ if (installed.trim() === bundled.trim()) {
4721
+ console.log(`\n ${green('✓')} ${tool.name}: rules are up to date (${rulesPath})`);
4722
+ } else {
4723
+ console.log(`\n ${yellow('!')} ${tool.name}: installed rules differ from bundled version.`);
4724
+ console.log(` ${dim(rulesPath)}\n`);
4725
+ const installedLines = installed.split('\n');
4726
+ const bundledLines = bundled.split('\n');
4727
+ const maxLines = Math.max(installedLines.length, bundledLines.length);
4728
+ for (let i = 0; i < maxLines; i++) {
4729
+ const a = installedLines[i];
4730
+ const b = bundledLines[i];
4731
+ if (a === undefined) {
4732
+ console.log(` ${green('+')} ${b}`);
4733
+ } else if (b === undefined) {
4734
+ console.log(` ${red('-')} ${a}`);
4735
+ } else if (a !== b) {
4736
+ console.log(` ${red('-')} ${a}`);
4737
+ console.log(` ${green('+')} ${b}`);
4738
+ }
4739
+ }
4740
+ console.log();
4741
+ console.log(dim(' To upgrade: context-vault rules install'));
4742
+ }
4743
+ }
4744
+ console.log();
4745
+ } else {
4746
+ console.log(`
4747
+ ${bold('context-vault rules')} <command>
4748
+
4749
+ Manage agent rules that teach AI tools when and how to use the vault.
4750
+
4751
+ ${bold('Commands:')}
4752
+ ${cyan('rules install')} Install agent rules for all detected AI tools
4753
+ ${cyan('rules show')} Print the currently installed rules file
4754
+ ${cyan('rules diff')} Show diff between installed rules and bundled version
4755
+ ${cyan('rules path')} Print the path where rules are/would be installed
4756
+
4757
+ ${bold('Installed to:')}
4758
+ ${cyan('Claude Code')} ~/.claude/rules/context-vault.md
4759
+ ${cyan('Cursor')} ~/.cursor/rules/context-vault.mdc
4760
+ ${cyan('Windsurf')} ~/.windsurfrules (appended with delimiters)
4294
4761
  `);
4295
4762
  }
4296
4763
  }
@@ -5105,6 +5572,160 @@ async function runRestart() {
5105
5572
  console.log();
5106
5573
  }
5107
5574
 
5575
+ async function runReconnect() {
5576
+ console.log();
5577
+ console.log(` ${bold('◇ context-vault reconnect')}`);
5578
+ console.log();
5579
+
5580
+ // 1. Read current config to get the correct vault dir
5581
+ const { resolveConfig } = await import('@context-vault/core/config');
5582
+ const config = resolveConfig();
5583
+ const vaultDir = config.vaultDir;
5584
+
5585
+ console.log(` Vault dir: ${cyan(vaultDir)}`);
5586
+ if (!existsSync(vaultDir)) {
5587
+ console.error(red(` Vault directory does not exist: ${vaultDir}`));
5588
+ console.error(dim(` Run context-vault setup to configure.`));
5589
+ process.exit(1);
5590
+ }
5591
+
5592
+ // Count entries to confirm it's a real vault
5593
+ const mdFiles = readdirSync(vaultDir, { recursive: true })
5594
+ .filter(f => String(f).endsWith('.md'));
5595
+ console.log(` Found ${mdFiles.length} markdown files`);
5596
+ console.log();
5597
+
5598
+ // 2. Kill all running context-vault serve processes (they have stale --vault-dir)
5599
+ const isWin = platform() === 'win32';
5600
+ let psOutput;
5601
+ try {
5602
+ const psCmd = isWin
5603
+ ? 'wmic process where "CommandLine like \'%context-vault%\'" get ProcessId,CommandLine /format:list'
5604
+ : 'ps aux';
5605
+ psOutput = execSync(psCmd, { encoding: 'utf-8', timeout: 5000 });
5606
+ } catch (e) {
5607
+ console.error(red(` Failed to list processes: ${e.message}`));
5608
+ process.exit(1);
5609
+ }
5610
+
5611
+ const currentPid = process.pid;
5612
+ const serverPids = [];
5613
+
5614
+ if (isWin) {
5615
+ const pidMatches = psOutput.matchAll(/ProcessId=(\d+)/g);
5616
+ for (const m of pidMatches) {
5617
+ const pid = parseInt(m[1], 10);
5618
+ if (pid !== currentPid) serverPids.push(pid);
5619
+ }
5620
+ } else {
5621
+ const lines = psOutput.split('\n');
5622
+ for (const line of lines) {
5623
+ const match = line.match(/^\S+\s+(\d+)\s/);
5624
+ if (!match) continue;
5625
+ const pid = parseInt(match[1], 10);
5626
+ if (pid === currentPid) continue;
5627
+ if (
5628
+ /context-vault.*(serve|stdio|server\/index)/.test(line) ||
5629
+ /server\/index\.js.*context-vault/.test(line)
5630
+ ) {
5631
+ serverPids.push(pid);
5632
+ }
5633
+ }
5634
+ }
5635
+
5636
+ if (serverPids.length > 0) {
5637
+ console.log(` Stopping ${serverPids.length} stale server process${serverPids.length === 1 ? '' : 'es'}...`);
5638
+ for (const pid of serverPids) {
5639
+ try {
5640
+ process.kill(pid, 'SIGTERM');
5641
+ console.log(` ${green('✓')} Stopped PID ${pid}`);
5642
+ } catch (e) {
5643
+ if (e.code !== 'ESRCH') {
5644
+ console.log(` ${yellow('!')} Could not stop PID ${pid}: ${e.message}`);
5645
+ }
5646
+ }
5647
+ }
5648
+ // Wait for graceful shutdown
5649
+ await new Promise((resolve) => setTimeout(resolve, 1500));
5650
+ // Force-kill any survivors
5651
+ for (const pid of serverPids) {
5652
+ try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch {}
5653
+ }
5654
+ console.log();
5655
+ } else {
5656
+ console.log(dim(' No running server processes found.'));
5657
+ console.log();
5658
+ }
5659
+
5660
+ // 3. Re-register MCP server with correct vault-dir for each detected tool
5661
+ const env = { ...process.env };
5662
+ delete env.CLAUDECODE;
5663
+
5664
+ const tools = [];
5665
+ try { execSync('which claude', { stdio: 'pipe' }); tools.push('claude'); } catch {}
5666
+ try { execSync('which codex', { stdio: 'pipe' }); tools.push('codex'); } catch {}
5667
+
5668
+ for (const tool of tools) {
5669
+ try {
5670
+ execFileSync(tool, ['mcp', 'remove', 'context-vault', '-s', 'user'], { stdio: 'pipe', env });
5671
+ } catch {}
5672
+
5673
+ try {
5674
+ if (isInstalledPackage()) {
5675
+ execFileSync(
5676
+ tool,
5677
+ ['mcp', 'add', '-s', 'user', 'context-vault', '--', 'context-vault', 'serve', '--vault-dir', vaultDir],
5678
+ { stdio: 'pipe', env }
5679
+ );
5680
+ } else if (isNpx()) {
5681
+ execFileSync(
5682
+ tool,
5683
+ ['mcp', 'add', '-s', 'user', 'context-vault', '-e', 'NODE_OPTIONS=--no-warnings=ExperimentalWarning',
5684
+ '--', 'npx', '-y', 'context-vault', 'serve', '--vault-dir', vaultDir],
5685
+ { stdio: 'pipe', env }
5686
+ );
5687
+ } else {
5688
+ execFileSync(
5689
+ tool,
5690
+ ['mcp', 'add', '-s', 'user', 'context-vault', '-e', 'NODE_OPTIONS=--no-warnings=ExperimentalWarning',
5691
+ '--', process.execPath, SERVER_PATH, '--vault-dir', vaultDir],
5692
+ { stdio: 'pipe', env }
5693
+ );
5694
+ }
5695
+ console.log(` ${green('✓')} ${tool} MCP re-registered with vault-dir: ${vaultDir}`);
5696
+ } catch (e) {
5697
+ console.log(` ${red('✘')} Failed to register ${tool}: ${e.stderr?.toString().trim() || e.message}`);
5698
+ }
5699
+ }
5700
+
5701
+ // 4. Reindex to ensure DB matches vault dir
5702
+ console.log();
5703
+ console.log(` Reindexing...`);
5704
+ try {
5705
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
5706
+ await import('@context-vault/core/db');
5707
+ const { embed } = await import('@context-vault/core/embed');
5708
+ const { reindex } = await import('@context-vault/core/index');
5709
+ const db = await initDatabase(config.dbPath);
5710
+ const stmts = prepareStatements(db);
5711
+ const ctx = {
5712
+ db, config, stmts, embed,
5713
+ insertVec: (r, e) => insertVec(stmts, r, e),
5714
+ deleteVec: (r) => deleteVec(stmts, r),
5715
+ };
5716
+ const stats = await reindex(ctx, { fullSync: true });
5717
+ db.close();
5718
+ console.log(` ${green('✓')} Reindex: +${stats.added} added, ~${stats.updated} updated, -${stats.removed} removed`);
5719
+ } catch (e) {
5720
+ console.log(` ${yellow('!')} Reindex failed: ${e.message}`);
5721
+ console.log(dim(` Run 'context-vault reindex --vault-dir ${vaultDir}' manually.`));
5722
+ }
5723
+
5724
+ console.log();
5725
+ console.log(green(' Reconnected.') + dim(' Start a new Claude session to use the updated vault.'));
5726
+ console.log();
5727
+ }
5728
+
5108
5729
  async function runConsolidate() {
5109
5730
  const dryRun = flags.has('--dry-run');
5110
5731
  const tagArg = getFlag('--tag');
@@ -5657,6 +6278,9 @@ async function main() {
5657
6278
  case 'skills':
5658
6279
  await runSkills();
5659
6280
  break;
6281
+ case 'rules':
6282
+ await runRules();
6283
+ break;
5660
6284
  case 'flush':
5661
6285
  await runFlush();
5662
6286
  break;
@@ -5726,6 +6350,9 @@ async function main() {
5726
6350
  case 'restart':
5727
6351
  await runRestart();
5728
6352
  break;
6353
+ case 'reconnect':
6354
+ await runReconnect();
6355
+ break;
5729
6356
  case 'consolidate':
5730
6357
  await runConsolidate();
5731
6358
  break;