claude-crap 0.1.2 → 0.2.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 (39) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +43 -23
  3. package/dist/index.js +79 -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/detector.d.ts +53 -0
  10. package/dist/scanner/detector.d.ts.map +1 -0
  11. package/dist/scanner/detector.js +173 -0
  12. package/dist/scanner/detector.js.map +1 -0
  13. package/dist/scanner/index.d.ts +22 -0
  14. package/dist/scanner/index.d.ts.map +1 -0
  15. package/dist/scanner/index.js +22 -0
  16. package/dist/scanner/index.js.map +1 -0
  17. package/dist/scanner/runner.d.ts +59 -0
  18. package/dist/scanner/runner.d.ts.map +1 -0
  19. package/dist/scanner/runner.js +159 -0
  20. package/dist/scanner/runner.js.map +1 -0
  21. package/dist/schemas/tool-schemas.d.ts +11 -0
  22. package/dist/schemas/tool-schemas.d.ts.map +1 -1
  23. package/dist/schemas/tool-schemas.js +11 -0
  24. package/dist/schemas/tool-schemas.js.map +1 -1
  25. package/package.json +5 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/bundle/mcp-server.mjs +452 -0
  28. package/plugin/bundle/mcp-server.mjs.map +4 -4
  29. package/plugin/package.json +1 -1
  30. package/src/index.ts +98 -0
  31. package/src/scanner/auto-scan.ts +212 -0
  32. package/src/scanner/detector.ts +224 -0
  33. package/src/scanner/index.ts +22 -0
  34. package/src/scanner/runner.ts +212 -0
  35. package/src/schemas/tool-schemas.ts +13 -0
  36. package/src/tests/auto-scan.test.ts +137 -0
  37. package/src/tests/integration/mcp-server.integration.test.ts +2 -1
  38. package/src/tests/scanner-detector.test.ts +181 -0
  39. package/src/tests/scanner-runner.test.ts +63 -0
@@ -8083,6 +8083,377 @@ 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
+
8086
8457
  // src/schemas/tool-schemas.ts
8087
8458
  var computeCrapSchema = {
8088
8459
  type: "object",
@@ -8210,6 +8581,13 @@ var ingestScannerOutputSchema = {
8210
8581
  required: ["scanner", "rawOutput"],
8211
8582
  additionalProperties: false
8212
8583
  };
8584
+ var autoScanSchema = {
8585
+ type: "object",
8586
+ 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.",
8587
+ properties: {},
8588
+ required: [],
8589
+ additionalProperties: false
8590
+ };
8213
8591
  var ingestSarifSchema = {
8214
8592
  type: "object",
8215
8593
  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 +8707,11 @@ async function main() {
8329
8707
  name: "score_project",
8330
8708
  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
8709
  inputSchema: scoreProjectSchema
8710
+ },
8711
+ {
8712
+ name: "auto_scan",
8713
+ description: "Auto-detect available scanners (ESLint, Semgrep, Bandit, Stryker) in the workspace, run them, and ingest findings into the SARIF store.",
8714
+ inputSchema: autoScanSchema
8332
8715
  }
8333
8716
  ]
8334
8717
  }));
@@ -8610,6 +8993,34 @@ async function main() {
8610
8993
  };
8611
8994
  }
8612
8995
  }
8996
+ case "auto_scan": {
8997
+ logger.info({ tool: "auto_scan" }, "Tool call received");
8998
+ try {
8999
+ const result = await autoScan(config.pluginRoot, sarifStore, logger);
9000
+ const markdown = renderAutoScanMarkdown(result);
9001
+ return {
9002
+ content: [
9003
+ { type: "text", text: markdown },
9004
+ { type: "text", text: JSON.stringify(result, null, 2) }
9005
+ ]
9006
+ };
9007
+ } catch (err) {
9008
+ logger.error({ err }, "auto_scan failed");
9009
+ return {
9010
+ content: [
9011
+ {
9012
+ type: "text",
9013
+ text: JSON.stringify(
9014
+ { tool: "auto_scan", status: "error", message: err.message },
9015
+ null,
9016
+ 2
9017
+ )
9018
+ }
9019
+ ],
9020
+ isError: true
9021
+ };
9022
+ }
9023
+ }
8613
9024
  default:
8614
9025
  throw new Error(`[claude-crap] Unknown tool: ${name}`);
8615
9026
  }
@@ -8661,6 +9072,47 @@ async function main() {
8661
9072
  const transport = new StdioServerTransport();
8662
9073
  await server.connect(transport);
8663
9074
  logger.info("claude-crap MCP server ready (stdio)");
9075
+ autoScan(config.pluginRoot, sarifStore, logger).then((result) => {
9076
+ const scanners = result.results.filter((r) => r.success).map((r) => r.scanner);
9077
+ logger.info(
9078
+ {
9079
+ scannersRun: scanners,
9080
+ totalFindings: result.totalFindings,
9081
+ durationMs: result.totalDurationMs
9082
+ },
9083
+ "auto-scan completed"
9084
+ );
9085
+ }).catch((err) => {
9086
+ logger.warn(
9087
+ { err: err.message },
9088
+ "auto-scan failed \u2014 continuing without it"
9089
+ );
9090
+ });
9091
+ }
9092
+ function renderAutoScanMarkdown(result) {
9093
+ const lines = ["## claude-crap :: auto-scan results\n"];
9094
+ lines.push("### Detected scanners\n");
9095
+ lines.push("| Scanner | Available | Reason |");
9096
+ lines.push("| ------- | :-------: | ------ |");
9097
+ for (const d of result.detected) {
9098
+ lines.push(`| ${d.scanner} | ${d.available ? "yes" : "no"} | ${d.reason} |`);
9099
+ }
9100
+ lines.push("");
9101
+ if (result.results.length > 0) {
9102
+ lines.push("### Execution results\n");
9103
+ lines.push("| Scanner | Status | Findings | Duration |");
9104
+ lines.push("| ------- | :----: | :------: | -------: |");
9105
+ for (const r of result.results) {
9106
+ const status = r.success ? "ok" : "failed";
9107
+ const duration = `${(r.durationMs / 1e3).toFixed(1)}s`;
9108
+ lines.push(`| ${r.scanner} | ${status} | ${r.findingsIngested} | ${duration} |`);
9109
+ }
9110
+ lines.push("");
9111
+ }
9112
+ lines.push(
9113
+ `**Total findings ingested:** ${result.totalFindings} in ${(result.totalDurationMs / 1e3).toFixed(1)}s`
9114
+ );
9115
+ return lines.join("\n");
8664
9116
  }
8665
9117
  function safeLoadStrictness(workspaceRoot, logger2) {
8666
9118
  try {