agentgui 1.0.436 → 1.0.438
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 +10 -0
- package/build-portable.js +28 -11
- package/lib/acp-manager.js +179 -0
- package/lib/ws-handlers-session.js +23 -128
- package/package.json +2 -2
- package/server.js +59 -56
package/CLAUDE.md
CHANGED
|
@@ -17,6 +17,7 @@ Server starts on `http://localhost:3000`, redirects to `/gm/`.
|
|
|
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
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
|
|
20
21
|
lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
|
|
21
22
|
bin/gmgui.cjs CLI entry point (npx agentgui / bunx agentgui)
|
|
22
23
|
static/index.html Main HTML shell
|
|
@@ -49,6 +50,14 @@ static/templates/ 31 HTML template fragments for event rendering
|
|
|
49
50
|
- `STARTUP_CWD` - Working directory passed to agents
|
|
50
51
|
- `HOT_RELOAD` - Set to "false" to disable watch mode
|
|
51
52
|
|
|
53
|
+
## ACP Tool Lifecycle
|
|
54
|
+
|
|
55
|
+
On startup, agentgui auto-launches bundled ACP tools (opencode, kilo) as HTTP servers:
|
|
56
|
+
- OpenCode: port 18100 (`opencode acp --port 18100`)
|
|
57
|
+
- Kilo: port 18101 (`kilo acp --port 18101`)
|
|
58
|
+
|
|
59
|
+
Managed by `lib/acp-manager.js`. Features: crash restart with exponential backoff (max 10 in 5min), health checks every 30s via `GET /provider`, clean shutdown on SIGTERM. The `acpPort` field on discovered agents is set automatically once healthy. Models are queried from the running ACP HTTP servers via their `/provider` endpoint.
|
|
60
|
+
|
|
52
61
|
## REST API
|
|
53
62
|
|
|
54
63
|
All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
@@ -68,6 +77,7 @@ All routes are prefixed with `BASE_URL` (default `/gm`).
|
|
|
68
77
|
- `GET /api/sessions/:id/chunks` - Get session chunks (query: since)
|
|
69
78
|
- `GET /api/sessions/:id/execution` - Get execution events (query: limit, offset, filterType)
|
|
70
79
|
- `GET /api/agents` - List discovered agents
|
|
80
|
+
- `GET /api/acp/status` - ACP tool lifecycle status (ports, health, PIDs, restart counts)
|
|
71
81
|
- `GET /api/home` - Get home directory
|
|
72
82
|
- `POST /api/stt` - Speech-to-text (raw audio body)
|
|
73
83
|
- `POST /api/tts` - Text-to-speech (body: text)
|
package/build-portable.js
CHANGED
|
@@ -122,6 +122,32 @@ const audioDeps = new Set();
|
|
|
122
122
|
collectDeps('audio-decode', audioDeps);
|
|
123
123
|
for (const dep of audioDeps) copyPkg(dep);
|
|
124
124
|
|
|
125
|
+
log('Copying ACP tools (opencode, kilo, gemini) for portable...');
|
|
126
|
+
const acpNativeTools = [
|
|
127
|
+
{ wrapper: 'opencode-ai', platformPrefix: 'opencode-windows-x64' },
|
|
128
|
+
{ wrapper: '@kilocode/cli', platformPrefix: '@kilocode/cli-windows-x64' },
|
|
129
|
+
];
|
|
130
|
+
for (const tool of acpNativeTools) {
|
|
131
|
+
const wrapperSrc = path.join(nm, tool.wrapper);
|
|
132
|
+
if (fs.existsSync(wrapperSrc)) copyDir(wrapperSrc, path.join(destNm, tool.wrapper));
|
|
133
|
+
for (const suffix of ['', '-baseline']) {
|
|
134
|
+
const platPkg = tool.platformPrefix + suffix;
|
|
135
|
+
const platSrc = path.join(nm, platPkg);
|
|
136
|
+
if (fs.existsSync(platSrc)) {
|
|
137
|
+
copyDir(platSrc, path.join(destNm, platPkg));
|
|
138
|
+
log(` ${platPkg} (${Math.round(sizeOf(platSrc) / 1024 / 1024)}MB)`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const geminiSrc = path.join(nm, '@google', 'gemini-cli');
|
|
143
|
+
if (fs.existsSync(geminiSrc)) {
|
|
144
|
+
copyDir(geminiSrc, path.join(destNm, '@google', 'gemini-cli'));
|
|
145
|
+
const geminiDeps = new Set();
|
|
146
|
+
collectDeps('@google/gemini-cli', geminiDeps);
|
|
147
|
+
for (const dep of geminiDeps) copyPkg(dep);
|
|
148
|
+
log(` gemini-cli + ${geminiDeps.size} deps`);
|
|
149
|
+
}
|
|
150
|
+
|
|
125
151
|
log('Copying @anthropic-ai/claude-code ripgrep (win32)...');
|
|
126
152
|
const claudeSrc = path.join(nm, '@anthropic-ai', 'claude-code');
|
|
127
153
|
const claudeDest = path.join(destNm, '@anthropic-ai', 'claude-code');
|
|
@@ -157,17 +183,8 @@ if (process.env.NO_BUNDLE_MODELS === 'true') {
|
|
|
157
183
|
}
|
|
158
184
|
}
|
|
159
185
|
|
|
160
|
-
fs.writeFileSync(path.join(out, 'README.txt'),
|
|
161
|
-
'# AgentGUI Portable'
|
|
162
|
-
'',
|
|
163
|
-
'No installation required. Double-click agentgui.exe to start.',
|
|
164
|
-
'',
|
|
165
|
-
'Web interface: http://localhost:3000/gm/',
|
|
166
|
-
'',
|
|
167
|
-
'Data is stored in the data/ folder next to the executable.',
|
|
168
|
-
'',
|
|
169
|
-
'Requirements: None - fully self-contained.',
|
|
170
|
-
].join('\n'));
|
|
186
|
+
fs.writeFileSync(path.join(out, 'README.txt'),
|
|
187
|
+
'# AgentGUI Portable\n\nDouble-click agentgui.exe to start.\nWeb interface: http://localhost:3000/gm/\nData stored in data/ folder. Fully self-contained.\n');
|
|
171
188
|
|
|
172
189
|
const totalMB = Math.round(sizeOf(out) / 1024 / 1024);
|
|
173
190
|
log(`Build complete! Total: ${totalMB}MB Output: ${out}`);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
9
|
+
const isWindows = os.platform() === 'win32';
|
|
10
|
+
|
|
11
|
+
const ACP_TOOLS = [
|
|
12
|
+
{ id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
|
|
13
|
+
{ id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const MAX_RESTARTS = 10;
|
|
17
|
+
const RESTART_WINDOW_MS = 300000;
|
|
18
|
+
const HEALTH_INTERVAL_MS = 30000;
|
|
19
|
+
const STARTUP_GRACE_MS = 5000;
|
|
20
|
+
const processes = new Map();
|
|
21
|
+
let healthTimer = null;
|
|
22
|
+
let shuttingDown = false;
|
|
23
|
+
|
|
24
|
+
function log(msg) { console.log('[ACP] ' + msg); }
|
|
25
|
+
|
|
26
|
+
function resolveBinary(cmd) {
|
|
27
|
+
const ext = isWindows ? '.cmd' : '';
|
|
28
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
|
|
29
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
30
|
+
return cmd;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function startProcess(tool) {
|
|
34
|
+
if (shuttingDown) return null;
|
|
35
|
+
const bin = resolveBinary(tool.cmd);
|
|
36
|
+
const args = [...tool.args, '--port', String(tool.port)];
|
|
37
|
+
const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
|
|
38
|
+
if (isWindows) opts.shell = true;
|
|
39
|
+
|
|
40
|
+
let proc;
|
|
41
|
+
try { proc = spawn(bin, args, opts); }
|
|
42
|
+
catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
|
|
43
|
+
|
|
44
|
+
const entry = {
|
|
45
|
+
id: tool.id, port: tool.port, process: proc, pid: proc.pid,
|
|
46
|
+
startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
proc.stdout.on('data', () => {});
|
|
50
|
+
proc.stderr.on('data', (d) => {
|
|
51
|
+
const t = d.toString().trim();
|
|
52
|
+
if (t) log(tool.id + ': ' + t.substring(0, 200));
|
|
53
|
+
});
|
|
54
|
+
proc.stdout.on('error', () => {});
|
|
55
|
+
proc.stderr.on('error', () => {});
|
|
56
|
+
proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
|
|
57
|
+
|
|
58
|
+
proc.on('close', (code) => {
|
|
59
|
+
entry.healthy = false;
|
|
60
|
+
if (shuttingDown) return;
|
|
61
|
+
log(tool.id + ' exited code ' + code);
|
|
62
|
+
scheduleRestart(tool, entry.restarts);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
processes.set(tool.id, entry);
|
|
66
|
+
log(tool.id + ' started port ' + tool.port + ' pid ' + proc.pid);
|
|
67
|
+
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scheduleRestart(tool, prevRestarts = []) {
|
|
72
|
+
if (shuttingDown) return;
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
|
|
75
|
+
if (recent.length >= MAX_RESTARTS) {
|
|
76
|
+
log(tool.id + ' exceeded restart limit, giving up');
|
|
77
|
+
processes.delete(tool.id);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
|
|
81
|
+
log(tool.id + ' restarting in ' + delay + 'ms');
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
if (shuttingDown) return;
|
|
84
|
+
const entry = startProcess(tool);
|
|
85
|
+
if (entry) entry.restarts = [...recent, Date.now()];
|
|
86
|
+
}, delay);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function checkHealth(toolId) {
|
|
90
|
+
const entry = processes.get(toolId);
|
|
91
|
+
if (!entry || shuttingDown) return;
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
|
|
94
|
+
signal: AbortSignal.timeout(3000), headers: { 'Accept': 'application/json' }
|
|
95
|
+
});
|
|
96
|
+
entry.healthy = res.ok;
|
|
97
|
+
} catch (_) { entry.healthy = false; }
|
|
98
|
+
entry.lastHealthCheck = Date.now();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function startAll() {
|
|
102
|
+
log('starting ACP tools...');
|
|
103
|
+
for (const tool of ACP_TOOLS) {
|
|
104
|
+
const bin = resolveBinary(tool.cmd);
|
|
105
|
+
if (bin === tool.cmd && !fs.existsSync(bin)) {
|
|
106
|
+
log(tool.id + ' not found, skipping');
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
startProcess(tool);
|
|
110
|
+
}
|
|
111
|
+
healthTimer = setInterval(() => {
|
|
112
|
+
for (const [id] of processes) checkHealth(id);
|
|
113
|
+
}, HEALTH_INTERVAL_MS);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function stopAll() {
|
|
117
|
+
shuttingDown = true;
|
|
118
|
+
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
119
|
+
const kills = [];
|
|
120
|
+
for (const [id, entry] of processes) {
|
|
121
|
+
log('stopping ' + id + ' pid ' + entry.pid);
|
|
122
|
+
kills.push(new Promise(resolve => {
|
|
123
|
+
const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
124
|
+
entry.process.on('close', () => { clearTimeout(t); resolve(); });
|
|
125
|
+
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
await Promise.all(kills);
|
|
129
|
+
processes.clear();
|
|
130
|
+
log('all stopped');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getStatus() {
|
|
134
|
+
return Array.from(processes.values()).map(e => ({
|
|
135
|
+
id: e.id, port: e.port, pid: e.pid, healthy: e.healthy,
|
|
136
|
+
uptime: Date.now() - e.startedAt, restartCount: e.restarts.length,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function getPort(agentId) {
|
|
141
|
+
const e = processes.get(agentId);
|
|
142
|
+
return e?.healthy ? e.port : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getRunningPorts() {
|
|
146
|
+
const ports = {};
|
|
147
|
+
for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
|
|
148
|
+
return ports;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function restart(agentId) {
|
|
152
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
153
|
+
if (!tool) return false;
|
|
154
|
+
const entry = processes.get(agentId);
|
|
155
|
+
if (entry) { try { entry.process.kill('SIGTERM'); } catch (_) {} processes.delete(agentId); }
|
|
156
|
+
startProcess(tool);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function queryModels(agentId) {
|
|
161
|
+
const entry = processes.get(agentId);
|
|
162
|
+
if (!entry?.healthy) return [];
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
|
|
165
|
+
signal: AbortSignal.timeout(5000), headers: { 'Accept': 'application/json' }
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) return [];
|
|
168
|
+
const data = await res.json();
|
|
169
|
+
const models = [];
|
|
170
|
+
for (const prov of (data.all || [])) {
|
|
171
|
+
for (const m of Object.values(prov.models || {})) {
|
|
172
|
+
models.push({ id: m.id, label: m.name || m.id, provider: prov.name || prov.id });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return models;
|
|
176
|
+
} catch (_) { return []; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const ACP_TOOL_CONFIGS = ACP_TOOLS;
|
|
@@ -68,71 +68,6 @@ async function acpFetch(port, pth, body = null) {
|
|
|
68
68
|
return res.json();
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function getApiKey(name) {
|
|
72
|
-
const envMap = {
|
|
73
|
-
anthropic: 'ANTHROPIC_API_KEY',
|
|
74
|
-
google: 'GOOGLE_GENAI_API_KEY',
|
|
75
|
-
openai: 'OPENAI_API_KEY'
|
|
76
|
-
};
|
|
77
|
-
if (process.env[envMap[name]]) return process.env[envMap[name]];
|
|
78
|
-
const configPaths = {
|
|
79
|
-
anthropic: [
|
|
80
|
-
path.join(os.homedir(), '.claude.json'),
|
|
81
|
-
path.join(os.homedir(), '.anthropic.json')
|
|
82
|
-
],
|
|
83
|
-
google: [
|
|
84
|
-
path.join(os.homedir(), '.gemini.json'),
|
|
85
|
-
path.join(os.homedir(), '.config', 'gemini', 'credentials.json')
|
|
86
|
-
],
|
|
87
|
-
openai: [
|
|
88
|
-
path.join(os.homedir(), '.openai.json')
|
|
89
|
-
]
|
|
90
|
-
};
|
|
91
|
-
for (const p of (configPaths[name] || [])) {
|
|
92
|
-
const data = readJson(p);
|
|
93
|
-
if (data) {
|
|
94
|
-
const key = data.api_key || data.apiKey || data.github_token;
|
|
95
|
-
if (key) return key;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function fetchModelsFromAPI(agentId) {
|
|
102
|
-
if (agentId === 'claude-code') {
|
|
103
|
-
const apiKey = getApiKey('anthropic');
|
|
104
|
-
if (!apiKey) return null;
|
|
105
|
-
try {
|
|
106
|
-
const res = await fetch('https://api.anthropic.com/v1/models', {
|
|
107
|
-
headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },
|
|
108
|
-
signal: AbortSignal.timeout(8000)
|
|
109
|
-
});
|
|
110
|
-
if (!res.ok) return null;
|
|
111
|
-
const data = await res.json();
|
|
112
|
-
const items = (data.data || []).filter(m => m.id && m.id.startsWith('claude-'));
|
|
113
|
-
if (items.length === 0) return null;
|
|
114
|
-
return items.map(m => ({ id: m.id, label: m.display_name || m.id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) }));
|
|
115
|
-
} catch { return null; }
|
|
116
|
-
}
|
|
117
|
-
if (agentId === 'gemini') {
|
|
118
|
-
const apiKey = getApiKey('google');
|
|
119
|
-
if (!apiKey) return null;
|
|
120
|
-
try {
|
|
121
|
-
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
|
|
122
|
-
signal: AbortSignal.timeout(8000)
|
|
123
|
-
});
|
|
124
|
-
if (!res.ok) return null;
|
|
125
|
-
const data = await res.json();
|
|
126
|
-
const items = (data.models || []).filter(m => m.name && m.name.includes('gemini'));
|
|
127
|
-
if (items.length === 0) return null;
|
|
128
|
-
return items.map(m => {
|
|
129
|
-
const id = m.name.replace(/^models\//, '');
|
|
130
|
-
return { id, label: id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) };
|
|
131
|
-
});
|
|
132
|
-
} catch { return null; }
|
|
133
|
-
}
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
71
|
|
|
137
72
|
export function register(router, deps) {
|
|
138
73
|
const { db, discoveredAgents, modelCache,
|
|
@@ -194,69 +129,29 @@ export function register(router, deps) {
|
|
|
194
129
|
return d;
|
|
195
130
|
});
|
|
196
131
|
|
|
197
|
-
router.handle('agent.models', async (p) => {
|
|
198
|
-
const cached = modelCache.get(p.id);
|
|
199
|
-
if (cached && (Date.now() - cached.ts) < 300000) return { models: cached.models };
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const id = m.id || m;
|
|
221
|
-
if (id) allModels.set(id, m.name || m.label || id);
|
|
222
|
-
}
|
|
223
|
-
try {
|
|
224
|
-
const desc = await acpFetch(port, `/agents/${agentId}/descriptor`);
|
|
225
|
-
const enumVals = desc?.specs?.config?.properties?.model?.enum || desc?.specs?.input?.properties?.model?.enum || [];
|
|
226
|
-
for (const v of enumVals) allModels.set(v, v);
|
|
227
|
-
} catch (_) {}
|
|
228
|
-
}
|
|
229
|
-
if (allModels.size > 0) {
|
|
230
|
-
const models = Array.from(allModels.entries()).map(([id, label]) => ({ id, label }));
|
|
231
|
-
modelCache.set(p.id, { models, ts: Date.now() });
|
|
232
|
-
return { models };
|
|
233
|
-
}
|
|
234
|
-
} catch (_) {}
|
|
235
|
-
}
|
|
236
|
-
const acpAgents = discoveredAgents.filter(x => x.protocol === 'acp' && x.acpPort);
|
|
237
|
-
for (const acpAgent of acpAgents) {
|
|
238
|
-
const port = acpAgent.acpPort;
|
|
239
|
-
try {
|
|
240
|
-
const desc = await acpFetch(port, `/agents/${p.id}/descriptor`);
|
|
241
|
-
if (desc) {
|
|
242
|
-
const enumVals = desc?.specs?.config?.properties?.model?.enum || desc?.specs?.input?.properties?.model?.enum || [];
|
|
243
|
-
if (enumVals.length > 0) {
|
|
244
|
-
const models = enumVals.map(v => ({ id: v, label: v }));
|
|
245
|
-
modelCache.set(p.id, { models, ts: Date.now() });
|
|
246
|
-
return { models };
|
|
247
|
-
}
|
|
248
|
-
const name = desc?.metadata?.ref?.name;
|
|
249
|
-
if (name) { const models = [{ id: p.id, label: name }]; modelCache.set(p.id, { models, ts: Date.now() }); return { models }; }
|
|
250
|
-
}
|
|
251
|
-
} catch (_) {}
|
|
252
|
-
}
|
|
253
|
-
const apiModels = await fetchModelsFromAPI(p.id);
|
|
254
|
-
if (apiModels && apiModels.length > 0) {
|
|
255
|
-
modelCache.set(p.id, { models: apiModels, ts: Date.now() });
|
|
256
|
-
return { models: apiModels };
|
|
257
|
-
}
|
|
258
|
-
return { models: [] };
|
|
259
|
-
});
|
|
132
|
+
router.handle('agent.models', async (p) => {
|
|
133
|
+
const cached = modelCache.get(p.id);
|
|
134
|
+
if (cached && (Date.now() - cached.ts) < 300000) return { models: cached.models };
|
|
135
|
+
let models = [];
|
|
136
|
+
if (p.id === 'claude-code') {
|
|
137
|
+
models = [
|
|
138
|
+
{ id: 'claude-haiku', label: 'Haiku' },
|
|
139
|
+
{ id: 'claude-sonnet', label: 'Sonnet' },
|
|
140
|
+
{ id: 'claude-opus', label: 'Opus' }
|
|
141
|
+
];
|
|
142
|
+
} else {
|
|
143
|
+
const agent = discoveredAgents.find(x => x.id === p.id);
|
|
144
|
+
if (agent?.protocol === 'acp' && agent.acpPort) {
|
|
145
|
+
try {
|
|
146
|
+
const data = await acpFetch(agent.acpPort, '/models');
|
|
147
|
+
const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
|
|
148
|
+
models = list.map(m => ({ id: m.id || m.model_id, label: m.name || m.display_name || m.id || m.model_id })).filter(m => m.id);
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (models.length > 0) modelCache.set(p.id, { models, ts: Date.now() });
|
|
153
|
+
return { models };
|
|
154
|
+
});
|
|
260
155
|
|
|
261
156
|
router.handle('agent.search', (p) => db.searchAgents(discoveredAgents, p.query || p));
|
|
262
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.438",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"fsbrowse": "^0.2.18",
|
|
34
34
|
"google-auth-library": "^10.5.0",
|
|
35
35
|
"onnxruntime-node": "1.21.0",
|
|
36
|
-
"opencode-ai": "
|
|
36
|
+
"opencode-ai": "^1.2.15",
|
|
37
37
|
"puppeteer-core": "^24.37.5",
|
|
38
38
|
"webtalk": "^1.0.31",
|
|
39
39
|
"ws": "^8.14.2"
|
package/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { register as registerConvHandlers } from './lib/ws-handlers-conv.js';
|
|
|
22
22
|
import { register as registerSessionHandlers } from './lib/ws-handlers-session.js';
|
|
23
23
|
import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
24
24
|
import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
25
|
+
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, queryModels as queryACPModels } from './lib/acp-manager.js';
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -411,22 +412,13 @@ function discoverAgents() {
|
|
|
411
412
|
|
|
412
413
|
// Function to discover agents from external ACP servers
|
|
413
414
|
async function discoverExternalACPServers() {
|
|
414
|
-
// Default ACP servers to query (excluding local server to prevent recursion)
|
|
415
|
-
const acpServers = [
|
|
416
|
-
'http://localhost:8080', // Common default ACP port
|
|
417
|
-
];
|
|
418
|
-
|
|
419
415
|
const externalAgents = [];
|
|
420
|
-
for (const
|
|
416
|
+
for (const agent of discoveredAgents.filter(a => a.protocol === 'acp' && a.acpPort)) {
|
|
421
417
|
try {
|
|
422
|
-
|
|
423
|
-
const agents = await queryACPServerAgents(serverUrl);
|
|
418
|
+
const agents = await queryACPServerAgents(`http://localhost:${agent.acpPort}`);
|
|
424
419
|
externalAgents.push(...agents);
|
|
425
|
-
} catch (
|
|
426
|
-
console.error(`Failed to query ${serverUrl}:`, error.message);
|
|
427
|
-
}
|
|
420
|
+
} catch (_) {}
|
|
428
421
|
}
|
|
429
|
-
|
|
430
422
|
return externalAgents;
|
|
431
423
|
}
|
|
432
424
|
|
|
@@ -435,53 +427,40 @@ initializeDescriptors(discoveredAgents);
|
|
|
435
427
|
|
|
436
428
|
const modelCache = new Map();
|
|
437
429
|
|
|
438
|
-
function modelIdToLabel(id) {
|
|
439
|
-
const base = id.replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
440
|
-
const m = base.match(/^(\w+)-(\d+)(?:-(\d+))?$/);
|
|
441
|
-
if (m) return `${m[1].charAt(0).toUpperCase() + m[1].slice(1)} ${m[3] ? m[2] + '.' + m[3] : m[2]}`;
|
|
442
|
-
return base.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
443
|
-
}
|
|
444
|
-
|
|
445
430
|
async function getModelsForAgent(agentId) {
|
|
446
431
|
const cached = modelCache.get(agentId);
|
|
447
|
-
if (cached && Date.now() - cached.timestamp <
|
|
448
|
-
let models =
|
|
432
|
+
if (cached && Date.now() - cached.timestamp < 300000) return cached.models;
|
|
433
|
+
let models = [];
|
|
449
434
|
if (agentId === 'claude-code') {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const id = m.name.replace(/^models\//, '');
|
|
476
|
-
return { id, label: id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) };
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
} catch (_) {}
|
|
435
|
+
models = [
|
|
436
|
+
{ id: 'claude-haiku', label: 'Haiku' },
|
|
437
|
+
{ id: 'claude-sonnet', label: 'Sonnet' },
|
|
438
|
+
{ id: 'claude-opus', label: 'Opus' }
|
|
439
|
+
];
|
|
440
|
+
} else {
|
|
441
|
+
const agent = discoveredAgents.find(a => a.id === agentId);
|
|
442
|
+
if (agent?.protocol === 'acp') {
|
|
443
|
+
const acpPort = getACPPort(agentId) || agent.acpPort;
|
|
444
|
+
if (acpPort) {
|
|
445
|
+
try {
|
|
446
|
+
models = await queryACPModels(agentId);
|
|
447
|
+
if (!models.length) {
|
|
448
|
+
const res = await fetch(`http://localhost:${acpPort}/models`, {
|
|
449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
450
|
+
signal: AbortSignal.timeout(3000)
|
|
451
|
+
});
|
|
452
|
+
if (res.ok) {
|
|
453
|
+
const data = await res.json();
|
|
454
|
+
const list = data?.data || data?.models || (Array.isArray(data) ? data : []);
|
|
455
|
+
models = list.map(m => ({ id: m.id || m.model_id, label: m.name || m.display_name || m.id || m.model_id })).filter(m => m.id);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} catch (_) {}
|
|
459
|
+
}
|
|
480
460
|
}
|
|
481
461
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return result;
|
|
462
|
+
modelCache.set(agentId, { models, timestamp: Date.now() });
|
|
463
|
+
return models;
|
|
485
464
|
}
|
|
486
465
|
|
|
487
466
|
const GEMINI_SCOPES = [
|
|
@@ -1775,6 +1754,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
1775
1754
|
return;
|
|
1776
1755
|
}
|
|
1777
1756
|
|
|
1757
|
+
if (pathOnly === '/api/acp/status' && req.method === 'GET') {
|
|
1758
|
+
sendJSON(req, res, 200, { tools: getACPStatus() });
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1778
1762
|
if (pathOnly === '/api/ws-stats' && req.method === 'GET') {
|
|
1779
1763
|
const stats = wsOptimizer.getStats();
|
|
1780
1764
|
sendJSON(req, res, 200, stats);
|
|
@@ -3851,7 +3835,7 @@ registerConvHandlers(wsRouter, {
|
|
|
3851
3835
|
});
|
|
3852
3836
|
|
|
3853
3837
|
registerSessionHandlers(wsRouter, {
|
|
3854
|
-
db: queries, discoveredAgents,
|
|
3838
|
+
db: queries, discoveredAgents, modelCache,
|
|
3855
3839
|
getAgentDescriptor, activeScripts, broadcastSync,
|
|
3856
3840
|
startGeminiOAuth, geminiOAuthState: () => geminiOAuthState
|
|
3857
3841
|
});
|
|
@@ -3993,7 +3977,11 @@ if (watch) {
|
|
|
3993
3977
|
|
|
3994
3978
|
process.on('SIGTERM', () => {
|
|
3995
3979
|
console.log('[SIGNAL] SIGTERM received - graceful shutdown');
|
|
3996
|
-
|
|
3980
|
+
stopACPTools().then(() => {
|
|
3981
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
3982
|
+
}).catch(() => {
|
|
3983
|
+
wss.close(() => server.close(() => process.exit(0)));
|
|
3984
|
+
});
|
|
3997
3985
|
});
|
|
3998
3986
|
|
|
3999
3987
|
server.on('error', (err) => {
|
|
@@ -4196,6 +4184,21 @@ function onServerReady() {
|
|
|
4196
4184
|
|
|
4197
4185
|
resumeInterruptedStreams().catch(err => console.error('[RESUME] Startup error:', err.message));
|
|
4198
4186
|
|
|
4187
|
+
startACPTools().then(() => {
|
|
4188
|
+
setTimeout(() => {
|
|
4189
|
+
const acpStatus = getACPStatus();
|
|
4190
|
+
for (const s of acpStatus) {
|
|
4191
|
+
if (s.healthy) {
|
|
4192
|
+
const agent = discoveredAgents.find(a => a.id === s.id);
|
|
4193
|
+
if (agent) { agent.acpPort = s.port; }
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
if (acpStatus.length > 0) {
|
|
4197
|
+
console.log(`[ACP] Tools ready: ${acpStatus.filter(s => s.healthy).map(s => s.id + ':' + s.port).join(', ') || 'none healthy yet'}`);
|
|
4198
|
+
}
|
|
4199
|
+
}, 6000);
|
|
4200
|
+
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
4201
|
+
|
|
4199
4202
|
ensureModelsDownloaded().then(async ok => {
|
|
4200
4203
|
if (ok) console.log('[MODELS] Speech models ready');
|
|
4201
4204
|
else console.log('[MODELS] Speech model download failed');
|