claude-crap 0.1.2 → 0.3.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +44 -23
  3. package/dist/index.js +142 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/scanner/auto-scan.d.ts +57 -0
  6. package/dist/scanner/auto-scan.d.ts.map +1 -0
  7. package/dist/scanner/auto-scan.js +138 -0
  8. package/dist/scanner/auto-scan.js.map +1 -0
  9. package/dist/scanner/bootstrap.d.ts +89 -0
  10. package/dist/scanner/bootstrap.d.ts.map +1 -0
  11. package/dist/scanner/bootstrap.js +278 -0
  12. package/dist/scanner/bootstrap.js.map +1 -0
  13. package/dist/scanner/detector.d.ts +53 -0
  14. package/dist/scanner/detector.d.ts.map +1 -0
  15. package/dist/scanner/detector.js +173 -0
  16. package/dist/scanner/detector.js.map +1 -0
  17. package/dist/scanner/index.d.ts +23 -0
  18. package/dist/scanner/index.d.ts.map +1 -0
  19. package/dist/scanner/index.js +23 -0
  20. package/dist/scanner/index.js.map +1 -0
  21. package/dist/scanner/runner.d.ts +59 -0
  22. package/dist/scanner/runner.d.ts.map +1 -0
  23. package/dist/scanner/runner.js +159 -0
  24. package/dist/scanner/runner.js.map +1 -0
  25. package/dist/schemas/tool-schemas.d.ts +23 -0
  26. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  27. package/dist/schemas/tool-schemas.js +23 -0
  28. package/dist/schemas/tool-schemas.js.map +1 -1
  29. package/package.json +5 -1
  30. package/plugin/.claude-plugin/plugin.json +1 -1
  31. package/plugin/bundle/mcp-server.mjs +732 -0
  32. package/plugin/bundle/mcp-server.mjs.map +4 -4
  33. package/plugin/package.json +1 -1
  34. package/src/index.ts +176 -0
  35. package/src/scanner/auto-scan.ts +212 -0
  36. package/src/scanner/bootstrap.ts +383 -0
  37. package/src/scanner/detector.ts +224 -0
  38. package/src/scanner/index.ts +30 -0
  39. package/src/scanner/runner.ts +212 -0
  40. package/src/schemas/tool-schemas.ts +27 -0
  41. package/src/tests/auto-scan.test.ts +137 -0
  42. package/src/tests/integration/mcp-server.integration.test.ts +3 -1
  43. package/src/tests/scanner-bootstrap.test.ts +186 -0
  44. package/src/tests/scanner-detector.test.ts +181 -0
  45. package/src/tests/scanner-runner.test.ts +63 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -57,7 +57,11 @@ import { validateSarifDocument } from "./sarif/sarif-validator.js";
57
57
  import { loadCrapConfig, CrapConfigError } from "./crap-config.js";
58
58
  import { findTestFile } from "./tools/test-harness.js";
59
59
  import { resolveWithinWorkspace } from "./workspace-guard.js";
60
+ import { autoScan } from "./scanner/auto-scan.js";
61
+ import { bootstrapScanner } from "./scanner/bootstrap.js";
60
62
  import {
63
+ autoScanSchema,
64
+ bootstrapScannerSchema,
61
65
  computeCrapSchema,
62
66
  computeTdrSchema,
63
67
  analyzeFileAstSchema,
@@ -205,6 +209,18 @@ async function main(): Promise<void> {
205
209
  "Aggregate the project score across Maintainability, Reliability, Security and Overall, returning a chat-friendly Markdown summary, the structured JSON, the local dashboard URL, and the consolidated SARIF report path.",
206
210
  inputSchema: scoreProjectSchema,
207
211
  },
212
+ {
213
+ name: "auto_scan",
214
+ description:
215
+ "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
216
+ inputSchema: autoScanSchema,
217
+ },
218
+ {
219
+ name: "bootstrap_scanner",
220
+ description:
221
+ "Detect project type, install the right scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create minimal config, and run auto_scan to verify.",
222
+ inputSchema: bootstrapScannerSchema,
223
+ },
208
224
  ],
209
225
  }));
210
226
 
@@ -532,6 +548,65 @@ async function main(): Promise<void> {
532
548
  }
533
549
  }
534
550
 
