context-vault 3.4.4 → 3.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/assets/agent-rules.md +50 -0
  2. package/assets/setup-prompt.md +58 -0
  3. package/assets/skills/vault-setup/skill.md +81 -0
  4. package/bin/cli.js +533 -11
  5. package/dist/helpers.d.ts +2 -0
  6. package/dist/helpers.d.ts.map +1 -1
  7. package/dist/helpers.js +23 -0
  8. package/dist/helpers.js.map +1 -1
  9. package/dist/server.js +24 -2
  10. package/dist/server.js.map +1 -1
  11. package/dist/tools/context-status.js +29 -28
  12. package/dist/tools/context-status.js.map +1 -1
  13. package/dist/tools/get-context.d.ts.map +1 -1
  14. package/dist/tools/get-context.js +22 -18
  15. package/dist/tools/get-context.js.map +1 -1
  16. package/dist/tools/list-context.d.ts.map +1 -1
  17. package/dist/tools/list-context.js +8 -8
  18. package/dist/tools/list-context.js.map +1 -1
  19. package/dist/tools/save-context.d.ts.map +1 -1
  20. package/dist/tools/save-context.js +37 -22
  21. package/dist/tools/save-context.js.map +1 -1
  22. package/dist/tools/session-start.d.ts.map +1 -1
  23. package/dist/tools/session-start.js +39 -5
  24. package/dist/tools/session-start.js.map +1 -1
  25. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  26. package/node_modules/@context-vault/core/dist/capture.js +11 -0
  27. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  28. package/node_modules/@context-vault/core/dist/config.d.ts +8 -0
  29. package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
  30. package/node_modules/@context-vault/core/dist/config.js +20 -1
  31. package/node_modules/@context-vault/core/dist/config.js.map +1 -1
  32. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  33. package/node_modules/@context-vault/core/dist/frontmatter.js +2 -0
  34. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  35. package/node_modules/@context-vault/core/package.json +1 -1
  36. package/node_modules/@context-vault/core/src/capture.ts +9 -0
  37. package/node_modules/@context-vault/core/src/config.ts +22 -1
  38. package/node_modules/@context-vault/core/src/frontmatter.ts +2 -0
  39. package/package.json +2 -2
  40. package/scripts/prepack.js +17 -0
  41. package/src/helpers.ts +25 -0
  42. package/src/server.ts +25 -2
  43. package/src/tools/context-status.ts +30 -30
  44. package/src/tools/get-context.ts +23 -24
  45. package/src/tools/list-context.ts +8 -11
  46. package/src/tools/save-context.ts +39 -25
  47. package/src/tools/session-start.ts +36 -5
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);
@@ -353,12 +364,14 @@ ${bold('Commands:')}
353
364
  ${cyan('hooks')} install|uninstall Install or remove Claude Code memory hook
354
365
  ${cyan('claude')} install|uninstall Alias for hooks install|uninstall
355
366
  ${cyan('skills')} install Install bundled Claude Code skills
367
+ ${cyan('rules')} install Install agent rules for detected AI tools
356
368
  ${cyan('health')} Quick health check — vault, DB, entry count
357
369
  ${cyan('status')} Show vault diagnostics
358
370
  ${cyan('doctor')} Diagnose and repair common issues
359
371
  ${cyan('debug')} Generate AI-pasteable debug report
360
372
  ${cyan('daemon')} start|stop|status Run vault as a shared HTTP daemon (one process, all sessions)
361
373
  ${cyan('restart')} Stop running MCP server processes (client auto-restarts)
374
+ ${cyan('reconnect')} Fix vault path, kill stale servers, re-register MCP, reindex
362
375
  ${cyan('search')} Search vault entries from CLI
363
376
  ${cyan('save')} Save an entry to the vault from CLI
364
377
  ${cyan('import')} <path> Import entries from file, directory, or .zip archive
@@ -545,7 +558,7 @@ async function runSetup() {
545
558
  }
546
559
  } catch {}
547
560
 
