UNPKG

claude-coordination-system

Version:

🤖 Multi-Claude Parallel Processing Coordination System - Organize multiple Claude AI instances to work together seamlessly on complex development tasks

500 lines (420 loc) 15.1 kB
/** * Claude Sync Client - Natural Claude Coordination Interface * Transparent coordination without interrupting Claude conversation flow */ const fs = require('fs-extra'); const path = require('path'); const EventEmitter = require('events'); const chalk = require('chalk'); const readline = require('readline'); const { logWorker } = require('./development-logger'); class ClaudeSyncClient extends EventEmitter { constructor(sessionId, groupId, projectRoot, options = {}) { super(); this.sessionId = sessionId; this.groupId = groupId; this.projectRoot = projectRoot; this.coordinationDir = path.join(projectRoot, '.claude-coord'); this.options = { syncInterval: 2000, // 2 seconds heartbeatInterval: 5000, // 5 seconds showCoordination: true, // Show coordination messages primaryTerminal: false, // Is this the primary terminal for the group ...options }; // Client state this.isActive = false; this.lastHeartbeat = null; this.coordinationEngine = null; this.messageCheckTimer = null; this.heartbeatTimer = null; // Group collaboration state this.groupMembers = []; this.currentTask = null; this.activeFiles = []; // Initialize display this.setupDisplay(); console.log(chalk.blue(`🔄 Claude Sync Client initialized: ${sessionId} → ${groupId}`)); } /** * Start coordination client */ async start() { if (this.isActive) return; this.isActive = true; try { // Check if coordination engine is running await this.connectToCoordination(); // Register this session await this.registerSession(); // Start coordination loops this.startMessageCheckLoop(); this.startHeartbeatLoop(); // Setup terminal input handling this.setupTerminalHandling(); // Show status this.displayCoordinationStatus(); console.log(chalk.green(`✅ Claude Sync active: ${this.sessionId} (${this.groupId})`)); await logWorker(this.sessionId, 'Sync Client Started', { description: `Coordination client started for group: ${this.groupId}`, result: 'SUCCESS', group: this.groupId }); this.emit('client:started'); } catch (error) { console.error(chalk.red('❌ Failed to start Claude Sync:'), error.message); console.log(chalk.yellow('🔧 Make sure coordination engine is running:')); console.log(chalk.gray(' claude-coord start')); process.exit(1); } } /** * Stop coordination client */ async stop() { if (!this.isActive) return; this.isActive = false; // Clear timers if (this.messageCheckTimer) clearInterval(this.messageCheckTimer); if (this.heartbeatTimer) clearInterval(this.heartbeatTimer); // Release any held file locks await this.releaseAllFileLocks(); // Unregister session await this.unregisterSession(); console.log(chalk.yellow(`🛑 Claude Sync stopped: ${this.sessionId}`)); this.emit('client:stopped'); } /** * Connect to coordination engine */ async connectToCoordination() { const sessionFile = path.join(this.coordinationDir, 'active-sessions.json'); // Check if coordination files exist if (!await fs.pathExists(this.coordinationDir)) { throw new Error('Coordination engine not running. Start with: claude-coord start'); } console.log(chalk.gray('🔗 Connected to coordination engine')); } /** * Register this session with coordination engine */ async registerSession() { const sessionFile = path.join(this.coordinationDir, 'active-sessions.json'); const groupStateFile = path.join(this.coordinationDir, 'group-states.json'); // Load existing sessions let sessions = {}; if (await fs.pathExists(sessionFile)) { sessions = await fs.readJson(sessionFile); } // Add this session sessions[this.sessionId] = { id: this.sessionId, group: this.groupId, status: 'active', joinedAt: new Date().toISOString(), lastHeartbeat: new Date().toISOString(), metadata: { terminalId: this.sessionId, pid: process.pid, cwd: process.cwd() } }; await fs.writeJson(sessionFile, sessions, { spaces: 2 }); // Update group state let groupStates = {}; if (await fs.pathExists(groupStateFile)) { groupStates = await fs.readJson(groupStateFile); } if (!groupStates[this.groupId]) { groupStates[this.groupId] = { id: this.groupId, sessions: [], primarySession: this.sessionId, currentTask: null, fileScope: [], createdAt: new Date().toISOString() }; } if (!groupStates[this.groupId].sessions.includes(this.sessionId)) { groupStates[this.groupId].sessions.push(this.sessionId); } // Set as primary if first session in group if (groupStates[this.groupId].sessions.length === 1) { this.options.primaryTerminal = true; groupStates[this.groupId].primarySession = this.sessionId; } await fs.writeJson(groupStateFile, groupStates, { spaces: 2 }); console.log(chalk.green(`👥 Registered with group: ${this.groupId} (${groupStates[this.groupId].sessions.length} members)`)); } /** * Unregister session */ async unregisterSession() { const sessionFile = path.join(this.coordinationDir, 'active-sessions.json'); const groupStateFile = path.join(this.coordinationDir, 'group-states.json'); try { // Remove from sessions if (await fs.pathExists(sessionFile)) { const sessions = await fs.readJson(sessionFile); delete sessions[this.sessionId]; await fs.writeJson(sessionFile, sessions, { spaces: 2 }); } // Remove from group if (await fs.pathExists(groupStateFile)) { const groupStates = await fs.readJson(groupStateFile); if (groupStates[this.groupId]) { groupStates[this.groupId].sessions = groupStates[this.groupId].sessions.filter(id => id !== this.sessionId); // Assign new primary if needed if (groupStates[this.groupId].primarySession === this.sessionId && groupStates[this.groupId].sessions.length > 0) { groupStates[this.groupId].primarySession = groupStates[this.groupId].sessions[0]; } // Remove group if empty if (groupStates[this.groupId].sessions.length === 0) { delete groupStates[this.groupId]; } await fs.writeJson(groupStates, groupStates, { spaces: 2 }); } } } catch (error) { console.warn(chalk.yellow('⚠️ Could not unregister session:'), error.message); } } /** * Start message checking loop */ startMessageCheckLoop() { this.messageCheckTimer = setInterval(async () => { try { await this.checkForMessages(); await this.updateGroupStatus(); } catch (error) { console.error(chalk.red('❌ Message check error:'), error.message); } }, this.options.syncInterval); } /** * Start heartbeat loop */ startHeartbeatLoop() { this.heartbeatTimer = setInterval(async () => { try { await this.sendHeartbeat(); } catch (error) { console.error(chalk.red('❌ Heartbeat error:'), error.message); } }, this.options.heartbeatInterval); } /** * Check for coordination messages */ async checkForMessages() { // This will be implemented when coordination engine creates message files // For now, we'll check group state changes const groupStateFile = path.join(this.coordinationDir, 'group-states.json'); if (await fs.pathExists(groupStateFile)) { const groupStates = await fs.readJson(groupStateFile); const groupState = groupStates[this.groupId]; if (groupState) { // Update group members const previousMembers = this.groupMembers.length; this.groupMembers = groupState.sessions.filter(id => id !== this.sessionId); // Show member changes if (this.groupMembers.length !== previousMembers) { this.displayGroupMemberUpdate(); } // Check for task updates if (groupState.currentTask !== this.currentTask) { this.currentTask = groupState.currentTask; if (this.currentTask && !this.options.primaryTerminal) { this.displayTaskUpdate(); } } } } } /** * Send heartbeat */ async sendHeartbeat() { const sessionFile = path.join(this.coordinationDir, 'active-sessions.json'); if (await fs.pathExists(sessionFile)) { const sessions = await fs.readJson(sessionFile); if (sessions[this.sessionId]) { sessions[this.sessionId].lastHeartbeat = new Date().toISOString(); await fs.writeJson(sessionFile, sessions, { spaces: 2 }); } } } /** * Update group status */ async updateGroupStatus() { // Update terminal status display this.updateStatusDisplay(); } /** * Setup terminal display */ setupDisplay() { // Create status line at bottom of terminal this.statusLine = { group: this.groupId, members: 0, status: 'connecting', task: null }; } /** * Setup terminal input handling */ setupTerminalHandling() { // Listen for special coordination commands process.stdin.on('data', (data) => { const input = data.toString().trim().toLowerCase(); // Handle coordination commands if (input.startsWith('!coord ')) { this.handleCoordinationCommand(input.substring(7)); } }); } /** * Handle coordination commands */ async handleCoordinationCommand(command) { const parts = command.split(' '); const action = parts[0]; switch (action) { case 'status': this.displayDetailedStatus(); break; case 'members': this.displayGroupMembers(); break; case 'switch': if (parts[1]) { await this.switchGroup(parts[1]); } break; case 'task': if (parts[1]) { await this.broadcastTask(parts.slice(1).join(' ')); } break; case 'help': this.displayCoordinationHelp(); break; default: console.log(chalk.yellow('Unknown coordination command. Type !coord help for available commands.')); } } /** * Display coordination status */ displayCoordinationStatus() { if (!this.options.showCoordination) return; console.log(chalk.blue('─'.repeat(60))); console.log(chalk.blue('🔄 Claude Coordination Active')); console.log(chalk.gray(` Group: ${this.groupId} | Session: ${this.sessionId}`)); console.log(chalk.gray(` Primary: ${this.options.primaryTerminal ? 'YES' : 'NO'} | Members: ${this.groupMembers.length + 1}`)); console.log(chalk.blue('─'.repeat(60))); console.log(chalk.gray('💡 Type !coord help for coordination commands')); console.log(); } /** * Display group member update */ displayGroupMemberUpdate() { if (!this.options.showCoordination) return; console.log(chalk.cyan(`👥 Group updated: ${this.groupMembers.length + 1} members in ${this.groupId}`)); } /** * Display task update */ displayTaskUpdate() { if (!this.options.showCoordination) return; console.log(chalk.yellow(`📋 Group task: ${this.currentTask}`)); console.log(chalk.gray(' Coordinating with other group members...')); } /** * Update status display */ updateStatusDisplay() { // Update status line (could be implemented as terminal status bar) this.statusLine.members = this.groupMembers.length + 1; this.statusLine.status = 'active'; this.statusLine.task = this.currentTask; } /** * Display detailed status */ displayDetailedStatus() { console.log(chalk.blue('📊 Claude Coordination Status')); console.log(chalk.gray('─'.repeat(40))); console.log(`Group: ${chalk.cyan(this.groupId)}`); console.log(`Session: ${chalk.cyan(this.sessionId)}`); console.log(`Primary Terminal: ${this.options.primaryTerminal ? chalk.green('YES') : chalk.yellow('NO')}`); console.log(`Group Members: ${chalk.cyan(this.groupMembers.length + 1)}`); console.log(`Current Task: ${this.currentTask ? chalk.cyan(this.currentTask) : chalk.gray('None')}`); console.log(`Active Files: ${chalk.cyan(this.activeFiles.length)}`); console.log(`Status: ${chalk.green('Active')}`); } /** * Display group members */ displayGroupMembers() { console.log(chalk.blue(`👥 Group Members (${this.groupId})`)); console.log(chalk.gray('─'.repeat(30))); console.log(`${chalk.green('●')} ${this.sessionId} ${this.options.primaryTerminal ? chalk.yellow('(primary)') : ''}`); this.groupMembers.forEach(memberId => { console.log(`${chalk.green('●')} ${memberId}`); }); } /** * Display coordination help */ displayCoordinationHelp() { console.log(chalk.blue('🔄 Claude Coordination Commands')); console.log(chalk.gray('─'.repeat(40))); console.log('!coord status - Show coordination status'); console.log('!coord members - List group members'); console.log('!coord switch <group> - Switch to different group'); console.log('!coord task <description> - Broadcast task to group'); console.log('!coord help - Show this help'); console.log(); console.log(chalk.gray('💡 These commands work alongside normal Claude conversation')); } /** * Switch to different group */ async switchGroup(newGroupId) { await this.unregisterSession(); this.groupId = newGroupId; await this.registerSession(); console.log(chalk.green(`🔄 Switched to group: ${newGroupId}`)); this.displayCoordinationStatus(); } /** * Broadcast task to group */ async broadcastTask(taskDescription) { const groupStateFile = path.join(this.coordinationDir, 'group-states.json'); if (await fs.pathExists(groupStateFile)) { const groupStates = await fs.readJson(groupStateFile); if (groupStates[this.groupId]) { groupStates[this.groupId].currentTask = taskDescription; groupStates[this.groupId].taskUpdatedBy = this.sessionId; groupStates[this.groupId].taskUpdatedAt = new Date().toISOString(); await fs.writeJson(groupStateFile, groupStates, { spaces: 2 }); console.log(chalk.green(`📋 Task broadcasted to ${this.groupId}: ${taskDescription}`)); } } } /** * Release all file locks held by this session */ async releaseAllFileLocks() { // Implementation for releasing file locks this.activeFiles = []; } } module.exports = ClaudeSyncClient;