agentgui 1.0.121 → 1.0.122
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/database.js +4 -4
- package/lib/claude-runner.js +814 -67
- package/package.json +1 -1
- package/server.js +38 -10
- package/static/index.html +12 -0
- package/static/js/client.js +85 -8
- package/static/js/streaming-renderer.js +333 -6
- package/WAVE6_FINAL_REPORT.md +0 -178
package/lib/claude-runner.js
CHANGED
|
@@ -1,51 +1,86 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
timeout = 300000,
|
|
9
|
-
print = true,
|
|
10
|
-
resumeSessionId = null,
|
|
11
|
-
systemPrompt = null,
|
|
12
|
-
onEvent = null
|
|
13
|
-
} = config;
|
|
3
|
+
/**
|
|
4
|
+
* Agent Framework
|
|
5
|
+
* Extensible registry for AI agent CLI integrations
|
|
6
|
+
* Supports multiple protocols: direct JSON streaming, ACP (JSON-RPC), etc.
|
|
7
|
+
*/
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
class AgentRunner {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.id = config.id;
|
|
12
|
+
this.name = config.name;
|
|
13
|
+
this.command = config.command;
|
|
14
|
+
this.protocol = config.protocol || 'direct'; // 'direct' | 'acp' | etc
|
|
15
|
+
this.buildArgs = config.buildArgs || this.defaultBuildArgs;
|
|
16
|
+
this.parseOutput = config.parseOutput || this.defaultParseOutput;
|
|
17
|
+
this.supportsStdin = config.supportsStdin ?? true;
|
|
18
|
+
this.supportedFeatures = config.supportedFeatures || [];
|
|
19
|
+
this.protocolHandler = config.protocolHandler || null;
|
|
20
|
+
this.requiresAdapter = config.requiresAdapter || false;
|
|
21
|
+
this.adapterCommand = config.adapterCommand || null;
|
|
22
|
+
this.adapterArgs = config.adapterArgs || [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
defaultBuildArgs(prompt, config) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
22
28
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
defaultParseOutput(line) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(line);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
async run(prompt, cwd, config = {}) {
|
|
38
|
+
if (this.protocol === 'acp' && this.protocolHandler) {
|
|
39
|
+
return this.runACP(prompt, cwd, config);
|
|
40
|
+
}
|
|
41
|
+
return this.runDirect(prompt, cwd, config);
|
|
42
|
+
}
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
async runDirect(prompt, cwd, config = {}) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const {
|
|
47
|
+
timeout = 300000,
|
|
48
|
+
onEvent = null,
|
|
49
|
+
onError = null
|
|
50
|
+
} = config;
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
const args = this.buildArgs(prompt, config);
|
|
53
|
+
const proc = spawn(this.command, args, { cwd });
|
|
54
|
+
|
|
55
|
+
let jsonBuffer = '';
|
|
56
|
+
const outputs = [];
|
|
57
|
+
let timedOut = false;
|
|
58
|
+
let sessionId = null;
|
|
59
|
+
|
|
60
|
+
const timeoutHandle = setTimeout(() => {
|
|
61
|
+
timedOut = true;
|
|
62
|
+
proc.kill();
|
|
63
|
+
reject(new Error(`${this.name} timeout after ${timeout}ms`));
|
|
64
|
+
}, timeout);
|
|
65
|
+
|
|
66
|
+
// Write to stdin if supported
|
|
67
|
+
if (this.supportsStdin) {
|
|
68
|
+
proc.stdin.write(prompt);
|
|
69
|
+
proc.stdin.end();
|
|
70
|
+
}
|
|
40
71
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
72
|
+
proc.stdout.on('data', (chunk) => {
|
|
73
|
+
if (timedOut) return;
|
|
74
|
+
|
|
75
|
+
jsonBuffer += chunk.toString();
|
|
76
|
+
const lines = jsonBuffer.split('\n');
|
|
77
|
+
jsonBuffer = lines.pop();
|
|
78
|
+
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (line.trim()) {
|
|
81
|
+
const parsed = this.parseOutput(line);
|
|
82
|
+
if (!parsed) continue;
|
|
44
83
|
|
|
45
|
-
for (const line of lines) {
|
|
46
|
-
if (line.trim()) {
|
|
47
|
-
try {
|
|
48
|
-
const parsed = JSON.parse(line);
|
|
49
84
|
outputs.push(parsed);
|
|
50
85
|
|
|
51
86
|
if (parsed.session_id) {
|
|
@@ -54,48 +89,760 @@ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code
|
|
|
54
89
|
|
|
55
90
|
if (onEvent) {
|
|
56
91
|
try { onEvent(parsed); } catch (e) {
|
|
57
|
-
console.error(`[
|
|
92
|
+
console.error(`[${this.id}] onEvent error: ${e.message}`);
|
|
58
93
|
}
|
|
59
94
|
}
|
|
60
|
-
} catch (e) {
|
|
61
|
-
console.error(`[claude-runner] JSON parse error on line: ${line.substring(0, 100)}`);
|
|
62
95
|
}
|
|
63
96
|
}
|
|
64
|
-
}
|
|
65
|
-
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
proc.stderr.on('data', (chunk) => {
|
|
100
|
+
const errorText = chunk.toString();
|
|
101
|
+
console.error(`[${this.id}] stderr:`, errorText);
|
|
102
|
+
if (onError) {
|
|
103
|
+
try { onError(errorText); } catch (e) {}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
proc.on('close', (code) => {
|
|
108
|
+
clearTimeout(timeoutHandle);
|
|
109
|
+
if (timedOut) return;
|
|
110
|
+
|
|
111
|
+
// Some agents return non-zero but still produce valid output
|
|
112
|
+
const success = code === 0 || (outputs.length > 0 && this.allowNonZeroExit);
|
|
113
|
+
|
|
114
|
+
if (success) {
|
|
115
|
+
if (jsonBuffer.trim()) {
|
|
116
|
+
const parsed = this.parseOutput(jsonBuffer);
|
|
117
|
+
if (parsed) {
|
|
118
|
+
outputs.push(parsed);
|
|
119
|
+
if (parsed.session_id) sessionId = parsed.session_id;
|
|
120
|
+
if (onEvent) {
|
|
121
|
+
try { onEvent(parsed); } catch (e) {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
resolve({ outputs, sessionId });
|
|
126
|
+
} else {
|
|
127
|
+
reject(new Error(`${this.name} exited with code ${code}`));
|
|
128
|
+
}
|
|
129
|
+
});
|
|
66
130
|
|
|
67
|
-
|
|
68
|
-
|
|
131
|
+
proc.on('error', (err) => {
|
|
132
|
+
clearTimeout(timeoutHandle);
|
|
133
|
+
reject(err);
|
|
134
|
+
});
|
|
69
135
|
});
|
|
136
|
+
}
|
|
70
137
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
138
|
+
async runACP(prompt, cwd, config = {}) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const {
|
|
141
|
+
timeout = 300000,
|
|
142
|
+
onEvent = null,
|
|
143
|
+
onError = null
|
|
144
|
+
} = config;
|
|
74
145
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
146
|
+
// Use adapter if required (e.g., for Claude Code via Zed adapter)
|
|
147
|
+
const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
|
|
148
|
+
const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : ['acp'];
|
|
149
|
+
const args = [...baseArgs];
|
|
150
|
+
|
|
151
|
+
const proc = spawn(cmd, args, { cwd });
|
|
152
|
+
|
|
153
|
+
const outputs = [];
|
|
154
|
+
let timedOut = false;
|
|
155
|
+
let sessionId = null;
|
|
156
|
+
let requestId = 0;
|
|
157
|
+
let initialized = false;
|
|
158
|
+
|
|
159
|
+
const timeoutHandle = setTimeout(() => {
|
|
160
|
+
timedOut = true;
|
|
161
|
+
proc.kill();
|
|
162
|
+
reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
|
|
163
|
+
}, timeout);
|
|
164
|
+
|
|
165
|
+
// ACP protocol handler
|
|
166
|
+
const handleMessage = (message) => {
|
|
167
|
+
// Normalize ACP message to common format
|
|
168
|
+
const normalized = this.protocolHandler(message, { sessionId, initialized });
|
|
169
|
+
if (!normalized) {
|
|
170
|
+
// Check for initialization response
|
|
171
|
+
if (message.id === 1 && message.result) {
|
|
172
|
+
initialized = true;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
outputs.push(normalized);
|
|
178
|
+
|
|
179
|
+
if (normalized.session_id) {
|
|
180
|
+
sessionId = normalized.session_id;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (onEvent) {
|
|
184
|
+
try { onEvent(normalized); } catch (e) {
|
|
185
|
+
console.error(`[${this.id}] onEvent error: ${e.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
let buffer = '';
|
|
191
|
+
proc.stdout.on('data', (chunk) => {
|
|
192
|
+
if (timedOut) return;
|
|
193
|
+
|
|
194
|
+
buffer += chunk.toString();
|
|
195
|
+
const lines = buffer.split('\n');
|
|
196
|
+
buffer = lines.pop();
|
|
197
|
+
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
if (line.trim()) {
|
|
200
|
+
try {
|
|
201
|
+
const message = JSON.parse(line);
|
|
202
|
+
handleMessage(message);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
|
|
83
205
|
}
|
|
84
|
-
} catch (e) {
|
|
85
|
-
console.error(`[claude-runner] Final JSON parse error: ${jsonBuffer.substring(0, 100)}`);
|
|
86
206
|
}
|
|
87
207
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
proc.stderr.on('data', (chunk) => {
|
|
211
|
+
const errorText = chunk.toString();
|
|
212
|
+
console.error(`[${this.id}] stderr:`, errorText);
|
|
213
|
+
if (onError) {
|
|
214
|
+
try { onError(errorText); } catch (e) {}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Send ACP initialize request (protocolVersion must be an integer per ACP spec)
|
|
219
|
+
const initRequest = {
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
id: ++requestId,
|
|
222
|
+
method: 'initialize',
|
|
223
|
+
params: {
|
|
224
|
+
protocolVersion: 1,
|
|
225
|
+
clientCapabilities: {
|
|
226
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
227
|
+
terminal: true
|
|
228
|
+
},
|
|
229
|
+
clientInfo: {
|
|
230
|
+
name: 'agentgui',
|
|
231
|
+
title: 'AgentGUI',
|
|
232
|
+
version: '1.0.0'
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
proc.stdin.write(JSON.stringify(initRequest) + '\n');
|
|
237
|
+
|
|
238
|
+
let sessionCreated = false;
|
|
239
|
+
|
|
240
|
+
// Wait for initialization then create session and send prompt
|
|
241
|
+
const checkInitAndSend = () => {
|
|
242
|
+
if (initialized && !sessionCreated) {
|
|
243
|
+
sessionCreated = true;
|
|
244
|
+
|
|
245
|
+
// Step 1: Create a new session
|
|
246
|
+
const sessionRequest = {
|
|
247
|
+
jsonrpc: '2.0',
|
|
248
|
+
id: ++requestId,
|
|
249
|
+
method: 'session/new',
|
|
250
|
+
params: {
|
|
251
|
+
cwd: cwd,
|
|
252
|
+
mcpServers: []
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
proc.stdin.write(JSON.stringify(sessionRequest) + '\n');
|
|
256
|
+
} else if (!initialized) {
|
|
257
|
+
setTimeout(checkInitAndSend, 100);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
let promptId = null;
|
|
262
|
+
let completed = false;
|
|
263
|
+
|
|
264
|
+
// Handle session creation response and send prompt
|
|
265
|
+
const originalHandler = handleMessage;
|
|
266
|
+
const enhancedHandler = (message) => {
|
|
267
|
+
// Check for session/new response
|
|
268
|
+
if (message.id && message.result && message.result.sessionId) {
|
|
269
|
+
sessionId = message.result.sessionId;
|
|
270
|
+
|
|
271
|
+
// Step 2: Send the prompt
|
|
272
|
+
promptId = ++requestId;
|
|
273
|
+
const promptRequest = {
|
|
274
|
+
jsonrpc: '2.0',
|
|
275
|
+
id: promptId,
|
|
276
|
+
method: 'session/prompt',
|
|
277
|
+
params: {
|
|
278
|
+
sessionId: sessionId,
|
|
279
|
+
prompt: [{ type: 'text', text: prompt }]
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
proc.stdin.write(JSON.stringify(promptRequest) + '\n');
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Check for prompt response (end of turn)
|
|
287
|
+
if (message.id === promptId && message.result && message.result.stopReason) {
|
|
288
|
+
completed = true;
|
|
289
|
+
clearTimeout(timeoutHandle);
|
|
290
|
+
proc.kill();
|
|
291
|
+
resolve({ outputs, sessionId });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
originalHandler(message);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Override the message handler
|
|
299
|
+
buffer = '';
|
|
300
|
+
proc.stdout.removeAllListeners('data');
|
|
301
|
+
proc.stdout.on('data', (chunk) => {
|
|
302
|
+
if (timedOut || completed) return;
|
|
303
|
+
|
|
304
|
+
buffer += chunk.toString();
|
|
305
|
+
const lines = buffer.split('\n');
|
|
306
|
+
buffer = lines.pop();
|
|
307
|
+
|
|
308
|
+
for (const line of lines) {
|
|
309
|
+
if (line.trim()) {
|
|
310
|
+
try {
|
|
311
|
+
const message = JSON.parse(line);
|
|
312
|
+
|
|
313
|
+
// Check for initialization response
|
|
314
|
+
if (message.id === 1 && message.result) {
|
|
315
|
+
initialized = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
enhancedHandler(message);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
setTimeout(checkInitAndSend, 200);
|
|
327
|
+
|
|
328
|
+
proc.on('close', (code) => {
|
|
329
|
+
clearTimeout(timeoutHandle);
|
|
330
|
+
if (timedOut || completed) return;
|
|
331
|
+
|
|
332
|
+
if (code === 0 || outputs.length > 0) {
|
|
333
|
+
resolve({ outputs, sessionId });
|
|
334
|
+
} else {
|
|
335
|
+
reject(new Error(`${this.name} ACP exited with code ${code}`));
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
proc.on('error', (err) => {
|
|
340
|
+
clearTimeout(timeoutHandle);
|
|
341
|
+
reject(err);
|
|
342
|
+
});
|
|
92
343
|
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Agent Registry
|
|
349
|
+
*/
|
|
350
|
+
class AgentRegistry {
|
|
351
|
+
constructor() {
|
|
352
|
+
this.agents = new Map();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
register(config) {
|
|
356
|
+
const runner = new AgentRunner(config);
|
|
357
|
+
this.agents.set(config.id, runner);
|
|
358
|
+
return runner;
|
|
359
|
+
}
|
|
93
360
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
361
|
+
get(agentId) {
|
|
362
|
+
return this.agents.get(agentId);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
has(agentId) {
|
|
366
|
+
return this.agents.has(agentId);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
list() {
|
|
370
|
+
return Array.from(this.agents.values()).map(a => ({
|
|
371
|
+
id: a.id,
|
|
372
|
+
name: a.name,
|
|
373
|
+
command: a.command,
|
|
374
|
+
protocol: a.protocol,
|
|
375
|
+
requiresAdapter: a.requiresAdapter,
|
|
376
|
+
supportedFeatures: a.supportedFeatures
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
listACPAvailable() {
|
|
381
|
+
// Check which agents are actually installed
|
|
382
|
+
const { execSync } = require('child_process');
|
|
383
|
+
return this.list().filter(agent => {
|
|
384
|
+
try {
|
|
385
|
+
execSync(`which ${agent.command} 2>/dev/null`, { encoding: 'utf-8' });
|
|
386
|
+
return true;
|
|
387
|
+
} catch {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
97
390
|
});
|
|
98
|
-
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Create global registry
|
|
395
|
+
const registry = new AgentRegistry();
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Claude Code Agent
|
|
399
|
+
* Uses direct JSON streaming protocol
|
|
400
|
+
*/
|
|
401
|
+
registry.register({
|
|
402
|
+
id: 'claude-code',
|
|
403
|
+
name: 'Claude Code',
|
|
404
|
+
command: 'claude',
|
|
405
|
+
protocol: 'direct',
|
|
406
|
+
supportsStdin: true,
|
|
407
|
+
supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip'],
|
|
408
|
+
|
|
409
|
+
buildArgs(prompt, config) {
|
|
410
|
+
const {
|
|
411
|
+
verbose = true,
|
|
412
|
+
outputFormat = 'stream-json',
|
|
413
|
+
print = true,
|
|
414
|
+
resumeSessionId = null,
|
|
415
|
+
systemPrompt = null
|
|
416
|
+
} = config;
|
|
417
|
+
|
|
418
|
+
const flags = [];
|
|
419
|
+
if (print) flags.push('--print');
|
|
420
|
+
if (verbose) flags.push('--verbose');
|
|
421
|
+
flags.push(`--output-format=${outputFormat}`);
|
|
422
|
+
flags.push('--dangerously-skip-permissions');
|
|
423
|
+
if (resumeSessionId) flags.push('--resume', resumeSessionId);
|
|
424
|
+
if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
|
|
425
|
+
|
|
426
|
+
return flags;
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
parseOutput(line) {
|
|
430
|
+
try {
|
|
431
|
+
return JSON.parse(line);
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* OpenCode Agent
|
|
440
|
+
* Native ACP support
|
|
441
|
+
*/
|
|
442
|
+
registry.register({
|
|
443
|
+
id: 'opencode',
|
|
444
|
+
name: 'OpenCode',
|
|
445
|
+
command: 'opencode',
|
|
446
|
+
protocol: 'acp',
|
|
447
|
+
supportsStdin: false,
|
|
448
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
449
|
+
|
|
450
|
+
buildArgs(prompt, config) {
|
|
451
|
+
return ['acp'];
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
protocolHandler(message, context) {
|
|
455
|
+
if (!message || typeof message !== 'object') return null;
|
|
456
|
+
|
|
457
|
+
// Handle ACP session/update notifications
|
|
458
|
+
if (message.method === 'session/update') {
|
|
459
|
+
const params = message.params || {};
|
|
460
|
+
const update = params.update || {};
|
|
461
|
+
|
|
462
|
+
// Agent message chunk (text response)
|
|
463
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
|
|
464
|
+
return {
|
|
465
|
+
type: 'assistant',
|
|
466
|
+
message: {
|
|
467
|
+
role: 'assistant',
|
|
468
|
+
content: [update.content]
|
|
469
|
+
},
|
|
470
|
+
session_id: params.sessionId
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Tool call
|
|
475
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
476
|
+
return {
|
|
477
|
+
type: 'assistant',
|
|
478
|
+
message: {
|
|
479
|
+
role: 'assistant',
|
|
480
|
+
content: [{
|
|
481
|
+
type: 'tool_use',
|
|
482
|
+
id: update.toolCallId,
|
|
483
|
+
name: update.title || 'tool',
|
|
484
|
+
input: update.input || {}
|
|
485
|
+
}]
|
|
486
|
+
},
|
|
487
|
+
session_id: params.sessionId
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Tool call update (result)
|
|
492
|
+
if (update.sessionUpdate === 'tool_call_update' && update.status === 'completed') {
|
|
493
|
+
const content = update.content && update.content[0] ? update.content[0].content : null;
|
|
494
|
+
return {
|
|
495
|
+
type: 'assistant',
|
|
496
|
+
message: {
|
|
497
|
+
role: 'assistant',
|
|
498
|
+
content: [{
|
|
499
|
+
type: 'tool_result',
|
|
500
|
+
tool_use_id: update.toolCallId,
|
|
501
|
+
content: content ? (content.text || JSON.stringify(content)) : '',
|
|
502
|
+
is_error: false
|
|
503
|
+
}]
|
|
504
|
+
},
|
|
505
|
+
session_id: params.sessionId
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Usage update
|
|
510
|
+
if (update.sessionUpdate === 'usage_update') {
|
|
511
|
+
return {
|
|
512
|
+
type: 'usage',
|
|
513
|
+
usage: {
|
|
514
|
+
used: update.used,
|
|
515
|
+
size: update.size,
|
|
516
|
+
cost: update.cost
|
|
517
|
+
},
|
|
518
|
+
session_id: params.sessionId
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Plan update
|
|
523
|
+
if (update.sessionUpdate === 'plan') {
|
|
524
|
+
return {
|
|
525
|
+
type: 'plan',
|
|
526
|
+
entries: update.entries || [],
|
|
527
|
+
session_id: params.sessionId
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Skip other updates like available_commands_update
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Handle prompt response (end of turn)
|
|
536
|
+
if (message.id && message.result && message.result.stopReason) {
|
|
537
|
+
return {
|
|
538
|
+
type: 'result',
|
|
539
|
+
result: '',
|
|
540
|
+
stopReason: message.result.stopReason,
|
|
541
|
+
usage: message.result.usage,
|
|
542
|
+
session_id: context.sessionId
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (message.method === 'error' || message.error) {
|
|
547
|
+
return {
|
|
548
|
+
type: 'error',
|
|
549
|
+
error: message.error || message.params || { message: 'Unknown error' }
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Common ACP protocol handler for all ACP agents
|
|
559
|
+
*/
|
|
560
|
+
function createACPProtocolHandler() {
|
|
561
|
+
return function(message, context) {
|
|
562
|
+
if (!message || typeof message !== 'object') return null;
|
|
563
|
+
|
|
564
|
+
// Handle ACP session/update notifications
|
|
565
|
+
if (message.method === 'session/update') {
|
|
566
|
+
const params = message.params || {};
|
|
567
|
+
const update = params.update || {};
|
|
568
|
+
|
|
569
|
+
// Agent message chunk (text response)
|
|
570
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
|
|
571
|
+
return {
|
|
572
|
+
type: 'assistant',
|
|
573
|
+
message: {
|
|
574
|
+
role: 'assistant',
|
|
575
|
+
content: [update.content]
|
|
576
|
+
},
|
|
577
|
+
session_id: params.sessionId
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Tool call
|
|
582
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
583
|
+
return {
|
|
584
|
+
type: 'assistant',
|
|
585
|
+
message: {
|
|
586
|
+
role: 'assistant',
|
|
587
|
+
content: [{
|
|
588
|
+
type: 'tool_use',
|
|
589
|
+
id: update.toolCallId,
|
|
590
|
+
name: update.title || 'tool',
|
|
591
|
+
input: update.input || {}
|
|
592
|
+
}]
|
|
593
|
+
},
|
|
594
|
+
session_id: params.sessionId
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Tool call update (result)
|
|
599
|
+
if (update.sessionUpdate === 'tool_call_update' && update.status === 'completed') {
|
|
600
|
+
const content = update.content && update.content[0] ? update.content[0].content : null;
|
|
601
|
+
return {
|
|
602
|
+
type: 'assistant',
|
|
603
|
+
message: {
|
|
604
|
+
role: 'assistant',
|
|
605
|
+
content: [{
|
|
606
|
+
type: 'tool_result',
|
|
607
|
+
tool_use_id: update.toolCallId,
|
|
608
|
+
content: content ? (content.text || JSON.stringify(content)) : '',
|
|
609
|
+
is_error: false
|
|
610
|
+
}]
|
|
611
|
+
},
|
|
612
|
+
session_id: params.sessionId
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Usage update
|
|
617
|
+
if (update.sessionUpdate === 'usage_update') {
|
|
618
|
+
return {
|
|
619
|
+
type: 'usage',
|
|
620
|
+
usage: {
|
|
621
|
+
used: update.used,
|
|
622
|
+
size: update.size,
|
|
623
|
+
cost: update.cost
|
|
624
|
+
},
|
|
625
|
+
session_id: params.sessionId
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Handle prompt response (end of turn)
|
|
633
|
+
if (message.id && message.result && message.result.stopReason) {
|
|
634
|
+
return {
|
|
635
|
+
type: 'result',
|
|
636
|
+
result: '',
|
|
637
|
+
stopReason: message.result.stopReason,
|
|
638
|
+
usage: message.result.usage,
|
|
639
|
+
session_id: context.sessionId
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (message.method === 'error' || message.error) {
|
|
644
|
+
return {
|
|
645
|
+
type: 'error',
|
|
646
|
+
error: message.error || message.params || { message: 'Unknown error' }
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return null;
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Shared ACP handler
|
|
655
|
+
const acpProtocolHandler = createACPProtocolHandler();
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Gemini CLI Agent
|
|
659
|
+
* Native ACP support
|
|
660
|
+
*/
|
|
661
|
+
registry.register({
|
|
662
|
+
id: 'gemini',
|
|
663
|
+
name: 'Gemini CLI',
|
|
664
|
+
command: 'gemini',
|
|
665
|
+
protocol: 'acp',
|
|
666
|
+
supportsStdin: false,
|
|
667
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
668
|
+
buildArgs: () => ['acp'],
|
|
669
|
+
protocolHandler: acpProtocolHandler
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Goose Agent
|
|
674
|
+
* Native ACP support
|
|
675
|
+
*/
|
|
676
|
+
registry.register({
|
|
677
|
+
id: 'goose',
|
|
678
|
+
name: 'Goose',
|
|
679
|
+
command: 'goose',
|
|
680
|
+
protocol: 'acp',
|
|
681
|
+
supportsStdin: false,
|
|
682
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
683
|
+
buildArgs: () => ['acp'],
|
|
684
|
+
protocolHandler: acpProtocolHandler
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* OpenHands Agent
|
|
689
|
+
* Native ACP support
|
|
690
|
+
*/
|
|
691
|
+
registry.register({
|
|
692
|
+
id: 'openhands',
|
|
693
|
+
name: 'OpenHands',
|
|
694
|
+
command: 'openhands',
|
|
695
|
+
protocol: 'acp',
|
|
696
|
+
supportsStdin: false,
|
|
697
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
698
|
+
buildArgs: () => ['acp'],
|
|
699
|
+
protocolHandler: acpProtocolHandler
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Augment Code Agent - Native ACP support
|
|
704
|
+
*/
|
|
705
|
+
registry.register({
|
|
706
|
+
id: 'augment',
|
|
707
|
+
name: 'Augment Code',
|
|
708
|
+
command: 'augment',
|
|
709
|
+
protocol: 'acp',
|
|
710
|
+
supportsStdin: false,
|
|
711
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
712
|
+
buildArgs: () => ['acp'],
|
|
713
|
+
protocolHandler: acpProtocolHandler
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Cline Agent - Native ACP support
|
|
718
|
+
*/
|
|
719
|
+
registry.register({
|
|
720
|
+
id: 'cline',
|
|
721
|
+
name: 'Cline',
|
|
722
|
+
command: 'cline',
|
|
723
|
+
protocol: 'acp',
|
|
724
|
+
supportsStdin: false,
|
|
725
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
726
|
+
buildArgs: () => ['acp'],
|
|
727
|
+
protocolHandler: acpProtocolHandler
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Kimi CLI Agent (Moonshot AI) - Native ACP support
|
|
732
|
+
*/
|
|
733
|
+
registry.register({
|
|
734
|
+
id: 'kimi',
|
|
735
|
+
name: 'Kimi CLI',
|
|
736
|
+
command: 'kimi',
|
|
737
|
+
protocol: 'acp',
|
|
738
|
+
supportsStdin: false,
|
|
739
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
740
|
+
buildArgs: () => ['acp'],
|
|
741
|
+
protocolHandler: acpProtocolHandler
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Qwen Code Agent (Alibaba) - Native ACP support
|
|
746
|
+
*/
|
|
747
|
+
registry.register({
|
|
748
|
+
id: 'qwen',
|
|
749
|
+
name: 'Qwen Code',
|
|
750
|
+
command: 'qwen-code',
|
|
751
|
+
protocol: 'acp',
|
|
752
|
+
supportsStdin: false,
|
|
753
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
754
|
+
buildArgs: () => ['acp'],
|
|
755
|
+
protocolHandler: acpProtocolHandler
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Codex CLI Agent (OpenAI) - ACP support
|
|
760
|
+
*/
|
|
761
|
+
registry.register({
|
|
762
|
+
id: 'codex',
|
|
763
|
+
name: 'Codex CLI',
|
|
764
|
+
command: 'codex',
|
|
765
|
+
protocol: 'acp',
|
|
766
|
+
supportsStdin: false,
|
|
767
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
768
|
+
buildArgs: () => ['acp'],
|
|
769
|
+
protocolHandler: acpProtocolHandler
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Mistral Vibe Agent - Native ACP support
|
|
774
|
+
*/
|
|
775
|
+
registry.register({
|
|
776
|
+
id: 'mistral',
|
|
777
|
+
name: 'Mistral Vibe',
|
|
778
|
+
command: 'mistral-vibe',
|
|
779
|
+
protocol: 'acp',
|
|
780
|
+
supportsStdin: false,
|
|
781
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
782
|
+
buildArgs: () => ['acp'],
|
|
783
|
+
protocolHandler: acpProtocolHandler
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Kiro CLI Agent - Native ACP support
|
|
788
|
+
*/
|
|
789
|
+
registry.register({
|
|
790
|
+
id: 'kiro',
|
|
791
|
+
name: 'Kiro CLI',
|
|
792
|
+
command: 'kiro',
|
|
793
|
+
protocol: 'acp',
|
|
794
|
+
supportsStdin: false,
|
|
795
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
796
|
+
buildArgs: () => ['acp'],
|
|
797
|
+
protocolHandler: acpProtocolHandler
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* fast-agent - Native ACP support
|
|
802
|
+
*/
|
|
803
|
+
registry.register({
|
|
804
|
+
id: 'fast-agent',
|
|
805
|
+
name: 'fast-agent',
|
|
806
|
+
command: 'fast-agent',
|
|
807
|
+
protocol: 'acp',
|
|
808
|
+
supportsStdin: false,
|
|
809
|
+
supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
|
|
810
|
+
buildArgs: () => ['acp'],
|
|
811
|
+
protocolHandler: acpProtocolHandler
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Main export function - runs any registered agent
|
|
816
|
+
*/
|
|
817
|
+
export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
|
|
818
|
+
const agent = registry.get(agentId);
|
|
819
|
+
|
|
820
|
+
if (!agent) {
|
|
821
|
+
throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return agent.run(prompt, cwd, config);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Get list of registered agents
|
|
829
|
+
*/
|
|
830
|
+
export function getRegisteredAgents() {
|
|
831
|
+
return registry.list();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Get list of installed/available agents
|
|
836
|
+
*/
|
|
837
|
+
export function getAvailableAgents() {
|
|
838
|
+
return registry.listACPAvailable();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Check if an agent is registered
|
|
843
|
+
*/
|
|
844
|
+
export function isAgentRegistered(agentId) {
|
|
845
|
+
return registry.has(agentId);
|
|
99
846
|
}
|
|
100
847
|
|
|
101
848
|
export default runClaudeWithStreaming;
|