551
+ case "bootstrap_scanner": {
552
+ logger.info({ tool: "bootstrap_scanner" }, "Tool call received");
553
+ try {
554
+ const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
555
+ const markdown = renderBootstrapMarkdown(result);
556
+ return {
557
+ content: [
558
+ { type: "text", text: markdown },
559
+ { type: "text", text: JSON.stringify(result, null, 2) },
560
+ ],
561
+ isError: !result.success,
562
+ };
563
+ } catch (err) {
564
+ logger.error({ err }, "bootstrap_scanner failed");
565
+ return {
566
+ content: [
567
+ {
568
+ type: "text",
569
+ text: JSON.stringify(
570
+ { tool: "bootstrap_scanner", status: "error", message: (err as Error).message },
571
+ null,
572
+ 2,
573
+ ),
574
+ },
575
+ ],
576
+ isError: true,
577
+ };
578
+ }
579
+ }
580
+
581
+ case "auto_scan": {
582
+ logger.info({ tool: "auto_scan" }, "Tool call received");
583
+ try {
584
+ const result = await autoScan(config.pluginRoot, sarifStore, logger);
585
+ const markdown = renderAutoScanMarkdown(result);
586
+ return {
587
+ content: [
588
+ { type: "text", text: markdown },
589
+ { type: "text", text: JSON.stringify(result, null, 2) },
590
+ ],
591
+ };
592
+ } catch (err) {
593
+ logger.error({ err }, "auto_scan failed");
594
+ return {
595
+ content: [
596
+ {
597
+ type: "text",
598
+ text: JSON.stringify(
599
+ { tool: "auto_scan", status: "error", message: (err as Error).message },
600
+ null,
601
+ 2,
602
+ ),
603
+ },
604
+ ],
605
+ isError: true,
606
+ };
607
+ }
608
+ }
609
+
535
610
  default:
536
611
  throw new Error(`[claude-crap] Unknown tool: ${name}`);
537
612
  }
@@ -589,6 +664,107 @@ async function main(): Promise<void> {
589
664
  const transport = new StdioServerTransport();
590
665
  await server.connect(transport);
591
666
  logger.info("claude-crap MCP server ready (stdio)");
667
+
668
+ // Fire-and-forget: auto-scan runs in background, doesn't block tool calls.
669
+ // If the agent calls score_project before scanning finishes, it gets
670
+ // whatever is in the SARIF store so far. The next call after completion
671
+ // reflects all findings.
672
+ autoScan(config.pluginRoot, sarifStore, logger)
673
+ .then((result) => {
674
+ const scanners = result.results
675
+ .filter((r) => r.success)
676
+ .map((r) => r.scanner);
677
+ logger.info(
678
+ {
679
+ scannersRun: scanners,
680
+ totalFindings: result.totalFindings,
681
+ durationMs: result.totalDurationMs,
682
+ },
683
+ "auto-scan completed",
684
+ );
685
+ })
686
+ .catch((err) => {
687
+ logger.warn(
688
+ { err: (err as Error).message },
689
+ "auto-scan failed — continuing without it",
690
+ );
691
+ });
692
+ }
693
+
694
+ /**
695
+ * Render a human-readable Markdown summary of a bootstrap result.
696
+ */
697
+ function renderBootstrapMarkdown(result: import("./scanner/bootstrap.js").BootstrapResult): string {
698
+ const lines: string[] = ["## claude-crap :: bootstrap scanner\n"];
699
+
700
+ lines.push(`**Project type:** ${result.projectType}`);
701
+
702
+ if (result.alreadyConfigured) {
703
+ lines.push(`**Status:** Scanner(s) already configured: ${result.existingScanners.join(", ")}`);
704
+ lines.push("\nNo installation needed. Run `auto_scan` to ingest findings.");
705
+ return lines.join("\n");
706
+ }
707
+
708
+ lines.push("");
709
+
710
+ if (result.steps.length > 0) {
711
+ lines.push("### Steps\n");
712
+ lines.push("| Action | Status | Detail |");
713
+ lines.push("| ------ | :----: | ------ |");
714
+ for (const s of result.steps) {
715
+ const status = s.success ? "ok" : "failed";
716
+ lines.push(`| ${s.action} | ${status} | ${s.detail} |`);
717
+ }
718
+ lines.push("");
719
+ }
720
+
721
+ if (result.autoScanResult) {
722
+ const r = result.autoScanResult;
723
+ const scanners = r.results.filter((s) => s.success).map((s) => s.scanner);
724
+ lines.push(
725
+ `**Auto-scan:** ${r.totalFindings} finding(s) ingested from ${scanners.join(", ") || "no scanners"} in ${(r.totalDurationMs / 1000).toFixed(1)}s`,
726
+ );
727
+ lines.push("");
728
+ }
729
+
730
+ lines.push(`**Summary:** ${result.summary}`);
731
+ return lines.join("\n");
732
+ }
733
+
734
+ /**
735
+ * Render a human-readable Markdown summary of an auto-scan result.
736
+ */
737
+ function renderAutoScanMarkdown(result: import("./scanner/auto-scan.js").AutoScanResult): string {
738
+ const lines: string[] = ["## claude-crap :: auto-scan results\n"];
739
+
740
+ // Detection summary
741
+ lines.push("### Detected scanners\n");
742
+ lines.push("| Scanner | Available | Reason |");
743
+ lines.push("| ------- | :-------: | ------ |");
744
+ for (const d of result.detected) {
745
+ lines.push(`| ${d.scanner} | ${d.available ? "yes" : "no"} | ${d.reason} |`);
746
+ }
747
+ lines.push("");
748
+
749
+ // Execution results
750
+ if (result.results.length > 0) {
751
+ lines.push("### Execution results\n");
752
+ lines.push("| Scanner | Status | Findings | Duration |");
753
+ lines.push("| ------- | :----: | :------: | -------: |");
754
+ for (const r of result.results) {
755
+ const status = r.success ? "ok" : "failed";
756
+ const duration = `${(r.durationMs / 1000).toFixed(1)}s`;
757
+ lines.push(`| ${r.scanner} | ${status} | ${r.findingsIngested} | ${duration} |`);
758
+ }
759
+ lines.push("");
760
+ }
761
+
762
+ // Summary
763
+ lines.push(
764
+ `**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1000).toFixed(1)}s`,
765
+ );
766
+
767
+ return lines.join("\n");
592
768
  }
