agentgui 1.0.715 → 1.0.716

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.
@@ -1,1247 +1,189 @@
1
- import { spawnSync } from 'child_process';
2
- import { execa } from 'execa';
3
-
4
- const isWindows = process.platform === 'win32';
5
-
6
- function getSpawnOptions(cwd, additionalOptions = {}) {
7
- const options = { cwd, windowsHide: true, ...additionalOptions };
8
- if (isWindows) {
9
- options.shell = true;
10
- }
11
- if (!options.env) {
12
- options.env = { ...process.env };
13
- }
14
- // Remove CLAUDECODE so claude doesn't refuse to run inside another Claude Code session.
15
- // The gm plugin still works — claude sets CLAUDECODE itself once it starts.
16
- delete options.env.CLAUDECODE;
17
- return options;
18
- }
19
-
20
- function resolveCommand(command, npxPackage) {
21
- const whichCmd = isWindows ? 'where' : 'which';
22
- const check = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
23
- if (check.status === 0 && (check.stdout || '').trim()) {
24
- return { cmd: command, prefixArgs: [] };
25
- }
26
- if (npxPackage) {
27
- const npxCheck = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 });
28
- if (npxCheck.status === 0) {
29
- return { cmd: 'npx', prefixArgs: ['--yes', npxPackage] };
30
- }
31
- const bunCheck = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 });
32
- if (bunCheck.status === 0) {
33
- return { cmd: 'bun', prefixArgs: ['x', npxPackage] };
34
- }
35
- }
36
- return { cmd: command, prefixArgs: [] };
37
- }
38
-
39
- /**
40
- * Agent Framework
41
- * Extensible registry for AI agent CLI integrations
42
- * Supports multiple protocols: direct JSON streaming, ACP (JSON-RPC), etc.
43
- */
44
-
45
- class AgentRunner {
46
- constructor(config) {
47
- this.id = config.id;
48
- this.name = config.name;
49
- this.command = config.command;
50
- this.protocol = config.protocol || 'direct'; // 'direct' | 'acp' | etc
51
- this.buildArgs = config.buildArgs || this.defaultBuildArgs;
52
- this.parseOutput = config.parseOutput || this.defaultParseOutput;
53
- this.supportsStdin = config.supportsStdin ?? true;
54
- this.closeStdin = config.closeStdin ?? false; // close stdin so process doesn't block waiting for input
55
- this.supportedFeatures = config.supportedFeatures || [];
56
- this.protocolHandler = config.protocolHandler || null;
57
- this.requiresAdapter = config.requiresAdapter || false;
58
- this.adapterCommand = config.adapterCommand || null;
59
- this.adapterArgs = config.adapterArgs || [];
60
- this.npxPackage = config.npxPackage || null;
61
- this.spawnEnv = config.spawnEnv || {};
62
- }
63
-
64
- defaultBuildArgs(prompt, config) {
65
- return [];
66
- }
67
-
68
- defaultParseOutput(line) {
69
- try {
70
- return JSON.parse(line);
71
- } catch {
72
- return null;
73
- }
74
- }
75
-
76
- async run(prompt, cwd, config = {}) {
77
- if (this.protocol === 'acp' && this.protocolHandler) {
78
- return this.runACP(prompt, cwd, config);
79
- }
80
- return this.runDirect(prompt, cwd, config);
81
- }
82
-
83
- async runDirect(prompt, cwd, config = {}) {
84
- const {
85
- timeout = 300000,
86
- onEvent = null,
87
- onError = null,
88
- onRateLimit = null
89
- } = config;
90
-
91
- if (process.env.DEBUG === '1') {
92
- const sp = config.systemPrompt;
93
- console.error(`[prompt-trace] convId=${config.conversationId} promptType=${typeof prompt} promptLen=${String(prompt).length} prompt0=${String(prompt).slice(0, 100)} sysLen=${sp ? String(sp).length : 0}`);
94
- }
95
-
96
- const args = this.buildArgs(prompt, config);
97
- const spawnOpts = getSpawnOptions(cwd);
98
- if (Object.keys(this.spawnEnv).length > 0) {
99
- spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
100
- for (const [k, v] of Object.entries(this.spawnEnv)) {
101
- if (v === undefined) delete spawnOpts.env[k];
102
- }
103
- }
104
- if (cwd) spawnOpts.env.CLAUDE_PROJECT_DIR = cwd;
105
-
106
- const proc = execa(this.command, args, {
107
- cwd,
108
- env: spawnOpts.env,
109
- stdin: this.closeStdin ? 'ignore' : 'pipe',
110
- stdout: 'pipe',
111
- stderr: 'pipe',
112
- reject: false,
113
- timeout,
114
- windowsHide: true,
115
- shell: isWindows,
116
- });
117
-
118
- console.log(`[${this.id}] Spawned PID ${proc.pid} closeStdin=${this.closeStdin}`);
119
-
120
- if (config.onPid) { try { config.onPid(proc.pid); } catch (e) {} }
121
- if (config.onProcess) { try { config.onProcess(proc); } catch (e) {} }
122
-
123
- if (this.supportsStdin && this.stdinPrompt && proc.stdin) {
124
- proc.stdin.write(typeof prompt === 'string' ? prompt : String(prompt));
125
- }
126
-
127
- const outputs = [];
128
- let sessionId = null;
129
- let rateLimited = false;
130
- let retryAfterSec = 60;
131
- let authError = false;
132
- let authErrorMessage = '';
133
- let stderrBuffer = '';
134
-
135
- proc.stderr.on('data', (chunk) => {
136
- const errorText = chunk.toString();
137
- stderrBuffer += errorText;
138
- console.error(`[${this.id}] stderr:`, errorText);
139
- const authMatch = errorText.match(/401|unauthorized|invalid.*auth|invalid.*token|auth.*failed|permission denied|access denied/i);
140
- if (authMatch) { authError = true; authErrorMessage = errorText.trim(); }
141
- const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl|hit your limit/i);
142
- if (rateLimitMatch) {
143
- rateLimited = true;
144
- const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
145
- if (retryMatch) {
146
- retryAfterSec = parseInt(retryMatch[1], 10) || 60;
147
- } else {
148
- const resetTimeMatch = errorText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
149
- if (resetTimeMatch) {
150
- let hours = parseInt(resetTimeMatch[1], 10);
151
- const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
152
- const period = resetTimeMatch[3]?.toLowerCase();
153
- if (period === 'pm' && hours !== 12) hours += 12;
154
- if (period === 'am' && hours === 12) hours = 0;
155
- const now = new Date();
156
- const resetTime = new Date(now);
157
- resetTime.setUTCHours(hours, minutes, 0, 0);
158
- if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
159
- retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
160
- }
161
- }
162
- }
163
- if (onError) { try { onError(errorText); } catch (e) {} }
164
- });
165
-
166
- let jsonBuffer = '';
167
- proc.stdout.on('data', (chunk) => {
168
- jsonBuffer += chunk.toString();
169
- const lines = jsonBuffer.split('\n');
170
- jsonBuffer = lines.pop();
171
- for (const line of lines) {
172
- if (!line.trim()) continue;
173
- const parsed = this.parseOutput(line);
174
- if (!parsed) continue;
175
- outputs.push(parsed);
176
- if (parsed.session_id) sessionId = parsed.session_id;
177
- if (onEvent) { try { onEvent(parsed); } catch (e) { console.error(`[${this.id}] onEvent error: ${e.message}`); } }
178
- }
179
- });
180
-
181
- const result = await proc;
182
-
183
- if (proc.stdin && !proc.stdin.destroyed) { try { proc.stdin.end(); } catch (e) {} }
184
-
185
- if (jsonBuffer.trim()) {
186
- const parsed = this.parseOutput(jsonBuffer);
187
- if (parsed) {
188
- outputs.push(parsed);
189
- if (parsed.session_id) sessionId = parsed.session_id;
190
- if (onEvent) { try { onEvent(parsed); } catch (e) {} }
191
- }
192
- }
193
-
194
- if (result.timedOut) throw new Error(`${this.name} timeout after ${timeout}ms`);
195
-
196
- if (authError) {
197
- const err = new Error(`Authentication failed: ${authErrorMessage || 'Invalid credentials or unauthorized access'}`);
198
- err.authError = true; err.nonRetryable = true; throw err;
199
- }
200
-
201
- if (rateLimited) {
202
- const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
203
- err.rateLimited = true; err.retryAfterSec = retryAfterSec;
204
- if (onRateLimit) { try { onRateLimit({ retryAfterSec }); } catch (e) {} }
205
- throw err;
206
- }
207
-
208
- const code = result.exitCode;
209
- if (code === 0 || outputs.length > 0) return { outputs, sessionId };
210
- const stderrHint = stderrBuffer.trim() ? `: ${stderrBuffer.trim().slice(0, 200)}` : '';
211
- const codeHint = code === 143 ? ' (SIGTERM - process was killed)' : code === 137 ? ' (SIGKILL - out of memory or force-killed)' : '';
212
- throw new Error(`${this.name} exited with code ${code}${codeHint}${stderrHint}`);
213
- }
214
-
215
- async runACP(prompt, cwd, config = {}, _retryCount = 0) {
216
- const maxRetries = config.maxRetries ?? 1;
217
- try {
218
- return await this._runACPOnce(prompt, cwd, config);
219
- } catch (err) {
220
- const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
221
- const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
222
- if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
223
- const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
224
- console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
225
- await new Promise(r => setTimeout(r, delay));
226
- return this.runACP(prompt, cwd, config, _retryCount + 1);
227
- }
228
- if (err.isPrematureEnd) {
229
- const premErr = new Error(err.message);
230
- premErr.isPrematureEnd = true;
231
- premErr.exitCode = err.exitCode;
232
- premErr.stderrText = err.stderrText;
233
- throw premErr;
234
- }
235
- throw err;
236
- }
237
- }
238
-
239
- async _runACPOnce(prompt, cwd, config = {}) {
240
- return new Promise((resolve, reject) => {
241
- const {
242
- timeout = 300000,
243
- onEvent = null,
244
- onError = null
245
- } = config;
246
-
247
- let cmd, args;
248
- if (this.requiresAdapter && this.adapterCommand) {
249
- cmd = this.adapterCommand;
250
- args = [...this.adapterArgs];
251
- } else {
252
- const resolved = resolveCommand(this.command, this.npxPackage);
253
- cmd = resolved.cmd;
254
- args = [...resolved.prefixArgs, ...this.buildArgs(prompt, config)];
255
- }
256
-
257
- const spawnOpts = getSpawnOptions(cwd);
258
- if (Object.keys(this.spawnEnv).length > 0) {
259
- spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
260
- }
261
- const proc = spawn(cmd, args, spawnOpts);
262
-
263
- if (config.onPid) {
264
- try { config.onPid(proc.pid); } catch (e) {}
265
- }
266
-
267
- if (config.onProcess) {
268
- try { config.onProcess(proc); } catch (e) {}
269
- }
270
-
271
- const outputs = [];
272
- let timedOut = false;
273
- let sessionId = null;
274
- let requestId = 0;
275
- let initialized = false;
276
- let stderrText = '';
277
-
278
- const timeoutHandle = setTimeout(() => {
279
- timedOut = true;
280
- proc.kill();
281
- reject(new Error(`${this.name} ACP timeout after ${timeout}ms`));
282
- }, timeout);
283
-
284
- const handleMessage = (message) => {
285
- const normalized = this.protocolHandler(message, { sessionId, initialized });
286
- if (!normalized) {
287
- if (message.id === 1 && message.result) {
288
- initialized = true;
289
- }
290
- return;
291
- }
292
-
293
- outputs.push(normalized);
294
-
295
- if (normalized.session_id) {
296
- sessionId = normalized.session_id;
297
- }
298
-
299
- if (onEvent) {
300
- try { onEvent(normalized); } catch (e) {
301
- console.error(`[${this.id}] onEvent error: ${e.message}`);
302
- }
303
- }
304
- };
305
-
306
- proc.stdout.on('error', () => {});
307
- proc.stderr.on('error', () => {});
308
- let buffer = '';
309
- proc.stdout.on('data', (chunk) => {
310
- if (timedOut) return;
311
-
312
- buffer += chunk.toString();
313
- const lines = buffer.split('\n');
314
- buffer = lines.pop();
315
-
316
- for (const line of lines) {
317
- if (line.trim()) {
318
- try {
319
- const message = JSON.parse(line);
320
- handleMessage(message);
321
- } catch (e) {
322
- console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
323
- }
324
- }
325
- }
326
- });
327
-
328
- proc.stderr.on('data', (chunk) => {
329
- const errorText = chunk.toString();
330
- stderrText += errorText;
331
- console.error(`[${this.id}] stderr:`, errorText);
332
- if (onError) {
333
- try { onError(errorText); } catch (e) {}
334
- }
335
- });
336
-
337
- const initRequest = {
338
- jsonrpc: '2.0',
339
- id: ++requestId,
340
- method: 'initialize',
341
- params: {
342
- protocolVersion: 1,
343
- clientCapabilities: {
344
- fs: { readTextFile: true, writeTextFile: true },
345
- terminal: true
346
- },
347
- clientInfo: {
348
- name: 'agentgui',
349
- title: 'AgentGUI',
350
- version: '1.0.0'
351
- }
352
- }
353
- };
354
- proc.stdin.on('error', () => {});
355
- proc.stdin.write(JSON.stringify(initRequest) + '\n');
356
-
357
- let sessionCreated = false;
358
-
359
- const checkInitAndSend = () => {
360
- if (initialized && !sessionCreated) {
361
- sessionCreated = true;
362
-
363
- const sessionParams = {
364
- cwd: cwd,
365
- mcpServers: []
366
- };
367
- if (config.model) sessionParams.model = config.model;
368
- if (config.subAgent) sessionParams.agent = config.subAgent;
369
- if (config.systemPrompt) sessionParams.systemPrompt = config.systemPrompt;
370
- const sessionRequest = {
371
- jsonrpc: '2.0',
372
- id: ++requestId,
373
- method: 'session/new',
374
- params: sessionParams
375
- };
376
- proc.stdin.write(JSON.stringify(sessionRequest) + '\n');
377
- } else if (!initialized) {
378
- setTimeout(checkInitAndSend, 100);
379
- }
380
- };
381
-
382
- let promptId = null;
383
- let completed = false;
384
-
385
- const originalHandler = handleMessage;
386
- const enhancedHandler = (message) => {
387
- if (message.id && message.result && message.result.sessionId) {
388
- sessionId = message.result.sessionId;
389
-
390
- promptId = ++requestId;
391
- const promptRequest = {
392
- jsonrpc: '2.0',
393
- id: promptId,
394
- method: 'session/prompt',
395
- params: {
396
- sessionId: sessionId,
397
- prompt: [{ type: 'text', text: prompt }]
398
- }
399
- };
400
- proc.stdin.write(JSON.stringify(promptRequest) + '\n');
401
- return;
402
- }
403
-
404
- if (message.id === promptId && message.result && message.result.stopReason) {
405
- completed = true;
406
- draining = true;
407
- clearTimeout(timeoutHandle);
408
- // Wait a short time for any remaining events to be flushed before killing
409
- setTimeout(() => {
410
- draining = false;
411
- try { proc.kill(); } catch (e) {}
412
- resolve({ outputs, sessionId });
413
- }, 1000);
414
- return;
415
- }
416
-
417
- if (message.id === promptId && message.error) {
418
- completed = true;
419
- draining = true;
420
- clearTimeout(timeoutHandle);
421
- // Process the error message first, then delay for remaining events
422
- originalHandler(message);
423
- setTimeout(() => {
424
- draining = false;
425
- try { proc.kill(); } catch (e) {}
426
- reject(new Error(message.error.message || 'ACP prompt error'));
427
- }, 1000);
428
- return;
429
- }
430
-
431
- originalHandler(message);
432
- };
433
-
434
- buffer = '';
435
- proc.stdout.removeAllListeners('data');
436
- let draining = false;
437
- proc.stdout.on('data', (chunk) => {
438
- if (timedOut) return;
439
- // Continue processing during drain period after stopReason/error
440
- if (completed && !draining) return;
441
-
442
- buffer += chunk.toString();
443
- const lines = buffer.split('\n');
444
- buffer = lines.pop();
445
-
446
- for (const line of lines) {
447
- if (line.trim()) {
448
- try {
449
- const message = JSON.parse(line);
450
-
451
- if (message.id === 1 && message.result) {
452
- initialized = true;
453
- }
454
-
455
- enhancedHandler(message);
456
- } catch (e) {
457
- console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100));
458
- }
459
- }
460
- }
461
- });
462
-
463
- setTimeout(checkInitAndSend, 200);
464
-
465
- proc.on('close', (code) => {
466
- clearTimeout(timeoutHandle);
467
- if (timedOut || completed) return;
468
-
469
- // Flush any remaining buffer content
470
- if (buffer.trim()) {
471
- try {
472
- const message = JSON.parse(buffer.trim());
473
- if (message.id === 1 && message.result) {
474
- initialized = true;
475
- }
476
- enhancedHandler(message);
477
- } catch (e) {
478
- // Buffer might be incomplete, ignore parse errors on close
479
- }
480
- }
481
-
482
- if (code === 0 || outputs.length > 0) {
483
- resolve({ outputs, sessionId });
484
- } else {
485
- const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
486
- const err = new Error(`${this.name} ACP exited with code ${code}${detail}`);
487
- err.isPrematureEnd = true;
488
- err.exitCode = code;
489
- err.stderrText = stderrText;
490
- reject(err);
491
- }
492
- });
493
-
494
- proc.on('error', (err) => {
495
- clearTimeout(timeoutHandle);
496
- reject(err);
497
- });
498
- });
499
- }
500
- }
501
-
502
- /**
503
- * Agent Registry
504
- */
505
- class AgentRegistry {
506
- constructor() {
507
- this.agents = new Map();
508
- }
509
-
510
- register(config) {
511
- const runner = new AgentRunner(config);
512
- this.agents.set(config.id, runner);
513
- return runner;
514
- }
515
-
516
- get(agentId) {
517
- return this.agents.get(agentId);
518
- }
519
-
520
- has(agentId) {
521
- return this.agents.has(agentId);
522
- }
523
-
524
- list() {
525
- return Array.from(this.agents.values()).map(a => ({
526
- id: a.id,
527
- name: a.name,
528
- command: a.command,
529
- protocol: a.protocol,
530
- requiresAdapter: a.requiresAdapter,
531
- supportedFeatures: a.supportedFeatures,
532
- npxPackage: a.npxPackage
533
- }));
534
- }
535
-
536
- listACPAvailable() {
537
- return this.list().filter(agent => {
538
- try {
539
- const whichCmd = isWindows ? 'where' : 'which';
540
- const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
541
- if (which.status === 0) {
542
- const binPath = (which.stdout || '').trim().split('\n')[0].trim();
543
- if (binPath) {
544
- const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows });
545
- if (check.status === 0 && (check.stdout || '').trim().length > 0) return true;
546
- }
547
- }
548
- const a = this.agents.get(agent.id);
549
- if (a && a.npxPackage) {
550
- const npxCheck = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 });
551
- if (npxCheck.status === 0) return true;
552
- const bunCheck = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 });
553
- if (bunCheck.status === 0) return true;
554
- }
555
- return false;
556
- } catch {
557
- return false;
558
- }
559
- });
560
- }
561
- }
562
-
563
- // Create global registry
564
- const registry = new AgentRegistry();
565
-
566
- /**
567
- * Claude Code Agent
568
- * Uses direct JSON streaming protocol
569
- */
570
- registry.register({
571
- id: 'claude-code',
572
- name: 'Claude Code',
573
- command: 'claude',
574
- protocol: 'direct',
575
- supportsStdin: false, // stdin must be closed — claude blocks when stdin is an open socket (non-TTY)
576
- closeStdin: true, // close stdin on spawn so claude uses positional arg prompt immediately
577
- useJsonRpcStdin: false,
578
- supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip', 'steering'],
579
- spawnEnv: { MAX_THINKING_TOKENS: '0', AGENTGUI_SUBPROCESS: '1' },
580
-
581
- buildArgs(prompt, config) {
582
- const {
583
- verbose = true,
584
- outputFormat = 'stream-json',
585
- print = true,
586
- resumeSessionId = null,
587
- systemPrompt = null,
588
- model = null
589
- } = config;
590
-
591
- const flags = [];
592
- if (print) flags.push('--print');
593
- if (verbose) flags.push('--verbose');
594
- flags.push(`--output-format=${outputFormat}`);
595
- if (model) flags.push('--model', model);
596
- if (resumeSessionId) flags.push('--resume', resumeSessionId);
597
- if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
598
- flags.push('--dangerously-skip-permissions');
599
- flags.push(typeof prompt === 'string' ? prompt : String(prompt)); // positional arg - stdin stays open separately for steering
600
-
601
- return flags;
602
- },
603
-
604
- parseOutput(line) {
605
- try {
606
- const entry = JSON.parse(line);
607
- if (!entry || typeof entry !== 'object') return null;
608
-
609
- // Filter isMeta user entries (local command caveats, not real conversation turns)
610
- if (entry.type === 'user' && entry.isMeta === true) return null;
611
-
612
- // Mark isCompactSummary entries so renderer can display them specially
613
- // (already passes through as-is, renderer checks this flag)
614
-
615
- // Detect rate limit via isApiErrorMessage + error field
616
- if (entry.isApiErrorMessage === true && entry.error === 'rate_limit') {
617
- entry._rateLimitDetected = true;
618
- }
619
-
620
- // Annotate streaming fragments vs final consolidated response
621
- // assistant entries with stop_reason: null are fragments; non-null stop_reason is final
622
- if (entry.type === 'assistant' && entry.message) {
623
- entry._isFragment = entry.message.stop_reason === null || entry.message.stop_reason === undefined;
624
- }
625
-
626
- // Extract turn duration from system/turn_duration entries
627
- if (entry.type === 'system' && entry.subtype === 'turn_duration' && entry.durationMs) {
628
- entry._turnDurationMs = entry.durationMs;
629
- }
630
-
631
- // Extract compact boundary metadata
632
- if (entry.type === 'system' && entry.subtype === 'compact_boundary' && entry.compactMetadata) {
633
- entry._preTokens = entry.compactMetadata.preTokens;
634
- }
635
-
636
- // Normalize cache usage fields into a flat structure for cost tracking
637
- if (entry.message?.usage) {
638
- const u = entry.message.usage;
639
- entry._cacheUsage = {
640
- cache_creation: u.cache_creation_input_tokens || u['cache_creation.ephemeral_1h_input_tokens'] || u['cache_creation.ephemeral_5m_input_tokens'] || 0,
641
- cache_read: u.cache_read_input_tokens || 0
642
- };
643
- }
644
-
645
- return entry;
646
- } catch {
647
- return null;
648
- }
649
- }
650
- });
651
-
652
- /**
653
- * OpenCode Agent
654
- * Native ACP support
655
- */
656
- registry.register({
657
- id: 'opencode',
658
- name: 'OpenCode',
659
- command: 'opencode',
660
- protocol: 'acp',
661
- supportsStdin: false,
662
- npxPackage: 'opencode-ai',
663
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
664
-
665
- buildArgs(prompt, config) {
666
- return ['acp'];
667
- },
668
-
669
- protocolHandler(message, context) {
670
- if (!message || typeof message !== 'object') return null;
671
-
672
- // Handle ACP session/update notifications
673
- if (message.method === 'session/update') {
674
- const params = message.params || {};
675
- const update = params.update || {};
676
-
677
- // Agent message chunk (text response)
678
- if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
679
- let contentBlock;
680
-
681
- // Handle different content formats
682
- if (typeof update.content === 'string') {
683
- contentBlock = { type: 'text', text: update.content };
684
- } else if (update.content.type === 'text' && update.content.text) {
685
- contentBlock = update.content;
686
- } else if (update.content.text) {
687
- contentBlock = { type: 'text', text: update.content.text };
688
- } else if (update.content.content) {
689
- const inner = update.content.content;
690
- if (typeof inner === 'string') {
691
- contentBlock = { type: 'text', text: inner };
692
- } else if (inner.type === 'text' && inner.text) {
693
- contentBlock = inner;
694
- } else {
695
- contentBlock = { type: 'text', text: JSON.stringify(inner) };
696
- }
697
- } else {
698
- contentBlock = { type: 'text', text: JSON.stringify(update.content) };
699
- }
700
-
701
- return {
702
- type: 'assistant',
703
- message: {
704
- role: 'assistant',
705
- content: [contentBlock]
706
- },
707
- session_id: params.sessionId
708
- };
709
- }
710
-
711
- // Tool call
712
- if (update.sessionUpdate === 'tool_call') {
713
- return {
714
- type: 'assistant',
715
- message: {
716
- role: 'assistant',
717
- content: [{
718
- type: 'tool_use',
719
- id: update.toolCallId,
720
- name: update.title || update.kind || 'tool',
721
- kind: update.kind || 'other',
722
- input: update.rawInput || update.input || {}
723
- }]
724
- },
725
- session_id: params.sessionId
726
- };
727
- }
728
-
729
- // Tool call update (result) - handle all statuses
730
- if (update.sessionUpdate === 'tool_call_update') {
731
- const status = update.status;
732
- const isError = status === 'failed';
733
- const isCompleted = status === 'completed';
734
-
735
- if (!isCompleted && !isError) {
736
- return {
737
- type: 'tool_status',
738
- tool_use_id: update.toolCallId,
739
- status: status,
740
- kind: update.kind || 'other',
741
- locations: update.locations || [],
742
- session_id: params.sessionId
743
- };
744
- }
745
-
746
- const contentParts = [];
747
- if (update.content && Array.isArray(update.content)) {
748
- for (const item of update.content) {
749
- if (item.type === 'content' && item.content) {
750
- const innerContent = item.content;
751
- if (innerContent.type === 'text' && innerContent.text) {
752
- contentParts.push(innerContent.text);
753
- } else if (innerContent.type === 'resource' && innerContent.resource) {
754
- contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
755
- } else {
756
- contentParts.push(JSON.stringify(innerContent));
757
- }
758
- } else if (item.type === 'diff') {
759
- const diffText = item.oldText
760
- ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
761
- : `+++ ${item.path}\n${item.newText}`;
762
- contentParts.push(diffText);
763
- } else if (item.type === 'terminal') {
764
- contentParts.push(`[Terminal: ${item.terminalId}]`);
765
- }
766
- }
767
- }
768
-
769
- const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
770
-
771
- return {
772
- type: 'user',
773
- message: {
774
- role: 'user',
775
- content: [{
776
- type: 'tool_result',
777
- tool_use_id: update.toolCallId,
778
- content: combinedContent,
779
- is_error: isError
780
- }]
781
- },
782
- session_id: params.sessionId
783
- };
784
- }
785
-
786
- // Usage update
787
- if (update.sessionUpdate === 'usage_update') {
788
- return {
789
- type: 'usage',
790
- usage: {
791
- used: update.used,
792
- size: update.size,
793
- cost: update.cost
794
- },
795
- session_id: params.sessionId
796
- };
797
- }
798
-
799
- // Plan update
800
- if (update.sessionUpdate === 'plan') {
801
- return {
802
- type: 'plan',
803
- entries: update.entries || [],
804
- session_id: params.sessionId
805
- };
806
- }
807
-
808
- // Skip other updates like available_commands_update
809
- return null;
810
- }
811
-
812
- // Handle prompt response (end of turn)
813
- if (message.id && message.result && message.result.stopReason) {
814
- return {
815
- type: 'result',
816
- result: '',
817
- stopReason: message.result.stopReason,
818
- usage: message.result.usage,
819
- session_id: context.sessionId
820
- };
821
- }
822
-
823
- if (message.method === 'error' || message.error) {
824
- return {
825
- type: 'error',
826
- error: message.error || message.params || { message: 'Unknown error' }
827
- };
828
- }
829
-
830
- return null;
831
- }
832
- });
833
-
834
- /**
835
- * Common ACP protocol handler for all ACP agents
836
- */
837
- function createACPProtocolHandler() {
838
- return function(message, context) {
839
- if (!message || typeof message !== 'object') return null;
840
-
841
- // Handle ACP session/update notifications
842
- if (message.method === 'session/update') {
843
- const params = message.params || {};
844
- const update = params.update || {};
845
-
846
- // Agent message chunk (text response)
847
- if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
848
- let contentBlock;
849
-
850
- // Handle different content formats
851
- if (typeof update.content === 'string') {
852
- contentBlock = { type: 'text', text: update.content };
853
- } else if (update.content.type === 'text' && update.content.text) {
854
- contentBlock = update.content;
855
- } else if (update.content.text) {
856
- contentBlock = { type: 'text', text: update.content.text };
857
- } else if (update.content.content) {
858
- const inner = update.content.content;
859
- if (typeof inner === 'string') {
860
- contentBlock = { type: 'text', text: inner };
861
- } else if (inner.type === 'text' && inner.text) {
862
- contentBlock = inner;
863
- } else {
864
- contentBlock = { type: 'text', text: JSON.stringify(inner) };
865
- }
866
- } else {
867
- contentBlock = { type: 'text', text: JSON.stringify(update.content) };
868
- }
869
-
870
- return {
871
- type: 'assistant',
872
- message: {
873
- role: 'assistant',
874
- content: [contentBlock]
875
- },
876
- session_id: params.sessionId
877
- };
878
- }
879
-
880
- // Tool call
881
- if (update.sessionUpdate === 'tool_call') {
882
- return {
883
- type: 'assistant',
884
- message: {
885
- role: 'assistant',
886
- content: [{
887
- type: 'tool_use',
888
- id: update.toolCallId,
889
- name: update.title || update.kind || 'tool',
890
- kind: update.kind || 'other',
891
- input: update.rawInput || update.input || {}
892
- }]
893
- },
894
- session_id: params.sessionId
895
- };
896
- }
897
-
898
- // Tool call update (result) - handle all statuses
899
- if (update.sessionUpdate === 'tool_call_update') {
900
- const status = update.status;
901
- const isError = status === 'failed';
902
- const isCompleted = status === 'completed';
903
-
904
- if (!isCompleted && !isError) {
905
- return {
906
- type: 'tool_status',
907
- tool_use_id: update.toolCallId,
908
- status: status,
909
- kind: update.kind || 'other',
910
- locations: update.locations || [],
911
- session_id: params.sessionId
912
- };
913
- }
914
-
915
- const contentParts = [];
916
- if (update.content && Array.isArray(update.content)) {
917
- for (const item of update.content) {
918
- if (item.type === 'content' && item.content) {
919
- const innerContent = item.content;
920
- if (innerContent.type === 'text' && innerContent.text) {
921
- contentParts.push(innerContent.text);
922
- } else if (innerContent.type === 'resource' && innerContent.resource) {
923
- contentParts.push(innerContent.resource.text || JSON.stringify(innerContent.resource));
924
- } else {
925
- contentParts.push(JSON.stringify(innerContent));
926
- }
927
- } else if (item.type === 'diff') {
928
- const diffText = item.oldText
929
- ? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
930
- : `+++ ${item.path}\n${item.newText}`;
931
- contentParts.push(diffText);
932
- } else if (item.type === 'terminal') {
933
- contentParts.push(`[Terminal: ${item.terminalId}]`);
934
- }
935
- }
936
- }
937
-
938
- const combinedContent = contentParts.join('\n') || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
939
-
940
- return {
941
- type: 'user',
942
- message: {
943
- role: 'user',
944
- content: [{
945
- type: 'tool_result',
946
- tool_use_id: update.toolCallId,
947
- content: combinedContent,
948
- is_error: isError
949
- }]
950
- },
951
- session_id: params.sessionId
952
- };
953
- }
954
-
955
- // Usage update
956
- if (update.sessionUpdate === 'usage_update') {
957
- return {
958
- type: 'usage',
959
- usage: {
960
- used: update.used,
961
- size: update.size,
962
- cost: update.cost
963
- },
964
- session_id: params.sessionId
965
- };
966
- }
967
-
968
- // Plan update
969
- if (update.sessionUpdate === 'plan') {
970
- return {
971
- type: 'plan',
972
- entries: update.entries || [],
973
- session_id: params.sessionId
974
- };
975
- }
976
-
977
- return null;
978
- }
979
-
980
- // Handle prompt response (end of turn)
981
- if (message.id && message.result && message.result.stopReason) {
982
- return {
983
- type: 'result',
984
- result: '',
985
- stopReason: message.result.stopReason,
986
- usage: message.result.usage,
987
- session_id: context.sessionId
988
- };
989
- }
990
-
991
- if (message.method === 'error' || message.error) {
992
- return {
993
- type: 'error',
994
- error: message.error || message.params || { message: 'Unknown error' }
995
- };
996
- }
997
-
998
- return null;
999
- };
1000
- }
1001
-
1002
- // Shared ACP handler
1003
- const acpProtocolHandler = createACPProtocolHandler();
1004
-
1005
- /**
1006
- * Gemini CLI Agent
1007
- * Native ACP support
1008
- */
1009
- registry.register({
1010
- id: 'gemini',
1011
- name: 'Gemini CLI',
1012
- command: 'gemini',
1013
- protocol: 'acp',
1014
- supportsStdin: false,
1015
- npxPackage: '@google/gemini-cli',
1016
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1017
- buildArgs(prompt, config) {
1018
- const args = ['--experimental-acp', '--yolo'];
1019
- if (config?.model) args.push('--model', config.model);
1020
- return args;
1021
- },
1022
- protocolHandler: acpProtocolHandler
1023
- });
1024
-
1025
- /**
1026
- * Goose Agent
1027
- * Native ACP support
1028
- */
1029
- registry.register({
1030
- id: 'goose',
1031
- name: 'Goose',
1032
- command: 'goose',
1033
- protocol: 'acp',
1034
- supportsStdin: false,
1035
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1036
- buildArgs: () => ['acp'],
1037
- protocolHandler: acpProtocolHandler
1038
- });
1039
-
1040
- /**
1041
- * OpenHands Agent
1042
- * Native ACP support
1043
- */
1044
- registry.register({
1045
- id: 'openhands',
1046
- name: 'OpenHands',
1047
- command: 'openhands',
1048
- protocol: 'acp',
1049
- supportsStdin: false,
1050
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1051
- buildArgs: () => ['acp'],
1052
- protocolHandler: acpProtocolHandler
1053
- });
1054
-
1055
- /**
1056
- * Augment Code Agent - Native ACP support
1057
- */
1058
- registry.register({
1059
- id: 'augment',
1060
- name: 'Augment Code',
1061
- command: 'augment',
1062
- protocol: 'acp',
1063
- supportsStdin: false,
1064
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1065
- buildArgs: () => ['acp'],
1066
- protocolHandler: acpProtocolHandler
1067
- });
1068
-
1069
- /**
1070
- * Cline Agent - Native ACP support
1071
- */
1072
- registry.register({
1073
- id: 'cline',
1074
- name: 'Cline',
1075
- command: 'cline',
1076
- protocol: 'acp',
1077
- supportsStdin: false,
1078
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1079
- buildArgs: () => ['acp'],
1080
- protocolHandler: acpProtocolHandler
1081
- });
1082
-
1083
- /**
1084
- * Kimi CLI Agent (Moonshot AI) - Native ACP support
1085
- */
1086
- registry.register({
1087
- id: 'kimi',
1088
- name: 'Kimi CLI',
1089
- command: 'kimi',
1090
- protocol: 'acp',
1091
- supportsStdin: false,
1092
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1093
- buildArgs: () => ['acp'],
1094
- protocolHandler: acpProtocolHandler
1095
- });
1096
-
1097
- /**
1098
- * Qwen Code Agent (Alibaba) - Native ACP support
1099
- */
1100
- registry.register({
1101
- id: 'qwen',
1102
- name: 'Qwen Code',
1103
- command: 'qwen-code',
1104
- protocol: 'acp',
1105
- supportsStdin: false,
1106
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1107
- buildArgs: () => ['acp'],
1108
- protocolHandler: acpProtocolHandler
1109
- });
1110
-
1111
- /**
1112
- * Codex CLI Agent (OpenAI) - ACP support
1113
- */
1114
- registry.register({
1115
- id: 'codex',
1116
- name: 'Codex CLI',
1117
- command: 'codex',
1118
- protocol: 'acp',
1119
- supportsStdin: false,
1120
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1121
- buildArgs: () => ['acp'],
1122
- protocolHandler: acpProtocolHandler
1123
- });
1124
-
1125
- /**
1126
- * Mistral Vibe Agent - Native ACP support
1127
- */
1128
- registry.register({
1129
- id: 'mistral',
1130
- name: 'Mistral Vibe',
1131
- command: 'mistral-vibe',
1132
- protocol: 'acp',
1133
- supportsStdin: false,
1134
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1135
- buildArgs: () => ['acp'],
1136
- protocolHandler: acpProtocolHandler
1137
- });
1138
-
1139
- /**
1140
- * Kiro CLI Agent - Native ACP support
1141
- */
1142
- registry.register({
1143
- id: 'kiro',
1144
- name: 'Kiro CLI',
1145
- command: 'kiro',
1146
- protocol: 'acp',
1147
- supportsStdin: false,
1148
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1149
- buildArgs: () => ['acp'],
1150
- protocolHandler: acpProtocolHandler
1151
- });
1152
-
1153
- /**
1154
- * fast-agent - Native ACP support
1155
- */
1156
- registry.register({
1157
- id: 'fast-agent',
1158
- name: 'fast-agent',
1159
- command: 'fast-agent',
1160
- protocol: 'acp',
1161
- supportsStdin: false,
1162
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
1163
- buildArgs: () => ['acp'],
1164
- protocolHandler: acpProtocolHandler
1165
- });
1166
-
1167
- /**
1168
- * Kilo CLI Agent (OpenCode fork)
1169
- * Built on OpenCode, supports ACP protocol
1170
- * Uses 'kilo' command - installed via npm install -g @kilocode/cli
1171
- */
1172
- registry.register({
1173
- id: 'kilo',
1174
- name: 'Kilo CLI',
1175
- command: 'kilo',
1176
- protocol: 'acp',
1177
- supportsStdin: false,
1178
- npxPackage: '@kilocode/cli',
1179
- supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'],
1180
-
1181
- buildArgs(prompt, config) {
1182
- return ['acp'];
1183
- },
1184
-
1185
- protocolHandler(message, context) {
1186
- return acpProtocolHandler(message, context);
1187
- }
1188
- });
1189
-
1190
- /**
1191
- * Main export function - runs any registered agent
1192
- */
1193
- export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
1194
- prompt = typeof prompt === 'string' ? prompt : (prompt ? JSON.stringify(prompt) : '');
1195
- const agent = registry.get(agentId);
1196
-
1197
- if (!agent) {
1198
- throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
1199
- }
1200
-
1201
- const enhancedConfig = { ...config };
1202
- if (!enhancedConfig.systemPrompt) {
1203
- enhancedConfig.systemPrompt = '';
1204
- }
1205
-
1206
- // Append communication guidelines for all agents
1207
- const communicationGuidelines = `
1208
- RESPONSE FORMAT: Respond in short, plain text sentences only. No markdown. No bullet points. No bold or italic text. No headers. No numbered lists. No code blocks in prose responses. Write as if speaking aloud. Keep responses concise and conversational. Only share what the user needs to know: errors, required actions, or direct answers. Do not narrate progress or summarize completed steps.
1209
- `;
1210
-
1211
- if (!enhancedConfig.systemPrompt.includes('RESPONSE FORMAT')) {
1212
- enhancedConfig.systemPrompt = communicationGuidelines + enhancedConfig.systemPrompt;
1213
- }
1214
-
1215
- if (agentId && agentId !== 'claude-code') {
1216
- const displayAgentId = agentId.split('-·-')[0];
1217
- const agentPrefix = `use ${displayAgentId} subagent to. `;
1218
- if (!enhancedConfig.systemPrompt.includes(agentPrefix)) {
1219
- enhancedConfig.systemPrompt = agentPrefix + enhancedConfig.systemPrompt;
1220
- }
1221
- }
1222
-
1223
- return agent.run(prompt, cwd, enhancedConfig);
1224
- }
1225
-
1226
- /**
1227
- * Get list of registered agents
1228
- */
1229
- export function getRegisteredAgents() {
1230
- return registry.list();
1231
- }
1232
-
1233
- /**
1234
- * Get list of installed/available agents
1235
- */
1236
- export function getAvailableAgents() {
1237
- return registry.listACPAvailable();
1238
- }
1239
-
1240
- /**
1241
- * Check if an agent is registered
1242
- */
1243
- export function isAgentRegistered(agentId) {
1244
- return registry.has(agentId);
1245
- }
1246
-
1247
- export default runClaudeWithStreaming;
1
+ import { spawnSync } from 'child_process';
2
+ import { execa } from 'execa';
3
+ import { registerAllAgents } from './agent-registry-configs.js';
4
+ import { runACPWithRetry } from './acp-runner.js';
5
+
6
+ const isWindows = process.platform === 'win32';
7
+
8
+ function getSpawnOptions(cwd, additionalOptions = {}) {
9
+ const options = { cwd, windowsHide: true, ...additionalOptions };
10
+ if (isWindows) options.shell = true;
11
+ if (!options.env) options.env = { ...process.env };
12
+ delete options.env.CLAUDECODE;
13
+ return options;
14
+ }
15
+
16
+ function resolveCommand(command, npxPackage) {
17
+ const whichCmd = isWindows ? 'where' : 'which';
18
+ const check = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
19
+ if (check.status === 0 && (check.stdout || '').trim()) return { cmd: command, prefixArgs: [] };
20
+ if (npxPackage) {
21
+ const npxCheck = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 });
22
+ if (npxCheck.status === 0) return { cmd: 'npx', prefixArgs: ['--yes', npxPackage] };
23
+ const bunCheck = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 });
24
+ if (bunCheck.status === 0) return { cmd: 'bun', prefixArgs: ['x', npxPackage] };
25
+ }
26
+ return { cmd: command, prefixArgs: [] };
27
+ }
28
+
29
+ class AgentRunner {
30
+ constructor(config) {
31
+ this.id = config.id;
32
+ this.name = config.name;
33
+ this.command = config.command;
34
+ this.protocol = config.protocol || 'direct';
35
+ this.buildArgs = config.buildArgs || this.defaultBuildArgs;
36
+ this.parseOutput = config.parseOutput || this.defaultParseOutput;
37
+ this.supportsStdin = config.supportsStdin ?? true;
38
+ this.closeStdin = config.closeStdin ?? false;
39
+ this.supportedFeatures = config.supportedFeatures || [];
40
+ this.protocolHandler = config.protocolHandler || null;
41
+ this.requiresAdapter = config.requiresAdapter || false;
42
+ this.adapterCommand = config.adapterCommand || null;
43
+ this.adapterArgs = config.adapterArgs || [];
44
+ this.npxPackage = config.npxPackage || null;
45
+ this.spawnEnv = config.spawnEnv || {};
46
+ }
47
+
48
+ defaultBuildArgs() { return []; }
49
+ defaultParseOutput(line) { try { return JSON.parse(line); } catch { return null; } }
50
+
51
+ async run(prompt, cwd, config = {}) {
52
+ if (this.protocol === 'acp' && this.protocolHandler) return this.runACP(prompt, cwd, config);
53
+ return this.runDirect(prompt, cwd, config);
54
+ }
55
+
56
+ async runDirect(prompt, cwd, config = {}) {
57
+ const { timeout = 300000, onEvent = null, onError = null, onRateLimit = null } = config;
58
+ if (process.env.DEBUG === '1') {
59
+ const sp = config.systemPrompt;
60
+ console.error(`[prompt-trace] convId=${config.conversationId} promptType=${typeof prompt} promptLen=${String(prompt).length} prompt0=${String(prompt).slice(0, 100)} sysLen=${sp ? String(sp).length : 0}`);
61
+ }
62
+ const args = this.buildArgs(prompt, config);
63
+ const spawnOpts = getSpawnOptions(cwd);
64
+ if (Object.keys(this.spawnEnv).length > 0) {
65
+ spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
66
+ for (const [k, v] of Object.entries(this.spawnEnv)) { if (v === undefined) delete spawnOpts.env[k]; }
67
+ }
68
+ if (cwd) spawnOpts.env.CLAUDE_PROJECT_DIR = cwd;
69
+ const proc = execa(this.command, args, { cwd, env: spawnOpts.env, stdin: this.closeStdin ? 'ignore' : 'pipe', stdout: 'pipe', stderr: 'pipe', reject: false, timeout, windowsHide: true, shell: isWindows });
70
+ console.log(`[${this.id}] Spawned PID ${proc.pid} closeStdin=${this.closeStdin}`);
71
+ if (config.onPid) { try { config.onPid(proc.pid); } catch (e) {} }
72
+ if (config.onProcess) { try { config.onProcess(proc); } catch (e) {} }
73
+ if (this.supportsStdin && this.stdinPrompt && proc.stdin) {
74
+ proc.stdin.write(typeof prompt === 'string' ? prompt : String(prompt));
75
+ }
76
+ const outputs = [];
77
+ let sessionId = null, rateLimited = false, retryAfterSec = 60, authError = false, authErrorMessage = '', stderrBuffer = '';
78
+
79
+ proc.stderr.on('data', (chunk) => {
80
+ const errorText = chunk.toString();
81
+ stderrBuffer += errorText;
82
+ console.error(`[${this.id}] stderr:`, errorText);
83
+ if (errorText.match(/401|unauthorized|invalid.*auth|invalid.*token|auth.*failed|permission denied|access denied/i)) { authError = true; authErrorMessage = errorText.trim(); }
84
+ if (errorText.match(/rate.?limit|429|too many requests|overloaded|throttl|hit your limit/i)) {
85
+ rateLimited = true;
86
+ const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
87
+ if (retryMatch) { retryAfterSec = parseInt(retryMatch[1], 10) || 60; }
88
+ else {
89
+ const resetTimeMatch = errorText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
90
+ if (resetTimeMatch) {
91
+ let hours = parseInt(resetTimeMatch[1], 10);
92
+ const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
93
+ const period = resetTimeMatch[3]?.toLowerCase();
94
+ if (period === 'pm' && hours !== 12) hours += 12;
95
+ if (period === 'am' && hours === 12) hours = 0;
96
+ const now = new Date(), resetTime = new Date(now);
97
+ resetTime.setUTCHours(hours, minutes, 0, 0);
98
+ if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
99
+ retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
100
+ }
101
+ }
102
+ }
103
+ if (onError) { try { onError(errorText); } catch (e) {} }
104
+ });
105
+
106
+ let jsonBuffer = '';
107
+ proc.stdout.on('data', (chunk) => {
108
+ jsonBuffer += chunk.toString();
109
+ const lines = jsonBuffer.split('\n');
110
+ jsonBuffer = lines.pop();
111
+ for (const line of lines) {
112
+ if (!line.trim()) continue;
113
+ const parsed = this.parseOutput(line);
114
+ if (!parsed) continue;
115
+ outputs.push(parsed);
116
+ if (parsed.session_id) sessionId = parsed.session_id;
117
+ if (onEvent) { try { onEvent(parsed); } catch (e) { console.error(`[${this.id}] onEvent error: ${e.message}`); } }
118
+ }
119
+ });
120
+
121
+ const result = await proc;
122
+ if (proc.stdin && !proc.stdin.destroyed) { try { proc.stdin.end(); } catch (e) {} }
123
+ if (jsonBuffer.trim()) {
124
+ const parsed = this.parseOutput(jsonBuffer);
125
+ if (parsed) { outputs.push(parsed); if (parsed.session_id) sessionId = parsed.session_id; if (onEvent) { try { onEvent(parsed); } catch (e) {} } }
126
+ }
127
+ if (result.timedOut) throw new Error(`${this.name} timeout after ${timeout}ms`);
128
+ if (authError) { const err = new Error(`Authentication failed: ${authErrorMessage || 'Invalid credentials or unauthorized access'}`); err.authError = true; err.nonRetryable = true; throw err; }
129
+ if (rateLimited) { const err = new Error(`Rate limited - retry after ${retryAfterSec}s`); err.rateLimited = true; err.retryAfterSec = retryAfterSec; if (onRateLimit) { try { onRateLimit({ retryAfterSec }); } catch (e) {} } throw err; }
130
+ const code = result.exitCode;
131
+ if (code === 0 || outputs.length > 0) return { outputs, sessionId };
132
+ const stderrHint = stderrBuffer.trim() ? `: ${stderrBuffer.trim().slice(0, 200)}` : '';
133
+ const codeHint = code === 143 ? ' (SIGTERM - process was killed)' : code === 137 ? ' (SIGKILL - out of memory or force-killed)' : '';
134
+ throw new Error(`${this.name} exited with code ${code}${codeHint}${stderrHint}`);
135
+ }
136
+
137
+ async runACP(prompt, cwd, config = {}, _retryCount = 0) {
138
+ return runACPWithRetry(this, prompt, cwd, config, _retryCount);
139
+ }
140
+ }
141
+
142
+ class AgentRegistry {
143
+ constructor() { this.agents = new Map(); }
144
+ register(config) { const runner = new AgentRunner(config); this.agents.set(config.id, runner); return runner; }
145
+ get(agentId) { return this.agents.get(agentId); }
146
+ has(agentId) { return this.agents.has(agentId); }
147
+ list() {
148
+ return Array.from(this.agents.values()).map(a => ({ id: a.id, name: a.name, command: a.command, protocol: a.protocol, requiresAdapter: a.requiresAdapter, supportedFeatures: a.supportedFeatures, npxPackage: a.npxPackage }));
149
+ }
150
+ listACPAvailable() {
151
+ return this.list().filter(agent => {
152
+ try {
153
+ const whichCmd = isWindows ? 'where' : 'which';
154
+ const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
155
+ if (which.status === 0) {
156
+ const binPath = (which.stdout || '').trim().split('\n')[0].trim();
157
+ if (binPath) { const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows }); if (check.status === 0 && (check.stdout || '').trim().length > 0) return true; }
158
+ }
159
+ const a = this.agents.get(agent.id);
160
+ if (a && a.npxPackage) { const npxCheck = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 }); if (npxCheck.status === 0) return true; const bunCheck = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 }); if (bunCheck.status === 0) return true; }
161
+ return false;
162
+ } catch { return false; }
163
+ });
164
+ }
165
+ }
166
+
167
+ const registry = new AgentRegistry();
168
+ registerAllAgents(registry);
169
+
170
+ export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
171
+ prompt = typeof prompt === 'string' ? prompt : (prompt ? JSON.stringify(prompt) : '');
172
+ const agent = registry.get(agentId);
173
+ if (!agent) throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
174
+ const enhancedConfig = { ...config };
175
+ if (!enhancedConfig.systemPrompt) enhancedConfig.systemPrompt = '';
176
+ const communicationGuidelines = `\nRESPONSE FORMAT: Respond in short, plain text sentences only. No markdown. No bullet points. No bold or italic text. No headers. No numbered lists. No code blocks in prose responses. Write as if speaking aloud. Keep responses concise and conversational. Only share what the user needs to know: errors, required actions, or direct answers. Do not narrate progress or summarize completed steps.\n`;
177
+ if (!enhancedConfig.systemPrompt.includes('RESPONSE FORMAT')) enhancedConfig.systemPrompt = communicationGuidelines + enhancedConfig.systemPrompt;
178
+ if (agentId && agentId !== 'claude-code') {
179
+ const displayAgentId = agentId.split('-·-')[0];
180
+ const agentPrefix = `use ${displayAgentId} subagent to. `;
181
+ if (!enhancedConfig.systemPrompt.includes(agentPrefix)) enhancedConfig.systemPrompt = agentPrefix + enhancedConfig.systemPrompt;
182
+ }
183
+ return agent.run(prompt, cwd, enhancedConfig);
184
+ }
185
+
186
+ export function getRegisteredAgents() { return registry.list(); }
187
+ export function getAvailableAgents() { return registry.listACPAvailable(); }
188
+ export function isAgentRegistered(agentId) { return registry.has(agentId); }
189
+ export default runClaudeWithStreaming;