context-vault 3.4.5 → 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 (73) hide show
  1. package/bin/cli.js +299 -194
  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
@@ -252,12 +252,16 @@ const TOOLS = [
252
252
  name: 'Claude Code',
253
253
  detect: () => commandExistsAsync('claude'),
254
254
  configType: 'cli',
255
+ rulesPath: join(HOME, '.claude', 'rules', 'context-vault.md'),
256
+ rulesMethod: 'write',
255
257
  },
256
258
  {
257
259
  id: 'codex',
258
260
  name: 'Codex',
259
261
  detect: () => commandExistsAsync('codex'),
260
262
  configType: 'cli',
263
+ rulesPath: null,
264
+ rulesMethod: null,
261
265
  },
262
266
  {
263
267
  id: 'claude-desktop',
@@ -266,6 +270,8 @@ const TOOLS = [
266
270
  configType: 'json',
267
271
  configPath: join(appDataDir(), 'Claude', 'claude_desktop_config.json'),
268
272
  configKey: 'mcpServers',
273
+ rulesPath: null,
274
+ rulesMethod: null,
269
275
  },
270
276
  {
271
277
  id: 'cursor',
@@ -274,6 +280,8 @@ const TOOLS = [
274
280
  configType: 'json',
275
281
  configPath: join(HOME, '.cursor', 'mcp.json'),
276
282
  configKey: 'mcpServers',
283
+ rulesPath: join(HOME, '.cursor', 'rules', 'context-vault.mdc'),
284
+ rulesMethod: 'write',
277
285
  },
278
286
  {
279
287
  id: 'windsurf',
@@ -286,6 +294,8 @@ const TOOLS = [
286
294
  : join(HOME, '.codeium', 'windsurf', 'mcp_config.json');
287
295
  },
288
296
  configKey: 'mcpServers',
297
+ rulesPath: join(HOME, '.windsurfrules'),
298
+ rulesMethod: 'append',
289
299
  },
290
300
  {
291
301
  id: 'antigravity',
@@ -294,6 +304,8 @@ const TOOLS = [
294
304
  configType: 'json',
295
305
  configPath: join(HOME, '.gemini', 'antigravity', 'mcp_config.json'),
296
306
  configKey: 'mcpServers',
307
+ rulesPath: null,
308
+ rulesMethod: null,
297
309
  },
298
310
  {
299
311
  id: 'cline',
@@ -307,6 +319,8 @@ const TOOLS = [
307
319
  'cline_mcp_settings.json'
308
320
  ),
309
321
  configKey: 'mcpServers',
322
+ rulesPath: null,
323
+ rulesMethod: null,
310
324
  },
311
325
  {
312
326
  id: 'roo-code',
@@ -320,6 +334,8 @@ const TOOLS = [
320
334
  'cline_mcp_settings.json'
321
335
  ),
322
336
  configKey: 'mcpServers',
337
+ rulesPath: null,
338
+ rulesMethod: null,
323
339
  },
324
340
  ];
325
341
 
@@ -619,7 +635,7 @@ async function runSetup() {
619
635
  }
620
636
 
621
637
  // Detect tools
622
- console.log(dim(` [1/6]`) + bold(' Detecting tools...\n'));
638
+ console.log(dim(` [1/7]`) + bold(' Detecting tools...\n'));
623
639
  verbose(userLevel, 'Scanning for AI tools on this machine.');
624
640
  if (userLevel === 'beginner') console.log();
625
641
  const { detected, results: detectionResults } = await detectAllTools();
@@ -690,8 +706,21 @@ async function runSetup() {
690
706
  }
691
707
  }
692
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
+
693
722
  // Vault directory (content files)
694
- console.log(dim(` [2/6]`) + bold(' Configuring vault...\n'));
723
+ console.log(dim(` [2/7]`) + bold(' Configuring vault...\n'));
695
724
  verbose(userLevel, 'Your vault is a folder of plain markdown files — you own it.');
696
725
  if (userLevel === 'beginner') console.log();
697
726
 
@@ -711,7 +740,7 @@ async function runSetup() {
711
740
  }
712
741
  }
713
742
 
