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.
@@ -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");