claude-crap 0.3.8 → 0.4.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 +69 -27
- package/dist/adapters/common.d.ts +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/dotnet-format.d.ts +35 -0
- package/dist/adapters/dotnet-format.d.ts.map +1 -0
- package/dist/adapters/dotnet-format.js +96 -0
- package/dist/adapters/dotnet-format.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +4 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +2 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +19 -4
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/server.js +1 -1
- package/dist/index.js +74 -5
- package/dist/index.js.map +1 -1
- package/dist/monorepo/project-map.d.ts +112 -0
- package/dist/monorepo/project-map.d.ts.map +1 -0
- package/dist/monorepo/project-map.js +384 -0
- package/dist/monorepo/project-map.js.map +1 -0
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +6 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +7 -2
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +13 -0
- package/dist/scanner/runner.js.map +1 -1
- package/dist/schemas/tool-schemas.d.ts +16 -1
- package/dist/schemas/tool-schemas.d.ts.map +1 -1
- package/dist/schemas/tool-schemas.js +16 -1
- package/dist/schemas/tool-schemas.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +395 -29
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +4 -0
- package/src/crap-config.ts +27 -4
- package/src/dashboard/server.ts +1 -1
- package/src/index.ts +88 -5
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/bootstrap.ts +7 -1
- package/src/scanner/detector.ts +7 -2
- package/src/scanner/runner.ts +13 -0
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/tests/adapters/dispatch.test.ts +1 -1
- package/src/tests/auto-scan.test.ts +2 -2
- package/src/tests/boot-monorepo.test.ts +804 -0
- package/src/tests/boot-scanner-detection.test.ts +692 -0
- package/src/tests/boot-single-project.test.ts +780 -0
- package/src/tests/integration/mcp-server.integration.test.ts +2 -1
- package/src/tests/project-map.test.ts +302 -0
- package/src/tests/scanner-detector.test.ts +4 -4
|
@@ -2977,7 +2977,7 @@ var require_compile = __commonJS({
|
|
|
2977
2977
|
const schOrFunc = root.refs[ref];
|
|
2978
2978
|
if (schOrFunc)
|
|
2979
2979
|
return schOrFunc;
|
|
2980
|
-
let _sch =
|
|
2980
|
+
let _sch = resolve8.call(this, root, ref);
|
|
2981
2981
|
if (_sch === void 0) {
|
|
2982
2982
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2983
2983
|
const { schemaId } = this.opts;
|
|
@@ -3004,7 +3004,7 @@ var require_compile = __commonJS({
|
|
|
3004
3004
|
function sameSchemaEnv(s1, s2) {
|
|
3005
3005
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3006
3006
|
}
|
|
3007
|
-
function
|
|
3007
|
+
function resolve8(root, ref) {
|
|
3008
3008
|
let sch;
|
|
3009
3009
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3010
3010
|
ref = sch;
|
|
@@ -3579,7 +3579,7 @@ var require_fast_uri = __commonJS({
|
|
|
3579
3579
|
}
|
|
3580
3580
|
return uri;
|
|
3581
3581
|
}
|
|
3582
|
-
function
|
|
3582
|
+
function resolve8(baseURI, relativeURI, options) {
|
|
3583
3583
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3584
3584
|
const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
3585
3585
|
schemelessOptions.skipEscape = true;
|
|
@@ -3806,7 +3806,7 @@ var require_fast_uri = __commonJS({
|
|
|
3806
3806
|
var fastUri = {
|
|
3807
3807
|
SCHEMES,
|
|
3808
3808
|
normalize,
|
|
3809
|
-
resolve:
|
|
3809
|
+
resolve: resolve8,
|
|
3810
3810
|
resolveComponent,
|
|
3811
3811
|
equal,
|
|
3812
3812
|
serialize,
|
|
@@ -6931,6 +6931,67 @@ function adaptDartAnalyzer(rawOutput) {
|
|
|
6931
6931
|
};
|
|
6932
6932
|
}
|
|
6933
6933
|
|
|
6934
|
+
// src/adapters/dotnet-format.ts
|
|
6935
|
+
function adaptDotnetFormat(rawOutput) {
|
|
6936
|
+
let parsed;
|
|
6937
|
+
if (typeof rawOutput === "string") {
|
|
6938
|
+
try {
|
|
6939
|
+
parsed = JSON.parse(rawOutput);
|
|
6940
|
+
} catch {
|
|
6941
|
+
throw new Error("[dotnet-format adapter] rawOutput is not valid JSON");
|
|
6942
|
+
}
|
|
6943
|
+
} else if (Array.isArray(rawOutput)) {
|
|
6944
|
+
parsed = rawOutput;
|
|
6945
|
+
} else {
|
|
6946
|
+
throw new Error(
|
|
6947
|
+
"[dotnet-format adapter] rawOutput must be a JSON string or an array of document entries"
|
|
6948
|
+
);
|
|
6949
|
+
}
|
|
6950
|
+
if (!Array.isArray(parsed)) {
|
|
6951
|
+
throw new Error("[dotnet-format adapter] parsed output must be an array");
|
|
6952
|
+
}
|
|
6953
|
+
const EFFORT_MINUTES = 5;
|
|
6954
|
+
const results = [];
|
|
6955
|
+
let findingCount = 0;
|
|
6956
|
+
let totalEffortMinutes = 0;
|
|
6957
|
+
for (const doc of parsed) {
|
|
6958
|
+
if (!Array.isArray(doc.FileChanges)) continue;
|
|
6959
|
+
for (const change of doc.FileChanges) {
|
|
6960
|
+
findingCount++;
|
|
6961
|
+
totalEffortMinutes += EFFORT_MINUTES;
|
|
6962
|
+
results.push({
|
|
6963
|
+
ruleId: change.DiagnosticId,
|
|
6964
|
+
level: "warning",
|
|
6965
|
+
message: {
|
|
6966
|
+
text: change.FormatDescription
|
|
6967
|
+
},
|
|
6968
|
+
locations: [
|
|
6969
|
+
{
|
|
6970
|
+
physicalLocation: {
|
|
6971
|
+
artifactLocation: {
|
|
6972
|
+
uri: doc.FilePath
|
|
6973
|
+
},
|
|
6974
|
+
region: {
|
|
6975
|
+
startLine: change.LineNumber,
|
|
6976
|
+
startColumn: change.CharNumber
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
],
|
|
6981
|
+
properties: {
|
|
6982
|
+
effortMinutes: EFFORT_MINUTES
|
|
6983
|
+
}
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
return {
|
|
6988
|
+
document: wrapResultsInSarif("dotnet_format", "1.0.0", results),
|
|
6989
|
+
sourceTool: "dotnet_format",
|
|
6990
|
+
findingCount,
|
|
6991
|
+
totalEffortMinutes
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
|
|
6934
6995
|
// src/adapters/index.ts
|
|
6935
6996
|
function adaptScannerOutput(scanner, rawOutput) {
|
|
6936
6997
|
switch (scanner) {
|
|
@@ -6944,6 +7005,8 @@ function adaptScannerOutput(scanner, rawOutput) {
|
|
|
6944
7005
|
return adaptStryker(rawOutput);
|
|
6945
7006
|
case "dart_analyze":
|
|
6946
7007
|
return adaptDartAnalyzer(rawOutput);
|
|
7008
|
+
case "dotnet_format":
|
|
7009
|
+
return adaptDotnetFormat(rawOutput);
|
|
6947
7010
|
default: {
|
|
6948
7011
|
const exhaustive = scanner;
|
|
6949
7012
|
throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
|
|
@@ -7698,7 +7761,7 @@ async function startDashboard(options) {
|
|
|
7698
7761
|
root: publicRoot,
|
|
7699
7762
|
prefix: "/"
|
|
7700
7763
|
});
|
|
7701
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
7764
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.4.0" }));
|
|
7702
7765
|
fastify.get("/api/score", async () => {
|
|
7703
7766
|
const stats = await workspaceStatsProvider();
|
|
7704
7767
|
const score = await buildScore(config, sarifStore, stats, urlOf(fastify, config));
|
|
@@ -8373,6 +8436,7 @@ var CrapConfigError = class extends Error {
|
|
|
8373
8436
|
function loadCrapConfig(options) {
|
|
8374
8437
|
const fileResult = readFromFile(options.workspaceRoot);
|
|
8375
8438
|
const exclude = fileResult?.exclude ?? [];
|
|
8439
|
+
const projectDirs = fileResult?.projectDirs ?? [];
|
|
8376
8440
|
const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
|
|
8377
8441
|
if (typeof envRaw === "string" && envRaw.trim() !== "") {
|
|
8378
8442
|
const normalized = envRaw.trim().toLowerCase();
|
|
@@ -8381,12 +8445,12 @@ function loadCrapConfig(options) {
|
|
|
8381
8445
|
`[crap-config] CLAUDE_CRAP_STRICTNESS="${envRaw}" is not a valid strictness. Expected one of: ${STRICTNESS_VALUES.join(", ")}.`
|
|
8382
8446
|
);
|
|
8383
8447
|
}
|
|
8384
|
-
return { strictness: normalized, strictnessSource: "env", exclude };
|
|
8448
|
+
return { strictness: normalized, strictnessSource: "env", exclude, projectDirs };
|
|
8385
8449
|
}
|
|
8386
8450
|
if (fileResult?.strictness) {
|
|
8387
|
-
return { strictness: fileResult.strictness, strictnessSource: "file", exclude };
|
|
8451
|
+
return { strictness: fileResult.strictness, strictnessSource: "file", exclude, projectDirs };
|
|
8388
8452
|
}
|
|
8389
|
-
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude };
|
|
8453
|
+
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude, projectDirs };
|
|
8390
8454
|
}
|
|
8391
8455
|
function readFromFile(workspaceRoot) {
|
|
8392
8456
|
const filePath = join5(workspaceRoot, ".claude-crap.json");
|
|
@@ -8447,7 +8511,24 @@ function readFromFile(workspaceRoot) {
|
|
|
8447
8511
|
}
|
|
8448
8512
|
exclude = raw2;
|
|
8449
8513
|
}
|
|
8450
|
-
|
|
8514
|
+
let projectDirs = [];
|
|
8515
|
+
if ("projectDirs" in doc) {
|
|
8516
|
+
const raw2 = doc["projectDirs"];
|
|
8517
|
+
if (!Array.isArray(raw2)) {
|
|
8518
|
+
throw new CrapConfigError(
|
|
8519
|
+
`[crap-config] ${filePath}: 'projectDirs' must be an array of strings`
|
|
8520
|
+
);
|
|
8521
|
+
}
|
|
8522
|
+
for (const item of raw2) {
|
|
8523
|
+
if (typeof item !== "string") {
|
|
8524
|
+
throw new CrapConfigError(
|
|
8525
|
+
`[crap-config] ${filePath}: every entry in 'projectDirs' must be a string, got ${typeof item}`
|
|
8526
|
+
);
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
projectDirs = raw2;
|
|
8530
|
+
}
|
|
8531
|
+
return { strictness, exclude, projectDirs };
|
|
8451
8532
|
}
|
|
8452
8533
|
function isStrictness(value) {
|
|
8453
8534
|
return STRICTNESS_VALUES.includes(value);
|
|
@@ -8580,6 +8661,11 @@ var SCANNER_SIGNALS = {
|
|
|
8580
8661
|
],
|
|
8581
8662
|
packageJsonKeys: [],
|
|
8582
8663
|
binaryNames: ["dart"]
|
|
8664
|
+
},
|
|
8665
|
+
dotnet_format: {
|
|
8666
|
+
configFiles: [],
|
|
8667
|
+
packageJsonKeys: [],
|
|
8668
|
+
binaryNames: ["dotnet"]
|
|
8583
8669
|
}
|
|
8584
8670
|
};
|
|
8585
8671
|
function probeConfigFiles(workspaceRoot, scanner) {
|
|
@@ -8610,14 +8696,14 @@ function probePackageJson(workspaceRoot, scanner) {
|
|
|
8610
8696
|
}
|
|
8611
8697
|
}
|
|
8612
8698
|
function probeBinary(binaryName) {
|
|
8613
|
-
return new Promise((
|
|
8699
|
+
return new Promise((resolve8) => {
|
|
8614
8700
|
execFile("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
8615
|
-
|
|
8701
|
+
resolve8(err === null);
|
|
8616
8702
|
});
|
|
8617
8703
|
});
|
|
8618
8704
|
}
|
|
8619
8705
|
async function detectScanners(workspaceRoot) {
|
|
8620
|
-
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
|
|
8706
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
8621
8707
|
const results = await Promise.all(
|
|
8622
8708
|
scanners.map(async (scanner) => {
|
|
8623
8709
|
const configProbe = probeConfigFiles(workspaceRoot, scanner);
|
|
@@ -8689,7 +8775,7 @@ async function detectMonorepoScanners(workspaceRoot) {
|
|
|
8689
8775
|
}
|
|
8690
8776
|
if (subdirs.size === 0) return [];
|
|
8691
8777
|
const detections = [];
|
|
8692
|
-
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze"];
|
|
8778
|
+
const scanners = ["eslint", "semgrep", "bandit", "stryker", "dart_analyze", "dotnet_format"];
|
|
8693
8779
|
for (const subdir of subdirs) {
|
|
8694
8780
|
for (const scanner of scanners) {
|
|
8695
8781
|
const configProbe = probeConfigFiles(subdir, scanner);
|
|
@@ -8754,13 +8840,26 @@ function getScannerCommand(scanner, workspaceRoot) {
|
|
|
8754
8840
|
nonZeroIsNormal: true
|
|
8755
8841
|
// exits 3 when findings exist
|
|
8756
8842
|
};
|
|
8843
|
+
case "dotnet_format":
|
|
8844
|
+
return {
|
|
8845
|
+
command: "dotnet",
|
|
8846
|
+
args: [
|
|
8847
|
+
"format",
|
|
8848
|
+
"--verify-no-changes",
|
|
8849
|
+
"--report",
|
|
8850
|
+
join8(workspaceRoot, ".claude-crap", "dotnet-report.json")
|
|
8851
|
+
],
|
|
8852
|
+
timeoutMs: 12e4,
|
|
8853
|
+
nonZeroIsNormal: true,
|
|
8854
|
+
outputFile: join8(workspaceRoot, ".claude-crap", "dotnet-report.json")
|
|
8855
|
+
};
|
|
8757
8856
|
}
|
|
8758
8857
|
}
|
|
8759
8858
|
function runScanner(scanner, workspaceRoot, options) {
|
|
8760
8859
|
const start = Date.now();
|
|
8761
8860
|
const cwd = options?.workingDir ?? workspaceRoot;
|
|
8762
8861
|
const cmd = getScannerCommand(scanner, cwd);
|
|
8763
|
-
return new Promise((
|
|
8862
|
+
return new Promise((resolve8) => {
|
|
8764
8863
|
execFile2(
|
|
8765
8864
|
cmd.command,
|
|
8766
8865
|
cmd.args,
|
|
@@ -8779,7 +8878,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8779
8878
|
if (cmd.outputFile && existsSync3(cmd.outputFile)) {
|
|
8780
8879
|
try {
|
|
8781
8880
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8782
|
-
|
|
8881
|
+
resolve8({
|
|
8783
8882
|
scanner,
|
|
8784
8883
|
success: true,
|
|
8785
8884
|
rawOutput: fileOutput,
|
|
@@ -8789,7 +8888,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8789
8888
|
} catch {
|
|
8790
8889
|
}
|
|
8791
8890
|
}
|
|
8792
|
-
|
|
8891
|
+
resolve8({
|
|
8793
8892
|
scanner,
|
|
8794
8893
|
success: false,
|
|
8795
8894
|
rawOutput: "",
|
|
@@ -8802,7 +8901,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8802
8901
|
if (existsSync3(cmd.outputFile)) {
|
|
8803
8902
|
try {
|
|
8804
8903
|
const fileOutput = readFileSync4(cmd.outputFile, "utf-8");
|
|
8805
|
-
|
|
8904
|
+
resolve8({
|
|
8806
8905
|
scanner,
|
|
8807
8906
|
success: true,
|
|
8808
8907
|
rawOutput: fileOutput,
|
|
@@ -8810,7 +8909,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8810
8909
|
});
|
|
8811
8910
|
return;
|
|
8812
8911
|
} catch (readErr) {
|
|
8813
|
-
|
|
8912
|
+
resolve8({
|
|
8814
8913
|
scanner,
|
|
8815
8914
|
success: false,
|
|
8816
8915
|
rawOutput: "",
|
|
@@ -8820,7 +8919,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8820
8919
|
return;
|
|
8821
8920
|
}
|
|
8822
8921
|
}
|
|
8823
|
-
|
|
8922
|
+
resolve8({
|
|
8824
8923
|
scanner,
|
|
8825
8924
|
success: false,
|
|
8826
8925
|
rawOutput: "",
|
|
@@ -8831,7 +8930,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8831
8930
|
}
|
|
8832
8931
|
const output = stdout.trim();
|
|
8833
8932
|
if (!output) {
|
|
8834
|
-
|
|
8933
|
+
resolve8({
|
|
8835
8934
|
scanner,
|
|
8836
8935
|
success: true,
|
|
8837
8936
|
rawOutput: "[]",
|
|
@@ -8840,7 +8939,7 @@ function runScanner(scanner, workspaceRoot, options) {
|
|
|
8840
8939
|
});
|
|
8841
8940
|
return;
|
|
8842
8941
|
}
|
|
8843
|
-
|
|
8942
|
+
resolve8({
|
|
8844
8943
|
scanner,
|
|
8845
8944
|
success: true,
|
|
8846
8945
|
rawOutput: output,
|
|
@@ -8917,7 +9016,7 @@ export default [
|
|
|
8917
9016
|
`;
|
|
8918
9017
|
}
|
|
8919
9018
|
function npmInstall(workspaceRoot, packages) {
|
|
8920
|
-
return new Promise((
|
|
9019
|
+
return new Promise((resolve8) => {
|
|
8921
9020
|
execFile3(
|
|
8922
9021
|
"npm",
|
|
8923
9022
|
["install", "--save-dev", ...packages],
|
|
@@ -8928,14 +9027,14 @@ function npmInstall(workspaceRoot, packages) {
|
|
|
8928
9027
|
},
|
|
8929
9028
|
(err, stdout, stderr) => {
|
|
8930
9029
|
if (err) {
|
|
8931
|
-
|
|
9030
|
+
resolve8({
|
|
8932
9031
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8933
9032
|
success: false,
|
|
8934
9033
|
detail: stderr || err.message
|
|
8935
9034
|
});
|
|
8936
9035
|
return;
|
|
8937
9036
|
}
|
|
8938
|
-
|
|
9037
|
+
resolve8({
|
|
8939
9038
|
action: `npm install --save-dev ${packages.join(" ")}`,
|
|
8940
9039
|
success: true,
|
|
8941
9040
|
detail: `installed ${packages.join(", ")}`
|
|
@@ -8984,12 +9083,17 @@ function getRecommendation(projectType) {
|
|
|
8984
9083
|
installInstructions: "pip install bandit (or: pipx install bandit, poetry add --group dev bandit)"
|
|
8985
9084
|
};
|
|
8986
9085
|
case "java":
|
|
8987
|
-
case "csharp":
|
|
8988
9086
|
return {
|
|
8989
9087
|
scanner: "semgrep",
|
|
8990
9088
|
canAutoInstall: false,
|
|
8991
9089
|
installInstructions: "brew install semgrep (or: pip install semgrep, pipx install semgrep)"
|
|
8992
9090
|
};
|
|
9091
|
+
case "csharp":
|
|
9092
|
+
return {
|
|
9093
|
+
scanner: "dotnet_format",
|
|
9094
|
+
canAutoInstall: false,
|
|
9095
|
+
installInstructions: "Install the .NET SDK: https://dotnet.microsoft.com/download"
|
|
9096
|
+
};
|
|
8993
9097
|
case "dart":
|
|
8994
9098
|
return {
|
|
8995
9099
|
scanner: "dart_analyze",
|
|
@@ -9437,6 +9541,192 @@ async function autoScan(workspaceRoot, sarifStore, logger2, options) {
|
|
|
9437
9541
|
};
|
|
9438
9542
|
}
|
|
9439
9543
|
|
|
9544
|
+
// src/monorepo/project-map.ts
|
|
9545
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "node:fs";
|
|
9546
|
+
import { promises as fs8 } from "node:fs";
|
|
9547
|
+
import { join as join12, basename as basename2, resolve as resolve7 } from "node:path";
|
|
9548
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
9549
|
+
var MONOREPO_DIRS2 = ["apps", "packages", "libs", "modules", "services"];
|
|
9550
|
+
var SCANNER_FOR_TYPE = {
|
|
9551
|
+
typescript: "eslint",
|
|
9552
|
+
javascript: "eslint",
|
|
9553
|
+
python: "bandit",
|
|
9554
|
+
java: "semgrep",
|
|
9555
|
+
csharp: "dotnet_format",
|
|
9556
|
+
dart: "dart_analyze",
|
|
9557
|
+
unknown: null
|
|
9558
|
+
};
|
|
9559
|
+
var BINARY_FOR_SCANNER = {
|
|
9560
|
+
eslint: "eslint",
|
|
9561
|
+
bandit: "bandit",
|
|
9562
|
+
semgrep: "semgrep",
|
|
9563
|
+
dart_analyze: "dart",
|
|
9564
|
+
dotnet_format: "dotnet"
|
|
9565
|
+
};
|
|
9566
|
+
function probeBinary2(binaryName) {
|
|
9567
|
+
return new Promise((resolve8) => {
|
|
9568
|
+
execFile4("which", [binaryName], { timeout: 5e3 }, (err) => {
|
|
9569
|
+
resolve8(err === null);
|
|
9570
|
+
});
|
|
9571
|
+
});
|
|
9572
|
+
}
|
|
9573
|
+
function detectProjectType2(dir) {
|
|
9574
|
+
const has = (file) => existsSync6(join12(dir, file));
|
|
9575
|
+
if (has("pubspec.yaml")) return "dart";
|
|
9576
|
+
if (has("package.json")) {
|
|
9577
|
+
if (has("tsconfig.json")) return "typescript";
|
|
9578
|
+
return "javascript";
|
|
9579
|
+
}
|
|
9580
|
+
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) {
|
|
9581
|
+
return "python";
|
|
9582
|
+
}
|
|
9583
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) {
|
|
9584
|
+
return "java";
|
|
9585
|
+
}
|
|
9586
|
+
if (has("Directory.Build.props")) return "csharp";
|
|
9587
|
+
try {
|
|
9588
|
+
const entries = readdirSync3(dir);
|
|
9589
|
+
if (entries.some((e) => e.endsWith(".csproj") || e.endsWith(".sln"))) {
|
|
9590
|
+
return "csharp";
|
|
9591
|
+
}
|
|
9592
|
+
} catch {
|
|
9593
|
+
}
|
|
9594
|
+
return "unknown";
|
|
9595
|
+
}
|
|
9596
|
+
function extractWorkspacePatterns(workspaces) {
|
|
9597
|
+
if (Array.isArray(workspaces)) {
|
|
9598
|
+
return workspaces.filter((v) => typeof v === "string");
|
|
9599
|
+
}
|
|
9600
|
+
if (workspaces !== null && typeof workspaces === "object" && "packages" in workspaces && Array.isArray(workspaces.packages)) {
|
|
9601
|
+
return workspaces.packages.filter(
|
|
9602
|
+
(v) => typeof v === "string"
|
|
9603
|
+
);
|
|
9604
|
+
}
|
|
9605
|
+
return [];
|
|
9606
|
+
}
|
|
9607
|
+
function expandWorkspacePattern(workspaceRoot, pattern) {
|
|
9608
|
+
if (pattern.endsWith("/*")) {
|
|
9609
|
+
const parentDir = join12(workspaceRoot, pattern.slice(0, -2));
|
|
9610
|
+
try {
|
|
9611
|
+
const entries = readdirSync3(parentDir, { withFileTypes: true });
|
|
9612
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => join12(parentDir, e.name));
|
|
9613
|
+
} catch {
|
|
9614
|
+
return [];
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9617
|
+
const full = resolve7(workspaceRoot, pattern);
|
|
9618
|
+
try {
|
|
9619
|
+
const entries = readdirSync3(full, { withFileTypes: true });
|
|
9620
|
+
void entries;
|
|
9621
|
+
return [full];
|
|
9622
|
+
} catch {
|
|
9623
|
+
return [];
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
function collectSubdirectories(workspaceRoot, extraDirs) {
|
|
9627
|
+
const subdirs = /* @__PURE__ */ new Set();
|
|
9628
|
+
const pkgPath = join12(workspaceRoot, "package.json");
|
|
9629
|
+
if (existsSync6(pkgPath)) {
|
|
9630
|
+
try {
|
|
9631
|
+
const raw = readFileSync5(pkgPath, "utf-8");
|
|
9632
|
+
const pkg = JSON.parse(raw);
|
|
9633
|
+
const patterns = extractWorkspacePatterns(pkg["workspaces"]);
|
|
9634
|
+
for (const pattern of patterns) {
|
|
9635
|
+
for (const absPath of expandWorkspacePattern(workspaceRoot, pattern)) {
|
|
9636
|
+
subdirs.add(absPath);
|
|
9637
|
+
}
|
|
9638
|
+
}
|
|
9639
|
+
} catch {
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
if (extraDirs && extraDirs.length > 0) {
|
|
9643
|
+
for (const dir of extraDirs) {
|
|
9644
|
+
const absDir = resolve7(workspaceRoot, dir);
|
|
9645
|
+
if (!existsSync6(absDir)) continue;
|
|
9646
|
+
const hasMarker = PROJECT_MARKERS.some((m) => existsSync6(join12(absDir, m)));
|
|
9647
|
+
if (hasMarker) {
|
|
9648
|
+
subdirs.add(absDir);
|
|
9649
|
+
continue;
|
|
9650
|
+
}
|
|
9651
|
+
try {
|
|
9652
|
+
const entries = readdirSync3(absDir, { withFileTypes: true });
|
|
9653
|
+
for (const entry of entries) {
|
|
9654
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
9655
|
+
subdirs.add(join12(absDir, entry.name));
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
} catch {
|
|
9659
|
+
}
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
const configuredDirNames = new Set(extraDirs?.map((d) => d.split("/")[0]) ?? []);
|
|
9663
|
+
for (const dir of MONOREPO_DIRS2) {
|
|
9664
|
+
if (configuredDirNames.has(dir)) continue;
|
|
9665
|
+
const parentDir = join12(workspaceRoot, dir);
|
|
9666
|
+
try {
|
|
9667
|
+
const entries = readdirSync3(parentDir, { withFileTypes: true });
|
|
9668
|
+
for (const entry of entries) {
|
|
9669
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
9670
|
+
subdirs.add(join12(parentDir, entry.name));
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9673
|
+
} catch {
|
|
9674
|
+
}
|
|
9675
|
+
}
|
|
9676
|
+
return subdirs;
|
|
9677
|
+
}
|
|
9678
|
+
var PROJECT_MARKERS = [
|
|
9679
|
+
"package.json",
|
|
9680
|
+
"pubspec.yaml",
|
|
9681
|
+
"pyproject.toml",
|
|
9682
|
+
"setup.py",
|
|
9683
|
+
"pom.xml",
|
|
9684
|
+
"build.gradle",
|
|
9685
|
+
"build.gradle.kts",
|
|
9686
|
+
"Directory.Build.props"
|
|
9687
|
+
];
|
|
9688
|
+
async function discoverProjectMap(workspaceRoot, options) {
|
|
9689
|
+
const subdirs = collectSubdirectories(workspaceRoot, options?.projectDirs);
|
|
9690
|
+
const binaryCache = /* @__PURE__ */ new Map();
|
|
9691
|
+
const probeScanner = (scanner) => {
|
|
9692
|
+
const binaryName = BINARY_FOR_SCANNER[scanner];
|
|
9693
|
+
if (binaryName === void 0) return Promise.resolve(false);
|
|
9694
|
+
const cached = binaryCache.get(scanner);
|
|
9695
|
+
if (cached !== void 0) return cached;
|
|
9696
|
+
const probe = probeBinary2(binaryName);
|
|
9697
|
+
binaryCache.set(scanner, probe);
|
|
9698
|
+
return probe;
|
|
9699
|
+
};
|
|
9700
|
+
const projectEntries = await Promise.all(
|
|
9701
|
+
[...subdirs].map(async (absPath) => {
|
|
9702
|
+
const relPath = absPath.replace(workspaceRoot + "/", "");
|
|
9703
|
+
const type = detectProjectType2(absPath);
|
|
9704
|
+
const scanner = SCANNER_FOR_TYPE[type];
|
|
9705
|
+
const scannerAvailable = scanner !== null ? await probeScanner(scanner) : false;
|
|
9706
|
+
return {
|
|
9707
|
+
name: basename2(absPath),
|
|
9708
|
+
path: relPath,
|
|
9709
|
+
type,
|
|
9710
|
+
scanner,
|
|
9711
|
+
scannerAvailable
|
|
9712
|
+
};
|
|
9713
|
+
})
|
|
9714
|
+
);
|
|
9715
|
+
projectEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
9716
|
+
return {
|
|
9717
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9718
|
+
workspaceRoot,
|
|
9719
|
+
isMonorepo: projectEntries.length > 0,
|
|
9720
|
+
projects: projectEntries
|
|
9721
|
+
};
|
|
9722
|
+
}
|
|
9723
|
+
async function persistProjectMap(map, workspaceRoot) {
|
|
9724
|
+
const dir = join12(workspaceRoot, ".claude-crap");
|
|
9725
|
+
await fs8.mkdir(dir, { recursive: true });
|
|
9726
|
+
const filePath = join12(dir, "projects.json");
|
|
9727
|
+
await fs8.writeFile(filePath, JSON.stringify(map, null, 2) + "\n", "utf-8");
|
|
9728
|
+
}
|
|
9729
|
+
|
|
9440
9730
|
// src/schemas/tool-schemas.ts
|
|
9441
9731
|
var computeCrapSchema = {
|
|
9442
9732
|
type: "object",
|
|
@@ -9527,11 +9817,22 @@ var scoreProjectSchema = {
|
|
|
9527
9817
|
type: "string",
|
|
9528
9818
|
enum: ["markdown", "json", "both"],
|
|
9529
9819
|
description: "Output format. `markdown` returns only the chat summary, `json` returns only the structured snapshot, `both` (default) returns both as separate content blocks."
|
|
9820
|
+
},
|
|
9821
|
+
scope: {
|
|
9822
|
+
type: "string",
|
|
9823
|
+
description: "Optional project name from the project map. When provided, the score is computed only for files within that project's subtree. Omit to score the entire workspace."
|
|
9530
9824
|
}
|
|
9531
9825
|
},
|
|
9532
9826
|
required: [],
|
|
9533
9827
|
additionalProperties: false
|
|
9534
9828
|
};
|
|
9829
|
+
var listProjectsSchema = {
|
|
9830
|
+
type: "object",
|
|
9831
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner. In a single-project workspace, returns an empty list.",
|
|
9832
|
+
properties: {},
|
|
9833
|
+
required: [],
|
|
9834
|
+
additionalProperties: false
|
|
9835
|
+
};
|
|
9535
9836
|
var requireTestHarnessSchema = {
|
|
9536
9837
|
type: "object",
|
|
9537
9838
|
description: "Check whether a production source file has a matching test file. Returns the first existing test path, or the full list of paths the resolver probed when none exists. Use this BEFORE writing any functional code \u2014 the CLAUDE.md Golden Rule requires a test harness to exist first.",
|
|
@@ -9553,7 +9854,7 @@ var ingestScannerOutputSchema = {
|
|
|
9553
9854
|
properties: {
|
|
9554
9855
|
scanner: {
|
|
9555
9856
|
type: "string",
|
|
9556
|
-
enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze"],
|
|
9857
|
+
enum: ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"],
|
|
9557
9858
|
description: "Identifier of the producing scanner."
|
|
9558
9859
|
},
|
|
9559
9860
|
rawOutput: {
|
|
@@ -9614,12 +9915,17 @@ async function main() {
|
|
|
9614
9915
|
"claude-crap MCP server starting"
|
|
9615
9916
|
);
|
|
9616
9917
|
let userExclusions = [];
|
|
9918
|
+
let userProjectDirs = [];
|
|
9617
9919
|
try {
|
|
9618
9920
|
const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
|
|
9619
9921
|
userExclusions = crapConfig.exclude;
|
|
9922
|
+
userProjectDirs = crapConfig.projectDirs;
|
|
9620
9923
|
if (userExclusions.length > 0) {
|
|
9621
9924
|
logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
|
|
9622
9925
|
}
|
|
9926
|
+
if (userProjectDirs.length > 0) {
|
|
9927
|
+
logger.info({ projectDirs: userProjectDirs }, "user projectDirs loaded from .claude-crap.json");
|
|
9928
|
+
}
|
|
9623
9929
|
} catch {
|
|
9624
9930
|
}
|
|
9625
9931
|
const astEngine = new TreeSitterEngine();
|
|
@@ -9632,6 +9938,32 @@ async function main() {
|
|
|
9632
9938
|
{ findings: sarifStore.size(), path: sarifStore.consolidatedReportPath },
|
|
9633
9939
|
"SARIF store ready"
|
|
9634
9940
|
);
|
|
9941
|
+
let projectMap = null;
|
|
9942
|
+
try {
|
|
9943
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
9944
|
+
if (projectMap.isMonorepo) {
|
|
9945
|
+
logger.info(
|
|
9946
|
+
{ projects: projectMap.projects.map((p) => `${p.name}(${p.type})`), count: projectMap.projects.length },
|
|
9947
|
+
"monorepo project map discovered"
|
|
9948
|
+
);
|
|
9949
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
9950
|
+
const needsEslint = projectMap.projects.some(
|
|
9951
|
+
(p) => (p.type === "typescript" || p.type === "javascript") && !p.scannerAvailable
|
|
9952
|
+
);
|
|
9953
|
+
if (needsEslint) {
|
|
9954
|
+
logger.info("monorepo: JS/TS projects detected but ESLint not installed \u2014 bootstrapping");
|
|
9955
|
+
try {
|
|
9956
|
+
await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
9957
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
9958
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
9959
|
+
} catch (err) {
|
|
9960
|
+
logger.warn({ err: err.message }, "monorepo ESLint bootstrap failed");
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
}
|
|
9964
|
+
} catch (err) {
|
|
9965
|
+
logger.warn({ err: err.message }, "project map discovery failed");
|
|
9966
|
+
}
|
|
9635
9967
|
let dashboard = null;
|
|
9636
9968
|
try {
|
|
9637
9969
|
dashboard = await startDashboard({
|
|
@@ -9718,12 +10050,20 @@ async function main() {
|
|
|
9718
10050
|
name: "bootstrap_scanner",
|
|
9719
10051
|
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.",
|
|
9720
10052
|
inputSchema: bootstrapScannerSchema
|
|
10053
|
+
},
|
|
10054
|
+
{
|
|
10055
|
+
name: "list_projects",
|
|
10056
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner.",
|
|
10057
|
+
inputSchema: listProjectsSchema
|
|
9721
10058
|
}
|
|
9722
10059
|
]
|
|
9723
10060
|
}));
|
|
9724
10061
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
9725
10062
|
const { name, arguments: args } = request.params;
|
|
9726
10063
|
logger.info({ tool: name }, "Tool call received");
|
|
10064
|
+
return handleToolCall(name, args);
|
|
10065
|
+
});
|
|
10066
|
+
async function handleToolCall(name, args) {
|
|
9727
10067
|
switch (name) {
|
|
9728
10068
|
case "compute_crap": {
|
|
9729
10069
|
const typed = args;
|
|
@@ -9809,10 +10149,18 @@ async function main() {
|
|
|
9809
10149
|
case "score_project": {
|
|
9810
10150
|
const typed = args ?? {};
|
|
9811
10151
|
const format = typed.format ?? "both";
|
|
10152
|
+
let scoreRoot = config.pluginRoot;
|
|
10153
|
+
if (typed.scope && projectMap) {
|
|
10154
|
+
const project = projectMap.projects.find((p) => p.name === typed.scope);
|
|
10155
|
+
if (project) {
|
|
10156
|
+
const { join: join13 } = await import("node:path");
|
|
10157
|
+
scoreRoot = join13(config.pluginRoot, project.path);
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
9812
10160
|
try {
|
|
9813
|
-
const workspace = await estimateWorkspaceLoc(
|
|
10161
|
+
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
9814
10162
|
const score = computeProjectScore({
|
|
9815
|
-
workspaceRoot:
|
|
10163
|
+
workspaceRoot: scoreRoot,
|
|
9816
10164
|
minutesPerLoc: config.minutesPerLoc,
|
|
9817
10165
|
tdrMaxRating: config.tdrMaxRating,
|
|
9818
10166
|
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
@@ -10060,10 +10408,28 @@ async function main() {
|
|
|
10060
10408
|
};
|
|
10061
10409
|
}
|
|
10062
10410
|
}
|
|
10411
|
+
case "list_projects": {
|
|
10412
|
+
return {
|
|
10413
|
+
content: [
|
|
10414
|
+
{
|
|
10415
|
+
type: "text",
|
|
10416
|
+
text: JSON.stringify(
|
|
10417
|
+
{
|
|
10418
|
+
tool: "list_projects",
|
|
10419
|
+
isMonorepo: projectMap?.isMonorepo ?? false,
|
|
10420
|
+
projects: projectMap?.projects ?? []
|
|
10421
|
+
},
|
|
10422
|
+
null,
|
|
10423
|
+
2
|
|
10424
|
+
)
|
|
10425
|
+
}
|
|
10426
|
+
]
|
|
10427
|
+
};
|
|
10428
|
+
}
|
|
10063
10429
|
default:
|
|
10064
10430
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
10065
10431
|
}
|
|
10066
|
-
}
|
|
10432
|
+
}
|
|
10067
10433
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
10068
10434
|
resources: [
|
|
10069
10435
|
{
|