714
- if (!getFlag('--vault-dir') && !isNonInteractive) {
743
+ if (!getFlag('--vault-dir') && !isNonInteractive && !useRecommendedDefaults) {
715
744
  const existingVaults = scanForVaults();
716
745
  if (existingVaults.length === 1) {
717
746
  console.log(
@@ -743,9 +772,16 @@ async function runSetup() {
743
772
  }
744
773
  console.log();
745
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
+ }
746
782
  }
747
783
 
748
- const vaultDir = isNonInteractive
784
+ const vaultDir = (isNonInteractive || useRecommendedDefaults)
749
785
  ? defaultVaultDir
750
786
  : await prompt(` Vault directory:`, defaultVaultDir);
751
787
  let resolvedVaultDir = resolve(vaultDir);
@@ -757,7 +793,7 @@ async function runSetup() {
757
793
  console.error(dim(` Remove or rename the file, then run setup again.\n`));
758
794
  process.exit(1);
759
795
  }
760
- } else if (isNonInteractive) {
796
+ } else if (isNonInteractive || useRecommendedDefaults) {
761
797
  mkdirSync(resolvedVaultDir, { recursive: true });
762
798
  console.log(`\n ${green('+')} Created ${resolvedVaultDir}`);
763
799
  } else {
@@ -833,30 +869,6 @@ async function runSetup() {
833
869
  vaultConfig.devDir = join(HOME, 'dev');
834
870
  vaultConfig.mode = 'local';
835
871
 
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';
854
- }
855
- vaultConfig.telemetry = telemetryEnabled;
856
- console.log(
857
- ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
858
- );
859
-
860
872
  assertNotTestMode(configPath);
861
873
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
862
874
  console.log(`\n ${green('+')} Wrote ${configPath}`);
@@ -864,11 +876,11 @@ async function runSetup() {
864
876
  // Pre-download embedding model with spinner (skip with --skip-embeddings)
865
877
  const skipEmbeddings = flags.has('--skip-embeddings');
866
878
  if (skipEmbeddings) {
867
- console.log(`\n ${dim('[4/6]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
879
+ console.log(`\n ${dim('[3/7]')}${bold(' Embedding model')} ${dim('(skipped)')}`);
868
880
  console.log(dim(' FTS-only mode — full-text search works, semantic search disabled.'));
869
881
  console.log(dim(' To enable later: context-vault setup (without --skip-embeddings)'));
870
882
  } else {
871
- console.log(`\n ${dim('[4/6]')}${bold(' Downloading embedding model...')}`);
883
+ console.log(`\n ${dim('[3/7]')}${bold(' Downloading embedding model...')}`);
872
884
  verbose(userLevel, 'Enables meaning-based search. ~22MB download, runs fully offline.');
873
885
  console.log(dim(' all-MiniLM-L6-v2 (~22MB, one-time download)'));
874
886
  console.log(dim(` Slow connection? Re-run with --skip-embeddings (enables FTS-only mode)\n`));
@@ -951,7 +963,7 @@ async function runSetup() {
951
963
  }
952
964
 
953
965
  // Configure each tool — always pass vault dir explicitly to prevent config drift
954
- console.log(`\n ${dim('[5/6]')}${bold(' Configuring tools...\n')}`);
966
+ console.log(`\n ${dim('[4/7]')}${bold(' Configuring tools...\n')}`);
955
967
  verbose(userLevel, 'Writing config so your AI tool can find your vault.\n');
956
968
  const results = [];
957
969
  const customVaultDir = resolvedVaultDir;
@@ -973,116 +985,77 @@ async function runSetup() {
973
985
  }
974
986
  }
975
987
 
976
- // 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')}`);
977
990
  const claudeConfigured = results.some((r) => r.ok && r.tool.id === 'claude-code');
978
991
  const hookFlag = flags.has('--hooks');
992
+ const configuredTools = results.filter((r) => r.ok).map((r) => r.tool);
993
+ const installedRulesPaths = [];
994
+
979
995
  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."));
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.'));
987
1001
  console.log();
988
- const answer = await prompt(' Install Claude Code memory hook? (Y/n):', 'Y');
989
- installHook = answer.toLowerCase() !== 'n';
1002
+ const answer = await prompt(' Install Claude Code hooks? (Y/n):', 'Y');
1003
+ installHooks = answer.toLowerCase() !== 'n';
990
1004
  }
991
- if (installHook) {
1005
+ if (installHooks) {
992
1006
  try {
993
- const installed = installClaudeHook();
994
- if (installed) {
995
- console.log(`\n ${green('+')} Memory hook installed`);
996
- }
1007
+ const hookInstalled = installClaudeHook();
1008
+ if (hookInstalled) console.log(` ${green('+')} Memory recall hook installed`);
997
1009
  } catch (e) {
998
- console.log(`\n ${red('x')} Hook install failed: ${e.message}`);
1010
+ console.log(` ${red('x')} Memory hook failed: ${e.message}`);
999
1011
  }
1000
-
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.'));
1006
- 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
- }
1017
- }
1018
- } else if (hookFlag) {
1019
- try {
1020
- installSessionCaptureHook();
1021
- } 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}`);
1022
1017
  }
1023
-
1024
- // 3. Auto-capture hook (PostToolCall) — only offer if recall hook was installed
1025
- if (!isNonInteractive) {
1026
- 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).'));
1029
- 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 {}
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}`);
1045
1023
  }
