/** * ClaudeCliProtocol — Claude Code CLI stream-json 交互协议 * * 使用 `claude --input-format stream-json --output-format stream-json --verbose * --include-partial-messages --dangerously-skip-permissions` 与 Claude CLI 通信。 * * 持久化进程:启动一次,跨多次 sendPrompt 调用复用。 * * 与 AcpProtocol 的区别: * - 不使用 JSON-RPC(Claude CLI 自有协议) * - 输入:{"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}} * - 输出:NDJSON 流 * - {type:"stream_event", event:{type:"content_block_delta", delta:{type:"text_delta",...}}} * - {type:"assistant", message:{...}} (带 --include-partial-messages) * - {type:"result", result:"...", stop_reason:"end_turn"} * - initialize() / newSession() 为 no-op * * 事件兼容 AcpProtocol 接口: * - emit('session_update', { update: { sessionUpdate, content: { text } } }) * - emit('acp_event', { type, method, params, timestamp }) * - emit('error', Error) */ const { EventEmitter } = require('events'); const { spawn } = require('child_process'); class ClaudeCliProtocol extends EventEmitter { constructor(agentInfo = {}, options = {}) { super(); this.agentInfo = agentInfo; this.options = options; this.isConnected = false; this.sessionId = null; this._child = null; this._workingDir = null; this._buffer = ''; this._promptResolve = null; this._promptReject = null; } /** * 连接:启动 Claude CLI 持久进程 * transport 参数被忽略:本协议自行管理子进程 */ async connect(transport, workingDir) { this._workingDir = workingDir; const args = [ '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose', '--include-partial-messages', '--dangerously-skip-permissions', ]; const processEnv = { ...process.env }; delete processEnv.NODE_OPTIONS; delete processEnv.ELECTRON_RUN_AS_NODE; delete processEnv.CLAUDECODE; console.log(`[ClaudeCliProtocol] 启动: claude ${args.join(' ')}`); this._child = spawn('claude', args, { stdio: ['pipe', 'pipe', 'pipe'], env: processEnv, cwd: workingDir, }); // stdout → NDJSON 解析 this._child.stdout.on('data', (data) => { this._buffer += data.toString(); const lines = this._buffer.split('\n'); this._buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim()) continue; try { const event = JSON.parse(line); this._handleStreamEvent(event); } catch (e) { // 非 JSON 行,忽略 } } }); // stderr 转发 this._child.stderr.on('data', (data) => { const msg = data.toString().trim(); if (msg) { console.log('[ClaudeCliProtocol] stderr:', msg.substring(0, 300)); this.emit('stderr', msg); } }); // 进程退出 this._child.on('exit', (code, signal) => { console.log(`[ClaudeCliProtocol] 进程退出: code=${code} signal=${signal}`); this.isConnected = false; this._child = null; if (this._promptReject) { this._promptReject(new Error(`Claude CLI 进程退出: code=${code} signal=${signal}`)); this._promptResolve = null; this._promptReject = null; } this.emit('disconnected', { code, signal }); }); this._child.on('error', (err) => { console.error('[ClaudeCliProtocol] 进程错误:', err.message); this.emit('error', err); }); // 等待进程启动 await new Promise((resolve, reject) => { this._child.on('error', reject); setTimeout(resolve, 500); }); this.isConnected = true; console.log('[ClaudeCliProtocol] 进程已启动'); } /** * Claude CLI 不需要 JSON-RPC initialize — no-op */ async initialize() { console.log('[ClaudeCliProtocol] skip initialize (Claude CLI 自有协议)'); return { protocolVersion: 1 }; } /** * Claude CLI 不需要 session/new — no-op */ async newSession(options = {}) { this._workingDir = options.cwd || this._workingDir || '.'; this.sessionId = `claude-cli-${Date.now()}`; console.log('[ClaudeCliProtocol] newSession (virtual):', this.sessionId); return { sessionId: this.sessionId }; } /** * 发送 prompt 到 Claude CLI * * 写入 stream-json 格式消息到 stdin: * {"type":"user","message":{"role":"user","content":[{"type":"text","text":"..."}]}} * * 等待 type=result 消息后 resolve。 * * @param {string} text - prompt 文本 * @returns {Promise} */ sendPrompt(text) { return new Promise((resolve, reject) => { if (!this._child || !this.isConnected) { return reject(new Error('[ClaudeCliProtocol] 进程未启动')); } this._promptResolve = resolve; this._promptReject = reject; // Claude CLI stream-json 输入格式 const message = { type: 'user', message: { role: 'user', content: [{ type: 'text', text }], }, }; const jsonLine = JSON.stringify(message) + '\n'; console.log(`[ClaudeCliProtocol] 发送 prompt (${text.length} chars)`); try { this._child.stdin.write(jsonLine); } catch (e) { this._promptResolve = null; this._promptReject = null; reject(new Error(`[ClaudeCliProtocol] stdin 写入失败: ${e.message}`)); } }); } /** * 处理 Claude CLI stream-json 输出事件 * * 事件类型: * - {type:"system"} — 系统初始化 * - {type:"stream_event", event:{type:"content_block_delta", delta:{...}}} — 流式增量 * - {type:"assistant", message:{content:[{type:"text",text:"..."}]}} — 完整/部分消息 * - {type:"result", result:"...", stop_reason:"end_turn"} — 最终结果 * * @private */ _handleStreamEvent(event) { switch (event.type) { case 'system': this.sessionId = event.session_id || this.sessionId; this.emit('acp_event', { type: 'notification', method: 'system/init', params: event, timestamp: Date.now(), }); break; case 'stream_event': this._handleInnerStreamEvent(event.event, event.session_id); break; case 'assistant': // 带 --include-partial-messages 时,每次 API 响应更新都发送完整 assistant 消息 // 仅提取增量文本(由 stream_event 处理),这里用于工具检测 if (event.message?.content) { for (const block of event.message.content) { if (block.type === 'text' && block.text) { // assistant 消息的完整文本(包含已流式发送的内容) // 不再 emit session_update 避免重复,stream_event 已处理增量 } } } break; case 'result': this.sessionId = event.session_id || this.sessionId; this.emit('acp_event', { type: 'notification', method: 'session/result', params: { result: event.result, duration_ms: event.duration_ms, cost_usd: event.total_cost_usd, session_id: event.session_id, stop_reason: event.stop_reason, }, timestamp: Date.now(), }); if (this._promptResolve) { this._promptResolve(); this._promptResolve = null; this._promptReject = null; } break; default: this.emit('acp_event', { type: 'notification', method: `claude/${event.type}`, params: event, timestamp: Date.now(), }); } } /** * 处理 stream_event 内层事件(Anthropic Streaming API 格式) * * event.type: * - message_start — 消息开始 * - content_block_start — 内容块开始(text / thinking / tool_use) * - content_block_delta — 增量内容 * - delta.type: "text_delta" → emit session_update * - delta.type: "thinking_delta" → emit session_update (thought) * - content_block_stop — 内容块结束 * - message_delta — 消息级更新(stop_reason 等) * - message_stop — 消息结束 * * @private */ _handleInnerStreamEvent(innerEvent, sessionId) { if (!innerEvent) return; switch (innerEvent.type) { case 'content_block_delta': if (innerEvent.delta?.type === 'text_delta' && innerEvent.delta.text) { this.emit('session_update', { update: { sessionUpdate: 'agent_message_chunk', content: { text: innerEvent.delta.text }, }, }); } else if (innerEvent.delta?.type === 'thinking_delta' && innerEvent.delta.thinking) { this.emit('session_update', { update: { sessionUpdate: 'agent_thought_chunk', content: { text: innerEvent.delta.thinking }, }, }); } break; case 'content_block_start': // 可用于检测 tool_use 块开始 if (innerEvent.content_block?.type === 'tool_use') { this.emit('acp_event', { type: 'notification', method: 'claude/tool_use_start', params: { tool_use_id: innerEvent.content_block.id, name: innerEvent.content_block.name, }, timestamp: Date.now(), }); } break; case 'message_start': case 'content_block_stop': case 'message_delta': case 'message_stop': // 透传为 acp_event this.emit('acp_event', { type: 'notification', method: `claude/stream/${innerEvent.type}`, params: innerEvent, timestamp: Date.now(), }); break; } } /** * 断开连接 — 终止 Claude CLI 进程 */ disconnect() { if (this._child) { try { this._child.stdin.end(); } catch { } try { this._child.kill('SIGTERM'); } catch { } this._child = null; } this.isConnected = false; this.sessionId = null; } } module.exports = { ClaudeCliProtocol };