claude-crap 0.2.0 → 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.
- package/CHANGELOG.md +33 -0
- package/README.md +4 -3
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +89 -0
- package/dist/scanner/bootstrap.d.ts.map +1 -0
- package/dist/scanner/bootstrap.js +278 -0
- package/dist/scanner/bootstrap.js.map +1 -0
- package/dist/scanner/index.d.ts +1 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +1 -0
- package/dist/scanner/index.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +12 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +12 -0
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/mcp-server.mjs +280 -0
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package.json +1 -1
- package/src/index.ts +78 -0
- package/src/scanner/bootstrap.ts +383 -0
- package/src/scanner/index.ts +8 -0
- package/src/schemas/tool-schemas.ts +14 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/scanner-bootstrap.test.ts +186 -0
package/plugin/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -58,8 +58,10 @@ 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
60
|
import { autoScan } from "./scanner/auto-scan.js";
|
|
61
|
+
import { bootstrapScanner } from "./scanner/bootstrap.js";
|
|
61
62
|
import {
|
|
62
63
|
autoScanSchema,
|
|
64
|
+
bootstrapScannerSchema,
|
|
63
65
|
computeCrapSchema,
|
|
64
66
|
computeTdrSchema,
|
|
65
67
|
analyzeFileAstSchema,
|
|
@@ -213,6 +215,12 @@ async function main(): Promise<void> {
|
|
|
213
215
|
"Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
|
|
214
216
|
inputSchema: autoScanSchema,
|
|
215
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
|
+
},
|
|
216
224
|
],
|
|
217
225
|
}));
|
|
218
226
|
|
|
@@ -540,6 +548,36 @@ async function main(): Promise<void> {
|
|
|
540
548
|
}
|
|
541
549
|
}
|
|
542
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
|
+
|
|
543
581
|
case "auto_scan": {
|
|
544
582
|
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
545
583
|
try {
|
|
@@ -653,6 +691,46 @@ async function main(): Promise<void> {
|
|
|
653
691
|
});
|
|
654
692
|
}
|
|
655
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
|
+
|
|
656
734
|
/**
|
|
657
735
|
* Render a human-readable Markdown summary of an auto-scan result.
|
|
658
736
|
*/
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootstrap a scanner for projects that don't have one configured.
|
|
3
|
+
*
|
|
4
|
+
* Detects the project type from workspace signals (package.json,
|
|
5
|
+
* tsconfig.json, pyproject.toml, pom.xml, *.csproj, etc.), installs
|
|
6
|
+
* the appropriate scanner, creates a minimal config file, and runs
|
|
7
|
+
* `autoScan()` to verify and ingest findings immediately.
|
|
8
|
+
*
|
|
9
|
+
* Coverage maps to the five languages the tree-sitter engine supports:
|
|
10
|
+
*
|
|
11
|
+
* - JavaScript / TypeScript → ESLint (npm install + flat config)
|
|
12
|
+
* - Python → Bandit (install instructions only — virtualenv boundary)
|
|
13
|
+
* - Java → Semgrep (install instructions)
|
|
14
|
+
* - C# → Semgrep (install instructions)
|
|
15
|
+
* - Unknown → Semgrep (polyglot fallback)
|
|
16
|
+
*
|
|
17
|
+
* For JS/TS projects the tool runs `npm install --save-dev` and writes
|
|
18
|
+
* an `eslint.config.mjs`. For all other languages it returns manual
|
|
19
|
+
* install instructions rather than executing package managers whose
|
|
20
|
+
* environment assumptions may not hold.
|
|
21
|
+
*
|
|
22
|
+
* @module scanner/bootstrap
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, writeFileSync, readdirSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { execFile } from "node:child_process";
|
|
28
|
+
import type { Logger } from "pino";
|
|
29
|
+
import type { KnownScanner } from "../adapters/common.js";
|
|
30
|
+
import { detectScanners } from "./detector.js";
|
|
31
|
+
import { autoScan, type AutoScanResult } from "./auto-scan.js";
|
|
32
|
+
import type { SarifStore } from "../sarif/sarif-store.js";
|
|
33
|
+
|
|
34
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detected project type, aligned with tree-sitter supported languages.
|
|
38
|
+
*/
|
|
39
|
+
export type ProjectType =
|
|
40
|
+
| "javascript"
|
|
41
|
+
| "typescript"
|
|
42
|
+
| "python"
|
|
43
|
+
| "java"
|
|
44
|
+
| "csharp"
|
|
45
|
+
| "unknown";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A single step in the bootstrap process.
|
|
49
|
+
*/
|
|
50
|
+
export interface BootstrapStep {
|
|
51
|
+
/** What was attempted (e.g. "install eslint", "create eslint.config.mjs"). */
|
|
52
|
+
action: string;
|
|
53
|
+
/** Whether the step completed successfully. */
|
|
54
|
+
success: boolean;
|
|
55
|
+
/** Human-readable detail (command output, error message, or instruction). */
|
|
56
|
+
detail: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Complete result of a bootstrap_scanner invocation.
|
|
61
|
+
*/
|
|
62
|
+
export interface BootstrapResult {
|
|
63
|
+
/** Detected project type based on workspace signals. */
|
|
64
|
+
projectType: ProjectType;
|
|
65
|
+
/** Whether a scanner was already configured (detected by detector.ts). */
|
|
66
|
+
alreadyConfigured: boolean;
|
|
67
|
+
/** Which scanners were already available, if any. */
|
|
68
|
+
existingScanners: string[];
|
|
69
|
+
/** Steps executed (or instructions returned) during bootstrap. */
|
|
70
|
+
steps: BootstrapStep[];
|
|
71
|
+
/** The auto-scan result after installation (null if skipped). */
|
|
72
|
+
autoScanResult: AutoScanResult | null;
|
|
73
|
+
/** Whether the overall bootstrap succeeded. */
|
|
74
|
+
success: boolean;
|
|
75
|
+
/** Summary message suitable for display. */
|
|
76
|
+
summary: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Project type detection ─────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect the project type from workspace signals.
|
|
83
|
+
*
|
|
84
|
+
* Checks in priority order: TypeScript, JavaScript, Python, Java,
|
|
85
|
+
* C#, then unknown. TypeScript wins over plain JavaScript because
|
|
86
|
+
* `tsconfig.json` implies a superset.
|
|
87
|
+
*/
|
|
88
|
+
export function detectProjectType(workspaceRoot: string): ProjectType {
|
|
89
|
+
const has = (file: string) => existsSync(join(workspaceRoot, file));
|
|
90
|
+
|
|
91
|
+
// JS/TS detection — package.json is the anchor
|
|
92
|
+
if (has("package.json")) {
|
|
93
|
+
if (has("tsconfig.json")) return "typescript";
|
|
94
|
+
return "javascript";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Python detection
|
|
98
|
+
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
|
|
99
|
+
return "python";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Java detection
|
|
103
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
|
|
104
|
+
return "java";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// C# detection
|
|
108
|
+
if (has("Directory.Build.props")) return "csharp";
|
|
109
|
+
try {
|
|
110
|
+
const entries = readdirSync(workspaceRoot);
|
|
111
|
+
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
112
|
+
return "csharp";
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// readdirSync can fail on permissions — fall through
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return "unknown";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── ESLint config generation ───────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate a minimal ESLint flat config (ESLint 9+).
|
|
125
|
+
*
|
|
126
|
+
* @param isTypeScript Include typescript-eslint when true.
|
|
127
|
+
* @returns The config file content as a string.
|
|
128
|
+
*/
|
|
129
|
+
export function generateEslintConfig(isTypeScript: boolean): string {
|
|
130
|
+
if (isTypeScript) {
|
|
131
|
+
return `import js from "@eslint/js";
|
|
132
|
+
import tseslint from "typescript-eslint";
|
|
133
|
+
|
|
134
|
+
export default tseslint.config(
|
|
135
|
+
js.configs.recommended,
|
|
136
|
+
...tseslint.configs.recommended,
|
|
137
|
+
{
|
|
138
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return `import js from "@eslint/js";
|
|
145
|
+
|
|
146
|
+
export default [
|
|
147
|
+
js.configs.recommended,
|
|
148
|
+
{
|
|
149
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Installation helpers ───────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Run `npm install --save-dev` for the given packages.
|
|
159
|
+
*/
|
|
160
|
+
function npmInstall(
|
|
161
|
+
workspaceRoot: string,
|
|
162
|
+
packages: string[],
|
|
163
|
+
): Promise<BootstrapStep> {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
execFile(
|
|
166
|
+
"npm",
|
|
167
|
+
["install", "--save-dev", ...packages],
|
|
168
|
+
{
|
|
169
|
+
cwd: workspaceRoot,
|
|
170
|
+
timeout: 120_000,
|
|
171
|
+
env: { ...process.env, FORCE_COLOR: "0" },
|
|
172
|
+
},
|
|
173
|
+
(err, stdout, stderr) => {
|
|
174
|
+
if (err) {
|
|
175
|
+
resolve({
|
|
176
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
177
|
+
success: false,
|
|
178
|
+
detail: stderr || (err as Error).message,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
resolve({
|
|
183
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
184
|
+
success: true,
|
|
185
|
+
detail: `installed ${packages.join(", ")}`,
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Write the ESLint config file to the workspace root.
|
|
194
|
+
* Returns failure if the file already exists.
|
|
195
|
+
*/
|
|
196
|
+
function writeEslintConfigFile(
|
|
197
|
+
workspaceRoot: string,
|
|
198
|
+
isTypeScript: boolean,
|
|
199
|
+
): BootstrapStep {
|
|
200
|
+
const configPath = join(workspaceRoot, "eslint.config.mjs");
|
|
201
|
+
if (existsSync(configPath)) {
|
|
202
|
+
return {
|
|
203
|
+
action: "create eslint.config.mjs",
|
|
204
|
+
success: true,
|
|
205
|
+
detail: "eslint.config.mjs already exists — skipped",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
|
|
211
|
+
return {
|
|
212
|
+
action: "create eslint.config.mjs",
|
|
213
|
+
success: true,
|
|
214
|
+
detail: `created eslint.config.mjs (${isTypeScript ? "TypeScript" : "JavaScript"} template)`,
|
|
215
|
+
};
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
action: "create eslint.config.mjs",
|
|
219
|
+
success: false,
|
|
220
|
+
detail: (err as Error).message,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Scanner-to-language mapping ────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Map project type → recommended scanner and install instructions.
|
|
229
|
+
*/
|
|
230
|
+
interface ScannerRecommendation {
|
|
231
|
+
scanner: KnownScanner;
|
|
232
|
+
canAutoInstall: boolean;
|
|
233
|
+
installInstructions: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getRecommendation(projectType: ProjectType): ScannerRecommendation {
|
|
237
|
+
switch (projectType) {
|
|
238
|
+
case "javascript":
|
|
239
|
+
case "typescript":
|
|
240
|
+
return {
|
|
241
|
+
scanner: "eslint",
|
|
242
|
+
canAutoInstall: true,
|
|
243
|
+
installInstructions: "npm install --save-dev eslint @eslint/js",
|
|
244
|
+
};
|
|
245
|
+
case "python":
|
|
246
|
+
return {
|
|
247
|
+
scanner: "bandit",
|
|
248
|
+
canAutoInstall: false,
|
|
249
|
+
installInstructions:
|
|
250
|
+
"pip install bandit (or: pipx install bandit, poetry add --group dev bandit)",
|
|
251
|
+
};
|
|
252
|
+
case "java":
|
|
253
|
+
case "csharp":
|
|
254
|
+
return {
|
|
255
|
+
scanner: "semgrep",
|
|
256
|
+
canAutoInstall: false,
|
|
257
|
+
installInstructions:
|
|
258
|
+
"brew install semgrep (or: pip install semgrep, pipx install semgrep)",
|
|
259
|
+
};
|
|
260
|
+
case "unknown":
|
|
261
|
+
return {
|
|
262
|
+
scanner: "semgrep",
|
|
263
|
+
canAutoInstall: false,
|
|
264
|
+
installInstructions:
|
|
265
|
+
"brew install semgrep (or: pip install semgrep, pipx install semgrep)",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Main orchestrator ──────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Bootstrap a scanner for the current workspace.
|
|
274
|
+
*
|
|
275
|
+
* 1. Check if a scanner is already configured (short-circuit if so)
|
|
276
|
+
* 2. Detect the project type
|
|
277
|
+
* 3. Install the recommended scanner (or return instructions)
|
|
278
|
+
* 4. Run auto_scan to verify and ingest findings
|
|
279
|
+
*
|
|
280
|
+
* @param workspaceRoot Absolute path to the project root.
|
|
281
|
+
* @param sarifStore Live SARIF store for auto-scan ingestion.
|
|
282
|
+
* @param logger Pino logger for progress reporting.
|
|
283
|
+
*/
|
|
284
|
+
export async function bootstrapScanner(
|
|
285
|
+
workspaceRoot: string,
|
|
286
|
+
sarifStore: SarifStore,
|
|
287
|
+
logger: Logger,
|
|
288
|
+
): Promise<BootstrapResult> {
|
|
289
|
+
// 1. Check existing scanners
|
|
290
|
+
const detections = await detectScanners(workspaceRoot);
|
|
291
|
+
const available = detections.filter((d) => d.available);
|
|
292
|
+
|
|
293
|
+
if (available.length > 0) {
|
|
294
|
+
const existingScanners = available.map((d) => d.scanner);
|
|
295
|
+
logger.info(
|
|
296
|
+
{ existingScanners },
|
|
297
|
+
"bootstrap: scanner(s) already configured — skipping",
|
|
298
|
+
);
|
|
299
|
+
return {
|
|
300
|
+
projectType: detectProjectType(workspaceRoot),
|
|
301
|
+
alreadyConfigured: true,
|
|
302
|
+
existingScanners,
|
|
303
|
+
steps: [],
|
|
304
|
+
autoScanResult: null,
|
|
305
|
+
success: true,
|
|
306
|
+
summary: `Scanner(s) already configured: ${existingScanners.join(", ")}. Run auto_scan to ingest findings.`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 2. Detect project type
|
|
311
|
+
const projectType = detectProjectType(workspaceRoot);
|
|
312
|
+
const recommendation = getRecommendation(projectType);
|
|
313
|
+
const steps: BootstrapStep[] = [];
|
|
314
|
+
|
|
315
|
+
logger.info(
|
|
316
|
+
{ projectType, scanner: recommendation.scanner },
|
|
317
|
+
"bootstrap: detected project type",
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// 3. Install scanner
|
|
321
|
+
if (recommendation.canAutoInstall) {
|
|
322
|
+
// JS/TS: auto-install ESLint
|
|
323
|
+
const isTypeScript = projectType === "typescript";
|
|
324
|
+
const packages = isTypeScript
|
|
325
|
+
? ["eslint", "@eslint/js", "typescript-eslint"]
|
|
326
|
+
: ["eslint", "@eslint/js"];
|
|
327
|
+
|
|
328
|
+
const installStep = await npmInstall(workspaceRoot, packages);
|
|
329
|
+
steps.push(installStep);
|
|
330
|
+
|
|
331
|
+
if (installStep.success) {
|
|
332
|
+
const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
|
|
333
|
+
steps.push(configStep);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// Python / Java / C# / Unknown: return instructions
|
|
337
|
+
steps.push({
|
|
338
|
+
action: `suggest ${recommendation.scanner} install`,
|
|
339
|
+
success: true,
|
|
340
|
+
detail: recommendation.installInstructions,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 4. Run auto_scan if installation succeeded
|
|
345
|
+
const installSucceeded = steps.every((s) => s.success);
|
|
346
|
+
let autoScanResult: AutoScanResult | null = null;
|
|
347
|
+
|
|
348
|
+
if (installSucceeded && recommendation.canAutoInstall) {
|
|
349
|
+
try {
|
|
350
|
+
autoScanResult = await autoScan(workspaceRoot, sarifStore, logger);
|
|
351
|
+
} catch (err) {
|
|
352
|
+
logger.warn(
|
|
353
|
+
{ err: (err as Error).message },
|
|
354
|
+
"bootstrap: auto_scan after install failed",
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 5. Build result
|
|
360
|
+
const findings = autoScanResult?.totalFindings ?? 0;
|
|
361
|
+
const scannerInstalled = recommendation.canAutoInstall && installSucceeded;
|
|
362
|
+
|
|
363
|
+
let summary: string;
|
|
364
|
+
if (scannerInstalled && autoScanResult) {
|
|
365
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan found ${findings} finding(s).`;
|
|
366
|
+
} else if (scannerInstalled) {
|
|
367
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan did not run.`;
|
|
368
|
+
} else if (!recommendation.canAutoInstall) {
|
|
369
|
+
summary = `Detected ${projectType} project. Install ${recommendation.scanner} manually: ${recommendation.installInstructions}`;
|
|
370
|
+
} else {
|
|
371
|
+
summary = `Failed to install ${recommendation.scanner}. Check the error details in the steps.`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
projectType,
|
|
376
|
+
alreadyConfigured: false,
|
|
377
|
+
existingScanners: [],
|
|
378
|
+
steps,
|
|
379
|
+
autoScanResult,
|
|
380
|
+
success: installSucceeded,
|
|
381
|
+
summary,
|
|
382
|
+
};
|
|
383
|
+
}
|
package/src/scanner/index.ts
CHANGED
|
@@ -20,3 +20,11 @@
|
|
|
20
20
|
export { detectScanners, type ScannerDetection } from "./detector.js";
|
|
21
21
|
export { runScanner, type ScannerRunResult } from "./runner.js";
|
|
22
22
|
export { autoScan, type AutoScanResult, type ScannerResult } from "./auto-scan.js";
|
|
23
|
+
export {
|
|
24
|
+
bootstrapScanner,
|
|
25
|
+
detectProjectType,
|
|
26
|
+
generateEslintConfig,
|
|
27
|
+
type BootstrapResult,
|
|
28
|
+
type BootstrapStep,
|
|
29
|
+
type ProjectType,
|
|
30
|
+
} from "./bootstrap.js";
|
|
@@ -203,6 +203,20 @@ export const ingestScannerOutputSchema = {
|
|
|
203
203
|
* Schema for the `auto_scan` tool. Auto-detects available scanners
|
|
204
204
|
* in the workspace, runs them, and ingests findings into the SARIF store.
|
|
205
205
|
*/
|
|
206
|
+
/**
|
|
207
|
+
* Schema for the `bootstrap_scanner` tool. Detects project type,
|
|
208
|
+
* installs the appropriate scanner, creates config files, and runs
|
|
209
|
+
* auto_scan to verify.
|
|
210
|
+
*/
|
|
211
|
+
export const bootstrapScannerSchema = {
|
|
212
|
+
type: "object",
|
|
213
|
+
description:
|
|
214
|
+
"Detect the project type (JavaScript, TypeScript, Python, Java, C#), install the appropriate scanner (ESLint for JS/TS, Bandit for Python, Semgrep for Java/C#), create a minimal config file, and run auto_scan to verify. Skips installation if a scanner is already configured. Use this when auto_scan finds no scanners and quality grades are vacuously A.",
|
|
215
|
+
properties: {},
|
|
216
|
+
required: [],
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
} as const;
|
|
219
|
+
|
|
206
220
|
export const autoScanSchema = {
|
|
207
221
|
type: "object",
|
|
208
222
|
description:
|
|
@@ -232,7 +232,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
|
|
|
232
232
|
assert.ok(child && !child.killed, "server child should be running");
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
it("exposes all
|
|
235
|
+
it("exposes all nine tools via tools/list", async () => {
|
|
236
236
|
const response = await client!.request<{ result?: { tools?: Array<{ name: string }> } }>(
|
|
237
237
|
"tools/list",
|
|
238
238
|
);
|
|
@@ -240,6 +240,7 @@ describe("MCP server integration", { skip: !serverBuilt }, () => {
|
|
|
240
240
|
assert.deepEqual(names, [
|
|
241
241
|
"analyze_file_ast",
|
|
242
242
|
"auto_scan",
|
|
243
|
+
"bootstrap_scanner",
|
|
243
244
|
"compute_crap",
|
|
244
245
|
"compute_tdr",
|
|
245
246
|
"ingest_sarif",
|