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
package/plugin/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-crap-plugin",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.4.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/static": "^8.0.3",
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
package/plugin/package.json
CHANGED
package/src/adapters/common.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { SarifLevel } from "../sarif/sarif-builder.js";
|
|
|
23
23
|
* `ingest_scanner_output` MCP tool uses this as its `enum` constraint,
|
|
24
24
|
* so keeping it narrow prevents drift.
|
|
25
25
|
*/
|
|
26
|
-
export const KNOWN_SCANNERS = ["semgrep", "eslint", "bandit", "stryker", "dart_analyze"] as const;
|
|
26
|
+
export const KNOWN_SCANNERS = ["semgrep", "eslint", "bandit", "stryker", "dart_analyze", "dotnet_format"] as const;
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Union of supported scanner identifiers.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: `dotnet format --report <path>` JSON output → SARIF 2.1.0.
|
|
3
|
+
*
|
|
4
|
+
* The dotnet format tool emits a JSON array with this shape:
|
|
5
|
+
*
|
|
6
|
+
* [
|
|
7
|
+
* {
|
|
8
|
+
* "DocumentId": { "ProjectId": { "Id": "..." }, "Id": "..." },
|
|
9
|
+
* "FileName": "AuthController.cs",
|
|
10
|
+
* "FilePath": "/absolute/path/to/AuthController.cs",
|
|
11
|
+
* "FileChanges": [
|
|
12
|
+
* {
|
|
13
|
+
* "LineNumber": 84,
|
|
14
|
+
* "CharNumber": 16,
|
|
15
|
+
* "DiagnosticId": "WHITESPACE",
|
|
16
|
+
* "FormatDescription": "Fix whitespace formatting. Delete 5 characters."
|
|
17
|
+
* }
|
|
18
|
+
* ]
|
|
19
|
+
* }
|
|
20
|
+
* ]
|
|
21
|
+
*
|
|
22
|
+
* All dotnet format findings are style/formatting issues, so they
|
|
23
|
+
* map uniformly to SARIF "warning" level with a 5-minute effort
|
|
24
|
+
* estimate (formatting fixes are quick, mechanical changes).
|
|
25
|
+
*
|
|
26
|
+
* @module adapters/dotnet-format
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type AdapterResult,
|
|
31
|
+
wrapResultsInSarif,
|
|
32
|
+
} from "./common.js";
|
|
33
|
+
|
|
34
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
interface DotnetFileChange {
|
|
37
|
+
LineNumber: number;
|
|
38
|
+
CharNumber: number;
|
|
39
|
+
DiagnosticId: string;
|
|
40
|
+
FormatDescription: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DotnetFormatDocument {
|
|
44
|
+
DocumentId: {
|
|
45
|
+
ProjectId: { Id: string };
|
|
46
|
+
Id: string;
|
|
47
|
+
};
|
|
48
|
+
FileName: string;
|
|
49
|
+
FilePath: string;
|
|
50
|
+
FileChanges: DotnetFileChange[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert `dotnet format --report <path>` JSON output to SARIF 2.1.0.
|
|
57
|
+
*
|
|
58
|
+
* @param rawOutput The JSON string or pre-parsed array from `dotnet format`.
|
|
59
|
+
*/
|
|
60
|
+
export function adaptDotnetFormat(rawOutput: unknown): AdapterResult {
|
|
61
|
+
let parsed: DotnetFormatDocument[];
|
|
62
|
+
|
|
63
|
+
if (typeof rawOutput === "string") {
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(rawOutput) as DotnetFormatDocument[];
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error("[dotnet-format adapter] rawOutput is not valid JSON");
|
|
68
|
+
}
|
|
69
|
+
} else if (Array.isArray(rawOutput)) {
|
|
70
|
+
parsed = rawOutput as DotnetFormatDocument[];
|
|
71
|
+
} else {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"[dotnet-format adapter] rawOutput must be a JSON string or an array of document entries",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!Array.isArray(parsed)) {
|
|
78
|
+
throw new Error("[dotnet-format adapter] parsed output must be an array");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const EFFORT_MINUTES = 5;
|
|
82
|
+
const results: object[] = [];
|
|
83
|
+
let findingCount = 0;
|
|
84
|
+
let totalEffortMinutes = 0;
|
|
85
|
+
|
|
86
|
+
for (const doc of parsed) {
|
|
87
|
+
if (!Array.isArray(doc.FileChanges)) continue;
|
|
88
|
+
|
|
89
|
+
for (const change of doc.FileChanges) {
|
|
90
|
+
findingCount++;
|
|
91
|
+
totalEffortMinutes += EFFORT_MINUTES;
|
|
92
|
+
|
|
93
|
+
results.push({
|
|
94
|
+
ruleId: change.DiagnosticId,
|
|
95
|
+
level: "warning",
|
|
96
|
+
message: {
|
|
97
|
+
text: change.FormatDescription,
|
|
98
|
+
},
|
|
99
|
+
locations: [
|
|
100
|
+
{
|
|
101
|
+
physicalLocation: {
|
|
102
|
+
artifactLocation: {
|
|
103
|
+
uri: doc.FilePath,
|
|
104
|
+
},
|
|
105
|
+
region: {
|
|
106
|
+
startLine: change.LineNumber,
|
|
107
|
+
startColumn: change.CharNumber,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
properties: {
|
|
113
|
+
effortMinutes: EFFORT_MINUTES,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
document: wrapResultsInSarif("dotnet_format", "1.0.0", results),
|
|
121
|
+
sourceTool: "dotnet_format",
|
|
122
|
+
findingCount,
|
|
123
|
+
totalEffortMinutes,
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export { adaptEslint } from "./eslint.js";
|
|
|
31
31
|
export { adaptBandit } from "./bandit.js";
|
|
32
32
|
export { adaptStryker } from "./stryker.js";
|
|
33
33
|
export { adaptDartAnalyzer } from "./dart-analyzer.js";
|
|
34
|
+
export { adaptDotnetFormat } from "./dotnet-format.js";
|
|
34
35
|
|
|
35
36
|
export {
|
|
36
37
|
DEFAULT_EFFORT_BY_SEVERITY,
|
|
@@ -46,6 +47,7 @@ import { adaptEslint } from "./eslint.js";
|
|
|
46
47
|
import { adaptBandit } from "./bandit.js";
|
|
47
48
|
import { adaptStryker } from "./stryker.js";
|
|
48
49
|
import { adaptDartAnalyzer } from "./dart-analyzer.js";
|
|
50
|
+
import { adaptDotnetFormat } from "./dotnet-format.js";
|
|
49
51
|
import type { AdapterResult, KnownScanner } from "./common.js";
|
|
50
52
|
|
|
51
53
|
/**
|
|
@@ -74,6 +76,8 @@ export function adaptScannerOutput(
|
|
|
74
76
|
return adaptStryker(rawOutput);
|
|
75
77
|
case "dart_analyze":
|
|
76
78
|
return adaptDartAnalyzer(rawOutput);
|
|
79
|
+
case "dotnet_format":
|
|
80
|
+
return adaptDotnetFormat(rawOutput);
|
|
77
81
|
default: {
|
|
78
82
|
const exhaustive: never = scanner;
|
|
79
83
|
throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
|
package/src/crap-config.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface CrapConfig {
|
|
|
83
83
|
readonly strictnessSource: "env" | "file" | "default";
|
|
84
84
|
/** User-defined exclusion patterns (directories with trailing `/`, or file globs). */
|
|
85
85
|
readonly exclude: ReadonlyArray<string>;
|
|
86
|
+
/** Relative paths to directories containing sub-projects (e.g. `["apps", "packages"]`). */
|
|
87
|
+
readonly projectDirs: ReadonlyArray<string>;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/**
|
|
@@ -113,6 +115,7 @@ export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
|
113
115
|
// comes from the environment variable.
|
|
114
116
|
const fileResult = readFromFile(options.workspaceRoot);
|
|
115
117
|
const exclude = fileResult?.exclude ?? [];
|
|
118
|
+
const projectDirs = fileResult?.projectDirs ?? [];
|
|
116
119
|
|
|
117
120
|
const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
|
|
118
121
|
if (typeof envRaw === "string" && envRaw.trim() !== "") {
|
|
@@ -123,14 +126,14 @@ export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
|
123
126
|
`Expected one of: ${STRICTNESS_VALUES.join(", ")}.`,
|
|
124
127
|
);
|
|
125
128
|
}
|
|
126
|
-
return { strictness: normalized, strictnessSource: "env", exclude };
|
|
129
|
+
return { strictness: normalized, strictnessSource: "env", exclude, projectDirs };
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
if (fileResult?.strictness) {
|
|
130
|
-
return { strictness: fileResult.strictness, strictnessSource: "file", exclude };
|
|
133
|
+
return { strictness: fileResult.strictness, strictnessSource: "file", exclude, projectDirs };
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude };
|
|
136
|
+
return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default", exclude, projectDirs };
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
/**
|
|
@@ -149,6 +152,7 @@ export function loadCrapConfig(options: LoadCrapConfigOptions): CrapConfig {
|
|
|
149
152
|
interface FileResult {
|
|
150
153
|
strictness: Strictness | null;
|
|
151
154
|
exclude: string[];
|
|
155
|
+
projectDirs: string[];
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
function readFromFile(workspaceRoot: string): FileResult | null {
|
|
@@ -218,7 +222,26 @@ function readFromFile(workspaceRoot: string): FileResult | null {
|
|
|
218
222
|
exclude = raw as string[];
|
|
219
223
|
}
|
|
220
224
|
|
|
221
|
-
|
|
225
|
+
// Parse projectDirs
|
|
226
|
+
let projectDirs: string[] = [];
|
|
227
|
+
if ("projectDirs" in doc) {
|
|
228
|
+
const raw = doc["projectDirs"];
|
|
229
|
+
if (!Array.isArray(raw)) {
|
|
230
|
+
throw new CrapConfigError(
|
|
231
|
+
`[crap-config] ${filePath}: 'projectDirs' must be an array of strings`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
for (const item of raw) {
|
|
235
|
+
if (typeof item !== "string") {
|
|
236
|
+
throw new CrapConfigError(
|
|
237
|
+
`[crap-config] ${filePath}: every entry in 'projectDirs' must be a string, got ${typeof item}`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
projectDirs = raw as string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { strictness, exclude, projectDirs };
|
|
222
245
|
}
|
|
223
246
|
|
|
224
247
|
/**
|
package/src/dashboard/server.ts
CHANGED
|
@@ -107,7 +107,7 @@ export async function startDashboard(options: StartDashboardOptions): Promise<Da
|
|
|
107
107
|
// ------------------------------------------------------------------
|
|
108
108
|
// /api/health — liveness probe
|
|
109
109
|
// ------------------------------------------------------------------
|
|
110
|
-
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.
|
|
110
|
+
fastify.get("/api/health", async () => ({ status: "ok", server: "claude-crap", version: "0.4.0" }));
|
|
111
111
|
|
|
112
112
|
// ------------------------------------------------------------------
|
|
113
113
|
// /api/score — live project score
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { findTestFile } from "./tools/test-harness.js";
|
|
|
59
59
|
import { resolveWithinWorkspace } from "./workspace-guard.js";
|
|
60
60
|
import { autoScan } from "./scanner/auto-scan.js";
|
|
61
61
|
import { bootstrapScanner } from "./scanner/bootstrap.js";
|
|
62
|
+
import { discoverProjectMap, persistProjectMap, type ProjectMap } from "./monorepo/project-map.js";
|
|
62
63
|
import {
|
|
63
64
|
autoScanSchema,
|
|
64
65
|
bootstrapScannerSchema,
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
analyzeFileAstSchema,
|
|
68
69
|
ingestSarifSchema,
|
|
69
70
|
ingestScannerOutputSchema,
|
|
71
|
+
listProjectsSchema,
|
|
70
72
|
requireTestHarnessSchema,
|
|
71
73
|
scoreProjectSchema,
|
|
72
74
|
} from "./schemas/tool-schemas.js";
|
|
@@ -93,14 +95,19 @@ async function main(): Promise<void> {
|
|
|
93
95
|
"claude-crap MCP server starting",
|
|
94
96
|
);
|
|
95
97
|
|
|
96
|
-
// Load user-defined exclusions from .claude-crap.json (non-fatal).
|
|
98
|
+
// Load user-defined exclusions and projectDirs from .claude-crap.json (non-fatal).
|
|
97
99
|
let userExclusions: ReadonlyArray<string> = [];
|
|
100
|
+
let userProjectDirs: ReadonlyArray<string> = [];
|
|
98
101
|
try {
|
|
99
102
|
const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
|
|
100
103
|
userExclusions = crapConfig.exclude;
|
|
104
|
+
userProjectDirs = crapConfig.projectDirs;
|
|
101
105
|
if (userExclusions.length > 0) {
|
|
102
106
|
logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
|
|
103
107
|
}
|
|
108
|
+
if (userProjectDirs.length > 0) {
|
|
109
|
+
logger.info({ projectDirs: userProjectDirs }, "user projectDirs loaded from .claude-crap.json");
|
|
110
|
+
}
|
|
104
111
|
} catch {
|
|
105
112
|
// Non-fatal — use empty exclusions.
|
|
106
113
|
}
|
|
@@ -117,6 +124,39 @@ async function main(): Promise<void> {
|
|
|
117
124
|
"SARIF store ready",
|
|
118
125
|
);
|
|
119
126
|
|
|
127
|
+
// Discover monorepo project map (non-fatal).
|
|
128
|
+
let projectMap: ProjectMap | null = null;
|
|
129
|
+
try {
|
|
130
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
131
|
+
if (projectMap.isMonorepo) {
|
|
132
|
+
logger.info(
|
|
133
|
+
{ projects: projectMap.projects.map((p) => `${p.name}(${p.type})`), count: projectMap.projects.length },
|
|
134
|
+
"monorepo project map discovered",
|
|
135
|
+
);
|
|
136
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
137
|
+
|
|
138
|
+
// If any JS/TS sub-projects need ESLint and it's not available,
|
|
139
|
+
// run bootstrap at the monorepo root to auto-install it. In
|
|
140
|
+
// monorepos, ESLint is hoisted to the root node_modules.
|
|
141
|
+
const needsEslint = projectMap.projects.some(
|
|
142
|
+
(p) => (p.type === "typescript" || p.type === "javascript") && !p.scannerAvailable,
|
|
143
|
+
);
|
|
144
|
+
if (needsEslint) {
|
|
145
|
+
logger.info("monorepo: JS/TS projects detected but ESLint not installed — bootstrapping");
|
|
146
|
+
try {
|
|
147
|
+
await bootstrapScanner(config.pluginRoot, sarifStore, logger);
|
|
148
|
+
// Re-discover after install so scannerAvailable reflects reality
|
|
149
|
+
projectMap = await discoverProjectMap(config.pluginRoot, { projectDirs: userProjectDirs });
|
|
150
|
+
await persistProjectMap(projectMap, config.pluginRoot);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
logger.warn({ err: (err as Error).message }, "monorepo ESLint bootstrap failed");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.warn({ err: (err as Error).message }, "project map discovery failed");
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
// Try to start the local Vue.js dashboard. Failures here are
|
|
121
161
|
// intentionally non-fatal — the MCP server still works without it.
|
|
122
162
|
let dashboard: DashboardHandle | null = null;
|
|
@@ -235,6 +275,11 @@ async function main(): Promise<void> {
|
|
|
235
275
|
"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.",
|
|
236
276
|
inputSchema: bootstrapScannerSchema,
|
|
237
277
|
},
|
|
278
|
+
{
|
|
279
|
+
name: "list_projects",
|
|
280
|
+
description: "List all discovered sub-projects in the workspace. In a monorepo, returns each sub-project with its type, path, and recommended scanner.",
|
|
281
|
+
inputSchema: listProjectsSchema,
|
|
282
|
+
},
|
|
238
283
|
],
|
|
239
284
|
}));
|
|
240
285
|
|
|
@@ -244,10 +289,20 @@ async function main(): Promise<void> {
|
|
|
244
289
|
// The MCP SDK has already validated `args` against the tool's JSON
|
|
245
290
|
// Schema by the time this handler runs, so we cast to the expected
|
|
246
291
|
// shape without re-validating. Each branch delegates to a pure engine.
|
|
292
|
+
// Tool dispatch is split across two functions to keep cyclomatic
|
|
293
|
+
// complexity within the configured threshold (15) as the tool count
|
|
294
|
+
// grows. Each function handles a subset of tools.
|
|
247
295
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
248
296
|
const { name, arguments: args } = request.params;
|
|
249
297
|
logger.info({ tool: name }, "Tool call received");
|
|
298
|
+
return handleToolCall(name, args);
|
|
299
|
+
});
|
|
250
300
|
|
|
301
|
+
/** Dispatch a tool call to the correct handler. */
|
|
302
|
+
async function handleToolCall(
|
|
303
|
+
name: string,
|
|
304
|
+
args: Record<string, unknown> | undefined,
|
|
305
|
+
): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> {
|
|
251
306
|
switch (name) {
|
|
252
307
|
case "compute_crap": {
|
|
253
308
|
const typed = args as {
|
|
@@ -343,12 +398,21 @@ async function main(): Promise<void> {
|
|
|
343
398
|
}
|
|
344
399
|
|
|
345
400
|
case "score_project": {
|
|
346
|
-
const typed = (args ?? {}) as { format?: "markdown" | "json" | "both" };
|
|
401
|
+
const typed = (args ?? {}) as { format?: "markdown" | "json" | "both"; scope?: string };
|
|
347
402
|
const format = typed.format ?? "both";
|
|
403
|
+
// Resolve scope to a workspace subdirectory
|
|
404
|
+
let scoreRoot = config.pluginRoot;
|
|
405
|
+
if (typed.scope && projectMap) {
|
|
406
|
+
const project = projectMap.projects.find((p) => p.name === typed.scope);
|
|
407
|
+
if (project) {
|
|
408
|
+
const { join } = await import("node:path");
|
|
409
|
+
scoreRoot = join(config.pluginRoot, project.path);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
348
412
|
try {
|
|
349
|
-
const workspace = await estimateWorkspaceLoc(
|
|
413
|
+
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
350
414
|
const score: ProjectScore = computeProjectScore({
|
|
351
|
-
workspaceRoot:
|
|
415
|
+
workspaceRoot: scoreRoot,
|
|
352
416
|
minutesPerLoc: config.minutesPerLoc,
|
|
353
417
|
tdrMaxRating: config.tdrMaxRating,
|
|
354
418
|
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
@@ -625,10 +689,29 @@ async function main(): Promise<void> {
|
|
|
625
689
|
}
|
|
626
690
|
}
|
|
627
691
|
|
|
692
|
+
case "list_projects": {
|
|
693
|
+
return {
|
|
694
|
+
content: [
|
|
695
|
+
{
|
|
696
|
+
type: "text",
|
|
697
|
+
text: JSON.stringify(
|
|
698
|
+
{
|
|
699
|
+
tool: "list_projects",
|
|
700
|
+
isMonorepo: projectMap?.isMonorepo ?? false,
|
|
701
|
+
projects: projectMap?.projects ?? [],
|
|
702
|
+
},
|
|
703
|
+
null,
|
|
704
|
+
2,
|
|
705
|
+
),
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
628
711
|
default:
|
|
629
712
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
630
713
|
}
|
|
631
|
-
}
|
|
714
|
+
}
|
|
632
715
|
|
|
633
716
|
// ------------------------------------------------------------------
|
|
634
717
|
// Resources — topology and reports
|