agentgui 1.0.714 → 1.0.716
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +20 -7
- package/lib/acp-protocol.js +91 -0
- package/lib/acp-runner.js +136 -0
- package/lib/acp-sdk-manager.js +20 -64
- package/lib/agent-descriptors.js +47 -332
- package/lib/agent-registry-configs.js +125 -0
- package/lib/claude-runner.js +189 -1247
- package/lib/jsonl-watcher.js +61 -35
- package/lib/plugin-loader.js +3 -15
- package/lib/tool-manager.js +99 -621
- package/lib/tool-provisioner.js +93 -0
- package/lib/tool-spawner.js +121 -0
- package/lib/tool-version.js +196 -0
- package/lib/ws-handlers-conv.js +5 -198
- package/lib/ws-handlers-msg.js +119 -0
- package/lib/ws-handlers-oauth.js +76 -0
- package/lib/ws-handlers-queue.js +56 -0
- package/lib/ws-handlers-scripts.js +58 -0
- package/lib/ws-handlers-util.js +22 -206
- package/package.json +1 -1
- package/server.js +21 -3
- package/static/js/client.js +34 -35
- package/static/js/streaming-renderer.js +30 -49
package/CLAUDE.md
CHANGED
|
@@ -16,17 +16,28 @@ Server starts on `http://localhost:3000`, redirects to `/gm/`.
|
|
|
16
16
|
```
|
|
17
17
|
server.js HTTP server + WebSocket + all API routes (raw http.createServer)
|
|
18
18
|
database.js SQLite setup (WAL mode), schema, query functions
|
|
19
|
-
lib/claude-runner.js Agent framework -
|
|
20
|
-
lib/acp-
|
|
21
|
-
lib/
|
|
19
|
+
lib/claude-runner.js Agent framework - AgentRunner/AgentRegistry classes, direct protocol execution
|
|
20
|
+
lib/acp-runner.js ACP JSON-RPC session lifecycle (init, session/new, prompt, drain)
|
|
21
|
+
lib/acp-protocol.js ACP session/update message normalization (shared by all ACP agents)
|
|
22
|
+
lib/agent-registry-configs.js Agent registration configs (Claude Code, OpenCode, Gemini, 10+ ACP agents)
|
|
23
|
+
lib/agent-descriptors.js Data-driven ACP agent descriptor builder
|
|
24
|
+
lib/acp-sdk-manager.js ACP tool lifecycle - on-demand start opencode/kilo/codex, health checks, idle timeout
|
|
22
25
|
lib/acp-server-machine.js XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
|
|
26
|
+
lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
|
|
23
27
|
lib/ws-protocol.js WebSocket RPC router (WsRouter class)
|
|
24
28
|
lib/ws-optimizer.js Per-client priority queue for WS event batching
|
|
25
|
-
lib/ws-handlers-conv.js Conversation
|
|
29
|
+
lib/ws-handlers-conv.js Conversation CRUD, chunks, cancel, steer, inject RPC handlers
|
|
30
|
+
lib/ws-handlers-msg.js Message send/stream/list RPC handlers + execution start/enqueue
|
|
31
|
+
lib/ws-handlers-queue.js Queue list/delete/update RPC handlers
|
|
26
32
|
lib/ws-handlers-session.js Session/agent RPC handlers
|
|
27
33
|
lib/ws-handlers-run.js Thread/run RPC handlers
|
|
28
|
-
lib/ws-handlers-util.js Utility RPC handlers (speech, auth, git, tools)
|
|
29
|
-
lib/
|
|
34
|
+
lib/ws-handlers-util.js Utility RPC handlers (speech, auth, git, tools, voice)
|
|
35
|
+
lib/ws-handlers-oauth.js Gemini + Codex OAuth WS RPC handlers
|
|
36
|
+
lib/ws-handlers-scripts.js npm script run/stop WS RPC handlers
|
|
37
|
+
lib/tool-manager.js Tool facade - re-exports from tool-version, tool-spawner, tool-provisioner
|
|
38
|
+
lib/tool-version.js Version detection for CLI tools and plugins (data-driven framework paths)
|
|
39
|
+
lib/tool-spawner.js npm/bun install/update spawn with timeout and heartbeat
|
|
40
|
+
lib/tool-provisioner.js Auto-provisioning and periodic update checking
|
|
30
41
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
31
42
|
bin/gmgui.cjs CLI entry point (npx agentgui / bun x agentgui)
|
|
32
43
|
static/index.html Main HTML shell
|
|
@@ -169,7 +180,9 @@ Current tools:
|
|
|
169
180
|
- `cli-agent-browser`: bin=`agent-browser`, pkg=`agent-browser` — uses `-V` flag (not `--version`) for version detection
|
|
170
181
|
- `gm-cc`, `gm-oc`, `gm-gc`, `gm-kilo`, `gm-codex`: plugin tools
|
|
171
182
|
|
|
172
|
-
**
|
|
183
|
+
**BIN_MAP gotcha:** `lib/tool-version.js` has a single `BIN_MAP` constant shared by `checkCliInstalled()` and `getCliVersion()`. Any new CLI tool must be added there. `agent-browser` uses `-V` (not `--version`) — a `versionFlag` override handles this.
|
|
184
|
+
|
|
185
|
+
**Framework paths:** `lib/tool-version.js` uses a `FRAMEWORK_PATHS` data table instead of per-framework if/else chains. Each framework entry defines pluginDir, versionFile, parseVersion, and optional markerFile/fallbackInstalled. Adding a new framework means adding one entry to this table.
|
|
173
186
|
|
|
174
187
|
**Background provisioning:** `autoProvision()` runs at startup, checks/installs missing tools (~10s). `startPeriodicUpdateCheck()` runs every 6 hours in background to check for updates. Both broadcast tool status via WebSocket so UI stays in sync.
|
|
175
188
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
function normalizeContentBlock(content) {
|
|
2
|
+
if (typeof content === 'string') return { type: 'text', text: content };
|
|
3
|
+
if (content.type === 'text' && content.text) return content;
|
|
4
|
+
if (content.text) return { type: 'text', text: content.text };
|
|
5
|
+
if (content.content) {
|
|
6
|
+
const inner = content.content;
|
|
7
|
+
if (typeof inner === 'string') return { type: 'text', text: inner };
|
|
8
|
+
if (inner.type === 'text' && inner.text) return inner;
|
|
9
|
+
return { type: 'text', text: JSON.stringify(inner) };
|
|
10
|
+
}
|
|
11
|
+
return { type: 'text', text: JSON.stringify(content) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractToolResultContent(updateContent) {
|
|
15
|
+
const parts = [];
|
|
16
|
+
if (!updateContent || !Array.isArray(updateContent)) return '';
|
|
17
|
+
for (const item of updateContent) {
|
|
18
|
+
if (item.type === 'content' && item.content) {
|
|
19
|
+
const inner = item.content;
|
|
20
|
+
if (inner.type === 'text' && inner.text) parts.push(inner.text);
|
|
21
|
+
else if (inner.type === 'resource' && inner.resource) parts.push(inner.resource.text || JSON.stringify(inner.resource));
|
|
22
|
+
else parts.push(JSON.stringify(inner));
|
|
23
|
+
} else if (item.type === 'diff') {
|
|
24
|
+
parts.push(item.oldText
|
|
25
|
+
? `--- ${item.path}\n+++ ${item.path}\n${item.oldText}\n---\n${item.newText}`
|
|
26
|
+
: `+++ ${item.path}\n${item.newText}`);
|
|
27
|
+
} else if (item.type === 'terminal') {
|
|
28
|
+
parts.push(`[Terminal: ${item.terminalId}]`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return parts.join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleSessionUpdate(params) {
|
|
35
|
+
const update = params.update || {};
|
|
36
|
+
const sid = params.sessionId;
|
|
37
|
+
|
|
38
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content) {
|
|
39
|
+
return { type: 'assistant', message: { role: 'assistant', content: [normalizeContentBlock(update.content)] }, session_id: sid };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (update.sessionUpdate === 'tool_call') {
|
|
43
|
+
return {
|
|
44
|
+
type: 'assistant',
|
|
45
|
+
message: { role: 'assistant', content: [{ type: 'tool_use', id: update.toolCallId, name: update.title || update.kind || 'tool', kind: update.kind || 'other', input: update.rawInput || update.input || {} }] },
|
|
46
|
+
session_id: sid
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (update.sessionUpdate === 'tool_call_update') {
|
|
51
|
+
const isError = update.status === 'failed';
|
|
52
|
+
const isCompleted = update.status === 'completed';
|
|
53
|
+
if (!isCompleted && !isError) {
|
|
54
|
+
return { type: 'tool_status', tool_use_id: update.toolCallId, status: update.status, kind: update.kind || 'other', locations: update.locations || [], session_id: sid };
|
|
55
|
+
}
|
|
56
|
+
const content = extractToolResultContent(update.content) || (update.rawOutput ? JSON.stringify(update.rawOutput) : '');
|
|
57
|
+
return { type: 'user', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: update.toolCallId, content, is_error: isError }] }, session_id: sid };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (update.sessionUpdate === 'usage_update') {
|
|
61
|
+
return { type: 'usage', usage: { used: update.used, size: update.size, cost: update.cost }, session_id: sid };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (update.sessionUpdate === 'plan') {
|
|
65
|
+
return { type: 'plan', entries: update.entries || [], session_id: sid };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createACPProtocolHandler() {
|
|
72
|
+
return function(message, context) {
|
|
73
|
+
if (!message || typeof message !== 'object') return null;
|
|
74
|
+
|
|
75
|
+
if (message.method === 'session/update') {
|
|
76
|
+
return handleSessionUpdate(message.params || {});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.id && message.result && message.result.stopReason) {
|
|
80
|
+
return { type: 'result', result: '', stopReason: message.result.stopReason, usage: message.result.usage, session_id: context.sessionId };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (message.method === 'error' || message.error) {
|
|
84
|
+
return { type: 'error', error: message.error || message.params || { message: 'Unknown error' } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const acpProtocolHandler = createACPProtocolHandler();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
|
|
4
|
+
const isWindows = process.platform === 'win32';
|
|
5
|
+
|
|
6
|
+
function getSpawnOptions(cwd) {
|
|
7
|
+
const options = { cwd, windowsHide: true };
|
|
8
|
+
if (isWindows) options.shell = true;
|
|
9
|
+
options.env = { ...process.env };
|
|
10
|
+
delete options.env.CLAUDECODE;
|
|
11
|
+
return options;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveCommand(command, npxPackage) {
|
|
15
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
16
|
+
const check = spawnSync(whichCmd, [command], { encoding: 'utf-8', timeout: 3000 });
|
|
17
|
+
if (check.status === 0 && (check.stdout || '').trim()) return { cmd: command, prefixArgs: [] };
|
|
18
|
+
if (npxPackage) {
|
|
19
|
+
if (spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', timeout: 3000 }).status === 0) return { cmd: 'npx', prefixArgs: ['--yes', npxPackage] };
|
|
20
|
+
if (spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', timeout: 3000 }).status === 0) return { cmd: 'bun', prefixArgs: ['x', npxPackage] };
|
|
21
|
+
}
|
|
22
|
+
return { cmd: command, prefixArgs: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function runACPOnce(agent, prompt, cwd, config = {}) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const { timeout = 300000, onEvent = null, onError = null } = config;
|
|
28
|
+
let cmd, args;
|
|
29
|
+
if (agent.requiresAdapter && agent.adapterCommand) { cmd = agent.adapterCommand; args = [...agent.adapterArgs]; }
|
|
30
|
+
else { const resolved = resolveCommand(agent.command, agent.npxPackage); cmd = resolved.cmd; args = [...resolved.prefixArgs, ...agent.buildArgs(prompt, config)]; }
|
|
31
|
+
const spawnOpts = getSpawnOptions(cwd);
|
|
32
|
+
if (Object.keys(agent.spawnEnv).length > 0) spawnOpts.env = { ...spawnOpts.env, ...agent.spawnEnv };
|
|
33
|
+
const proc = spawn(cmd, args, spawnOpts);
|
|
34
|
+
if (config.onPid) { try { config.onPid(proc.pid); } catch (_) {} }
|
|
35
|
+
if (config.onProcess) { try { config.onProcess(proc); } catch (_) {} }
|
|
36
|
+
const outputs = [];
|
|
37
|
+
let timedOut = false, sessionId = null, requestId = 0, initialized = false, stderrText = '';
|
|
38
|
+
const timeoutHandle = setTimeout(() => { timedOut = true; proc.kill(); reject(new Error(`${agent.name} ACP timeout after ${timeout}ms`)); }, timeout);
|
|
39
|
+
|
|
40
|
+
const handleMessage = (message) => {
|
|
41
|
+
const normalized = agent.protocolHandler(message, { sessionId, initialized });
|
|
42
|
+
if (!normalized) { if (message.id === 1 && message.result) initialized = true; return; }
|
|
43
|
+
outputs.push(normalized);
|
|
44
|
+
if (normalized.session_id) sessionId = normalized.session_id;
|
|
45
|
+
if (onEvent) { try { onEvent(normalized); } catch (e) { console.error(`[${agent.id}] onEvent error: ${e.message}`); } }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
proc.stdout.on('error', () => {});
|
|
49
|
+
proc.stderr.on('error', () => {});
|
|
50
|
+
let buffer = '';
|
|
51
|
+
proc.stdout.on('data', (chunk) => {
|
|
52
|
+
if (timedOut) return;
|
|
53
|
+
buffer += chunk.toString();
|
|
54
|
+
const lines = buffer.split('\n'); buffer = lines.pop();
|
|
55
|
+
for (const line of lines) { if (line.trim()) { try { handleMessage(JSON.parse(line)); } catch (e) { console.error(`[${agent.id}] JSON parse error:`, line.substring(0, 100)); } } }
|
|
56
|
+
});
|
|
57
|
+
proc.stderr.on('data', (chunk) => { const t = chunk.toString(); stderrText += t; console.error(`[${agent.id}] stderr:`, t); if (onError) { try { onError(t); } catch (_) {} } });
|
|
58
|
+
|
|
59
|
+
proc.stdin.on('error', () => {});
|
|
60
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestId, method: 'initialize', params: { protocolVersion: 1, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, terminal: true }, clientInfo: { name: 'agentgui', title: 'AgentGUI', version: '1.0.0' } } }) + '\n');
|
|
61
|
+
|
|
62
|
+
let sessionCreated = false;
|
|
63
|
+
const checkInitAndSend = () => {
|
|
64
|
+
if (initialized && !sessionCreated) {
|
|
65
|
+
sessionCreated = true;
|
|
66
|
+
const sp = { cwd, mcpServers: [] };
|
|
67
|
+
if (config.model) sp.model = config.model;
|
|
68
|
+
if (config.subAgent) sp.agent = config.subAgent;
|
|
69
|
+
if (config.systemPrompt) sp.systemPrompt = config.systemPrompt;
|
|
70
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: ++requestId, method: 'session/new', params: sp }) + '\n');
|
|
71
|
+
} else if (!initialized) { setTimeout(checkInitAndSend, 100); }
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let promptId = null, completed = false, draining = false;
|
|
75
|
+
const enhancedHandler = (message) => {
|
|
76
|
+
if (message.id && message.result && message.result.sessionId) {
|
|
77
|
+
sessionId = message.result.sessionId;
|
|
78
|
+
promptId = ++requestId;
|
|
79
|
+
proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: promptId, method: 'session/prompt', params: { sessionId, prompt: [{ type: 'text', text: prompt }] } }) + '\n');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (message.id === promptId && message.result && message.result.stopReason) {
|
|
83
|
+
completed = true; draining = true; clearTimeout(timeoutHandle);
|
|
84
|
+
setTimeout(() => { draining = false; try { proc.kill(); } catch (_) {} resolve({ outputs, sessionId }); }, 1000);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (message.id === promptId && message.error) {
|
|
88
|
+
completed = true; draining = true; clearTimeout(timeoutHandle); handleMessage(message);
|
|
89
|
+
setTimeout(() => { draining = false; try { proc.kill(); } catch (_) {} reject(new Error(message.error.message || 'ACP prompt error')); }, 1000);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
handleMessage(message);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
buffer = '';
|
|
96
|
+
proc.stdout.removeAllListeners('data');
|
|
97
|
+
proc.stdout.on('data', (chunk) => {
|
|
98
|
+
if (timedOut || (completed && !draining)) return;
|
|
99
|
+
buffer += chunk.toString();
|
|
100
|
+
const lines = buffer.split('\n'); buffer = lines.pop();
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.trim()) {
|
|
103
|
+
try { const m = JSON.parse(line); if (m.id === 1 && m.result) initialized = true; enhancedHandler(m); }
|
|
104
|
+
catch (e) { console.error(`[${agent.id}] JSON parse error:`, line.substring(0, 100)); }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
setTimeout(checkInitAndSend, 200);
|
|
109
|
+
|
|
110
|
+
proc.on('close', (code) => {
|
|
111
|
+
clearTimeout(timeoutHandle);
|
|
112
|
+
if (timedOut || completed) return;
|
|
113
|
+
if (buffer.trim()) { try { const m = JSON.parse(buffer.trim()); if (m.id === 1 && m.result) initialized = true; enhancedHandler(m); } catch (_) {} }
|
|
114
|
+
if (code === 0 || outputs.length > 0) resolve({ outputs, sessionId });
|
|
115
|
+
else { const err = new Error(`${agent.name} ACP exited with code ${code}${stderrText ? `: ${stderrText.substring(0, 200)}` : ''}`); err.isPrematureEnd = true; err.exitCode = code; err.stderrText = stderrText; reject(err); }
|
|
116
|
+
});
|
|
117
|
+
proc.on('error', (err) => { clearTimeout(timeoutHandle); reject(err); });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function runACPWithRetry(agent, prompt, cwd, config = {}, _retryCount = 0) {
|
|
122
|
+
const maxRetries = config.maxRetries ?? 1;
|
|
123
|
+
try { return await runACPOnce(agent, prompt, cwd, config); }
|
|
124
|
+
catch (err) {
|
|
125
|
+
const isEmptyExit = err.isPrematureEnd || (err.message && err.message.includes('ACP exited with code'));
|
|
126
|
+
const isBinaryError = err.code === 'ENOENT' || (err.message && err.message.includes('ENOENT'));
|
|
127
|
+
if ((isEmptyExit || isBinaryError) && _retryCount < maxRetries) {
|
|
128
|
+
const delay = Math.min(1000 * Math.pow(2, _retryCount), 5000);
|
|
129
|
+
console.error(`[${agent.id}] ACP attempt ${_retryCount + 1} failed: ${err.message}. Retrying in ${delay}ms...`);
|
|
130
|
+
await new Promise(r => setTimeout(r, delay));
|
|
131
|
+
return runACPWithRetry(agent, prompt, cwd, config, _retryCount + 1);
|
|
132
|
+
}
|
|
133
|
+
if (err.isPrematureEnd) { const premErr = new Error(err.message); premErr.isPrematureEnd = true; premErr.exitCode = err.exitCode; premErr.stderrText = err.stderrText; throw premErr; }
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
package/lib/acp-sdk-manager.js
CHANGED
|
@@ -3,45 +3,31 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import { AbortError } from 'p-retry';
|
|
7
6
|
import * as acpMachine from './acp-server-machine.js';
|
|
8
7
|
|
|
9
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
9
|
const projectRoot = path.resolve(__dirname, '..');
|
|
11
10
|
const isWindows = os.platform() === 'win32';
|
|
12
|
-
|
|
13
11
|
const ACP_TOOLS = [
|
|
14
12
|
{ id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
|
|
15
13
|
{ id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
|
|
16
14
|
{ id: 'codex', cmd: 'codex', args: ['acp'], port: 18102, npxPkg: '@openai/codex' },
|
|
17
15
|
];
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const IDLE_TIMEOUT_MS = 120000;
|
|
22
|
-
|
|
23
|
-
const processes = new Map();
|
|
24
|
-
const idleTimers = new Map();
|
|
25
|
-
let healthTimer = null;
|
|
26
|
-
let shuttingDown = false;
|
|
27
|
-
|
|
16
|
+
const HEALTH_INTERVAL_MS = 30000, STARTUP_GRACE_MS = 5000, IDLE_TIMEOUT_MS = 120000;
|
|
17
|
+
const processes = new Map(), idleTimers = new Map();
|
|
18
|
+
let healthTimer = null, shuttingDown = false;
|
|
28
19
|
function log(msg) { console.log('[ACP-SDK] ' + msg); }
|
|
29
20
|
|
|
30
21
|
function resolveCommand(tool) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (fs.existsSync(localBin)) return { bin: localBin, args: tool.args };
|
|
34
|
-
return { bin: tool.cmd, args: tool.args };
|
|
22
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', tool.cmd + (isWindows ? '.cmd' : ''));
|
|
23
|
+
return fs.existsSync(localBin) ? { bin: localBin, args: tool.args } : { bin: tool.cmd, args: tool.args };
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
function resetIdleTimer(toolId) {
|
|
38
27
|
acpMachine.send(toolId, { type: 'TOUCH' });
|
|
39
28
|
const existing = idleTimers.get(toolId);
|
|
40
29
|
if (existing) clearTimeout(existing);
|
|
41
|
-
idleTimers.set(toolId, setTimeout(() => {
|
|
42
|
-
acpMachine.send(toolId, { type: 'IDLE_TIMEOUT' });
|
|
43
|
-
stopTool(toolId);
|
|
44
|
-
}, IDLE_TIMEOUT_MS));
|
|
30
|
+
idleTimers.set(toolId, setTimeout(() => { acpMachine.send(toolId, { type: 'IDLE_TIMEOUT' }); stopTool(toolId); }, IDLE_TIMEOUT_MS));
|
|
45
31
|
}
|
|
46
32
|
|
|
47
33
|
function clearIdleTimer(toolId) {
|
|
@@ -112,18 +98,10 @@ async function checkHealth(toolId, port) {
|
|
|
112
98
|
const p = port || ACP_TOOLS.find(t => t.id === toolId)?.port;
|
|
113
99
|
if (!p) return;
|
|
114
100
|
try {
|
|
115
|
-
const res = await fetch('http://127.0.0.1:' + p + '/provider', {
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const providerInfo = await res.json();
|
|
120
|
-
acpMachine.send(toolId, { type: 'HEALTHY', providerInfo });
|
|
121
|
-
} else {
|
|
122
|
-
acpMachine.send(toolId, { type: 'UNHEALTHY' });
|
|
123
|
-
}
|
|
124
|
-
} catch (_) {
|
|
125
|
-
acpMachine.send(toolId, { type: 'UNHEALTHY' });
|
|
126
|
-
}
|
|
101
|
+
const res = await fetch('http://127.0.0.1:' + p + '/provider', { signal: AbortSignal.timeout(3000) });
|
|
102
|
+
if (res.ok) acpMachine.send(toolId, { type: 'HEALTHY', providerInfo: await res.json() });
|
|
103
|
+
else acpMachine.send(toolId, { type: 'UNHEALTHY' });
|
|
104
|
+
} catch (_) { acpMachine.send(toolId, { type: 'UNHEALTHY' }); }
|
|
127
105
|
}
|
|
128
106
|
|
|
129
107
|
export async function ensureRunning(agentId) {
|
|
@@ -160,16 +138,14 @@ export async function stopAll() {
|
|
|
160
138
|
shuttingDown = true;
|
|
161
139
|
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
162
140
|
for (const toolId of idleTimers.keys()) clearIdleTimer(toolId);
|
|
163
|
-
|
|
164
|
-
for (const [id, proc] of processes) {
|
|
141
|
+
await Promise.all([...processes].map(([id, proc]) => {
|
|
165
142
|
log('stopping ' + id + ' pid ' + proc.pid);
|
|
166
|
-
|
|
143
|
+
return new Promise(resolve => {
|
|
167
144
|
const t = setTimeout(() => { try { proc.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
168
145
|
proc.on('close', () => { clearTimeout(t); resolve(); });
|
|
169
146
|
try { proc.kill('SIGTERM'); } catch (_) {}
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
await Promise.all(kills);
|
|
147
|
+
});
|
|
148
|
+
}));
|
|
173
149
|
processes.clear();
|
|
174
150
|
acpMachine.stopAll();
|
|
175
151
|
log('all stopped');
|
|
@@ -179,17 +155,7 @@ export function getStatus() {
|
|
|
179
155
|
return ACP_TOOLS.map(tool => {
|
|
180
156
|
const snap = acpMachine.snapshot(tool.id);
|
|
181
157
|
const ctx = snap?.context || {};
|
|
182
|
-
return {
|
|
183
|
-
id: tool.id,
|
|
184
|
-
port: tool.port,
|
|
185
|
-
running: snap?.value === 'running' || snap?.value === 'starting',
|
|
186
|
-
healthy: ctx.healthy || false,
|
|
187
|
-
pid: ctx.pid,
|
|
188
|
-
uptime: ctx.startedAt ? Date.now() - ctx.startedAt : 0,
|
|
189
|
-
restartCount: ctx.restarts?.length || 0,
|
|
190
|
-
idleMs: ctx.lastUsed ? Date.now() - ctx.lastUsed : 0,
|
|
191
|
-
providerInfo: ctx.providerInfo || null,
|
|
192
|
-
};
|
|
158
|
+
return { id: tool.id, port: tool.port, running: snap?.value === 'running' || snap?.value === 'starting', healthy: ctx.healthy || false, pid: ctx.pid, uptime: ctx.startedAt ? Date.now() - ctx.startedAt : 0, restartCount: ctx.restarts?.length || 0, idleMs: ctx.lastUsed ? Date.now() - ctx.lastUsed : 0, providerInfo: ctx.providerInfo || null };
|
|
193
159
|
});
|
|
194
160
|
}
|
|
195
161
|
|
|
@@ -199,33 +165,23 @@ export function getPort(agentId) {
|
|
|
199
165
|
|
|
200
166
|
export function getRunningPorts() {
|
|
201
167
|
const ports = {};
|
|
202
|
-
for (const tool of ACP_TOOLS) {
|
|
203
|
-
if (acpMachine.isHealthy(tool.id)) ports[tool.id] = tool.port;
|
|
204
|
-
}
|
|
168
|
+
for (const tool of ACP_TOOLS) { if (acpMachine.isHealthy(tool.id)) ports[tool.id] = tool.port; }
|
|
205
169
|
return ports;
|
|
206
170
|
}
|
|
207
171
|
|
|
208
172
|
export async function restart(agentId) {
|
|
209
173
|
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
210
174
|
if (!tool) return false;
|
|
211
|
-
stopTool(agentId);
|
|
212
|
-
startProcess(tool);
|
|
213
|
-
return true;
|
|
175
|
+
stopTool(agentId); startProcess(tool); return true;
|
|
214
176
|
}
|
|
215
177
|
|
|
216
178
|
export async function queryModels(agentId) {
|
|
217
179
|
const port = await ensureRunning(agentId);
|
|
218
180
|
if (!port) return [];
|
|
219
181
|
try {
|
|
220
|
-
const res = await fetch('http://127.0.0.1:' + port + '/models', {
|
|
221
|
-
|
|
222
|
-
});
|
|
223
|
-
if (!res.ok) return [];
|
|
224
|
-
const data = await res.json();
|
|
225
|
-
return data.models || [];
|
|
182
|
+
const res = await fetch('http://127.0.0.1:' + port + '/models', { signal: AbortSignal.timeout(3000) });
|
|
183
|
+
return res.ok ? ((await res.json()).models || []) : [];
|
|
226
184
|
} catch (_) { return []; }
|
|
227
185
|
}
|
|
228
186
|
|
|
229
|
-
export function isAvailable(agentId) {
|
|
230
|
-
return !!ACP_TOOLS.find(t => t.id === agentId);
|
|
231
|
-
}
|
|
187
|
+
export function isAvailable(agentId) { return !!ACP_TOOLS.find(t => t.id === agentId); }
|