claude-crap 0.2.0 → 0.3.1
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 +46 -0
- package/README.md +4 -3
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +12 -0
- package/dist/scanner/auto-scan.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 +326 -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 +336 -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/auto-scan.ts +15 -0
- package/src/scanner/bootstrap.ts +435 -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
|
@@ -8340,6 +8340,259 @@ function runScanner(scanner, workspaceRoot) {
|
|
|
8340
8340
|
});
|
|
8341
8341
|
}
|
|
8342
8342
|
|
|
8343
|
+
// src/scanner/bootstrap.ts
|
|
8344
|
+
import { existsSync as existsSync3, writeFileSync, readdirSync } from "node:fs";
|
|
8345
|
+
import { join as join8 } from "node:path";
|
|
8346
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
8347
|
+
function detectProjectType(workspaceRoot) {
|
|
8348
|
+
const has = (file) => existsSync3(join8(workspaceRoot, file));
|
|
8349
|
+
if (has("package.json")) {
|
|
8350
|
+
if (has("tsconfig.json")) return "typescript";
|
|
8351
|
+
return "javascript";
|
|
8352
|
+
}
|
|
8353
|
+
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
|
|
8354
|
+
return "python";
|
|
8355
|
+
}
|
|
8356
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
|
|
8357
|
+
return "java";
|
|
8358
|
+
}
|
|
8359
|
+
if (has("Directory.Build.props")) return "csharp";
|
|
8360
|
+
try {
|
|
8361
|
+
const entries = readdirSync(workspaceRoot);
|
|
8362
|
+
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
8363
|
+
return "csharp";
|
|
8364
|
+
}
|
|
8365
|
+
} catch {
|
|
8366
|
+
}
|
|
8367
|
+
return "unknown";
|
|
8368
|
+
}
|
|
8369
|
+
function generateEslintConfig(isTypeScript) {
|
|
8370
|
+
if (isTypeScript) {
|
|
8371
|
+
return `import js from "@eslint/js";
|
|
8372
|
+
import tseslint from "typescript-eslint";
|
|
8373
|
+
|
|
8374
|
+
export default tseslint.config(
|
|
8375
|
+
js.configs.recommended,
|
|
8376
|
+
...tseslint.configs.recommended,
|
|
8377
|
+
{
|
|
8378
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
8379
|
+
},
|
|
8380
|
+
);
|
|
8381
|
+
`;
|
|
8382
|
+
}
|
|
8383
|
+
return `import js from "@eslint/js";
|
|
8384
|
+
|
|
8385
|
+
export default [
|
|
8386
|
+
js.configs.recommended,
|
|
8387
|
+
{
|
|
8388
|
+
ignores: ["dist/", "node_modules/", "coverage/"],
|
|
8389
|
+
},
|
|
8390
|
+
];
|
|
8391
|
+
`;
|
|
8392
|
+
}
|
|
8393
|
+
function npmInstall(workspaceRoot, packages) {
|
|
8394
|
+
return new Promise((resolve6) => {
|
|
8395
|
+
execFile3(
|
|
8396
|
+
"npm",
|
|
8397
|
+
["install", "--save-dev", ...packages],
|
|
8398
|
+
{
|
|
8399
|
+
cwd: workspaceRoot,
|
|
8400
|
+
timeout: 12e4,
|
|
8401
|
+
env: { ...process.env, FORCE_COLOR: "0" }
|
|
8402
|
+
},
|
|
8403
|
+
(err, stdout, stderr) => {
|
|
8404
|
+
if (err) {
|
|
8405
|
+
resolve6({
|
|
8406
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8407
|
+
success: false,
|
|
8408
|
+
detail: stderr || err.message
|
|
8409
|
+
});
|
|
8410
|
+
return;
|
|
8411
|
+
}
|
|
8412
|
+
resolve6({
|
|
8413
|
+
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8414
|
+
success: true,
|
|
8415
|
+
detail: `installed ${packages.join(", ")}`
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
8418
|
+
);
|
|
8419
|
+
});
|
|
8420
|
+
}
|
|
8421
|
+
function writeEslintConfigFile(workspaceRoot, isTypeScript) {
|
|
8422
|
+
const configPath = join8(workspaceRoot, "eslint.config.mjs");
|
|
8423
|
+
if (existsSync3(configPath)) {
|
|
8424
|
+
return {
|
|
8425
|
+
action: "create eslint.config.mjs",
|
|
8426
|
+
success: true,
|
|
8427
|
+
detail: "eslint.config.mjs already exists \u2014 skipped"
|
|
8428
|
+
};
|
|
8429
|
+
}
|
|
8430
|
+
try {
|
|
8431
|
+
writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
|
|
8432
|
+
return {
|
|
8433
|
+
action: "create eslint.config.mjs",
|
|
8434
|
+
success: true,
|
|
8435
|
+
detail: `created eslint.config.mjs (${isTypeScript ? "TypeScript" : "JavaScript"} template)`
|
|
8436
|
+
};
|
|
8437
|
+
} catch (err) {
|
|
8438
|
+
return {
|
|
8439
|
+
action: "create eslint.config.mjs",
|
|
8440
|
+
success: false,
|
|
8441
|
+
detail: err.message
|
|
8442
|
+
};
|
|
8443
|
+
}
|
|
8444
|
+
}
|
|
8445
|
+
function getRecommendation(projectType) {
|
|
8446
|
+
switch (projectType) {
|
|
8447
|
+
case "javascript":
|
|
8448
|
+
case "typescript":
|
|
8449
|
+
return {
|
|
8450
|
+
scanner: "eslint",
|
|
8451
|
+
canAutoInstall: true,
|
|
8452
|
+
installInstructions: "npm install --save-dev eslint @eslint/js"
|
|
8453
|
+
};
|
|
8454
|
+
case "python":
|
|
8455
|
+
return {
|
|
8456
|
+
scanner: "bandit",
|
|
8457
|
+
canAutoInstall: false,
|
|
8458
|
+
installInstructions: "pip install bandit (or: pipx install bandit, poetry add --group dev bandit)"
|
|
8459
|
+
};
|
|
8460
|
+
case "java":
|
|
8461
|
+
case "csharp":
|
|
8462
|
+
return {
|
|
8463
|
+
scanner: "semgrep",
|
|
8464
|
+
canAutoInstall: false,
|
|
8465
|
+
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8466
|
+
};
|
|
8467
|
+
case "unknown":
|
|
8468
|
+
return {
|
|
8469
|
+
scanner: "semgrep",
|
|
8470
|
+
canAutoInstall: false,
|
|
8471
|
+
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8472
|
+
};
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
|
|
8476
|
+
const detections = await detectScanners(workspaceRoot);
|
|
8477
|
+
const available = detections.filter((d) => d.available);
|
|
8478
|
+
if (available.length > 0) {
|
|
8479
|
+
const existingScanners = available.map((d) => d.scanner);
|
|
8480
|
+
logger2.info(
|
|
8481
|
+
{ existingScanners },
|
|
8482
|
+
"bootstrap: scanner(s) already configured \u2014 skipping"
|
|
8483
|
+
);
|
|
8484
|
+
return {
|
|
8485
|
+
projectType: detectProjectType(workspaceRoot),
|
|
8486
|
+
alreadyConfigured: true,
|
|
8487
|
+
existingScanners,
|
|
8488
|
+
steps: [],
|
|
8489
|
+
autoScanResult: null,
|
|
8490
|
+
success: true,
|
|
8491
|
+
summary: `Scanner(s) already configured: ${existingScanners.join(", ")}. Run auto_scan to ingest findings.`
|
|
8492
|
+
};
|
|
8493
|
+
}
|
|
8494
|
+
const projectType = detectProjectType(workspaceRoot);
|
|
8495
|
+
const recommendation = getRecommendation(projectType);
|
|
8496
|
+
const steps = [];
|
|
8497
|
+
logger2.info(
|
|
8498
|
+
{ projectType, scanner: recommendation.scanner },
|
|
8499
|
+
"bootstrap: detected project type"
|
|
8500
|
+
);
|
|
8501
|
+
if (recommendation.canAutoInstall) {
|
|
8502
|
+
const isTypeScript = projectType === "typescript";
|
|
8503
|
+
const packages = isTypeScript ? ["eslint", "@eslint/js", "typescript-eslint"] : ["eslint", "@eslint/js"];
|
|
8504
|
+
const installStep = await npmInstall(workspaceRoot, packages);
|
|
8505
|
+
steps.push(installStep);
|
|
8506
|
+
if (installStep.success) {
|
|
8507
|
+
const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
|
|
8508
|
+
steps.push(configStep);
|
|
8509
|
+
}
|
|
8510
|
+
} else {
|
|
8511
|
+
steps.push({
|
|
8512
|
+
action: `suggest ${recommendation.scanner} install`,
|
|
8513
|
+
success: true,
|
|
8514
|
+
detail: recommendation.installInstructions
|
|
8515
|
+
});
|
|
8516
|
+
}
|
|
8517
|
+
const installSucceeded = steps.every((s) => s.success);
|
|
8518
|
+
let autoScanResult = null;
|
|
8519
|
+
if (installSucceeded && recommendation.canAutoInstall) {
|
|
8520
|
+
try {
|
|
8521
|
+
const scanStart = Date.now();
|
|
8522
|
+
const postDetections = await detectScanners(workspaceRoot);
|
|
8523
|
+
const postAvailable = postDetections.filter((d) => d.available);
|
|
8524
|
+
const scanResults = [];
|
|
8525
|
+
let scanFindings = 0;
|
|
8526
|
+
const settled = await Promise.allSettled(
|
|
8527
|
+
postAvailable.map((d) => runScanner(d.scanner, workspaceRoot))
|
|
8528
|
+
);
|
|
8529
|
+
for (let i = 0; i < postAvailable.length; i++) {
|
|
8530
|
+
const det = postAvailable[i];
|
|
8531
|
+
const res = settled[i];
|
|
8532
|
+
if (res.status === "rejected" || !res.value.success) {
|
|
8533
|
+
scanResults.push({
|
|
8534
|
+
scanner: det.scanner,
|
|
8535
|
+
success: false,
|
|
8536
|
+
findingsIngested: 0,
|
|
8537
|
+
durationMs: res.status === "fulfilled" ? res.value.durationMs : 0,
|
|
8538
|
+
error: res.status === "rejected" ? String(res.reason) : res.value.error ?? "unknown error"
|
|
8539
|
+
});
|
|
8540
|
+
continue;
|
|
8541
|
+
}
|
|
8542
|
+
const runResult = res.value;
|
|
8543
|
+
let parsed;
|
|
8544
|
+
try {
|
|
8545
|
+
parsed = JSON.parse(runResult.rawOutput);
|
|
8546
|
+
} catch {
|
|
8547
|
+
parsed = runResult.rawOutput;
|
|
8548
|
+
}
|
|
8549
|
+
const adapted = adaptScannerOutput(runResult.scanner, parsed);
|
|
8550
|
+
const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
|
|
8551
|
+
scanFindings += stats.accepted;
|
|
8552
|
+
scanResults.push({
|
|
8553
|
+
scanner: runResult.scanner,
|
|
8554
|
+
success: true,
|
|
8555
|
+
findingsIngested: stats.accepted,
|
|
8556
|
+
durationMs: runResult.durationMs
|
|
8557
|
+
});
|
|
8558
|
+
}
|
|
8559
|
+
if (scanFindings > 0) await sarifStore.persist();
|
|
8560
|
+
autoScanResult = {
|
|
8561
|
+
detected: postDetections,
|
|
8562
|
+
results: scanResults,
|
|
8563
|
+
totalFindings: scanFindings,
|
|
8564
|
+
totalDurationMs: Date.now() - scanStart
|
|
8565
|
+
};
|
|
8566
|
+
} catch (err) {
|
|
8567
|
+
logger2.warn(
|
|
8568
|
+
{ err: err.message },
|
|
8569
|
+
"bootstrap: scan after install failed"
|
|
8570
|
+
);
|
|
8571
|
+
}
|
|
8572
|
+
}
|
|
8573
|
+
const findings = autoScanResult?.totalFindings ?? 0;
|
|
8574
|
+
const scannerInstalled = recommendation.canAutoInstall && installSucceeded;
|
|
8575
|
+
let summary;
|
|
8576
|
+
if (scannerInstalled && autoScanResult) {
|
|
8577
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan found ${findings} finding(s).`;
|
|
8578
|
+
} else if (scannerInstalled) {
|
|
8579
|
+
summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan did not run.`;
|
|
8580
|
+
} else if (!recommendation.canAutoInstall) {
|
|
8581
|
+
summary = `Detected ${projectType} project. Install ${recommendation.scanner} manually: ${recommendation.installInstructions}`;
|
|
8582
|
+
} else {
|
|
8583
|
+
summary = `Failed to install ${recommendation.scanner}. Check the error details in the steps.`;
|
|
8584
|
+
}
|
|
8585
|
+
return {
|
|
8586
|
+
projectType,
|
|
8587
|
+
alreadyConfigured: false,
|
|
8588
|
+
existingScanners: [],
|
|
8589
|
+
steps,
|
|
8590
|
+
autoScanResult,
|
|
8591
|
+
success: installSucceeded,
|
|
8592
|
+
summary
|
|
8593
|
+
};
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8343
8596
|
// src/scanner/auto-scan.ts
|
|
8344
8597
|
function ingestScannerRun(scanner, rawOutput, sarifStore) {
|
|
8345
8598
|
let parsed;
|
|
@@ -8364,6 +8617,18 @@ async function autoScan(workspaceRoot, sarifStore, logger2) {
|
|
|
8364
8617
|
"auto-scan: detection complete"
|
|
8365
8618
|
);
|
|
8366
8619
|
if (available.length === 0) {
|
|
8620
|
+
logger2.info("auto-scan: no scanners found, attempting bootstrap");
|
|
8621
|
+
try {
|
|
8622
|
+
const bootstrapResult = await bootstrapScanner(workspaceRoot, sarifStore, logger2);
|
|
8623
|
+
if (bootstrapResult.autoScanResult) {
|
|
8624
|
+
return bootstrapResult.autoScanResult;
|
|
8625
|
+
}
|
|
8626
|
+
} catch (err) {
|
|
8627
|
+
logger2.warn(
|
|
8628
|
+
{ err: err.message },
|
|
8629
|
+
"auto-scan: bootstrap failed \u2014 continuing with empty results"
|
|
8630
|
+
);
|
|
8631
|
+
}
|
|
8367
8632
|
return {
|
|
8368
8633
|
detected,
|
|
8369
8634
|
results: [],
|
|
@@ -8581,6 +8846,13 @@ var ingestScannerOutputSchema = {
|
|
|
8581
8846
|
required: ["scanner", "rawOutput"],
|
|
8582
8847
|
additionalProperties: false
|
|
8583
8848
|
};
|
|
8849
|
+
var bootstrapScannerSchema = {
|
|
8850
|
+
type: "object",
|
|
8851
|
+
description: "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.",
|
|
8852
|
+
properties: {},
|
|
8853
|
+
required: [],
|
|
8854
|
+
additionalProperties: false
|
|
8855
|
+
};
|
|
8584
8856
|
var autoScanSchema = {
|
|
8585
8857
|
type: "object",
|
|
8586
8858
|
description: "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, execute them, and ingest findings into the SARIF store. Returns detection results, per-scanner execution stats, and total findings ingested. Call this to populate findings without manual scanner invocation.",
|
|
@@ -8712,6 +8984,11 @@ async function main() {
|
|
|
8712
8984
|
name: "auto_scan",
|
|
8713
8985
|
description: "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
|
|
8714
8986
|
inputSchema: autoScanSchema
|
|
8987
|
+
},
|
|
8988
|
+
{
|
|
8989
|
+
name: "bootstrap_scanner",
|
|
8990
|
+
description: "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.",
|
|
8991
|
+
inputSchema: bootstrapScannerSchema
|
|
8715
8992
|
}
|
|
8716
8993
|
]
|
|
8717
8994
|
}));
|
|
@@ -8993,6 +9270,35 @@ async function main() {
|
|
|
8993
9270
|
};
|
|
8994
9271
|
}
|
|
8995
9272
|
}
|
|
9273
|
+
case "bootstrap_scanner": {
|
|
9274
|
+
logger.info({ tool: "bootstrap_scanner" }, "Tool call received");
|
|
9275
|
+
try {
|
|
9276
|
+
const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
9277
|
+
const markdown = renderBootstrapMarkdown(result);
|
|
9278
|
+
return {
|
|
9279
|
+
content: [
|
|
9280
|
+
{ type: "text", text: markdown },
|
|
9281
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
9282
|
+
],
|
|
9283
|
+
isError: !result.success
|
|
9284
|
+
};
|
|
9285
|
+
} catch (err) {
|
|
9286
|
+
logger.error({ err }, "bootstrap_scanner failed");
|
|
9287
|
+
return {
|
|
9288
|
+
content: [
|
|
9289
|
+
{
|
|
9290
|
+
type: "text",
|
|
9291
|
+
text: JSON.stringify(
|
|
9292
|
+
{ tool: "bootstrap_scanner", status: "error", message: err.message },
|
|
9293
|
+
null,
|
|
9294
|
+
2
|
|
9295
|
+
)
|
|
9296
|
+
}
|
|
9297
|
+
],
|
|
9298
|
+
isError: true
|
|
9299
|
+
};
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
8996
9302
|
case "auto_scan": {
|
|
8997
9303
|
logger.info({ tool: "auto_scan" }, "Tool call received");
|
|
8998
9304
|
try {
|
|
@@ -9089,6 +9395,36 @@ async function main() {
|
|
|
9089
9395
|
);
|
|
9090
9396
|
});
|
|
9091
9397
|
}
|
|
9398
|
+
function renderBootstrapMarkdown(result) {
|
|
9399
|
+
const lines = ["## claude-crap :: bootstrap scanner\n"];
|
|
9400
|
+
lines.push(`**Project type:** ${result.projectType}`);
|
|
9401
|
+
if (result.alreadyConfigured) {
|
|
9402
|
+
lines.push(`**Status:** Scanner(s) already configured: ${result.existingScanners.join(", ")}`);
|
|
9403
|
+
lines.push("\nNo installation needed. Run `auto_scan` to ingest findings.");
|
|
9404
|
+
return lines.join("\n");
|
|
9405
|
+
}
|
|
9406
|
+
lines.push("");
|
|
9407
|
+
if (result.steps.length > 0) {
|
|
9408
|
+
lines.push("### Steps\n");
|
|
9409
|
+
lines.push("| Action | Status | Detail |");
|
|
9410
|
+
lines.push("| ------ | :----: | ------ |");
|
|
9411
|
+
for (const s of result.steps) {
|
|
9412
|
+
const status = s.success ? "ok" : "failed";
|
|
9413
|
+
lines.push(`| ${s.action} | ${status} | ${s.detail} |`);
|
|
9414
|
+
}
|
|
9415
|
+
lines.push("");
|
|
9416
|
+
}
|
|
9417
|
+
if (result.autoScanResult) {
|
|
9418
|
+
const r = result.autoScanResult;
|
|
9419
|
+
const scanners = r.results.filter((s) => s.success).map((s) => s.scanner);
|
|
9420
|
+
lines.push(
|
|
9421
|
+
`**Auto-scan:** ${r.totalFindings} finding(s) ingested from ${scanners.join(", ") || "no scanners"} in ${(r.totalDurationMs / 1e3).toFixed(1)}s`
|
|
9422
|
+
);
|
|
9423
|
+
lines.push("");
|
|
9424
|
+
}
|
|
9425
|
+
lines.push(`**Summary:** ${result.summary}`);
|
|
9426
|
+
return lines.join("\n");
|
|
9427
|
+
}
|
|
9092
9428
|
function renderAutoScanMarkdown(result) {
|
|
9093
9429
|
const lines = ["## claude-crap :: auto-scan results\n"];
|
|
9094
9430
|
lines.push("### Detected scanners\n");
|