@kridaydave/code-mapper 1.0.0 → 1.0.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -0
  3. package/bin/code-mapper.mjs +86 -0
  4. package/dist/graph/GraphAnalyzer.js +32 -65
  5. package/dist/graph/GraphAnalyzer.js.map +1 -1
  6. package/dist/graph/GraphBuilder.js +18 -45
  7. package/dist/graph/GraphBuilder.js.map +1 -1
  8. package/dist/index.js +100 -23
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/cache.js +8 -17
  11. package/dist/mcp/cache.js.map +1 -1
  12. package/dist/mcp/resources.js +5 -1
  13. package/dist/mcp/resources.js.map +1 -1
  14. package/dist/mcp/tools.js +190 -35
  15. package/dist/mcp/tools.js.map +1 -1
  16. package/dist/parser/ComplexityAnalyzer.js +19 -2
  17. package/dist/parser/ComplexityAnalyzer.js.map +1 -1
  18. package/dist/parser/FileAnalyzer.js +8 -30
  19. package/dist/parser/FileAnalyzer.js.map +1 -1
  20. package/dist/parser/ProjectParser.js +8 -5
  21. package/dist/parser/ProjectParser.js.map +1 -1
  22. package/dist/parser/ProjectParser.test.js +1 -17
  23. package/dist/parser/ProjectParser.test.js.map +1 -1
  24. package/dist/tui/index.js +239 -0
  25. package/dist/tui/index.js.map +1 -0
  26. package/package.json +82 -35
  27. package/AGENTS.md +0 -174
  28. package/docs/PHASE2_PLAN.md +0 -435
  29. package/fixtures/test-project/calculator.ts +0 -28
  30. package/fixtures/test-project/index.ts +0 -2
  31. package/fixtures/test-project/math.ts +0 -11
  32. package/src/graph/Graph.test.ts +0 -222
  33. package/src/graph/GraphAnalyzer.ts +0 -502
  34. package/src/graph/GraphBuilder.ts +0 -258
  35. package/src/graph/types.ts +0 -42
  36. package/src/index.ts +0 -38
  37. package/src/mcp/cache.ts +0 -89
  38. package/src/mcp/resources.ts +0 -137
  39. package/src/mcp/tools.test.ts +0 -104
  40. package/src/mcp/tools.ts +0 -529
  41. package/src/parser/ComplexityAnalyzer.ts +0 -275
  42. package/src/parser/FileAnalyzer.ts +0 -215
  43. package/src/parser/ProjectParser.test.ts +0 -96
  44. package/src/parser/ProjectParser.ts +0 -172
  45. package/src/parser/types.ts +0 -77
  46. package/src/types/graphology-pagerank.d.ts +0 -20
  47. package/tsconfig.json +0 -17
  48. package/vitest.config.ts +0 -15
