@in-the-loop-labs/pair-review 2.3.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/skills/review-model-guidance/SKILL.md +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/README.md +15 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +287 -14
- package/public/index.html +121 -57
- package/public/js/components/AIPanel.js +2 -1
- package/public/js/components/AdvancedConfigTab.js +2 -2
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +187 -28
- package/public/js/components/CouncilProgressModal.js +4 -7
- package/public/js/components/SplitButton.js +66 -1
- package/public/js/components/VoiceCentricConfigTab.js +2 -2
- package/public/js/index.js +274 -21
- package/public/js/pr.js +194 -5
- package/public/local.html +8 -1
- package/public/pr.html +17 -2
- package/src/ai/codex-provider.js +14 -2
- package/src/ai/copilot-provider.js +1 -10
- package/src/ai/cursor-agent-provider.js +1 -10
- package/src/ai/gemini-provider.js +8 -17
- package/src/chat/acp-bridge.js +442 -0
- package/src/chat/api-reference.js +539 -0
- package/src/chat/chat-providers.js +290 -0
- package/src/chat/claude-code-bridge.js +499 -0
- package/src/chat/codex-bridge.js +601 -0
- package/src/chat/pi-bridge.js +56 -3
- package/src/chat/prompt-builder.js +12 -11
- package/src/chat/session-manager.js +110 -29
- package/src/config.js +4 -2
- package/src/database.js +50 -2
- package/src/github/client.js +43 -0
- package/src/routes/chat.js +60 -27
- package/src/routes/config.js +24 -1
- package/src/routes/github-collections.js +126 -0
- package/src/routes/mcp.js +2 -1
- package/src/routes/pr.js +166 -2
- package/src/routes/reviews.js +2 -1
- package/src/routes/shared.js +70 -49
- package/src/server.js +27 -1
- package/src/utils/safe-parse-json.js +19 -0
- package/.pi/skills/pair-review-api/SKILL.md +0 -448
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code Bridge
|
|
4
|
+
*
|
|
5
|
+
* Manages a long-lived Claude Code CLI process in stream-json mode for
|
|
6
|
+
* interactive chat sessions. Communicates over stdin/stdout using NDJSON:
|
|
7
|
+
* - Sends JSON user messages on stdin
|
|
8
|
+
* - Receives NDJSON events (system, assistant, stream_event, result, etc.) on stdout
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the PiBridge / AcpBridge EventEmitter interface so all bridges
|
|
11
|
+
* can be used interchangeably.
|
|
12
|
+
*
|
|
13
|
+
* Emits high-level events: delta, complete, error, tool_use, status, ready, close, session.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { EventEmitter } = require('events');
|
|
17
|
+
const { spawn } = require('child_process');
|
|
18
|
+
const { createInterface } = require('readline');
|
|
19
|
+
const crypto = require('crypto');
|
|
20
|
+
const logger = require('../utils/logger');
|
|
21
|
+
const { quoteShellArgs } = require('../ai/provider');
|
|
22
|
+
|
|
23
|
+
const CLAUDE_CHAT_TOOLS = 'Read,Bash,Grep,Glob,Agent';
|
|
24
|
+
|
|
25
|
+
// Default dependencies (overridable for testing)
|
|
26
|
+
const defaults = {
|
|
27
|
+
spawn,
|
|
28
|
+
createInterface,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
class ClaudeCodeBridge extends EventEmitter {
|
|
32
|
+
/**
|
|
33
|
+
* @param {Object} options
|
|
34
|
+
* @param {string} [options.model] - Model ID (e.g., 'claude-sonnet-4-6')
|
|
35
|
+
* @param {string} [options.cwd] - Working directory for Claude process
|
|
36
|
+
* @param {string} [options.systemPrompt] - System prompt text (prepended to first message)
|
|
37
|
+
* @param {string} [options.claudeCommand] - Override binary (default: env PAIR_REVIEW_CLAUDE_CMD or 'claude')
|
|
38
|
+
* @param {Object} [options.env] - Extra env vars for subprocess
|
|
39
|
+
* @param {boolean} [options.useShell] - Use shell mode for multi-word commands
|
|
40
|
+
* @param {string} [options.resumeSessionId] - Session ID for resumption
|
|
41
|
+
* @param {Object} [options._deps] - { spawn, createInterface } for testing
|
|
42
|
+
*/
|
|
43
|
+
constructor(options = {}) {
|
|
44
|
+
super();
|
|
45
|
+
this.model = options.model || null;
|
|
46
|
+
this.cwd = options.cwd || process.cwd();
|
|
47
|
+
this.systemPrompt = options.systemPrompt || null;
|
|
48
|
+
this.claudeCommand = options.claudeCommand || process.env.PAIR_REVIEW_CLAUDE_CMD || 'claude';
|
|
49
|
+
this.env = options.env || {};
|
|
50
|
+
this.useShell = options.useShell || false;
|
|
51
|
+
this.resumeSessionId = options.resumeSessionId || null;
|
|
52
|
+
|
|
53
|
+
this._deps = { ...defaults, ...options._deps };
|
|
54
|
+
this._process = null;
|
|
55
|
+
this._readline = null;
|
|
56
|
+
this._sessionId = null;
|
|
57
|
+
this._ready = false;
|
|
58
|
+
this._closing = false;
|
|
59
|
+
this._accumulatedText = '';
|
|
60
|
+
this._inMessage = false;
|
|
61
|
+
this._firstMessage = !options.resumeSessionId;
|
|
62
|
+
this._activeTools = new Map();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Spawn the Claude CLI subprocess in stream-json mode.
|
|
67
|
+
*
|
|
68
|
+
* With --input-format stream-json, the CLI does NOT emit system/init until
|
|
69
|
+
* the first user message is sent on stdin. So start() spawns the process,
|
|
70
|
+
* wires up I/O, and marks the bridge as ready immediately. The session ID
|
|
71
|
+
* is captured later when system/init arrives with the first response.
|
|
72
|
+
*
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
async start() {
|
|
76
|
+
if (this._process) {
|
|
77
|
+
throw new Error('ClaudeCodeBridge already started');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const deps = this._deps;
|
|
81
|
+
const command = this.claudeCommand;
|
|
82
|
+
const args = this._buildArgs();
|
|
83
|
+
const useShell = this.useShell;
|
|
84
|
+
|
|
85
|
+
// For multi-word commands (e.g. "devx claude"), use shell mode.
|
|
86
|
+
// quoteShellArgs handles args containing shell-sensitive chars like {}.
|
|
87
|
+
const spawnCmd = useShell ? `${command} ${quoteShellArgs(args).join(' ')}` : command;
|
|
88
|
+
const spawnArgs = useShell ? [] : args;
|
|
89
|
+
|
|
90
|
+
logger.info(`[ClaudeCodeBridge] Starting: ${command} ${args.join(' ')}`);
|
|
91
|
+
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
// Remove CLAUDECODE env var to avoid "nested session" error
|
|
94
|
+
const env = { ...process.env, ...this.env };
|
|
95
|
+
delete env.CLAUDECODE;
|
|
96
|
+
|
|
97
|
+
const proc = deps.spawn(spawnCmd, spawnArgs, {
|
|
98
|
+
cwd: this.cwd,
|
|
99
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
100
|
+
env,
|
|
101
|
+
shell: useShell,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this._process = proc;
|
|
105
|
+
let spawned = false;
|
|
106
|
+
|
|
107
|
+
// Handle spawn error (e.g., ENOENT)
|
|
108
|
+
proc.on('error', (err) => {
|
|
109
|
+
if (!spawned) {
|
|
110
|
+
this._process = null;
|
|
111
|
+
reject(new Error(`Failed to start Claude CLI: ${err.message}`));
|
|
112
|
+
} else {
|
|
113
|
+
logger.error(`[ClaudeCodeBridge] Process error: ${err.message}`);
|
|
114
|
+
this.emit('error', { error: err });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Handle process exit
|
|
119
|
+
proc.on('close', (code, signal) => {
|
|
120
|
+
this._ready = false;
|
|
121
|
+
this._process = null;
|
|
122
|
+
|
|
123
|
+
if (!spawned) {
|
|
124
|
+
reject(new Error(`Claude CLI exited before ready (code=${code}, signal=${signal})`));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!this._closing) {
|
|
129
|
+
logger.warn(`[ClaudeCodeBridge] Process exited unexpectedly (code=${code}, signal=${signal})`);
|
|
130
|
+
this.emit('error', { error: new Error(`Claude CLI exited (code=${code}, signal=${signal})`) });
|
|
131
|
+
} else {
|
|
132
|
+
logger.info(`[ClaudeCodeBridge] Process exited (code=${code}, signal=${signal})`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.emit('close');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Collect stderr for diagnostics
|
|
139
|
+
proc.stderr.on('data', (data) => {
|
|
140
|
+
const text = data.toString().trim();
|
|
141
|
+
if (text) {
|
|
142
|
+
logger.debug(`[ClaudeCodeBridge] stderr: ${text}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Handle stdin errors (e.g., EPIPE if process dies)
|
|
147
|
+
proc.stdin.on('error', (err) => {
|
|
148
|
+
logger.error(`[ClaudeCodeBridge] stdin error: ${err.message}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Set up line-by-line parsing of stdout for NDJSON
|
|
152
|
+
this._readline = deps.createInterface({
|
|
153
|
+
input: proc.stdout,
|
|
154
|
+
crlfDelay: Infinity,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this._readline.on('line', (line) => {
|
|
158
|
+
if (!line.trim()) return;
|
|
159
|
+
|
|
160
|
+
let msg;
|
|
161
|
+
try {
|
|
162
|
+
msg = JSON.parse(line);
|
|
163
|
+
} catch {
|
|
164
|
+
logger.debug(`[ClaudeCodeBridge] Ignoring unparseable line: ${line.substring(0, 100)}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this._handleMessage(msg);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// The CLI with --input-format stream-json doesn't emit anything until
|
|
172
|
+
// the first user message is sent. Mark ready after a tick so that
|
|
173
|
+
// synchronous spawn errors (ENOENT) can reject the promise first.
|
|
174
|
+
setImmediate(() => {
|
|
175
|
+
if (!this._process) return; // error already fired and cleaned up
|
|
176
|
+
spawned = true;
|
|
177
|
+
this._ready = true;
|
|
178
|
+
logger.info(`[ClaudeCodeBridge] Spawned (PID ${proc.pid}), ready for messages`);
|
|
179
|
+
this.emit('ready');
|
|
180
|
+
resolve();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Send a user message to the Claude CLI process.
|
|
187
|
+
* @param {string} content - The message text
|
|
188
|
+
*/
|
|
189
|
+
async sendMessage(content) {
|
|
190
|
+
if (!this.isReady()) {
|
|
191
|
+
throw new Error('ClaudeCodeBridge is not ready');
|
|
192
|
+
}
|
|
193
|
+
if (this.isBusy()) {
|
|
194
|
+
throw new Error('ClaudeCodeBridge is busy');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this._accumulatedText = '';
|
|
198
|
+
this._inMessage = true;
|
|
199
|
+
|
|
200
|
+
let messageContent = content;
|
|
201
|
+
const isFirst = this.systemPrompt && this._firstMessage;
|
|
202
|
+
if (isFirst) {
|
|
203
|
+
messageContent = this.systemPrompt + '\n\n' + content;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
logger.debug(`[ClaudeCodeBridge] Sending prompt (${messageContent.length} chars): ${messageContent.substring(0, 100)}${messageContent.length > 100 ? '...' : ''}`);
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
this._write({
|
|
210
|
+
type: 'user',
|
|
211
|
+
message: { role: 'user', content: messageContent },
|
|
212
|
+
session_id: this._sessionId || '',
|
|
213
|
+
parent_tool_use_id: null,
|
|
214
|
+
});
|
|
215
|
+
if (isFirst) this._firstMessage = false;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
this._inMessage = false;
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Abort the current operation by sending an interrupt control request.
|
|
224
|
+
*/
|
|
225
|
+
abort() {
|
|
226
|
+
if (!this.isReady()) return;
|
|
227
|
+
|
|
228
|
+
if (this._sessionId) {
|
|
229
|
+
logger.debug('[ClaudeCodeBridge] Sending interrupt');
|
|
230
|
+
this._write({
|
|
231
|
+
type: 'control_request',
|
|
232
|
+
request: { subtype: 'interrupt' },
|
|
233
|
+
request_id: crypto.randomUUID(),
|
|
234
|
+
});
|
|
235
|
+
} else if (this._process) {
|
|
236
|
+
logger.debug('[ClaudeCodeBridge] No session ID yet, sending SIGTERM');
|
|
237
|
+
this._process.kill('SIGTERM');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Gracefully shut down the Claude CLI process.
|
|
243
|
+
* @returns {Promise<void>}
|
|
244
|
+
*/
|
|
245
|
+
async close() {
|
|
246
|
+
if (!this._process) return;
|
|
247
|
+
|
|
248
|
+
this._closing = true;
|
|
249
|
+
this._activeTools.clear();
|
|
250
|
+
this.removeAllListeners();
|
|
251
|
+
|
|
252
|
+
// Close readline if it exists
|
|
253
|
+
if (this._readline) {
|
|
254
|
+
this._readline.close();
|
|
255
|
+
this._readline = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return new Promise((resolve) => {
|
|
259
|
+
const proc = this._process;
|
|
260
|
+
if (!proc) {
|
|
261
|
+
resolve();
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// If process already exited, 'close' won't fire again — resolve immediately
|
|
266
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
267
|
+
this._process = null;
|
|
268
|
+
resolve();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Give the process a moment to exit gracefully, then force kill
|
|
273
|
+
const killTimeout = setTimeout(() => {
|
|
274
|
+
if (this._process) {
|
|
275
|
+
logger.warn('[ClaudeCodeBridge] Force killing process');
|
|
276
|
+
this._process.kill('SIGKILL');
|
|
277
|
+
}
|
|
278
|
+
}, 3000);
|
|
279
|
+
|
|
280
|
+
const onClose = () => {
|
|
281
|
+
clearTimeout(killTimeout);
|
|
282
|
+
this._process = null;
|
|
283
|
+
resolve();
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
proc.once('close', onClose);
|
|
287
|
+
|
|
288
|
+
// Close stdin then send SIGTERM
|
|
289
|
+
try {
|
|
290
|
+
proc.stdin.end();
|
|
291
|
+
} catch {
|
|
292
|
+
// stdin may already be closed
|
|
293
|
+
}
|
|
294
|
+
proc.kill('SIGTERM');
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if the CLI process is alive and ready.
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
isReady() {
|
|
303
|
+
return this._ready && this._process !== null && !this._closing;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if the bridge is currently processing a message.
|
|
308
|
+
* @returns {boolean}
|
|
309
|
+
*/
|
|
310
|
+
isBusy() {
|
|
311
|
+
return this._inMessage;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Private methods
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Build CLI arguments for the Claude process.
|
|
320
|
+
* @returns {string[]}
|
|
321
|
+
*/
|
|
322
|
+
_buildArgs() {
|
|
323
|
+
const args = [
|
|
324
|
+
'-p', '',
|
|
325
|
+
'--output-format', 'stream-json',
|
|
326
|
+
'--input-format', 'stream-json',
|
|
327
|
+
'--verbose',
|
|
328
|
+
'--include-partial-messages',
|
|
329
|
+
'--allowedTools', CLAUDE_CHAT_TOOLS,
|
|
330
|
+
'--settings', '{"disableAllHooks":true}',
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
if (this.resumeSessionId) {
|
|
334
|
+
args.unshift('--resume', this.resumeSessionId);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (this.model) {
|
|
338
|
+
args.push('--model', this.model);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return args;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Write a JSON object as NDJSON to the process stdin.
|
|
346
|
+
* @param {Object} obj - The object to serialize and write
|
|
347
|
+
*/
|
|
348
|
+
_write(obj) {
|
|
349
|
+
if (!this._process || !this._process.stdin.writable) {
|
|
350
|
+
throw new Error('Claude CLI process stdin is not writable');
|
|
351
|
+
}
|
|
352
|
+
this._process.stdin.write(JSON.stringify(obj) + '\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Route an incoming NDJSON message to the appropriate handler.
|
|
357
|
+
* @param {Object} msg - Parsed JSON message from Claude CLI stdout
|
|
358
|
+
*/
|
|
359
|
+
_handleMessage(msg) {
|
|
360
|
+
const type = msg.type;
|
|
361
|
+
const subtype = msg.subtype;
|
|
362
|
+
|
|
363
|
+
switch (type) {
|
|
364
|
+
case 'system':
|
|
365
|
+
this._handleSystemMessage(msg, subtype);
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case 'assistant':
|
|
369
|
+
this.emit('status', { status: 'working' });
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case 'stream_event':
|
|
373
|
+
this._handleStreamEvent(msg);
|
|
374
|
+
break;
|
|
375
|
+
|
|
376
|
+
case 'user':
|
|
377
|
+
// tool_result content blocks signal tool execution completed
|
|
378
|
+
if (Array.isArray(msg.content)) {
|
|
379
|
+
for (const block of msg.content) {
|
|
380
|
+
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
381
|
+
const toolName = this._activeTools.get(block.tool_use_id) || null;
|
|
382
|
+
this._activeTools.delete(block.tool_use_id);
|
|
383
|
+
this.emit('tool_use', {
|
|
384
|
+
toolCallId: block.tool_use_id,
|
|
385
|
+
toolName,
|
|
386
|
+
status: 'end',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
|
|
393
|
+
case 'tool_progress':
|
|
394
|
+
this.emit('tool_use', {
|
|
395
|
+
toolCallId: msg.tool_use_id,
|
|
396
|
+
toolName: msg.tool_name,
|
|
397
|
+
status: 'update',
|
|
398
|
+
});
|
|
399
|
+
break;
|
|
400
|
+
|
|
401
|
+
case 'result':
|
|
402
|
+
this._handleResult(msg, subtype);
|
|
403
|
+
break;
|
|
404
|
+
|
|
405
|
+
case 'keep_alive':
|
|
406
|
+
// Ignore keep-alive pings
|
|
407
|
+
break;
|
|
408
|
+
|
|
409
|
+
default:
|
|
410
|
+
logger.debug(`[ClaudeCodeBridge] Unhandled message type: ${type}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Handle system messages (init, status, etc.).
|
|
416
|
+
* @param {Object} msg - The system message
|
|
417
|
+
* @param {string} subtype - The system message subtype
|
|
418
|
+
*/
|
|
419
|
+
_handleSystemMessage(msg, subtype) {
|
|
420
|
+
switch (subtype) {
|
|
421
|
+
case 'init':
|
|
422
|
+
// The CLI emits system/init at the start of every response turn.
|
|
423
|
+
// Only capture and emit on the first one.
|
|
424
|
+
if (!this._sessionId) {
|
|
425
|
+
this._sessionId = msg.session_id || null;
|
|
426
|
+
logger.info(`[ClaudeCodeBridge] Session initialized (session ${this._sessionId})`);
|
|
427
|
+
this.emit('session', { sessionId: this._sessionId });
|
|
428
|
+
}
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case 'status':
|
|
432
|
+
this.emit('status', { status: 'working' });
|
|
433
|
+
break;
|
|
434
|
+
|
|
435
|
+
default:
|
|
436
|
+
logger.debug(`[ClaudeCodeBridge] Unhandled system subtype: ${subtype}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Handle stream_event messages (content deltas, tool use starts, etc.).
|
|
442
|
+
* @param {Object} msg - The stream_event message
|
|
443
|
+
*/
|
|
444
|
+
_handleStreamEvent(msg) {
|
|
445
|
+
const event = msg.event;
|
|
446
|
+
if (!event) return;
|
|
447
|
+
|
|
448
|
+
switch (event.type) {
|
|
449
|
+
case 'content_block_delta':
|
|
450
|
+
if (event.delta && event.delta.type === 'text_delta') {
|
|
451
|
+
const text = event.delta.text || '';
|
|
452
|
+
if (text) {
|
|
453
|
+
this._accumulatedText += text;
|
|
454
|
+
this.emit('delta', { text });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
break;
|
|
458
|
+
|
|
459
|
+
case 'content_block_start':
|
|
460
|
+
if (event.content_block && event.content_block.type === 'tool_use') {
|
|
461
|
+
const { id, name } = event.content_block;
|
|
462
|
+
this._activeTools.set(id, name);
|
|
463
|
+
this.emit('tool_use', {
|
|
464
|
+
toolCallId: id,
|
|
465
|
+
toolName: name,
|
|
466
|
+
status: 'start',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
default:
|
|
472
|
+
logger.debug(`[ClaudeCodeBridge] Unhandled stream_event type: ${event.type}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Handle result messages (success or error).
|
|
478
|
+
* @param {Object} msg - The result message
|
|
479
|
+
* @param {string} subtype - The result subtype
|
|
480
|
+
*/
|
|
481
|
+
_handleResult(msg, subtype) {
|
|
482
|
+
this._inMessage = false;
|
|
483
|
+
const fullText = this._accumulatedText;
|
|
484
|
+
this._accumulatedText = '';
|
|
485
|
+
this._activeTools.clear();
|
|
486
|
+
|
|
487
|
+
if (subtype && subtype !== 'success') {
|
|
488
|
+
const errorMessage = (Array.isArray(msg.errors) && msg.errors.length)
|
|
489
|
+
? msg.errors.join('\n')
|
|
490
|
+
: subtype;
|
|
491
|
+
logger.error(`[ClaudeCodeBridge] Result error: ${errorMessage}`);
|
|
492
|
+
this.emit('error', { error: new Error(errorMessage) });
|
|
493
|
+
} else {
|
|
494
|
+
this.emit('complete', { fullText });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = ClaudeCodeBridge;
|