import { EventEmitter } from 'node:events'; import { spawn } from 'node:child_process'; import crossSpawn from 'cross-spawn'; const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn; class AcpClient extends EventEmitter { constructor({ command = process.env.HERMES_CLI_PATH || 'hermes acp', cwd = process.cwd(), env = process.env } = {}) { super(); const commandParts = command.trim().split(/\s+/); this.command = commandParts.shift() || 'hermes'; this.args = commandParts; this.cwd = cwd; this.env = env; this.process = null; this.nextId = 1; this.pending = new Map(); this.buffer = ''; this.requestHandlers = new Map(); this.initialized = false; } start() { if (this.process) { return; } this.process = spawnFunction(this.command, this.args, { cwd: this.cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...this.env }, }); this.process.stdout.on('data', (chunk) => this.handleData(chunk)); this.process.stderr.on('data', (chunk) => { const text = chunk.toString(); if (text.trim()) { this.emit('stderr', text); } }); this.process.on('error', (error) => this.rejectAll(error)); this.process.on('close', (code, signal) => { this.rejectAll(new Error(`hermes-acp exited with code ${code ?? 'null'}${signal ? ` signal ${signal}` : ''}`)); this.emit('close', { code, signal }); this.process = null; this.initialized = false; }); } async initialize() { if (this.initialized) { return; } this.start(); await this.request('initialize', { protocolVersion: 1, clientInfo: { name: 'CloudCLI', version: '1.0.0', }, capabilities: { fs: false, terminal: false, session: { requestPermission: true, }, }, }); this.initialized = true; this.notify('initialized', {}); } onRequest(method, handler) { this.requestHandlers.set(method, handler); } registerRequestHandler(method, handler) { const handlers = this.requestHandlers.get(method) || new Set(); handlers.add(handler); this.requestHandlers.set(method, handlers); return () => { handlers.delete(handler); if (handlers.size === 0) { this.requestHandlers.delete(method); } }; } request(method, params) { this.start(); const id = this.nextId; this.nextId += 1; const payload = { jsonrpc: '2.0', id, method, params }; return new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); this.writeMessage(payload); }); } notify(method, params) { this.start(); this.writeMessage({ jsonrpc: '2.0', method, params }); } writeMessage(payload) { if (!this.process || !this.process.stdin || this.process.stdin.destroyed) { throw new Error('hermes-acp process is not running'); } const line = `${JSON.stringify(payload)}\n`; this.process.stdin.write(line); } handleData(chunk) { this.buffer += chunk.toString(); while (this.buffer.length > 0) { if (this.buffer.startsWith('Content-Length:')) { const headerEnd = this.buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) { return; } const header = this.buffer.slice(0, headerEnd); const match = header.match(/Content-Length:\s*(\d+)/i); if (!match) { this.buffer = this.buffer.slice(headerEnd + 4); continue; } const length = Number(match[1]); const messageStart = headerEnd + 4; if (this.buffer.length < messageStart + length) { return; } const raw = this.buffer.slice(messageStart, messageStart + length); this.buffer = this.buffer.slice(messageStart + length); this.dispatchRaw(raw); continue; } const newlineIndex = this.buffer.indexOf('\n'); if (newlineIndex === -1) { return; } const raw = this.buffer.slice(0, newlineIndex).trim(); this.buffer = this.buffer.slice(newlineIndex + 1); if (raw) { this.dispatchRaw(raw); } } } dispatchRaw(raw) { let message; try { message = JSON.parse(raw); } catch (error) { this.emit('error', error); return; } void this.dispatchMessage(message); } async dispatchMessage(message) { if (Object.prototype.hasOwnProperty.call(message, 'id') && (message.result !== undefined || message.error !== undefined)) { const pending = this.pending.get(message.id); if (!pending) { return; } this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message || JSON.stringify(message.error))); } else { pending.resolve(message.result); } return; } if (Object.prototype.hasOwnProperty.call(message, 'id') && message.method) { const handler = this.requestHandlers.get(message.method); if (!handler) { this.writeMessage({ jsonrpc: '2.0', id: message.id, error: { code: -32601, message: `No handler for ${message.method}` }, }); return; } try { const result = handler instanceof Set ? await this.dispatchRequestHandlers(handler, message.params) : await handler(message.params); this.writeMessage({ jsonrpc: '2.0', id: message.id, result }); } catch (error) { this.writeMessage({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: error instanceof Error ? error.message : String(error) }, }); } return; } if (message.method) { this.emit(message.method, message.params); this.emit('notification', { method: message.method, params: message.params }); } } rejectAll(error) { for (const pending of this.pending.values()) { pending.reject(error); } this.pending.clear(); } async dispatchRequestHandlers(handlers, params) { let fallbackResult = null; let sawHandler = false; for (const handler of Array.from(handlers).reverse()) { sawHandler = true; const result = await handler(params); const outcome = result?.outcome?.outcome; if (outcome !== 'cancelled') { return result; } fallbackResult = result; } if (sawHandler && fallbackResult) { return fallbackResult; } return { outcome: { outcome: 'cancelled' } }; } close() { if (!this.process) { return; } this.process.kill('SIGTERM'); } } class HermesConnectionManager { constructor() { this.connections = new Map(); } async getConnection(cwd) { const key = cwd || process.cwd(); let connection = this.connections.get(key); if (!connection) { connection = new AcpClient({ cwd: key }); connection.on('close', () => { this.connections.delete(key); }); this.connections.set(key, connection); } await connection.initialize(); return connection; } closeAll() { for (const connection of this.connections.values()) { connection.close(); } this.connections.clear(); } } const hermesConnectionManager = new HermesConnectionManager(); export { AcpClient, HermesConnectionManager, hermesConnectionManager, };