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.
- package/.claude-plugin/marketplace.json +1 -1
- package/README.md +31 -1
- package/dist/cli.js +416 -111
- package/dist/cli.js.map +1 -1
- package/dist/generators/AWSQConfigGenerator.d.ts +20 -0
- package/dist/generators/AWSQConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AWSQConfigGenerator.js +48 -19
- package/dist/generators/AWSQConfigGenerator.js.map +1 -1
- package/dist/generators/AiderConfigGenerator.d.ts +18 -0
- package/dist/generators/AiderConfigGenerator.d.ts.map +1 -1
- package/dist/generators/AiderConfigGenerator.js +48 -10
- package/dist/generators/AiderConfigGenerator.js.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.d.ts +9 -3
- package/dist/generators/ClaudeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClaudeConfigGenerator.js +30 -42
- package/dist/generators/ClaudeConfigGenerator.js.map +1 -1
- package/dist/generators/ClineConfigGenerator.d.ts +13 -0
- package/dist/generators/ClineConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ClineConfigGenerator.js +44 -11
- package/dist/generators/ClineConfigGenerator.js.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.d.ts +15 -0
- package/dist/generators/CodeiumConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CodeiumConfigGenerator.js +45 -15
- package/dist/generators/CodeiumConfigGenerator.js.map +1 -1
- package/dist/generators/CodexConfigGenerator.d.ts +29 -0
- package/dist/generators/CodexConfigGenerator.d.ts.map +1 -0
- package/dist/generators/CodexConfigGenerator.js +80 -0
- package/dist/generators/CodexConfigGenerator.js.map +1 -0
- package/dist/generators/ConfigGeneratorFactory.d.ts +10 -1
- package/dist/generators/ConfigGeneratorFactory.d.ts.map +1 -1
- package/dist/generators/ConfigGeneratorFactory.js +22 -8
- package/dist/generators/ConfigGeneratorFactory.js.map +1 -1
- package/dist/generators/ContinueConfigGenerator.d.ts +16 -1
- package/dist/generators/ContinueConfigGenerator.d.ts.map +1 -1
- package/dist/generators/ContinueConfigGenerator.js +41 -73
- package/dist/generators/ContinueConfigGenerator.js.map +1 -1
- package/dist/generators/CopilotConfigGenerator.d.ts +13 -2
- package/dist/generators/CopilotConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CopilotConfigGenerator.js +31 -51
- package/dist/generators/CopilotConfigGenerator.js.map +1 -1
- package/dist/generators/CursorConfigGenerator.d.ts +8 -1
- package/dist/generators/CursorConfigGenerator.d.ts.map +1 -1
- package/dist/generators/CursorConfigGenerator.js +27 -51
- package/dist/generators/CursorConfigGenerator.js.map +1 -1
- package/dist/generators/GeminiConfigGenerator.d.ts +23 -0
- package/dist/generators/GeminiConfigGenerator.d.ts.map +1 -0
- package/dist/generators/GeminiConfigGenerator.js +59 -0
- package/dist/generators/GeminiConfigGenerator.js.map +1 -0
- package/dist/generators/JetBrainsConfigGenerator.d.ts +11 -0
- package/dist/generators/JetBrainsConfigGenerator.d.ts.map +1 -1
- package/dist/generators/JetBrainsConfigGenerator.js +39 -21
- package/dist/generators/JetBrainsConfigGenerator.js.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.d.ts +14 -0
- package/dist/generators/VSCodeConfigGenerator.d.ts.map +1 -1
- package/dist/generators/VSCodeConfigGenerator.js +43 -15
- package/dist/generators/VSCodeConfigGenerator.js.map +1 -1
- package/dist/hooks/codex.d.ts +20 -0
- package/dist/hooks/codex.d.ts.map +1 -0
- package/dist/hooks/codex.js +91 -0
- package/dist/hooks/codex.js.map +1 -0
- package/dist/hooks/core.d.ts +62 -0
- package/dist/hooks/core.d.ts.map +1 -0
- package/dist/hooks/core.js +119 -0
- package/dist/hooks/core.js.map +1 -0
- package/dist/hooks/cursor.d.ts +72 -0
- package/dist/hooks/cursor.d.ts.map +1 -0
- package/dist/hooks/cursor.js +163 -0
- package/dist/hooks/cursor.js.map +1 -0
- package/dist/installer.d.ts +30 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +355 -0
- package/dist/installer.js.map +1 -0
- package/dist/mcp-tools/whitelistTools.d.ts +1 -1
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- 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.
|
|
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
|
|
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 {
|
|
16
|
-
import
|
|
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
|
|
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('-
|
|
211
|
-
.option('-
|
|
212
|
-
.option('-
|
|
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('🛠️
|
|
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
|
-
|
|
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
|
|
784
|
-
|
|
785
|
-
let
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
934
|
+
config = await buildRulesInteractively();
|
|
819
935
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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(
|
|
837
|
-
|
|
951
|
+
await fs.access(chosenPath);
|
|
952
|
+
exists = true;
|
|
838
953
|
}
|
|
839
|
-
catch {
|
|
840
|
-
|
|
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
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|