agentgui 1.0.758 → 1.0.759

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