548
- console.log(`\n ${dim('[2/2]')}${bold(' Configuring tools...\n')}`);
561
+ console.log(`\n ${dim('[2/3]')}${bold(' Configuring tools...\n')}`);
549
562
  for (const tool of selected) {
550
563
  try {
551
564
  if (tool.configType === 'cli' && tool.id === 'codex') {
@@ -561,6 +574,40 @@ async function runSetup() {
561
574
  }
562
575
  }
563
576
 
577
+ // Offer rules installation for users who previously skipped or used an older version
578
+ console.log(`\n ${dim('[3/3]')}${bold(' Agent rules...\n')}`);
579
+ const rulesContent = loadAgentRules();
580
+ if (rulesContent && !flags.has('--no-rules')) {
581
+ const missingRules = selected.filter((t) => {
582
+ const p = getRulesPathForTool(t);
583
+ return p && !existsSync(p);
584
+ });
585
+ if (missingRules.length > 0) {
586
+ console.log(dim(' Agent rules teach your AI when to save knowledge automatically.'));
587
+ console.log(dim(' No rules file detected for: ' + missingRules.map((t) => t.name).join(', ')));
588
+ console.log();
589
+ const rulesAnswer = await prompt(' Install agent rules? (Y/n):', 'Y');
590
+ if (rulesAnswer.toLowerCase() !== 'n') {
591
+ for (const tool of missingRules) {
592
+ try {
593
+ const ok = installAgentRulesForTool(tool, rulesContent);
594
+ const rulesPath = getRulesPathForTool(tool);
595
+ if (ok) {
596
+ console.log(` ${green('+')} ${tool.name} — agent rules installed`);
597
+ if (rulesPath) console.log(` ${dim(rulesPath)}`);
598
+ }
599
+ } catch (e) {
600
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
601
+ }
602
+ }
603
+ } else {
604
+ console.log(dim(' Skipped — install later: context-vault rules install'));
605
+ }
606
+ } else {
607
+ console.log(dim(' Agent rules already installed — skipping.'));
608
+ }
609
+ }
610
+
564
611
  console.log();
565
612
  console.log(green(' ✓ Tool configs updated.'));
566
613
  console.log(dim(' Restart your AI tools to apply the changes.'));
@@ -810,6 +857,7 @@ async function runSetup() {
810
857
  ` ${telemetryEnabled ? green('+') : dim('-')} Telemetry: ${telemetryEnabled ? 'enabled' : 'disabled'}`
811
858
  );
812
859
 
860
+ assertNotTestMode(configPath);
813
861
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
814
862
  console.log(`\n ${green('+')} Wrote ${configPath}`);
815
863
 
@@ -902,12 +950,11 @@ async function runSetup() {
902
950
  } catch {}
903
951
  }
904
952
 
905
- // Configure each tool — pass vault dir as arg if non-default
953
+ // Configure each tool — always pass vault dir explicitly to prevent config drift
906
954
  console.log(`\n ${dim('[5/6]')}${bold(' Configuring tools...\n')}`);
907
955
  verbose(userLevel, 'Writing config so your AI tool can find your vault.\n');
908
956
  const results = [];
909
- const defaultVDir = join(HOME, 'vault');
910
- const customVaultDir = resolvedVaultDir !== resolve(defaultVDir) ? resolvedVaultDir : null;
957
+ const customVaultDir = resolvedVaultDir;
911
958
 
912
959
  for (const tool of selected) {
913
960
  try {
@@ -938,8 +985,8 @@ async function runSetup() {
938
985
  console.log(dim(' Searches your vault on every prompt and injects relevant entries'));
939
986
  console.log(dim(" as additional context alongside Claude's native memory."));
940
987
  console.log();
941
- const answer = await prompt(' Install Claude Code memory hook? (y/N):', 'N');
942
- installHook = answer.toLowerCase() === 'y';
988
+ const answer = await prompt(' Install Claude Code memory hook? (Y/n):', 'Y');
989
+ installHook = answer.toLowerCase() !== 'n';
943
990
  }
944
991
  if (installHook) {
945
992
  try {
@@ -1006,6 +1053,7 @@ async function runSetup() {
1006
1053
  console.log();
1007
1054
  console.log(dim(' Install Claude Code skills? (recommended)'));
1008
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)'));
1009
1057
  console.log();
1010
1058
  const skillAnswer = await prompt(' Install Claude Code skills? (Y/n):', 'Y');
1011
1059
  const installSkillsFlag = skillAnswer.toLowerCase() !== 'n';
@@ -1025,6 +1073,48 @@ async function runSetup() {
1025
1073
  }
1026
1074
  }
1027
1075
 
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 = [];
1079
+ 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 });
1102
+ }
1103
+ }
1104
+ } catch (e) {
1105
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
1106
+ }
1107
+ }
1108
+ } else {
1109
+ console.log(dim(' Agent rules file not found in package — skipping.'));
1110
+ }
1111
+ } else {
1112
+ console.log(dim(' Skipped — install later: context-vault rules install'));
1113
+ }
1114
+ } else if (flags.has('--no-rules')) {
1115
+ console.log(dim(' Agent rules skipped (--no-rules)'));
1116
+ }
1117
+
1028
1118
  // Seed entry
