import { Octokit } from "@octokit/rest";
interface RepositoryStructureArgs {
owner: string;
repo: string;
path?: string;
branch?: string;
max_depth?: number;
}
interface FileTreeItem {
name: string;
path: string;
type: 'file' | 'dir';
size?: number;
download_url?: string;
}
export const analyzeRepositoryStructure = {
name: "analyze_repository_structure",
description: "Analyze the structure and architecture of a GitHub repository. This tool provides a comprehensive overview of the repository's file and folder organization, helping to understand the project layout and architecture.",
parameters: {
type: "object",
properties: {
owner: {
type: "string",
description: "The GitHub username or organization name that owns the repository"
},
repo: {
type: "string",
description: "The name of the GitHub repository"
},
path: {
type: "string",
description: "Optional: Specific path within the repository to analyze (default: root directory)",
default: ""
},
branch: {
type: "string",
description: "Optional: Branch name to analyze (default: main/master branch)",
default: "main"
},
max_depth: {
type: "number",
description: "Optional: Maximum depth to traverse the directory structure (default: 3, max: 5)",
default: 3,
minimum: 1,
maximum: 5
}
},
required: ["owner", "repo"]
},
async run(args: RepositoryStructureArgs) {
try {
// Parameter validation
if (!args.owner || !args.repo) {
throw new Error("Both 'owner' and 'repo' parameters are required");
}
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
const maxDepth = Math.min(args.max_depth || 3, 5);
const branch = args.branch || "main";
const basePath = args.path || "";
// Get repository information
const repoInfo = await octokit.rest.repos.get({
owner: args.owner,
repo: args.repo
});
// Get repository contents recursively
const structure = await getRepositoryStructure(
octokit,
args.owner,
args.repo,
basePath,
branch,
maxDepth
);
// Generate analysis report
const analysis = generateStructureAnalysis(structure, repoInfo.data);
return {
content: [{
type: "text",
text: analysis
}],
isError: false
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
return {
content: [{
type: "text",
text: `β **Error analyzing repository structure**\n\n${errorMessage}\n\n**Common issues:**\n- Repository not found or private\n- Invalid branch name\n- Rate limit exceeded (consider adding GITHUB_TOKEN)\n- Network connectivity issues`
}],
isError: true
};
}
}
};
async function getRepositoryStructure(
octokit: Octokit,
owner: string,
repo: string,
path: string,
branch: string,
maxDepth: number,
currentDepth: number = 0
): Promise<FileTreeItem[]> {
if (currentDepth >= maxDepth) {
return [];
}
try {
const response = await octokit.rest.repos.getContent({
owner,
repo,
path,
ref: branch
});
const contents = Array.isArray(response.data) ? response.data : [response.data];
const structure: FileTreeItem[] = [];
for (const item of contents) {
if ('type' in item) {
const fileItem: FileTreeItem = {
name: item.name,
path: item.path,
type: item.type === 'dir' ? 'dir' : 'file',
size: item.size,
download_url: item.download_url || undefined
};
structure.push(fileItem);
// Recursively get subdirectory contents
if (item.type === 'dir' && currentDepth < maxDepth - 1) {
const subStructure = await getRepositoryStructure(
octokit,
owner,
repo,
item.path,
branch,
maxDepth,
currentDepth + 1
);
structure.push(...subStructure);
}
}
}
return structure;
} catch (error) {
console.error(`Error fetching contents for path: ${path}`, error);
return [];
}
}
function generateStructureAnalysis(structure: FileTreeItem[], repoData: any): string {
const files = structure.filter(item => item.type === 'file');
const directories = structure.filter(item => item.type === 'dir');
// Analyze file types
const fileExtensions = new Map<string, number>();
const importantFiles: string[] = [];
files.forEach(file => {
const ext = file.name.split('.').pop()?.toLowerCase() || 'no-extension';
fileExtensions.set(ext, (fileExtensions.get(ext) || 0) + 1);
// Identify important files
const importantPatterns = [
'readme', 'license', 'package.json', 'requirements.txt', 'dockerfile',
'makefile', 'cmake', 'cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
'.gitignore', '.env', 'config', 'settings'
];
if (importantPatterns.some(pattern =>
file.name.toLowerCase().includes(pattern.toLowerCase())
)) {
importantFiles.push(file.name);
}
});
// Generate tree structure
const treeStructure = generateTreeView(structure);
// Calculate total size
const totalSize = files.reduce((sum, file) => sum + (file.size || 0), 0);
const formattedSize = formatBytes(totalSize);
return `# π Repository Structure Analysis
## π Repository Information
- **Repository:** ${repoData.full_name}
- **Description:** ${repoData.description || 'No description available'}
- **Language:** ${repoData.language || 'Not specified'}
- **Stars:** ${repoData.stargazers_count} β
- **Forks:** ${repoData.forks_count} π΄
- **Last Updated:** ${new Date(repoData.updated_at).toLocaleDateString()}
## π Directory Structure
\`\`\`
${treeStructure}
\`\`\`
## π Statistics
- **Total Files:** ${files.length}
- **Total Directories:** ${directories.length}
- **Total Size:** ${formattedSize}
## π File Type Distribution
${Array.from(fileExtensions.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ext, count]) => `- **${ext}:** ${count} files`)
.join('\n')}
## π Important Files Detected
${importantFiles.length > 0
? importantFiles.map(file => `- \`${file}\``).join('\n')
: 'No important configuration files detected in the analyzed depth.'}
## ποΈ Project Architecture Insights
${generateArchitectureInsights(structure, fileExtensions)}
---
*Analysis completed at ${new Date().toLocaleString()}*`;
}
function generateTreeView(structure: FileTreeItem[]): string {
const pathMap = new Map<string, FileTreeItem>();
structure.forEach(item => pathMap.set(item.path, item));
const tree: string[] = [];
const processed = new Set<string>();
// Sort by path depth and name
const sortedItems = structure.sort((a, b) => {
const depthA = a.path.split('/').length;
const depthB = b.path.split('/').length;
if (depthA !== depthB) return depthA - depthB;
return a.path.localeCompare(b.path);
});
sortedItems.forEach(item => {
if (processed.has(item.path)) return;
const depth = item.path.split('/').length - 1;
const indent = ' '.repeat(depth);
const icon = item.type === 'dir' ? 'π' : 'π';
const sizeInfo = item.type === 'file' && item.size ? ` (${formatBytes(item.size)})` : '';
tree.push(`${indent}${icon} ${item.name}${sizeInfo}`);
processed.add(item.path);
});
return tree.slice(0, 50).join('\n') + (tree.length > 50 ? '\n... (truncated)' : '');
}
function generateArchitectureInsights(structure: FileTreeItem[], fileExtensions: Map<string, number>): string {
const insights: string[] = [];
// Detect project type
if (fileExtensions.has('js') || fileExtensions.has('ts') || fileExtensions.has('json')) {
insights.push('π¨ **JavaScript/TypeScript Project** - Modern web development stack detected');
}
if (fileExtensions.has('py')) {
insights.push('π **Python Project** - Python-based application or library');
}
if (fileExtensions.has('java')) {
insights.push('β **Java Project** - Enterprise or Android development');
}
if (fileExtensions.has('go')) {
insights.push('πΉ **Go Project** - High-performance backend service');
}
if (fileExtensions.has('rs')) {
insights.push('π¦ **Rust Project** - Systems programming with memory safety');
}
// Detect frameworks and tools
const hasPackageJson = structure.some(item => item.name === 'package.json');
const hasDockerfile = structure.some(item => item.name.toLowerCase().includes('dockerfile'));
const hasReadme = structure.some(item => item.name.toLowerCase().includes('readme'));
if (hasPackageJson) insights.push('π¦ **Node.js Ecosystem** - Uses npm/yarn package management');
if (hasDockerfile) insights.push('π³ **Containerized** - Docker deployment ready');
if (hasReadme) insights.push('π **Well Documented** - README file present');
return insights.length > 0 ? insights.join('\n') : 'No specific architecture patterns detected in the analyzed structure.';
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}