UNPKG

claude-code-graph

Version:

Claude Code with live structural graphs for large codebases

507 lines (437 loc) 13.9 kB
/** * Cluster Tools - Claude-facing interface for compressed graph exploration * Provides ClusterList, ClusterExpand, and FileGet tools */ import { readFile } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; export class ClusterTools { constructor(rootPath) { this.rootPath = rootPath; this.graphDir = join(rootPath, '.graph'); this.clustersDir = join(this.graphDir, 'clusters'); this.cache = new Map(); } /** * Initialize cluster tools */ async initialize() { try { // Check if super-graph exists const superGraphPath = join(this.graphDir, 'supergraph.json'); if (!existsSync(superGraphPath)) { console.warn('Super-graph not found, cluster tools will be limited'); return false; } return true; } catch (error) { console.warn('ClusterTools initialization failed:', error.message); return false; } } /** * ClusterList - Get overview of all clusters (startup tool) * Returns: Compressed view of entire codebase in ~1k tokens */ async clusterList(options = {}) { const { includeEdges = true, includeMetrics = true, maxClusters = 50 } = options; try { const superGraph = await this.loadSuperGraph(); if (!superGraph) { return { success: false, error: "No super-graph available. Run 'ccg build' first.", clusters: [] }; } // Sort clusters by importance (size and connectivity) const clusterList = Object.values(superGraph.clusters) .map(cluster => this.enrichClusterData(cluster, superGraph)) .sort((a, b) => b.importance - a.importance) .slice(0, maxClusters) .map(cluster => ({ id: cluster.id, summary: cluster.description, files: cluster.files, size: cluster.size, languages: cluster.languages, keyFiles: cluster.keyFiles.map(f => this.shortenPath(f)), connections: cluster.connections, importance: Math.round(cluster.importance) })); const result = { success: true, clusters: clusterList, total: Object.keys(superGraph.clusters).length, ...(includeEdges && { majorConnections: this.getMajorConnections(superGraph, 5) }), ...(includeMetrics && { metrics: this.getMetrics(superGraph) }) }; return result; } catch (error) { return { success: false, error: `Failed to load cluster list: ${error.message}`, clusters: [] }; } } /** * ClusterExpand - Get detailed view of specific cluster * Returns: Files and internal structure of one cluster */ async clusterExpand(clusterId, options = {}) { const { depth = 1, includeFileDetails = false, maxFiles = 50 } = options; try { const clusterData = await this.loadCluster(clusterId); if (!clusterData) { return { success: false, error: `Cluster '${clusterId}' not found` }; } const superGraph = await this.loadSuperGraph(); const clusterInfo = superGraph?.clusters[clusterId]; // Get file list with optional details let files = []; for (const file of clusterData.files.slice(0, maxFiles)) { const fileInfo = { path: file, shortPath: this.shortenPath(file), type: this.getFileType(file) }; if (includeFileDetails) { const basics = await this.getFileBasics(file); Object.assign(fileInfo, basics); } files.push(fileInfo); } // Get connected clusters const connections = this.getClusterConnections(clusterId, superGraph); return { success: true, cluster: { id: clusterId, description: clusterInfo?.description || 'No description', totalFiles: clusterData.files.length, showing: Math.min(maxFiles, clusterData.files.length), languages: clusterInfo?.languages || [], keyFiles: clusterInfo?.keyFiles?.map(f => this.shortenPath(f)) || [] }, files, connections, hasMore: clusterData.files.length > maxFiles }; } catch (error) { return { success: false, error: `Failed to expand cluster: ${error.message}` }; } } /** * FileGet - Get detailed info about specific file * Returns: File-level details for precise reasoning */ async fileGet(filePath, options = {}) { const { includeContent = false, includeSymbols = true, includeDependencies = true } = options; try { const fullPath = join(this.rootPath, filePath); if (!existsSync(fullPath)) { return { success: false, error: `File not found: ${filePath}` }; } const fileInfo = { path: filePath, shortPath: this.shortenPath(filePath), type: this.getFileType(filePath), size: await this.getFileSize(fullPath), language: this.getLanguage(filePath) }; // Add content if requested (be careful with large files) if (includeContent) { const content = await readFile(fullPath, 'utf8'); if (content.length > 10000) { fileInfo.contentPreview = content.slice(0, 1000) + '\n... (truncated)'; fileInfo.fullSize = content.length; } else { fileInfo.content = content; } } // Extract symbols and structure if (includeSymbols) { fileInfo.symbols = await this.extractFileSymbols(fullPath, fileInfo.language); } // Get dependencies if (includeDependencies) { fileInfo.dependencies = await this.getFileDependencies(filePath); } return { success: true, file: fileInfo }; } catch (error) { return { success: false, error: `Failed to get file info: ${error.message}` }; } } /** * ClusterSearch - Find clusters matching query */ async clusterSearch(query, options = {}) { const { maxResults = 10 } = options; try { const superGraph = await this.loadSuperGraph(); if (!superGraph) return { success: false, results: [] }; const queryLower = query.toLowerCase(); const results = []; Object.values(superGraph.clusters).forEach(cluster => { let score = 0; // Check description if (cluster.description.toLowerCase().includes(queryLower)) score += 10; // Check languages cluster.languages?.forEach(lang => { if (lang.toLowerCase().includes(queryLower)) score += 5; }); // Check key files cluster.keyFiles?.forEach(file => { if (file.toLowerCase().includes(queryLower)) score += 3; }); if (score > 0) { results.push({ cluster: { id: cluster.id, description: cluster.description, files: cluster.files, languages: cluster.languages }, score }); } }); return { success: true, results: results .sort((a, b) => b.score - a.score) .slice(0, maxResults) }; } catch (error) { return { success: false, error: error.message, results: [] }; } } // Helper methods async loadSuperGraph() { const cacheKey = 'supergraph'; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } try { const content = await readFile(join(this.graphDir, 'supergraph.json'), 'utf8'); const data = JSON.parse(content); this.cache.set(cacheKey, data); return data; } catch (error) { return null; } } async loadCluster(clusterId) { const cacheKey = `cluster_${clusterId}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey); } try { const content = await readFile(join(this.clustersDir, `${clusterId}.json`), 'utf8'); const data = JSON.parse(content); this.cache.set(cacheKey, data); return data; } catch (error) { return null; } } enrichClusterData(cluster, superGraph) { // Calculate importance based on size and connectivity const connections = superGraph.edges.filter(e => e.from === cluster.id || e.to === cluster.id ).length; const importance = cluster.files * 0.3 + connections * 2; return { ...cluster, connections, importance }; } getMajorConnections(superGraph, limit) { return superGraph.edges .sort((a, b) => b.weight - a.weight) .slice(0, limit) .map(edge => ({ from: superGraph.clusters[edge.from]?.description || edge.from, to: superGraph.clusters[edge.to]?.description || edge.to, strength: edge.weight })); } getMetrics(superGraph) { return { totalFiles: superGraph.metadata.totalFiles, totalClusters: superGraph.metadata.totalClusters, compressionRatio: `${superGraph.metadata.compressionRatio}:1`, lastUpdated: superGraph.metadata.timestamp }; } getClusterConnections(clusterId, superGraph) { if (!superGraph) return []; return superGraph.edges .filter(e => e.from === clusterId || e.to === clusterId) .map(edge => ({ target: edge.from === clusterId ? edge.to : edge.from, description: superGraph.clusters[edge.from === clusterId ? edge.to : edge.from]?.description, weight: edge.weight, direction: edge.from === clusterId ? 'outgoing' : 'incoming' })) .sort((a, b) => b.weight - a.weight); } shortenPath(filePath) { if (!filePath) return ''; // Remove leading ./ let shortened = filePath.replace(/^\.\//, ''); // Compress common prefixes shortened = shortened .replace(/^open_spiel\//, 'os/') .replace(/^src\//, '') .replace(/^lib\//, ''); // Keep only last 2-3 components for very long paths const parts = shortened.split('/'); if (parts.length > 4) { return `.../${parts.slice(-2).join('/')}`; } return shortened; } getFileType(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); const typeMap = { 'py': 'Python', 'js': 'JavaScript', 'ts': 'TypeScript', 'cpp': 'C++', 'c': 'C', 'h': 'Header', 'hpp': 'C++ Header', 'json': 'Config', 'md': 'Documentation', 'txt': 'Text', 'yml': 'Config', 'yaml': 'Config' }; return typeMap[ext] || 'Unknown'; } getLanguage(filePath) { const ext = filePath.split('.').pop()?.toLowerCase(); return ext || 'unknown'; } async getFileSize(fullPath) { try { const stats = await import('fs/promises').then(fs => fs.stat(fullPath)); const bytes = stats.size; if (bytes > 1024) { return `${Math.round(bytes / 1024)}KB`; } else { return `${bytes}B`; } } catch { return 'Unknown'; } } async getFileBasics(filePath) { // Quick file analysis without full content read try { const fullPath = join(this.rootPath, filePath); const content = await readFile(fullPath, 'utf8'); const lines = content.split('\n'); return { lines: lines.length, hasDocstring: this.hasDocstring(content, this.getLanguage(filePath)), estimatedComplexity: this.estimateComplexity(content) }; } catch { return {}; } } hasDocstring(content, language) { if (language === 'py') { return content.includes('"""') || content.includes("'''"); } else if (['js', 'ts'].includes(language)) { return content.includes('/**') || content.includes('//'); } return false; } estimateComplexity(content) { const lines = content.split('\n').filter(l => l.trim().length > 0); const functions = (content.match(/def |function |class /g) || []).length; if (functions > 20) return 'High'; if (functions > 5) return 'Medium'; return 'Low'; } async extractFileSymbols(fullPath, language) { // Simple symbol extraction - could be enhanced with proper AST parsing try { const content = await readFile(fullPath, 'utf8'); const symbols = []; if (language === 'py') { // Extract Python classes and functions const classMatches = content.matchAll(/^class\s+(\w+)/gm); for (const match of classMatches) { symbols.push({ type: 'class', name: match[1] }); } const funcMatches = content.matchAll(/^def\s+(\w+)/gm); for (const match of funcMatches) { symbols.push({ type: 'function', name: match[1] }); } } else if (['js', 'ts'].includes(language)) { // Extract JavaScript/TypeScript functions and classes const classMatches = content.matchAll(/class\s+(\w+)/g); for (const match of classMatches) { symbols.push({ type: 'class', name: match[1] }); } const funcMatches = content.matchAll(/function\s+(\w+)|(\w+)\s*\(/g); for (const match of funcMatches) { symbols.push({ type: 'function', name: match[1] || match[2] }); } } return symbols.slice(0, 10); // Limit for context efficiency } catch { return []; } } async getFileDependencies(filePath) { // This would integrate with the main graph data // For now, return placeholder return { imports: [], exports: [], dependents: [] }; } }