agentgui 1.0.274 → 1.0.275

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.
Files changed (69) hide show
  1. package/CLAUDE.md +280 -280
  2. package/IPFS_DOWNLOADER.md +277 -277
  3. package/TASK_2C_COMPLETION.md +334 -334
  4. package/bin/gmgui.cjs +54 -54
  5. package/build-portable.js +3 -42
  6. package/database.js +1422 -1406
  7. package/lib/claude-runner.js +1130 -1130
  8. package/lib/ipfs-downloader.js +459 -459
  9. package/lib/speech.js +152 -152
  10. package/package.json +1 -1
  11. package/readme.md +76 -76
  12. package/server.js +3787 -3794
  13. package/setup-npm-token.sh +68 -68
  14. package/static/app.js +773 -773
  15. package/static/event-rendering-showcase.html +708 -708
  16. package/static/index.html +3178 -3180
  17. package/static/js/agent-auth.js +298 -298
  18. package/static/js/audio-recorder-processor.js +18 -18
  19. package/static/js/client.js +2656 -2656
  20. package/static/js/conversations.js +583 -583
  21. package/static/js/dialogs.js +267 -267
  22. package/static/js/event-consolidator.js +101 -101
  23. package/static/js/event-filter.js +311 -311
  24. package/static/js/event-processor.js +452 -452
  25. package/static/js/features.js +413 -413
  26. package/static/js/kalman-filter.js +67 -67
  27. package/static/js/progress-dialog.js +130 -130
  28. package/static/js/script-runner.js +219 -219
  29. package/static/js/streaming-renderer.js +2123 -2120
  30. package/static/js/syntax-highlighter.js +269 -269
  31. package/static/js/tts-websocket-handler.js +152 -152
  32. package/static/js/ui-components.js +431 -431
  33. package/static/js/voice.js +849 -849
  34. package/static/js/websocket-manager.js +596 -596
  35. package/static/templates/INDEX.html +465 -465
  36. package/static/templates/README.md +190 -190
  37. package/static/templates/agent-capabilities.html +56 -56
  38. package/static/templates/agent-metadata-panel.html +44 -44
  39. package/static/templates/agent-status-badge.html +30 -30
  40. package/static/templates/code-annotation-panel.html +155 -155
  41. package/static/templates/code-suggestion-panel.html +184 -184
  42. package/static/templates/command-header.html +77 -77
  43. package/static/templates/command-output-scrollable.html +118 -118
  44. package/static/templates/elapsed-time.html +54 -54
  45. package/static/templates/error-alert.html +106 -106
  46. package/static/templates/error-history-timeline.html +160 -160
  47. package/static/templates/error-recovery-options.html +109 -109
  48. package/static/templates/error-stack-trace.html +95 -95
  49. package/static/templates/error-summary.html +80 -80
  50. package/static/templates/event-counter.html +48 -48
  51. package/static/templates/execution-actions.html +97 -97
  52. package/static/templates/execution-progress-bar.html +80 -80
  53. package/static/templates/execution-stepper.html +120 -120
  54. package/static/templates/file-breadcrumb.html +118 -118
  55. package/static/templates/file-diff-viewer.html +121 -121
  56. package/static/templates/file-metadata.html +133 -133
  57. package/static/templates/file-read-panel.html +66 -66
  58. package/static/templates/file-write-panel.html +120 -120
  59. package/static/templates/git-branch-remote.html +107 -107
  60. package/static/templates/git-diff-list.html +101 -101
  61. package/static/templates/git-log-visualization.html +153 -153
  62. package/static/templates/git-status-panel.html +115 -115
  63. package/static/templates/quality-metrics-display.html +170 -170
  64. package/static/templates/terminal-output-panel.html +87 -87
  65. package/static/templates/test-results-display.html +144 -144
  66. package/static/theme.js +72 -72
  67. package/test-download-progress.js +223 -223
  68. package/test-websocket-broadcast.js +147 -147
  69. package/tests/ipfs-downloader.test.js +370 -370
