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
JavaScript
/**
* 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;