UNPKG

@mrtkrcm/acp-claude-code

Version:

ACP (Agent Client Protocol) bridge for Claude Code

258 lines 11.2 kB
import { Writable, Readable } from "node:stream"; import { ClaudeACPAgent } from "./agent.js"; import { DiagnosticSystem } from "./diagnostics.js"; import { createLogger } from "./logger.js"; import { AgentSideConnection } from "./protocol.js"; export async function main() { // Check for diagnostic mode FIRST - before any setup if (process.argv.includes('--diagnose')) { await runDiagnostics(); return; } // Check for setup mode if (process.argv.includes('--setup')) { await runSetup(); return; } // Check for test mode if (process.argv.includes('--test')) { await runTest(); return; } // Check for reset permissions if (process.argv.includes('--reset-permissions')) { await resetPermissions(); return; } // Validate environment early (only for normal operation) await validateEnvironment(); // Add process debugging for Zed startup issues process.on('disconnect', () => { console.error('[ACP-Claude] Parent process disconnected'); }); process.on('SIGPIPE', () => { console.error('[ACP-Claude] SIGPIPE received - parent closed pipe'); process.exit(1); }); // Initialize centralized logging const logger = createLogger('ACP-Bridge'); logger.writeStartupMessage(); logger.info("Starting Claude Code ACP Bridge..."); // Run pre-flight checks await performPreflightChecks(logger); try { // Prevent any accidental stdout writes that could corrupt the protocol console.log = (...args) => { console.error("[WARNING] console.log intercepted:", ...args); }; logger.debug("Creating ACP connection via stdio..."); // Convert Node.js streams to Web Streams // IMPORTANT: stdout is for sending to client, stdin is for receiving from client const outputStream = Writable.toWeb(process.stdout); const inputStream = Readable.toWeb(process.stdin); // We're implementing an Agent, so we use AgentSideConnection // First parameter is output (to client), second is input (from client) let agent = null; let connection = null; connection = new AgentSideConnection((client) => { logger.debug("Creating ClaudeACPAgent with client"); agent = new ClaudeACPAgent(client); return agent; }, outputStream, // WritableStream for sending data to client (stdout) inputStream, // ReadableStream for receiving data from client (stdin) { fileSystemEnabled: true, // Enable direct file system operations }); // Log connection creation success logger.debug("ACP Connection created successfully"); logger.info("Claude Code ACP Bridge is running"); // Keep the process alive process.stdin.resume(); // Handle graceful shutdown process.on("SIGINT", async () => { logger.info("Received SIGINT, shutting down..."); await gracefulShutdown(agent, connection, logger); }); process.on("SIGTERM", async () => { logger.info("Received SIGTERM, shutting down..."); await gracefulShutdown(agent, connection, logger); }); // Handle uncaught errors process.on("uncaughtException", (error) => { logger.error(`[FATAL] Uncaught exception: ${error.message}`, { stack: error.stack }); logger.destroy(); process.exit(1); }); process.on("unhandledRejection", (reason, promise) => { logger.error(`[FATAL] Unhandled rejection`, { promise: String(promise), reason: String(reason) }); logger.destroy(); process.exit(1); }); } catch (error) { logger.error(`[FATAL] Error starting ACP bridge: ${error}`); logger.destroy(); process.exit(1); } } async function gracefulShutdown(agent, connection, logger) { try { logger.info("Starting graceful shutdown..."); // Set a timeout for shutdown operations const shutdownTimeout = new Promise((_, reject) => { setTimeout(() => reject(new Error("Shutdown timeout")), 10000); }); const shutdownPromise = async () => { // Cleanup agent first if (agent && typeof agent.destroy === 'function') { await agent.destroy(); } // Cleanup connection if (connection && typeof connection.destroy === 'function') { connection.destroy(); } logger.destroy(); }; // Race between shutdown and timeout await Promise.race([shutdownPromise(), shutdownTimeout]); logger.info("Graceful shutdown completed"); process.exit(0); } catch (error) { logger.error("Error during graceful shutdown:", error); process.exit(1); } } async function runDiagnostics() { console.error("🔍 Running ACP-Claude-Code Diagnostics...\n"); try { const report = await DiagnosticSystem.generateReport(); const formattedReport = DiagnosticSystem.formatReport(report); console.error(formattedReport); // Exit with appropriate code process.exit(report.compatible ? 0 : 1); } catch (error) { console.error("ERROR: Failed to generate diagnostic report:", error); process.exit(1); } } async function performPreflightChecks(logger) { try { const report = await DiagnosticSystem.generateReport(); // Log critical issues that could prevent operation const criticalIssues = report.issues.filter(issue => issue.level === 'error' && ['EXECUTABLE_NOT_FOUND', 'NOT_AUTHENTICATED', 'NODE_VERSION'].includes(issue.code || '')); if (criticalIssues.length > 0) { console.error("CRITICAL: Issues detected that may prevent operation:"); criticalIssues.forEach(issue => { console.error(` ERROR: ${issue.message}`); if (issue.solution) { console.error(` → Solution: ${issue.solution}`); } }); console.error("\nRun 'acp-claude-code --diagnose' for detailed analysis."); console.error("Attempting to continue anyway...\n"); } // Log warnings for non-optimal conditions const warnings = report.issues.filter(issue => issue.level === 'warning'); if (warnings.length > 0 && process.env.ACP_DEBUG === "true") { logger.debug(`Found ${warnings.length} non-critical warnings. Run --diagnose for details.`); } logger.debug(`Compatibility score: ${report.score}/100`); } catch (error) { logger.warn(`Preflight check failed: ${error}`); // Continue anyway - don't block on diagnostic failures } } async function validateEnvironment() { const errors = []; // Check Node.js version const nodeVersion = parseInt(process.version.slice(1), 10); if (nodeVersion < 18) { errors.push(`Node.js ${process.version} is too old. Requires Node.js 18+`); } // Validate environment variables const maxTurns = process.env.ACP_MAX_TURNS; if (maxTurns && (!/^\d+$/.test(maxTurns) || parseInt(maxTurns, 10) < 0)) { errors.push(`Invalid ACP_MAX_TURNS: "${maxTurns}" must be a non-negative integer`); } const permissionMode = process.env.ACP_PERMISSION_MODE; if (permissionMode && !["default", "acceptEdits", "bypassPermissions", "plan"].includes(permissionMode)) { errors.push(`Invalid ACP_PERMISSION_MODE: "${permissionMode}"`); } if (errors.length > 0) { console.error("ERROR: Environment validation failed:"); errors.forEach(error => console.error(` • ${error}`)); process.exit(1); } } async function runSetup() { console.error("SETUP: ACP-Claude-Code Setup Wizard\n"); try { const report = await DiagnosticSystem.generateReport(); console.error("SUCCESS: System Check:"); console.error(` Platform: ${report.platform.platform} (${report.platform.arch})`); console.error(` Node.js: ${report.platform.nodeVersion}`); console.error(` Claude Code: ${report.claudeCode.available ? 'Found' : 'Not Found'}`); console.error(` Authentication: ${report.claudeCode.authenticated ? 'Ready' : 'Required'}`); console.error(` Score: ${report.score}/100\n`); if (!report.claudeCode.available) { console.error("ERROR: Claude Code not found. Please install:"); console.error(" npm install -g @anthropic-ai/claude-code"); console.error(" OR set ACP_PATH_TO_CLAUDE_CODE_EXECUTABLE\n"); } if (!report.claudeCode.authenticated) { console.error("AUTH: Authentication required. Run:"); console.error(" claude setup-token\n"); } console.error("CONFIG: Recommended Zed configuration:"); console.error('{\n "agent_servers": {\n "claude-code": {\n "command": "npx",'); console.error(' "args": ["@mrtkrcm/acp-claude-code"],\n "env": {'); console.error(' "ACP_PERMISSION_MODE": "acceptEdits"\n }\n }\n }\n}'); process.exit(report.compatible ? 0 : 1); } catch (error) { console.error("ERROR: Setup failed:", error); process.exit(1); } } async function runTest() { console.error("TEST: Testing ACP-Claude-Code Connection\n"); try { const report = await DiagnosticSystem.generateReport(); const metrics = DiagnosticSystem.getSystemMetrics(); console.error("STATUS: System Status:"); console.error(` Memory: ${Math.round(metrics.memory.heapUsed / 1024 / 1024)}MB used`); console.error(` Uptime: ${Math.round(metrics.uptime)}s`); console.error(` Compatible: ${report.compatible ? 'Yes' : 'No'}`); if (report.claudeCode.available && report.claudeCode.authenticated) { console.error("SUCCESS: Connection test passed"); process.exit(0); } else { console.error("ERROR: Connection test failed"); process.exit(1); } } catch (error) { console.error("ERROR: Test failed:", error); process.exit(1); } } async function resetPermissions() { console.error("RESET: Resetting permission settings\n"); // For now, just show instructions since permissions are session-based console.error("Permission modes available:"); console.error(" • default - Ask for each operation"); console.error(" • acceptEdits - Auto-accept file edits"); console.error(" • bypassPermissions - Allow all operations"); console.error("\nSet via environment variable:"); console.error(" ACP_PERMISSION_MODE=acceptEdits"); console.error("\nOr use runtime markers:"); console.error(" [ACP:PERMISSION:ACCEPT_EDITS]"); process.exit(0); } export { ClaudeACPAgent }; //# sourceMappingURL=index.js.map