UNPKG

claude-collab

Version:

Claude Collab - The AI collaboration framework that prevents echo chambers

277 lines • 10.1 kB
"use strict"; /** * Connection Helper for Claude-Collab CLI * Provides easy-to-use connection management with auto-reconnection */ const WebSocket = require('ws'); const { EventEmitter } = require('events'); class CLIConnectionHelper extends EventEmitter { constructor(serverUrl) { super(); this.serverUrl = serverUrl; this.ws = null; this.isAuthenticated = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.reconnectDelay = 1000; this.maxReconnectDelay = 10000; this.reconnectBackoffMultiplier = 1.5; this.messageQueue = []; this.isConnected = false; this.isReconnecting = false; this.manualDisconnect = false; this.pingInterval = null; this.pingTimeout = null; } /** * Connect to server with automatic retry */ async connect() { console.log('- Connecting to server...'); return new Promise((resolve, reject) => { try { this.ws = new WebSocket(this.serverUrl); let connectionTimeout; this.ws.on('open', () => { clearTimeout(connectionTimeout); console.log('āœ” Connected to server'); this.isConnected = true; this.isReconnecting = false; this.reconnectAttempts = 0; this.reconnectDelay = 1000; // Process queued messages while (this.messageQueue.length > 0) { const msg = this.messageQueue.shift(); this.send(msg); } // Start heartbeat this.startHeartbeat(); this.emit('connected', { reconnected: this.reconnectAttempts > 0, attemptCount: this.reconnectAttempts }); resolve(this); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); // Reset heartbeat timeout on any message if (this.pingTimeout) { clearTimeout(this.pingTimeout); this.pingTimeout = setTimeout(() => this.handlePingTimeout(), 35000); } // Handle pong response if (message.type === 'pong') { return; // Don't emit pong messages } this.emit('message', message); } catch (error) { console.error('Failed to parse message:', error); } }); this.ws.on('close', () => { clearTimeout(connectionTimeout); this.isConnected = false; this.stopHeartbeat(); if (!this.manualDisconnect && !this.isReconnecting) { console.log('⚠ Disconnected from server'); this.emit('disconnected', { manual: false }); this.attemptReconnect(); } else if (this.manualDisconnect) { this.emit('disconnected', { manual: true }); } }); this.ws.on('error', (error) => { clearTimeout(connectionTimeout); if (error.code === 'ECONNREFUSED') { this.emit('serverUnavailable', { message: 'Server is not running', suggestion: 'Please start the server with: cc server' }); if (!this.isReconnecting) { reject(error); } } else { this.emit('error', error); if (!this.isReconnecting) { reject(error); } } }); // Connection timeout connectionTimeout = setTimeout(() => { if (!this.isConnected && !this.isReconnecting) { this.ws.close(); this.emit('connectionTimeout', { message: 'Connection timeout', suggestion: 'Check if the server is running on the correct port' }); reject(new Error('Connection timeout')); } }, 10000); } catch (error) { this.handleConnectionError(error); reject(error); } }); } /** * Attempt to reconnect with exponential backoff */ async attemptReconnect() { if (this.isReconnecting || this.manualDisconnect) return; this.isReconnecting = true; this.reconnectAttempts++; if (this.reconnectAttempts > this.maxReconnectAttempts) { this.emit('reconnectFailed', { attempts: this.reconnectAttempts }); console.error(`\nāŒ Failed to reconnect after ${this.reconnectAttempts} attempts`); console.log('šŸ’” Please check the server status and try again\n'); process.exit(1); return; } console.log(`šŸ”„ Reconnecting... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`); this.emit('reconnecting', { attempt: this.reconnectAttempts, maxAttempts: this.maxReconnectAttempts }); // Wait with exponential backoff await new Promise(resolve => setTimeout(resolve, this.reconnectDelay)); // Increase delay for next attempt this.reconnectDelay = Math.min(this.reconnectDelay * this.reconnectBackoffMultiplier, this.maxReconnectDelay); try { await this.connect(); } catch (error) { // Connection failed, will retry automatically } } /** * Send a message to the server */ send(data) { const message = typeof data === 'string' ? data : JSON.stringify(data); if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(message); } else { // Queue message for later this.messageQueue.push(data); this.emit('messageQueued', { queueLength: this.messageQueue.length }); console.log(`šŸ“‹ Message queued (${this.messageQueue.length} in queue)`); } } /** * Disconnect from server */ disconnect() { this.manualDisconnect = true; if (this.ws) { this.ws.close(); } } /** * Force reconnect */ forceReconnect() { if (this.ws) { this.ws.close(); } this.manualDisconnect = false; this.isReconnecting = false; return this.connect(); } /** * Get connection state */ getState() { return { isConnected: this.isConnected, isReconnecting: this.isReconnecting, reconnectAttempts: this.reconnectAttempts, queuedMessages: this.messageQueue.length }; } /** * Handle connection errors with user-friendly messages */ handleConnectionError(error) { const errorMessages = { 'ECONNREFUSED': { message: 'Cannot connect to Claude-Collab server', solution: "Run 'cc server' in another terminal to start the server" }, 'ENOTFOUND': { message: 'Server address not found', solution: 'Check your server URL or network connection' }, 'ETIMEDOUT': { message: 'Connection timed out', solution: 'Check if the server is running and accessible' }, 'ECONNRESET': { message: 'Connection was reset', solution: 'The server may have restarted. Try reconnecting.' } }; const errorInfo = errorMessages[error.code] || { message: `Connection error: ${error.message}`, solution: 'Check your network connection and server status' }; console.error(`\nāŒ ${errorInfo.message}`); console.log(`šŸ’” ${errorInfo.solution}\n`); } /** * Start heartbeat mechanism */ startHeartbeat() { this.stopHeartbeat(); // Clear any existing heartbeat // Send ping every 30 seconds this.pingInterval = setInterval(() => { if (this.isConnected && this.ws && this.ws.readyState === WebSocket.OPEN) { this.send({ type: 'ping' }); } }, 30000); // Expect pong within 35 seconds this.pingTimeout = setTimeout(() => this.handlePingTimeout(), 35000); } /** * Stop heartbeat mechanism */ stopHeartbeat() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } if (this.pingTimeout) { clearTimeout(this.pingTimeout); this.pingTimeout = null; } } /** * Handle ping timeout */ handlePingTimeout() { console.log('āš ļø Heartbeat timeout - connection may be stale'); if (this.ws) { this.ws.close(); } } } /** * Create connection with defaults */ async function createConnection(serverUrl = 'ws://localhost:8765') { const helper = new CLIConnectionHelper(serverUrl); await helper.connect(); return helper; } module.exports = { CLIConnectionHelper, createConnection }; //# sourceMappingURL=connection-helper.js.map