agentgui 1.0.831 → 1.0.833
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/CHANGELOG.md +9 -0
- package/database-migrations-acp.js +133 -0
- package/database-migrations.js +149 -0
- package/database-schema.js +175 -0
- package/database.js +80 -650
- package/lib/claude-runner-acp.js +155 -0
- package/lib/claude-runner-agents.js +104 -0
- package/lib/claude-runner-direct.js +116 -0
- package/lib/claude-runner-run.js +49 -0
- package/lib/claude-runner.js +55 -1266
- package/lib/plugins/agents-plugin.js +0 -3
- package/lib/speech-manager.js +1 -7
- package/lib/ws-handlers-conv.js +0 -144
- package/lib/ws-handlers-conv2.js +169 -0
- package/lib/ws-handlers-session.js +3 -117
- package/lib/ws-handlers-session2.js +106 -0
- package/package.json +1 -1
- package/server.js +12 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { AgentRunner, getSpawnOptions, resolveCommand } from './claude-runner.js';
|
|
3
|
+
|
|
4
|
+
function buildEnhancedHandler({ proc, outputs, sessionRef, promptIdRef, completedRef, drainingRef, requestIdRef, originalHandler, resolve, reject, clearTimeoutHandle, promptText }) {
|
|
5
|
+
return function(message) {
|
|
6
|
+
if (message.id && message.result && message.result.sessionId) {
|
|
7
|
+
sessionRef.value = message.result.sessionId;
|
|
8
|
+
promptIdRef.value = ++requestIdRef.value;
|
|
9
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: promptIdRef.value, method: 'session/prompt', params: { sessionId: sessionRef.value, prompt: [{ type: 'text', text: promptText }] } }) + '\n');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (promptIdRef.value !== null && message.id === promptIdRef.value && message.result && message.result.stopReason) {
|
|
13
|
+
completedRef.value = true; drainingRef.value = true;
|
|
14
|
+
clearTimeoutHandle();
|
|
15
|
+
setTimeout(() => { drainingRef.value = false; try { proc.kill(); } catch (e) {} resolve({ outputs, sessionId: sessionRef.value }); }, 1000);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (promptIdRef.value !== null && message.id === promptIdRef.value && message.error) {
|
|
19
|
+
completedRef.value = true; drainingRef.value = true;
|
|
20
|
+
clearTimeoutHandle();
|
|
21
|
+
originalHandler(message);
|
|
22
|
+
setTimeout(() => { drainingRef.value = false; try { proc.kill(); } catch (e) {} reject(new Error(message.error.message || 'ACP prompt error')); }, 1000);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
originalHandler(message);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
AgentRunner.prototype.runACP = async function(prompt, cwd, config = {}, _retryCount = 0) {
|
|
30
|
+
const maxRetries = config.maxRetries ?? 1;
|
|
31
|
+
try {
|
|
32
|
+
return await this._runACPOnce(prompt, cwd, config);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
|
|
35
|
+
const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
|
|
36
|
+
if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
|
|
37
|
+
const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
|
|
38
|
+
console.error(`[${this.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
39
|
+
await new Promise(r => setTimeout(r, delay));
|
|
40
|
+
return this.runACP(prompt, cwd, config, _retryCount + 1);
|
|
41
|
+
}
|
|
42
|
+
if (err.isPrematureEnd) {
|
|
43
|
+
const premErr = new Error(err.message);
|
|
44
|
+
premErr.isPrematureEnd = true;
|
|
45
|
+
premErr.exitCode = err.exitCode;
|
|
46
|
+
premErr.stderrText = err.stderrText;
|
|
47
|
+
throw premErr;
|
|
48
|
+
}
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
AgentRunner.prototype._runACPOnce = function(prompt, cwd, config = {}) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const { timeout = 300000, onEvent = null, onError = null } = config;
|
|
56
|
+
let cmd, args;
|
|
57
|
+
if (this.requiresAdapter && this.adapterCommand) {
|
|
58
|
+
cmd = this.adapterCommand; args = [...this.adapterArgs];
|
|
59
|
+
} else {
|
|
60
|
+
const resolved = resolveCommand(this.command, this.npxPackage);
|
|
61
|
+
cmd = resolved.cmd; args = [...resolved.prefixArgs, ...this.buildArgs(prompt, config)];
|
|
62
|
+
}
|
|
63
|
+
const spawnOpts = getSpawnOptions(cwd);
|
|
64
|
+
if (Object.keys(this.spawnEnv).length > 0) spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
|
|
65
|
+
const proc = spawn(cmd, args, spawnOpts);
|
|
66
|
+
|
|
67
|
+
if (config.onPid) { try { config.onPid(proc.pid); } catch (e) { console.error(`[${this.id}] onPid callback failed:`, e.message); } }
|
|
68
|
+
if (config.onProcess) { try { config.onProcess(proc); } catch (e) { console.error(`[${this.id}] onProcess callback failed:`, e.message); } }
|
|
69
|
+
|
|
70
|
+
const outputs = [];
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
let stderrText = '';
|
|
73
|
+
const sessionRef = { value: null };
|
|
74
|
+
const promptIdRef = { value: null };
|
|
75
|
+
const completedRef = { value: false };
|
|
76
|
+
const drainingRef = { value: false };
|
|
77
|
+
const requestIdRef = { value: 0 };
|
|
78
|
+
let initialized = false;
|
|
79
|
+
|
|
80
|
+
const timeoutHandle = setTimeout(() => { timedOut = true; proc.kill(); reject(new Error(`${this.name} ACP timeout after ${timeout}ms`)); }, timeout);
|
|
81
|
+
const clearTimeoutHandle = () => clearTimeout(timeoutHandle);
|
|
82
|
+
|
|
83
|
+
const originalHandler = (message) => {
|
|
84
|
+
const normalized = this.protocolHandler(message, { sessionId: sessionRef.value, initialized });
|
|
85
|
+
if (!normalized) { if (message.id === 1 && message.result) initialized = true; return; }
|
|
86
|
+
outputs.push(normalized);
|
|
87
|
+
if (normalized.session_id) sessionRef.value = normalized.session_id;
|
|
88
|
+
if (onEvent) { try { onEvent(normalized); } catch (e) { console.error(`[${this.id}] onEvent error: ${e.message}`); } }
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const enhancedHandler = buildEnhancedHandler({ proc, outputs, sessionRef, promptIdRef, completedRef, drainingRef, requestIdRef, originalHandler, resolve, reject, clearTimeoutHandle, promptText: prompt });
|
|
92
|
+
|
|
93
|
+
proc.stdout.on('error', () => {});
|
|
94
|
+
proc.stderr.on('error', () => {});
|
|
95
|
+
proc.stderr.on('data', (chunk) => {
|
|
96
|
+
const errorText = chunk.toString();
|
|
97
|
+
stderrText += errorText;
|
|
98
|
+
console.error(`[${this.id}] stderr:`, errorText);
|
|
99
|
+
if (onError) { try { onError(errorText); } catch (e) {} }
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
proc.stdin.on('error', () => {});
|
|
103
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestIdRef.value, method: 'initialize', params: { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true }, clientInfo: { name: 'agentgui', title: 'AgentGUI', version: '1.0.0' } } }) + '\n');
|
|
104
|
+
|
|
105
|
+
let buffer = '';
|
|
106
|
+
proc.stdout.on('data', (chunk) => {
|
|
107
|
+
if (timedOut || (completedRef.value && !drainingRef.value)) return;
|
|
108
|
+
buffer += chunk.toString();
|
|
109
|
+
const lines = buffer.split('\n'); buffer = lines.pop();
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (line.trim()) {
|
|
112
|
+
try {
|
|
113
|
+
const message = JSON.parse(line);
|
|
114
|
+
if (message.id === 1 && message.result) initialized = true;
|
|
115
|
+
enhancedHandler(message);
|
|
116
|
+
} catch (e) { console.error(`[${this.id}] JSON parse error:`, line.substring(0, 100)); }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const checkInitAndSend = () => {
|
|
122
|
+
if (initialized) {
|
|
123
|
+
const sessionParams = { cwd, mcpServers: [] };
|
|
124
|
+
if (config.model) sessionParams.model = config.model;
|
|
125
|
+
if (config.subAgent) sessionParams.agent = config.subAgent;
|
|
126
|
+
if (config.systemPrompt) sessionParams.systemPrompt = config.systemPrompt;
|
|
127
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestIdRef.value, method: 'session/new', params: sessionParams }) + '\n');
|
|
128
|
+
} else {
|
|
129
|
+
setTimeout(checkInitAndSend, 100);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
setTimeout(checkInitAndSend, 200);
|
|
133
|
+
|
|
134
|
+
proc.on('close', (code) => {
|
|
135
|
+
clearTimeout(timeoutHandle);
|
|
136
|
+
if (timedOut || completedRef.value) return;
|
|
137
|
+
if (buffer.trim()) {
|
|
138
|
+
try {
|
|
139
|
+
const message = JSON.parse(buffer.trim());
|
|
140
|
+
if (message.id === 1 && message.result) initialized = true;
|
|
141
|
+
enhancedHandler(message);
|
|
142
|
+
} catch (e) {}
|
|
143
|
+
}
|
|
144
|
+
if (code === 0 || outputs.length > 0) resolve({ outputs, sessionId: sessionRef.value });
|
|
145
|
+
else {
|
|
146
|
+
const detail = stderrText ? `: ${stderrText.substring(0, 200)}` : '';
|
|
147
|
+
const err = new Error(`${this.name} ACP exited with code ${code}${detail}`);
|
|
148
|
+
err.isPrematureEnd = true; err.exitCode = code; err.stderrText = stderrText;
|
|
149
|
+
reject(err);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
proc.on('error', (err) => { clearTimeout(timeoutHandle); reject(err); });
|
|
154
|
+
});
|
|
155
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { AgentRunner } from './claude-runner.js';
|
|
3
|
+
import { acpProtocolHandler } from './acp-protocol.js';
|
|
4
|
+
import './claude-runner-direct.js';
|
|
5
|
+
import './claude-runner-acp.js';
|
|
6
|
+
|
|
7
|
+
const isWindows = process.platform === 'win32';
|
|
8
|
+
|
|
9
|
+
export class AgentRegistry {
|
|
10
|
+
constructor() { this.agents = new Map(); }
|
|
11
|
+
|
|
12
|
+
register(config) {
|
|
13
|
+
const runner = new AgentRunner(config);
|
|
14
|
+
this.agents.set(config.id, runner);
|
|
15
|
+
return runner;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(agentId) { return this.agents.get(agentId); }
|
|
19
|
+
has(agentId) { return this.agents.has(agentId); }
|
|
20
|
+
|
|
21
|
+
list() {
|
|
22
|
+
return Array.from(this.agents.values()).map(a => ({
|
|
23
|
+
id: a.id, name: a.name, command: a.command, protocol: a.protocol,
|
|
24
|
+
requiresAdapter: a.requiresAdapter, supportedFeatures: a.supportedFeatures, npxPackage: a.npxPackage
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
listACPAvailable() {
|
|
29
|
+
return this.list().filter(agent => {
|
|
30
|
+
try {
|
|
31
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
32
|
+
const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
|
|
33
|
+
if (which.status === 0) {
|
|
34
|
+
const binPath = (which.stdout || '').trim().split('\n')[0].trim();
|
|
35
|
+
if (binPath) {
|
|
36
|
+
const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows });
|
|
37
|
+
if (check.status === 0 && (check.stdout || '').trim().length > 0) return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const a = this.agents.get(agent.id);
|
|
41
|
+
if (a && a.npxPackage) {
|
|
42
|
+
const npxCheck = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 });
|
|
43
|
+
if (npxCheck.status === 0) return true;
|
|
44
|
+
const bunCheck = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 });
|
|
45
|
+
if (bunCheck.status === 0) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
} catch { return false; }
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const registry = new AgentRegistry();
|
|
54
|
+
|
|
55
|
+
function parseClaudeOutput(line) {
|
|
56
|
+
try {
|
|
57
|
+
const entry = JSON.parse(line);
|
|
58
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
59
|
+
if (entry.type === 'user' && entry.isMeta === true) return null;
|
|
60
|
+
if (entry.isApiErrorMessage === true && entry.error === 'rate_limit') entry._rateLimitDetected = true;
|
|
61
|
+
if (entry.type === 'assistant' && entry.message) entry._isFragment = entry.message.stop_reason === null || entry.message.stop_reason === undefined;
|
|
62
|
+
if (entry.type === 'system' && entry.subtype === 'turn_duration' && entry.durationMs) entry._turnDurationMs = entry.durationMs;
|
|
63
|
+
if (entry.type === 'system' && entry.subtype === 'compact_boundary' && entry.compactMetadata) entry._preTokens = entry.compactMetadata.preTokens;
|
|
64
|
+
if (entry.message?.usage) {
|
|
65
|
+
const u = entry.message.usage;
|
|
66
|
+
entry._cacheUsage = { cache_creation: u.cache_creation_input_tokens || u['cache_creation.ephemeral_1h_input_tokens'] || u['cache_creation.ephemeral_5m_input_tokens'] || 0, cache_read: u.cache_read_input_tokens || 0 };
|
|
67
|
+
}
|
|
68
|
+
return entry;
|
|
69
|
+
} catch { return null; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registry.register({
|
|
73
|
+
id: 'claude-code', name: 'Claude Code', command: 'claude', protocol: 'direct', supportsStdin: false, closeStdin: true,
|
|
74
|
+
useJsonRpcStdin: false, supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip', 'steering'],
|
|
75
|
+
spawnEnv: { MAX_THINKING_TOKENS: '0', AGENTGUI_SUBPROCESS: '1' },
|
|
76
|
+
buildArgs(prompt, config) {
|
|
77
|
+
const { verbose = true, outputFormat = 'stream-json', print = true, resumeSessionId = null, systemPrompt = null, model = null } = config;
|
|
78
|
+
const flags = [];
|
|
79
|
+
if (print) flags.push('--print');
|
|
80
|
+
if (verbose) flags.push('--verbose');
|
|
81
|
+
flags.push(`--output-format=${outputFormat}`);
|
|
82
|
+
if (model) flags.push('--model', model);
|
|
83
|
+
if (resumeSessionId) flags.push('--resume', resumeSessionId);
|
|
84
|
+
if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
|
|
85
|
+
flags.push('--dangerously-skip-permissions');
|
|
86
|
+
flags.push(typeof prompt === 'string' ? prompt : String(prompt));
|
|
87
|
+
return flags;
|
|
88
|
+
},
|
|
89
|
+
parseOutput: parseClaudeOutput
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
registry.register({ id: 'opencode', name: 'OpenCode', command: 'opencode', protocol: 'acp', supportsStdin: false, npxPackage: 'opencode-ai', supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
93
|
+
registry.register({ id: 'gemini', name: 'Gemini CLI', command: 'gemini', protocol: 'acp', supportsStdin: false, npxPackage: '@google/gemini-cli', supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs(prompt, config) { const args = ['--experimental-acp', '--yolo']; if (config?.model) args.push('--model', config.model); return args; }, protocolHandler: acpProtocolHandler });
|
|
94
|
+
registry.register({ id: 'goose', name: 'Goose', command: 'goose', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
95
|
+
registry.register({ id: 'openhands', name: 'OpenHands', command: 'openhands', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
96
|
+
registry.register({ id: 'augment', name: 'Augment Code', command: 'augment', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
97
|
+
registry.register({ id: 'cline', name: 'Cline', command: 'cline', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
98
|
+
registry.register({ id: 'kimi', name: 'Kimi CLI', command: 'kimi', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
99
|
+
registry.register({ id: 'qwen', name: 'Qwen Code', command: 'qwen-code', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
100
|
+
registry.register({ id: 'codex', name: 'Codex CLI', command: 'codex', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
101
|
+
registry.register({ id: 'mistral', name: 'Mistral Vibe', command: 'mistral-vibe', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
102
|
+
registry.register({ id: 'kiro', name: 'Kiro CLI', command: 'kiro', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
103
|
+
registry.register({ id: 'fast-agent', name: 'fast-agent', command: 'fast-agent', protocol: 'acp', supportsStdin: false, supportedFeatures: ['streaming', 'resume', 'acp-protocol'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
104
|
+
registry.register({ id: 'kilo', name: 'Kilo CLI', command: 'kilo', protocol: 'acp', supportsStdin: false, npxPackage: '@kilocode/cli', supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'], buildArgs: () => ['acp'], protocolHandler: acpProtocolHandler });
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { AgentRunner, getSpawnOptions } from './claude-runner.js';
|
|
3
|
+
|
|
4
|
+
AgentRunner.prototype.runDirect = function(prompt, cwd, config = {}) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const { timeout = 300000, onEvent = null, onError = null, onRateLimit = null } = config;
|
|
7
|
+
const args = this.buildArgs(prompt, config);
|
|
8
|
+
const spawnOpts = getSpawnOptions(cwd);
|
|
9
|
+
if (Object.keys(this.spawnEnv).length > 0) spawnOpts.env = { ...spawnOpts.env, ...this.spawnEnv };
|
|
10
|
+
if (this.closeStdin) spawnOpts.stdio = ['ignore', 'pipe', 'pipe'];
|
|
11
|
+
const proc = spawn(this.command, args, spawnOpts);
|
|
12
|
+
console.log(`[${this.id}] Spawned PID ${proc.pid} closeStdin=${this.closeStdin}`);
|
|
13
|
+
|
|
14
|
+
if (config.onPid) { try { config.onPid(proc.pid); } catch (e) { console.error(`[${this.id}] onPid callback failed:`, e.message); } }
|
|
15
|
+
if (config.onProcess) { try { config.onProcess(proc); } catch (e) { console.error(`[${this.id}] onProcess callback failed:`, e.message); } }
|
|
16
|
+
|
|
17
|
+
let jsonBuffer = '';
|
|
18
|
+
const outputs = [];
|
|
19
|
+
let timedOut = false;
|
|
20
|
+
let sessionId = null;
|
|
21
|
+
let rateLimited = false;
|
|
22
|
+
let retryAfterSec = 60;
|
|
23
|
+
let authError = false;
|
|
24
|
+
let authErrorMessage = '';
|
|
25
|
+
|
|
26
|
+
const timeoutHandle = setTimeout(() => {
|
|
27
|
+
timedOut = true;
|
|
28
|
+
proc.kill();
|
|
29
|
+
reject(new Error(`${this.name} timeout after ${timeout}ms`));
|
|
30
|
+
}, timeout);
|
|
31
|
+
|
|
32
|
+
if (this.supportsStdin) proc.stdin.write(prompt);
|
|
33
|
+
|
|
34
|
+
proc.stdout.on('error', () => {});
|
|
35
|
+
if (proc.stderr) proc.stderr.on('error', () => {});
|
|
36
|
+
proc.stdout.on('data', (chunk) => {
|
|
37
|
+
if (timedOut) return;
|
|
38
|
+
jsonBuffer += chunk.toString();
|
|
39
|
+
const lines = jsonBuffer.split('\n');
|
|
40
|
+
jsonBuffer = lines.pop();
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (line.trim()) {
|
|
43
|
+
const parsed = this.parseOutput(line);
|
|
44
|
+
if (!parsed) continue;
|
|
45
|
+
outputs.push(parsed);
|
|
46
|
+
if (parsed.session_id) sessionId = parsed.session_id;
|
|
47
|
+
if (onEvent) { try { onEvent(parsed); } catch (e) { console.error(`[${this.id}] onEvent error: ${e.message}`); } }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (proc.stderr) proc.stderr.on('data', (chunk) => {
|
|
53
|
+
const errorText = chunk.toString();
|
|
54
|
+
console.error(`[${this.id}] stderr:`, errorText);
|
|
55
|
+
if (/401|unauthorized|invalid.*auth|invalid.*token|auth.*failed|permission denied|access denied/i.test(errorText)) {
|
|
56
|
+
authError = true;
|
|
57
|
+
authErrorMessage = errorText.trim();
|
|
58
|
+
}
|
|
59
|
+
const rateLimitMatch = errorText.match(/rate.?limit|429|too many requests|overloaded|throttl|hit your limit/i);
|
|
60
|
+
if (rateLimitMatch) {
|
|
61
|
+
rateLimited = true;
|
|
62
|
+
const retryMatch = errorText.match(/retry.?after[:\s]+(\d+)/i);
|
|
63
|
+
if (retryMatch) {
|
|
64
|
+
retryAfterSec = parseInt(retryMatch[1], 10) || 60;
|
|
65
|
+
} else {
|
|
66
|
+
const resetTimeMatch = errorText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
67
|
+
if (resetTimeMatch) {
|
|
68
|
+
let hours = parseInt(resetTimeMatch[1], 10);
|
|
69
|
+
const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
|
|
70
|
+
const period = resetTimeMatch[3]?.toLowerCase();
|
|
71
|
+
if (period === 'pm' && hours !== 12) hours += 12;
|
|
72
|
+
if (period === 'am' && hours === 12) hours = 0;
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const resetTime = new Date(now);
|
|
75
|
+
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
76
|
+
if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
77
|
+
retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (onError) { try { onError(errorText); } catch (e) {} }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
proc.on('close', (code) => {
|
|
85
|
+
clearTimeout(timeoutHandle);
|
|
86
|
+
if (timedOut) return;
|
|
87
|
+
if (authError) {
|
|
88
|
+
const err = new Error(`Authentication failed: ${authErrorMessage || 'Invalid credentials or unauthorized access'}`);
|
|
89
|
+
err.authError = true;
|
|
90
|
+
err.nonRetryable = true;
|
|
91
|
+
reject(err);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (rateLimited) {
|
|
95
|
+
const err = new Error(`Rate limited - retry after ${retryAfterSec}s`);
|
|
96
|
+
err.rateLimited = true;
|
|
97
|
+
err.retryAfterSec = retryAfterSec;
|
|
98
|
+
if (onRateLimit) { try { onRateLimit({ retryAfterSec }); } catch (e) {} }
|
|
99
|
+
reject(err);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (jsonBuffer.trim()) {
|
|
103
|
+
const parsed = this.parseOutput(jsonBuffer);
|
|
104
|
+
if (parsed) {
|
|
105
|
+
outputs.push(parsed);
|
|
106
|
+
if (parsed.session_id) sessionId = parsed.session_id;
|
|
107
|
+
if (onEvent) { try { onEvent(parsed); } catch (e) {} }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (code === 0 || outputs.length > 0) resolve({ outputs, sessionId });
|
|
111
|
+
else reject(new Error(`${this.name} exited with code ${code}`));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
proc.on('error', (err) => { clearTimeout(timeoutHandle); reject(err); });
|
|
115
|
+
});
|
|
116
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { registry } from './claude-runner-agents.js';
|
|
2
|
+
|
|
3
|
+
const communicationGuidelines = `
|
|
4
|
+
COMMUNICATION STYLE: Minimize output. Only inform the user about:
|
|
5
|
+
- Critical errors that block work
|
|
6
|
+
- User needs to know info (e.g., "port in use", "authentication failed", "file not found")
|
|
7
|
+
- Action required from user
|
|
8
|
+
- Important decisions that affect their work
|
|
9
|
+
|
|
10
|
+
DO NOT output:
|
|
11
|
+
- Progress updates ("doing X now", "completed Y", "searching for...")
|
|
12
|
+
- Verbose summaries of what was done
|
|
13
|
+
- Status checks or verification messages
|
|
14
|
+
- Detailed explanations unless asked
|
|
15
|
+
- "Working on...", "Looking for...", step-by-step progress
|
|
16
|
+
|
|
17
|
+
INSTEAD:
|
|
18
|
+
- Run tools silently
|
|
19
|
+
- Show results only when relevant
|
|
20
|
+
- Be conversational and direct
|
|
21
|
+
- Let code/output speak for itself
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export async function runClaudeWithStreaming(prompt, cwd, agentId = 'claude-code', config = {}) {
|
|
25
|
+
const agent = registry.get(agentId);
|
|
26
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}. Registered agents: ${registry.list().map(a => a.id).join(', ')}`);
|
|
27
|
+
|
|
28
|
+
const enhancedConfig = { ...config };
|
|
29
|
+
if (!enhancedConfig.systemPrompt) enhancedConfig.systemPrompt = '';
|
|
30
|
+
|
|
31
|
+
if (!enhancedConfig.systemPrompt.includes('COMMUNICATION STYLE')) {
|
|
32
|
+
enhancedConfig.systemPrompt = communicationGuidelines + enhancedConfig.systemPrompt;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (agentId && agentId !== 'claude-code') {
|
|
36
|
+
const displayAgentId = agentId.split('-·-')[0];
|
|
37
|
+
const agentPrefix = `use ${displayAgentId} subagent to. `;
|
|
38
|
+
if (!enhancedConfig.systemPrompt.includes(agentPrefix)) {
|
|
39
|
+
enhancedConfig.systemPrompt = agentPrefix + enhancedConfig.systemPrompt;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return agent.run(prompt, cwd, enhancedConfig);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getRegisteredAgents() { return registry.list(); }
|
|
47
|
+
export function getAvailableAgents() { return registry.listACPAvailable(); }
|
|
48
|
+
export function isAgentRegistered(agentId) { return registry.has(agentId); }
|
|
49
|
+
export default runClaudeWithStreaming;
|