@@ -1,1130 +1,1130 @@
1
- import { spawn } from 'child_process';
2
-
3
- const isWindows = process.platform === 'win32';
4
-
5
- function getSpawnOptions(cwd, additionalOptions = {}) {
6
- const options = { cwd, ...additionalOptions };
7
- if (isWindows) {
8
- options.shell = true;
9
- }
10
- return options;
11
- }
12
-
13
- /**
14
- * Agent Framework
15
- * Extensible registry for AI agent CLI integrations
16
- * Supports multiple protocols: direct JSON streaming, ACP (JSON-RPC), etc.
17
- */
18
-
19
- class AgentRunner {
20
- constructor(config) {
21
- this.id = config.id;
22
- this.name = config.name;
23
- this.command = config.command;
24
- this.protocol = config.protocol || 'direct'; // 'direct' | 'acp' | etc
25
- this.buildArgs = config.buildArgs || this.defaultBuildArgs;
26
- this.parseOutput = config.parseOutput || this.defaultParseOutput;
27
- this.supportsStdin = config.supportsStdin ?? true;
28
- this.supportedFeatures = config.supportedFeatures || [];
29
- this.protocolHandler = config.protocolHandler || null;
30
- this.requiresAdapter = config.requiresAdapter || false;
31
- this.adapterCommand = config.adapterCommand || null;
32
- this.adapterArgs = config.adapterArgs || [];
33
- }
34
-
35
- defaultBuildArgs(prompt, config) {
36
- return [];
37
- }
38
-
39
- defaultParseOutput(line) {
40
- try {
41
- return JSON.parse(line);
42
- } catch {
43
- return null;
44
- }
45
- }
46
-
47
- async run(prompt, cwd, config = {}) {
48
- if (this.protocol === 'acp' && this.protocolHandler) {
49
- return this.runACP(prompt, cwd, config);
50
- }
51
- return this.runDirect(prompt, cwd, config);
52
- }
53
-
54
- async runDirect(prompt, cwd, config = {}) {
55
- return new Promise((resolve, reject) => {
56
- const {
57
- timeout = 300000,
58
- onEvent = null,
59
- onError = null,
60
- onRateLimit = null
61
- } = config;
62
-
63
- const args = this.buildArgs(prompt, config);
64
- const proc = spawn(this.command, args, getSpawnOptions(cwd));
65
-
66
- if (config.onPid) {
67
- try { config.onPid(proc.pid); } catch (e) {}
68
- }
69
-
70
- let jsonBuffer = '';
71
- const outputs = [];
72
- let timedOut = false;
73
- let sessionId = null;
74
- let rateLimited = false;
75
- let retryAfterSec = 60;
76
-
77
- const timeoutHandle = setTimeout(() => {
78
- timedOut = true;
79
- proc.kill();
80
- reject(new Error(`${this.name} timeout after ${timeout}ms`));
81
- }, timeout);
82
-
83
- // Write to stdin if supported
84
- if (this.supportsStdin) {
85
- proc.stdin.write(prompt);
86
- proc.stdin.end();
87
- }
88
-
89
- proc.stdout.on('data', (chunk) => {
90
- if (timedOut) return;
91
-
92
- jsonBuffer += chunk.toString();
93
- const lines = jsonBuffer.split('\n');
94
- jsonBuffer = lines.pop();
95
-
96
- for (const line of lines) {
97
- if (line.trim()) {
98
- const parsed = this.parseOutput(line);
99
- if (!parsed) continue;
100
-
101
- outputs.push(parsed);
102
-
103
- if (parsed.session_id) {
104
- sessionId = parsed.session_id;
105
- }
106
-
107
- if (onEvent) {
108
- try { onEvent(parsed); } catch (e) {
109
- console.error(`[${this.id}] onEvent error: ${e.message}`);
110
- }
111
- }
112
- }
113
- }
114
- });
115
-
116
- proc.stderr.on('data', (chunk) => {
117
- const errorText = chunk.toString();
118
- console.error(`[${this.id}] stderr:`, errorText);
119
-
120
- const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl|hit your limit/i);
121
- if (rateLimitMatch) {
122
- rateLimited = true;
123
- const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
124
- if (retryMatch) {
125
- retryAfterSec = parseInt(retryMatch[1], 10) || 60;
126
- } else {
127
- const resetTimeMatch = errorText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
128
- if (resetTimeMatch) {
129
- let hours = parseInt(resetTimeMatch[1], 10);
130
- const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
131
- const period = resetTimeMatch[3]?.toLowerCase();
132
- const tz = resetTimeMatch[4]?.toUpperCase() || 'UTC';
133
-
134
- if (period === 'pm' && hours !== 12) hours += 12;
135
- if (period === 'am' && hours === 12) hours = 0;
136
-
137
- const now = new Date();
138
- const resetTime = new Date(now);
139
- resetTime.setUTCHours(hours, minutes, 0, 0);
140
-
141
- if (resetTime <= now) {
142
- resetTime.setUTCDate(resetTime.getUTCDate() + 1);
143
- }
144
-
145
- retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
146
- }
147
- }
148
- }
149
-
150
- if (onError) {
151
- try { onError(errorText); } catch (e) {}
152
- }
153
- });
154
-
155
- proc.on('close', (code) => {
156
- clearTimeout(timeoutHandle);
157
- if (timedOut) return;
158
-
159
- if (rateLimited) {
160
- const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
161
- err.rateLimited = true;
162
- err.retryAfterSec = retryAfterSec;
163
- if (onRateLimit) {
164
- try { onRateLimit({ retryAfterSec }); } catch (e) {}
165
- }
166
- reject(err);
167
- return;
168
- }
169
-
170
- if (jsonBuffer.trim()) {
171
- const parsed = this.parseOutput(jsonBuffer);
172
- if (parsed) {
173
- outputs.push(parsed);
174
- if (parsed.session_id) sessionId = parsed.session_id;
175
- if (onEvent) {
176
- try { onEvent(parsed); } catch (e) {}
177
- }
178
- }
179
- }
180
-
181
- if (code === 0 || outputs.length > 0) {
182
- resolve({ outputs, sessionId });
183
- } else {
184
- reject(new Error(`${this.name} exited with code ${code}`));
185
- }
186
- });
187
-
188
- proc.on('error', (err) => {
189
- clearTimeout(timeoutHandle);
190
- reject(err);
191
- });
192
- });
193
- }
194
-
195
- async runACP(prompt, cwd, config = {}, _retryCount = 0) {
196
- const maxRetries = config.maxRetries ?? 1;
197
- try {
198
- return await this._runACPOnce(prompt, cwd, config);
199
- } catch (err) {
200
- const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
201
- const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
202
- if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
203
- const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
204
- console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
205
- await new Promise(r => setTimeout(r, delay));
206
- return this.runACP(prompt, cwd, config, _retryCount + 1);
207
- }
208
- if (err.isPrematureEnd) {
209
- const premErr = new Error(err.message);
210
- premErr.isPrematureEnd = true;
211
- premErr.exitCode = err.exitCode;
212
- premErr.stderrText = err.stderrText;
213
- throw premErr;
214
- }
215
- throw err;
216
- }
217
- }
218
-
219
- async _runACPOnce(prompt, cwd, config = {}) {
220
- return new Promise((resolve, reject) => {
221
- const {
222
- timeout = 300000,
223
- onEvent = null,
224
- onError = null
225
- } = config;
226
-
227
- const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
228
- const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : this.buildArgs(prompt, config);
229
- const args = [...baseArgs];
230
-
231
- const proc = spawn(cmd, args, getSpawnOptions(cwd));
232
-
233
- if (config.onPid) {
234
- try { config.onPid(proc.pid); } catch (e) {}
235
- }
236
-
237
- const outputs = [];
238
- let timedOut = false;
239
- let sessionId = null;
240
- let requestId = 0;
241
- let initialized = false;
242
- let stderrText = '';
243
-
244
- const timeoutHandle = setTimeout(() => {
245
- timedOut = true;
246
- proc.kill();
247
- reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
248
- }, timeout);
249
-
250
- const handleMessage = (message) => {
251
- const normalized = this.protocolHandler(message, { sessionId, initialized });
252
- if (!normalized) {
253
- if (message.id === 1 && message.result) {
254
- initialized = true;
255
- }
256
- return;
257
- }
258
-
259
- outputs.push(normalized);
260
-
261
- if (normalized.session_id) {
262
- sessionId = normalized.session_id;
263
- }
264
-
265
- if (onEvent) {
266
- try { onEvent(normalized); } catch (e) {
267
- console.error(`[${this.id}] onEvent error: ${e.message}`);
268
- }
269
- }
270
- };
271
-
272
- let buffer = '';
273
- proc.stdout.on('data', (chunk) => {
274
- if (timedOut) return;
275
-
276
- buffer += chunk.toString();
277
- const lines = buffer.split('\n');
278
- buffer = lines.pop();
279
-
280
- for (const line of lines) {
281
- if (line.trim()) {
282
- try {
283
- const message = JSON.parse(line);
284
- handleMessage(message);
285
- } catch (e) {
286
- console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
287
- }
288
- }
289
- }
290
- });
291
-
292
- proc.stderr.on('data', (chunk) => {
293
- const errorText = chunk.toString();
294
- stderrText += errorText;
295
- console.error(`[${this.id}] stderr:`, errorText);
296
- if (onError) {
297
- try { onError(errorText); } catch (e) {}
298
- }
299
- });
300
-
301
- const initRequest = {
302
- jsonrpc: '2.0',
303
- id: ++requestId,
304
- method: 'initialize',
305
- params: {
306
- protocolVersion: 1,
307
- clientCapabilities: {
308
- fs: { readTextFile: true, writeTextFile: true },
309
- terminal: true
310
- },
311
- clientInfo: {
312
- name: 'agentgui',
313
- title: 'AgentGUI',
314
- version: '1.0.0'
315
- }
316
- }
317
- };
318
- proc.stdin.write(JSON.stringify(initRequest) + '\n');
319
-
320
- let sessionCreated = false;
321
-
322
- const checkInitAndSend = () => {
323
- if (initialized && !sessionCreated) {
324
- sessionCreated = true;
325
-
326
- const sessionParams = {
327
- cwd: cwd,
328
- mcpServers: []
329
- };
330
- if (config.model) sessionParams.model = config.model;
331
- const sessionRequest = {
332
- jsonrpc: '2.0',
333
- id: ++requestId,
334
- method: 'session/new',
335
- params: sessionParams
336
- };
337
- proc.stdin.write(JSON.stringify(sessionRequest) + '\n');
338
- } else if (!initialized) {
339
- setTimeout(checkInitAndSend, 100);
340
- }
341
- };
342
-
343
- let promptId = null;
344
- let completed = false;
345
-
346
- const originalHandler = handleMessage;
347
- const enhancedHandler = (message) => {
348
- if (message.id && message.result && message.result.sessionId) {
349
- sessionId = message.result.sessionId;
350
-
351
- promptId = ++requestId;
352
- const promptRequest = {
353
- jsonrpc: '2.0',
354
- id: promptId,
355
- method: 'session/prompt',
356
- params: {
357
- sessionId: sessionId,
358
- prompt: [{ type: 'text', text: prompt }]
359
- }
360
- };
361
- proc.stdin.write(JSON.stringify(promptRequest) + '\n');
362
- return;
363
- }
364
-
365
- if (message.id === promptId && message.result && message.result.stopReason) {
366
- completed = true;
367
- draining = true;
368
- clearTimeout(timeoutHandle);
369
- // Wait a short time for any remaining events to be flushed before killing
370
- setTimeout(() => {
371
- draining = false;
372
- try { proc.kill(); } catch (e) {}
373
- resolve({ outputs, sessionId });
374
- }, 1000);
375
- return;
376
- }
377
-
378
- if (message.id === promptId && message.error) {
379
- completed = true;
380
- draining = true;
381
- clearTimeout(timeoutHandle);
382
- // Process the error message first, then delay for remaining events
383
- originalHandler(message);
384
- setTimeout(() => {
385
- draining = false;
386
- try { proc.kill(); } catch (e) {}
387
- reject(new Error(message.error.message || 'ACP prompt error'));
388
- }, 1000);
389
- return;
390
- }
391
-
392
- originalHandler(message);
393
- };
394
-
395
- buffer = '';
396
- proc.stdout.removeAllListeners('data');
397
- let draining = false;
398
- proc.stdout.on('data', (chunk) => {
399
- if (timedOut) return;
400
- // Continue processing during drain period after stopReason/error
401
- if (completed && !draining) return;
402
-
403
- buffer += chunk.toString();
404
- const lines = buffer.split('\n');
405
- buffer = lines.pop();
406
-
407
- for (const line of lines) {
408
- if (line.trim()) {
409
- try {
410
- const message = JSON.parse(line);
411
-
412
- if (message.id === 1 && message.result) {
413
- initialized = true;
414
- }
415
-
416
- enhancedHandler(message);
417
- } catch (e) {
418
- console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
419
- }
420
- }
421
- }
422
- });
423
-
424
- setTimeout(checkInitAndSend, 200);
425
-
426
- proc.on('close', (code) => {
427
- clearTimeout(timeoutHandle);
428
- if (timedOut || completed) return;
429
-
430
- // Flush any remaining buffer content
431
- if (buffer.trim()) {
432
- try {
433
- const message = JSON.parse(buffer.trim());
434
- if (message.id === 1 && message.result) {
435
- initialized = true;
436
- }
437
- enhancedHandler(message);
438
- } catch (e) {
439
- // Buffer might be incomplete, ignore parse errors on close
440
- }
441
- }
442
-
443
- if (code === 0 || outputs.length > 0) {
444
- resolve({ outputs, sessionId });
445
- } else {
446
- const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
447
- const err = new Error(`${this.name} ACP exited with code ${code}${detail}`);
448
- err.isPrematureEnd = true;
449
- err.exitCode = code;
450
- err.stderrText = stderrText;
451
- reject(err);
452
- }
453
- });
454
-
455
- proc.on('error', (err) => {
456
- clearTimeout(timeoutHandle);
457
- reject(err);
458
- });
459
- });
460
- }
461
- }
462
-
463
- /**
464
- * Agent Registry
465
- */
466
- class AgentRegistry {
467
- constructor() {
468
- this.agents = new Map();
469
- }
470
-
471
- register(config) {
472
- const runner = new AgentRunner(config);
473
- this.agents.set(config.id, runner);
474
- return runner;
475
- }
476
-
477
- get(agentId) {
478
- return this.agents.get(agentId);
479
- }
480
-
481
- has(agentId) {
482
- return this.agents.has(agentId);
483
- }
484
-
485
- list() {
486
- return Array.from(this.agents.values()).map(a => ({
487
- id: a.id,
488
- name: a.name,
489
- command: a.command,
490
- protocol: a.protocol,
491
- requiresAdapter: a.requiresAdapter,
492
- supportedFeatures: a.supportedFeatures
493
- }));
494
- }
495
-
496
- listACPAvailable() {
497
- const { spawnSync } = require('child_process');
498
- const isWindows = process.platform === 'win32';
499
- return this.list().filter(agent => {
500
- try {
501
- const whichCmd = isWindows ? 'where' : 'which';
502
- const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
503
- if (which.status !== 0) return false;
504
- const binPath = (which.stdout || '').trim().split('\n')[0].trim();
505
- if (!binPath) return false;
506
- const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows });
507
- return check.status === 0 && (check.stdout || '').trim().length > 0;
508
- } catch {
509
- return false;
510
- }
511
- });
512
- }
513
- }
514
-
515
- // Create global registry
516
- const registry = new AgentRegistry();
517
-
518
- /**
519
- * Claude Code Agent
520
- * Uses direct JSON streaming protocol
521
- */
522
- registry.register({
523
- id: 'claude-code',
524
- name: 'Claude Code',
525
- command: 'claude',
526
- protocol: 'direct',
527
- supportsStdin: true,
528
- supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip'],
529
-
530
- buildArgs(prompt, config) {
531
- const {
532
- verbose = true,
533
- outputFormat = 'stream-json',
534
- print = true,
535
- resumeSessionId = null,
536
- systemPrompt = null,
537
- model = null
538
- } = config;
539
-
540
- const flags = [];
541
- if (print) flags.push('--print');
542
- if (verbose) flags.push('--verbose');
543
- flags.push(`--output-format=${outputFormat}`);
544
- flags.push('--dangerously-skip-permissions');
545
- if (model) flags.push('--model', model);
546
- if (resumeSessionId) flags.push('--resume', resumeSessionId);
547
- if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
548
-
549
- return flags;
550
- },
551
-
552
- parseOutput(line) {
553
- try {
554
- return JSON.parse(line);
555
- } catch {
556
- return null;
557
- }
558
- }
559
- });
560
-
561
- /**
562
- * OpenCode Agent
563
- * Native ACP support
564
- */
565
- registry.register({
566
- id: 'opencode',
567
- name: 'OpenCode',
568
- command: 'opencode',
569
- protocol: 'acp',
570
- supportsStdin: false,
571
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
572
-
573
- buildArgs(prompt, config) {
574
- return ['acp'];
575
- },
576
-
577
- protocolHandler(message, context) {
578
- if (!message || typeof message !== 'object') return null;
579
-
580
- // Handle ACP session/update notifications
581
- if (message.method === 'session/update') {
582
- const params = message.params || {};
583
- const update = params.update || {};
584
-
585
- // Agent message chunk (text response)
586
- if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
587
- let contentBlock;
588
-
589
- // Handle different content formats
590
- if (typeof update.content === 'string') {
591
- contentBlock = { type: 'text', text: update.content };
592
- } else if (update.content.type === 'text' && update.content.text) {
593
- contentBlock = update.content;
594
- } else if (update.content.text) {
595
- contentBlock = { type: 'text', text: update.content.text };
596
- } else if (update.content.content) {
597
- const inner = update.content.content;
598
- if (typeof inner === 'string') {
599
- contentBlock = { type: 'text', text: inner };
600
- } else if (inner.type === 'text' && inner.text) {
601
- contentBlock = inner;
602
- } else {
603
- contentBlock = { type: 'text', text: JSON.stringify(inner) };
604
- }
605
- } else {
606
- contentBlock = { type: 'text', text: JSON.stringify(update.content) };
607
- }
608
-
609
- return {
610
- type: 'assistant',
611
- message: {
612
- role: 'assistant',
613
- content: [contentBlock]
614
- },
615
- session_id: params.sessionId
616
- };
617
- }
618
-
619
- // Tool call
620
- if (update.sessionUpdate === 'tool_call') {
621
- return {
622
- type: 'assistant',
623
- message: {
624
- role: 'assistant',
625
- content: [{
626
- type: 'tool_use',
627
- id: update.toolCallId,
628
- name: update.title || update.kind || 'tool',
629
- kind: update.kind || 'other',
630
- input: update.rawInput || update.input || {}
631
- }]
632
- },
633
- session_id: params.sessionId
634
- };
635
- }
636
-
637
- // Tool call update (result) - handle all statuses
638
- if (update.sessionUpdate === 'tool_call_update') {
639
- const status = update.status;
640
- const isError = status === 'failed';
641
- const isCompleted = status === 'completed';
642
-
643
- if (!isCompleted && !isError) {
644
- return {
645
- type: 'tool_status',
646
- tool_use_id: update.toolCallId,
647
- status: status,
648
- kind: update.kind || 'other',
649
- locations: update.locations || [],
650
- session_id: params.sessionId
651
- };
652
- }
653
-
654
- const contentParts = [];
655
- if (update.content && Array.isArray(update.content)) {
656
- for (const item of update.content) {
657
- if (item.type === 'content' && item.content) {
658
- const innerContent = item.content;
659
- if (innerContent.type === 'text' && innerContent.text) {
660
- contentParts.push(innerContent.text);
661
- } else if (innerContent.type === 'resource' && innerContent.resource) {
662
- contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
663
- } else {
664
- contentParts.push(JSON.stringify(innerContent));
665
- }
666
- } else if (item.type === 'diff') {
667
- const diffText = item.oldText
668
- ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
669
- : `+++ ${item.path}\n${item.newText}`;
670
- contentParts.push(diffText);
671
- } else if (item.type === 'terminal') {
672
- contentParts.push(`[Terminal: ${item.terminalId}]`);
673
- }
674
- }
675
- }
676
-
677
- const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
678
-
679
- return {
680
- type: 'user',
681
- message: {
682
- role: 'user',
683
- content: [{
684
- type: 'tool_result',
685
- tool_use_id: update.toolCallId,
686
- content: combinedContent,
687
- is_error: isError
688
- }]
689
- },
690
- session_id: params.sessionId
691
- };
692
- }
693
-
694
- // Usage update
695
- if (update.sessionUpdate === 'usage_update') {
696
- return {
697
- type: 'usage',
698
- usage: {
699
- used: update.used,
700
- size: update.size,
701
- cost: update.cost
702
- },
703
- session_id: params.sessionId
704
- };
705
- }
706
-
707
- // Plan update
708
- if (update.sessionUpdate === 'plan') {
709
- return {
710
- type: 'plan',
711
- entries: update.entries || [],
712
- session_id: params.sessionId
713
- };
714
- }
715
-
716
- // Skip other updates like available_commands_update
717
- return null;
718
- }
719
-
720
- // Handle prompt response (end of turn)
721
- if (message.id && message.result && message.result.stopReason) {
722
- return {
723
- type: 'result',
724
- result: '',
725
- stopReason: message.result.stopReason,
726
- usage: message.result.usage,
727
- session_id: context.sessionId
728
- };
729
- }
730
-
731
- if (message.method === 'error' || message.error) {
732
- return {
733
- type: 'error',
734
- error: message.error || message.params || { message: 'Unknown error' }
735
- };
736
- }
737
-
738
- return null;
739
- }
740
- });
741
-
742
- /**
743
- * Common ACP protocol handler for all ACP agents
744
- */
745
- function createACPProtocolHandler() {
746
- return function(message, context) {
747
- if (!message || typeof message !== 'object') return null;
748
-
749
- // Handle ACP session/update notifications
750
- if (message.method === 'session/update') {
751
- const params = message.params || {};
752
- const update = params.update || {};
753
-
754
- // Agent message chunk (text response)
755
- if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
756
- let contentBlock;
757
-
758
- // Handle different content formats
759
- if (typeof update.content === 'string') {
760
- contentBlock = { type: 'text', text: update.content };
761
- } else if (update.content.type === 'text' && update.content.text) {
762
- contentBlock = update.content;
763
- } else if (update.content.text) {
764
- contentBlock = { type: 'text', text: update.content.text };
765
- } else if (update.content.content) {
766
- const inner = update.content.content;
767
- if (typeof inner === 'string') {
768
- contentBlock = { type: 'text', text: inner };
769
- } else if (inner.type === 'text' && inner.text) {
770
- contentBlock = inner;
771
- } else {
772
- contentBlock = { type: 'text', text: JSON.stringify(inner) };
773
- }
774
- } else {
775
- contentBlock = { type: 'text', text: JSON.stringify(update.content) };
776
- }
777
-
778
- return {
779
- type: 'assistant',
780
- message: {
781
- role: 'assistant',
782
- content: [contentBlock]
783
- },
784
- session_id: params.sessionId
785
- };
786
- }
787
-
788
- // Tool call
789
- if (update.sessionUpdate === 'tool_call') {
790
- return {
791
- type: 'assistant',
792
- message: {
793
- role: 'assistant',
794
- content: [{
795
- type: 'tool_use',
796
- id: update.toolCallId,
797
- name: update.title || update.kind || 'tool',
798
- kind: update.kind || 'other',
799
- input: update.rawInput || update.input || {}
800
- }]
801
- },
802
- session_id: params.sessionId
803
- };
804
- }
805
-
806
- // Tool call update (result) - handle all statuses
807
- if (update.sessionUpdate === 'tool_call_update') {
808
- const status = update.status;
809
- const isError = status === 'failed';
810
- const isCompleted = status === 'completed';
811
-
812
- if (!isCompleted && !isError) {
813
- return {
814
- type: 'tool_status',
815
- tool_use_id: update.toolCallId,
816
- status: status,
817
- kind: update.kind || 'other',
818
- locations: update.locations || [],
819
- session_id: params.sessionId
820
- };
821
- }
822
-
823
- const contentParts = [];
824
- if (update.content && Array.isArray(update.content)) {
825
- for (const item of update.content) {
826
- if (item.type === 'content' && item.content) {
827
- const innerContent = item.content;
828
- if (innerContent.type === 'text' && innerContent.text) {
829
- contentParts.push(innerContent.text);
830
- } else if (innerContent.type === 'resource' && innerContent.resource) {
831
- contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
832
- } else {
833
- contentParts.push(JSON.stringify(innerContent));
834
- }
835
- } else if (item.type === 'diff') {
836
- const diffText = item.oldText
837
- ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
838
- : `+++ ${item.path}\n${item.newText}`;
839
- contentParts.push(diffText);
840
- } else if (item.type === 'terminal') {
841
- contentParts.push(`[Terminal: ${item.terminalId}]`);
842
- }
843
- }
844
- }
845
-
846
- const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
847
-
848
- return {
849
- type: 'user',
850
- message: {
851
- role: 'user',
852
- content: [{
853
- type: 'tool_result',
854
- tool_use_id: update.toolCallId,
855
- content: combinedContent,
856
- is_error: isError
857
- }]
858
- },
859
- session_id: params.sessionId
860
- };
861
- }
862
-
863
- // Usage update
864
- if (update.sessionUpdate === 'usage_update') {
865
- return {
866
- type: 'usage',
867
- usage: {
868
- used: update.used,
869
- size: update.size,
870
- cost: update.cost
871
- },
872
- session_id: params.sessionId
873
- };
874
- }
875
-
876
- // Plan update
877
- if (update.sessionUpdate === 'plan') {
878
- return {
879
- type: 'plan',
880
- entries: update.entries || [],
881
- session_id: params.sessionId
882
- };
883
- }
884
-
885
- return null;
886
- }
887
-
888
- // Handle prompt response (end of turn)
889
- if (message.id && message.result && message.result.stopReason) {
890
- return {
891
- type: 'result',
892
- result: '',
893
- stopReason: message.result.stopReason,
894
- usage: message.result.usage,
895
- session_id: context.sessionId
896
- };
897
- }
898
-
899
- if (message.method === 'error' || message.error) {
900
- return {
901
- type: 'error',
902
- error: message.error || message.params || { message: 'Unknown error' }
903
- };
904
- }
905
-
906
- return null;
907
- };
908
- }
909
-
910
- // Shared ACP handler
911
- const acpProtocolHandler = createACPProtocolHandler();
912
-
913
- /**
914
- * Gemini CLI Agent
915
- * Native ACP support
916
- */
917
- registry.register({
918
- id: 'gemini',
919
- name: 'Gemini CLI',
920
- command: 'gemini',
921
- protocol: 'acp',
922
- supportsStdin: false,
923
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
924
- buildArgs(prompt, config) {
925
- const args = ['--experimental-acp', '--yolo'];
926
- if (config?.model) args.push('--model', config.model);
927
- return args;
928
- },
929
- protocolHandler: acpProtocolHandler
930
- });
931
-
932
- /**
933
- * Goose Agent
934
- * Native ACP support
935
- */
936
- registry.register({
937
- id: 'goose',
938
- name: 'Goose',
939
- command: 'goose',
940
- protocol: 'acp',
941
- supportsStdin: false,
942
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
943
- buildArgs: () => ['acp'],
944
- protocolHandler: acpProtocolHandler
945
- });
946
-
947
- /**
948
- * OpenHands Agent
949
- * Native ACP support
950
- */
951
- registry.register({
952
- id: 'openhands',
953
- name: 'OpenHands',
954
- command: 'openhands',
955
- protocol: 'acp',
956
- supportsStdin: false,
957
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
958
- buildArgs: () => ['acp'],
959
- protocolHandler: acpProtocolHandler
960
- });
961
-
962
- /**
963
- * Augment Code Agent - Native ACP support
964
- */
965
- registry.register({
966
- id: 'augment',
967
- name: 'Augment Code',
968
- command: 'augment',
969
- protocol: 'acp',
970
- supportsStdin: false,
971
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
972
- buildArgs: () => ['acp'],
973
- protocolHandler: acpProtocolHandler
974
- });
975
-
976
- /**
977
- * Cline Agent - Native ACP support
978
- */
979
- registry.register({
980
- id: 'cline',
981
- name: 'Cline',
982
- command: 'cline',
983
- protocol: 'acp',
984
- supportsStdin: false,
985
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
986
- buildArgs: () => ['acp'],
987
- protocolHandler: acpProtocolHandler
988
- });
989
-
990
- /**
991
- * Kimi CLI Agent (Moonshot AI) - Native ACP support
992
- */
993
- registry.register({
994
- id: 'kimi',
995
- name: 'Kimi CLI',
996
- command: 'kimi',
997
- protocol: 'acp',
998
- supportsStdin: false,
999
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1000
- buildArgs: () => ['acp'],
1001
- protocolHandler: acpProtocolHandler
1002
- });
1003
-
1004
- /**
1005
- * Qwen Code Agent (Alibaba) - Native ACP support
1006
- */
1007
- registry.register({
1008
- id: 'qwen',
1009
- name: 'Qwen Code',
1010
- command: 'qwen-code',
1011
- protocol: 'acp',
1012
- supportsStdin: false,
1013
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1014
- buildArgs: () => ['acp'],
1015
- protocolHandler: acpProtocolHandler
1016
- });
1017
-
1018
- /**
1019
- * Codex CLI Agent (OpenAI) - ACP support
1020
- */
1021
- registry.register({
1022
- id: 'codex',
1023
- name: 'Codex CLI',
1024
- command: 'codex',
1025
- protocol: 'acp',
1026
- supportsStdin: false,
1027
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1028
- buildArgs: () => ['acp'],
1029
- protocolHandler: acpProtocolHandler
1030
- });
1031
-
1032
- /**
1033
- * Mistral Vibe Agent - Native ACP support
1034
- */
1035
- registry.register({
1036
- id: 'mistral',
1037
- name: 'Mistral Vibe',
1038
- command: 'mistral-vibe',
1039
- protocol: 'acp',
1040
- supportsStdin: false,
1041
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1042
- buildArgs: () => ['acp'],
1043
- protocolHandler: acpProtocolHandler
1044
- });
1045
-
1046
- /**
1047
- * Kiro CLI Agent - Native ACP support
1048
- */
1049
- registry.register({
1050
- id: 'kiro',
1051
- name: 'Kiro CLI',
1052
- command: 'kiro',
1053
- protocol: 'acp',
1054
- supportsStdin: false,
1055
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1056
- buildArgs: () => ['acp'],
1057
- protocolHandler: acpProtocolHandler
1058
- });
1059
-
1060
- /**
1061
- * fast-agent - Native ACP support
1062
- */
1063
- registry.register({
1064
- id: 'fast-agent',
1065
- name: 'fast-agent',
1066
- command: 'fast-agent',
1067
- protocol: 'acp',
1068
- supportsStdin: false,
1069
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1070
- buildArgs: () => ['acp'],
1071
- protocolHandler: acpProtocolHandler
1072
- });
1073
-
1074
- /**
1075
- * Kilo CLI Agent (OpenCode fork)
1076
- * Built on OpenCode, supports ACP protocol
1077
- * Uses 'kilo' command - installed via npm install -g @kilocode/cli
1078
- */
1079
- registry.register({
1080
- id: 'kilo',
1081
- name: 'Kilo CLI',
1082
- command: 'kilo',
1083
- protocol: 'acp',
1084
- supportsStdin: false,
1085
- supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'],
1086
-
1087
- buildArgs(prompt, config) {
1088
- return ['acp'];
1089
- },
1090
-
1091
- protocolHandler(message, context) {
1092
- return acpProtocolHandler(message, context);
1093
- }
1094
- });
1095
-
1096
- /**
1097
- * Main export function - runs any registered agent
1098
- */
1099
- export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
1100
- const agent = registry.get(agentId);
1101
-
1102
- if (!agent) {
1103
- throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
1104
- }
1105
-
1106
- return agent.run(prompt, cwd, config);
1107
- }
1108
-
1109
- /**
1110
- * Get list of registered agents
1111
- */
1112
- export function getRegisteredAgents() {
1113
- return registry.list();
1114
- }
1115
-
1116
- /**
1117
- * Get list of installed/available agents
1118
- */
1119
- export function getAvailableAgents() {
1120
- return registry.listACPAvailable();
1121
- }
1122
-
1123
- /**
1124
- * Check if an agent is registered
1125
- */
1126
- export function isAgentRegistered(agentId) {
1127
- return registry.has(agentId);
1128
- }
1129
-
1130
- export default runClaudeWithStreaming;
1
+ import { spawn } from 'child_process';
2
+
3
+ const isWindows = process.platform === 'win32';
4
+
5
+ function getSpawnOptions(cwd, additionalOptions = {}) {
6
+ const options = { cwd, ...additionalOptions };
7
+ if (isWindows) {
8
+ options.shell = true;
9
+ }
10
+ return options;
11
+ }
12
+
13
+ /**
14
+ * Agent Framework
15
+ * Extensible registry for AI agent CLI integrations
16
+ * Supports multiple protocols: direct JSON streaming, ACP (JSON-RPC), etc.
17
+ */
18
+
19
+ class AgentRunner {
20
+ constructor(config) {
21
+ this.id = config.id;
22
+ this.name = config.name;
23
+ this.command = config.command;
24
+ this.protocol = config.protocol || 'direct'; // 'direct' | 'acp' | etc
25
+ this.buildArgs = config.buildArgs || this.defaultBuildArgs;
26
+ this.parseOutput = config.parseOutput || this.defaultParseOutput;
27
+ this.supportsStdin = config.supportsStdin ?? true;
28
+ this.supportedFeatures = config.supportedFeatures || [];
29
+ this.protocolHandler = config.protocolHandler || null;
30
+ this.requiresAdapter = config.requiresAdapter || false;
31
+ this.adapterCommand = config.adapterCommand || null;
32
+ this.adapterArgs = config.adapterArgs || [];
33
+ }
34
+
35
+ defaultBuildArgs(prompt, config) {
36
+ return [];
37
+ }
38
+
39
+ defaultParseOutput(line) {
40
+ try {
41
+ return JSON.parse(line);
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ async run(prompt, cwd, config = {}) {
48
+ if (this.protocol === 'acp' && this.protocolHandler) {
49
+ return this.runACP(prompt, cwd, config);
50
+ }
51
+ return this.runDirect(prompt, cwd, config);
52
+ }
53
+
54
+ async runDirect(prompt, cwd, config = {}) {
55
+ return new Promise((resolve, reject) => {
56
+ const {
57
+ timeout = 300000,
58
+ onEvent = null,
59
+ onError = null,
60
+ onRateLimit = null
61
+ } = config;
62
+
63
+ const args = this.buildArgs(prompt, config);
64
+ const proc = spawn(this.command, args, getSpawnOptions(cwd));
65
+
66
+ if (config.onPid) {
67
+ try { config.onPid(proc.pid); } catch (e) {}
68
+ }
69
+
70
+ let jsonBuffer = '';
71
+ const outputs = [];
72
+ let timedOut = false;
73
+ let sessionId = null;
74
+ let rateLimited = false;
75
+ let retryAfterSec = 60;
76
+
77
+ const timeoutHandle = setTimeout(() => {
78
+ timedOut = true;
79
+ proc.kill();
80
+ reject(new Error(`${this.name} timeout after ${timeout}ms`));
81
+ }, timeout);
82
+
83
+ // Write to stdin if supported
84
+ if (this.supportsStdin) {
85
+ proc.stdin.write(prompt);
86
+ proc.stdin.end();
87
+ }
88
+
89
+ proc.stdout.on('data', (chunk) => {
90
+ if (timedOut) return;
91
+
92
+ jsonBuffer += chunk.toString();
93
+ const lines = jsonBuffer.split('\n');
94
+ jsonBuffer = lines.pop();
95
+
96
+ for (const line of lines) {
97
+ if (line.trim()) {
98
+ const parsed = this.parseOutput(line);
99
+ if (!parsed) continue;
100
+
101
+ outputs.push(parsed);
102
+
103
+ if (parsed.session_id) {
104
+ sessionId = parsed.session_id;
105
+ }
106
+
107
+ if (onEvent) {
108
+ try { onEvent(parsed); } catch (e) {
109
+ console.error(`[${this.id}] onEvent error: ${e.message}`);
110
+ }
111
+ }
112
+ }
113
+ }
114
+ });
115
+
116
+ proc.stderr.on('data', (chunk) => {
117
+ const errorText = chunk.toString();
118
+ console.error(`[${this.id}] stderr:`, errorText);
119
+
120
+ const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl|hit your limit/i);
121
+ if (rateLimitMatch) {
122
+ rateLimited = true;
123
+ const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
124
+ if (retryMatch) {
125
+ retryAfterSec = parseInt(retryMatch[1], 10) || 60;
126
+ } else {
127
+ const resetTimeMatch = errorText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
128
+ if (resetTimeMatch) {
129
+ let hours = parseInt(resetTimeMatch[1], 10);
130
+ const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
131
+ const period = resetTimeMatch[3]?.toLowerCase();
132
+ const tz = resetTimeMatch[4]?.toUpperCase() || 'UTC';
133
+
134
+ if (period === 'pm' && hours !== 12) hours += 12;
135
+ if (period === 'am' && hours === 12) hours = 0;
136
+
137
+ const now = new Date();
138
+ const resetTime = new Date(now);
139
+ resetTime.setUTCHours(hours, minutes, 0, 0);
140
+
141
+ if (resetTime <= now) {
142
+ resetTime.setUTCDate(resetTime.getUTCDate() + 1);
143
+ }
144
+
145
+ retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
146
+ }
147
+ }
148
+ }
149
+
150
+ if (onError) {
151
+ try { onError(errorText); } catch (e) {}
152
+ }
153
+ });
154
+
155
+ proc.on('close', (code) => {
156
+ clearTimeout(timeoutHandle);
157
+ if (timedOut) return;
158
+
159
+ if (rateLimited) {
160
+ const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
161
+ err.rateLimited = true;
162
+ err.retryAfterSec = retryAfterSec;
163
+ if (onRateLimit) {
164
+ try { onRateLimit({ retryAfterSec }); } catch (e) {}
165
+ }
166
+ reject(err);
167
+ return;
168
+ }
169
+
170
+ if (jsonBuffer.trim()) {
171
+ const parsed = this.parseOutput(jsonBuffer);
172
+ if (parsed) {
173
+ outputs.push(parsed);
174
+ if (parsed.session_id) sessionId = parsed.session_id;
175
+ if (onEvent) {
176
+ try { onEvent(parsed); } catch (e) {}
177
+ }
178
+ }
179
+ }
180
+
181
+ if (code === 0 || outputs.length > 0) {
182
+ resolve({ outputs, sessionId });
183
+ } else {
184
+ reject(new Error(`${this.name} exited with code ${code}`));
185
+ }
186
+ });
187
+
188
+ proc.on('error', (err) => {
189
+ clearTimeout(timeoutHandle);
190
+ reject(err);
191
+ });
192
+ });
193
+ }
194
+
195
+ async runACP(prompt, cwd, config = {}, _retryCount = 0) {
196
+ const maxRetries = config.maxRetries ?? 1;
197
+ try {
198
+ return await this._runACPOnce(prompt, cwd, config);
199
+ } catch (err) {
200
+ const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
201
+ const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
202
+ if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
203
+ const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
204
+ console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
205
+ await new Promise(r => setTimeout(r, delay));
206
+ return this.runACP(prompt, cwd, config, _retryCount + 1);
207
+ }
208
+ if (err.isPrematureEnd) {
209
+ const premErr = new Error(err.message);
210
+ premErr.isPrematureEnd = true;
211
+ premErr.exitCode = err.exitCode;
212
+ premErr.stderrText = err.stderrText;
213
+ throw premErr;
214
+ }
215
+ throw err;
216
+ }
217
+ }
218
+
219
+ async _runACPOnce(prompt, cwd, config = {}) {
220
+ return new Promise((resolve, reject) => {
221
+ const {
222
+ timeout = 300000,
223
+ onEvent = null,
224
+ onError = null
225
+ } = config;
226
+
227
+ const cmd = this.requiresAdapter && this.adapterCommand ? this.adapterCommand : this.command;
228
+ const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : this.buildArgs(prompt, config);
229
+ const args = [...baseArgs];
230
+
231
+ const proc = spawn(cmd, args, getSpawnOptions(cwd));
232
+
233
+ if (config.onPid) {
234
+ try { config.onPid(proc.pid); } catch (e) {}
235
+ }
236
+
237
+ const outputs = [];
238
+ let timedOut = false;
239
+ let sessionId = null;
240
+ let requestId = 0;
241
+ let initialized = false;
242
+ let stderrText = '';
243
+
244
+ const timeoutHandle = setTimeout(() => {
245
+ timedOut = true;
246
+ proc.kill();
247
+ reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
248
+ }, timeout);
249
+
250
+ const handleMessage = (message) => {
251
+ const normalized = this.protocolHandler(message, { sessionId, initialized });
252
+ if (!normalized) {
253
+ if (message.id === 1 && message.result) {
254
+ initialized = true;
255
+ }
256
+ return;
257
+ }
258
+
259
+ outputs.push(normalized);
260
+
261
+ if (normalized.session_id) {
262
+ sessionId = normalized.session_id;
263
+ }
264
+
265
+ if (onEvent) {
266
+ try { onEvent(normalized); } catch (e) {
267
+ console.error(`[${this.id}] onEvent error: ${e.message}`);
268
+ }
269
+ }
270
+ };
271
+
272
+ let buffer = '';
273
+ proc.stdout.on('data', (chunk) => {
274
+ if (timedOut) return;
275
+
276
+ buffer += chunk.toString();
277
+ const lines = buffer.split('\n');
278
+ buffer = lines.pop();
279
+
280
+ for (const line of lines) {
281
+ if (line.trim()) {
282
+ try {
283
+ const message = JSON.parse(line);
284
+ handleMessage(message);
285
+ } catch (e) {
286
+ console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
287
+ }
288
+ }
289
+ }
290
+ });
291
+
292
+ proc.stderr.on('data', (chunk) => {
293
+ const errorText = chunk.toString();
294
+ stderrText += errorText;
295
+ console.error(`[${this.id}] stderr:`, errorText);
296
+ if (onError) {
297
+ try { onError(errorText); } catch (e) {}
298
+ }
299
+ });
300
+
301
+ const initRequest = {
302
+ jsonrpc: '2.0',
303
+ id: ++requestId,
304
+ method: 'initialize',
305
+ params: {
306
+ protocolVersion: 1,
307
+ clientCapabilities: {
308
+ fs: { readTextFile: true, writeTextFile: true },
309
+ terminal: true
310
+ },
311
+ clientInfo: {
312
+ name: 'agentgui',
313
+ title: 'AgentGUI',
314
+ version: '1.0.0'
315
+ }
316
+ }
317
+ };
318
+ proc.stdin.write(JSON.stringify(initRequest) + '\n');
319
+
320
+ let sessionCreated = false;
321
+
322
+ const checkInitAndSend = () => {
323
+ if (initialized && !sessionCreated) {
324
+ sessionCreated = true;
325
+
326
+ const sessionParams = {
327
+ cwd: cwd,
328
+ mcpServers: []
329
+ };
330
+ if (config.model) sessionParams.model = config.model;
331
+ const sessionRequest = {
332
+ jsonrpc: '2.0',
333
+ id: ++requestId,
334
+ method: 'session/new',
335
+ params: sessionParams
336
+ };
337
+ proc.stdin.write(JSON.stringify(sessionRequest) + '\n');
338
+ } else if (!initialized) {
339
+ setTimeout(checkInitAndSend, 100);
340
+ }
341
+ };
342
+
343
+ let promptId = null;
344
+ let completed = false;
345
+
346
+ const originalHandler = handleMessage;
347
+ const enhancedHandler = (message) => {
348
+ if (message.id && message.result && message.result.sessionId) {
349
+ sessionId = message.result.sessionId;
350
+
351
+ promptId = ++requestId;
352
+ const promptRequest = {
353
+ jsonrpc: '2.0',
354
+ id: promptId,
355
+ method: 'session/prompt',
356
+ params: {
357
+ sessionId: sessionId,
358
+ prompt: [{ type: 'text', text: prompt }]
359
+ }
360
+ };
361
+ proc.stdin.write(JSON.stringify(promptRequest) + '\n');
362
+ return;
363
+ }
364
+
365
+ if (message.id === promptId && message.result && message.result.stopReason) {
366
+ completed = true;
367
+ draining = true;
368
+ clearTimeout(timeoutHandle);
369
+ // Wait a short time for any remaining events to be flushed before killing
370
+ setTimeout(() => {
371
+ draining = false;
372
+ try { proc.kill(); } catch (e) {}
373
+ resolve({ outputs, sessionId });
374
+ }, 1000);
375
+ return;
376
+ }
377
+
378
+ if (message.id === promptId && message.error) {
379
+ completed = true;
380
+ draining = true;
381
+ clearTimeout(timeoutHandle);
382
+ // Process the error message first, then delay for remaining events
383
+ originalHandler(message);
384
+ setTimeout(() => {
385
+ draining = false;
386
+ try { proc.kill(); } catch (e) {}
387
+ reject(new Error(message.error.message || 'ACP prompt error'));
388
+ }, 1000);
389
+ return;
390
+ }
391
+
392
+ originalHandler(message);
393
+ };
394
+
395
+ buffer = '';
396
+ proc.stdout.removeAllListeners('data');
397
+ let draining = false;
398
+ proc.stdout.on('data', (chunk) => {
399
+ if (timedOut) return;
400
+ // Continue processing during drain period after stopReason/error
401
+ if (completed && !draining) return;
402
+
403
+ buffer += chunk.toString();
404
+ const lines = buffer.split('\n');
405
+ buffer = lines.pop();
406
+
407
+ for (const line of lines) {
408
+ if (line.trim()) {
409
+ try {
410
+ const message = JSON.parse(line);
411
+
412
+ if (message.id === 1 && message.result) {
413
+ initialized = true;
414
+ }
415
+
416
+ enhancedHandler(message);
417
+ } catch (e) {
418
+ console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
419
+ }
420
+ }
421
+ }
422
+ });
423
+
424
+ setTimeout(checkInitAndSend, 200);
425
+
426
+ proc.on('close', (code) => {
427
+ clearTimeout(timeoutHandle);
428
+ if (timedOut || completed) return;
429
+
430
+ // Flush any remaining buffer content
431
+ if (buffer.trim()) {
432
+ try {
433
+ const message = JSON.parse(buffer.trim());
434
+ if (message.id === 1 && message.result) {
435
+ initialized = true;
436
+ }
437
+ enhancedHandler(message);
438
+ } catch (e) {
439
+ // Buffer might be incomplete, ignore parse errors on close
440
+ }
441
+ }
442
+
443
+ if (code === 0 || outputs.length > 0) {
444
+ resolve({ outputs, sessionId });
445
+ } else {
446
+ const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
447
+ const err = new Error(`${this.name} ACP exited with code ${code}${detail}`);
448
+ err.isPrematureEnd = true;
449
+ err.exitCode = code;
450
+ err.stderrText = stderrText;
451
+ reject(err);
452
+ }
453
+ });
454
+
455
+ proc.on('error', (err) => {
456
+ clearTimeout(timeoutHandle);
457
+ reject(err);
458
+ });
459
+ });
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Agent Registry
465
+ */
466
+ class AgentRegistry {
467
+ constructor() {
468
+ this.agents = new Map();
469
+ }
470
+
471
+ register(config) {
472
+ const runner = new AgentRunner(config);
473
+ this.agents.set(config.id, runner);
474
+ return runner;
475
+ }
476
+
477
+ get(agentId) {
478
+ return this.agents.get(agentId);
479
+ }
480
+
481
+ has(agentId) {
482
+ return this.agents.has(agentId);
483
+ }
484
+
485
+ list() {
486
+ return Array.from(this.agents.values()).map(a => ({
487
+ id: a.id,
488
+ name: a.name,
489
+ command: a.command,
490
+ protocol: a.protocol,
491
+ requiresAdapter: a.requiresAdapter,
492
+ supportedFeatures: a.supportedFeatures
493
+ }));
494
+ }
495
+
496
+ listACPAvailable() {
497
+ const { spawnSync } = require('child_process');
498
+ const isWindows = process.platform === 'win32';
499
+ return this.list().filter(agent => {
500
+ try {
501
+ const whichCmd = isWindows ? 'where' : 'which';
502
+ const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
503
+ if (which.status !== 0) return false;
504
+ const binPath = (which.stdout || '').trim().split('\n')[0].trim();
505
+ if (!binPath) return false;
506
+ const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows });
507
+ return check.status === 0 && (check.stdout || '').trim().length > 0;
508
+ } catch {
509
+ return false;
510
+ }
511
+ });
512
+ }
513
+ }
514
+
515
+ // Create global registry
516
+ const registry = new AgentRegistry();
517
+
518
+ /**
519
+ * Claude Code Agent
520
+ * Uses direct JSON streaming protocol
521
+ */
522
+ registry.register({
523
+ id: 'claude-code',
524
+ name: 'Claude Code',
525
+ command: 'claude',
526
+ protocol: 'direct',
527
+ supportsStdin: true,
528
+ supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip'],
529
+
530
+ buildArgs(prompt, config) {
531
+ const {
532
+ verbose = true,
533
+ outputFormat = 'stream-json',
534
+ print = true,
535
+ resumeSessionId = null,
536
+ systemPrompt = null,
537
+ model = null
538
+ } = config;
539
+
540
+ const flags = [];
541
+ if (print) flags.push('--print');
542
+ if (verbose) flags.push('--verbose');
543
+ flags.push(`--output-format=${outputFormat}`);
544
+ flags.push('--dangerously-skip-permissions');
545
+ if (model) flags.push('--model', model);
546
+ if (resumeSessionId) flags.push('--resume', resumeSessionId);
547
+ if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
548
+
549
+ return flags;
550
+ },
551
+
552
+ parseOutput(line) {
553
+ try {
554
+ return JSON.parse(line);
555
+ } catch {
556
+ return null;
557
+ }
558
+ }
559
+ });
560
+
561
+ /**
562
+ * OpenCode Agent
563
+ * Native ACP support
564
+ */
565
+ registry.register({
566
+ id: 'opencode',
567
+ name: 'OpenCode',
568
+ command: 'opencode',
569
+ protocol: 'acp',
570
+ supportsStdin: false,
571
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
572
+
573
+ buildArgs(prompt, config) {
574
+ return ['acp'];
575
+ },
576
+
577
+ protocolHandler(message, context) {
578
+ if (!message || typeof message !== 'object') return null;
579
+
580
+ // Handle ACP session/update notifications
581
+ if (message.method === 'session/update') {
582
+ const params = message.params || {};
583
+ const update = params.update || {};
584
+
585
+ // Agent message chunk (text response)
586
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
587
+ let contentBlock;
588
+
589
+ // Handle different content formats
590
+ if (typeof update.content === 'string') {
591
+ contentBlock = { type: 'text', text: update.content };
592
+ } else if (update.content.type === 'text' && update.content.text) {
593
+ contentBlock = update.content;
594
+ } else if (update.content.text) {
595
+ contentBlock = { type: 'text', text: update.content.text };
596
+ } else if (update.content.content) {
597
+ const inner = update.content.content;
598
+ if (typeof inner === 'string') {
599
+ contentBlock = { type: 'text', text: inner };
600
+ } else if (inner.type === 'text' && inner.text) {
601
+ contentBlock = inner;
602
+ } else {
603
+ contentBlock = { type: 'text', text: JSON.stringify(inner) };
604
+ }
605
+ } else {
606
+ contentBlock = { type: 'text', text: JSON.stringify(update.content) };
607
+ }
608
+
609
+ return {
610
+ type: 'assistant',
611
+ message: {
612
+ role: 'assistant',
613
+ content: [contentBlock]
614
+ },
615
+ session_id: params.sessionId
616
+ };
617
+ }
618
+
619
+ // Tool call
620
+ if (update.sessionUpdate === 'tool_call') {
621
+ return {
622
+ type: 'assistant',
623
+ message: {
624
+ role: 'assistant',
625
+ content: [{
626
+ type: 'tool_use',
627
+ id: update.toolCallId,
628
+ name: update.title || update.kind || 'tool',
629
+ kind: update.kind || 'other',
630
+ input: update.rawInput || update.input || {}
631
+ }]
632
+ },
633
+ session_id: params.sessionId
634
+ };
635
+ }
636
+
637
+ // Tool call update (result) - handle all statuses
638
+ if (update.sessionUpdate === 'tool_call_update') {
639
+ const status = update.status;
640
+ const isError = status === 'failed';
641
+ const isCompleted = status === 'completed';
642
+
643
+ if (!isCompleted && !isError) {
644
+ return {
645
+ type: 'tool_status',
646
+ tool_use_id: update.toolCallId,
647
+ status: status,
648
+ kind: update.kind || 'other',
649
+ locations: update.locations || [],
650
+ session_id: params.sessionId
651
+ };
652
+ }
653
+
654
+ const contentParts = [];
655
+ if (update.content && Array.isArray(update.content)) {
656
+ for (const item of update.content) {
657
+ if (item.type === 'content' && item.content) {
658
+ const innerContent = item.content;
659
+ if (innerContent.type === 'text' && innerContent.text) {
660
+ contentParts.push(innerContent.text);
661
+ } else if (innerContent.type === 'resource' && innerContent.resource) {
662
+ contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
663
+ } else {
664
+ contentParts.push(JSON.stringify(innerContent));
665
+ }
666
+ } else if (item.type === 'diff') {
667
+ const diffText = item.oldText
668
+ ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
669
+ : `+++ ${item.path}\n${item.newText}`;
670
+ contentParts.push(diffText);
671
+ } else if (item.type === 'terminal') {
672
+ contentParts.push(`[Terminal: ${item.terminalId}]`);
673
+ }
674
+ }
675
+ }
676
+
677
+ const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
678
+
679
+ return {
680
+ type: 'user',
681
+ message: {
682
+ role: 'user',
683
+ content: [{
684
+ type: 'tool_result',
685
+ tool_use_id: update.toolCallId,
686
+ content: combinedContent,
687
+ is_error: isError
688
+ }]
689
+ },
690
+ session_id: params.sessionId
691
+ };
692
+ }
693
+
694
+ // Usage update
695
+ if (update.sessionUpdate === 'usage_update') {
696
+ return {
697
+ type: 'usage',
698
+ usage: {
699
+ used: update.used,
700
+ size: update.size,
701
+ cost: update.cost
702
+ },
703
+ session_id: params.sessionId
704
+ };
705
+ }
706
+
707
+ // Plan update
708
+ if (update.sessionUpdate === 'plan') {
709
+ return {
710
+ type: 'plan',
711
+ entries: update.entries || [],
712
+ session_id: params.sessionId
713
+ };
714
+ }
715
+
716
+ // Skip other updates like available_commands_update
717
+ return null;
718
+ }
719
+
720
+ // Handle prompt response (end of turn)
721
+ if (message.id && message.result && message.result.stopReason) {
722
+ return {
723
+ type: 'result',
724
+ result: '',
725
+ stopReason: message.result.stopReason,
726
+ usage: message.result.usage,
727
+ session_id: context.sessionId
728
+ };
729
+ }
730
+
731
+ if (message.method === 'error' || message.error) {
732
+ return {
733
+ type: 'error',
734
+ error: message.error || message.params || { message: 'Unknown error' }
735
+ };
736
+ }
737
+
738
+ return null;
739
+ }
740
+ });
741
+
742
+ /**
743
+ * Common ACP protocol handler for all ACP agents
744
+ */
745
+ function createACPProtocolHandler() {
746
+ return function(message, context) {
747
+ if (!message || typeof message !== 'object') return null;
748
+
749
+ // Handle ACP session/update notifications
750
+ if (message.method === 'session/update') {
751
+ const params = message.params || {};
752
+ const update = params.update || {};
753
+
754
+ // Agent message chunk (text response)
755
+ if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
756
+ let contentBlock;
757
+
758
+ // Handle different content formats
759
+ if (typeof update.content === 'string') {
760
+ contentBlock = { type: 'text', text: update.content };
761
+ } else if (update.content.type === 'text' && update.content.text) {
762
+ contentBlock = update.content;
763
+ } else if (update.content.text) {
764
+ contentBlock = { type: 'text', text: update.content.text };
765
+ } else if (update.content.content) {
766
+ const inner = update.content.content;
767
+ if (typeof inner === 'string') {
768
+ contentBlock = { type: 'text', text: inner };
769
+ } else if (inner.type === 'text' && inner.text) {
770
+ contentBlock = inner;
771
+ } else {
772
+ contentBlock = { type: 'text', text: JSON.stringify(inner) };
773
+ }
774
+ } else {
775
+ contentBlock = { type: 'text', text: JSON.stringify(update.content) };
776
+ }
777
+
778
+ return {
779
+ type: 'assistant',
780
+ message: {
781
+ role: 'assistant',
782
+ content: [contentBlock]
783
+ },
784
+ session_id: params.sessionId
785
+ };
786
+ }
787
+
788
+ // Tool call
789
+ if (update.sessionUpdate === 'tool_call') {
790
+ return {
791
+ type: 'assistant',
792
+ message: {
793
+ role: 'assistant',
794
+ content: [{
795
+ type: 'tool_use',
796
+ id: update.toolCallId,
797
+ name: update.title || update.kind || 'tool',
798
+ kind: update.kind || 'other',
799
+ input: update.rawInput || update.input || {}
800
+ }]
801
+ },
802
+ session_id: params.sessionId
803
+ };
804
+ }
805
+
806
+ // Tool call update (result) - handle all statuses
807
+ if (update.sessionUpdate === 'tool_call_update') {
808
+ const status = update.status;
809
+ const isError = status === 'failed';
810
+ const isCompleted = status === 'completed';
811
+
812
+ if (!isCompleted && !isError) {
813
+ return {
814
+ type: 'tool_status',
815
+ tool_use_id: update.toolCallId,
816
+ status: status,
817
+ kind: update.kind || 'other',
818
+ locations: update.locations || [],
819
+ session_id: params.sessionId
820
+ };
821
+ }
822
+
823
+ const contentParts = [];
824
+ if (update.content && Array.isArray(update.content)) {
825
+ for (const item of update.content) {
826
+ if (item.type === 'content' && item.content) {
827
+ const innerContent = item.content;
828
+ if (innerContent.type === 'text' && innerContent.text) {
829
+ contentParts.push(innerContent.text);
830
+ } else if (innerContent.type === 'resource' && innerContent.resource) {
831
+ contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
832
+ } else {
833
+ contentParts.push(JSON.stringify(innerContent));
834
+ }
835
+ } else if (item.type === 'diff') {
836
+ const diffText = item.oldText
837
+ ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
838
+ : `+++ ${item.path}\n${item.newText}`;
839
+ contentParts.push(diffText);
840
+ } else if (item.type === 'terminal') {
841
+ contentParts.push(`[Terminal: ${item.terminalId}]`);
842
+ }
843
+ }
844
+ }
845
+
846
+ const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
847
+
848
+ return {
849
+ type: 'user',
850
+ message: {
851
+ role: 'user',
852
+ content: [{
853
+ type: 'tool_result',
854
+ tool_use_id: update.toolCallId,
855
+ content: combinedContent,
856
+ is_error: isError
857
+ }]
858
+ },
859
+ session_id: params.sessionId
860
+ };
861
+ }
862
+
863
+ // Usage update
864
+ if (update.sessionUpdate === 'usage_update') {
865
+ return {
866
+ type: 'usage',
867
+ usage: {
868
+ used: update.used,
869
+ size: update.size,
870
+ cost: update.cost
871
+ },
872
+ session_id: params.sessionId
873
+ };
874
+ }
875
+
876
+ // Plan update
877
+ if (update.sessionUpdate === 'plan') {
878
+ return {
879
+ type: 'plan',
880
+ entries: update.entries || [],
881
+ session_id: params.sessionId
882
+ };
883
+ }
884
+
885
+ return null;
886
+ }
887
+
888
+ // Handle prompt response (end of turn)
889
+ if (message.id && message.result && message.result.stopReason) {
890
+ return {
891
+ type: 'result',
892
+ result: '',
893
+ stopReason: message.result.stopReason,
894
+ usage: message.result.usage,
895
+ session_id: context.sessionId
896
+ };
897
+ }
898
+
899
+ if (message.method === 'error' || message.error) {
900
+ return {
901
+ type: 'error',
902
+ error: message.error || message.params || { message: 'Unknown error' }
903
+ };
904
+ }
905
+
906
+ return null;
907
+ };
908
+ }
909
+
910
+ // Shared ACP handler
911
+ const acpProtocolHandler = createACPProtocolHandler();
912
+
913
+ /**
914
+ * Gemini CLI Agent
915
+ * Native ACP support
916
+ */
917
+ registry.register({
918
+ id: 'gemini',
919
+ name: 'Gemini CLI',
920
+ command: 'gemini',
921
+ protocol: 'acp',
922
+ supportsStdin: false,
923
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
924
+ buildArgs(prompt, config) {
925
+ const args = ['--experimental-acp', '--yolo'];
926
+ if (config?.model) args.push('--model', config.model);
927
+ return args;
928
+ },
929
+ protocolHandler: acpProtocolHandler
930
+ });
931
+
932
+ /**
933
+ * Goose Agent
934
+ * Native ACP support
935
+ */
936
+ registry.register({
937
+ id: 'goose',
938
+ name: 'Goose',
939
+ command: 'goose',
940
+ protocol: 'acp',
941
+ supportsStdin: false,
942
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
943
+ buildArgs: () => ['acp'],
944
+ protocolHandler: acpProtocolHandler
945
+ });
946
+
947
+ /**
948
+ * OpenHands Agent
949
+ * Native ACP support
950
+ */
951
+ registry.register({
952
+ id: 'openhands',
953
+ name: 'OpenHands',
954
+ command: 'openhands',
955
+ protocol: 'acp',
956
+ supportsStdin: false,
957
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
958
+ buildArgs: () => ['acp'],
959
+ protocolHandler: acpProtocolHandler
960
+ });
961
+
962
+ /**
963
+ * Augment Code Agent - Native ACP support
964
+ */
965
+ registry.register({
966
+ id: 'augment',
967
+ name: 'Augment Code',
968
+ command: 'augment',
969
+ protocol: 'acp',
970
+ supportsStdin: false,
971
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
972
+ buildArgs: () => ['acp'],
973
+ protocolHandler: acpProtocolHandler
974
+ });
975
+
976
+ /**
977
+ * Cline Agent - Native ACP support
978
+ */
979
+ registry.register({
980
+ id: 'cline',
981
+ name: 'Cline',
982
+ command: 'cline',
983
+ protocol: 'acp',
984
+ supportsStdin: false,
985
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
986
+ buildArgs: () => ['acp'],
987
+ protocolHandler: acpProtocolHandler
988
+ });
989
+
990
+ /**
991
+ * Kimi CLI Agent (Moonshot AI) - Native ACP support
992
+ */
993
+ registry.register({
994
+ id: 'kimi',
995
+ name: 'Kimi CLI',
996
+ command: 'kimi',
997
+ protocol: 'acp',
998
+ supportsStdin: false,
999
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1000
+ buildArgs: () => ['acp'],
1001
+ protocolHandler: acpProtocolHandler
1002
+ });
1003
+
1004
+ /**
1005
+ * Qwen Code Agent (Alibaba) - Native ACP support
1006
+ */
1007
+ registry.register({
1008
+ id: 'qwen',
1009
+ name: 'Qwen Code',
1010
+ command: 'qwen-code',
1011
+ protocol: 'acp',
1012
+ supportsStdin: false,
1013
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1014
+ buildArgs: () => ['acp'],
1015
+ protocolHandler: acpProtocolHandler
1016
+ });
1017
+
1018
+ /**
1019
+ * Codex CLI Agent (OpenAI) - ACP support
1020
+ */
1021
+ registry.register({
1022
+ id: 'codex',
1023
+ name: 'Codex CLI',
1024
+ command: 'codex',
1025
+ protocol: 'acp',
1026
+ supportsStdin: false,
1027
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1028
+ buildArgs: () => ['acp'],
1029
+ protocolHandler: acpProtocolHandler
1030
+ });
1031
+
1032
+ /**
1033
+ * Mistral Vibe Agent - Native ACP support
1034
+ */
1035
+ registry.register({
1036
+ id: 'mistral',
1037
+ name: 'Mistral Vibe',
1038
+ command: 'mistral-vibe',
1039
+ protocol: 'acp',
1040
+ supportsStdin: false,
1041
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1042
+ buildArgs: () => ['acp'],
1043
+ protocolHandler: acpProtocolHandler
1044
+ });
1045
+
1046
+ /**
1047
+ * Kiro CLI Agent - Native ACP support
1048
+ */
1049
+ registry.register({
1050
+ id: 'kiro',
1051
+ name: 'Kiro CLI',
1052
+ command: 'kiro',
1053
+ protocol: 'acp',
1054
+ supportsStdin: false,
1055
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1056
+ buildArgs: () => ['acp'],
1057
+ protocolHandler: acpProtocolHandler
1058
+ });
1059
+
1060
+ /**
1061
+ * fast-agent - Native ACP support
1062
+ */
1063
+ registry.register({
1064
+ id: 'fast-agent',
1065
+ name: 'fast-agent',
1066
+ command: 'fast-agent',
1067
+ protocol: 'acp',
1068
+ supportsStdin: false,
1069
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1070
+ buildArgs: () => ['acp'],
1071
+ protocolHandler: acpProtocolHandler
1072
+ });
1073
+
1074
+ /**
1075
+ * Kilo CLI Agent (OpenCode fork)
1076
+ * Built on OpenCode, supports ACP protocol
1077
+ * Uses 'kilo' command - installed via npm install -g @kilocode/cli
1078
+ */
1079
+ registry.register({
1080
+ id: 'kilo',
1081
+ name: 'Kilo CLI',
1082
+ command: 'kilo',
1083
+ protocol: 'acp',
1084
+ supportsStdin: false,
1085
+ supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'],
1086
+
1087
+ buildArgs(prompt, config) {
1088
+ return ['acp'];
1089
+ },
1090
+
1091
+ protocolHandler(message, context) {
1092
+ return acpProtocolHandler(message, context);
1093
+ }
1094
+ });
1095
+
1096
+ /**
1097
+ * Main export function - runs any registered agent
1098
+ */
1099
+ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
1100
+ const agent = registry.get(agentId);
1101
+
1102
+ if (!agent) {
1103
+ throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
1104
+ }
1105
+
1106
+ return agent.run(prompt, cwd, config);
1107
+ }
1108
+
1109
+ /**
1110
+ * Get list of registered agents
1111
+ */
1112
+ export function getRegisteredAgents() {
1113
+ return registry.list();
1114
+ }
1115
+
1116
+ /**
1117
+ * Get list of installed/available agents
1118
+ */
1119
+ export function getAvailableAgents() {
1120
+ return registry.listACPAvailable();
1121
+ }
1122
+
1123
+ /**
1124
+ * Check if an agent is registered
1125
+ */
1126
+ export function isAgentRegistered(agentId) {
1127
+ return registry.has(agentId);
1128
+ }
1129
+
1130
+ export default runClaudeWithStreaming;