eslint-plugin-ai-guard 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/dist/cli/index.js +54 -13
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/mcp/server.js +4428 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -33,7 +33,14 @@
|
|
|
33
33
|
|
|
34
34
|
## What is AI Guard?
|
|
35
35
|
|
|
36
|
-
**AI Guard** (`eslint-plugin-ai-guard`)
|
|
36
|
+
**AI Guard** (`eslint-plugin-ai-guard`) provides:
|
|
37
|
+
- **ESLint plugin** with 18 deterministic rules and 4 presets (`recommended`, `strict`, `security`, `agent`)
|
|
38
|
+
- **CLI** (`ai-guard`) for terminal, changed-file, and CI scanning
|
|
39
|
+
- **GitHub Action** with native SARIF 2.1.0 output for GitHub Code Scanning
|
|
40
|
+
- **Claude Code integration** with a zero-config PostToolUse hook
|
|
41
|
+
- **MCP server** (`ai-guard-mcp`) for local AI-agent workflows (Claude Code, Claude Desktop, Antigravity)
|
|
42
|
+
|
|
43
|
+
It is engineered to detect reliability bugs, async hazards, security vulnerabilities, and code-scaffolding defects frequently introduced during AI-assisted development (GitHub Copilot, Cursor, Claude Code, Gemini Code Assist, etc.).
|
|
37
44
|
|
|
38
45
|
AI Guard analyzes Abstract Syntax Trees (AST) using ESLint's native engine. It runs locally in your editor, in your terminal via the zero-config CLI, and in your CI/CD pipelines via native SARIF 2.1.0 integration with GitHub Code Scanning.
|
|
39
46
|
|
|
@@ -87,7 +94,7 @@ flowchart TD
|
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
subgraph Tiers["3. Classification & Presets"]
|
|
90
|
-
E --> F{"Active Preset\n(recommended | strict | security)"}
|
|
97
|
+
E --> F{"Active Preset\n(recommended | strict | security | agent)"}
|
|
91
98
|
F --> G["Confidence Tiering & AST Filtering"]
|
|
92
99
|
end
|
|
93
100
|
|
|
@@ -614,6 +621,39 @@ For the complete methodology, raw findings, and benchmark harness, see [**`docs/
|
|
|
614
621
|
|
|
615
622
|
---
|
|
616
623
|
|
|
624
|
+
## MCP Server (Model Context Protocol)
|
|
625
|
+
|
|
626
|
+
AI Guard includes a local MCP server that exposes its deterministic analysis engine to MCP-capable AI clients (Claude Code, Claude Desktop, and other MCP-compatible integrations).
|
|
627
|
+
|
|
628
|
+
```bash
|
|
629
|
+
npx ai-guard-mcp
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**Available tools:**
|
|
633
|
+
|
|
634
|
+
| Tool | Description |
|
|
635
|
+
| :--- | :--- |
|
|
636
|
+
| `ai_guard_scan_file` | Scan a single JS/TS file with structured findings |
|
|
637
|
+
| `ai_guard_scan_diff` | Scan changed files in git diff (including newly created untracked JS/TS files) |
|
|
638
|
+
| `ai_guard_rules` | List available rules and presets |
|
|
639
|
+
|
|
640
|
+
**Claude Code configuration** (`.claude/mcp.json`):
|
|
641
|
+
|
|
642
|
+
```json
|
|
643
|
+
{
|
|
644
|
+
"mcpServers": {
|
|
645
|
+
"ai-guard": {
|
|
646
|
+
"command": "npx",
|
|
647
|
+
"args": ["ai-guard-mcp"]
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
For detailed documentation, tool schemas, and security model, see [**`docs/mcp-server.md`**](./docs/mcp-server.md).
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
617
657
|
## Learn More & Ecosystem
|
|
618
658
|
|
|
619
659
|
Visit [getaiguard.dev](https://getaiguard.dev) to explore interactive documentation, rule catalogs, benchmarks, and deep-dive engineering articles.
|
package/dist/cli/index.js
CHANGED
|
@@ -1247,6 +1247,7 @@ var AGENT_RULES = {
|
|
|
1247
1247
|
};
|
|
1248
1248
|
|
|
1249
1249
|
// cli/utils/eslint-runner.ts
|
|
1250
|
+
var import_meta = {};
|
|
1250
1251
|
var ISSUE_CONFIDENCE = {
|
|
1251
1252
|
// HIGH — near-certain bugs: false positive rate <2% in real-world codebases
|
|
1252
1253
|
"ai-guard/no-hardcoded-secret": "high",
|
|
@@ -1489,6 +1490,24 @@ async function loadPluginModuleFromCwd(cwd) {
|
|
|
1489
1490
|
log.debug(`Local src plugin import failed: ${reason}`);
|
|
1490
1491
|
}
|
|
1491
1492
|
}
|
|
1493
|
+
try {
|
|
1494
|
+
const selfRequire = createRequire3(import_meta.url);
|
|
1495
|
+
const selfResolved = selfRequire.resolve("eslint-plugin-ai-guard");
|
|
1496
|
+
return import((0, import_url.pathToFileURL)(selfResolved).href);
|
|
1497
|
+
} catch {
|
|
1498
|
+
}
|
|
1499
|
+
try {
|
|
1500
|
+
const pkgRoot = import_path.default.resolve(import_path.default.dirname((0, import_url.fileURLToPath)(import_meta.url)), "..", "..");
|
|
1501
|
+
const selfDist = import_path.default.join(pkgRoot, "dist", "index.js");
|
|
1502
|
+
if (import_fs.default.existsSync(selfDist)) {
|
|
1503
|
+
return import((0, import_url.pathToFileURL)(selfDist).href);
|
|
1504
|
+
}
|
|
1505
|
+
const selfSrc = import_path.default.join(pkgRoot, "src", "index.ts");
|
|
1506
|
+
if (import_fs.default.existsSync(selfSrc)) {
|
|
1507
|
+
return import((0, import_url.pathToFileURL)(selfSrc).href);
|
|
1508
|
+
}
|
|
1509
|
+
} catch {
|
|
1510
|
+
}
|
|
1492
1511
|
throw new Error(
|
|
1493
1512
|
"eslint-plugin-ai-guard is not installed. Run: npm install --save-dev eslint-plugin-ai-guard"
|
|
1494
1513
|
);
|
|
@@ -1772,13 +1791,13 @@ var import_path3 = __toESM(require("path"));
|
|
|
1772
1791
|
var import_module = require("module");
|
|
1773
1792
|
var import_url2 = require("url");
|
|
1774
1793
|
var import_path2 = __toESM(require("path"));
|
|
1775
|
-
var
|
|
1794
|
+
var import_meta2 = {};
|
|
1776
1795
|
var _version;
|
|
1777
1796
|
function readVersion() {
|
|
1778
1797
|
try {
|
|
1779
|
-
const require_ = (0, import_module.createRequire)(
|
|
1798
|
+
const require_ = (0, import_module.createRequire)(import_meta2.url);
|
|
1780
1799
|
const pkgPath = import_path2.default.resolve(
|
|
1781
|
-
import_path2.default.dirname((0, import_url2.fileURLToPath)(
|
|
1800
|
+
import_path2.default.dirname((0, import_url2.fileURLToPath)(import_meta2.url)),
|
|
1782
1801
|
"../../package.json"
|
|
1783
1802
|
);
|
|
1784
1803
|
const pkg = require_(pkgPath);
|
|
@@ -2534,7 +2553,7 @@ function tryPrDiff(branch, cwd, debugInfo) {
|
|
|
2534
2553
|
const mergeBase = runGit(["merge-base", "HEAD", rawBranch], cwd);
|
|
2535
2554
|
if (mergeBase) {
|
|
2536
2555
|
if (!debugInfo.mergeBase) debugInfo.mergeBase = mergeBase;
|
|
2537
|
-
const out = runGit(
|
|
2556
|
+
const out = runGit(["diff", "--name-only", "--diff-filter=ACMRT", mergeBase], cwd);
|
|
2538
2557
|
if (out !== null) {
|
|
2539
2558
|
debugInfo.strategySucceeded = strategy;
|
|
2540
2559
|
return { output: out, strategy, mergeBase };
|
|
@@ -2573,6 +2592,7 @@ function tryPrDiff(branch, cwd, debugInfo) {
|
|
|
2573
2592
|
}
|
|
2574
2593
|
function filterFiles(rawFiles, extensions, workingDirectory, cwd, debugInfo) {
|
|
2575
2594
|
const kept = [];
|
|
2595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2576
2596
|
for (const rel of rawFiles) {
|
|
2577
2597
|
const normalized = rel.replace(/\\/g, "/").trim();
|
|
2578
2598
|
if (!normalized) continue;
|
|
@@ -2597,7 +2617,10 @@ function filterFiles(rawFiles, extensions, workingDirectory, cwd, debugInfo) {
|
|
|
2597
2617
|
debugInfo.droppedMissing++;
|
|
2598
2618
|
continue;
|
|
2599
2619
|
}
|
|
2600
|
-
|
|
2620
|
+
if (!seen.has(abs)) {
|
|
2621
|
+
seen.add(abs);
|
|
2622
|
+
kept.push(abs);
|
|
2623
|
+
}
|
|
2601
2624
|
}
|
|
2602
2625
|
return kept;
|
|
2603
2626
|
}
|
|
@@ -2611,8 +2634,10 @@ function getChangedFiles(options = {}) {
|
|
|
2611
2634
|
cwd = process.cwd(),
|
|
2612
2635
|
extensions = DEFAULT_EXTENSIONS,
|
|
2613
2636
|
workingDirectory,
|
|
2614
|
-
debug = false
|
|
2637
|
+
debug = false,
|
|
2638
|
+
includeUntracked
|
|
2615
2639
|
} = options;
|
|
2640
|
+
const shouldIncludeUntracked = includeUntracked ?? !staged;
|
|
2616
2641
|
const debugInfo = {
|
|
2617
2642
|
strategiesAttempted: [],
|
|
2618
2643
|
strategySucceeded: null,
|
|
@@ -2656,7 +2681,15 @@ function getChangedFiles(options = {}) {
|
|
|
2656
2681
|
usedBase = resolvedBranch;
|
|
2657
2682
|
const diffResult = tryPrDiff(resolvedBranch, cwd, debugInfo);
|
|
2658
2683
|
if (diffResult) {
|
|
2659
|
-
|
|
2684
|
+
const untracked = shouldIncludeUntracked ? runGit(["ls-files", "--others", "--exclude-standard"], cwd) ?? "" : "";
|
|
2685
|
+
if (shouldIncludeUntracked && untracked) {
|
|
2686
|
+
debugInfo.strategiesAttempted.push("git ls-files --others --exclude-standard");
|
|
2687
|
+
}
|
|
2688
|
+
const combined = /* @__PURE__ */ new Set([
|
|
2689
|
+
...parseFileList(diffResult.output),
|
|
2690
|
+
...parseFileList(untracked)
|
|
2691
|
+
]);
|
|
2692
|
+
rawOutput = [...combined].join("\n");
|
|
2660
2693
|
} else {
|
|
2661
2694
|
const shallow = debugInfo.isShallowClone ? " Shallow clone detected \u2014 try fetch-depth: 0 in your workflow." : "";
|
|
2662
2695
|
const detached = debugInfo.isDetachedHead ? " HEAD is detached \u2014 ensure checkout fetches the branch." : "";
|
|
@@ -2666,8 +2699,16 @@ function getChangedFiles(options = {}) {
|
|
|
2666
2699
|
mode = "uncommitted";
|
|
2667
2700
|
const staged_ = runGit(["diff", "--name-only", "--cached", "--diff-filter=ACMRT"], cwd) ?? "";
|
|
2668
2701
|
const unstaged = runGit(["diff", "--name-only", "--diff-filter=ACMRT"], cwd) ?? "";
|
|
2702
|
+
const untracked = shouldIncludeUntracked ? runGit(["ls-files", "--others", "--exclude-standard"], cwd) ?? "" : "";
|
|
2669
2703
|
debugInfo.strategiesAttempted.push("git diff --cached", "git diff (unstaged)");
|
|
2670
|
-
|
|
2704
|
+
if (shouldIncludeUntracked) {
|
|
2705
|
+
debugInfo.strategiesAttempted.push("git ls-files --others --exclude-standard");
|
|
2706
|
+
}
|
|
2707
|
+
const combined = /* @__PURE__ */ new Set([
|
|
2708
|
+
...parseFileList(staged_),
|
|
2709
|
+
...parseFileList(unstaged),
|
|
2710
|
+
...parseFileList(untracked)
|
|
2711
|
+
]);
|
|
2671
2712
|
rawOutput = [...combined].join("\n");
|
|
2672
2713
|
if (rawOutput) debugInfo.strategySucceeded = "uncommitted changes";
|
|
2673
2714
|
if (!rawOutput) {
|
|
@@ -3748,7 +3789,7 @@ async function verifyConfigLoads(configPath, cwd) {
|
|
|
3748
3789
|
}
|
|
3749
3790
|
}
|
|
3750
3791
|
function registerInitCommand(program2) {
|
|
3751
|
-
program2.command("init").description("Configure eslint-plugin-ai-guard in this project").option("--preset <name>", "Preset to use: recommended | strict | security", "recommended").option("--flat", "Force flat config format (eslint.config.mjs) regardless of ESLint version").option("--dry-run", "Preview what would change without writing any files").option("-y, --yes", "Skip all prompts \u2014 use defaults (recommended preset, generate workflow + agent context)").action(async (opts) => {
|
|
3792
|
+
program2.command("init").description("Configure eslint-plugin-ai-guard in this project").option("--preset <name>", "Preset to use: recommended | strict | security | agent", "recommended").option("--flat", "Force flat config format (eslint.config.mjs) regardless of ESLint version").option("--dry-run", "Preview what would change without writing any files").option("-y, --yes", "Skip all prompts \u2014 use defaults (recommended preset, generate workflow + agent context)").action(async (opts) => {
|
|
3752
3793
|
const preset = opts.preset ?? "recommended";
|
|
3753
3794
|
const cwd = process.cwd();
|
|
3754
3795
|
const isDryRun = opts.dryRun === true;
|
|
@@ -4169,7 +4210,7 @@ var import_chalk8 = __toESM(require("chalk"));
|
|
|
4169
4210
|
var import_path10 = __toESM(require("path"));
|
|
4170
4211
|
init_logger();
|
|
4171
4212
|
init_config_manager();
|
|
4172
|
-
var
|
|
4213
|
+
var import_meta3 = {};
|
|
4173
4214
|
async function probeConfigLoad(configPath, cwd) {
|
|
4174
4215
|
try {
|
|
4175
4216
|
const { ESLint } = await import("eslint");
|
|
@@ -4297,7 +4338,7 @@ ${env.allConfigPaths.map((p) => ` ${import_path10.default.relative(cwd, p)
|
|
|
4297
4338
|
if (!utilsOk) {
|
|
4298
4339
|
try {
|
|
4299
4340
|
const selfDir = import_path10.default.dirname(
|
|
4300
|
-
new URL(
|
|
4341
|
+
new URL(import_meta3.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
|
|
4301
4342
|
);
|
|
4302
4343
|
const pkgPath = import_path10.default.join(selfDir, "../../node_modules/@typescript-eslint/utils/package.json");
|
|
4303
4344
|
if ((await import("fs")).existsSync(pkgPath)) {
|
|
@@ -4664,7 +4705,7 @@ function computeNewIssues(result, baselineSet, mode) {
|
|
|
4664
4705
|
};
|
|
4665
4706
|
}
|
|
4666
4707
|
function registerBaselineCommand(program2) {
|
|
4667
|
-
program2.command("baseline [action]").description("Save current issues as baseline; future runs show only new issues (save | check)").option("--save", "Save current state as the new baseline").option("--check", "Show only issues introduced since the last baseline").option("--mode <name>", "Baseline match mode: strict | stable", "stable").option("--path <dir>", "Directory to scan", ".").option("--preset <name>", "Preset: recommended | strict | security", "recommended").action(async (action, opts) => {
|
|
4708
|
+
program2.command("baseline [action]").description("Save current issues as baseline; future runs show only new issues (save | check)").option("--save", "Save current state as the new baseline").option("--check", "Show only issues introduced since the last baseline").option("--mode <name>", "Baseline match mode: strict | stable", "stable").option("--path <dir>", "Directory to scan", ".").option("--preset <name>", "Preset: recommended | strict | security | agent", "recommended").action(async (action, opts) => {
|
|
4668
4709
|
const cwd = process.cwd();
|
|
4669
4710
|
const preset = opts.preset ?? "recommended";
|
|
4670
4711
|
const mode = opts.mode ?? "stable";
|
|
@@ -5179,7 +5220,7 @@ function escHtml(str) {
|
|
|
5179
5220
|
function registerReportCommand(program2) {
|
|
5180
5221
|
program2.command("report").description(
|
|
5181
5222
|
"Generate a beautiful HTML report of AI Guard scan results\nPerfect for sharing with your team or in Slack."
|
|
5182
|
-
).option("--path <dir>", "Directory or file to scan", ".").option("--preset <preset>", "Rule preset: recommended | strict | security", "recommended").option("--html", "Generate HTML report (default behavior)").option("--json", "Output raw JSON to stdout instead of writing HTML").option("--output <filename>", "Output file name", "ai-guard-report.html").option("--no-open", "Do not open the report in the browser after generating").action(async (opts) => {
|
|
5223
|
+
).option("--path <dir>", "Directory or file to scan", ".").option("--preset <preset>", "Rule preset: recommended | strict | security | agent", "recommended").option("--html", "Generate HTML report (default behavior)").option("--json", "Output raw JSON to stdout instead of writing HTML").option("--output <filename>", "Output file name", "ai-guard-report.html").option("--no-open", "Do not open the report in the browser after generating").action(async (opts) => {
|
|
5183
5224
|
const preset = ["recommended", "strict", "security"].includes(opts.preset) ? opts.preset : "recommended";
|
|
5184
5225
|
const spinner = opts.json ? null : (0, import_ora4.default)({ text: "Scanning\u2026", color: "cyan" }).start();
|
|
5185
5226
|
let result;
|