context-vault 3.4.5 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/cli.js +489 -299
  2. package/dist/server.js +2 -0
  3. package/dist/server.js.map +1 -1
  4. package/dist/status.d.ts.map +1 -1
  5. package/dist/status.js +29 -0
  6. package/dist/status.js.map +1 -1
  7. package/dist/tools/context-status.d.ts.map +1 -1
  8. package/dist/tools/context-status.js +39 -5
  9. package/dist/tools/context-status.js.map +1 -1
  10. package/dist/tools/get-context.d.ts.map +1 -1
  11. package/dist/tools/get-context.js +1 -0
  12. package/dist/tools/get-context.js.map +1 -1
  13. package/dist/tools/list-context.d.ts +2 -1
  14. package/dist/tools/list-context.d.ts.map +1 -1
  15. package/dist/tools/list-context.js +22 -5
  16. package/dist/tools/list-context.js.map +1 -1
  17. package/dist/tools/save-context.d.ts +2 -1
  18. package/dist/tools/save-context.d.ts.map +1 -1
  19. package/dist/tools/save-context.js +58 -4
  20. package/dist/tools/save-context.js.map +1 -1
  21. package/dist/tools/session-start.d.ts.map +1 -1
  22. package/dist/tools/session-start.js +192 -7
  23. package/dist/tools/session-start.js.map +1 -1
  24. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  25. package/node_modules/@context-vault/core/dist/capture.js +2 -0
  26. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  27. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  28. package/node_modules/@context-vault/core/dist/config.js +27 -1
  29. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  30. package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
  31. package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
  32. package/node_modules/@context-vault/core/dist/constants.js +13 -0
  33. package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
  34. package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
  35. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  36. package/node_modules/@context-vault/core/dist/db.js +73 -9
  37. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  38. package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
  39. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  40. package/node_modules/@context-vault/core/dist/index.js +58 -10
  41. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  42. package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
  43. package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
  44. package/node_modules/@context-vault/core/dist/indexing.js +22 -0
  45. package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
  46. package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
  47. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  48. package/node_modules/@context-vault/core/dist/main.js +3 -1
  49. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  50. package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
  51. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  52. package/node_modules/@context-vault/core/dist/search.js +82 -6
  53. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  54. package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
  55. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  56. package/node_modules/@context-vault/core/package.json +5 -1
  57. package/node_modules/@context-vault/core/src/capture.ts +2 -0
  58. package/node_modules/@context-vault/core/src/config.ts +18 -1
  59. package/node_modules/@context-vault/core/src/constants.ts +15 -0
  60. package/node_modules/@context-vault/core/src/db.ts +73 -9
  61. package/node_modules/@context-vault/core/src/index.ts +65 -11
  62. package/node_modules/@context-vault/core/src/indexing.ts +35 -0
  63. package/node_modules/@context-vault/core/src/main.ts +5 -0
  64. package/node_modules/@context-vault/core/src/search.ts +96 -6
  65. package/node_modules/@context-vault/core/src/types.ts +26 -0
  66. package/package.json +2 -2
  67. package/src/server.ts +3 -0
  68. package/src/status.ts +35 -0
  69. package/src/tools/context-status.ts +40 -5
  70. package/src/tools/get-context.ts +1 -0
  71. package/src/tools/list-context.ts +20 -5
  72. package/src/tools/save-context.ts +67 -4
  73. package/src/tools/session-start.ts +222 -9
package/bin/cli.js CHANGED
@@ -188,6 +188,7 @@ const args = process.argv.slice(2);
188
188
  const command = args[0];
189
189
  const flags = new Set(args.filter((a) => a.startsWith('--')));
190
190
  const isNonInteractive = flags.has('--yes') || !process.stdin.isTTY;
191
+ const isDryRun = flags.has('--dry-run');
191
192
 
