agentgui 1.0.715 → 1.0.716

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 - spawns CLI processes, parses stream-json output
20
- lib/acp-manager.js ACP tool lifecycle - auto-starts opencode/kilo HTTP servers, restart on crash
21
- lib/execution-machine.js XState v5 machine per conversation: idle/streaming/draining/rate_limited states
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/message/queue RPC handlers (~70 methods total)
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/tool-manager.js Tool detection, installation, version checking
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
- **binMap gotcha:** `checkCliInstalled()` and `getCliVersion()` both have a `binMap` object. Any new CLI tool must be added to BOTH. `agent-browser` uses `-V` (not `--version`) — a `versionFlag` override handles this.
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
+ }
@@ -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 HEALTH_INTERVAL_MS = 30000;
20
- const STARTUP_GRACE_MS = 5000;
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 ext = isWindows ? '.cmd' : '';
32
- const localBin = path.join(projectRoot, 'node_modules', '.bin', tool.cmd + ext);
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
- signal: AbortSignal.timeout(3000),
117
- });
118
- if (res.ok) {
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
- const kills = [];
164
- for (const [id, proc] of processes) {
141
+ await Promise.all([...processes].map(([id, proc]) => {
165
142
  log('stopping ' + id + ' pid ' + proc.pid);
166
- kills.push(new Promise(resolve => {
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
- signal: AbortSignal.timeout(3000),
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); }