593
769
 
594
770
  /**
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Orchestrator: detect available scanners, run them, and ingest results.
3
+ *
4
+ * This module ties together the detector, runner, and adapter pipeline
5
+ * into a single `autoScan()` function that:
6
+ *
7
+ * 1. Probes the workspace for available scanners
8
+ * 2. Executes detected scanners in parallel
9
+ * 3. Routes each scanner's output through its adapter
10
+ * 4. Ingests the normalized SARIF into the store
11
+ *
12
+ * The function is designed to be called:
13
+ * - At MCP server boot (fire-and-forget, non-blocking)
14
+ * - On demand via the `auto_scan` MCP tool
15
+ *
16
+ * Failures in individual scanners are logged and skipped — a broken
17
+ * Semgrep install should not prevent ESLint from running.
18
+ *
19
+ * @module scanner/auto-scan
20
+ */
21
+
22
+ import type { Logger } from "pino";
23
+ import { detectScanners, type ScannerDetection } from "./detector.js";
24
+ import { runScanner, type ScannerRunResult } from "./runner.js";
25
+ import { adaptScannerOutput, type KnownScanner } from "../adapters/index.js";
26
+ import type { SarifStore } from "../sarif/sarif-store.js";
27
+
28
+ // ── Types ──────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Per-scanner result within the auto-scan summary.
32
+ */
33
+ export interface ScannerResult {
34
+ scanner: KnownScanner;
35
+ success: boolean;
36
+ findingsIngested: number;
37
+ durationMs: number;
38
+ error?: string;
39
+ }
40
+
41
+ /**
42
+ * Complete result of an auto-scan run.
43
+ */
44
+ export interface AutoScanResult {
45
+ /** Detection results for all four scanners. */
46
+ detected: ScannerDetection[];
47
+ /** Execution + ingestion results for scanners that were available. */
48
+ results: ScannerResult[];
49
+ /** Total findings ingested across all scanners. */
50
+ totalFindings: number;
51
+ /** Wall-clock time for the entire auto-scan. */
52
+ totalDurationMs: number;
53
+ }
54
+
55
+ // ── Orchestrator ───────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Ingest a single scanner's raw output through its adapter and into
59
+ * the SARIF store. Returns the number of accepted findings.
60
+ */
61
+ function ingestScannerRun(
62
+ scanner: KnownScanner,
63
+ rawOutput: string,
64
+ sarifStore: SarifStore,
65
+ ): { accepted: number } {
66
+ // Parse the raw output — adapters accept string or object
67
+ let parsed: unknown;
68
+ try {
69
+ parsed = JSON.parse(rawOutput);
70
+ } catch {
71
+ // Semgrep outputs SARIF as a string, others are JSON.
72
+ // If parsing fails, pass the raw string to the adapter.
73
+ parsed = rawOutput;
74
+ }
75
+
76
+ const adapted = adaptScannerOutput(scanner, parsed);
77
+ const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
78
+ return { accepted: stats.accepted };
79
+ }
80
+
81
+ /**
82
+ * Auto-detect, run, and ingest all available scanners.
83
+ *
84
+ * @param workspaceRoot Absolute path to the project root.
85
+ * @param sarifStore Live SARIF store to ingest findings into.
86
+ * @param logger Pino logger for progress and error reporting.
87
+ * @returns Summary of what was detected, run, and ingested.
88
+ */
89
+ export async function autoScan(
90
+ workspaceRoot: string,
91
+ sarifStore: SarifStore,
92
+ logger: Logger,
93
+ ): Promise<AutoScanResult> {
94
+ const start = Date.now();
95
+
96
+ // 1. Detect available scanners
97
+ const detected = await detectScanners(workspaceRoot);
98
+ const available = detected.filter((d) => d.available);
99
+
100
+ logger.info(
101
+ {
102
+ detected: detected.map((d) => `${d.scanner}:${d.available}`),
103
+ available: available.length,
104
+ },
105
+ "auto-scan: detection complete",
106
+ );
107
+
108
+ if (available.length === 0) {
109
+ return {
110
+ detected,
111
+ results: [],
112
+ totalFindings: 0,
113
+ totalDurationMs: Date.now() - start,
114
+ };
115
+ }
116
+
117
+ // 2. Run all available scanners in parallel
118
+ const runResults = await Promise.allSettled(
119
+ available.map((d) => runScanner(d.scanner, workspaceRoot)),
120
+ );
121
+
122
+ // 3. Ingest results
123
+ const results: ScannerResult[] = [];
124
+ let totalFindings = 0;
125
+ let persistNeeded = false;
126
+
127
+ for (let i = 0; i < available.length; i++) {
128
+ const detection = available[i]!;
129
+ const settled = runResults[i]!;
130
+
131
+ if (settled.status === "rejected") {
132
+ const error = String(settled.reason);
133
+ logger.warn(
134
+ { scanner: detection.scanner, error },
135
+ "auto-scan: scanner execution rejected",
136
+ );
137
+ results.push({
138
+ scanner: detection.scanner,
139
+ success: false,
140
+ findingsIngested: 0,
141
+ durationMs: 0,
142
+ error,
143
+ });
144
+ continue;
145
+ }
146
+
147
+ const runResult: ScannerRunResult = settled.value;
148
+
149
+ if (!runResult.success) {
150
+ logger.warn(
151
+ { scanner: runResult.scanner, error: runResult.error },
152
+ "auto-scan: scanner returned failure",
153
+ );
154
+ results.push({
155
+ scanner: runResult.scanner,
156
+ success: false,
157
+ findingsIngested: 0,
158
+ durationMs: runResult.durationMs,
159
+ error: runResult.error ?? "unknown error",
160
+ });
161
+ continue;
162
+ }
163
+
164
+ // Ingest through adapter pipeline
165
+ try {
166
+ const { accepted } = ingestScannerRun(
167
+ runResult.scanner,
168
+ runResult.rawOutput,
169
+ sarifStore,
170
+ );
171
+ totalFindings += accepted;
172
+ persistNeeded = true;
173
+
174
+ logger.info(
175
+ { scanner: runResult.scanner, accepted, durationMs: runResult.durationMs },
176
+ "auto-scan: scanner ingested",
177
+ );
178
+
179
+ results.push({
180
+ scanner: runResult.scanner,
181
+ success: true,
182
+ findingsIngested: accepted,
183
+ durationMs: runResult.durationMs,
184
+ });
185
+ } catch (err) {
186
+ const error = (err as Error).message;
187
+ logger.warn(
188
+ { scanner: runResult.scanner, error },
189
+ "auto-scan: adapter/ingestion failed",
190
+ );
191
+ results.push({
192
+ scanner: runResult.scanner,
193
+ success: false,
194
+ findingsIngested: 0,
195
+ durationMs: runResult.durationMs,
196
+ error,
197
+ });
198
+ }
199
+ }
200
+
201
+ // 4. Persist consolidated SARIF if anything was ingested
202
+ if (persistNeeded) {
203
+ await sarifStore.persist();
204
+ }
205
+
206
+ return {
207
+ detected,
208
+ results,
209
+ totalFindings,
210
+ totalDurationMs: Date.now() - start,
211
+ };
212
+ }