claude-code-graph
Version:
Claude Code with live structural graphs for large codebases
1,133 lines (954 loc) ⢠34.6 kB
JavaScript
import { GraphTool } from './GraphTool.js';
import { ClusterTools } from './ClusterTools.js';
import path from 'path';
import fs from 'fs/promises';
import { spawn, exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Graph-specific commands for Claude Code
* Provides direct user access to graph functionality via slash commands
*/
export class GraphCommands {
constructor(rootPath) {
this.rootPath = rootPath;
this.graphTool = new GraphTool(rootPath);
this.clusterTools = new ClusterTools(rootPath);
this.initialized = false;
}
/**
* Initialize graph commands
*/
async initialize() {
try {
this.initialized = await this.graphTool.initialize();
return this.initialized;
} catch (error) {
console.warn('GraphCommands initialization failed:', error.message);
this.initialized = false;
return false;
}
}
/**
* Get all available graph commands
*/
getAvailableCommands() {
return {
'/graph-overview': {
description: 'Show architecture overview of the codebase',
usage: '/graph-overview',
example: '/graph-overview'
},
'/find-related': {
description: 'Find files related to a specific file',
usage: '/find-related <filename>',
example: '/find-related src/auth/login.js'
},
'/hot-paths': {
description: 'Show most frequently used code paths',
usage: '/hot-paths [--limit=N]',
example: '/hot-paths --limit=5'
},
'/cycles': {
description: 'Detect circular dependencies',
usage: '/cycles [--language=<lang>]',
example: '/cycles --language=javascript'
},
'/graph-stats': {
description: 'Show graph statistics and health info',
usage: '/graph-stats',
example: '/graph-stats'
},
'/graph-health': {
description: 'Check graph system health and diagnostics',
usage: '/graph-health',
example: '/graph-health'
},
'/dstart': {
description: 'Start the graph update daemon',
usage: '/dstart [--background]',
example: '/dstart --background'
},
'/dstop': {
description: 'Stop the graph update daemon',
usage: '/dstop [--force]',
example: '/dstop'
},
'/gupdate': {
description: 'Manually update all graphs (rebuild)',
usage: '/gupdate [--force]',
example: '/gupdate --force'
},
'/dstatus': {
description: 'Show daemon status and performance metrics',
usage: '/dstatus',
example: '/dstatus'
},
'/clusters': {
description: 'Show compressed cluster overview of codebase',
usage: '/clusters [--limit=N]',
example: '/clusters --limit=10'
},
'/cluster': {
description: 'Expand specific cluster to see files',
usage: '/cluster <cluster_id>',
example: '/cluster c0'
},
'/cfile': {
description: 'Get detailed information about a specific file',
usage: '/cfile <file_path>',
example: '/cfile src/main.py'
},
'/csearch': {
description: 'Search clusters by description or keywords',
usage: '/csearch <query>',
example: '/csearch "test utils"'
}
};
}
/**
* Execute a graph command
*/
async executeCommand(command, args = [], options = {}) {
if (!this.initialized) {
await this.initialize();
}
if (!this.initialized) {
return this.formatError('Graph system not available',
'Run `ccg doctor` to check system health');
}
try {
switch (command) {
case '/graph-overview':
return await this.handleOverviewCommand(args, options);
case '/find-related':
return await this.handleFindRelatedCommand(args, options);
case '/hot-paths':
return await this.handleHotPathsCommand(args, options);
case '/cycles':
return await this.handleCyclesCommand(args, options);
case '/graph-stats':
return await this.handleStatsCommand(args, options);
case '/graph-health':
return await this.handleHealthCommand(args, options);
case '/dstart':
return await this.handleDaemonStartCommand(args, options);
case '/dstop':
return await this.handleDaemonStopCommand(args, options);
case '/gupdate':
return await this.handleGraphUpdateCommand(args, options);
case '/dstatus':
return await this.handleDaemonStatusCommand(args, options);
case '/clusters':
return await this.handleClustersCommand(args, options);
case '/cluster':
return await this.handleClusterExpandCommand(args, options);
case '/cfile':
return await this.handleClusterFileCommand(args, options);
case '/csearch':
return await this.handleClusterSearchCommand(args, options);
default:
return this.formatError(`Unknown command: ${command}`,
'Use `/graph-help` to see available commands');
}
} catch (error) {
return this.formatError(`Command failed: ${error.message}`,
'Check graph system health with `/graph-health`');
}
}
/**
* Handle /graph-overview command
*/
async handleOverviewCommand(args, options) {
const overview = await this.graphTool.getArchitectureOverview();
if (!overview) {
return this.formatError('No architecture data available');
}
return this.formatArchitectureOverview(overview);
}
/**
* Handle /find-related command
*/
async handleFindRelatedCommand(args, options) {
if (args.length === 0) {
return this.formatError('Missing filename argument',
'Usage: /find-related <filename>');
}
const filename = args[0];
const depth = options.depth || 3;
const maxResults = options.limit || 20;
const related = await this.graphTool.findRelatedFiles(filename, {
depth,
maxResults
});
if (related.length === 0) {
return this.formatInfo(`No related files found for: ${filename}`,
'File may not be in the graph or has no relationships');
}
return this.formatRelatedFiles(filename, related);
}
/**
* Handle /hot-paths command
*/
async handleHotPathsCommand(args, options) {
const limit = options.limit || 10;
const hotPaths = await this.graphTool.getHotPaths();
if (hotPaths.length === 0) {
return this.formatInfo('No hot paths found',
'The codebase may be too small or have simple structure');
}
return this.formatHotPaths(hotPaths.slice(0, limit));
}
/**
* Handle /cycles command
*/
async handleCyclesCommand(args, options) {
const cycles = await this.graphTool.detectCycles();
const languageFilter = options.language;
let filteredCycles = cycles;
if (languageFilter) {
filteredCycles = cycles.filter(cycle =>
cycle.language.toLowerCase() === languageFilter.toLowerCase());
}
if (filteredCycles.length === 0) {
const message = languageFilter
? `No cycles found in ${languageFilter}`
: 'No circular dependencies detected';
return this.formatSuccess(message, 'This is good! Your code has clean dependencies.');
}
return this.formatCycles(filteredCycles);
}
/**
* Handle /graph-stats command
*/
async handleStatsCommand(args, options) {
const stats = await this.graphTool.getStats();
if (!stats) {
return this.formatError('No statistics available');
}
return this.formatStats(stats);
}
/**
* Handle /graph-health command
*/
async handleHealthCommand(args, options) {
const health = await this.graphTool.checkHealth();
return this.formatHealth(health);
}
/**
* Handle /dstart command - Start the graph daemon
*/
async handleDaemonStartCommand(args, options) {
try {
// Check if daemon is already running
const isRunning = await this.isDaemonRunning();
if (isRunning.running) {
return this.formatInfo('Daemon already running',
`PID: ${isRunning.pid}. Use \`/dstop\` to stop it first.`);
}
const background = options.background || args.includes('--background');
if (background) {
// Start daemon in background
const result = await this.startDaemonBackground();
if (result.success) {
return this.formatSuccess('Graph daemon started in background',
`PID: ${result.pid}. Use \`/dstatus\` to monitor progress.`);
} else {
return this.formatError('Failed to start daemon', result.error);
}
} else {
// Start daemon and show initial output
return this.formatInfo('Starting graph daemon...',
'Daemon will run in background. Use `/dstatus` to check status.');
}
} catch (error) {
return this.formatError('Failed to start daemon', error.message);
}
}
/**
* Handle /dstop command - Stop the graph daemon
*/
async handleDaemonStopCommand(args, options) {
try {
const isRunning = await this.isDaemonRunning();
if (!isRunning.running) {
return this.formatInfo('Daemon not running',
'Use `/dstart` to start the daemon.');
}
const force = options.force || args.includes('--force');
const result = await this.stopDaemon(isRunning.pid, force);
if (result.success) {
return this.formatSuccess('Graph daemon stopped',
`Daemon with PID ${isRunning.pid} has been terminated.`);
} else {
return this.formatError('Failed to stop daemon', result.error);
}
} catch (error) {
return this.formatError('Failed to stop daemon', error.message);
}
}
/**
* Handle /gupdate command - Manual graph update
*/
async handleGraphUpdateCommand(args, options) {
try {
const force = options.force || args.includes('--force');
// Check if daemon is running - prefer daemon refresh if available
const isRunning = await this.isDaemonRunning();
if (isRunning.running && !force) {
// Send refresh signal to daemon
const result = await this.refreshDaemon(isRunning.pid);
if (result.success) {
return this.formatSuccess('Graph update triggered',
'Daemon received refresh signal. Updates will complete shortly.');
}
}
// Manual rebuild (daemon not running or force requested)
return this.formatInfo('Starting manual graph rebuild...',
'This may take a few seconds for large codebases.');
// Actually run the rebuild
const buildResult = await this.runGraphBuild();
if (buildResult.success) {
return this.formatSuccess('Graph rebuild completed',
`Processed ${buildResult.stats?.total_files || 'unknown'} files in ${buildResult.duration}s`);
} else {
return this.formatError('Graph rebuild failed', buildResult.error);
}
} catch (error) {
return this.formatError('Graph update failed', error.message);
}
}
/**
* Handle /dstatus command - Show daemon status
*/
async handleDaemonStatusCommand(args, options) {
try {
const status = await this.getDaemonStatus();
return this.formatDaemonStatus(status);
} catch (error) {
return this.formatError('Failed to get daemon status', error.message);
}
}
/**
* Handle /clusters command - Show cluster overview
*/
async handleClustersCommand(args, options) {
try {
const limit = options.limit || parseInt(args.find(arg => arg.startsWith('--limit='))?.split('=')[1]) || 20;
const result = await this.clusterTools.clusterList({
maxClusters: limit,
includeEdges: true,
includeMetrics: true
});
if (!result.success) {
return this.formatError('Failed to load clusters', result.error);
}
return this.formatClusters(result);
} catch (error) {
return this.formatError('Cluster command failed', error.message);
}
}
/**
* Handle /cluster command - Expand specific cluster
*/
async handleClusterExpandCommand(args, options) {
if (args.length === 0) {
return this.formatError('Missing cluster ID', 'Usage: /cluster <cluster_id> (e.g., /cluster c0)');
}
try {
const clusterId = args[0];
const maxFiles = options.files || 20;
const result = await this.clusterTools.clusterExpand(clusterId, {
includeFileDetails: true,
maxFiles
});
if (!result.success) {
return this.formatError(`Failed to expand cluster ${clusterId}`, result.error);
}
return this.formatClusterExpansion(result);
} catch (error) {
return this.formatError('Cluster expansion failed', error.message);
}
}
/**
* Handle /cfile command - Get file details
*/
async handleClusterFileCommand(args, options) {
if (args.length === 0) {
return this.formatError('Missing file path', 'Usage: /cfile <file_path>');
}
try {
const filePath = args[0];
const result = await this.clusterTools.fileGet(filePath, {
includeSymbols: true,
includeDependencies: true,
includeContent: options.content || false
});
if (!result.success) {
return this.formatError(`Failed to get file info for ${filePath}`, result.error);
}
return this.formatFileDetails(result);
} catch (error) {
return this.formatError('File details failed', error.message);
}
}
/**
* Handle /csearch command - Search clusters
*/
async handleClusterSearchCommand(args, options) {
if (args.length === 0) {
return this.formatError('Missing search query', 'Usage: /csearch <query>');
}
try {
const query = args.join(' ');
const maxResults = options.limit || 10;
const result = await this.clusterTools.clusterSearch(query, { maxResults });
if (!result.success) {
return this.formatError('Search failed', result.error);
}
return this.formatSearchResults(query, result);
} catch (error) {
return this.formatError('Cluster search failed', error.message);
}
}
// Formatting methods
/**
* Format architecture overview
*/
formatArchitectureOverview(overview) {
let output = '## š Architecture Overview\n\n';
// Metrics summary
if (overview.metrics) {
output += '### Project Metrics\n';
output += `- **Total Files**: ${overview.metrics.total_files || 'Unknown'}\n`;
output += `- **Last Updated**: ${overview.metrics.timestamp || 'Unknown'}\n\n`;
if (overview.metrics.by_language) {
output += '### Languages\n';
Object.entries(overview.metrics.by_language).forEach(([lang, count]) => {
if (count > 0) {
output += `- **${lang}**: ${count} files\n`;
}
});
output += '\n';
}
}
// Module analysis
if (overview.modules) {
output += '### Module Analysis\n';
Object.entries(overview.modules).forEach(([language, analysis]) => {
if (analysis.nodeCount > 0) {
output += `#### ${language.charAt(0).toUpperCase() + language.slice(1)}\n`;
output += `- **Nodes**: ${analysis.nodeCount}\n`;
output += `- **Edges**: ${analysis.edgeCount}\n`;
output += `- **Avg Degree**: ${analysis.avgDegree?.toFixed(2) || 'N/A'}\n`;
if (analysis.topNodes && analysis.topNodes.length > 0) {
output += `- **Most Connected**: ${analysis.topNodes[0].file || analysis.topNodes[0].id}\n`;
}
output += '\n';
}
});
}
// Hot paths preview
if (overview.hotPaths && overview.hotPaths.length > 0) {
output += '### š„ Top Hot Paths\n';
overview.hotPaths.slice(0, 3).forEach((path, i) => {
output += `${i + 1}. ${path.language}: ${path.path.map(p => p.file || p.id).join(' ā ')}\n`;
});
output += '\n*Use `/hot-paths` for complete list*\n\n';
}
// Cycles warning
if (overview.cycles && overview.cycles.length > 0) {
output += '### ā ļø Circular Dependencies\n';
output += `Found ${overview.cycles.length} circular dependencies.\n`;
output += '*Use `/cycles` for details*\n\n';
}
return { type: 'success', content: output };
}
/**
* Format related files
*/
formatRelatedFiles(filename, related) {
let output = `## š Files Related to \`${filename}\`\n\n`;
if (related.length === 0) {
output += '*No related files found*\n';
return { type: 'info', content: output };
}
// Group by relationship type
const byRelationship = {};
related.forEach(file => {
const rel = file.relationship || 'unknown';
if (!byRelationship[rel]) byRelationship[rel] = [];
byRelationship[rel].push(file);
});
Object.entries(byRelationship).forEach(([relationship, files]) => {
output += `### ${this.formatRelationshipTitle(relationship)}\n`;
files.forEach(file => {
const confidence = (file.confidence * 100).toFixed(0);
output += `- \`${file.path}\` (${confidence}% confidence)\n`;
});
output += '\n';
});
return { type: 'success', content: output };
}
/**
* Format hot paths
*/
formatHotPaths(hotPaths) {
let output = '## š„ Hot Paths (Most Used Code Paths)\n\n';
hotPaths.forEach((pathInfo, i) => {
output += `### ${i + 1}. ${pathInfo.language} Path\n`;
const pathStr = pathInfo.path.map(node => `\`${node.file || node.id}\``).join(' ā ');
output += `${pathStr}\n\n`;
});
return { type: 'success', content: output };
}
/**
* Format cycles
*/
formatCycles(cycles) {
let output = '## š Circular Dependencies\n\n';
if (cycles.length === 0) {
output += 'ā
No circular dependencies found!\n';
return { type: 'success', content: output };
}
output += `Found ${cycles.length} circular dependenc${cycles.length === 1 ? 'y' : 'ies'}:\n\n`;
cycles.forEach((cycle, i) => {
output += `### ${i + 1}. ${cycle.language} Cycle (${cycle.length} nodes)\n`;
const cycleStr = cycle.nodes.map(node => `\`${node.file || node.id}\``).join(' ā ');
output += `${cycleStr} ā *(back to start)*\n\n`;
});
output += 'š” **Tip**: Circular dependencies can make code harder to understand and test. Consider refactoring to break these cycles.\n';
return { type: 'warning', content: output };
}
/**
* Format statistics
*/
formatStats(stats) {
let output = '## š Graph Statistics\n\n';
output += `- **Total Graphs**: ${stats.totalGraphs}\n`;
output += `- **Total Nodes**: ${stats.totalNodes}\n`;
output += `- **Total Edges**: ${stats.totalEdges}\n`;
output += `- **Last Update**: ${stats.lastUpdate || 'Unknown'}\n\n`;
if (stats.languages && stats.languages.length > 0) {
output += '### By Language\n';
stats.languages.forEach(lang => {
output += `- **${lang.language}**: ${lang.nodes} nodes, ${lang.edges} edges\n`;
});
}
return { type: 'info', content: output };
}
/**
* Format health status
*/
formatHealth(health) {
let output = '## š„ Graph System Health\n\n';
const status = health.healthy ? 'ā
Healthy' : 'ā Unhealthy';
output += `**Status**: ${status}\n\n`;
if (health.stats) {
output += '### Current Statistics\n';
output += `- **Graphs Available**: ${health.stats.totalGraphs}\n`;
output += `- **Total Nodes**: ${health.stats.totalNodes}\n`;
output += `- **Total Edges**: ${health.stats.totalEdges}\n\n`;
}
if (health.issues && health.issues.length > 0) {
output += '### Issues\n';
health.issues.forEach(issue => {
output += `- ā ļø ${issue}\n`;
});
output += '\nš” Run `ccg doctor` for more detailed diagnostics.\n';
}
return {
type: health.healthy ? 'success' : 'error',
content: output
};
}
// Helper methods for daemon control
/**
* Check if daemon is running
*/
async isDaemonRunning() {
const lockFile = path.join(this.rootPath, '.graph', 'daemon.lock');
try {
await fs.access(lockFile);
const pidContent = await fs.readFile(lockFile, 'utf8');
const pid = parseInt(pidContent.trim());
// Check if process exists
try {
process.kill(pid, 0);
return { running: true, pid };
} catch (e) {
// Process doesn't exist, remove stale lock file
await fs.unlink(lockFile).catch(() => {});
return { running: false };
}
} catch (e) {
return { running: false };
}
}
/**
* Start daemon in background
*/
async startDaemonBackground() {
try {
const venvPath = path.join(this.rootPath, '.venv', 'bin', 'activate');
const daemonPath = path.join(this.rootPath, 'tools', 'codegraphd.py');
// Check if virtual environment exists
const hasVenv = await fs.access(venvPath).then(() => true).catch(() => false);
let command;
if (hasVenv) {
command = `source ${venvPath} && python ${daemonPath}`;
} else {
command = `python3 ${daemonPath}`;
}
// Start daemon in background
const child = spawn('bash', ['-c', command], {
detached: true,
stdio: 'ignore',
cwd: this.rootPath
});
child.unref();
// Wait a moment to see if it started successfully
await new Promise(resolve => setTimeout(resolve, 1000));
const status = await this.isDaemonRunning();
if (status.running) {
return { success: true, pid: status.pid };
} else {
return { success: false, error: 'Daemon failed to start' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* Stop daemon
*/
async stopDaemon(pid, force = false) {
try {
const signal = force ? 'SIGKILL' : 'SIGTERM';
process.kill(pid, signal);
// Wait a moment for graceful shutdown
if (!force) {
await new Promise(resolve => setTimeout(resolve, 2000));
// Check if still running, force kill if needed
try {
process.kill(pid, 0);
process.kill(pid, 'SIGKILL');
} catch (e) {
// Process already stopped
}
}
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* Send refresh signal to daemon
*/
async refreshDaemon(pid) {
try {
process.kill(pid, 'SIGUSR1');
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* Run manual graph build
*/
async runGraphBuild() {
return new Promise((resolve) => {
const startTime = Date.now();
const buildScript = path.join(this.rootPath, 'tools', 'codegraph.sh');
const child = spawn('bash', [buildScript], {
cwd: this.rootPath,
stdio: 'pipe'
});
let output = '';
let error = '';
child.stdout?.on('data', (data) => {
output += data.toString();
});
child.stderr?.on('data', (data) => {
error += data.toString();
});
child.on('close', async (code) => {
const duration = (Date.now() - startTime) / 1000;
if (code === 0) {
// Try to get updated stats
let stats = null;
try {
const metricsPath = path.join(this.rootPath, '.graph', 'metrics.json');
const metricsContent = await fs.readFile(metricsPath, 'utf8');
stats = JSON.parse(metricsContent);
} catch (e) {
// Ignore metrics errors
}
resolve({
success: true,
duration: duration.toFixed(1),
stats,
output
});
} else {
resolve({
success: false,
error: error || 'Build script failed',
output
});
}
});
child.on('error', (err) => {
resolve({
success: false,
error: err.message
});
});
});
}
/**
* Get comprehensive daemon status
*/
async getDaemonStatus() {
const isRunning = await this.isDaemonRunning();
const status = {
running: isRunning.running,
pid: isRunning.pid,
metrics: null,
graphs: null
};
// Get daemon metrics if available
try {
const metricsPath = path.join(this.rootPath, '.graph', 'metrics.json');
const metricsContent = await fs.readFile(metricsPath, 'utf8');
const metrics = JSON.parse(metricsContent);
status.metrics = metrics.daemon || null;
status.graphs = {
total_files: metrics.total_files,
by_language: metrics.by_language,
last_build: metrics.timestamp
};
} catch (e) {
// Metrics not available
}
return status;
}
/**
* Format daemon status for display
*/
formatDaemonStatus(status) {
let output = '## š¤ Graph Daemon Status\n\n';
if (status.running) {
output += `**Status**: š¢ Running (PID: ${status.pid})\n\n`;
if (status.metrics) {
const metrics = status.metrics;
output += '### Performance Metrics\n';
output += `- **Uptime**: ${this.formatUptime(metrics.daemon_start)}\n`;
output += `- **Total Updates**: ${metrics.updates}\n`;
output += `- **Errors**: ${metrics.errors}\n`;
output += `- **Average Update Time**: ${metrics.avg_time.toFixed(2)}s\n`;
if (metrics.last_update) {
const lastUpdate = new Date(metrics.last_update);
const timeSince = Math.floor((Date.now() - lastUpdate.getTime()) / 1000);
output += `- **Last Update**: ${timeSince}s ago\n`;
}
output += '\n';
}
if (status.graphs) {
output += '### Graph Status\n';
output += `- **Total Files**: ${status.graphs.total_files || 'Unknown'}\n`;
if (status.graphs.by_language) {
output += '- **Languages**:\n';
Object.entries(status.graphs.by_language).forEach(([lang, count]) => {
if (count > 0) {
output += ` - ${lang}: ${count} files\n`;
}
});
}
output += '\n';
}
output += '### Controls\n';
output += '- `/dstop` - Stop daemon\n';
output += '- `/gupdate` - Trigger manual refresh\n';
} else {
output += '**Status**: š“ Not Running\n\n';
output += '### Quick Start\n';
output += '- `/dstart` - Start daemon\n';
output += '- `/gupdate` - Manual graph rebuild\n';
}
return { type: status.running ? 'success' : 'info', content: output };
}
/**
* Format uptime duration
*/
formatUptime(startTime) {
try {
const start = new Date(startTime);
const now = new Date();
const diffMs = now - start;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
if (diffHours > 0) {
return `${diffHours}h ${diffMinutes}m`;
} else {
return `${diffMinutes}m`;
}
} catch (e) {
return 'Unknown';
}
}
formatRelationshipTitle(relationship) {
const titles = {
'imports': 'š„ Imports',
'calls': 'š Function Calls',
'inheritance': '𧬠Inheritance',
'reverse_imports': 'š¤ Imported By',
'reverse_calls': 'š² Called By',
'reverse_inheritance': 'š„ Inherited By'
};
return titles[relationship] || `š ${relationship.replace(/_/g, ' ')}`;
}
formatError(message, details = null) {
return {
type: 'error',
content: `ā **Error**: ${message}${details ? `\n\nš” ${details}` : ''}`
};
}
formatSuccess(message, details = null) {
return {
type: 'success',
content: `ā
**Success**: ${message}${details ? `\n\n${details}` : ''}`
};
}
formatInfo(message, details = null) {
return {
type: 'info',
content: `ā¹ļø **Info**: ${message}${details ? `\n\n${details}` : ''}`
};
}
/**
* Format clusters overview
*/
formatClusters(result) {
let output = '## š§ Codebase Clusters Overview\n\n';
if (result.metrics) {
output += `**Compression**: ${result.metrics.totalFiles} files ā ${result.total} clusters (${result.metrics.compressionRatio})\n`;
output += `**Last Updated**: ${new Date(result.metrics.lastUpdated).toLocaleString()}\n\n`;
}
if (result.clusters.length === 0) {
output += '*No clusters available. Run `/gupdate` to rebuild graphs.*\n';
return { type: 'info', content: output };
}
output += '### Clusters (by importance)\n\n';
result.clusters.forEach((cluster, i) => {
output += `#### ${i + 1}. \`${cluster.id}\`: ${cluster.summary}\n`;
output += `- **Files**: ${cluster.files} | **Languages**: ${cluster.languages.join(', ')}\n`;
output += `- **Key Files**: ${cluster.keyFiles.slice(0, 3).join(', ')}${cluster.keyFiles.length > 3 ? '...' : ''}\n`;
output += `- **Connections**: ${cluster.connections} | **Importance**: ${cluster.importance}\n\n`;
});
if (result.majorConnections && result.majorConnections.length > 0) {
output += '### š Major Cluster Connections\n\n';
result.majorConnections.forEach(conn => {
output += `- **${conn.from}** ā **${conn.to}** (strength: ${conn.strength})\n`;
});
output += '\n';
}
output += '### š” Navigation Commands\n';
output += '- `/cluster <id>` - Expand specific cluster\n';
output += '- `/csearch <query>` - Search clusters\n';
output += '- `/cfile <path>` - Get file details\n';
return { type: 'success', content: output };
}
/**
* Format cluster expansion
*/
formatClusterExpansion(result) {
const cluster = result.cluster;
let output = `## š Cluster \`${cluster.id}\` Details\n\n`;
output += `**Description**: ${cluster.description}\n`;
output += `**Files**: ${cluster.showing}/${cluster.totalFiles} shown\n`;
output += `**Languages**: ${cluster.languages.join(', ')}\n\n`;
if (cluster.keyFiles && cluster.keyFiles.length > 0) {
output += '### š Key Files\n';
cluster.keyFiles.forEach(file => {
output += `- \`${file}\`\n`;
});
output += '\n';
}
output += '### š All Files\n';
result.files.forEach((file, i) => {
output += `${i + 1}. \`${file.shortPath}\` (${file.type})`;
if (file.lines) {
output += ` - ${file.lines} lines, ${file.estimatedComplexity} complexity`;
}
output += '\n';
});
if (result.hasMore) {
output += `\n*... and ${cluster.totalFiles - cluster.showing} more files*\n`;
}
if (result.connections && result.connections.length > 0) {
output += '\n### š Connected Clusters\n';
result.connections.slice(0, 5).forEach(conn => {
const arrow = conn.direction === 'outgoing' ? 'ā' : 'ā';
output += `- ${arrow} **${conn.description}** (weight: ${conn.weight})\n`;
});
}
return { type: 'success', content: output };
}
/**
* Format file details
*/
formatFileDetails(result) {
const file = result.file;
let output = `## š File Details: \`${file.shortPath}\`\n\n`;
output += `**Type**: ${file.type} (${file.language})\n`;
output += `**Size**: ${file.size}\n`;
output += `**Path**: \`${file.path}\`\n\n`;
if (file.symbols && file.symbols.length > 0) {
output += '### š§ Symbols\n';
const symbolsByType = {};
file.symbols.forEach(symbol => {
if (!symbolsByType[symbol.type]) symbolsByType[symbol.type] = [];
symbolsByType[symbol.type].push(symbol.name);
});
Object.entries(symbolsByType).forEach(([type, names]) => {
output += `- **${type}**: ${names.slice(0, 5).join(', ')}${names.length > 5 ? `... (+${names.length - 5} more)` : ''}\n`;
});
output += '\n';
}
if (file.dependencies) {
output += '### š Dependencies\n';
output += `- **Imports**: ${file.dependencies.imports.length}\n`;
output += `- **Exports**: ${file.dependencies.exports.length}\n`;
output += `- **Dependents**: ${file.dependencies.dependents.length}\n\n`;
}
if (file.contentPreview) {
output += '### š Content Preview\n';
output += '```\n';
output += file.contentPreview;
output += '\n```\n';
output += `\n*Preview of ${file.fullSize} characters total*\n`;
}
return { type: 'success', content: output };
}
/**
* Format search results
*/
formatSearchResults(query, result) {
let output = `## š Search Results for "${query}"\n\n`;
if (result.results.length === 0) {
output += '*No clusters found matching your query.*\n\n';
output += 'š” Try broader terms or use `/clusters` to see all available clusters.\n';
return { type: 'info', content: output };
}
output += `Found ${result.results.length} matching cluster${result.results.length === 1 ? '' : 's'}:\n\n`;
result.results.forEach((match, i) => {
const cluster = match.cluster;
output += `### ${i + 1}. \`${cluster.id}\` (Score: ${match.score})\n`;
output += `**Description**: ${cluster.description}\n`;
output += `**Files**: ${cluster.files} | **Languages**: ${cluster.languages.join(', ')}\n`;
output += `*Use \`/cluster ${cluster.id}\` to expand*\n\n`;
});
return { type: 'success', content: output };
}
}