192
193
  function getFlag(name) {
193
194
  const idx = args.indexOf(name);
@@ -252,12 +253,16 @@ const TOOLS = [
252
253
  name: 'Claude Code',
253
254
  detect: () => commandExistsAsync('claude'),
254
255
  configType: 'cli',
256
+ rulesPath: join(HOME, '.claude', 'rules', 'context-vault.md'),
257
+ rulesMethod: 'write',
255
258
  },
256
259
  {
257
260
  id: 'codex',
258
261
  name: 'Codex',
259
262
  detect: () => commandExistsAsync('codex'),
260
263
  configType: 'cli',
264
+ rulesPath: null,
265
+ rulesMethod: null,
261
266
  },
262
267
  {
263
268
  id: 'claude-desktop',
@@ -266,6 +271,8 @@ const TOOLS = [
266
271
  configType: 'json',
267
272
  configPath: join(appDataDir(), 'Claude', 'claude_desktop_config.json'),
268
273
  configKey: 'mcpServers',
274
+ rulesPath: null,
275
+ rulesMethod: null,
269
276
  },
270
277
  {
271
278
  id: 'cursor',
@@ -274,6 +281,8 @@ const TOOLS = [
274
281
  configType: 'json',
275
282
  configPath: join(HOME, '.cursor', 'mcp.json'),
276
283
  configKey: 'mcpServers',
284
+ rulesPath: join(HOME, '.cursor', 'rules', 'context-vault.mdc'),
285
+ rulesMethod: 'write',
277
286
  },
278
287
  {
279
288
  id: 'windsurf',
@@ -286,6 +295,8 @@ const TOOLS = [
286
295
  : join(HOME, '.codeium', 'windsurf', 'mcp_config.json');
287
296
  },
288
297
  configKey: 'mcpServers',
298
+ rulesPath: join(HOME, '.windsurfrules'),
299
+ rulesMethod: 'append',
289
300
  },
290
301
  {
291
302
  id: 'antigravity',
@@ -294,6 +305,8 @@ const TOOLS = [
294
305
  configType: 'json',
295
306
  configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'),
296
307
  configKey: 'mcpServers',
308
+ rulesPath: null,
309
+ rulesMethod: null,
297
310
  },
298
311
  {
299
312
  id: 'cline',
@@ -307,6 +320,8 @@ const TOOLS = [
307
320
  'cline_mcp_settings.json'
308
321
  ),
309
322
  configKey: 'mcpServers',
323
+ rulesPath: null,
324
+ rulesMethod: null,
310
325
  },
311
326
  {
312
327
  id: 'roo-code',
@@ -320,6 +335,8 @@ const TOOLS = [
320
335
  'cline_mcp_settings.json'
321
336
  ),
322
337
  configKey: 'mcpServers',
338
+ rulesPath: null,
339
+ rulesMethod: null,
323
340
  },
324
341
  ];
325
342
 
@@ -410,6 +427,7 @@ ${bold('Commands:')}
410
427
  --yes Non-interactive mode (accept all defaults)
411
428
  --force Overwrite existing config without confirmation
412
429
  --skip-embeddings Skip embedding model download (FTS-only mode)
430
+ --dry-run Show what setup would do without writing anything
413
431
  `);
414
432
  }
415
433
 
@@ -421,11 +439,15 @@ async function runSetup() {
421
439
  console.log();
422
440
  console.log(` ${bold('◇ context-vault')} ${dim(`v${VERSION}`)}`);
423
441
  console.log(dim(' Persistent memory for AI agents'));
442
+ if (isDryRun) {
443
+ console.log();
444
+ console.log(yellow(' [dry-run] No files will be written. Showing what setup would do.'));
445
+ }
424
446
  console.log();
425
447
 
426
448
  // Check for existing installation
427
449
  const existingConfig = join(HOME, '.context-mcp', 'config.json');
428
- if (existsSync(existingConfig) && !isNonInteractive) {
450
+ if (existsSync(existingConfig) && !isNonInteractive && !isDryRun) {
429
451
  let existingVault = '(unknown)';
430
452
  try {
431
453
  const cfg = JSON.parse(readFileSync(existingConfig, 'utf-8'));
@@ -619,7 +641,7 @@ async function runSetup() {
619
641
  }
620
642
 
621
643
  // Detect tools
622
- console.log(dim(` [1/6]`) + bold(' Detecting tools...\n'));
644
+ console.log(dim(` [1/7]`) + bold(' Detecting tools...\n'));
623
645
  verbose(userLevel, 'Scanning for AI tools on this machine.');
624
646
  if (userLevel === 'beginner') console.log();
625
647
  const { detected, results: detectionResults } = await detectAllTools();
@@ -656,9 +678,9 @@ async function runSetup() {
656
678
  ${dim('}')}\n`);
657
679
  }
658
680
 
659
- // In non-interactive mode, continue setup without tools (vault, config, etc.)
660
- if (isNonInteractive) {
661
- console.log(dim(' Continuing setup without tool configuration (--yes mode).\n'));
681
+ // In non-interactive/dry-run mode, continue setup without tools (vault, config, etc.)
682
+ if (isDryRun || isNonInteractive) {
683
+ console.log(dim(` Continuing setup without tool configuration (${isDryRun ? '--dry-run' : '--yes'} mode).\n`));
662
684
  } else {
663
685
  return;
664
686
  }
@@ -666,7 +688,7 @@ async function runSetup() {
666
688
 
667
689
  // Select tools
668
690
  let selected;
669
- if (isNonInteractive || detected.length === 1) {
691
+ if (isDryRun || isNonInteractive || detected.length === 1) {
670
692
  selected = detected;
671
693
  if (detected.length === 1) {
672
694
  console.log(` ${dim('→')} Auto-selected ${detected[0].name}\n`);
@@ -690,8 +712,23 @@ async function runSetup() {
690
712
  }
691
713
  }
692
714
 
715
+ // Fast path for new users: recommended defaults
716
+ let useRecommendedDefaults = false;
717
+ const existingConfigForFastPath = join(HOME, '.context-mcp', 'config.json');
718
+ const isNewInstall = !existsSync(existingConfigForFastPath);
719
+ if (isDryRun) {
720
+ useRecommendedDefaults = true;
721
+ } else if (isNewInstall && !isNonInteractive) {
722
+ console.log(dim(' Install with recommended settings?'));
723
+ console.log(dim(' Vault in default location, all hooks, skills, and rules installed.'));
724
+ console.log();
725
+ const fastAnswer = await prompt(' Install with recommended settings? (Y/n):', 'Y');
726
+ useRecommendedDefaults = fastAnswer.toLowerCase() !== 'n';
727
+ if (useRecommendedDefaults) console.log();
728
+ }
729
+
693
730
  // Vault directory (content files)
694
- console.log(dim(` [2/6]`) + bold(' Configuring vault...\n'));
731
+ console.log(dim(` [2/7]`) + bold(' Configuring vault...\n'));
695
732
  verbose(userLevel, 'Your vault is a folder of plain markdown files — you own it.');
696
733
  if (userLevel === 'beginner') console.log();
697
734
 
@@ -711,7 +748,7 @@ async function runSetup() {
711
748
  }
712
749
  }
713
750
 
714
- if (!getFlag('--vault-dir') && !isNonInteractive) {
751
+ if (!getFlag('--vault-dir') && !isNonInteractive && !useRecommendedDefaults) {
715
752
  const existingVaults = scanForVaults();
716
753
  if (existingVaults.length === 1) {
717
754
  console.log(
@@ -743,9 +780,16 @@ async function runSetup() {
743
780
  }
744
781
  console.log();
745
782
  }
783
+ } else if (!getFlag('--vault-dir') && useRecommendedDefaults) {
784
+ // Fast path: still use detected vaults if found
785
+ const existingVaults = scanForVaults();
786
+ if (existingVaults.length >= 1) {
787
+ defaultVaultDir = existingVaults[0].path;
788
+ console.log(` ${green('+')} Using existing vault at ${defaultVaultDir}`);
789
+ }
746
790
  }
747
791
 
748
- const vaultDir = isNonInteractive
792
+ const vaultDir = (isNonInteractive || useRecommendedDefaults || isDryRun)
749
793
  ? defaultVaultDir
750
794
  : await prompt(` Vault directory:`, defaultVaultDir);
751
795
  let resolvedVaultDir = resolve(vaultDir);
@@ -757,7 +801,9 @@ async function runSetup() {
757
801
  console.error(dim(` Remove or rename the file, then run setup again.\n`));
758
802
  process.exit(1);
759
803
  }
760
- } else if (isNonInteractive) {
804
+ } else if (isDryRun) {
805
+ console.log(`\n ${yellow('[dry-run]')} Would create directory: ${resolvedVaultDir}`);
806
+ } else if (isNonInteractive || useRecommendedDefaults) {
761
807
  mkdirSync(resolvedVaultDir, { recursive: true });
762
808
  console.log(`\n ${green('+')} Created ${resolvedVaultDir}`);
763
809
  } else {
@@ -772,11 +818,19 @@ async function runSetup() {
772
818
  }
773
819
 
774
820
  // Write marker file for vault auto-detection
775
- writeMarkerFile(resolvedVaultDir);
821
+ if (isDryRun) {
822
+ console.log(` ${yellow('[dry-run]')} Would write marker file: ${join(resolvedVaultDir, MARKER_FILE)}`);
823
+ } else {
824
+ writeMarkerFile(resolvedVaultDir);
825
+ }
776
826
 
777
827
  // Ensure data dir exists for DB storage
778
828
  const dataDir = join(HOME, '.context-mcp');
779
- mkdirSync(dataDir, { recursive: true });
829
+ if (isDryRun) {
830
+ console.log(` ${yellow('[dry-run]')} Would create directory: ${dataDir}`);
831
+ } else {
832
+ mkdirSync(dataDir, { recursive: true });
833
+ }
780
834
 
781
835
  // Write config.json to data dir (persistent, survives reinstalls)
782
836
  const configPath = join(dataDir, 'config.json');
@@ -812,18 +866,21 @@ async function runSetup() {
812
866
  );
813
867
  console.log(` Setup would change vaultDir to: ${resolvedVaultDir}`);
814
868
 
815
- if (isNonInteractive) {
869
+ if (isDryRun) {
870
+ console.log(` ${yellow('[dry-run]')} Would change vaultDir from ${resolve(existingVaultDir)} to ${resolvedVaultDir}`);
871
+ resolvedVaultDir = resolve(existingVaultDir);
872
+ } else if (isNonInteractive) {
816
873
  console.log();
817
874
  console.log(red(' Refusing to overwrite vaultDir in non-interactive mode.'));
818
875
  console.log(dim(' Use --force to override, or --vault-dir to set explicitly.'));
819
876
  process.exit(1);
820
- }
821
-
822
- console.log();
823
- const overwrite = await prompt(' Overwrite? (y/N):', 'N');
824
- if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
825
- console.log(dim(` Keeping existing vaultDir: ${resolve(existingVaultDir)}`));
826
- resolvedVaultDir = resolve(existingVaultDir);
877
+ } else {
878
+ console.log();
879
+ const overwrite = await prompt(' Overwrite? (y/N):', 'N');
880
+ if (overwrite.toLowerCase() !== 'y' && overwrite.toLowerCase() !== 'yes') {
881
+ console.log(dim(` Keeping existing vaultDir: ${resolve(existingVaultDir)}`));
882
+ resolvedVaultDir = resolve(existingVaultDir);
883
+ }
827
884
  }
828
885
  }
829
886
 
@@ -833,42 +890,26 @@ async function runSetup() {
833
890
  vaultConfig.devDir = join(HOME, 'dev');
834
891
  vaultConfig.mode = 'local';
835
892
 
836
- // Telemetry opt-in
837
- console.log(`\n ${dim('[3/6]')}${bold(' Anonymous error reporting\n')}`);
838
- verbose(userLevel, 'Entirely optional — works identically either way.\n');
839
- console.log(dim(' When enabled, unhandled errors send a minimal event (type, tool name,'));
840
- console.log(dim(' version, platform) to help diagnose issues. No vault content,'));
841
- console.log(dim(' file paths, or personal data is ever sent. Off by default.'));
842
- console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
843
- console.log();
844
-
845
- let telemetryEnabled = vaultConfig.telemetry === true;
846
- if (!isNonInteractive) {
847
- const defaultChoice = telemetryEnabled ? 'Y' : 'n';
848
- const telemetryAnswer = await prompt(
849
- ` Enable anonymous error reporting? (y/N):`,
850
- defaultChoice
851
- );
852
- telemetryEnabled =
853
- telemetryAnswer.toLowerCase() === 'y' || telemetryAnswer.toLowerCase() === 'yes';
893
+ if (isDryRun) {
894
+ console.log(`\n ${yellow('[dry-run]')} Would write config: ${configPath}`);
895
+ console.log(dim(` ${JSON.stringify(vaultConfig, null, 2)}`));
896
+ } else {
897
+ assertNotTestMode(configPath);
898
+ writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
899
+ console.log(`\n ${green('+')} Wrote ${configPath}`);
854
900
  }
855
- vaultConfig.telemetry = telemetryEnabled;
856
- console.log(
857
- ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
858
- );
859
-
860
- assertNotTestMode(configPath);
861
- writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
862
- console.log(`\n ${green('+')} Wrote ${configPath}`);
863
901
 
864
902
  // Pre-download embedding model with spinner (skip with --skip-embeddings)
865
- const skipEmbeddings = flags.has('--skip-embeddings');
866
- if (skipEmbeddings) {
867
- console.log(`\n ${dim('[4/6]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
903
+ const skipEmbeddings = flags.has('--skip-embeddings') || isDryRun;
904
+ if (isDryRun) {
905
+ console.log(`\n ${dim('[3/7]')}${bold(' Embedding model')} ${yellow('(dry-run, skipped)')}`);
906
+ console.log(` ${yellow('[dry-run]')} Would download embedding model (~22MB)`);
907
+ } else if (skipEmbeddings) {
908
+ console.log(`\n ${dim('[3/7]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
868
909
  console.log(dim(' FTS-only mode — full-text search works, semantic search disabled.'));
869
910
  console.log(dim(' To enable later: context-vault setup (without --skip-embeddings)'));
870
911
  } else {
871
- console.log(`\n ${dim('[4/6]')}${bold(' Downloading embedding model...')}`);
912
+ console.log(`\n ${dim('[3/7]')}${bold(' Downloading embedding model...')}`);
872
913
  verbose(userLevel, 'Enables meaning-based search. ~22MB download, runs fully offline.');
873
914
  console.log(dim(' all-MiniLM-L6-v2 (~22MB, one-time download)'));
874
915
  console.log(dim(` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`));
@@ -944,236 +985,264 @@ async function runSetup() {
944
985
  // Clean up legacy project-root config.json if it exists
945
986
  const legacyConfigPath = join(ROOT, 'config.json');
946
987
  if (existsSync(legacyConfigPath)) {
947
- try {
948
- unlinkSync(legacyConfigPath);
949
- console.log(` ${dim('Removed legacy config at ' + legacyConfigPath)}`);
950
- } catch {}
988
+ if (isDryRun) {
989
+ console.log(` ${yellow('[dry-run]')} Would remove legacy config: ${legacyConfigPath}`);
990
+ } else {
991
+ try {
992
+ unlinkSync(legacyConfigPath);
993
+ console.log(` ${dim('Removed legacy config at ' + legacyConfigPath)}`);
994
+ } catch {}
995
+ }
951
996
  }
952
997
 
953
998
  // Configure each tool — always pass vault dir explicitly to prevent config drift
954
- console.log(`\n ${dim('[5/6]')}${bold(' Configuring tools...\n')}`);
999
+ console.log(`\n ${dim('[4/7]')}${bold(' Configuring tools...\n')}`);
955
1000
  verbose(userLevel, 'Writing config so your AI tool can find your vault.\n');
956
1001
  const results = [];
957
1002
  const customVaultDir = resolvedVaultDir;
958
1003
 
959
1004
  for (const tool of selected) {
960
- try {
961
- if (tool.configType === 'cli' && tool.id === 'codex') {
962
- await configureCodex(tool, customVaultDir);
963
- } else if (tool.configType === 'cli') {
964
- await configureClaude(tool, customVaultDir);
965
- } else {
966
- configureJsonTool(tool, customVaultDir);
967
- }
1005
+ if (isDryRun) {
1006
+ console.log(` ${yellow('[dry-run]')} Would configure: ${tool.name} (${tool.configPath || tool.id})`);
968
1007
  results.push({ tool, ok: true });
969
- console.log(` ${green('+')} ${tool.name} — configured`);
970
- } catch (e) {
971
- results.push({ tool, ok: false, error: e.message });
972
- console.log(` ${red('x')} ${tool.name} — ${e.message}`);
1008
+ } else {
1009
+ try {
1010
+ if (tool.configType === 'cli' && tool.id === 'codex') {
1011
+ await configureCodex(tool, customVaultDir);
1012
+ } else if (tool.configType === 'cli') {
1013
+ await configureClaude(tool, customVaultDir);
1014
+ } else {
1015
+ configureJsonTool(tool, customVaultDir);
1016
+ }
1017
+ results.push({ tool, ok: true });
1018
+ console.log(` ${green('+')} ${tool.name} — configured`);
1019
+ } catch (e) {
1020
+ results.push({ tool, ok: false, error: e.message });
1021
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
1022
+ }
973
1023
  }
974
1024
  }
975
1025
 
976
- // Claude Code hooks (opt-in)
1026
+ // Claude Code extras: hooks, skills, rules (bundled into one step)
1027
+ console.log(`\n ${dim('[5/7]')}${bold(' Extras...\n')}`);
977
1028
  const claudeConfigured = results.some((r) => r.ok && r.tool.id === 'claude-code');
978
1029
  const hookFlag = flags.has('--hooks');
979
- if (claudeConfigured) {
980
- // 1. Recall hook (UserPromptSubmit)
981
- let installHook = hookFlag;
982
- if (!hookFlag && !isNonInteractive) {
983
- console.log();
984
- console.log(dim(' Claude Code detected — install memory hook?'));
985
- console.log(dim(' Searches your vault on every prompt and injects relevant entries'));
986
- console.log(dim(" as additional context alongside Claude's native memory."));
987
- console.log();
988
- const answer = await prompt(' Install Claude Code memory hook? (Y/n):', 'Y');
989
- installHook = answer.toLowerCase() !== 'n';
990
- }
991
- if (installHook) {
992
- try {
993
- const installed = installClaudeHook();
994
- if (installed) {
995
- console.log(`\n ${green('+')} Memory hook installed`);
996
- }
997
- } catch (e) {
998
- console.log(`\n ${red('x')} Hook install failed: ${e.message}`);
999
- }
1030
+ const configuredTools = results.filter((r) => r.ok).map((r) => r.tool);
1031
+ const installedRulesPaths = [];
1000
1032
 
1001
- // 2. Session capture hook (SessionEnd) — only offer if recall hook was installed
1002
- if (!isNonInteractive) {
1003
- console.log();
1004
- console.log(dim(' Auto-save session summaries when Claude Code exits?'));
1005
- console.log(dim(' Captures files touched, tools used, and decisions made per session.'));
1033
+ if (claudeConfigured) {
1034
+ if (isDryRun) {
1035
+ console.log(` ${yellow('[dry-run]')} Would install Claude Code hooks (memory recall, session capture, auto-capture)`);
1036
+ console.log(` ${yellow('[dry-run]')} Would install Claude Code skills (compile-context, vault-setup)`);
1037
+ } else {
1038
+ // Bundled hooks prompt: one Y/n for all three hooks
1039
+ let installHooks = hookFlag || useRecommendedDefaults;
1040
+ if (!hookFlag && !isNonInteractive && !useRecommendedDefaults) {
1041
+ console.log(dim(' Install Claude Code hooks? (recommended)'));
1042
+ console.log(dim(' Memory recall, session capture, and auto-capture.'));
1006
1043
  console.log();
1007
- const captureAnswer = await prompt(' Install session capture hook? (Y/n):', 'Y');
1008
- if (captureAnswer.toLowerCase() !== 'n') {
1009
- try {
1010
- const captureInstalled = installSessionCaptureHook();
1011
- if (captureInstalled) {
1012
- console.log(` ${green('+')} Session capture hook installed`);
1013
- }
1014
- } catch (e) {
1015
- console.log(` ${red('x')} Session capture hook failed: ${e.message}`);
1016
- }
1044
+ const answer = await prompt(' Install Claude Code hooks? (Y/n):', 'Y');
1045
+ installHooks = answer.toLowerCase() !== 'n';
1046
+ }
1047
+ if (installHooks) {
1048
+ try {
1049
+ const hookInstalled = installClaudeHook();
1050
+ if (hookInstalled) console.log(` ${green('+')} Memory recall hook installed`);
1051
+ } catch (e) {
1052
+ console.log(` ${red('x')} Memory hook failed: ${e.message}`);
1017
1053
  }
1018
- } else if (hookFlag) {
1019
1054
  try {
1020
- installSessionCaptureHook();
1021
- } catch {}
1055
+ const captureInstalled = installSessionCaptureHook();
1056
+ if (captureInstalled) console.log(` ${green('+')} Session capture hook installed`);
1057
+ } catch (e) {
1058
+ console.log(` ${red('x')} Session capture hook failed: ${e.message}`);
1059
+ }
1060
+ try {
1061
+ const acInstalled = installPostToolCallHook();
1062
+ if (acInstalled) console.log(` ${green('+')} Auto-capture hook installed`);
1063
+ } catch (e) {
1064
+ console.log(` ${red('x')} Auto-capture hook failed: ${e.message}`);
1065
+ }
1066
+ } else {
1067
+ console.log(dim(` Hooks skipped. Install later: context-vault hooks install`));
1022
1068
  }
1023
1069
 
1024
- // 3. Auto-capture hook (PostToolCall) only offer if recall hook was installed
1025
- if (!isNonInteractive) {
1070
+ // Skills (bundled, no separate prompt unless not using fast path)
1071
+ let installSkillsFlag = useRecommendedDefaults || isNonInteractive;
1072
+ if (!isNonInteractive && !useRecommendedDefaults) {
1026
1073
  console.log();
1027
- console.log(dim(' Passively log tool calls for richer session summaries?'));
1028
- console.log(dim(' Records tool names and file paths after each tool call (lightweight).'));
1074
+ console.log(dim(' Install Claude Code skills? (recommended)'));
1075
+ console.log(dim(' compile-context, vault-setup'));
1029
1076
  console.log();
1030
- const autoCaptureAnswer = await prompt(' Install auto-capture hook? (Y/n):', 'Y');
1031
- if (autoCaptureAnswer.toLowerCase() !== 'n') {
1032
- try {
1033
- const acInstalled = installPostToolCallHook();
1034
- if (acInstalled) {
1035
- console.log(` ${green('+')} Auto-capture hook installed`);
1036
- }
1037
- } catch (e) {
1038
- console.log(` ${red('x')} Auto-capture hook failed: ${e.message}`);
1039
- }
1040
- }
1041
- } else if (hookFlag) {
1042
- try {
1043
- installPostToolCallHook();
1044
- } catch {}
1077
+ const skillAnswer = await prompt(' Install Claude Code skills? (Y/n):', 'Y');
1078
+ installSkillsFlag = skillAnswer.toLowerCase() !== 'n';
1045
1079
  }
1046
- } else if (!isNonInteractive && !hookFlag) {
1047
- console.log(dim(` Skipped — install later: context-vault hooks install`));
1048
- }
1049
- }
1050
-
1051
- // Claude Code skills (opt-in)
1052
- if (claudeConfigured && !isNonInteractive) {
1053
- console.log();
1054
- console.log(dim(' Install Claude Code skills? (recommended)'));
1055
- console.log(dim(' compile-context — compile vault entries into a project brief'));
1056
- console.log(dim(' vault-setup — agent-assisted vault customization (/vault-setup)'));
1057
- console.log();
1058
- const skillAnswer = await prompt(' Install Claude Code skills? (Y/n):', 'Y');
1059
- const installSkillsFlag = skillAnswer.toLowerCase() !== 'n';
1060
- if (installSkillsFlag) {
1061
- try {
1062
- const names = installSkills();
1063
- if (names.length > 0) {
1080
+ if (installSkillsFlag) {
1081
+ try {
1082
+ const names = installSkills();
1064
1083
  for (const name of names) {
1065
- console.log(`\n ${green('+')} ${name} skill installed`);
1084
+ console.log(` ${green('+')} ${name} skill installed`);
1066
1085
  }
1086
+ } catch (e) {
1087
+ console.log(` ${red('x')} Skills install failed: ${e.message}`);
1067
1088
  }
1068
- } catch (e) {
1069
- console.log(`\n ${red('x')} Skills install failed: ${e.message}`);
1089
+ } else {
1090
+ console.log(dim(` Skills skipped. Install later: context-vault skills install`));
1070
1091
  }
1071
- } else {
1072
- console.log(dim(` Skipped — install later: context-vault skills install`));
1073
1092
  }
1074
1093
  }
1075
1094
 
1076
- // Agent rules installation (opt-in per tool, skip if --no-rules)
1077
- const configuredTools = results.filter((r) => r.ok).map((r) => r.tool);
1078
- const installedRulesPaths = [];
1095
+ // Agent rules installation
1079
1096
  if (configuredTools.length > 0 && !flags.has('--no-rules')) {
1080
- let installRules = isNonInteractive;
1081
- if (!isNonInteractive) {
1082
- console.log();
1083
- console.log(dim(' Install agent rules? (recommended)'));
1084
- console.log(dim(' Teaches your AI agent when and how to save knowledge to the vault'));
1085
- console.log(dim(' automatically the key to building useful memory over time.'));
1086
- console.log();
1087
- const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
1088
- installRules = rulesAnswer.toLowerCase() !== 'n';
1089
- }
1090
- if (installRules) {
1091
- const rulesContent = loadAgentRules();
1092
- if (rulesContent) {
1093
- for (const tool of configuredTools) {
1094
- try {
1095
- const installed = installAgentRulesForTool(tool, rulesContent);
1096
- const rulesPath = getRulesPathForTool(tool);
1097
- if (installed) {
1098
- console.log(` ${green('+')} ${tool.name} agent rules installed`);
1099
- if (rulesPath) {
1100
- console.log(` ${dim(rulesPath)}`);
1101
- installedRulesPaths.push({ tool: tool.name, path: rulesPath });
1097
+ if (isDryRun) {
1098
+ for (const tool of configuredTools) {
1099
+ const rulesPath = getRulesPathForTool(tool);
1100
+ console.log(` ${yellow('[dry-run]')} Would install agent rules for ${tool.name}${rulesPath ? ': ' + rulesPath : ''}`);
1101
+ }
1102
+ } else {
1103
+ let installRules = isNonInteractive || useRecommendedDefaults;
1104
+ if (!isNonInteractive && !useRecommendedDefaults) {
1105
+ console.log();
1106
+ console.log(dim(' Install agent rules? (recommended)'));
1107
+ console.log(dim(' Teaches your AI agent when and how to save knowledge to the vault.'));
1108
+ console.log();
1109
+ const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
1110
+ installRules = rulesAnswer.toLowerCase() !== 'n';
1111
+ }
1112
+ if (installRules) {
1113
+ const rulesContent = loadAgentRules();
1114
+ if (rulesContent) {
1115
+ for (const tool of configuredTools) {
1116
+ try {
1117
+ const installed = installAgentRulesForTool(tool, rulesContent);
1118
+ const rulesPath = getRulesPathForTool(tool);
1119
+ if (installed) {
1120
+ console.log(` ${green('+')} ${tool.name} agent rules installed`);
1121
+ if (rulesPath) {
1122
+ console.log(` ${dim(rulesPath)}`);
1123
+ installedRulesPaths.push({ tool: tool.name, path: rulesPath });
1124
+ }
1102
1125
  }
1126
+ } catch (e) {
1127
+ console.log(` ${red('x')} ${tool.name} rules: ${e.message}`);
1103
1128
  }
1104
- } catch (e) {
1105
- console.log(` ${red('x')} ${tool.name} — ${e.message}`);
1106
1129
  }
1130
+ } else {
1131
+ console.log(dim(' Agent rules file not found in package.'));
1107
1132
  }
1108
1133
  } else {
1109
- console.log(dim(' Agent rules file not found in package — skipping.'));
1134
+ console.log(dim(' Rules skipped. Install later: context-vault rules install'));
1110
1135
  }
1111
- } else {
1112
- console.log(dim(' Skipped — install later: context-vault rules install'));
1113
1136
  }
1114
1137
  } else if (flags.has('--no-rules')) {
1115
1138
  console.log(dim(' Agent rules skipped (--no-rules)'));
1116
1139
  }
1117
1140
 
1118
1141
  // Seed entry
1119
- const seeded = createSeedEntries(resolvedVaultDir);
1120
- if (seeded > 0) {
1142
+ if (isDryRun) {
1143
+ console.log(`\n ${yellow('[dry-run]')} Would create seed entries in ${resolvedVaultDir}`);
1144
+ } else {
1145
+ const seeded = createSeedEntries(resolvedVaultDir);
1146
+ if (seeded > 0) {
1147
+ console.log(
1148
+ `\n ${green('+')} Created ${seeded} starter ${seeded === 1 ? 'entry' : 'entries'} in vault`
1149
+ );
1150
+ }
1151
+ }
1152
+
1153
+ // Telemetry opt-in (moved to end, after user has seen value)
1154
+ console.log(`\n ${dim('[6/7]')}${bold(' Anonymous error reporting\n')}`);
1155
+ if (isDryRun) {
1156
+ console.log(` ${yellow('[dry-run]')} Would prompt for telemetry preference`);
1157
+ console.log(` ${yellow('[dry-run]')} Would update config: ${configPath}`);
1158
+ } else {
1159
+ verbose(userLevel, 'Entirely optional. Works identically either way.\n');
1160
+ console.log(dim(' When enabled, unhandled errors send a minimal event (type, tool name,'));
1161
+ console.log(dim(' version, platform) to help diagnose issues. No vault content,'));
1162
+ console.log(dim(' file paths, or personal data is ever sent. Off by default.'));
1163
+ console.log(dim(` Full schema: ${MARKETING_URL}/telemetry`));
1164
+ console.log();
1165
+
1166
+ let telemetryEnabled = vaultConfig.telemetry === true;
1167
+ if (!isNonInteractive && !useRecommendedDefaults) {
1168
+ const defaultChoice = telemetryEnabled ? 'Y' : 'n';
1169
+ const telemetryAnswer = await prompt(
1170
+ ` Enable anonymous error reporting? (y/N):`,
1171
+ defaultChoice
1172
+ );
1173
+ telemetryEnabled =
1174
+ telemetryAnswer.toLowerCase() === 'y' || telemetryAnswer.toLowerCase() === 'yes';
1175
+ }
1176
+ vaultConfig.telemetry = telemetryEnabled;
1121
1177
  console.log(
1122
- `\n ${green('+')} Created ${seeded} starter ${seeded === 1 ? 'entry' : 'entries'} in vault`
1178
+ ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
1123
1179
  );
1180
+
1181
+ // Re-write config with telemetry setting
1182
+ assertNotTestMode(configPath);
1183
+ writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1124
1184
  }
1125
1185
 
1126
1186
  // Health check
1127
- console.log(`\n ${dim('[6/6]')}${bold(' Health check...')}\n`);
1128
- verbose(userLevel, 'Verifying vault, config, and database are accessible.\n');
1187
+ console.log(`\n ${dim('[7/7]')}${bold(' Health check...')}\n`);
1129
1188
  const okResults = results.filter((r) => r.ok);
1189
+ let passed = 0;
1190
+ let checksTotal = 0;
1130
1191
 
1131
- // Verify DB is accessible
1132
- let dbAccessible = false;
1133
- let dbError = null;
1134
- try {
1135
- const { initDatabase } = await import('@context-vault/core/db');
1136
- const db = await initDatabase(vaultConfig.dbPath);
1137
- db.prepare('SELECT 1').get();
1138
- db.close();
1139
- dbAccessible = true;
1140
- } catch (e) {
1141
- dbError = e;
1142
- }
1192
+ if (isDryRun) {
1193
+ console.log(` ${yellow('[dry-run]')} Skipping health check (no files were written)`);
1194
+ console.log(` ${yellow('[dry-run]')} Skipping smoke test`);
1195
+ } else {
1196
+ verbose(userLevel, 'Verifying vault, config, and database are accessible.\n');
1143
1197
 
1144
- const checks = [
1145
- { label: 'Vault directory exists', pass: existsSync(resolvedVaultDir) },
1146
- { label: 'Config file written', pass: existsSync(configPath) },
1147
- { label: 'Database accessible', pass: dbAccessible, error: dbError },
1148
- { label: 'At least one tool configured', pass: okResults.length > 0 },
1149
- ];
1150
- const passed = checks.filter((c) => c.pass).length;
1151
- for (const c of checks) {
1152
- console.log(` ${c.pass ? green('✓') : red('✗')} ${c.label}`);
1153
- if (!c.pass && c.error) {
1154
- console.log(` ${dim(c.error.message)}`);
1155
- if (c.error.message.includes('EACCES') || c.error.message.includes('permission')) {
1156
- console.log(` ${dim('Fix: check file permissions on ' + vaultConfig.dbPath)}`);
1198
+ // Verify DB is accessible
1199
+ let dbAccessible = false;
1200
+ let dbError = null;
1201
+ try {
1202
+ const { initDatabase } = await import('@context-vault/core/db');
1203
+ const db = await initDatabase(vaultConfig.dbPath);
1204
+ db.prepare('SELECT 1').get();
1205
+ db.close();
1206
+ dbAccessible = true;
1207
+ } catch (e) {
1208
+ dbError = e;
1209
+ }
1210
+
1211
+ const checks = [
1212
+ { label: 'Vault directory exists', pass: existsSync(resolvedVaultDir) },
1213
+ { label: 'Config file written', pass: existsSync(configPath) },
1214
+ { label: 'Database accessible', pass: dbAccessible, error: dbError },
1215
+ { label: 'At least one tool configured', pass: okResults.length > 0 },
1216
+ ];
1217
+ passed = checks.filter((c) => c.pass).length;
1218
+ checksTotal = checks.length;
1219
+ for (const c of checks) {
1220
+ console.log(` ${c.pass ? green('✓') : red('✗')} ${c.label}`);
1221
+ if (!c.pass && c.error) {
1222
+ console.log(` ${dim(c.error.message)}`);
1223
+ if (c.error.message.includes('EACCES') || c.error.message.includes('permission')) {
1224
+ console.log(` ${dim('Fix: check file permissions on ' + vaultConfig.dbPath)}`);
1225
+ }
1157
1226
  }
1158
1227
  }
1159
- }
1160
1228
 
1161
- // Smoke test — write and read a test file to verify vault I/O
1162
- {
1163
- const testFile = join(resolvedVaultDir, '.smoke-test-' + Date.now() + '.md');
1164
- try {
1165
- writeFileSync(testFile, '# Smoke test\n');
1166
- const content = readFileSync(testFile, 'utf-8');
1167
- unlinkSync(testFile);
1168
- if (content.includes('Smoke test')) {
1169
- console.log(` ${green('✓')} Smoke test: vault read/write verified`);
1170
- } else {
1171
- console.log(` ${red('✗')} Smoke test: file written but content mismatch`);
1229
+ // Smoke test — write and read a test file to verify vault I/O
1230
+ {
1231
+ const testFile = join(resolvedVaultDir, '.smoke-test-' + Date.now() + '.md');
1232
+ try {
1233
+ writeFileSync(testFile, '# Smoke test\n');
1234
+ const content = readFileSync(testFile, 'utf-8');
1235
+ unlinkSync(testFile);
1236
+ if (content.includes('Smoke test')) {
1237
+ console.log(` ${green('✓')} Smoke test: vault read/write verified`);
1238
+ } else {
1239
+ console.log(` ${red('✗')} Smoke test: file written but content mismatch`);
1240
+ }
1241
+ } catch (e) {
1242
+ try { unlinkSync(testFile); } catch {}
1243
+ console.log(` ${red('✗')} Smoke test failed: ${e.message}`);
1244
+ console.log(` ${dim('Check permissions on ' + resolvedVaultDir)}`);
1172
1245
  }
1173
- } catch (e) {
1174
- try { unlinkSync(testFile); } catch {}
1175
- console.log(` ${red('✗')} Smoke test failed: ${e.message}`);
1176
- console.log(` ${dim('Check permissions on ' + resolvedVaultDir)}`);
1177
1246
  }
1178
1247
  }
1179
1248
 
@@ -1183,9 +1252,26 @@ async function runSetup() {
1183
1252
  const cli = isNpx() ? 'npx context-vault' : 'context-vault';
1184
1253
 
1185
1254
  let boxLines;
1255
+ if (isDryRun) {
1256
+ boxLines = [
1257
+ ` ${yellow('Dry run complete')} (${elapsed}s)`,
1258
+ ``,
1259
+ ` No files were written. Run without --dry-run to apply.`,
1260
+ ];
1261
+ const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
1262
+ const pad = (s) => s + ' '.repeat(Math.max(0, innerWidth - s.length));
1263
+ console.log();
1264
+ console.log(` ${dim('┌' + '─'.repeat(innerWidth) + '┐')}`);
1265
+ for (const line of boxLines) {
1266
+ console.log(` ${dim('│')}${pad(line)}${dim('│')}`);
1267
+ }
1268
+ console.log(` ${dim('└' + '─'.repeat(innerWidth) + '┘')}`);
1269
+ console.log();
1270
+ return;
1271
+ }
1186
1272
  if (userLevel === 'beginner') {
1187
1273
  boxLines = [
1188
- ` ✓ Setup complete — ${passed}/${checks.length} checks passed (${elapsed}s)`,
1274
+ ` ✓ Setup complete — ${passed}/${checksTotal} checks passed (${elapsed}s)`,
1189
1275
  ``,
1190
1276
  ` ${bold('What to do next:')}`,
1191
1277
  ``,
@@ -1203,7 +1289,7 @@ async function runSetup() {
1203
1289
  ];
1204
1290
  } else {
1205
1291
  boxLines = [
1206
- ` ✓ Setup complete — ${passed}/${checks.length} checks passed (${elapsed}s)`,
1292
+ ` ✓ Setup complete — ${passed}/${checksTotal} checks passed (${elapsed}s)`,
1207
1293
  ``,
1208
1294
  ` ${bold('Next:')} restart ${toolName} to activate the vault`,
1209
1295
  ``,
@@ -1894,7 +1980,11 @@ async function runSwitch() {
1894
1980
  }
1895
1981
 
1896
1982
  async function runReindex() {
1897
- console.log(dim('Loading vault...'));
1983
+ const dryRun = flags.has('--dry-run');
1984
+ const kindIdx = args.indexOf('--kind');
1985
+ const kindFilter = kindIdx !== -1 && args[kindIdx + 1] ? args[kindIdx + 1] : null;
1986
+
1987
+ console.log(dim(dryRun ? 'Analyzing vault (dry run)...' : 'Loading vault...'));
1898
1988
 
1899
1989
  const { resolveConfig } = await import('@context-vault/core/config');
1900
1990
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
@@ -1920,14 +2010,34 @@ async function runReindex() {
1920
2010
  deleteVec: (r) => deleteVec(stmts, r),
1921
2011
  };
1922
2012
 
1923
- const stats = await reindex(ctx, { fullSync: true });
2013
+ const reindexOpts = {
2014
+ fullSync: true,
2015
+ indexingConfig: config.indexing,
2016
+ dryRun,
2017
+ kindFilter,
2018
+ };
2019
+
2020
+ const stats = await reindex(ctx, reindexOpts);
1924
2021
 
1925
2022
  db.close();
1926
- console.log(green('✓ Reindex complete'));
1927
- console.log(` ${green('+')} ${stats.added} added`);
1928
- console.log(` ${yellow('~')} ${stats.updated} updated`);
1929
- console.log(` ${red('-')} ${stats.removed} removed`);
1930
- console.log(` ${dim('·')} ${stats.unchanged} unchanged`);
2023
+
2024
+ if (dryRun) {
2025
+ console.log(yellow('Dry run results (no changes made):'));
2026
+ console.log(` Would index: ${stats.added}`);
2027
+ console.log(` Would skip: ${stats.skippedIndexing ?? 0}`);
2028
+ } else {
2029
+ console.log(green('✓ Reindex complete'));
2030
+ console.log(` ${green('+')} ${stats.added} added`);
2031
+ console.log(` ${yellow('~')} ${stats.updated} updated`);
2032
+ console.log(` ${red('-')} ${stats.removed} removed`);
2033
+ console.log(` ${dim('·')} ${stats.unchanged} unchanged`);
2034
+ if (stats.skippedIndexing) {
2035
+ console.log(` ${dim('○')} ${stats.skippedIndexing} skipped indexing`);
2036
+ }
2037
+ if (stats.embeddingsCleared) {
2038
+ console.log(` ${dim('⊘')} ${stats.embeddingsCleared} embeddings cleared`);
2039
+ }
2040
+ }
1931
2041
  }
1932
2042
 
1933
2043
  async function runMigrateDirs() {
@@ -2278,7 +2388,8 @@ async function runStatus() {
2278
2388
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
2279
2389
  const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
2280
2390
  const countStr = String(c).padStart(4);
2281
- const plural = kind.endsWith('s') ? kind : kind + 's';
2391
+ const IRREGULAR_PLURALS = { activity: 'activities', inbox: 'inboxes', index: 'indexes', match: 'matches' };
2392
+ const plural = IRREGULAR_PLURALS[kind] || (kind.endsWith('s') ? kind : kind + 's');
2282
2393
  console.log(` ${dim(bar)} ${countStr} ${plural}`);
2283
2394
  }
2284
2395
  } else {
@@ -3796,6 +3907,78 @@ async function runSessionEnd() {
3796
3907
  meta: { session_id: session_id ?? null, cwd, message_count },
3797
3908
  });
3798
3909
  console.log(`context-vault session captured — id: ${entry.id}`);
3910
+
3911
+ // ── Auto-insight extraction ──────────────────────────────────────────────
3912
+ const aiConfig = config.autoInsights ?? { enabled: true, patterns: ['★ Insight'], minChars: 50, maxPerSession: 5, tier: 'working' };
3913
+ if (aiConfig.enabled !== false) {
3914
+ try {
3915
+ const patterns = aiConfig.patterns ?? ['★ Insight'];
3916
+ const minChars = aiConfig.minChars ?? 50;
3917
+ const maxInsights = aiConfig.maxPerSession ?? 5;
3918
+ const defaultTier = aiConfig.tier ?? 'working';
3919
+
3920
+ // Build regex for all configured patterns
3921
+ const escapedPatterns = patterns.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
3922
+ const patternRe = new RegExp(
3923
+ `(?:${escapedPatterns.join('|')})[─\\s]*\`?\\n([\\s\\S]*?)\\n\`?─{10,}`,
3924
+ 'g'
3925
+ );
3926
+
3927
+ const insightBlocks = [];
3928
+ for (const turn of turns) {
3929
+ if (turn.role !== 'assistant') continue;
3930
+ const text = extractText(turn);
3931
+ if (!text) continue;
3932
+ for (const m of text.matchAll(patternRe)) {
3933
+ const insightBody = m[1].trim();
3934
+ if (insightBody.length >= minChars && insightBlocks.length < maxInsights) {
3935
+ insightBlocks.push(insightBody);
3936
+ }
3937
+ }
3938
+ }
3939
+
3940
+ if (insightBlocks.length > 0) {
3941
+ // Check existing auto-insight entries for dedup (by title, lightweight)
3942
+ const existingTitles = new Set();
3943
+ try {
3944
+ const rows = db.prepare(
3945
+ `SELECT title FROM entries WHERE tags LIKE '%auto-insight%' ORDER BY created_at DESC LIMIT 100`
3946
+ ).all();
3947
+ for (const r of rows) {
3948
+ if (r.title) existingTitles.add(r.title.toLowerCase());
3949
+ }
3950
+ } catch {}
3951
+
3952
+ let savedCount = 0;
3953
+ for (const insightBody of insightBlocks) {
3954
+ const boldMatch = insightBody.match(/\*\*(.+?)\*\*/);
3955
+ const firstLine = insightBody.split('\n')[0].replace(/\*\*/g, '').trim();
3956
+ const insightTitle = boldMatch ? boldMatch[1].slice(0, 80) : firstLine.slice(0, 80);
3957
+
3958
+ // Skip near-duplicates by title
3959
+ if (existingTitles.has(insightTitle.toLowerCase())) continue;
3960
+
3961
+ const insightTags = ['auto-insight', 'session-insight', `bucket:${project}`];
3962
+ await captureAndIndex(ctx, {
3963
+ kind: 'insight',
3964
+ title: insightTitle,
3965
+ body: insightBody,
3966
+ tags: insightTags,
3967
+ source: `claude-code session ${new Date().toISOString().slice(0, 10)}`,
3968
+ tier: defaultTier,
3969
+ meta: { auto_extracted: true, session_id: session_id ?? null },
3970
+ });
3971
+ existingTitles.add(insightTitle.toLowerCase());
3972
+ savedCount++;
3973
+ }
3974
+ if (savedCount > 0) {
3975
+ console.log(`context-vault auto-insights — ${savedCount} insight${savedCount === 1 ? '' : 's'} saved`);
3976
+ }
3977
+ }
3978
+ } catch {
3979
+ // Auto-insight extraction is best-effort
3980
+ }
3981
+ }
3799
3982
  } catch {
3800
3983
  // fail silently — never block session end
3801
3984
  } finally {
@@ -4102,48 +4285,45 @@ function loadAgentRules() {
4102
4285
  * Returns null for tools with no rules install path.
4103
4286
  */
4104
4287
  function getRulesPathForTool(tool) {
4105
- if (tool.id === 'claude-code') return join(HOME, '.claude', 'rules', 'context-vault.md');
4106
- if (tool.id === 'cursor') return join(HOME, '.cursor', 'rules', 'context-vault.mdc');
4107
- if (tool.id === 'windsurf') return join(HOME, '.windsurfrules');
4108
- return null;
4288
+ return tool.rulesPath || null;
4109
4289
  }
4110
4290
 
4111
4291
  /**
4112
4292
  * Install agent rules for a specific tool.
4113
- * - Claude Code: writes ~/.claude/rules/context-vault.md
4114
- * - Cursor: appends to .cursorrules in cwd (with delimiters)
4115
- * - Windsurf: appends to .windsurfrules in cwd (with delimiters)
4116
- * - Other tools: skipped
4117
- * Returns true if installed, false if skipped or already present.
4293
+ * Uses tool.rulesPath and tool.rulesMethod from the TOOLS array.
4294
+ * - 'write' method: writes the file directly (Claude Code, Cursor)
4295
+ * - 'append' method: appends with delimiter markers (Windsurf)
4296
+ * Returns true if installed/updated, false if already up to date or skipped.
4118
4297
  */
4119
4298
  function installAgentRulesForTool(tool, rulesContent) {
4120
- if (tool.id === 'claude-code') {
4121
- const rulesDir = join(HOME, '.claude', 'rules');
4122
- const rulesPath = join(rulesDir, 'context-vault.md');
4299
+ const rulesPath = tool.rulesPath;
4300
+ if (!rulesPath) return false;
4301
+
4302
+ if (tool.rulesMethod === 'write') {
4123
4303
  if (existsSync(rulesPath)) {
4124
4304
  const existing = readFileSync(rulesPath, 'utf-8');
4125
4305
  if (existing.trim() === rulesContent.trim()) return false;
4126
4306
  }
4127
- mkdirSync(rulesDir, { recursive: true });
4128
- writeFileSync(rulesPath, rulesContent);
4129
- return true;
4130
- }
4131
-
4132
- if (tool.id === 'cursor') {
4133
- const rulesPath = join(HOME, '.cursor', 'rules', 'context-vault.mdc');
4134
- // Cursor supports project rules in .cursor/rules/ directory
4135
- if (existsSync(rulesPath)) return false;
4136
- mkdirSync(join(HOME, '.cursor', 'rules'), { recursive: true });
4307
+ mkdirSync(dirname(rulesPath), { recursive: true });
4137
4308
  writeFileSync(rulesPath, rulesContent);
4138
4309
  return true;
4139
4310
  }
4140
4311
 
4141
- if (tool.id === 'windsurf') {
4142
- const rulesPath = join(HOME, '.windsurfrules');
4312
+ if (tool.rulesMethod === 'append') {
4143
4313
  const delimited = `\n${RULES_DELIMITER_START}\n${rulesContent}\n${RULES_DELIMITER_END}\n`;
4144
4314
  if (existsSync(rulesPath)) {
4145
4315
  const existing = readFileSync(rulesPath, 'utf-8');
4146
- if (existing.includes(RULES_DELIMITER_START)) return false;
4316
+ if (existing.includes(RULES_DELIMITER_START)) {
4317
+ const delimiterRegex = new RegExp(
4318
+ `\n?${RULES_DELIMITER_START}[\\s\\S]*?${RULES_DELIMITER_END}\n?`,
4319
+ 'g'
4320
+ );
4321
+ const existingSection = existing.match(delimiterRegex)?.[0] || '';
4322
+ if (existingSection.includes(rulesContent.trim())) return false;
4323
+ const cleaned = existing.replace(delimiterRegex, '');
4324
+ writeFileSync(rulesPath, cleaned + delimited);
4325
+ return true;
4326
+ }
4147
4327
  writeFileSync(rulesPath, existing + delimited);
4148
4328
  } else {
4149
4329
  writeFileSync(rulesPath, delimited.trimStart());
@@ -4151,7 +4331,6 @@ function installAgentRulesForTool(tool, rulesContent) {
4151
4331
  return true;
4152
4332
  }
4153
4333
 
4154
- // Other tools: no rules installation path yet
4155
4334
  return false;
4156
4335
  }
4157
4336
 
@@ -4567,19 +4746,28 @@ async function runRules() {
4567
4746
  console.log();
4568
4747
  } else if (sub === 'show') {
4569
4748
  const { detected } = await detectAllTools();
4570
- const tool = detected.find((t) => getRulesPathForTool(t));
4571
- if (!tool) {
4749
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4750
+ if (toolsWithRules.length === 0) {
4572
4751
  console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4573
4752
  process.exit(1);
4574
4753
  }
4575
- const rulesPath = getRulesPathForTool(tool);
4576
- if (!rulesPath || !existsSync(rulesPath)) {
4577
- console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4578
- console.log(dim(` Run: context-vault rules install\n`));
4754
+ let anyShown = false;
4755
+ for (const tool of toolsWithRules) {
4756
+ const rulesPath = getRulesPathForTool(tool);
4757
+ if (!rulesPath || !existsSync(rulesPath)) {
4758
+ console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4759
+ console.log(dim(` Run: context-vault rules install`));
4760
+ continue;
4761
+ }
4762
+ if (anyShown) console.log(dim(' ' + '─'.repeat(40)));
4763
+ console.log(`\n ${dim(`${tool.name}: ${rulesPath}`)}\n`);
4764
+ console.log(readFileSync(rulesPath, 'utf-8'));
4765
+ anyShown = true;
4766
+ }
4767
+ if (!anyShown) {
4768
+ console.log(dim(`\n Run: context-vault rules install\n`));
4579
4769
  process.exit(1);
4580
4770
  }
4581
- console.log(`\n ${dim(`${tool.name}: ${rulesPath}`)}\n`);
4582
- console.log(readFileSync(rulesPath, 'utf-8'));
4583
4771
  } else if (sub === 'path') {
4584
4772
  const { detected } = await detectAllTools();
4585
4773
  const supportedTools = detected.filter((t) => getRulesPathForTool(t));
@@ -4601,42 +4789,44 @@ async function runRules() {
4601
4789
  process.exit(1);
4602
4790
  }
4603
4791
  const { detected } = await detectAllTools();
4604
- const tool = detected.find((t) => getRulesPathForTool(t));
4605
- if (!tool) {
4792
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4793
+ if (toolsWithRules.length === 0) {
4606
4794
  console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4607
4795
  process.exit(1);
4608
4796
  }
4609
- const rulesPath = getRulesPathForTool(tool);
4610
- if (!rulesPath || !existsSync(rulesPath)) {
4611
- console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4612
- console.log(dim(` Run: context-vault rules install\n`));
4613
- process.exit(1);
4614
- }
4615
- const installed = readFileSync(rulesPath, 'utf-8');
4616
- if (installed.trim() === bundled.trim()) {
4617
- console.log(`\n ${green('✓')} Rules are up to date (${rulesPath})\n`);
4618
- } else {
4619
- console.log(`\n ${yellow('!')} Installed rules differ from bundled version.`);
4620
- console.log(` ${dim(rulesPath)}\n`);
4621
- const installedLines = installed.split('\n');
4622
- const bundledLines = bundled.split('\n');
4623
- const maxLines = Math.max(installedLines.length, bundledLines.length);
4624
- for (let i = 0; i < maxLines; i++) {
4625
- const a = installedLines[i];
4626
- const b = bundledLines[i];
4627
- if (a === undefined) {
4628
- console.log(` ${green('+')} ${b}`);
4629
- } else if (b === undefined) {
4630
- console.log(` ${red('-')} ${a}`);
4631
- } else if (a !== b) {
4632
- console.log(` ${red('-')} ${a}`);
4633
- console.log(` ${green('+')} ${b}`);
4797
+ for (const tool of toolsWithRules) {
4798
+ const rulesPath = getRulesPathForTool(tool);
4799
+ if (!rulesPath || !existsSync(rulesPath)) {
4800
+ console.log(`\n ${yellow('!')} No rules file installed for ${tool.name}.`);
4801
+ console.log(dim(` Run: context-vault rules install`));
4802
+ continue;
4803
+ }
4804
+ const installed = readFileSync(rulesPath, 'utf-8');
4805
+ if (installed.trim() === bundled.trim()) {
4806
+ console.log(`\n ${green('✓')} ${tool.name}: rules are up to date (${rulesPath})`);
4807
+ } else {
4808
+ console.log(`\n ${yellow('!')} ${tool.name}: installed rules differ from bundled version.`);
4809
+ console.log(` ${dim(rulesPath)}\n`);
4810
+ const installedLines = installed.split('\n');
4811
+ const bundledLines = bundled.split('\n');
4812
+ const maxLines = Math.max(installedLines.length, bundledLines.length);
4813
+ for (let i = 0; i < maxLines; i++) {
4814
+ const a = installedLines[i];
4815
+ const b = bundledLines[i];
4816
+ if (a === undefined) {
4817
+ console.log(` ${green('+')} ${b}`);
4818
+ } else if (b === undefined) {
4819
+ console.log(` ${red('-')} ${a}`);
4820
+ } else if (a !== b) {
4821
+ console.log(` ${red('-')} ${a}`);
4822
+ console.log(` ${green('+')} ${b}`);
4823
+ }
4634
4824
  }
4825
+ console.log();
4826
+ console.log(dim(' To upgrade: context-vault rules install'));
4635
4827
  }
4636
- console.log();
4637
- console.log(dim(' To upgrade: context-vault rules install'));
4638
- console.log();
4639
4828
  }
4829
+ console.log();
4640
4830
  } else {
4641
4831
  console.log(`
4642
4832
  ${bold('context-vault rules')} <command>