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 CHANGED
@@ -33,7 +33,14 @@
33
33
 
34
34
  ## What is AI Guard?
35
35
 
36
- **AI Guard** (`eslint-plugin-ai-guard`) is a deterministic ESLint plugin, CLI, and GitHub Action 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.).
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 import_meta = {};
1794
+ var import_meta2 = {};
1776
1795
  var _version;
1777
1796
  function readVersion() {
1778
1797
  try {
1779
- const require_ = (0, import_module.createRequire)(import_meta.url);
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)(import_meta.url)),
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(`diff --name-only --diff-filter=ACMRT ${mergeBase}`, cwd);
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
- kept.push(abs);
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
- rawOutput = diffResult.output;
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
- const combined = /* @__PURE__ */ new Set([...parseFileList(staged_), ...parseFileList(unstaged)]);
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 import_meta2 = {};
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(import_meta2.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")
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;