1046
- } else if (!isNonInteractive && !hookFlag) {
1047
- console.log(dim(` Skipped install later: context-vault hooks install`));
1024
+ } else {
1025
+ console.log(dim(` Hooks skipped. Install later: context-vault hooks install`));
1048
1026
  }
1049
- }
1050
1027
 
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';
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
+ }
1060
1038
  if (installSkillsFlag) {
1061
1039
  try {
1062
1040
  const names = installSkills();
1063
- if (names.length > 0) {
1064
- for (const name of names) {
1065
- console.log(`\n ${green('+')} ${name} skill installed`);
1066
- }
1041
+ for (const name of names) {
1042
+ console.log(` ${green('+')} ${name} skill installed`);
1067
1043
  }
1068
1044
  } catch (e) {
1069
- console.log(`\n ${red('x')} Skills install failed: ${e.message}`);
1045
+ console.log(` ${red('x')} Skills install failed: ${e.message}`);
1070
1046
  }
1071
1047
  } else {
1072
- console.log(dim(` Skipped install later: context-vault skills install`));
1048
+ console.log(dim(` Skills skipped. Install later: context-vault skills install`));
1073
1049
  }
1074
1050
  }
1075
1051
 
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 = [];
1052
+ // Agent rules installation
1079
1053
  if (configuredTools.length > 0 && !flags.has('--no-rules')) {
1080
- let installRules = isNonInteractive;
1081
- if (!isNonInteractive) {
1054
+ let installRules = isNonInteractive || useRecommendedDefaults;
1055
+ if (!isNonInteractive && !useRecommendedDefaults) {
1082
1056
  console.log();
1083
1057
  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.'));
1058
+ console.log(dim(' Teaches your AI agent when and how to save knowledge to the vault.'));
1086
1059
  console.log();
1087
1060
  const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
1088
1061
  installRules = rulesAnswer.toLowerCase() !== 'n';
@@ -1095,21 +1068,21 @@ async function runSetup() {
1095
1068
  const installed = installAgentRulesForTool(tool, rulesContent);
1096
1069
  const rulesPath = getRulesPathForTool(tool);
1097
1070
  if (installed) {
1098
- console.log(` ${green('+')} ${tool.name} agent rules installed`);
1071
+ console.log(` ${green('+')} ${tool.name} agent rules installed`);
1099
1072
  if (rulesPath) {
1100
1073
  console.log(` ${dim(rulesPath)}`);
1101
1074
  installedRulesPaths.push({ tool: tool.name, path: rulesPath });
1102
1075
  }
1103
1076
  }
1104
1077
  } catch (e) {
1105
- console.log(` ${red('x')} ${tool.name} ${e.message}`);
1078
+ console.log(` ${red('x')} ${tool.name} rules: ${e.message}`);
1106
1079
  }
1107
1080
  }
1108
1081
  } else {
1109
- console.log(dim(' Agent rules file not found in package — skipping.'));
1082
+ console.log(dim(' Agent rules file not found in package.'));
1110
1083
  }
1111
1084
  } else {
1112
- console.log(dim(' Skipped install later: context-vault rules install'));
1085
+ console.log(dim(' Rules skipped. Install later: context-vault rules install'));
1113
1086
  }
1114
1087
  } else if (flags.has('--no-rules')) {
1115
1088
  console.log(dim(' Agent rules skipped (--no-rules)'));
@@ -1123,8 +1096,36 @@ async function runSetup() {
1123
1096
  );
1124
1097
  }
1125
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
+
1126
1127
  // Health check
1127
- console.log(`\n ${dim('[6/6]')}${bold(' Health check...')}\n`);
1128
+ console.log(`\n ${dim('[7/7]')}${bold(' Health check...')}\n`);
1128
1129
  verbose(userLevel, 'Verifying vault, config, and database are accessible.\n');
1129
1130
  const okResults = results.filter((r) => r.ok);
1130
1131
 
@@ -1894,7 +1895,11 @@ async function runSwitch() {
1894
1895
  }
1895
1896
 
1896
1897
  async function runReindex() {
1897
- 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...'));
1898
1903
 
1899
1904
  const { resolveConfig } = await import('@context-vault/core/config');
1900
1905
  const { initDatabase, prepareStatements, insertVec, deleteVec } =
@@ -1920,14 +1925,34 @@ async function runReindex() {
1920
1925
  deleteVec: (r) => deleteVec(stmts, r),
1921
1926
  };
1922
1927
 
1923
- 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);
1924
1936
 
