claude-crap 0.3.7 → 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 +74 -7
- 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/dart-analyzer.d.ts +41 -0
- package/dist/adapters/dart-analyzer.d.ts.map +1 -0
- package/dist/adapters/dart-analyzer.js +120 -0
- package/dist/adapters/dart-analyzer.js.map +1 -0
- 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 +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/crap-config.d.ts +4 -0
- package/dist/crap-config.d.ts.map +1 -1
- package/dist/crap-config.js +51 -28
- package/dist/crap-config.js.map +1 -1
- package/dist/dashboard/file-detail.d.ts.map +1 -1
- package/dist/dashboard/file-detail.js.map +1 -1
- package/dist/dashboard/server.d.ts +2 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +7 -12
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +89 -5
- package/dist/index.js.map +1 -1
- package/dist/metrics/workspace-walker.d.ts +4 -1
- package/dist/metrics/workspace-walker.d.ts.map +1 -1
- package/dist/metrics/workspace-walker.js +12 -28
- package/dist/metrics/workspace-walker.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/auto-scan.d.ts +1 -0
- package/dist/scanner/auto-scan.d.ts.map +1 -1
- package/dist/scanner/auto-scan.js +14 -5
- package/dist/scanner/auto-scan.js.map +1 -1
- package/dist/scanner/bootstrap.d.ts +1 -1
- package/dist/scanner/bootstrap.d.ts.map +1 -1
- package/dist/scanner/bootstrap.js +15 -1
- package/dist/scanner/bootstrap.js.map +1 -1
- package/dist/scanner/complexity-scanner.d.ts +2 -0
- package/dist/scanner/complexity-scanner.d.ts.map +1 -1
- package/dist/scanner/complexity-scanner.js +11 -26
- package/dist/scanner/complexity-scanner.js.map +1 -1
- package/dist/scanner/detector.d.ts +24 -4
- package/dist/scanner/detector.d.ts.map +1 -1
- package/dist/scanner/detector.js +110 -10
- package/dist/scanner/detector.js.map +1 -1
- package/dist/scanner/runner.d.ts +4 -1
- package/dist/scanner/runner.d.ts.map +1 -1
- package/dist/scanner/runner.js +25 -3
- 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/dist/shared/exclusions.d.ts +53 -0
- package/dist/shared/exclusions.d.ts.map +1 -0
- package/dist/shared/exclusions.js +126 -0
- package/dist/shared/exclusions.js.map +1 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +37 -0
- package/plugin/bundle/mcp-server.mjs +762 -144
- package/plugin/bundle/mcp-server.mjs.map +4 -4
- package/plugin/package-lock.json +15 -2
- package/plugin/package.json +2 -1
- package/scripts/bundle-plugin.mjs +2 -1
- package/src/adapters/common.ts +1 -1
- package/src/adapters/dart-analyzer.ts +161 -0
- package/src/adapters/dotnet-format.ts +125 -0
- package/src/adapters/index.ts +8 -0
- package/src/crap-config.ts +78 -18
- package/src/dashboard/file-detail.ts +0 -2
- package/src/dashboard/server.ts +9 -10
- package/src/index.ts +103 -5
- package/src/metrics/workspace-walker.ts +15 -27
- package/src/monorepo/project-map.ts +476 -0
- package/src/scanner/auto-scan.ts +17 -6
- package/src/scanner/bootstrap.ts +18 -1
- package/src/scanner/complexity-scanner.ts +15 -26
- package/src/scanner/detector.ts +119 -10
- package/src/scanner/runner.ts +25 -2
- package/src/schemas/tool-schemas.ts +17 -1
- package/src/shared/exclusions.ts +156 -0
- package/src/tests/adapters/dispatch.test.ts +2 -2
- 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/exclusions.test.ts +117 -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 +31 -11
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,6 +95,23 @@ async function main(): Promise<void> {
|
|
|
93
95
|
"claude-crap MCP server starting",
|
|
94
96
|
);
|
|
95
97
|
|
|
98
|
+
// Load user-defined exclusions and projectDirs from .claude-crap.json (non-fatal).
|
|
99
|
+
let userExclusions: ReadonlyArray<string> = [];
|
|
100
|
+
let userProjectDirs: ReadonlyArray<string> = [];
|
|
101
|
+
try {
|
|
102
|
+
const crapConfig = loadCrapConfig({ workspaceRoot: config.pluginRoot });
|
|
103
|
+
userExclusions = crapConfig.exclude;
|
|
104
|
+
userProjectDirs = crapConfig.projectDirs;
|
|
105
|
+
if (userExclusions.length > 0) {
|
|
106
|
+
logger.info({ exclude: userExclusions }, "user exclusions loaded from .claude-crap.json");
|
|
107
|
+
}
|
|
108
|
+
if (userProjectDirs.length > 0) {
|
|
109
|
+
logger.info({ projectDirs: userProjectDirs }, "user projectDirs loaded from .claude-crap.json");
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// Non-fatal — use empty exclusions.
|
|
113
|
+
}
|
|
114
|
+
|
|
96
115
|
// Long-lived engines. Created once at boot and reused for every call.
|
|
97
116
|
const astEngine = new TreeSitterEngine();
|
|
98
117
|
const sarifStore = new SarifStore({
|
|
@@ -105,6 +124,39 @@ async function main(): Promise<void> {
|
|
|
105
124
|
"SARIF store ready",
|
|
106
125
|
);
|
|
107
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
|
+
|
|
108
160
|
// Try to start the local Vue.js dashboard. Failures here are
|
|
109
161
|
// intentionally non-fatal — the MCP server still works without it.
|
|
110
162
|
let dashboard: DashboardHandle | null = null;
|
|
@@ -112,9 +164,10 @@ async function main(): Promise<void> {
|
|
|
112
164
|
dashboard = await startDashboard({
|
|
113
165
|
config,
|
|
114
166
|
sarifStore,
|
|
115
|
-
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot),
|
|
167
|
+
workspaceStatsProvider: () => estimateWorkspaceLoc(config.pluginRoot, { exclude: userExclusions }),
|
|
116
168
|
logger,
|
|
117
169
|
astEngine,
|
|
170
|
+
exclude: userExclusions,
|
|
118
171
|
});
|
|
119
172
|
} catch (err) {
|
|
120
173
|
logger.warn(
|
|
@@ -222,6 +275,11 @@ async function main(): Promise<void> {
|
|
|
222
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.",
|
|
223
276
|
inputSchema: bootstrapScannerSchema,
|
|
224
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
|
+
},
|
|
225
283
|
],
|
|
226
284
|
}));
|
|
227
285
|
|
|
@@ -231,10 +289,20 @@ async function main(): Promise<void> {
|
|
|
231
289
|
// The MCP SDK has already validated `args` against the tool's JSON
|
|
232
290
|
// Schema by the time this handler runs, so we cast to the expected
|
|
233
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.
|
|
234
295
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
235
296
|
const { name, arguments: args } = request.params;
|
|
236
297
|
logger.info({ tool: name }, "Tool call received");
|
|
298
|
+
return handleToolCall(name, args);
|
|
299
|
+
});
|
|
237
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 }> {
|
|
238
306
|
switch (name) {
|
|
239
307
|
case "compute_crap": {
|
|
240
308
|
const typed = args as {
|
|
@@ -330,12 +398,21 @@ async function main(): Promise<void> {
|
|
|
330
398
|
}
|
|
331
399
|
|
|
332
400
|
case "score_project": {
|
|
333
|
-
const typed = (args ?? {}) as { format?: "markdown" | "json" | "both" };
|
|
401
|
+
const typed = (args ?? {}) as { format?: "markdown" | "json" | "both"; scope?: string };
|
|
334
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
|
+
}
|
|
335
412
|
try {
|
|
336
|
-
const workspace = await estimateWorkspaceLoc(
|
|
413
|
+
const workspace = await estimateWorkspaceLoc(scoreRoot, { exclude: userExclusions });
|
|
337
414
|
const score: ProjectScore = computeProjectScore({
|
|
338
|
-
workspaceRoot:
|
|
415
|
+
workspaceRoot: scoreRoot,
|
|
339
416
|
minutesPerLoc: config.minutesPerLoc,
|
|
340
417
|
tdrMaxRating: config.tdrMaxRating,
|
|
341
418
|
workspace: { physicalLoc: workspace.physicalLoc, fileCount: workspace.fileCount },
|
|
@@ -585,6 +662,7 @@ async function main(): Promise<void> {
|
|
|
585
662
|
const result = await autoScan(config.pluginRoot, sarifStore, logger, {
|
|
586
663
|
engine: astEngine,
|
|
587
664
|
cyclomaticMax: config.cyclomaticMax,
|
|
665
|
+
exclude: userExclusions,
|
|
588
666
|
});
|
|
589
667
|
const markdown = renderAutoScanMarkdown(result);
|
|
590
668
|
return {
|
|
@@ -611,10 +689,29 @@ async function main(): Promise<void> {
|
|
|
611
689
|
}
|
|
612
690
|
}
|
|
613
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
|
+
|
|
614
711
|
default:
|
|
615
712
|
throw new Error(`[claude-crap] Unknown tool: ${name}`);
|
|
616
713
|
}
|
|
617
|
-
}
|
|
714
|
+
}
|
|
618
715
|
|
|
619
716
|
// ------------------------------------------------------------------
|
|
620
717
|
// Resources — topology and reports
|
|
@@ -676,6 +773,7 @@ async function main(): Promise<void> {
|
|
|
676
773
|
autoScan(config.pluginRoot, sarifStore, logger, {
|
|
677
774
|
engine: astEngine,
|
|
678
775
|
cyclomaticMax: config.cyclomaticMax,
|
|
776
|
+
exclude: userExclusions,
|
|
679
777
|
})
|
|
680
778
|
.then((result) => {
|
|
681
779
|
const scanners = result.results
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { promises as fs } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
18
|
+
import { join, relative } from "node:path";
|
|
19
|
+
|
|
20
|
+
import { createExclusionFilter } from "../shared/exclusions.js";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Result returned by {@link estimateWorkspaceLoc}.
|
|
@@ -29,26 +31,9 @@ export interface WorkspaceWalkResult {
|
|
|
29
31
|
readonly truncated: boolean;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
const SKIP_DIRS: ReadonlySet<string> = new Set([
|
|
37
|
-
"node_modules",
|
|
38
|
-
".git",
|
|
39
|
-
"dist",
|
|
40
|
-
"build",
|
|
41
|
-
"out",
|
|
42
|
-
"target",
|
|
43
|
-
".venv",
|
|
44
|
-
"venv",
|
|
45
|
-
"__pycache__",
|
|
46
|
-
".cache",
|
|
47
|
-
".next",
|
|
48
|
-
".nuxt",
|
|
49
|
-
".claude-crap",
|
|
50
|
-
".codesight",
|
|
51
|
-
]);
|
|
34
|
+
// Directory exclusions are now centralized in src/shared/exclusions.ts.
|
|
35
|
+
// The createExclusionFilter() factory is called once per walk with
|
|
36
|
+
// optional user-defined patterns from .claude-crap.json.
|
|
52
37
|
|
|
53
38
|
/**
|
|
54
39
|
* Extensions the walker treats as "code". Anything else is ignored,
|
|
@@ -91,9 +76,14 @@ export const MAX_FILES_WALKED = 20_000;
|
|
|
91
76
|
* (which is tiny and contains the manifest).
|
|
92
77
|
*
|
|
93
78
|
* @param workspaceRoot Absolute path to the workspace root.
|
|
79
|
+
* @param options Optional settings including user-defined exclusion patterns.
|
|
94
80
|
* @returns A {@link WorkspaceWalkResult} snapshot.
|
|
95
81
|
*/
|
|
96
|
-
export async function estimateWorkspaceLoc(
|
|
82
|
+
export async function estimateWorkspaceLoc(
|
|
83
|
+
workspaceRoot: string,
|
|
84
|
+
options?: { exclude?: ReadonlyArray<string> },
|
|
85
|
+
): Promise<WorkspaceWalkResult> {
|
|
86
|
+
const filter = createExclusionFilter(options?.exclude);
|
|
97
87
|
let physicalLoc = 0;
|
|
98
88
|
let fileCount = 0;
|
|
99
89
|
let truncated = false;
|
|
@@ -108,11 +98,9 @@ export async function estimateWorkspaceLoc(workspaceRoot: string): Promise<Works
|
|
|
108
98
|
}
|
|
109
99
|
for (const entry of entries) {
|
|
110
100
|
if (truncated) return;
|
|
111
|
-
// Skip hidden files except the plugin manifest dir.
|
|
112
|
-
if (entry.name.startsWith(".") && entry.name !== ".claude-plugin") continue;
|
|
113
101
|
const full = join(dir, entry.name);
|
|
114
102
|
if (entry.isDirectory()) {
|
|
115
|
-
if (
|
|
103
|
+
if (filter.shouldSkipDir(entry.name)) continue;
|
|
116
104
|
await walk(full);
|
|
117
105
|
continue;
|
|
118
106
|
}
|
|
@@ -122,6 +110,8 @@ export async function estimateWorkspaceLoc(workspaceRoot: string): Promise<Works
|
|
|
122
110
|
if (dot < 0) continue;
|
|
123
111
|
const ext = lower.substring(dot);
|
|
124
112
|
if (!CODE_EXTENSIONS.has(ext)) continue;
|
|
113
|
+
const relPath = relative(workspaceRoot, full);
|
|
114
|
+
if (filter.shouldSkipFile(relPath, entry.name)) continue;
|
|
125
115
|
fileCount += 1;
|
|
126
116
|
if (fileCount > MAX_FILES_WALKED) {
|
|
127
117
|
truncated = true;
|
|
@@ -130,8 +120,6 @@ export async function estimateWorkspaceLoc(workspaceRoot: string): Promise<Works
|
|
|
130
120
|
try {
|
|
131
121
|
const content = await fs.readFile(full, "utf8");
|
|
132
122
|
if (content.length > 0) {
|
|
133
|
-
// Subtract 1 for trailing newline, matching what most editors
|
|
134
|
-
// report as the file's line count.
|
|
135
123
|
const lines = content.split(/\r?\n/).length;
|
|
136
124
|
physicalLoc += content.endsWith("\n") ? lines - 1 : lines;
|
|
137
125
|
}
|