codebase-analyzer-mcp 2.0.4 → 2.1.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +6 -4
- package/CLAUDE.md +6 -10
- package/README.md +29 -16
- package/agents/analysis/architecture-analyzer.md +1 -1
- package/agents/analysis/dataflow-tracer.md +1 -1
- package/agents/analysis/pattern-detective.md +1 -1
- package/agents/research/codebase-explorer.md +18 -10
- package/commands/analyze.md +8 -8
- package/dist/cli/index.js +530 -54
- package/dist/mcp/server.js +482 -45
- package/package.json +6 -9
- package/skills/codebase-analysis/SKILL.md +27 -94
- package/skills/codebase-analysis/references/api-reference.md +68 -1
- package/commands/compare.md +0 -66
- package/commands/explore.md +0 -64
- package/commands/patterns.md +0 -75
- package/commands/trace.md +0 -65
- package/skills/add-mcp-tool/SKILL.md +0 -209
- package/skills/debugging-analysis/SKILL.md +0 -213
package/dist/mcp/server.js
CHANGED
|
@@ -31228,7 +31228,7 @@ function estimateTokens(obj) {
|
|
|
31228
31228
|
function buildAnalysisResult(analysisId, source, depth, surface, structural, semantic, durationMs) {
|
|
31229
31229
|
const summary = buildSummary(surface, structural, semantic);
|
|
31230
31230
|
const sections = buildExpandableSections(surface, structural, semantic);
|
|
31231
|
-
const forAgent = buildAgentDigest(surface, summary, sections);
|
|
31231
|
+
const forAgent = buildAgentDigest(analysisId, surface, summary, sections);
|
|
31232
31232
|
const tokenCost = estimateTokens({
|
|
31233
31233
|
repositoryMap: surface.repositoryMap,
|
|
31234
31234
|
summary,
|
|
@@ -31285,24 +31285,34 @@ function buildExpandableSections(surface, structural, semantic) {
|
|
|
31285
31285
|
const sections = [];
|
|
31286
31286
|
for (const module of surface.identifiedModules.slice(0, 10)) {
|
|
31287
31287
|
const structuralData = structural.find((s2) => s2.modulePath === module.path);
|
|
31288
|
+
const isDocModule = module.primaryLanguage === "Markdown" || module.primaryLanguage === "MDX";
|
|
31288
31289
|
const section = {
|
|
31289
31290
|
id: `module_${module.path.replace(/[^a-zA-Z0-9]/g, "_")}`,
|
|
31290
31291
|
title: `Module: ${module.name}`,
|
|
31291
31292
|
type: "module",
|
|
31292
|
-
summary: `${module.type} module with ${module.fileCount} files in ${module.primaryLanguage}`,
|
|
31293
|
-
canExpand: !!structuralData,
|
|
31293
|
+
summary: isDocModule ? `Documentation module with ${module.fileCount} files` : `${module.type} module with ${module.fileCount} files in ${module.primaryLanguage}`,
|
|
31294
|
+
canExpand: !!(structuralData || isDocModule),
|
|
31294
31295
|
expansionCost: {
|
|
31295
31296
|
detail: structuralData ? estimateTokens(structuralData.symbols.slice(0, 20)) : 0,
|
|
31296
31297
|
full: structuralData ? estimateTokens(structuralData) : 0
|
|
31297
31298
|
}
|
|
31298
31299
|
};
|
|
31299
31300
|
if (structuralData) {
|
|
31300
|
-
|
|
31301
|
-
|
|
31302
|
-
|
|
31303
|
-
|
|
31304
|
-
|
|
31305
|
-
|
|
31301
|
+
if (isDocModule) {
|
|
31302
|
+
const headings = structuralData.symbols.filter((s2) => s2.type === "class" || s2.type === "function").slice(0, 20).map((s2) => ({ title: s2.name, file: s2.file, line: s2.line }));
|
|
31303
|
+
section.detail = {
|
|
31304
|
+
type: "documentation",
|
|
31305
|
+
headings,
|
|
31306
|
+
fileCount: module.fileCount
|
|
31307
|
+
};
|
|
31308
|
+
} else {
|
|
31309
|
+
section.detail = {
|
|
31310
|
+
exports: structuralData.exports,
|
|
31311
|
+
complexity: structuralData.complexity,
|
|
31312
|
+
symbolCount: structuralData.symbols.length,
|
|
31313
|
+
importCount: structuralData.imports.length
|
|
31314
|
+
};
|
|
31315
|
+
}
|
|
31306
31316
|
}
|
|
31307
31317
|
sections.push(section);
|
|
31308
31318
|
}
|
|
@@ -31366,7 +31376,7 @@ function buildExpandableSections(surface, structural, semantic) {
|
|
|
31366
31376
|
}
|
|
31367
31377
|
return sections;
|
|
31368
31378
|
}
|
|
31369
|
-
function buildAgentDigest(surface, summary, sections) {
|
|
31379
|
+
function buildAgentDigest(analysisId, surface, summary, sections) {
|
|
31370
31380
|
const { repositoryMap, complexity } = surface;
|
|
31371
31381
|
const quickSummary = `${repositoryMap.name} is a ${summary.complexity} complexity ${summary.architectureType} codebase with ${repositoryMap.fileCount} files primarily in ${repositoryMap.languages[0]?.language || "mixed languages"}. ${summary.primaryPatterns.length > 0 ? `Key patterns include ${summary.primaryPatterns.slice(0, 3).join(", ")}.` : ""}`;
|
|
31372
31382
|
const keyInsights = [];
|
|
@@ -31396,6 +31406,7 @@ function buildAgentDigest(surface, summary, sections) {
|
|
|
31396
31406
|
suggestedNextSteps.push(`Focus on core modules: ${coreModules.map((m2) => m2.name).join(", ")}`);
|
|
31397
31407
|
}
|
|
31398
31408
|
}
|
|
31409
|
+
suggestedNextSteps.push(`Use read_files with analysisId "${analysisId}" to read specific files from the repository`);
|
|
31399
31410
|
return {
|
|
31400
31411
|
quickSummary,
|
|
31401
31412
|
keyInsights,
|
|
@@ -53445,6 +53456,10 @@ async function surfaceAnalysis(repoPath, options = {}) {
|
|
|
53445
53456
|
});
|
|
53446
53457
|
const fileInfos = await gatherFileInfo(repoPath, files);
|
|
53447
53458
|
const repositoryMap = buildRepositoryMap(repoPath, fileInfos, options.sourceName);
|
|
53459
|
+
const readmeContent = await readReadmeContent(repoPath, fileInfos);
|
|
53460
|
+
if (readmeContent) {
|
|
53461
|
+
repositoryMap.readme = readmeContent.slice(0, 5000);
|
|
53462
|
+
}
|
|
53448
53463
|
const identifiedModules = identifyModules(fileInfos);
|
|
53449
53464
|
const complexity = calculateComplexity(fileInfos, identifiedModules);
|
|
53450
53465
|
const estimatedAnalysisTime = estimateAnalysisTimes(fileInfos, complexity);
|
|
@@ -53508,7 +53523,6 @@ function buildRepositoryMap(repoPath, files, sourceName) {
|
|
|
53508
53523
|
const estimatedTokens = Math.ceil(totalSize / 4);
|
|
53509
53524
|
const entryPoints = findEntryPoints(files);
|
|
53510
53525
|
const structure = buildDirectoryTree(files);
|
|
53511
|
-
const readme = extractReadme(repoPath, files);
|
|
53512
53526
|
return {
|
|
53513
53527
|
name,
|
|
53514
53528
|
languages,
|
|
@@ -53516,8 +53530,7 @@ function buildRepositoryMap(repoPath, files, sourceName) {
|
|
|
53516
53530
|
totalSize,
|
|
53517
53531
|
estimatedTokens,
|
|
53518
53532
|
entryPoints,
|
|
53519
|
-
structure
|
|
53520
|
-
readme
|
|
53533
|
+
structure
|
|
53521
53534
|
};
|
|
53522
53535
|
}
|
|
53523
53536
|
function findEntryPoints(files) {
|
|
@@ -53593,17 +53606,6 @@ function buildDirectoryTree(files) {
|
|
|
53593
53606
|
};
|
|
53594
53607
|
return collapseTree(root);
|
|
53595
53608
|
}
|
|
53596
|
-
function extractReadme(repoPath, files) {
|
|
53597
|
-
const readmeFile = files.find((f) => f.relativePath.toLowerCase() === "readme.md" || f.relativePath.toLowerCase() === "readme");
|
|
53598
|
-
if (readmeFile && readmeFile.size < 50000) {
|
|
53599
|
-
try {
|
|
53600
|
-
return `README found at ${readmeFile.relativePath}`;
|
|
53601
|
-
} catch {
|
|
53602
|
-
return;
|
|
53603
|
-
}
|
|
53604
|
-
}
|
|
53605
|
-
return;
|
|
53606
|
-
}
|
|
53607
53609
|
function identifyModules(files) {
|
|
53608
53610
|
const modules = new Map;
|
|
53609
53611
|
for (const file2 of files) {
|
|
@@ -53676,6 +53678,18 @@ function estimateAnalysisTimes(files, complexity) {
|
|
|
53676
53678
|
semantic: Math.round(semanticTime)
|
|
53677
53679
|
};
|
|
53678
53680
|
}
|
|
53681
|
+
async function readReadmeContent(repoPath, files) {
|
|
53682
|
+
const readmeFile = files.find((f) => f.relativePath.toLowerCase() === "readme.md" || f.relativePath.toLowerCase() === "readme");
|
|
53683
|
+
if (readmeFile && readmeFile.size < 50000) {
|
|
53684
|
+
try {
|
|
53685
|
+
const content = await readFile(join(repoPath, readmeFile.relativePath), "utf-8");
|
|
53686
|
+
return content;
|
|
53687
|
+
} catch {
|
|
53688
|
+
return;
|
|
53689
|
+
}
|
|
53690
|
+
}
|
|
53691
|
+
return;
|
|
53692
|
+
}
|
|
53679
53693
|
// src/core/layers/structural.ts
|
|
53680
53694
|
import { extname as extname2 } from "path";
|
|
53681
53695
|
var loadedLanguages = new Map;
|
|
@@ -53760,6 +53774,24 @@ async function structuralAnalysis(module, files) {
|
|
|
53760
53774
|
let totalClasses = 0;
|
|
53761
53775
|
for (const file2 of files) {
|
|
53762
53776
|
const ext2 = extname2(file2.path).toLowerCase();
|
|
53777
|
+
if (ext2 === ".md" || ext2 === ".mdx") {
|
|
53778
|
+
const mdSymbols = analyzeMarkdownFile(file2.path, file2.content);
|
|
53779
|
+
symbols.push(...mdSymbols);
|
|
53780
|
+
const lines2 = file2.content.split(`
|
|
53781
|
+
`);
|
|
53782
|
+
totalLoc += lines2.filter((l) => l.trim()).length;
|
|
53783
|
+
continue;
|
|
53784
|
+
}
|
|
53785
|
+
if (ext2 === ".sh" || ext2 === ".bash" || ext2 === ".zsh") {
|
|
53786
|
+
const shAnalysis = analyzeShellFile(file2.path, file2.content);
|
|
53787
|
+
symbols.push(...shAnalysis.symbols);
|
|
53788
|
+
imports.push(...shAnalysis.imports);
|
|
53789
|
+
const lines2 = file2.content.split(`
|
|
53790
|
+
`);
|
|
53791
|
+
totalLoc += lines2.filter((l) => l.trim() && !l.trim().startsWith("#")).length;
|
|
53792
|
+
totalFunctions += shAnalysis.symbols.filter((s) => s.type === "function").length;
|
|
53793
|
+
continue;
|
|
53794
|
+
}
|
|
53763
53795
|
const config2 = getLanguageConfig(ext2);
|
|
53764
53796
|
if (!config2) {
|
|
53765
53797
|
continue;
|
|
@@ -53904,6 +53936,83 @@ async function analyzeFileWithRegex(filePath, content, config2) {
|
|
|
53904
53936
|
}
|
|
53905
53937
|
return { symbols, imports, exports };
|
|
53906
53938
|
}
|
|
53939
|
+
function analyzeMarkdownFile(filePath, content) {
|
|
53940
|
+
const symbols = [];
|
|
53941
|
+
const lines = content.split(`
|
|
53942
|
+
`);
|
|
53943
|
+
if (lines[0]?.trim() === "---") {
|
|
53944
|
+
for (let i = 1;i < lines.length; i++) {
|
|
53945
|
+
if (lines[i].trim() === "---")
|
|
53946
|
+
break;
|
|
53947
|
+
const keyMatch = lines[i].match(/^(\w[\w-]*):\s/);
|
|
53948
|
+
if (keyMatch) {
|
|
53949
|
+
symbols.push({
|
|
53950
|
+
name: `frontmatter:${keyMatch[1]}`,
|
|
53951
|
+
type: "variable",
|
|
53952
|
+
file: filePath,
|
|
53953
|
+
line: i + 1,
|
|
53954
|
+
exported: false
|
|
53955
|
+
});
|
|
53956
|
+
}
|
|
53957
|
+
}
|
|
53958
|
+
}
|
|
53959
|
+
for (let i = 0;i < lines.length; i++) {
|
|
53960
|
+
const headingMatch = lines[i].match(/^(#{1,2})\s+(.+)/);
|
|
53961
|
+
if (headingMatch) {
|
|
53962
|
+
const level = headingMatch[1].length;
|
|
53963
|
+
symbols.push({
|
|
53964
|
+
name: headingMatch[2].trim(),
|
|
53965
|
+
type: level === 1 ? "class" : "function",
|
|
53966
|
+
file: filePath,
|
|
53967
|
+
line: i + 1,
|
|
53968
|
+
exported: false
|
|
53969
|
+
});
|
|
53970
|
+
}
|
|
53971
|
+
}
|
|
53972
|
+
return symbols;
|
|
53973
|
+
}
|
|
53974
|
+
function analyzeShellFile(filePath, content) {
|
|
53975
|
+
const symbols = [];
|
|
53976
|
+
const imports = [];
|
|
53977
|
+
const lines = content.split(`
|
|
53978
|
+
`);
|
|
53979
|
+
for (let i = 0;i < lines.length; i++) {
|
|
53980
|
+
const line = lines[i];
|
|
53981
|
+
const funcMatch = line.match(/^(?:function\s+)?(\w+)\s*\(\)\s*\{/) || line.match(/^function\s+(\w+)\s*\{/);
|
|
53982
|
+
if (funcMatch) {
|
|
53983
|
+
symbols.push({
|
|
53984
|
+
name: funcMatch[1],
|
|
53985
|
+
type: "function",
|
|
53986
|
+
file: filePath,
|
|
53987
|
+
line: i + 1,
|
|
53988
|
+
exported: false
|
|
53989
|
+
});
|
|
53990
|
+
continue;
|
|
53991
|
+
}
|
|
53992
|
+
const constMatch = line.match(/^([A-Z][A-Z0-9_]+)=/);
|
|
53993
|
+
if (constMatch) {
|
|
53994
|
+
symbols.push({
|
|
53995
|
+
name: constMatch[1],
|
|
53996
|
+
type: "constant",
|
|
53997
|
+
file: filePath,
|
|
53998
|
+
line: i + 1,
|
|
53999
|
+
exported: false
|
|
54000
|
+
});
|
|
54001
|
+
continue;
|
|
54002
|
+
}
|
|
54003
|
+
const sourceMatch = line.match(/^(?:source|\.) +["']?([^"'\s]+)["']?/);
|
|
54004
|
+
if (sourceMatch) {
|
|
54005
|
+
imports.push({
|
|
54006
|
+
from: filePath,
|
|
54007
|
+
to: sourceMatch[1],
|
|
54008
|
+
importedNames: [],
|
|
54009
|
+
isDefault: false,
|
|
54010
|
+
isType: false
|
|
54011
|
+
});
|
|
54012
|
+
}
|
|
54013
|
+
}
|
|
54014
|
+
return { symbols, imports };
|
|
54015
|
+
}
|
|
53907
54016
|
function getLineNumber(content, index) {
|
|
53908
54017
|
return content.slice(0, index).split(`
|
|
53909
54018
|
`).length;
|
|
@@ -69789,6 +69898,7 @@ class AnalysisCache {
|
|
|
69789
69898
|
return null;
|
|
69790
69899
|
}
|
|
69791
69900
|
if (Date.now() > entry.expiresAt) {
|
|
69901
|
+
entry.value.cleanup?.().catch(() => {});
|
|
69792
69902
|
this.cache.delete(key);
|
|
69793
69903
|
return null;
|
|
69794
69904
|
}
|
|
@@ -69798,6 +69908,7 @@ class AnalysisCache {
|
|
|
69798
69908
|
for (const entry of this.cache.values()) {
|
|
69799
69909
|
if (entry.value.result.analysisId === analysisId) {
|
|
69800
69910
|
if (Date.now() > entry.expiresAt) {
|
|
69911
|
+
entry.value.cleanup?.().catch(() => {});
|
|
69801
69912
|
this.cache.delete(entry.key);
|
|
69802
69913
|
return null;
|
|
69803
69914
|
}
|
|
@@ -69820,6 +69931,8 @@ class AnalysisCache {
|
|
|
69820
69931
|
}
|
|
69821
69932
|
invalidate(source, commitHash) {
|
|
69822
69933
|
const key = this.generateKey(source, commitHash);
|
|
69934
|
+
const entry = this.cache.get(key);
|
|
69935
|
+
entry?.value.cleanup?.().catch(() => {});
|
|
69823
69936
|
return this.cache.delete(key);
|
|
69824
69937
|
}
|
|
69825
69938
|
clearExpired() {
|
|
@@ -69827,6 +69940,7 @@ class AnalysisCache {
|
|
|
69827
69940
|
let cleared = 0;
|
|
69828
69941
|
for (const [key, entry] of this.cache.entries()) {
|
|
69829
69942
|
if (now > entry.expiresAt) {
|
|
69943
|
+
entry.value.cleanup?.().catch(() => {});
|
|
69830
69944
|
this.cache.delete(key);
|
|
69831
69945
|
cleared++;
|
|
69832
69946
|
}
|
|
@@ -69834,6 +69948,9 @@ class AnalysisCache {
|
|
|
69834
69948
|
return cleared;
|
|
69835
69949
|
}
|
|
69836
69950
|
clear() {
|
|
69951
|
+
for (const entry of this.cache.values()) {
|
|
69952
|
+
entry.value.cleanup?.().catch(() => {});
|
|
69953
|
+
}
|
|
69837
69954
|
this.cache.clear();
|
|
69838
69955
|
}
|
|
69839
69956
|
stats() {
|
|
@@ -69860,11 +69977,16 @@ class AnalysisCache {
|
|
|
69860
69977
|
}
|
|
69861
69978
|
}
|
|
69862
69979
|
if (oldestKey) {
|
|
69980
|
+
const entry = this.cache.get(oldestKey);
|
|
69981
|
+
entry?.value.cleanup?.().catch(() => {});
|
|
69863
69982
|
this.cache.delete(oldestKey);
|
|
69864
69983
|
}
|
|
69865
69984
|
}
|
|
69866
69985
|
}
|
|
69867
69986
|
var analysisCache = new AnalysisCache;
|
|
69987
|
+
process.on("beforeExit", () => {
|
|
69988
|
+
analysisCache.clear();
|
|
69989
|
+
});
|
|
69868
69990
|
|
|
69869
69991
|
// src/core/orchestrator.ts
|
|
69870
69992
|
init_disclosure();
|
|
@@ -70087,6 +70209,14 @@ async function orchestrateAnalysis(repoPath, options = {}) {
|
|
|
70087
70209
|
logger.orchestrator("Surface-only mode, skipping deeper analysis");
|
|
70088
70210
|
const result2 = buildAnalysisResult(analysisId, repoPath, depth, surface, [], null, Date.now() - startTime);
|
|
70089
70211
|
result2.warnings = warnings.length > 0 ? warnings : undefined;
|
|
70212
|
+
analysisCache.set(repoPath, {
|
|
70213
|
+
result: result2,
|
|
70214
|
+
surface,
|
|
70215
|
+
structural: [],
|
|
70216
|
+
semantic: null,
|
|
70217
|
+
repoPath,
|
|
70218
|
+
cleanup: options.cleanup
|
|
70219
|
+
}, undefined, depth);
|
|
70090
70220
|
return result2;
|
|
70091
70221
|
}
|
|
70092
70222
|
logger.progress("structural", "Phase 2: Starting structural analysis");
|
|
@@ -70173,7 +70303,9 @@ async function orchestrateAnalysis(repoPath, options = {}) {
|
|
|
70173
70303
|
result,
|
|
70174
70304
|
surface,
|
|
70175
70305
|
structural,
|
|
70176
|
-
semantic
|
|
70306
|
+
semantic,
|
|
70307
|
+
repoPath,
|
|
70308
|
+
cleanup: options.cleanup
|
|
70177
70309
|
}, undefined, depth);
|
|
70178
70310
|
state.phase = "complete";
|
|
70179
70311
|
logger.orchestrator(`Analysis complete`, {
|
|
@@ -70327,19 +70459,14 @@ function getCapabilities() {
|
|
|
70327
70459
|
parameters: ["source", "from", "to"]
|
|
70328
70460
|
},
|
|
70329
70461
|
{
|
|
70330
|
-
name: "
|
|
70331
|
-
description: "
|
|
70332
|
-
parameters: ["
|
|
70462
|
+
name: "read_files",
|
|
70463
|
+
description: "Read specific files from a previously analyzed repository",
|
|
70464
|
+
parameters: ["analysisId", "paths", "maxLines"]
|
|
70333
70465
|
},
|
|
70334
70466
|
{
|
|
70335
70467
|
name: "query_repo",
|
|
70336
|
-
description: "Ask
|
|
70468
|
+
description: "Ask a question about a codebase and get an AI-powered answer with relevant files",
|
|
70337
70469
|
parameters: ["source", "question"]
|
|
70338
|
-
},
|
|
70339
|
-
{
|
|
70340
|
-
name: "compare_repos",
|
|
70341
|
-
description: "Compare how repositories approach the same problem",
|
|
70342
|
-
parameters: ["sources", "aspect"]
|
|
70343
70470
|
}
|
|
70344
70471
|
],
|
|
70345
70472
|
models: {
|
|
@@ -70452,13 +70579,15 @@ async function executeAnalyzeRepo(input) {
|
|
|
70452
70579
|
exclude,
|
|
70453
70580
|
tokenBudget,
|
|
70454
70581
|
includeSemantics,
|
|
70455
|
-
sourceName
|
|
70582
|
+
sourceName,
|
|
70583
|
+
cleanup
|
|
70456
70584
|
});
|
|
70457
70585
|
return result;
|
|
70458
|
-
}
|
|
70586
|
+
} catch (error48) {
|
|
70459
70587
|
if (cleanup) {
|
|
70460
70588
|
await cleanup();
|
|
70461
70589
|
}
|
|
70590
|
+
throw error48;
|
|
70462
70591
|
}
|
|
70463
70592
|
}
|
|
70464
70593
|
// src/mcp/tools/expand.ts
|
|
@@ -70682,13 +70811,263 @@ If you cannot find the entry point, explain what you looked for in the summary a
|
|
|
70682
70811
|
}
|
|
70683
70812
|
}
|
|
70684
70813
|
}
|
|
70814
|
+
// src/mcp/tools/read-files.ts
|
|
70815
|
+
import { readFile as readFile6, stat as stat5 } from "fs/promises";
|
|
70816
|
+
import { join as join6, resolve, normalize as normalize2 } from "path";
|
|
70817
|
+
var readFilesSchema = {
|
|
70818
|
+
analysisId: exports_external.string().describe("The analysisId from a previous analyze_repo result"),
|
|
70819
|
+
paths: exports_external.array(exports_external.string()).min(1).max(20).describe("Relative file paths from the repository (max 20)"),
|
|
70820
|
+
maxLines: exports_external.number().min(1).max(2000).default(500).optional().describe("Maximum lines per file (default 500, max 2000)")
|
|
70821
|
+
};
|
|
70822
|
+
async function executeReadFiles(input) {
|
|
70823
|
+
const { analysisId, paths, maxLines = 500 } = input;
|
|
70824
|
+
const cached2 = analysisCache.getByAnalysisId(analysisId);
|
|
70825
|
+
if (!cached2) {
|
|
70826
|
+
return {
|
|
70827
|
+
error: `Analysis ${analysisId} not found in cache. It may have expired. Run analyze_repo again.`
|
|
70828
|
+
};
|
|
70829
|
+
}
|
|
70830
|
+
const repoPath = cached2.repoPath;
|
|
70831
|
+
if (!repoPath) {
|
|
70832
|
+
return {
|
|
70833
|
+
error: `No repository path stored for analysis ${analysisId}. This analysis predates the read_files feature.`
|
|
70834
|
+
};
|
|
70835
|
+
}
|
|
70836
|
+
try {
|
|
70837
|
+
await stat5(repoPath);
|
|
70838
|
+
} catch {
|
|
70839
|
+
return {
|
|
70840
|
+
error: `Repository at ${repoPath} is no longer available. Run analyze_repo again.`
|
|
70841
|
+
};
|
|
70842
|
+
}
|
|
70843
|
+
const resolvedRepoPath = resolve(repoPath);
|
|
70844
|
+
const effectiveMaxLines = Math.min(maxLines, 2000);
|
|
70845
|
+
const files = await Promise.all(paths.slice(0, 20).map(async (filePath) => {
|
|
70846
|
+
const normalized = normalize2(filePath);
|
|
70847
|
+
if (normalized.startsWith("..") || normalized.startsWith("/")) {
|
|
70848
|
+
return { path: filePath, error: "Invalid path: must be relative and within the repository" };
|
|
70849
|
+
}
|
|
70850
|
+
const fullPath = resolve(join6(resolvedRepoPath, normalized));
|
|
70851
|
+
if (!fullPath.startsWith(resolvedRepoPath)) {
|
|
70852
|
+
return { path: filePath, error: "Invalid path: traversal outside repository" };
|
|
70853
|
+
}
|
|
70854
|
+
try {
|
|
70855
|
+
const content = await readFile6(fullPath, "utf-8");
|
|
70856
|
+
const lines = content.split(`
|
|
70857
|
+
`);
|
|
70858
|
+
const truncated = lines.length > effectiveMaxLines;
|
|
70859
|
+
const outputContent = truncated ? lines.slice(0, effectiveMaxLines).join(`
|
|
70860
|
+
`) : content;
|
|
70861
|
+
return {
|
|
70862
|
+
path: filePath,
|
|
70863
|
+
content: outputContent,
|
|
70864
|
+
lineCount: lines.length,
|
|
70865
|
+
truncated
|
|
70866
|
+
};
|
|
70867
|
+
} catch {
|
|
70868
|
+
return { path: filePath, error: "File not found or not readable" };
|
|
70869
|
+
}
|
|
70870
|
+
}));
|
|
70871
|
+
return {
|
|
70872
|
+
analysisId,
|
|
70873
|
+
files
|
|
70874
|
+
};
|
|
70875
|
+
}
|
|
70876
|
+
// src/mcp/tools/query.ts
|
|
70877
|
+
import { basename as basename5, join as join7 } from "path";
|
|
70878
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
70879
|
+
var queryRepoSchema = {
|
|
70880
|
+
source: exports_external.string().describe("Local path or GitHub URL to the repository"),
|
|
70881
|
+
question: exports_external.string().describe("Question about the codebase (e.g. 'how is authentication handled?')")
|
|
70882
|
+
};
|
|
70883
|
+
function extractSourceName2(source) {
|
|
70884
|
+
const githubMatch = source.match(/github\.com\/([^\/]+\/[^\/]+)/);
|
|
70885
|
+
if (githubMatch) {
|
|
70886
|
+
return githubMatch[1].replace(/\.git$/, "");
|
|
70887
|
+
}
|
|
70888
|
+
return basename5(source) || source;
|
|
70889
|
+
}
|
|
70890
|
+
function scoreFileRelevance(filePath, symbols, question) {
|
|
70891
|
+
const q = question.toLowerCase();
|
|
70892
|
+
const words = q.split(/\s+/).filter((w) => w.length > 2);
|
|
70893
|
+
let score = 0;
|
|
70894
|
+
const pathLower = filePath.toLowerCase();
|
|
70895
|
+
for (const word of words) {
|
|
70896
|
+
if (pathLower.includes(word))
|
|
70897
|
+
score += 3;
|
|
70898
|
+
}
|
|
70899
|
+
for (const sym of symbols) {
|
|
70900
|
+
const symLower = sym.toLowerCase();
|
|
70901
|
+
for (const word of words) {
|
|
70902
|
+
if (symLower.includes(word))
|
|
70903
|
+
score += 2;
|
|
70904
|
+
}
|
|
70905
|
+
}
|
|
70906
|
+
return score;
|
|
70907
|
+
}
|
|
70908
|
+
async function executeQueryRepo(input) {
|
|
70909
|
+
const { source, question } = input;
|
|
70910
|
+
const sourceName = extractSourceName2(source);
|
|
70911
|
+
const { repoPath, cleanup } = await resolveSource(source);
|
|
70912
|
+
try {
|
|
70913
|
+
let cached2 = analysisCache.get(repoPath);
|
|
70914
|
+
let analysisId;
|
|
70915
|
+
if (cached2) {
|
|
70916
|
+
analysisId = cached2.result.analysisId;
|
|
70917
|
+
} else {
|
|
70918
|
+
const result = await orchestrateAnalysis(repoPath, {
|
|
70919
|
+
depth: "standard",
|
|
70920
|
+
sourceName,
|
|
70921
|
+
cleanup
|
|
70922
|
+
});
|
|
70923
|
+
analysisId = result.analysisId;
|
|
70924
|
+
cached2 = analysisCache.get(repoPath);
|
|
70925
|
+
if (!cached2) {
|
|
70926
|
+
throw new Error("Analysis completed but cache lookup failed");
|
|
70927
|
+
}
|
|
70928
|
+
}
|
|
70929
|
+
const fileSymbols = new Map;
|
|
70930
|
+
for (const mod of cached2.structural) {
|
|
70931
|
+
for (const sym of mod.symbols) {
|
|
70932
|
+
if (sym.file && sym.name) {
|
|
70933
|
+
const existing = fileSymbols.get(sym.file) || [];
|
|
70934
|
+
existing.push(sym.name);
|
|
70935
|
+
fileSymbols.set(sym.file, existing);
|
|
70936
|
+
}
|
|
70937
|
+
}
|
|
70938
|
+
if (mod.exports.length > 0) {
|
|
70939
|
+
const existing = fileSymbols.get(mod.modulePath) || [];
|
|
70940
|
+
existing.push(...mod.exports);
|
|
70941
|
+
fileSymbols.set(mod.modulePath, existing);
|
|
70942
|
+
}
|
|
70943
|
+
}
|
|
70944
|
+
const collectFiles = (node, prefix) => {
|
|
70945
|
+
const path3 = prefix ? `${prefix}/${node.name}` : node.name;
|
|
70946
|
+
if (node.type === "file") {
|
|
70947
|
+
if (!fileSymbols.has(path3)) {
|
|
70948
|
+
fileSymbols.set(path3, []);
|
|
70949
|
+
}
|
|
70950
|
+
} else if (node.children) {
|
|
70951
|
+
for (const child of node.children) {
|
|
70952
|
+
collectFiles(child, path3);
|
|
70953
|
+
}
|
|
70954
|
+
}
|
|
70955
|
+
};
|
|
70956
|
+
if (cached2.surface.repositoryMap.structure?.children) {
|
|
70957
|
+
for (const child of cached2.surface.repositoryMap.structure.children) {
|
|
70958
|
+
collectFiles(child, "");
|
|
70959
|
+
}
|
|
70960
|
+
}
|
|
70961
|
+
const scored = Array.from(fileSymbols.entries()).map(([path3, symbols]) => ({
|
|
70962
|
+
path: path3,
|
|
70963
|
+
symbols,
|
|
70964
|
+
score: scoreFileRelevance(path3, symbols, question)
|
|
70965
|
+
})).filter((f3) => f3.score > 0).sort((a, b) => b.score - a.score).slice(0, 15);
|
|
70966
|
+
const filesToRead = scored.length > 0 ? scored.map((f3) => f3.path) : (cached2.surface.repositoryMap.entryPoints || []).slice(0, 10);
|
|
70967
|
+
const fileContents = new Map;
|
|
70968
|
+
let totalChars = 0;
|
|
70969
|
+
const MAX_TOTAL_CHARS = 1e5;
|
|
70970
|
+
const MAX_PER_FILE = 4000;
|
|
70971
|
+
for (const filePath of filesToRead) {
|
|
70972
|
+
if (totalChars >= MAX_TOTAL_CHARS)
|
|
70973
|
+
break;
|
|
70974
|
+
const fullPath = join7(repoPath, filePath);
|
|
70975
|
+
try {
|
|
70976
|
+
const content = await readFile7(fullPath, "utf-8");
|
|
70977
|
+
const truncated = content.length > MAX_PER_FILE ? content.slice(0, MAX_PER_FILE) + `
|
|
70978
|
+
... [truncated]` : content;
|
|
70979
|
+
fileContents.set(filePath, truncated);
|
|
70980
|
+
totalChars += truncated.length;
|
|
70981
|
+
} catch {}
|
|
70982
|
+
}
|
|
70983
|
+
try {
|
|
70984
|
+
return await queryWithGemini(question, analysisId, cached2, fileContents);
|
|
70985
|
+
} catch {
|
|
70986
|
+
return buildFallbackAnswer(question, analysisId, cached2, scored, fileContents);
|
|
70987
|
+
}
|
|
70988
|
+
} catch (error48) {
|
|
70989
|
+
if (cleanup) {
|
|
70990
|
+
await cleanup();
|
|
70991
|
+
}
|
|
70992
|
+
throw error48;
|
|
70993
|
+
}
|
|
70994
|
+
}
|
|
70995
|
+
async function queryWithGemini(question, analysisId, cached2, fileContents) {
|
|
70996
|
+
const surface = cached2.surface;
|
|
70997
|
+
const fileSummary = Array.from(fileContents.entries()).map(([path3, content]) => `--- ${path3} ---
|
|
70998
|
+
${content}`).join(`
|
|
70999
|
+
|
|
71000
|
+
`);
|
|
71001
|
+
const structuralSummary = cached2.structural.map((mod) => {
|
|
71002
|
+
const exports = mod.exports.slice(0, 10).join(", ");
|
|
71003
|
+
const funcs = mod.complexity.functionCount;
|
|
71004
|
+
const classes = mod.complexity.classCount;
|
|
71005
|
+
return `- ${mod.modulePath}: ${funcs} functions, ${classes} classes. Exports: ${exports || "none"}`;
|
|
71006
|
+
}).join(`
|
|
71007
|
+
`);
|
|
71008
|
+
const prompt = `Answer this question about a codebase:
|
|
71009
|
+
|
|
71010
|
+
QUESTION: ${question}
|
|
71011
|
+
|
|
71012
|
+
Repository: ${surface.repositoryMap.name}
|
|
71013
|
+
Languages: ${surface.repositoryMap.languages.map((l) => l.language).join(", ")}
|
|
71014
|
+
Entry points: ${surface.repositoryMap.entryPoints.slice(0, 10).join(", ")}
|
|
71015
|
+
Modules: ${surface.identifiedModules.map((m2) => m2.name).join(", ")}
|
|
71016
|
+
|
|
71017
|
+
Structural overview:
|
|
71018
|
+
${structuralSummary}
|
|
71019
|
+
|
|
71020
|
+
Relevant file contents:
|
|
71021
|
+
${fileSummary}
|
|
71022
|
+
|
|
71023
|
+
Respond with this exact JSON structure:
|
|
71024
|
+
{
|
|
71025
|
+
"answer": "Clear, detailed answer to the question based on the code",
|
|
71026
|
+
"relevantFiles": [
|
|
71027
|
+
{"path": "relative/path.ts", "reason": "Why this file is relevant"}
|
|
71028
|
+
],
|
|
71029
|
+
"confidence": "high" | "medium" | "low",
|
|
71030
|
+
"suggestedFollowUps": ["Follow-up question 1", "Follow-up question 2"]
|
|
71031
|
+
}
|
|
71032
|
+
|
|
71033
|
+
Guidelines:
|
|
71034
|
+
- Reference specific files and code when possible
|
|
71035
|
+
- If the code doesn't clearly answer the question, say so and set confidence to "low"
|
|
71036
|
+
- Suggest 2-3 follow-up questions that would help understand more
|
|
71037
|
+
- Keep relevantFiles to the most important 5-8 files`;
|
|
71038
|
+
const result = await generateJsonWithGemini(prompt, {
|
|
71039
|
+
maxOutputTokens: 4096
|
|
71040
|
+
});
|
|
71041
|
+
return { ...result, analysisId };
|
|
71042
|
+
}
|
|
71043
|
+
function buildFallbackAnswer(question, analysisId, cached2, scored, fileContents) {
|
|
71044
|
+
const surface = cached2.surface;
|
|
71045
|
+
const topFiles = scored.slice(0, 8);
|
|
71046
|
+
const relevantFiles = topFiles.map((f3) => ({
|
|
71047
|
+
path: f3.path,
|
|
71048
|
+
reason: f3.symbols.length > 0 ? `Contains relevant symbols: ${f3.symbols.slice(0, 5).join(", ")}` : `File path matches question keywords`
|
|
71049
|
+
}));
|
|
71050
|
+
const answer = topFiles.length > 0 ? `Based on keyword matching against the codebase structure, the most relevant files for "${question}" are listed below. ` + `The repository is a ${surface.repositoryMap.languages[0]?.language || "unknown"} project with ${surface.repositoryMap.fileCount} files. ` + `For a more detailed answer, ensure GEMINI_API_KEY is set. ` + `Use read_files with analysisId "${analysisId}" to examine the relevant files.` : `Could not find files matching "${question}" through keyword search. ` + `The repository contains ${surface.repositoryMap.fileCount} files primarily in ${surface.repositoryMap.languages[0]?.language || "unknown"}. ` + `Try rephrasing the question or use read_files with analysisId "${analysisId}" to explore specific files. ` + `For AI-powered answers, set GEMINI_API_KEY.`;
|
|
71051
|
+
return {
|
|
71052
|
+
answer,
|
|
71053
|
+
relevantFiles,
|
|
71054
|
+
confidence: topFiles.length > 3 ? "medium" : "low",
|
|
71055
|
+
analysisId,
|
|
71056
|
+
suggestedFollowUps: [
|
|
71057
|
+
`Use read_files to examine: ${topFiles.slice(0, 3).map((f3) => f3.path).join(", ")}`,
|
|
71058
|
+
`Use expand_section to drill into specific modules`,
|
|
71059
|
+
`Use trace_dataflow to follow data through the system`
|
|
71060
|
+
]
|
|
71061
|
+
};
|
|
71062
|
+
}
|
|
70685
71063
|
// package.json
|
|
70686
71064
|
var package_default = {
|
|
70687
71065
|
name: "codebase-analyzer-mcp",
|
|
70688
|
-
version: "2.0
|
|
71066
|
+
version: "2.1.0",
|
|
70689
71067
|
description: "Multi-layer codebase analysis with Gemini AI. MCP server + Claude plugin with progressive disclosure.",
|
|
70690
71068
|
type: "module",
|
|
70691
71069
|
main: "dist/mcp/server.js",
|
|
71070
|
+
packageManager: "bun@1.3.8",
|
|
70692
71071
|
bin: {
|
|
70693
71072
|
cba: "dist/cli/index.js",
|
|
70694
71073
|
"codebase-analyzer": "dist/cli/index.js"
|
|
@@ -70704,19 +71083,15 @@ var package_default = {
|
|
|
70704
71083
|
"AGENTS.md"
|
|
70705
71084
|
],
|
|
70706
71085
|
scripts: {
|
|
70707
|
-
build: "bun
|
|
70708
|
-
"build:js": `bun build src/mcp/server.ts --outfile dist/mcp/server.js --target node && bun build src/cli/index.ts --outfile dist/cli/index.js --target node && node -e "const fs=require('fs');const c=fs.readFileSync('dist/cli/index.js','utf8');fs.writeFileSync('dist/cli/index.js','#!/usr/bin/env node\\n'+c)"`,
|
|
71086
|
+
build: "bun scripts/build.ts",
|
|
70709
71087
|
dev: "bun --watch src/cli/index.ts",
|
|
70710
71088
|
start: "bun dist/mcp/server.js",
|
|
70711
71089
|
typecheck: "tsc --noEmit",
|
|
70712
71090
|
test: "bun test",
|
|
70713
71091
|
cli: "bun src/cli/index.ts",
|
|
70714
|
-
|
|
70715
|
-
|
|
70716
|
-
|
|
70717
|
-
"release:minor": "npm version minor && bun run version:sync",
|
|
70718
|
-
"release:major": "npm version major && bun run version:sync",
|
|
70719
|
-
prepublishOnly: "bun run version:sync && bun run build:js"
|
|
71092
|
+
version: "bun scripts/sync-version.ts && git add .",
|
|
71093
|
+
postversion: "git push --follow-tags",
|
|
71094
|
+
prepublishOnly: "bun run build"
|
|
70720
71095
|
},
|
|
70721
71096
|
repository: {
|
|
70722
71097
|
type: "git",
|
|
@@ -70919,6 +71294,68 @@ server.tool("trace_dataflow", "Trace data flow through the codebase from an entr
|
|
|
70919
71294
|
};
|
|
70920
71295
|
}
|
|
70921
71296
|
});
|
|
71297
|
+
server.tool("read_files", "Read specific files from a previously analyzed repository. Use the analysisId from analyze_repo to access files without re-cloning.", {
|
|
71298
|
+
analysisId: readFilesSchema.analysisId,
|
|
71299
|
+
paths: readFilesSchema.paths,
|
|
71300
|
+
maxLines: readFilesSchema.maxLines
|
|
71301
|
+
}, async ({ analysisId, paths, maxLines }) => {
|
|
71302
|
+
try {
|
|
71303
|
+
const result = await executeReadFiles({
|
|
71304
|
+
analysisId,
|
|
71305
|
+
paths,
|
|
71306
|
+
maxLines
|
|
71307
|
+
});
|
|
71308
|
+
return {
|
|
71309
|
+
content: [
|
|
71310
|
+
{
|
|
71311
|
+
type: "text",
|
|
71312
|
+
text: JSON.stringify(result, null, 2)
|
|
71313
|
+
}
|
|
71314
|
+
]
|
|
71315
|
+
};
|
|
71316
|
+
} catch (error48) {
|
|
71317
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
71318
|
+
return {
|
|
71319
|
+
content: [
|
|
71320
|
+
{
|
|
71321
|
+
type: "text",
|
|
71322
|
+
text: `Error reading files: ${message}`
|
|
71323
|
+
}
|
|
71324
|
+
],
|
|
71325
|
+
isError: true
|
|
71326
|
+
};
|
|
71327
|
+
}
|
|
71328
|
+
});
|
|
71329
|
+
server.tool("query_repo", "Ask a question about a codebase and get an AI-powered answer with relevant file references. Uses cached analysis when available. Works best with GEMINI_API_KEY set, falls back to keyword matching without it.", {
|
|
71330
|
+
source: queryRepoSchema.source,
|
|
71331
|
+
question: queryRepoSchema.question
|
|
71332
|
+
}, async ({ source, question }) => {
|
|
71333
|
+
try {
|
|
71334
|
+
const result = await executeQueryRepo({
|
|
71335
|
+
source,
|
|
71336
|
+
question
|
|
71337
|
+
});
|
|
71338
|
+
return {
|
|
71339
|
+
content: [
|
|
71340
|
+
{
|
|
71341
|
+
type: "text",
|
|
71342
|
+
text: JSON.stringify(result, null, 2)
|
|
71343
|
+
}
|
|
71344
|
+
]
|
|
71345
|
+
};
|
|
71346
|
+
} catch (error48) {
|
|
71347
|
+
const message = error48 instanceof Error ? error48.message : String(error48);
|
|
71348
|
+
return {
|
|
71349
|
+
content: [
|
|
71350
|
+
{
|
|
71351
|
+
type: "text",
|
|
71352
|
+
text: `Error querying repository: ${message}`
|
|
71353
|
+
}
|
|
71354
|
+
],
|
|
71355
|
+
isError: true
|
|
71356
|
+
};
|
|
71357
|
+
}
|
|
71358
|
+
});
|
|
70922
71359
|
async function main() {
|
|
70923
71360
|
const transport = new StdioServerTransport;
|
|
70924
71361
|
await server.connect(transport);
|