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
@@ -8083,6 +8083,586 @@ function resolveWithinWorkspace(workspaceRoot, filePath) {
8083
8083
  return candidate;
8084
8084
  }
8085
8085
 
8086
+ // src/scanner/detector.ts
8087
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
8088
+ import { join as join6 } from "node:path";
8089
+ import { execFile } from "node:child_process";
8090
+ var SCANNER_SIGNALS = {
8091
+ eslint: {
8092
+ configFiles: [
8093
+ "eslint.config.js",
8094
+ "eslint.config.mjs",
8095
+ "eslint.config.cjs",
8096
+ "eslint.config.ts",
8097
+ "eslint.config.mts",
8098
+ "eslint.config.cts",
8099
+ ".eslintrc.js",
8100
+ ".eslintrc.cjs",
8101
+ ".eslintrc.yaml",
8102
+ ".eslintrc.yml",
8103
+ ".eslintrc.json"
8104
+ ],
8105
+ packageJsonKeys: ["eslint"],
8106
+ binaryNames: ["eslint"]
8107
+ },
8108
+ semgrep: {
8109
+ configFiles: [
8110
+ ".semgrep.yml",
8111
+ ".semgrep.yaml",
8112
+ ".semgrep.json"
8113
+ ],
8114
+ packageJsonKeys: [],
8115
+ binaryNames: ["semgrep"]
8116
+ },
8117
+ bandit: {
8118
+ configFiles: [
8119
+ ".bandit",
8120
+ "bandit.yaml",
8121
+ "bandit.yml"
8122
+ ],
8123
+ packageJsonKeys: [],
8124
+ binaryNames: ["bandit"]
8125
+ },
8126
+ stryker: {
8127
+ configFiles: [
8128
+ "stryker.conf.js",
8129
+ "stryker.conf.mjs",
8130
+ "stryker.conf.cjs",
8131
+ "stryker.conf.json",
8132
+ ".strykerrc",
8133
+ ".strykerrc.json"
8134
+ ],
8135
+ packageJsonKeys: ["@stryker-mutator/core"],
8136
+ binaryNames: ["stryker"]
8137
+ }
8138
+ };
8139
+ function probeConfigFiles(workspaceRoot, scanner) {
8140
+ const signals = SCANNER_SIGNALS[scanner];
8141
+ for (const file of signals.configFiles) {
8142
+ const fullPath = join6(workspaceRoot, file);
8143
+ if (existsSync(fullPath)) {
8144
+ return { found: true, path: fullPath };
8145
+ }
8146
+ }
8147
+ return { found: false };
8148
+ }
8149
+ function probePackageJson(workspaceRoot, scanner) {
8150
+ const signals = SCANNER_SIGNALS[scanner];
8151
+ if (signals.packageJsonKeys.length === 0) return false;
8152
+ const pkgPath = join6(workspaceRoot, "package.json");
8153
+ if (!existsSync(pkgPath)) return false;
8154
+ try {
8155
+ const raw = readFileSync2(pkgPath, "utf-8");
8156
+ const pkg = JSON.parse(raw);
8157
+ const deps = {
8158
+ ...typeof pkg.dependencies === "object" && pkg.dependencies !== null ? pkg.dependencies : {},
8159
+ ...typeof pkg.devDependencies === "object" && pkg.devDependencies !== null ? pkg.devDependencies : {}
8160
+ };
8161
+ return signals.packageJsonKeys.some((key) => key in deps);
8162
+ } catch {
8163
+ return false;
8164
+ }
8165
+ }
8166
+ function probeBinary(binaryName) {
8167
+ return new Promise((resolve6) => {
8168
+ execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
8169
+ resolve6(err === null);
8170
+ });
8171
+ });
8172
+ }
8173
+ async function detectScanners(workspaceRoot) {
8174
+ const scanners = ["eslint", "semgrep", "bandit", "stryker"];
8175
+ const results = await Promise.all(
8176
+ scanners.map(async (scanner) => {
8177
+ const configProbe = probeConfigFiles(workspaceRoot, scanner);
8178
+ if (configProbe.found && configProbe.path) {
8179
+ return {
8180
+ scanner,
8181
+ available: true,
8182
+ reason: `config file found: ${configProbe.path.replace(workspaceRoot + "/", "")}`,
8183
+ configPath: configProbe.path
8184
+ };
8185
+ }
8186
+ if (probePackageJson(workspaceRoot, scanner)) {
8187
+ return {
8188
+ scanner,
8189
+ available: true,
8190
+ reason: `found in package.json dependencies`
8191
+ };
8192
+ }
8193
+ const signals = SCANNER_SIGNALS[scanner];
8194
+ for (const bin of signals.binaryNames) {
8195
+ if (await probeBinary(bin)) {
8196
+ return {
8197
+ scanner,
8198
+ available: true,
8199
+ reason: `binary "${bin}" found on PATH`
8200
+ };
8201
+ }
8202
+ }
8203
+ return {
8204
+ scanner,
8205
+ available: false,
8206
+ reason: "no config file, package.json entry, or binary found"
8207
+ };
8208
+ })
8209
+ );
8210
+ return results;
8211
+ }
8212
+
8213
+ // src/scanner/runner.ts
8214
+ import { execFile as execFile2 } from "node:child_process";
8215
+ import { readFileSync as readFileSync3, existsSync as existsSync2 } from "node:fs";
8216
+ import { join as join7 } from "node:path";
8217
+ function getScannerCommand(scanner, workspaceRoot) {
8218
+ switch (scanner) {
8219
+ case "eslint":
8220
+ return {
8221
+ command: "npx",
8222
+ args: ["eslint", "-f", "json", "."],
8223
+ timeoutMs: 12e4,
8224
+ nonZeroIsNormal: true
8225
+ };
8226
+ case "semgrep":
8227
+ return {
8228
+ command: "semgrep",
8229
+ args: ["--sarif", "--quiet", "."],
8230
+ timeoutMs: 12e4,
8231
+ nonZeroIsNormal: false
8232
+ };
8233
+ case "bandit":
8234
+ return {
8235
+ command: "bandit",
8236
+ args: ["-f", "json", "-r", ".", "-q"],
8237
+ timeoutMs: 12e4,
8238
+ nonZeroIsNormal: true
8239
+ };
8240
+ case "stryker":
8241
+ return {
8242
+ command: "npx",
8243
+ args: ["stryker", "run"],
8244
+ timeoutMs: 3e5,
8245
+ nonZeroIsNormal: false,
8246
+ outputFile: join7(workspaceRoot, "reports", "mutation", "mutation.json")
8247
+ };
8248
+ }
8249
+ }
8250
+ function runScanner(scanner, workspaceRoot) {
8251
+ const start = Date.now();
8252
+ const cmd = getScannerCommand(scanner, workspaceRoot);
8253
+ return new Promise((resolve6) => {
8254
+ execFile2(
8255
+ cmd.command,
8256
+ cmd.args,
8257
+ {
8258
+ cwd: workspaceRoot,
8259
+ timeout: cmd.timeoutMs,
8260
+ maxBuffer: 50 * 1024 * 1024,
8261
+ // 50 MB — large codebases produce verbose output
8262
+ env: { ...process.env, FORCE_COLOR: "0" }
8263
+ // suppress ANSI in output
8264
+ },
8265
+ (err, stdout, stderr) => {
8266
+ const durationMs = Date.now() - start;
8267
+ if (err && !cmd.nonZeroIsNormal) {
8268
+ if (cmd.outputFile && existsSync2(cmd.outputFile)) {
8269
+ try {
8270
+ const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8271
+ resolve6({
8272
+ scanner,
8273
+ success: true,
8274
+ rawOutput: fileOutput,
8275
+ durationMs
8276
+ });
8277
+ return;
8278
+ } catch {
8279
+ }
8280
+ }
8281
+ resolve6({
8282
+ scanner,
8283
+ success: false,
8284
+ rawOutput: "",
8285
+ error: stderr || err.message,
8286
+ durationMs
8287
+ });
8288
+ return;
8289
+ }
8290
+ if (cmd.outputFile) {
8291
+ if (existsSync2(cmd.outputFile)) {
8292
+ try {
8293
+ const fileOutput = readFileSync3(cmd.outputFile, "utf-8");
8294
+ resolve6({
8295
+ scanner,
8296
+ success: true,
8297
+ rawOutput: fileOutput,
8298
+ durationMs
8299
+ });
8300
+ return;
8301
+ } catch (readErr) {
8302
+ resolve6({
8303
+ scanner,
8304
+ success: false,
8305
+ rawOutput: "",
8306
+ error: `Failed to read output file: ${readErr.message}`,
8307
+ durationMs
8308
+ });
8309
+ return;
8310
+ }
8311
+ }
8312
+ resolve6({
8313
+ scanner,
8314
+ success: false,
8315
+ rawOutput: "",
8316
+ error: `Scanner completed but output file not found: ${cmd.outputFile}`,
8317
+ durationMs
8318
+ });
8319
+ return;
8320
+ }
8321
+ const output = stdout.trim();
8322
+ if (!output) {
8323
+ resolve6({
8324
+ scanner,
8325
+ success: true,
8326
+ rawOutput: "[]",
8327
+ // ESLint returns empty when no files match
8328
+ durationMs
8329
+ });
8330
+ return;
8331
+ }
8332
+ resolve6({
8333
+ scanner,
8334
+ success: true,
8335
+ rawOutput: output,
8336
+ durationMs
8337
+ });
8338
+ }
8339
+ );
8340
+ });
8341
+ }
8342
+
8343
+ // src/scanner/auto-scan.ts
8344
+ function ingestScannerRun(scanner, rawOutput, sarifStore) {
8345
+ let parsed;
8346
+ try {
8347
+ parsed = JSON.parse(rawOutput);
8348
+ } catch {
8349
+ parsed = rawOutput;
8350
+ }
8351
+ const adapted = adaptScannerOutput(scanner, parsed);
8352
+ const stats = sarifStore.ingestRun(adapted.document, adapted.sourceTool);
8353
+ return { accepted: stats.accepted };
8354
+ }
8355
+ async function autoScan(workspaceRoot, sarifStore, logger2) {
8356
+ const start = Date.now();
8357
+ const detected = await detectScanners(workspaceRoot);
8358
+ const available = detected.filter((d) => d.available);
8359
+ logger2.info(
8360
+ {
8361
+ detected: detected.map((d) => `${d.scanner}:${d.available}`),
8362
+ available: available.length
8363
+ },
8364
+ "auto-scan: detection complete"
8365
+ );
8366
+ if (available.length === 0) {
8367
+ return {
8368
+ detected,
8369
+ results: [],
8370
+ totalFindings: 0,
8371
+ totalDurationMs: Date.now() - start
8372
+ };
8373
+ }
8374
+ const runResults = await Promise.allSettled(
8375
+ available.map((d) => runScanner(d.scanner, workspaceRoot))
8376
+ );
8377
+ const results = [];
8378
+ let totalFindings = 0;
8379
+ let persistNeeded = false;
8380
+ for (let i = 0; i < available.length; i++) {
8381
+ const detection = available[i];
8382
+ const settled = runResults[i];
8383
+ if (settled.status === "rejected") {
8384
+ const error = String(settled.reason);
8385
+ logger2.warn(
8386
+ { scanner: detection.scanner, error },
8387
+ "auto-scan: scanner execution rejected"
8388
+ );
8389
+ results.push({
8390
+ scanner: detection.scanner,
8391
+ success: false,
8392
+ findingsIngested: 0,
8393
+ durationMs: 0,
8394
+ error
8395
+ });
8396
+ continue;
8397
+ }
8398
+ const runResult = settled.value;
8399
+ if (!runResult.success) {
8400
+ logger2.warn(
8401
+ { scanner: runResult.scanner, error: runResult.error },
8402
+ "auto-scan: scanner returned failure"
8403
+ );
8404
+ results.push({
8405
+ scanner: runResult.scanner,
8406
+ success: false,
8407
+ findingsIngested: 0,
8408
+ durationMs: runResult.durationMs,
8409
+ error: runResult.error ?? "unknown error"
8410
+ });
8411
+ continue;
8412
+ }
8413
+ try {
8414
+ const { accepted } = ingestScannerRun(
8415
+ runResult.scanner,
8416
+ runResult.rawOutput,
8417
+ sarifStore
8418
+ );
8419
+ totalFindings += accepted;
8420
+ persistNeeded = true;
8421
+ logger2.info(
8422
+ { scanner: runResult.scanner, accepted, durationMs: runResult.durationMs },
8423
+ "auto-scan: scanner ingested"
8424
+ );
8425
+ results.push({
8426
+ scanner: runResult.scanner,
8427
+ success: true,
8428
+ findingsIngested: accepted,
8429
+ durationMs: runResult.durationMs
8430
+ });
8431
+ } catch (err) {
8432
+ const error = err.message;
8433
+ logger2.warn(
8434
+ { scanner: runResult.scanner, error },
8435
+ "auto-scan: adapter/ingestion failed"
8436
+ );
8437
+ results.push({
8438
+ scanner: runResult.scanner,
8439
+ success: false,
8440
+ findingsIngested: 0,
8441
+ durationMs: runResult.durationMs,
8442
+ error
8443
+ });
8444
+ }
8445
+ }
8446
+ if (persistNeeded) {
8447
+ await sarifStore.persist();
8448
+ }
8449
+ return {
8450
+ detected,
8451
+ results,
8452
+ totalFindings,
8453
+ totalDurationMs: Date.now() - start
8454
+ };
8455
+ }
8456
+
8457
+ // src/scanner/bootstrap.ts
8458
+ import { existsSync as existsSync3, writeFileSync, readdirSync } from "node:fs";
8459
+ import { join as join8 } from "node:path";
8460
+ import { execFile as execFile3 } from "node:child_process";
8461
+ function detectProjectType(workspaceRoot) {
8462
+ const has = (file) => existsSync3(join8(workspaceRoot, file));
8463
+ if (has("package.json")) {
8464
+ if (has("tsconfig.json")) return "typescript";
8465
+ return "javascript";
8466
+ }
8467
+ if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
8468
+ return "python";
8469
+ }
8470
+ if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
8471
+ return "java";
8472
+ }
8473
+ if (has("Directory.Build.props")) return "csharp";
8474
+ try {
8475
+ const entries = readdirSync(workspaceRoot);
8476
+ if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
8477
+ return "csharp";
8478
+ }
8479
+ } catch {
8480
+ }
8481
+ return "unknown";
8482
+ }
8483
+ function generateEslintConfig(isTypeScript) {
8484
+ if (isTypeScript) {
8485
+ return `import js from "@eslint/js";
8486
+ import tseslint from "typescript-eslint";
8487
+
8488
+ export default tseslint.config(
8489
+ js.configs.recommended,
8490
+ ...tseslint.configs.recommended,
8491
+ {
8492
+ ignores: ["dist/", "node_modules/", "coverage/"],
8493
+ },
8494
+ );
8495
+ `;
8496
+ }
8497
+ return `import js from "@eslint/js";
8498
+
8499
+ export default [
8500
+ js.configs.recommended,
8501
+ {
8502
+ ignores: ["dist/", "node_modules/", "coverage/"],
8503
+ },
8504
+ ];
8505
+ `;
8506
+ }
8507
+ function npmInstall(workspaceRoot, packages) {
8508
+ return new Promise((resolve6) => {
8509
+ execFile3(
8510
+ "npm",
8511
+ ["install", "--save-dev", ...packages],
8512
+ {
8513
+ cwd: workspaceRoot,
8514
+ timeout: 12e4,
8515
+ env: { ...process.env, FORCE_COLOR: "0" }
8516
+ },
8517
+ (err, stdout, stderr) => {
8518
+ if (err) {
8519
+ resolve6({
8520
+ action: `npm install --save-dev ${packages.join(" ")}`,
8521
+ success: false,
8522
+ detail: stderr || err.message
8523
+ });
8524
+ return;
8525
+ }
8526
+ resolve6({
8527
+ action: `npm install --save-dev ${packages.join(" ")}`,
8528
+ success: true,
8529
+ detail: `installed ${packages.join(", ")}`
8530
+ });
8531
+ }
8532
+ );
8533
+ });
8534
+ }
8535
+ function writeEslintConfigFile(workspaceRoot, isTypeScript) {
8536
+ const configPath = join8(workspaceRoot, "eslint.config.mjs");
8537
+ if (existsSync3(configPath)) {
8538
+ return {
8539
+ action: "create eslint.config.mjs",
8540
+ success: true,
8541
+ detail: "eslint.config.mjs already exists \u2014 skipped"
8542
+ };
8543
+ }
8544
+ try {
8545
+ writeFileSync(configPath, generateEslintConfig(isTypeScript), "utf-8");
8546
+ return {
8547
+ action: "create eslint.config.mjs",
8548
+ success: true,
8549
+ detail: `created eslint.config.mjs (${isTypeScript ? "TypeScript" : "JavaScript"} template)`
8550
+ };
8551
+ } catch (err) {
8552
+ return {
8553
+ action: "create eslint.config.mjs",
8554
+ success: false,
8555
+ detail: err.message
8556
+ };
8557
+ }
8558
+ }
8559
+ function getRecommendation(projectType) {
8560
+ switch (projectType) {
8561
+ case "javascript":
8562
+ case "typescript":
8563
+ return {
8564
+ scanner: "eslint",
8565
+ canAutoInstall: true,
8566
+ installInstructions: "npm install --save-dev eslint @eslint/js"
8567
+ };
8568
+ case "python":
8569
+ return {
8570
+ scanner: "bandit",
8571
+ canAutoInstall: false,
8572
+ installInstructions: "pip install bandit (or: pipx install bandit, poetry add --group dev bandit)"
8573
+ };
8574
+ case "java":
8575
+ case "csharp":
8576
+ return {
8577
+ scanner: "semgrep",
8578
+ canAutoInstall: false,
8579
+ installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
8580
+ };
8581
+ case "unknown":
8582
+ return {
8583
+ scanner: "semgrep",
8584
+ canAutoInstall: false,
8585
+ installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
8586
+ };
8587
+ }
8588
+ }
8589
+ async function bootstrapScanner(workspaceRoot, sarifStore, logger2) {
8590
+ const detections = await detectScanners(workspaceRoot);
8591
+ const available = detections.filter((d) => d.available);
8592
+ if (available.length > 0) {
8593
+ const existingScanners = available.map((d) => d.scanner);
8594
+ logger2.info(
8595
+ { existingScanners },
8596
+ "bootstrap: scanner(s) already configured \u2014 skipping"
8597
+ );
8598
+ return {
8599
+ projectType: detectProjectType(workspaceRoot),
8600
+ alreadyConfigured: true,
8601
+ existingScanners,
8602
+ steps: [],
8603
+ autoScanResult: null,
8604
+ success: true,
8605
+ summary: `Scanner(s) already configured: ${existingScanners.join(", ")}. Run auto_scan to ingest findings.`
8606
+ };
8607
+ }
8608
+ const projectType = detectProjectType(workspaceRoot);
8609
+ const recommendation = getRecommendation(projectType);
8610
+ const steps = [];
8611
+ logger2.info(
8612
+ { projectType, scanner: recommendation.scanner },
8613
+ "bootstrap: detected project type"
8614
+ );
8615
+ if (recommendation.canAutoInstall) {
8616
+ const isTypeScript = projectType === "typescript";
8617
+ const packages = isTypeScript ? ["eslint", "@eslint/js", "typescript-eslint"] : ["eslint", "@eslint/js"];
8618
+ const installStep = await npmInstall(workspaceRoot, packages);
8619
+ steps.push(installStep);
8620
+ if (installStep.success) {
8621
+ const configStep = writeEslintConfigFile(workspaceRoot, isTypeScript);
8622
+ steps.push(configStep);
8623
+ }
8624
+ } else {
8625
+ steps.push({
8626
+ action: `suggest ${recommendation.scanner} install`,
8627
+ success: true,
8628
+ detail: recommendation.installInstructions
8629
+ });
8630
+ }
8631
+ const installSucceeded = steps.every((s) => s.success);
8632
+ let autoScanResult = null;
8633
+ if (installSucceeded && recommendation.canAutoInstall) {
8634
+ try {
8635
+ autoScanResult = await autoScan(workspaceRoot, sarifStore, logger2);
8636
+ } catch (err) {
8637
+ logger2.warn(
8638
+ { err: err.message },
8639
+ "bootstrap: auto_scan after install failed"
8640
+ );
8641
+ }
8642
+ }
8643
+ const findings = autoScanResult?.totalFindings ?? 0;
8644
+ const scannerInstalled = recommendation.canAutoInstall && installSucceeded;
8645
+ let summary;
8646
+ if (scannerInstalled && autoScanResult) {
8647
+ summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan found ${findings} finding(s).`;
8648
+ } else if (scannerInstalled) {
8649
+ summary = `Installed ${recommendation.scanner} for ${projectType} project. Auto-scan did not run.`;
8650
+ } else if (!recommendation.canAutoInstall) {
8651
+ summary = `Detected ${projectType} project. Install ${recommendation.scanner} manually: ${recommendation.installInstructions}`;
8652
+ } else {
8653
+ summary = `Failed to install ${recommendation.scanner}. Check the error details in the steps.`;
8654
+ }
8655
+ return {
8656
+ projectType,
8657
+ alreadyConfigured: false,
8658
+ existingScanners: [],
8659
+ steps,
8660
+ autoScanResult,
8661
+ success: installSucceeded,
8662
+ summary
8663
+ };
8664
+ }
8665
+
8086
8666
  // src/schemas/tool-schemas.ts
8087
8667
  var computeCrapSchema = {
8088
8668
  type: "object",
@@ -8210,6 +8790,20 @@ var ingestScannerOutputSchema = {
8210
8790
  required: ["scanner", "rawOutput"],
8211
8791
  additionalProperties: false
8212
8792
  };
8793
+ var bootstrapScannerSchema = {
8794
+ type: "object",
8795
+ 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.",
8796
+ properties: {},
8797
+ required: [],
8798
+ additionalProperties: false
8799
+ };
8800
+ var autoScanSchema = {
8801
+ type: "object",
8802
+ 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.",
8803
+ properties: {},
8804
+ required: [],
8805
+ additionalProperties: false
8806
+ };
8213
8807
  var ingestSarifSchema = {
8214
8808
  type: "object",
8215
8809
  description: "Ingest a raw SARIF 2.1.0 report produced by an external scanner (Semgrep, ESLint, Bandit, Stryker, etc.), deduplicate it against the internal store, and return the normalized document. The agent should call this once per scanner invocation, not once per finding.",
@@ -8329,6 +8923,16 @@ async function main() {
8329
8923
  name: "score_project",
8330
8924
  description: "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.",
8331
8925
  inputSchema: scoreProjectSchema
8926
+ },
8927
+ {
8928
+ name: "auto_scan",
8929
+ description: "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
8930
+ inputSchema: autoScanSchema
8931
+ },
8932
+ {
8933
+ name: "bootstrap_scanner",
8934
+ 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.",
8935
+ inputSchema: bootstrapScannerSchema
8332
8936
  }
8333
8937
  ]
8334
8938
  }));
@@ -8610,6 +9214,63 @@ async function main() {
8610
9214
  };
8611
9215
  }
8612
9216
  }
9217
+ case "bootstrap_scanner": {
9218
+ logger.info({ tool: "bootstrap_scanner" }, "Tool call received");
9219
+ try {
9220
+ const result = await bootstrapScanner(config.pluginRoot, sarifStore, logger);
9221
+ const markdown = renderBootstrapMarkdown(result);
9222
+ return {
9223
+ content: [
9224
+ { type: "text", text: markdown },
9225
+ { type: "text", text: JSON.stringify(result, null, 2) }
9226
+ ],
9227
+ isError: !result.success
9228
+ };
9229
+ } catch (err) {
9230
+ logger.error({ err }, "bootstrap_scanner failed");
9231
+ return {
9232
+ content: [
9233
+ {
9234
+ type: "text",
9235
+ text: JSON.stringify(
9236
+ { tool: "bootstrap_scanner", status: "error", message: err.message },
9237
+ null,
9238
+ 2
9239
+ )
9240
+ }
9241
+ ],
9242
+ isError: true
9243
+ };
9244
+ }
9245
+ }
9246
+ case "auto_scan": {
9247
+ logger.info({ tool: "auto_scan" }, "Tool call received");
9248
+ try {
9249
+ const result = await autoScan(config.pluginRoot, sarifStore, logger);
9250
+ const markdown = renderAutoScanMarkdown(result);
9251
+ return {
9252
+ content: [
9253
+ { type: "text", text: markdown },
9254
+ { type: "text", text: JSON.stringify(result, null, 2) }
9255
+ ]
9256
+ };
9257
+ } catch (err) {
9258
+ logger.error({ err }, "auto_scan failed");
9259
+ return {
9260
+ content: [
9261
+ {
9262
+ type: "text",
9263
+ text: JSON.stringify(
9264
+ { tool: "auto_scan", status: "error", message: err.message },
9265
+ null,
9266
+ 2
9267
+ )
9268
+ }
9269
+ ],
9270
+ isError: true
9271
+ };
9272
+ }
9273
+ }
8613
9274
  default:
8614
9275
  throw new Error(`[claude-crap] Unknown tool: ${name}`);
8615
9276
  }
@@ -8661,6 +9322,77 @@ async function main() {
8661
9322
  const transport = new StdioServerTransport();
8662
9323
  await server.connect(transport);
8663
9324
  logger.info("claude-crap MCP server ready (stdio)");
9325
+ autoScan(config.pluginRoot, sarifStore, logger).then((result) => {
9326
+ const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
9327
+ logger.info(
9328
+ {
9329
+ scannersRun: scanners,
9330
+ totalFindings: result.totalFindings,
9331
+ durationMs: result.totalDurationMs
9332
+ },
9333
+ "auto-scan completed"
9334
+ );
9335
+ }).catch((err) => {
9336
+ logger.warn(
9337
+ { err: err.message },
9338
+ "auto-scan failed \u2014 continuing without it"
9339
+ );
9340
+ });
9341
+ }
9342
+ function renderBootstrapMarkdown(result) {
9343
+ const lines = ["## claude-crap :: bootstrap scanner\n"];
9344
+ lines.push(`**Project type:** ${result.projectType}`);
9345
+ if (result.alreadyConfigured) {
9346
+ lines.push(`**Status:** Scanner(s) already configured: ${result.existingScanners.join(", ")}`);
9347
+ lines.push("\nNo installation needed. Run `auto_scan` to ingest findings.");
9348
+ return lines.join("\n");
9349
+ }
9350
+ lines.push("");
9351
+ if (result.steps.length > 0) {
9352
+ lines.push("### Steps\n");
9353
+ lines.push("| Action | Status | Detail |");
9354
+ lines.push("| ------ | :----: | ------ |");
9355
+ for (const s of result.steps) {
9356
+ const status = s.success ? "ok" : "failed";
9357
+ lines.push(`| ${s.action} | ${status} | ${s.detail} |`);
9358
+ }
9359
+ lines.push("");
9360
+ }
9361
+ if (result.autoScanResult) {
9362
+ const r = result.autoScanResult;
9363
+ const scanners = r.results.filter((s) => s.success).map((s) => s.scanner);
9364
+ lines.push(
9365
+ `**Auto-scan:** ${r.totalFindings} finding(s) ingested from ${scanners.join(", ") || "no scanners"} in ${(r.totalDurationMs / 1e3).toFixed(1)}s`
9366
+ );
9367
+ lines.push("");
9368
+ }
9369
+ lines.push(`**Summary:** ${result.summary}`);
9370
+ return lines.join("\n");
9371
+ }
9372
+ function renderAutoScanMarkdown(result) {
9373
+ const lines = ["## claude-crap :: auto-scan results\n"];
9374
+ lines.push("### Detected scanners\n");
9375
+ lines.push("| Scanner | Available | Reason |");
9376
+ lines.push("| ------- | :-------: | ------ |");
9377
+ for (const d of result.detected) {
9378
+ lines.push(`| ${d.scanner} | ${d.available ? "yes" : "no"} | ${d.reason} |`);
9379
+ }
9380
+ lines.push("");
9381
+ if (result.results.length > 0) {
9382
+ lines.push("### Execution results\n");
9383
+ lines.push("| Scanner | Status | Findings | Duration |");
9384
+ lines.push("| ------- | :----: | :------: | -------: |");
9385
+ for (const r of result.results) {
9386
+ const status = r.success ? "ok" : "failed";
9387
+ const duration = `${(r.durationMs / 1e3).toFixed(1)}s`;
9388
+ lines.push(`| ${r.scanner} | ${status} | ${r.findingsIngested} | ${duration} |`);
9389
+ }
9390
+ lines.push("");
9391
+ }
9392
+ lines.push(
9393
+ `**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1e3).toFixed(1)}s`
9394
+ );
9395
+ return lines.join("\n");
8664
9396
  }
8665
9397
  function safeLoadStrictness(workspaceRoot, logger2) {
8666
9398
  try {