1029
1119
  const seeded = createSeedEntries(resolvedVaultDir);
1030
1120
  if (seeded > 0) {
@@ -1128,6 +1218,21 @@ async function runSetup() {
1128
1218
  : []),
1129
1219
  ];
1130
1220
  }
1221
+ if (installedRulesPaths.length > 0) {
1222
+ boxLines.push(``, ` ${bold('Agent rules installed:')}`);
1223
+ for (const { path } of installedRulesPaths) {
1224
+ boxLines.push(` ${dim(path)}`);
1225
+ }
1226
+ boxLines.push(
1227
+ ``,
1228
+ ` ${dim(`View: ${cli} rules show`)}`,
1229
+ ` ${dim(`Remove: ${cli} uninstall`)}`,
1230
+ ` ${dim(`Skip: ${cli} setup --no-rules`)}`
1231
+ );
1232
+ }
1233
+ if (claudeConfigured) {
1234
+ boxLines.push(``, ` ${dim('Personalize: run /vault-setup in your next session')}`);
1235
+ }
1131
1236
  const innerWidth = Math.max(...boxLines.map((l) => l.length)) + 2;
1132
1237
  const pad = (s) => s + ' '.repeat(Math.max(0, innerWidth - s.length));
1133
1238
  console.log();
@@ -1368,8 +1473,8 @@ function configureJsonTool(tool, vaultDir) {
1368
1473
  function createSeedEntries(vaultDir) {
1369
1474
  let created = 0;
1370
1475
 
1371
- // Entry 1: Getting started (improved)
1372
- const insightDir = join(vaultDir, 'knowledge', 'insights');
1476
+ // Entry 1: Getting started
1477
+ const insightDir = join(vaultDir, 'knowledge', 'insight');
1373
1478
  const insightPath = join(insightDir, 'getting-started.md');
1374
1479
  if (!existsSync(insightPath)) {
1375
1480
  mkdirSync(insightDir, { recursive: true });
@@ -1379,6 +1484,9 @@ function createSeedEntries(vaultDir) {
1379
1484
  insightPath,
1380
1485
  `---
1381
1486
  id: ${id1}
1487
+ title: Getting started with your context vault
1488
+ kind: insight
1489
+ tier: durable
1382
1490
  tags: ["getting-started", "vault"]
1383
1491
  source: context-vault-setup
1384
1492
  created: ${now}
@@ -1402,7 +1510,7 @@ ${insightPath}
1402
1510
  }
1403
1511
 
1404
1512
  // Entry 2: Example decision
1405
- const decisionDir = join(vaultDir, 'knowledge', 'decisions');
1513
+ const decisionDir = join(vaultDir, 'knowledge', 'decision');
1406
1514
  const decisionPath = join(decisionDir, 'example-local-first-data.md');
1407
1515
  if (!existsSync(decisionPath)) {
1408
1516
  mkdirSync(decisionDir, { recursive: true });
@@ -1412,6 +1520,9 @@ ${insightPath}
1412
1520
  decisionPath,
1413
1521
  `---
1414
1522
  id: ${id2}
1523
+ title: Use local-first data storage over cloud databases
1524
+ kind: decision
1525
+ tier: durable
1415
1526
  tags: ["example", "architecture"]
1416
1527
  source: context-vault-setup
1417
1528
  created: ${now}
@@ -1584,6 +1695,7 @@ async function runConnect() {
1584
1695
  modeConfig.mode = 'hosted';
1585
1696
  modeConfig.hostedUrl = hostedUrl;
1586
1697
  mkdirSync(join(HOME, '.context-mcp'), { recursive: true });
1698
+ assertNotTestMode(modeConfigPath);
1587
1699
  writeFileSync(modeConfigPath, JSON.stringify(modeConfig, null, 2) + '\n');
1588
1700
 
1589
1701
  console.log();
@@ -1696,6 +1808,7 @@ async function runSwitch() {
1696
1808
  if (target === 'local') {
1697
1809
  vaultConfig.mode = 'local';
1698
1810
  mkdirSync(dataDir, { recursive: true });
1811
+ assertNotTestMode(configPath);
1699
1812
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1700
1813
 
1701
1814
  console.log();
@@ -1756,6 +1869,7 @@ async function runSwitch() {
1756
1869
  vaultConfig.hostedUrl = hostedUrl;
1757
1870
  vaultConfig.apiKey = apiKey;
1758
1871
  mkdirSync(dataDir, { recursive: true });
1872
+ assertNotTestMode(configPath);
1759
1873
  writeFileSync(configPath, JSON.stringify(vaultConfig, null, 2) + '\n');
1760
1874
 
1761
1875
  for (const tool of detected) {
@@ -2131,6 +2245,11 @@ async function runStatus() {
2131
2245
  }
2132
2246
 
2133
2247
  const status = gatherVaultStatus({ db, config });
2248
+ let schemaVersion = 'unknown';
2249
+ try {
2250
+ const row = db.prepare('PRAGMA user_version').get();
2251
+ schemaVersion = String(row?.user_version ?? 'unknown');
2252
+ } catch {}
2134
2253
 
2135
2254
  db.close();
2136
2255
 
@@ -2148,7 +2267,7 @@ async function runStatus() {
2148
2267
  ` Config: ${config.configPath} ${dim(`(${existsSync(config.configPath) ? 'exists' : 'missing'})`)}`
2149
2268
  );
2150
2269
  console.log(` Resolved: ${status.resolvedFrom}`);
2151
- console.log(` Schema: v7 (teams)`);
2270
+ console.log(` Schema: v${schemaVersion}`);
2152
2271
 
2153
2272
  if (status.kindCounts.length) {
2154
2273
  const BAR_WIDTH = 20;
@@ -2159,7 +2278,8 @@ async function runStatus() {
2159
2278
  const filled = maxCount > 0 ? Math.round((c / maxCount) * BAR_WIDTH) : 0;
2160
2279
  const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
2161
2280
  const countStr = String(c).padStart(4);
2162
- console.log(` ${dim(bar)} ${countStr} ${kind}s`);
2281
+ const plural = kind.endsWith('s') ? kind : kind + 's';
2282
+ console.log(` ${dim(bar)} ${countStr} ${plural}`);
2163
2283
  }
2164
2284
  } else {
2165
2285
  console.log(`\n ${dim('(empty — no entries indexed)')}`);
@@ -2325,6 +2445,34 @@ async function runUninstall() {
2325
2445
  console.log(` ${green('+')} Removed installed skills`);
2326
2446
  }
2327
2447
 
2448
+ // Remove agent rules files
2449
+ const claudeRulesPath = join(HOME, '.claude', 'rules', 'context-vault.md');
2450
+ const cursorRulesPath = join(HOME, '.cursor', 'rules', 'context-vault.mdc');
2451
+ const windsurfRulesPath = join(HOME, '.windsurfrules');
2452
+
2453
+ if (existsSync(claudeRulesPath)) {
2454
+ unlinkSync(claudeRulesPath);
2455
+ console.log(` ${green('+')} Removed agent rules (Claude Code: ${claudeRulesPath})`);
2456
+ }
2457
+ if (existsSync(cursorRulesPath)) {
2458
+ unlinkSync(cursorRulesPath);
2459
+ console.log(` ${green('+')} Removed agent rules (Cursor: ${cursorRulesPath})`);
2460
+ }
2461
+ if (existsSync(windsurfRulesPath)) {
2462
+ const content = readFileSync(windsurfRulesPath, 'utf-8');
2463
+ if (content.includes(RULES_DELIMITER_START)) {
2464
+ const cleaned = content
2465
+ .replace(new RegExp(`\n?${RULES_DELIMITER_START}[\\s\\S]*?${RULES_DELIMITER_END}\n?`, 'g'), '\n')
2466
+ .trim();
2467
+ if (cleaned) {
2468
+ writeFileSync(windsurfRulesPath, cleaned + '\n');
2469
+ } else {
2470
+ unlinkSync(windsurfRulesPath);
2471
+ }
2472
+ console.log(` ${green('+')} Removed agent rules section from ${windsurfRulesPath}`);
2473
+ }
2474
+ }
2475
+
2328
2476
  // Optionally remove data directory
2329
2477
  const dataDir = join(HOME, '.context-mcp');
2330
2478
  if (existsSync(dataDir)) {
@@ -3936,6 +4084,77 @@ function installSkills() {
3936
4084
  return installed;
3937
4085
  }
3938
4086
 
4087
+ const RULES_DELIMITER_START = '<!-- context-vault agent rules -->';
4088
+ const RULES_DELIMITER_END = '<!-- /context-vault agent rules -->';
4089
+
4090
+ /**
4091
+ * Load agent-rules.md from the assets directory.
4092
+ * Returns the file content or null if not found.
4093
+ */
4094
+ function loadAgentRules() {
4095
+ const rulesPath = join(ROOT, 'assets', 'agent-rules.md');
4096
+ if (!existsSync(rulesPath)) return null;
4097
+ return readFileSync(rulesPath, 'utf-8');
4098
+ }
4099
+
4100
+ /**
4101
+ * Return the path where agent rules are/would be installed for a given tool.
4102
+ * Returns null for tools with no rules install path.
4103
+ */
4104
+ 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;
4109
+ }
4110
+
4111
+ /**
4112
+ * 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.
4118
+ */
4119
+ 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');
4123
+ if (existsSync(rulesPath)) {
4124
+ const existing = readFileSync(rulesPath, 'utf-8');
4125
+ if (existing.trim() === rulesContent.trim()) return false;
4126
+ }
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 });
4137
+ writeFileSync(rulesPath, rulesContent);
4138
+ return true;
4139
+ }
4140
+
4141
+ if (tool.id === 'windsurf') {
4142
+ const rulesPath = join(HOME, '.windsurfrules');
4143
+ const delimited = `\n${RULES_DELIMITER_START}\n${rulesContent}\n${RULES_DELIMITER_END}\n`;
4144
+ if (existsSync(rulesPath)) {
4145
+ const existing = readFileSync(rulesPath, 'utf-8');
4146
+ if (existing.includes(RULES_DELIMITER_START)) return false;
4147
+ writeFileSync(rulesPath, existing + delimited);
4148
+ } else {
4149
+ writeFileSync(rulesPath, delimited.trimStart());
4150
+ }
4151
+ return true;
4152
+ }
4153
+
4154
+ // Other tools: no rules installation path yet
4155
+ return false;
4156
+ }
4157
+
3939
4158
  /** Returns the path to Claude Code's global settings.json */
3940
4159
  function claudeSettingsPath() {
3941
4160
  return join(HOME, '.claude', 'settings.json');
@@ -4291,6 +4510,149 @@ ${bold('Commands:')}
4291
4510
 
4292
4511
  ${bold('Bundled skills:')}
4293
4512
  ${cyan('compile-context')} Compile vault entries into a project brief using create_snapshot
4513
+ ${cyan('vault-setup')} Agent-assisted vault customization (run /vault-setup)
4514
+ `);
4515
+ }
4516
+ }
4517
+
4518
+ async function runRules() {
4519
+ const sub = args[1];
4520
+
4521
+ if (sub === 'install') {
4522
+ console.log();
4523
+ const rulesContent = loadAgentRules();
4524
+ if (!rulesContent) {
4525
+ console.log(` ${yellow('!')} Agent rules file not found in package.\n`);
4526
+ process.exit(1);
4527
+ }
4528
+
4529
+ const { detected } = await detectAllTools();
4530
+ if (detected.length === 0) {
4531
+ console.log(` ${yellow('!')} No supported tools detected.\n`);
4532
+ process.exit(1);
4533
+ }
4534
+
4535
+ let installed = 0;
4536
+ for (const tool of detected) {
4537
+ try {
4538
+ const ok = installAgentRulesForTool(tool, rulesContent);
4539
+ const rulesPath = getRulesPathForTool(tool);
4540
+ if (ok) {
4541
+ console.log(` ${green('+')} ${tool.name} — agent rules installed`);
4542
+ if (rulesPath) console.log(` ${dim(rulesPath)}`);
4543
+ installed++;
4544
+ } else {
4545
+ const hasPath = !!rulesPath;
4546
+ const alreadyExists = hasPath && existsSync(rulesPath);
4547
+ if (alreadyExists) {
4548
+ console.log(` ${dim('-')} ${tool.name} — already installed`);
4549
+ } else if (hasPath) {
4550
+ console.log(` ${dim('-')} ${tool.name} — skipped (up to date)`);
4551
+ } else {
4552
+ console.log(` ${dim('-')} ${tool.name} — not supported`);
4553
+ }
4554
+ }
4555
+ } catch (e) {
4556
+ console.log(` ${red('x')} ${tool.name} — ${e.message}`);
4557
+ }
4558
+ }
4559
+
4560
+ console.log();
4561
+ if (installed > 0) {
4562
+ console.log(dim(' Rules teach your AI agent when to save knowledge automatically.'));
4563
+ console.log(dim(' Restart your AI tools to apply.'));
4564
+ console.log(dim(` View: context-vault rules show`));
4565
+ console.log(dim(` Remove: context-vault uninstall`));
4566
+ }
4567
+ console.log();
4568
+ } else if (sub === 'show') {
4569
+ const { detected } = await detectAllTools();
4570
+ const tool = detected.find((t) => getRulesPathForTool(t));
4571
+ if (!tool) {
4572
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4573
+ process.exit(1);
4574
+ }
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`));
4579
+ process.exit(1);
4580
+ }
4581
+ console.log(`\n ${dim(`${tool.name}: ${rulesPath}`)}\n`);
4582
+ console.log(readFileSync(rulesPath, 'utf-8'));
4583
+ } else if (sub === 'path') {
4584
+ const { detected } = await detectAllTools();
4585
+ const supportedTools = detected.filter((t) => getRulesPathForTool(t));
4586
+ if (supportedTools.length === 0) {
4587
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4588
+ process.exit(1);
4589
+ }
4590
+ console.log();
4591
+ for (const tool of supportedTools) {
4592
+ const p = getRulesPathForTool(tool);
4593
+ const installed = existsSync(p);
4594
+ console.log(` ${tool.name}: ${p} ${installed ? green('(installed)') : dim('(not installed)')}`);
4595
+ }
4596
+ console.log();
4597
+ } else if (sub === 'diff') {
4598
+ const bundled = loadAgentRules();
4599
+ if (!bundled) {
4600
+ console.log(`\n ${yellow('!')} Agent rules file not found in package.\n`);
4601
+ process.exit(1);
4602
+ }
4603
+ const { detected } = await detectAllTools();
4604
+ const tool = detected.find((t) => getRulesPathForTool(t));
4605
+ if (!tool) {
4606
+ console.log(`\n ${yellow('!')} No supported tool detected.\n`);
4607
+ process.exit(1);
4608
+ }
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}`);
4634
+ }
4635
+ }
4636
+ console.log();
4637
+ console.log(dim(' To upgrade: context-vault rules install'));
4638
+ console.log();
4639
+ }
4640
+ } else {
4641
+ console.log(`
4642
+ ${bold('context-vault rules')} <command>
4643
+
4644
+ Manage agent rules that teach AI tools when and how to use the vault.
4645
+
4646
+ ${bold('Commands:')}
4647
+ ${cyan('rules install')} Install agent rules for all detected AI tools
4648
+ ${cyan('rules show')} Print the currently installed rules file
4649
+ ${cyan('rules diff')} Show diff between installed rules and bundled version
4650
+ ${cyan('rules path')} Print the path where rules are/would be installed
4651
+
4652
+ ${bold('Installed to:')}
4653
+ ${cyan('Claude Code')} ~/.claude/rules/context-vault.md
4654
+ ${cyan('Cursor')} ~/.cursor/rules/context-vault.mdc
4655
+ ${cyan('Windsurf')} ~/.windsurfrules (appended with delimiters)
4294
4656
  `);
4295
4657
  }
4296
4658
  }
@@ -5105,6 +5467,160 @@ async function runRestart() {
5105
5467
  console.log();
5106
5468
  }
5107
5469
 
5470
+ async function runReconnect() {
5471
+ console.log();
5472
+ console.log(` ${bold('◇ context-vault reconnect')}`);
5473
+ console.log();
5474
+
5475
+ // 1. Read current config to get the correct vault dir
5476
+ const { resolveConfig } = await import('@context-vault/core/config');
5477
+ const config = resolveConfig();
5478
+ const vaultDir = config.vaultDir;
5479
+
5480
+ console.log(` Vault dir: ${cyan(vaultDir)}`);
5481
+ if (!existsSync(vaultDir)) {
5482
+ console.error(red(` Vault directory does not exist: ${vaultDir}`));
5483
+ console.error(dim(` Run context-vault setup to configure.`));
5484
+ process.exit(1);
5485
+ }
5486
+
5487
+ // Count entries to confirm it's a real vault
5488
+ const mdFiles = readdirSync(vaultDir, { recursive: true })
5489
+ .filter(f => String(f).endsWith('.md'));
5490
+ console.log(` Found ${mdFiles.length} markdown files`);
5491
+ console.log();
5492
+
5493
+ // 2. Kill all running context-vault serve processes (they have stale --vault-dir)
5494
+ const isWin = platform() === 'win32';
5495
+ let psOutput;
5496
+ try {
5497
+ const psCmd = isWin
5498
+ ? 'wmic process where "CommandLine like \'%context-vault%\'" get ProcessId,CommandLine /format:list'
5499
+ : 'ps aux';
5500
+ psOutput = execSync(psCmd, { encoding: 'utf-8', timeout: 5000 });
5501
+ } catch (e) {
5502
+ console.error(red(` Failed to list processes: ${e.message}`));
5503
+ process.exit(1);
5504
+ }
5505
+
5506
+ const currentPid = process.pid;
5507
+ const serverPids = [];
5508
+
5509
+ if (isWin) {
5510
+ const pidMatches = psOutput.matchAll(/ProcessId=(\d+)/g);
5511
+ for (const m of pidMatches) {
5512
+ const pid = parseInt(m[1], 10);
5513
+ if (pid !== currentPid) serverPids.push(pid);
5514
+ }
5515
+ } else {
5516
+ const lines = psOutput.split('\n');
5517
+ for (const line of lines) {
5518
+ const match = line.match(/^\S+\s+(\d+)\s/);
5519
+ if (!match) continue;
5520
+ const pid = parseInt(match[1], 10);
5521
+ if (pid === currentPid) continue;
5522
+ if (
5523
+ /context-vault.*(serve|stdio|server\/index)/.test(line) ||
5524
+ /server\/index\.js.*context-vault/.test(line)
5525
+ ) {
5526
+ serverPids.push(pid);
5527
+ }
5528
+ }
5529
+ }
5530
+
5531
+ if (serverPids.length > 0) {
5532
+ console.log(` Stopping ${serverPids.length} stale server process${serverPids.length === 1 ? '' : 'es'}...`);
5533
+ for (const pid of serverPids) {
5534
+ try {
5535
+ process.kill(pid, 'SIGTERM');
5536
+ console.log(` ${green('✓')} Stopped PID ${pid}`);
5537
+ } catch (e) {
5538
+ if (e.code !== 'ESRCH') {
5539
+ console.log(` ${yellow('!')} Could not stop PID ${pid}: ${e.message}`);
5540
+ }
5541
+ }
5542
+ }
5543
+ // Wait for graceful shutdown
5544
+ await new Promise((resolve) => setTimeout(resolve, 1500));
5545
+ // Force-kill any survivors
5546
+ for (const pid of serverPids) {
5547
+ try { process.kill(pid, 0); process.kill(pid, 'SIGKILL'); } catch {}
5548
+ }
5549
+ console.log();
5550
+ } else {
5551
+ console.log(dim(' No running server processes found.'));
5552
+ console.log();
5553
+ }
5554
+
5555
+ // 3. Re-register MCP server with correct vault-dir for each detected tool
5556
+ const env = { ...process.env };
5557
+ delete env.CLAUDECODE;
5558
+
5559
+ const tools = [];
5560
+ try { execSync('which claude', { stdio: 'pipe' }); tools.push('claude'); } catch {}
5561
+ try { execSync('which codex', { stdio: 'pipe' }); tools.push('codex'); } catch {}
5562
+
5563
+ for (const tool of tools) {
5564
+ try {
5565
+ execFileSync(tool, ['mcp', 'remove', 'context-vault', '-s', 'user'], { stdio: 'pipe', env });
5566
+ } catch {}
5567
+
5568
+ try {
5569
+ if (isInstalledPackage()) {
5570
+ execFileSync(
5571
+ tool,
5572
+ ['mcp', 'add', '-s', 'user', 'context-vault', '--', 'context-vault', 'serve', '--vault-dir', vaultDir],
5573
+ { stdio: 'pipe', env }
5574
+ );
5575
+ } else if (isNpx()) {
5576
+ execFileSync(
5577
+ tool,
5578
+ ['mcp', 'add', '-s', 'user', 'context-vault', '-e', 'NODE_OPTIONS=--no-warnings=ExperimentalWarning',
5579
+ '--', 'npx', '-y', 'context-vault', 'serve', '--vault-dir', vaultDir],
5580
+ { stdio: 'pipe', env }
5581
+ );
5582
+ } else {
5583
+ execFileSync(
5584
+ tool,
5585
+ ['mcp', 'add', '-s', 'user', 'context-vault', '-e', 'NODE_OPTIONS=--no-warnings=ExperimentalWarning',
5586
+ '--', process.execPath, SERVER_PATH, '--vault-dir', vaultDir],
5587
+ { stdio: 'pipe', env }
5588
+ );
5589
+ }
5590
+ console.log(` ${green('✓')} ${tool} MCP re-registered with vault-dir: ${vaultDir}`);
5591
+ } catch (e) {
5592
+ console.log(` ${red('✘')} Failed to register ${tool}: ${e.stderr?.toString().trim() || e.message}`);
5593
+ }
5594
+ }
5595
+
5596
+ // 4. Reindex to ensure DB matches vault dir
5597
+ console.log();
5598
+ console.log(` Reindexing...`);
5599
+ try {
5600
+ const { initDatabase, prepareStatements, insertVec, deleteVec } =
5601
+ await import('@context-vault/core/db');
5602
+ const { embed } = await import('@context-vault/core/embed');
5603
+ const { reindex } = await import('@context-vault/core/index');
5604
+ const db = await initDatabase(config.dbPath);
5605
+ const stmts = prepareStatements(db);
5606
+ const ctx = {
5607
+ db, config, stmts, embed,
5608
+ insertVec: (r, e) => insertVec(stmts, r, e),
5609
+ deleteVec: (r) => deleteVec(stmts, r),
5610
+ };
5611
+ const stats = await reindex(ctx, { fullSync: true });
5612
+ db.close();
5613
+ console.log(` ${green('✓')} Reindex: +${stats.added} added, ~${stats.updated} updated, -${stats.removed} removed`);
5614
+ } catch (e) {
5615
+ console.log(` ${yellow('!')} Reindex failed: ${e.message}`);
5616
+ console.log(dim(` Run 'context-vault reindex --vault-dir ${vaultDir}' manually.`));
5617
+ }
5618
+
5619
+ console.log();
5620
+ console.log(green(' Reconnected.') + dim(' Start a new Claude session to use the updated vault.'));
5621
+ console.log();
5622
+ }
5623
+
5108
5624
  async function runConsolidate() {
5109
5625
  const dryRun = flags.has('--dry-run');
5110
5626
  const tagArg = getFlag('--tag');
@@ -5657,6 +6173,9 @@ async function main() {
5657
6173
  case 'skills':
5658
6174
  await runSkills();
5659
6175
  break;
6176
+ case 'rules':
6177
+ await runRules();
6178
+ break;
5660
6179
  case 'flush':
5661
6180
  await runFlush();
5662
6181
  break;
@@ -5726,6 +6245,9 @@ async function main() {
5726
6245
  case 'restart':
5727
6246
  await runRestart();
5728
6247
  break;
6248
+ case 'reconnect':
6249
+ await runReconnect();
6250
+ break;
5729
6251
  case 'consolidate':
5730
6252
  await runConsolidate();
5731
6253
  break;