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.
@@ -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
- section.detail = {
31301
- exports: structuralData.exports,
31302
- complexity: structuralData.complexity,
31303
- symbolCount: structuralData.symbols.length,
31304
- importCount: structuralData.imports.length
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: "extract_feature",
70331
- description: "Analyze how a specific feature is implemented",
70332
- parameters: ["source", "feature"]
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 questions about the codebase",
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
- } finally {
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.4",
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 run build:js",
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
- cba: "bun src/cli/index.ts",
70715
- "version:sync": "bun scripts/sync-version.ts",
70716
- release: "npm version patch && bun run version:sync",
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);