code-auditor-mcp 3.0.5 → 3.0.7

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 (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +31 -1
  3. package/dist/cli.js +416 -111
  4. package/dist/cli.js.map +1 -1
  5. package/dist/generators/AWSQConfigGenerator.d.ts +20 -0
  6. package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
  7. package/dist/generators/AWSQConfigGenerator.js +48 -19
  8. package/dist/generators/AWSQConfigGenerator.js.map +1 -1
  9. package/dist/generators/AiderConfigGenerator.d.ts +18 -0
  10. package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
  11. package/dist/generators/AiderConfigGenerator.js +48 -10
  12. package/dist/generators/AiderConfigGenerator.js.map +1 -1
  13. package/dist/generators/ClaudeConfigGenerator.d.ts +9 -3
  14. package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
  15. package/dist/generators/ClaudeConfigGenerator.js +30 -42
  16. package/dist/generators/ClaudeConfigGenerator.js.map +1 -1
  17. package/dist/generators/ClineConfigGenerator.d.ts +13 -0
  18. package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
  19. package/dist/generators/ClineConfigGenerator.js +44 -11
  20. package/dist/generators/ClineConfigGenerator.js.map +1 -1
  21. package/dist/generators/CodeiumConfigGenerator.d.ts +15 -0
  22. package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
  23. package/dist/generators/CodeiumConfigGenerator.js +45 -15
  24. package/dist/generators/CodeiumConfigGenerator.js.map +1 -1
  25. package/dist/generators/CodexConfigGenerator.d.ts +29 -0
  26. package/dist/generators/CodexConfigGenerator.d.ts.map +1 -0
  27. package/dist/generators/CodexConfigGenerator.js +80 -0
  28. package/dist/generators/CodexConfigGenerator.js.map +1 -0
  29. package/dist/generators/ConfigGeneratorFactory.d.ts +10 -1
  30. package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
  31. package/dist/generators/ConfigGeneratorFactory.js +22 -8
  32. package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
  33. package/dist/generators/ContinueConfigGenerator.d.ts +16 -1
  34. package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
  35. package/dist/generators/ContinueConfigGenerator.js +41 -73
  36. package/dist/generators/ContinueConfigGenerator.js.map +1 -1
  37. package/dist/generators/CopilotConfigGenerator.d.ts +13 -2
  38. package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
  39. package/dist/generators/CopilotConfigGenerator.js +31 -51
  40. package/dist/generators/CopilotConfigGenerator.js.map +1 -1
  41. package/dist/generators/CursorConfigGenerator.d.ts +8 -1
  42. package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
  43. package/dist/generators/CursorConfigGenerator.js +27 -51
  44. package/dist/generators/CursorConfigGenerator.js.map +1 -1
  45. package/dist/generators/GeminiConfigGenerator.d.ts +23 -0
  46. package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -0
  47. package/dist/generators/GeminiConfigGenerator.js +59 -0
  48. package/dist/generators/GeminiConfigGenerator.js.map +1 -0
  49. package/dist/generators/JetBrainsConfigGenerator.d.ts +11 -0
  50. package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
  51. package/dist/generators/JetBrainsConfigGenerator.js +39 -21
  52. package/dist/generators/JetBrainsConfigGenerator.js.map +1 -1
  53. package/dist/generators/VSCodeConfigGenerator.d.ts +14 -0
  54. package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
  55. package/dist/generators/VSCodeConfigGenerator.js +43 -15
  56. package/dist/generators/VSCodeConfigGenerator.js.map +1 -1
  57. package/dist/hooks/codex.d.ts +20 -0
  58. package/dist/hooks/codex.d.ts.map +1 -0
  59. package/dist/hooks/codex.js +91 -0
  60. package/dist/hooks/codex.js.map +1 -0
  61. package/dist/hooks/core.d.ts +62 -0
  62. package/dist/hooks/core.d.ts.map +1 -0
  63. package/dist/hooks/core.js +119 -0
  64. package/dist/hooks/core.js.map +1 -0
  65. package/dist/hooks/cursor.d.ts +72 -0
  66. package/dist/hooks/cursor.d.ts.map +1 -0
  67. package/dist/hooks/cursor.js +163 -0
  68. package/dist/hooks/cursor.js.map +1 -0
  69. package/dist/installer.d.ts +30 -0
  70. package/dist/installer.d.ts.map +1 -0
  71. package/dist/installer.js +355 -0
  72. package/dist/installer.js.map +1 -0
  73. package/dist/mcp-tools/whitelistTools.d.ts +1 -1
  74. package/package.json +3 -2
  75. package/plugin/.claude-plugin/plugin.json +1 -1
  76. package/plugin/skills/code-auditor/SKILL.md +13 -5
@@ -10,7 +10,7 @@
10
10
  "name": "code-auditor",
11
11
  "source": "./plugin",
12
12
  "description": "Diff-scoped code quality auditing on every edit. Indexes your codebase, enforces invariants, and feeds violations back to the agent so fixes happen inline.",
13
- "version": "3.0.5",
13
+ "version": "3.0.6",
14
14
  "author": {
15
15
  "name": "Ben Hammond"
16
16
  },
package/README.md CHANGED
@@ -4,12 +4,27 @@ Architectural invariants enforced inside your AI agent's edit loop. When the age
4
4
 
5
5
  ## Install
6
6
 
7
+ ```bash
8
+ npm install -g code-auditor-mcp
9
+ code-audit install --agent all
10
+ ```
11
+
12
+ Two commands to install everywhere. `code-audit install --agent all` copies the skill to every AI coding tool on your machine. Use `--agent` for specific tools.
13
+
14
+ What you get per tool: the skill (SKILL.md), MCP server access, and hook wiring where the tool supports it (blocking on Claude Code and Codex, advisory on Cursor).
15
+
16
+ ```bash
17
+ code-audit install --list # see the support matrix
18
+ ```
19
+
20
+ Claude Code users can also install via plugin:
21
+
7
22
  ```bash
8
23
  claude plugin marketplace add BenAHammond/code-auditor-mcp
9
24
  claude plugin install code-auditor
10
25
  ```
11
26
 
12
- The hook auto-installs the auditor on first use via npx — no npm commands needed. That gives you the hook, MCP server, and `/code-auditor` skill. Then tell your agent to set it up:
27
+ The hook auto-installs the auditor on first use via npx.
13
28
 
14
29
  ## Prompt examples
15
30
 
@@ -39,6 +54,21 @@ Five kinds. The agent writes them to `.codeauditor.json`. Bad configs fail the a
39
54
  | `naming` | Exported symbols not matching a pattern |
40
55
  | `ast-pattern` | AST nodes matching an ast-grep pattern |
41
56
 
57
+ ## Works with your agent
58
+
59
+ One skill, one CLI, one MCP server. Every agent gets the same audit engine — the hook contract is the only difference.
60
+
61
+ | Agent | Skill | Hooks / Blocking | MCP | Verified |
62
+ |-------|-------|------------------|-----|----------|
63
+ | Claude Code | Plugin or `code-audit install` | **Yes — blocking** | Yes | 2026-07-19 |
64
+ | Cursor | `code-audit install --agent cursor` (project-only) | **Advisory** | Yes | 2026-07-19 |
65
+ | Codex | `code-audit install --agent codex` + plugin | **Yes — blocking** | Yes | 2026-07-19 |
66
+ | Gemini CLI | `code-audit install --agent gemini` | No | Yes | 2026-07-19 |
67
+ | VS Code / Copilot | `code-audit install --agent agents` | No | Yes | 2026-07-19 |
68
+ | Other SKILL.md tools | `code-audit install --agent agents` | No | Yes | 2026-07-19 |
69
+
70
+ Hook behavior: **Blocking** means violations at or above `--fail-on` severity prevent the edit from landing (the agent sees the violation and fixes inline). **Advisory** means violations are reported through the strongest available feedback channel but the edit has already occurred. Cursor's `afterFileEdit` hook is fire-and-forget with no output consumption. MCP is available everywhere for shell-less use.
71
+
42
72
  ## License
43
73
 
44
74
  MIT
package/dist/cli.js CHANGED
@@ -12,9 +12,8 @@ import { promises as fs } from 'fs';
12
12
  import { createInterface } from 'readline';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { dirname, isAbsolute, join, relative, resolve } from 'path';
15
- import { ConfigGeneratorFactory } from './generators/ConfigGeneratorFactory.js';
16
- import { InteractivePrompts } from './ui/InteractivePrompts.js';
17
- import { DEFAULT_SERVER_URL, DEFAULT_PORT } from './constants.js';
15
+ import { DEFAULT_PORT } from './constants.js';
16
+ import inquirer from 'inquirer';
18
17
  import { CodeMapGenerator } from './services/CodeMapGenerator.js';
19
18
  import { initParsers } from './languages/index.js';
20
19
  import { queryParser } from './search/QueryParser.js';
@@ -37,11 +36,19 @@ program
37
36
  .option('-c, --config <config>', 'Configuration name')
38
37
  .option('-o, --output <dir>', 'Output directory for reports')
39
38
  .option('-f, --format <format>', 'Report format: html, json, csv, or sarif')
39
+ .option('--fail-on <severity>', 'Exit code 2 when violations at or above this severity exist')
40
40
  .action(async (options) => {
41
41
  console.log(chalk.blue('🔍 Code Quality Audit Tool'));
42
42
  console.log(chalk.gray('══════════════════════════════════════════════════'));
43
43
  try {
44
44
  await initParsers();
45
+ // Validate --fail-on severity
46
+ const validSeverities = ['critical', 'warning', 'suggestion'];
47
+ const failOnSeverity = options.failOn;
48
+ if (failOnSeverity && !validSeverities.includes(failOnSeverity)) {
49
+ console.error(chalk.red(`Invalid --fail-on severity: "${failOnSeverity}". Must be one of: ${validSeverities.join(', ')}`));
50
+ process.exit(1);
51
+ }
45
52
  const runner = createAuditRunner({
46
53
  projectRoot: options.path,
47
54
  configName: options.config,
@@ -67,6 +74,19 @@ program
67
74
  await fs.writeFile(reportPath, report, 'utf-8');
68
75
  console.log(chalk.green(`\nReport written to ${reportPath}`));
69
76
  }
77
+ // Exit code based on --fail-on
78
+ if (failOnSeverity) {
79
+ const violations = Object.values(result.analyzerResults).flatMap((r) => r.violations || []);
80
+ const severityOrder = ['critical', 'warning', 'suggestion'];
81
+ const failIndex = severityOrder.indexOf(failOnSeverity);
82
+ const hasAtOrAbove = violations.some((v) => {
83
+ const vIndex = severityOrder.indexOf(v.severity);
84
+ return vIndex >= 0 && vIndex <= failIndex;
85
+ });
86
+ if (hasAtOrAbove) {
87
+ process.exit(2);
88
+ }
89
+ }
70
90
  }
71
91
  catch (error) {
72
92
  console.error(chalk.red('Error:'), error);
@@ -204,15 +224,13 @@ program
204
224
  program
205
225
  .command('generate-config')
206
226
  .alias('gen')
207
- .description('Generate configuration for AI coding assistants')
208
- .option('-t, --tool <tool>', 'Specific tool (cursor, continue, copilot, awsq, codeium, claude, all)')
227
+ .description('Generate a .codeauditor.json scaffold with invariant rules')
209
228
  .option('-o, --output <dir>', 'Output directory', '.')
210
- .option('-s, --server-url <url>', 'MCP server URL', DEFAULT_SERVER_URL)
211
- .option('-i, --interactive', 'Interactive mode for tool selection')
212
- .option('-f, --force', 'Force overwrite existing files without confirmation')
213
- .option('-y, --yes', 'Skip all confirmation prompts (same as --force)')
229
+ .option('-i, --interactive', 'Interactive rule builder')
230
+ .option('-f, --force', 'Force overwrite existing file without confirmation')
231
+ .option('-y, --yes', 'Skip confirmation prompts (same as --force)')
214
232
  .action(async (options) => {
215
- console.log(chalk.blue('🛠️ AI Tool Configuration Generator'));
233
+ console.log(chalk.blue('🛠️ Code Auditor Config Generator'));
216
234
  console.log(chalk.gray('════════════════════════════════════════════════════'));
217
235
  try {
218
236
  await generateConfigurations(options);
@@ -770,132 +788,419 @@ tasksCmd
770
788
  process.exit(1);
771
789
  }
772
790
  });
791
+ // Install command — copy skill folder to agent-specific paths
792
+ program
793
+ .command('install')
794
+ .description('Install the code-auditor skill for AI coding tools')
795
+ .option('--agent <agent>', 'Target agent: claude, cursor, codex, gemini, agents, or all', 'all')
796
+ .option('--scope <scope>', 'Install scope: user (~) or project (.)', 'user')
797
+ .option('--hooks', 'Offer hook wiring (default)', true)
798
+ .option('--no-hooks', 'Skip hook wiring prompt')
799
+ .option('--list', 'Print the support matrix and exit')
800
+ .action(async (options) => {
801
+ try {
802
+ const { runInstall } = await import('./installer.js');
803
+ await runInstall({
804
+ agent: options.agent,
805
+ scope: options.scope,
806
+ hooks: options.hooks,
807
+ list: options.list,
808
+ });
809
+ }
810
+ catch (error) {
811
+ console.error(chalk.red('Error:'), error);
812
+ process.exit(1);
813
+ }
814
+ });
815
+ // Cursor hook adapter — postToolUse (Write matcher) → scoped audit → additional_context + exit 2
816
+ // Verified 2026-07-20: cursor.com/docs/hooks + cursor.com/docs/reference/third-party-hooks
817
+ program
818
+ .command('cursor-hook')
819
+ .description('Internal: Cursor postToolUse hook adapter (blocks edits with critical violations via additional_context)')
820
+ .action(async () => {
821
+ try {
822
+ await import('./hooks/cursor.js');
823
+ }
824
+ catch (error) {
825
+ console.error(chalk.red('Error:'), error);
826
+ process.exit(1);
827
+ }
828
+ });
829
+ // Codex hook adapter — stdin JSON → diff-scoped audit → stdout JSON (blocking)
830
+ program
831
+ .command('codex-hook')
832
+ .description('Internal: Codex PostToolUse hook adapter (reads stdin JSON, runs audit, exits 2 on critical violations)')
833
+ .action(async () => {
834
+ try {
835
+ await import('./hooks/codex.js');
836
+ }
837
+ catch (error) {
838
+ console.error(chalk.red('Error:'), error);
839
+ process.exit(1);
840
+ }
841
+ });
773
842
  // Parse command line arguments
774
843
  program.parse(process.argv);
775
844
  // If no command was provided, show help
776
845
  if (!process.argv.slice(2).length) {
777
846
  program.outputHelp();
778
847
  }
779
- /**
780
- * Generate configurations for AI tools
781
- */
848
+ const RULE_KINDS = [
849
+ {
850
+ kind: 'import-ban',
851
+ label: 'Import Ban — forbid importing a specific module',
852
+ description: 'No file may import the banned module (e.g. deprecated packages, legacy libraries).',
853
+ requiredFields: ['module'],
854
+ },
855
+ {
856
+ kind: 'call-constraint',
857
+ label: 'Call Constraint — restrict who can call a function',
858
+ description: 'Only allow or deny specific callers from invoking a function.',
859
+ requiredFields: ['callee'],
860
+ },
861
+ {
862
+ kind: 'module-boundary',
863
+ label: 'Module Boundary — enforce layer isolation',
864
+ description: 'Files matching a "from" glob may not import from files matching a "to" glob.',
865
+ requiredFields: ['from', 'to'],
866
+ },
867
+ {
868
+ kind: 'naming',
869
+ label: 'Naming — enforce export naming conventions',
870
+ description: 'Exported symbols in matching files must match a regex pattern.',
871
+ requiredFields: ['path', 'exports'],
872
+ },
873
+ {
874
+ kind: 'ast-pattern',
875
+ label: 'AST Pattern — ban syntactic patterns',
876
+ description: 'Match AST nodes using ast-grep patterns (e.g. "new Function($$$)").',
877
+ requiredFields: ['pattern'],
878
+ },
879
+ ];
880
+ const SCAFFOLD_CONFIG = {
881
+ $schema: 'https://unpkg.com/code-auditor-mcp/dist/invariant-rules.schema.json',
882
+ rules: [
883
+ {
884
+ id: 'ban-deprecated-lib',
885
+ kind: 'import-ban',
886
+ severity: 'critical',
887
+ message: 'This module is deprecated — prefer the replacement instead.',
888
+ module: 'deprecated-lib',
889
+ },
890
+ {
891
+ id: 'data-layer-no-browser',
892
+ kind: 'module-boundary',
893
+ severity: 'critical',
894
+ message: 'Data access layer must not import from browser-only modules.',
895
+ from: 'src/data/**',
896
+ to: 'src/browser/**',
897
+ },
898
+ {
899
+ id: 'api-routes-naming',
900
+ kind: 'naming',
901
+ severity: 'warning',
902
+ message: 'API route files must export a handler matching the HTTP method.',
903
+ path: 'src/api/**',
904
+ exports: '^(get|post|put|delete|patch)\\b',
905
+ },
906
+ {
907
+ id: 'no-eval',
908
+ kind: 'ast-pattern',
909
+ severity: 'critical',
910
+ pattern: 'eval($$$)',
911
+ message: 'eval() is forbidden in this codebase.',
912
+ },
913
+ ],
914
+ };
782
915
  async function generateConfigurations(options) {
783
- const prompts = new InteractivePrompts();
784
- let tools = [];
785
- let serverUrl = options.serverUrl;
786
- let outputDir = options.output;
787
- // Determine which tools to configure
788
- if (options.interactive && !options.tool) {
789
- // Interactive mode
790
- tools = await prompts.selectTools();
791
- if (!options.force && !options.yes) {
792
- serverUrl = await prompts.confirmServerUrl(serverUrl);
793
- outputDir = await prompts.selectOutputDirectory(outputDir);
794
- }
795
- }
796
- else if (!options.tool) {
797
- // No tool specified and not interactive - show available tools
798
- const factory = new ConfigGeneratorFactory(serverUrl);
799
- const availableTools = factory.getToolInfo();
800
- console.log(chalk.yellow('No tool specified. Available tools:'));
801
- availableTools.forEach(tool => {
802
- console.log(chalk.gray(` • ${tool.name} - ${tool.displayName}`));
916
+ const outputDir = resolve(options.output || '.');
917
+ const outputPath = join(outputDir, '.codeauditor.json');
918
+ let config;
919
+ if (options.interactive) {
920
+ // --- Interactive rule builder ---
921
+ console.log(chalk.cyan('\nBuild your .codeauditor.json interactively.\n'));
922
+ const { addRules } = await inquirer.prompt({
923
+ addRules: {
924
+ type: 'confirm',
925
+ message: 'Would you like to add invariant rules?',
926
+ default: true,
927
+ },
803
928
  });
804
- console.log(chalk.blue('\nUsage examples:'));
805
- console.log(chalk.gray(' code-auditor gen --tool cursor'));
806
- console.log(chalk.gray(' code-auditor gen --tool cursor,claude,continue'));
807
- console.log(chalk.gray(' code-auditor gen --tool all'));
808
- console.log(chalk.gray(' code-auditor gen --interactive'));
809
- return;
810
- }
811
- else {
812
- // Command line mode
813
- if (options.tool === 'all') {
814
- const factory = new ConfigGeneratorFactory(serverUrl);
815
- tools = factory.getAvailableTools();
929
+ if (!addRules) {
930
+ console.log(chalk.yellow('No rules selected. Writing empty config.'));
931
+ config = { $schema: SCAFFOLD_CONFIG.$schema, rules: [] };
816
932
  }
817
933
  else {
818
- tools = options.tool.split(',').map((t) => t.trim());
934
+ config = await buildRulesInteractively();
819
935
  }
820
- }
821
- console.log(chalk.blue(`\nGenerating configurations for: ${tools.join(', ')}`));
822
- console.log(chalk.gray(`Server URL: ${serverUrl}`));
823
- console.log(chalk.gray(`Output directory: ${outputDir}\n`));
824
- // Create factory
825
- const factory = new ConfigGeneratorFactory(serverUrl);
826
- const generatedFiles = [];
827
- const errors = [];
828
- // Check for existing files
829
- const existingFiles = [];
830
- for (const tool of tools) {
831
- const generator = factory.createGenerator(tool);
832
- if (generator) {
833
- const config = generator.generateConfig();
834
- const outputPath = resolve(outputDir, config.filename);
936
+ // Confirm output directory
937
+ if (!options.force && !options.yes) {
938
+ const { dir } = await inquirer.prompt({
939
+ dir: {
940
+ type: 'input',
941
+ message: 'Output directory:',
942
+ default: outputDir,
943
+ },
944
+ });
945
+ // Re-resolve with the user's choice (they might just hit enter)
946
+ const chosenDir = resolve(dir || outputDir);
947
+ const chosenPath = join(chosenDir, '.codeauditor.json');
948
+ // Check for existing file
949
+ let exists = false;
835
950
  try {
836
- await fs.access(outputPath);
837
- existingFiles.push(config.filename);
951
+ await fs.access(chosenPath);
952
+ exists = true;
838
953
  }
839
- catch {
840
- // File doesn't exist, which is fine
954
+ catch { /* ok */ }
955
+ if (exists) {
956
+ const { overwrite } = await inquirer.prompt({
957
+ overwrite: {
958
+ type: 'confirm',
959
+ message: chalk.yellow(`.codeauditor.json already exists at ${chosenPath}. Overwrite?`),
960
+ default: false,
961
+ },
962
+ });
963
+ if (!overwrite) {
964
+ console.log(chalk.yellow('Operation cancelled.'));
965
+ return;
966
+ }
841
967
  }
968
+ await writeConfigFile(chosenPath, config);
969
+ }
970
+ else {
971
+ await writeConfigFile(outputPath, config);
842
972
  }
843
973
  }
844
- // Confirm overwrite if needed
845
- if (existingFiles.length > 0 && !options.force && !options.yes) {
846
- const shouldOverwrite = await prompts.confirmOverwrite(existingFiles);
847
- if (!shouldOverwrite) {
848
- console.log(chalk.yellow('Operation cancelled.'));
974
+ else {
975
+ // --- Non-interactive: scaffold template ---
976
+ let exists = false;
977
+ try {
978
+ await fs.access(outputPath);
979
+ exists = true;
980
+ }
981
+ catch { /* ok */ }
982
+ if (exists && !options.force && !options.yes) {
983
+ console.log(chalk.yellow(`.codeauditor.json already exists at ${outputPath}`));
984
+ console.log(chalk.gray('Use --force or --yes to overwrite, or --interactive to build a custom config.'));
849
985
  return;
850
986
  }
987
+ if (exists && (options.force || options.yes)) {
988
+ console.log(chalk.yellow('Overwriting existing .codeauditor.json...'));
989
+ }
990
+ config = SCAFFOLD_CONFIG;
991
+ await writeConfigFile(outputPath, config);
851
992
  }
852
- else if (existingFiles.length > 0 && (options.force || options.yes)) {
853
- console.log(chalk.yellow(`Overwriting ${existingFiles.length} existing file(s)...`));
854
- }
855
- // Generate configurations
856
- for (const tool of tools) {
857
- try {
858
- const generator = factory.createGenerator(tool);
859
- if (!generator) {
860
- errors.push(`Unknown tool: ${tool}`);
861
- continue;
993
+ console.log(chalk.blue('\nNext steps:'));
994
+ console.log(chalk.gray(' 1. Edit .codeauditor.json to match your codebase conventions'));
995
+ console.log(chalk.gray(' 2. Run ') + chalk.cyan('code-audit') + chalk.gray(' to enforce your rules'));
996
+ console.log(chalk.gray(' 3. Use ') + chalk.cyan('code-audit changed --fail-on critical') + chalk.gray(' in your agent hook'));
997
+ }
998
+ /**
999
+ * Interactive rule builder — walks the user through adding rules one at a time.
1000
+ */
1001
+ async function buildRulesInteractively() {
1002
+ const rules = [];
1003
+ let addMore = true;
1004
+ while (addMore) {
1005
+ console.log(chalk.gray(`\n── Rule ${rules.length + 1} ──`));
1006
+ // Select rule kind
1007
+ const { kind } = await inquirer.prompt({
1008
+ kind: {
1009
+ type: 'list',
1010
+ message: 'Select rule kind:',
1011
+ choices: RULE_KINDS.map((k) => ({
1012
+ name: k.label,
1013
+ value: k.kind,
1014
+ })),
1015
+ pageSize: 10,
1016
+ },
1017
+ });
1018
+ const kindInfo = RULE_KINDS.find((k) => k.kind === kind);
1019
+ console.log(chalk.dim(kindInfo.description));
1020
+ // Common fields
1021
+ const common = await inquirer.prompt({
1022
+ id: {
1023
+ type: 'input',
1024
+ message: 'Rule ID (unique kebab-case identifier):',
1025
+ validate: (input) => {
1026
+ if (!input.trim())
1027
+ return 'Rule ID is required';
1028
+ if (!/^[a-z][a-z0-9-]*$/.test(input))
1029
+ return 'Use kebab-case (lowercase, digits, hyphens)';
1030
+ return true;
1031
+ },
1032
+ },
1033
+ severity: {
1034
+ type: 'list',
1035
+ message: 'Severity:',
1036
+ choices: [
1037
+ { name: chalk.red('Critical — exit code 2, blocks the agent loop'), value: 'critical' },
1038
+ { name: chalk.yellow('Warning — visible, non-blocking'), value: 'warning' },
1039
+ { name: chalk.blue('Suggestion — informational'), value: 'suggestion' },
1040
+ ],
1041
+ default: 'warning',
1042
+ },
1043
+ message: {
1044
+ type: 'input',
1045
+ message: 'Violation message (shown when rule is broken):',
1046
+ validate: (input) => input.trim() ? true : 'Message is required',
1047
+ },
1048
+ });
1049
+ const rule = {
1050
+ id: common.id,
1051
+ kind,
1052
+ severity: common.severity,
1053
+ message: common.message,
1054
+ };
1055
+ // Kind-specific fields
1056
+ switch (kind) {
1057
+ case 'import-ban': {
1058
+ const { module } = await inquirer.prompt({
1059
+ module: {
1060
+ type: 'input',
1061
+ message: 'Banned module specifier (e.g. "lodash" or "@old-lib/*"):',
1062
+ validate: (input) => input.trim() ? true : 'Module specifier is required',
1063
+ },
1064
+ });
1065
+ rule.module = module;
1066
+ const { addExcept } = await inquirer.prompt({
1067
+ addExcept: {
1068
+ type: 'confirm',
1069
+ message: 'Add exception paths (files allowed to import it)?',
1070
+ default: false,
1071
+ },
1072
+ });
1073
+ if (addExcept) {
1074
+ const { except } = await inquirer.prompt({
1075
+ except: {
1076
+ type: 'input',
1077
+ message: 'Exception globs (comma-separated, e.g. "src/migration/**"):',
1078
+ },
1079
+ });
1080
+ const exceptList = except.split(',').map((s) => s.trim()).filter(Boolean);
1081
+ if (exceptList.length > 0)
1082
+ rule.except = exceptList;
1083
+ }
1084
+ break;
1085
+ }
1086
+ case 'call-constraint': {
1087
+ const { callee } = await inquirer.prompt({
1088
+ callee: {
1089
+ type: 'input',
1090
+ message: 'Callee (function name, optionally path-qualified as "path/glob#name"):',
1091
+ validate: (input) => input.trim() ? true : 'Callee is required',
1092
+ },
1093
+ });
1094
+ rule.callee = callee;
1095
+ const { mode } = await inquirer.prompt({
1096
+ mode: {
1097
+ type: 'list',
1098
+ message: 'Restriction mode:',
1099
+ choices: [
1100
+ { name: 'Allow only specific callers (allowFrom)', value: 'allow' },
1101
+ { name: 'Deny specific callers (denyFrom)', value: 'deny' },
1102
+ ],
1103
+ },
1104
+ });
1105
+ const { paths } = await inquirer.prompt({
1106
+ paths: {
1107
+ type: 'input',
1108
+ message: `Path globs (comma-separated) for ${mode === 'allow' ? 'allowFrom' : 'denyFrom'}:`,
1109
+ validate: (input) => input.trim() ? true : 'At least one path glob is required',
1110
+ },
1111
+ });
1112
+ const pathList = paths.split(',').map((s) => s.trim()).filter(Boolean);
1113
+ if (mode === 'allow') {
1114
+ rule.allowFrom = pathList;
1115
+ }
1116
+ else {
1117
+ rule.denyFrom = pathList;
1118
+ }
1119
+ break;
1120
+ }
1121
+ case 'module-boundary': {
1122
+ const { from, to } = await inquirer.prompt({
1123
+ from: {
1124
+ type: 'input',
1125
+ message: 'From (path glob for files that must not import):',
1126
+ validate: (input) => input.trim() ? true : '"from" path glob is required',
1127
+ },
1128
+ to: {
1129
+ type: 'input',
1130
+ message: 'To (path glob for files that must not be imported):',
1131
+ validate: (input) => input.trim() ? true : '"to" path glob is required',
1132
+ },
1133
+ });
1134
+ rule.from = from;
1135
+ rule.to = to;
1136
+ break;
862
1137
  }
863
- const config = generator.generateConfig();
864
- const outputPath = resolve(outputDir, config.filename);
865
- // Ensure directory exists
866
- await fs.mkdir(dirname(outputPath), { recursive: true });
867
- // Write main config file
868
- await fs.writeFile(outputPath, config.content);
869
- generatedFiles.push(config.filename);
870
- // Write additional files if any
871
- if (config.additionalFiles) {
872
- for (const additionalFile of config.additionalFiles) {
873
- const additionalPath = resolve(outputDir, additionalFile.filename);
874
- await fs.mkdir(dirname(additionalPath), { recursive: true });
875
- await fs.writeFile(additionalPath, additionalFile.content);
876
- generatedFiles.push(additionalFile.filename);
1138
+ case 'naming': {
1139
+ const { path, exports: exportPattern } = await inquirer.prompt({
1140
+ path: {
1141
+ type: 'input',
1142
+ message: 'Path (glob for files this rule applies to):',
1143
+ validate: (input) => input.trim() ? true : 'Path glob is required',
1144
+ },
1145
+ exports: {
1146
+ type: 'input',
1147
+ message: 'Exports regex (exported symbols must match, e.g. "^use[A-Z]"):',
1148
+ validate: (input) => input.trim() ? true : 'Exports regex is required',
1149
+ },
1150
+ });
1151
+ rule.path = path;
1152
+ rule.exports = exportPattern;
1153
+ break;
1154
+ }
1155
+ case 'ast-pattern': {
1156
+ const { pattern } = await inquirer.prompt({
1157
+ pattern: {
1158
+ type: 'input',
1159
+ message: 'AST pattern (ast-grep syntax, e.g. "new Function($$$)"):',
1160
+ validate: (input) => input.trim() ? true : 'Pattern is required',
1161
+ },
1162
+ });
1163
+ rule.pattern = pattern;
1164
+ const { addLanguage } = await inquirer.prompt({
1165
+ addLanguage: {
1166
+ type: 'confirm',
1167
+ message: 'Restrict to a specific language? (default: typescript)',
1168
+ default: false,
1169
+ },
1170
+ });
1171
+ if (addLanguage) {
1172
+ const { language } = await inquirer.prompt({
1173
+ language: {
1174
+ type: 'list',
1175
+ message: 'Language:',
1176
+ choices: ['typescript', 'javascript', 'go'],
1177
+ },
1178
+ });
1179
+ rule.language = language;
877
1180
  }
1181
+ break;
878
1182
  }
879
- // Show success and instructions
880
- console.log(chalk.green(`✓ Generated ${generator.getToolName()} configuration: ${config.filename}`));
881
- console.log(chalk.dim('Instructions:'));
882
- console.log(chalk.gray(config.instructions.trim()));
883
- console.log('');
884
1183
  }
885
- catch (error) {
886
- errors.push(`Failed to generate config for ${tool}: ${error}`);
887
- }
888
- }
889
- // Show summary
890
- if (generatedFiles.length > 0) {
891
- prompts.displaySuccess(generatedFiles);
892
- }
893
- if (errors.length > 0) {
894
- console.log(chalk.red('\nErrors:'));
895
- errors.forEach(error => {
896
- console.log(chalk.red(` • ${error}`));
1184
+ rules.push(rule);
1185
+ console.log(chalk.green(` Added rule "${rule.id}" [${rule.kind}]`));
1186
+ const { cont } = await inquirer.prompt({
1187
+ cont: {
1188
+ type: 'confirm',
1189
+ message: 'Add another rule?',
1190
+ default: true,
1191
+ },
897
1192
  });
1193
+ addMore = cont;
898
1194
  }
1195
+ return {
1196
+ $schema: 'https://unpkg.com/code-auditor-mcp/dist/invariant-rules.schema.json',
1197
+ rules,
1198
+ };
1199
+ }
1200
+ async function writeConfigFile(outputPath, config) {
1201
+ await fs.mkdir(dirname(outputPath), { recursive: true });
1202
+ await fs.writeFile(outputPath, JSON.stringify(config, null, 2) + '\n');
1203
+ console.log(chalk.green(`\n✓ Written .codeauditor.json to ${outputPath}`));
899
1204
  }
900
1205
  /**
901
1206
  * Generate code map for a project