package/src/mcp/tools.ts DELETED
@@ -1,529 +0,0 @@
1
- import { resolve } from "node:path";
2
- import * as fs from "node:fs";
3
- import { z } from "zod";
4
- import { McpServer } from "@modelcontextprotocol/server";
5
- import { ProjectParser } from "../parser/ProjectParser.js";
6
- import { Project } from "ts-morph";
7
- import { ComplexityAnalyzer, ComplexityResult } from "../parser/ComplexityAnalyzer.js";
8
- import { GraphBuilder } from "../graph/GraphBuilder.js";
9
- import { GraphAnalyzer } from "../graph/GraphAnalyzer.js";
10
- import { GraphNode, GraphEdge, RankedFile } from "../graph/types.js";
11
- import { analyzerCache, normalizeCacheKey, clearAnalyzerCache, setLastScannedDirectory, getAnalyzerFromCache, setAnalyzerInCache, getPendingAnalyzer, setPendingAnalyzer } from "./cache.js";
12
-
13
- const BLOCKED_PATHS = [
14
- /^c:\\windows/i,
15
- /^c:\\program files/i,
16
- /^c:\\programdata/i,
17
- /^\/etc\//i,
18
- /^\/usr\//i,
19
- /^\/var\//i,
20
- /^\/system volumes/i,
21
- /^\/private\//i,
22
- /^\\\\\?\\/i,
23
- ];
24
-
25
- function validateDirectory(directory: string): string {
26
- const absoluteDir = resolve(directory);
27
-
28
- if (BLOCKED_PATHS.some(pattern => pattern.test(absoluteDir))) {
29
- throw new Error(`Access to system path is not allowed: ${absoluteDir}`);
30
- }
31
-
32
- if (!fs.existsSync(absoluteDir)) {
33
- throw new Error(`Directory does not exist: ${absoluteDir}`);
34
- }
35
-
36
- if (!fs.statSync(absoluteDir).isDirectory()) {
37
- throw new Error(`Path is not a directory: ${absoluteDir}`);
38
- }
39
-
40
- return absoluteDir;
41
- }
42
-
43
- async function getAnalyzer(directory: string): Promise<GraphAnalyzer> {
44
- const normalizedDir = normalizeCacheKey(directory);
45
- const cached = getAnalyzerFromCache(normalizedDir);
46
- if (cached) {
47
- return cached;
48
- }
49
-
50
- const pending = getPendingAnalyzer(normalizedDir);
51
- if (pending) {
52
- return pending;
53
- }
54
-
55
- const analyzerPromise = (async () => {
56
- const parser = new ProjectParser();
57
- const parseResult = await parser.parse(directory);
58
- const builder = new GraphBuilder();
59
- const { graph, nodes, edges } = builder.build(parseResult);
60
- const analyzer = new GraphAnalyzer(graph, parseResult, nodes, edges);
61
- setAnalyzerInCache(normalizedDir, analyzer);
62
- setLastScannedDirectory(normalizedDir);
63
- return analyzer;
64
- })();
65
-
66
- setPendingAnalyzer(normalizedDir, analyzerPromise);
67
- return analyzerPromise;
68
- }
69
-
70
- function safeHandler(fn: () => Promise<{
71
- content: Array<{ type: "text"; text: string }>;
72
- structuredContent?: Record<string, unknown>;
73
- }>): Promise<{
74
- content: Array<{ type: "text"; text: string }>;
75
- structuredContent?: Record<string, unknown>;
76
- isError?: boolean;
77
- }> {
78
- return fn().then(
79
- (result) => result,
80
- (error: unknown) => {
81
- const message = error instanceof Error ? error.message : String(error);
82
- return {
83
- content: [{ type: "text", text: `Error: ${message}` }],
84
- isError: true,
85
- };
86
- }
87
- );
88
- }
89
-
90
- export { validateDirectory, getAnalyzer, safeHandler, clearAnalyzerCache };
91
-
92
- export function registerTools(server: McpServer): void {
93
- // Tool 1: scan_codebase
94
- server.registerTool(
95
- "scan_codebase",
96
- {
97
- title: "Scan Codebase",
98
- description: "Scan a directory and return a summary of all files, functions, classes, and their relationships. Use this first before any other analysis.",
99
- inputSchema: z.object({
100
- directory: z.string().describe("Path to the directory to scan (relative or absolute)"),
101
- }),
102
- outputSchema: z.object({
103
- directory: z.string(),
104
- totalFiles: z.number(),
105
- totalFunctions: z.number(),
106
- totalClasses: z.number(),
107
- totalImports: z.number(),
108
- totalExports: z.number(),
109
- files: z.array(z.object({
110
- relativePath: z.string(),
111
- functionCount: z.number(),
112
- classCount: z.number(),
113
- importCount: z.number(),
114
- exportCount: z.number(),
115
- totalLines: z.number(),
116
- })),
117
- }),
118
- },
119
- async ({ directory }) => {
120
- return await safeHandler(async () => {
121
- const validatedDir = validateDirectory(directory);
122
- const analyzer = await getAnalyzer(validatedDir);
123
- const parseResult = analyzer.getParseResult();
124
-
125
- const output = {
126
- directory: parseResult.directory,
127
- totalFiles: parseResult.totalFiles,
128
- totalFunctions: parseResult.totalFunctions,
129
- totalClasses: parseResult.totalClasses,
130
- totalImports: parseResult.totalImports,
131
- totalExports: parseResult.totalExports,
132
- files: parseResult.files.map(f => ({
133
- relativePath: f.relativePath,
134
- functionCount: f.functions.length,
135
- classCount: f.classes.length,
136
- importCount: f.imports.length,
137
- exportCount: f.exports.length,
138
- totalLines: f.totalLines,
139
- })),
140
- };
141
-
142
- return {
143
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
144
- structuredContent: output,
145
- };
146
- });
147
- }
148
- );
149
-
150
- // Tool 2: find_function
151
- server.registerTool(
152
- "find_function",
153
- {
154
- title: "Find Function or Class",
155
- description: "Search for a function or class by name across the codebase. Returns location, signature, callers, and callees.",
156
- inputSchema: z.object({
157
- name: z.string().describe("Name of the function or class to search for (case-insensitive partial match)"),
158
- directory: z.string().describe("Path to the codebase directory (must be scanned first)"),
159
- type: z.enum(["function", "class", "any"]).default("any").describe("Type of symbol to search for"),
160
- }),
161
- outputSchema: z.object({
162
- matches: z.array(z.object({
163
- name: z.string(),
164
- filePath: z.string(),
165
- relativePath: z.string(),
166
- lineNumber: z.number(),
167
- kind: z.string(),
168
- parameters: z.array(z.string()),
169
- returnType: z.string(),
170
- isExported: z.boolean(),
171
- })),
172
- callers: z.array(z.string()),
173
- callees: z.array(z.string()),
174
- totalMatches: z.number(),
175
- }),
176
- },
177
- async ({ name, directory, type }) => {
178
- return await safeHandler(async () => {
179
- const validatedDir = validateDirectory(directory);
180
- const analyzer = await getAnalyzer(validatedDir);
181
- const matches = analyzer.findFunction(name, type);
182
-
183
- const callers: string[] = [];
184
- const callees: string[] = [];
185
-
186
- for (const match of matches) {
187
- const nodeId = match.kind === "class"
188
- ? `class:${match.filePath}:${match.name}:${match.lineNumber}`
189
- : `fn:${match.filePath}:${match.name}:${match.lineNumber}`;
190
- const directCallers = analyzer.getCallers(nodeId);
191
- const directCallees = analyzer.getCallees(nodeId);
192
- if (directCallers.length > 0 || directCallees.length > 0) {
193
- callers.push(...directCallers);
194
- callees.push(...directCallees);
195
- } else {
196
- const fileId = `file:${match.filePath}`;
197
- const fileCallers = analyzer.getCallers(fileId);
198
- const fileCallees = analyzer.getCallees(fileId);
199
- callers.push(...fileCallers.map((c: string) => `${c} (file-level)`));
200
- callees.push(...fileCallees.map((c: string) => `${c} (file-level)`));
201
- }
202
- }
203
-
204
- const output = {
205
- matches,
206
- callers: [...new Set(callers)],
207
- callees: [...new Set(callees)],
208
- totalMatches: matches.length,
209
- };
210
-
211
- return {
212
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
213
- structuredContent: output,
214
- };
215
- });
216
- }
217
- );
218
-
219
- // Tool 3: analyze_dependencies
220
- server.registerTool(
221
- "analyze_dependencies",
222
- {
223
- title: "Analyze Dependencies",
224
- description: "Returns the dependency graph between files. Can return the full graph or a subgraph for a specific file. Supports JSON, Mermaid, DOT, and PlantUML output formats.",
225
- inputSchema: z.object({
226
- directory: z.string().describe("Path to the codebase directory (must be scanned first)"),
227
- targetFile: z.string().optional().describe("Optional: filter to show only nodes related to this file"),
228
- format: z.enum(["json", "mermaid", "dot", "plantuml"]).default("json").describe("Output format: json for data, mermaid/dot/plantuml for visual diagram"),
229
- }),
230
- outputSchema: z.object({
231
- format: z.string(),
232
- nodeCount: z.number(),
233
- edgeCount: z.number(),
234
- nodes: z.array(z.object({
235
- id: z.string(),
236
- kind: z.string(),
237
- label: z.string(),
238
- filePath: z.string(),
239
- })),
240
- edges: z.array(z.object({
241
- source: z.string(),
242
- target: z.string(),
243
- kind: z.string(),
244
- label: z.string(),
245
- })),
246
- cycles: z.array(z.array(z.string())),
247
- mermaid: z.string().optional(),
248
- dot: z.string().optional(),
249
- plantuml: z.string().optional(),
250
- }),
251
- },
252
- async ({ directory, targetFile, format }) => {
253
- return await safeHandler(async () => {
254
- const validatedDir = validateDirectory(directory);
255
- const analyzer = await getAnalyzer(validatedDir);
256
- let nodes = analyzer.getNodes();
257
- let edges = analyzer.getEdges();
258
- let cycles: string[][] = [];
259
-
260
- if (targetFile) {
261
- const sanitizedTarget = targetFile.replace(/[\/\\]/g, "").replace(/\.\./g, ".");
262
- const matchingNodes = nodes.filter((n: GraphNode) => n.filePath.includes(targetFile) || n.label.includes(targetFile));
263
- const matchingIds = new Set(matchingNodes.map((n: GraphNode) => n.id));
264
- const expandedIds = new Set<string>(matchingIds);
265
- for (const nodeId of matchingIds) {
266
- for (const neighbor of analyzer.getGraph().inNeighbors(nodeId)) {
267
- expandedIds.add(neighbor);
268
- }
269
- for (const neighbor of analyzer.getGraph().outNeighbors(nodeId)) {
270
- expandedIds.add(neighbor);
271
- }
272
- }
273
- const expandedNodeSet = new Set(expandedIds);
274
- nodes = nodes.filter((n: GraphNode) => expandedNodeSet.has(n.id));
275
- edges = edges.filter((e: GraphEdge) => expandedNodeSet.has(e.source) && expandedNodeSet.has(e.target));
276
- cycles = [];
277
- } else {
278
- cycles = analyzer.detectCycles();
279
- }
280
-
281
- const output: {
282
- format: "json" | "mermaid" | "dot" | "plantuml";
283
- nodeCount: number;
284
- edgeCount: number;
285
- nodes: { id: string; kind: string; label: string; filePath: string }[];
286
- edges: { source: string; target: string; kind: string; label: string }[];
287
- cycles: string[][];
288
- mermaid?: string;
289
- dot?: string;
290
- plantuml?: string;
291
- } = {
292
- format,
293
- nodeCount: nodes.length,
294
- edgeCount: edges.length,
295
- nodes: nodes.map((n: GraphNode) => ({ id: n.id, kind: n.kind, label: n.label, filePath: n.filePath })),
296
- edges: edges.map((e: GraphEdge) => ({ source: e.source, target: e.target, kind: e.kind, label: e.label })),
297
- cycles,
298
- };
299
-
300
- if (format === "mermaid") {
301
- output.mermaid = analyzer.toMermaid(targetFile);
302
- } else if (format === "dot") {
303
- output.dot = analyzer.toDot(targetFile);
304
- } else if (format === "plantuml") {
305
- output.plantuml = analyzer.toPlantUML(targetFile);
306
- }
307
-
308
- return {
309
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
310
- structuredContent: output,
311
- };
312
- });
313
- }
314
- );
315
-
316
- // Tool 4: rank_impact
317
- server.registerTool(
318
- "rank_impact",
319
- {
320
- title: "Rank Impact",
321
- description: "Ranks files by centrality to identify the most important/central files in the codebase. Use this to answer questions like 'Where should I add a new feature?' or 'Which files are most critical?'",
322
- inputSchema: z.object({
323
- directory: z.string().describe("Path to the codebase directory (must be scanned first)"),
324
- metric: z.enum(["inDegree", "outDegree", "betweenness", "pagerank"]).default("inDegree").describe("Centrality metric: inDegree (most depended upon), outDegree (most dependencies), betweenness (most on critical paths), pagerank (most influential based on random walk)"),
325
- topN: z.number().default(10).describe("Number of top results to return"),
326
- }),
327
- outputSchema: z.object({
328
- metric: z.string(),
329
- ranked: z.array(z.object({
330
- relativePath: z.string(),
331
- score: z.number(),
332
- functionCount: z.number(),
333
- classCount: z.number(),
334
- importCount: z.number(),
335
- exportCount: z.number(),
336
- })),
337
- recommendations: z.array(z.string()),
338
- }),
339
- },
340
- async ({ directory, metric, topN }) => {
341
- return await safeHandler(async () => {
342
- const validatedDir = validateDirectory(directory);
343
- const analyzer = await getAnalyzer(validatedDir);
344
- const ranked = analyzer.rankImpact(metric);
345
- const top = ranked.slice(0, topN);
346
-
347
- const recommendations: string[] = [];
348
- if (top.length > 0) {
349
- recommendations.push(`Most central file: ${top[0].relativePath} (score: ${top[0].score})`);
350
- recommendations.push("This file is the most depended-upon module. Changes here will have the widest impact.");
351
- if (top.length > 1) {
352
- recommendations.push(`Second most central: ${top[1].relativePath} (score: ${top[1].score})`);
353
- }
354
- const leafNodes = ranked.filter((r: RankedFile) => r.score === 0).slice(0, 3);
355
- if (leafNodes.length > 0) {
356
- recommendations.push(`Leaf files (no dependents): ${leafNodes.map((l: RankedFile) => l.relativePath).join(", ")}`);
357
- }
358
- }
359
-
360
- const output = {
361
- metric,
362
- ranked: top.map((r: RankedFile) => ({
363
- relativePath: r.relativePath,
364
- score: r.score,
365
- functionCount: r.functionCount,
366
- classCount: r.classCount,
367
- importCount: r.importCount,
368
- exportCount: r.exportCount,
369
- })),
370
- recommendations,
371
- };
372
-
373
- return {
374
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
375
- structuredContent: output,
376
- };
377
- });
378
- }
379
- );
380
-
381
- // Tool 5: trace_call_chain
382
- server.registerTool(
383
- "trace_call_chain",
384
- {
385
- title: "Trace Call Chain",
386
- description: "Traces the call chain / dependency path from one function or file to another. Shows the full path through the codebase.",
387
- inputSchema: z.object({
388
- from: z.string().describe("Starting function, class, or file name (case-insensitive partial match)"),
389
- to: z.string().describe("Target function, class, or file name (case-insensitive partial match)"),
390
- directory: z.string().describe("Path to the codebase directory (must be scanned first)"),
391
- }),
392
- outputSchema: z.object({
393
- found: z.boolean(),
394
- from: z.string(),
395
- to: z.string(),
396
- paths: z.array(z.array(z.string())),
397
- pathCount: z.number(),
398
- }),
399
- },
400
- async ({ from, to, directory }) => {
401
- return await safeHandler(async () => {
402
- const validatedDir = validateDirectory(directory);
403
- const analyzer = await getAnalyzer(validatedDir);
404
- const result = analyzer.traceCallChain(from, to);
405
-
406
- const output = {
407
- found: result.found,
408
- from,
409
- to,
410
- paths: result.paths,
411
- pathCount: result.paths.length,
412
- };
413
-
414
- return {
415
- content: [{
416
- type: "text",
417
- text: result.found
418
- ? `Found ${result.paths.length} path(s) from "${from}" to "${to}":\n${JSON.stringify(result.paths, null, 2)}`
419
- : `No path found from "${from}" to "${to}". These symbols may not be connected in the dependency graph.`,
420
- }],
421
- structuredContent: output,
422
- };
423
- });
424
- }
425
- );
426
-
427
- // Tool 6: analyze_complexity
428
- server.registerTool(
429
- "analyze_complexity",
430
- {
431
- title: "Analyze Code Complexity",
432
- description: "Analyze code complexity metrics for each file in the codebase. Identifies files that may need refactoring based on cyclomatic complexity, cognitive complexity, nesting depth, and size.",
433
- inputSchema: z.object({
434
- directory: z.string().describe("Path to the codebase directory (must be scanned first)"),
435
- threshold: z.number().optional().describe("Minimum complexity score to report (0-100)"),
436
- topN: z.number().default(10).describe("Number of most complex files to return"),
437
- }),
438
- outputSchema: z.object({
439
- totalFiles: z.number(),
440
- files: z.array(z.object({
441
- relativePath: z.string(),
442
- cyclomaticComplexity: z.number(),
443
- cognitiveComplexity: z.number(),
444
- nestingDepth: z.number(),
445
- linesOfCode: z.number(),
446
- functionCount: z.number(),
447
- classCount: z.number(),
448
- overallScore: z.number(),
449
- issues: z.array(z.string()),
450
- })),
451
- summary: z.object({
452
- avgComplexity: z.number(),
453
- maxComplexity: z.number(),
454
- filesNeedingRefactoring: z.number(),
455
- }),
456
- }),
457
- },
458
- async ({ directory, threshold, topN }) => {
459
- return await safeHandler(async () => {
460
- const validatedDir = validateDirectory(directory);
461
- const parser = new ProjectParser();
462
- const parseResult = await parser.parse(validatedDir);
463
-
464
- const project = new Project({
465
- skipAddingFilesFromTsConfig: true,
466
- compilerOptions: {
467
- allowJs: true,
468
- checkJs: false,
469
- noEmit: true,
470
- },
471
- });
472
-
473
- const sourceFiles = project.getSourceFiles();
474
- for (const sf of sourceFiles) {
475
- project.removeSourceFile(sf);
476
- }
477
-
478
- const files = parseResult.files.map(f => f.filePath);
479
- project.addSourceFilesAtPaths(files);
480
-
481
- const complexityAnalyzer = new ComplexityAnalyzer(project, validatedDir);
482
- const results = complexityAnalyzer.analyzeProject(parseResult);
483
-
484
- const filteredResults = threshold
485
- ? results.filter(r => r.overallScore >= threshold)
486
- : results;
487
-
488
- const sortedResults = filteredResults
489
- .sort((a, b) => b.overallScore - a.overallScore)
490
- .slice(0, topN);
491
-
492
- const avgComplexity = results.length > 0
493
- ? Math.round(results.reduce((sum, r) => sum + r.overallScore, 0) / results.length)
494
- : 0;
495
-
496
- const maxComplexity = results.length > 0
497
- ? Math.max(...results.map(r => r.overallScore))
498
- : 0;
499
-
500
- const filesNeedingRefactoring = results.filter(r => r.overallScore >= 50).length;
501
-
502
- const output = {
503
- totalFiles: results.length,
504
- files: sortedResults.map(r => ({
505
- relativePath: r.relativePath,
506
- cyclomaticComplexity: r.cyclomaticComplexity,
507
- cognitiveComplexity: r.cognitiveComplexity,
508
- nestingDepth: r.nestingDepth,
509
- linesOfCode: r.linesOfCode,
510
- functionCount: r.functionCount,
511
- classCount: r.classCount,
512
- overallScore: r.overallScore,
513
- issues: r.issues,
514
- })),
515
- summary: {
516
- avgComplexity,
517
- maxComplexity,
518
- filesNeedingRefactoring,
519
- },
520
- };
521
-
522
- return {
523
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
524
- structuredContent: output,
525
- };
526
- });
527
- }
528
- );
529
- }