1925
1937
  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`);
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
+ }
1931
1956
  }
1932
1957
 
1933
1958
  async function runMigrateDirs() {
@@ -2278,7 +2303,8 @@ async function runStatus() {
2278
2303
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
2279
2304
  const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
2280
2305
  const countStr = String(c).padStart(4);
2281
- const plural = kind.endsWith('s') ? kind : 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');
2282
2308
  console.log(` ${dim(bar)} ${countStr} ${plural}`);
2283
2309
  }
2284
2310
  } else {
@@ -3796,6 +3822,78 @@ async function runSessionEnd() {
3796
3822
  meta: { session_id: session_id ?? null, cwd, message_count },
3797
3823
  });
3798
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
+ }
3799
3897
  } catch {
3800
3898
  // fail silently — never block session end
3801
3899
  } finally {
@@ -4102,48 +4200,45 @@ function loadAgentRules() {
4102
4200
  * Returns null for tools with no rules install path.
4103
4201
  */
4104
4202
  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;
4203
+ return tool.rulesPath || null;
4109
4204
  }
4110
4205
 
4111
4206
  /**
4112
4207
  * 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.
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.
4118
4212
  */
4119
4213
  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');
4214
+ const rulesPath = tool.rulesPath;
4215
+ if (!rulesPath) return false;
4216
+
4217
+ if (tool.rulesMethod === 'write') {
4123
4218
  if (existsSync(rulesPath)) {
4124
4219
  const existing = readFileSync(rulesPath, 'utf-8');
4125
4220
  if (existing.trim() === rulesContent.trim()) return false;
4126
4221
  }
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 });
4222
+ mkdirSync(dirname(rulesPath), { recursive: true });
4137
4223
  writeFileSync(rulesPath, rulesContent);
4138
4224
  return true;
4139
4225
  }
4140
4226
 
4141
- if (tool.id === 'windsurf') {
4142
- const rulesPath = join(HOME, '.windsurfrules');
4227
+ if (tool.rulesMethod === 'append') {
4143
4228
  const delimited = `\n${RULES_DELIMITER_START}\n${rulesContent}\n${RULES_DELIMITER_END}\n`;
4144
4229
  if (existsSync(rulesPath)) {
4145
4230
  const existing = readFileSync(rulesPath, 'utf-8');
4146
- if (existing.includes(RULES_DELIMITER_START)) return false;
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
+ }
4147
4242
  writeFileSync(rulesPath, existing + delimited);
4148
4243
  } else {
4149
4244
  writeFileSync(rulesPath, delimited.trimStart());
@@ -4151,7 +4246,6 @@ function installAgentRulesForTool(tool, rulesContent) {
4151
4246
  return true;
4152
4247
  }
4153
4248
 
4154
- // Other tools: no rules installation path yet
4155
4249
  return false;
4156
4250
  }
4157
4251
 
@@ -4567,19 +4661,28 @@ async function runRules() {
4567
4661
  console.log();
4568
4662
  } else if (sub === 'show') {
4569
4663
  const { detected } = await detectAllTools();
4570
- const tool = detected.find((t) => getRulesPathForTool(t));
4571
- if (!tool) {
4664
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4665
+ if (toolsWithRules.length === 0) {
4572
4666
  console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4573
4667
  process.exit(1);
4574
4668
  }
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`));
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`));
4579
4684
  process.exit(1);
4580
4685
  }
4581
- console.log(`\n ${dim(`${tool.name}: ${rulesPath}`)}\n`);
4582
- console.log(readFileSync(rulesPath, 'utf-8'));
4583
4686
  } else if (sub === 'path') {
4584
4687
  const { detected } = await detectAllTools();
4585
4688
  const supportedTools = detected.filter((t) => getRulesPathForTool(t));
@@ -4601,42 +4704,44 @@ async function runRules() {
4601
4704
  process.exit(1);
4602
4705
  }
4603
4706
  const { detected } = await detectAllTools();
4604
- const tool = detected.find((t) => getRulesPathForTool(t));
4605
- if (!tool) {
4707
+ const toolsWithRules = detected.filter((t) => getRulesPathForTool(t));
4708
+ if (toolsWithRules.length === 0) {
4606
4709
  console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4607
4710
  process.exit(1);
4608
4711
  }
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}`);
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
+ }
4634
4739
  }
4740
+ console.log();
4741
+ console.log(dim(' To upgrade: context-vault rules install'));
4635
4742
  }
4636
- console.log();
4637
- console.log(dim(' To upgrade: context-vault rules install'));
4638
- console.log();
4639
4743
  }
4744
+ console.log();
4640
4745
  } else {
4641
4746
  console.log(`
4642
4747
  ${bold('context-vault rules')} <command>