agentgui 1.0.758 → 1.0.760
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/claude-runner.js +1266 -189
- package/lib/routes-oauth.js +105 -0
- package/lib/routes-util.js +106 -0
- package/package.json +1 -1
- package/server.js +8 -227
package/lib/claude-runner.js
CHANGED
|
@@ -1,189 +1,1266 @@
|
|
|
1
|
-
import { spawnSync } from 'child_process';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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;
|