agentgui 1.0.582 → 1.0.584
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 +36 -0
- package/docs/index.html +65 -0
- package/lib/{acp-manager.js → acp-sdk-manager.js} +225 -164
- package/lib/ws-handlers-session.js +1 -1
- package/package.json +2 -1
- package/server.js +48 -21
- package/static/js/client.js +9 -2
- package/static/js/conversations.js +5 -0
- package/lib/acp-http-client.js +0 -125
- package/lib/acp-process-lifecycle.js +0 -65
package/CLAUDE.md
CHANGED
|
@@ -350,3 +350,39 @@ queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: D
|
|
|
350
350
|
- Database persists across page reload ✓
|
|
351
351
|
- Frontend shows "Up-to-date" or "Update available" ✓
|
|
352
352
|
- Tool install history records the action ✓
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## ACP SDK Integration
|
|
357
|
+
|
|
358
|
+
### Current Status
|
|
359
|
+
- **@agentclientprotocol/sdk** (`^0.4.1`) has been added to dependencies
|
|
360
|
+
- The SDK is positioned as the main protocol for client-server and server-ACP tools communication
|
|
361
|
+
|
|
362
|
+
### Clear All Conversations Fix
|
|
363
|
+
|
|
364
|
+
**Issue:** After clicking "Clear All Conversations", the conversation threads would reappear in the sidebar.
|
|
365
|
+
|
|
366
|
+
**Root Cause:** The `all_conversations_deleted` broadcast event was being sent by the server (in `lib/ws-handlers-conv.js`), but:
|
|
367
|
+
1. The event type was not in the `BROADCAST_TYPES` set in `server.js`, so it wasn't being broadcast to all clients
|
|
368
|
+
2. The conversation manager (`static/js/conversations.js`) had no handler for this event type
|
|
369
|
+
3. Client cleanup in `handleAllConversationsDeleted` was incomplete
|
|
370
|
+
|
|
371
|
+
**Solution Applied:**
|
|
372
|
+
1. Added `'all_conversations_deleted'` to `BROADCAST_TYPES` set (server.js:4147)
|
|
373
|
+
2. Added event handler in conversation manager to clear all local state (conversations.js:573-577)
|
|
374
|
+
3. Enhanced client cleanup to clear all caches and state before reloading (client.js:1321-1330)
|
|
375
|
+
|
|
376
|
+
**Files Modified:**
|
|
377
|
+
- `server.js`: Added `all_conversations_deleted` to BROADCAST_TYPES
|
|
378
|
+
- `static/js/conversations.js`: Added handler for all_conversations_deleted event
|
|
379
|
+
- `static/js/client.js`: Enhanced handleAllConversationsDeleted with complete state cleanup
|
|
380
|
+
|
|
381
|
+
### Next Steps for Full ACP SDK Integration
|
|
382
|
+
The ACP SDK dependency has been added. Full integration would involve:
|
|
383
|
+
1. Replacing custom WebSocket protocol with ACP SDK's RPC/messaging layer
|
|
384
|
+
2. Updating `lib/acp-manager.js` to use ACP SDK for ACP tool communication
|
|
385
|
+
3. Migrating `lib/ws-protocol.js` handlers to use ACP SDK message types
|
|
386
|
+
4. Updating client-side WebSocket handlers to work with ACP SDK events
|
|
387
|
+
|
|
388
|
+
This refactoring is optional and can be done incrementally as needed.
|
package/docs/index.html
CHANGED
|
@@ -364,6 +364,31 @@
|
|
|
364
364
|
</div>
|
|
365
365
|
</section>
|
|
366
366
|
|
|
367
|
+
<!-- Why AgentGUI -->
|
|
368
|
+
<section class="section">
|
|
369
|
+
<div class="container">
|
|
370
|
+
<h2 class="section-title">Why AgentGUI?</h2>
|
|
371
|
+
<p class="section-subtitle">Stop juggling multiple terminal windows. Work with all your AI agents in one unified interface.</p>
|
|
372
|
+
|
|
373
|
+
<div class="features-grid">
|
|
374
|
+
<div class="feature-card">
|
|
375
|
+
<h3>📊 Compare Side-by-Side</h3>
|
|
376
|
+
<p>Test the same prompt across Claude Code, Gemini CLI, OpenCode, and others to find the best approach for your task.</p>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<div class="feature-card">
|
|
380
|
+
<h3>💾 Never Lose Context</h3>
|
|
381
|
+
<p>Every conversation, file change, and terminal output is automatically saved. Resume interrupted work exactly where you left off.</p>
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
<div class="feature-card">
|
|
385
|
+
<h3>👁️ See Everything</h3>
|
|
386
|
+
<p>Watch streaming responses, file changes, and tool calls in real-time instead of parsing raw JSON in your terminal.</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
</section>
|
|
391
|
+
|
|
367
392
|
<!-- Features -->
|
|
368
393
|
<section class="section" style="background: #f9fafb;">
|
|
369
394
|
<div class="container">
|
|
@@ -450,6 +475,46 @@
|
|
|
450
475
|
</div>
|
|
451
476
|
</section>
|
|
452
477
|
|
|
478
|
+
<!-- Use Cases -->
|
|
479
|
+
<section class="section">
|
|
480
|
+
<div class="container">
|
|
481
|
+
<h2 class="section-title">Use Cases</h2>
|
|
482
|
+
<p class="section-subtitle">Practical scenarios where AgentGUI shines</p>
|
|
483
|
+
|
|
484
|
+
<div class="features-grid">
|
|
485
|
+
<div class="feature-card">
|
|
486
|
+
<h3>Multi-Agent Comparison</h3>
|
|
487
|
+
<p>Run the same task through different agents to compare approaches, code quality, and execution speed.</p>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div class="feature-card">
|
|
491
|
+
<h3>Long-Running Projects</h3>
|
|
492
|
+
<p>Build complex features across multiple sessions without losing context or conversation history.</p>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
495
|
+
<div class="feature-card">
|
|
496
|
+
<h3>Team Collaboration</h3>
|
|
497
|
+
<p>Share conversation URLs and working directories for pair programming with AI agents.</p>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<div class="feature-card">
|
|
501
|
+
<h3>Agent Development</h3>
|
|
502
|
+
<p>Test and debug custom agents with full visibility into streaming events and tool calls.</p>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<div class="feature-card">
|
|
506
|
+
<h3>Offline Speech</h3>
|
|
507
|
+
<p>Use local speech-to-text and text-to-speech without API costs or internet dependency.</p>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div class="feature-card">
|
|
511
|
+
<h3>Code Review Sessions</h3>
|
|
512
|
+
<p>Review AI-generated changes in a visual diff view with file browser integration.</p>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
</div>
|
|
516
|
+
</section>
|
|
517
|
+
|
|
453
518
|
<!-- Architecture -->
|
|
454
519
|
<section class="section" style="background: #f9fafb;">
|
|
455
520
|
<div class="container">
|
|
@@ -1,164 +1,225 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
entry
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const entry = processes.get(
|
|
91
|
-
if (entry)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
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 HEALTH_INTERVAL_MS = 30000;
|
|
17
|
+
const STARTUP_GRACE_MS = 5000;
|
|
18
|
+
const MAX_RESTARTS = 10;
|
|
19
|
+
const RESTART_WINDOW_MS = 300000;
|
|
20
|
+
const IDLE_TIMEOUT_MS = 120000;
|
|
21
|
+
|
|
22
|
+
const processes = new Map();
|
|
23
|
+
let healthTimer = null;
|
|
24
|
+
let shuttingDown = false;
|
|
25
|
+
|
|
26
|
+
function log(msg) { console.log('[ACP-SDK] ' + msg); }
|
|
27
|
+
|
|
28
|
+
function resolveBinary(cmd) {
|
|
29
|
+
const ext = isWindows ? '.cmd' : '';
|
|
30
|
+
const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
|
|
31
|
+
if (fs.existsSync(localBin)) return localBin;
|
|
32
|
+
return cmd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function startProcess(tool) {
|
|
36
|
+
if (shuttingDown) return null;
|
|
37
|
+
const existing = processes.get(tool.id);
|
|
38
|
+
if (existing?.process && !existing.process.killed) return existing;
|
|
39
|
+
|
|
40
|
+
const cmd = resolveBinary(tool.cmd);
|
|
41
|
+
const entry = {
|
|
42
|
+
id: tool.id,
|
|
43
|
+
port: tool.port,
|
|
44
|
+
startedAt: Date.now(),
|
|
45
|
+
lastUsed: Date.now(),
|
|
46
|
+
lastHealthCheck: 0,
|
|
47
|
+
healthy: false,
|
|
48
|
+
process: null,
|
|
49
|
+
pid: null,
|
|
50
|
+
restarts: [],
|
|
51
|
+
idleTimer: null,
|
|
52
|
+
providerInfo: null,
|
|
53
|
+
_stopping: false
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
entry.process = spawn(cmd, tool.args, {
|
|
58
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
59
|
+
detached: false
|
|
60
|
+
});
|
|
61
|
+
entry.pid = entry.process.pid;
|
|
62
|
+
|
|
63
|
+
entry.process.on('close', (code) => {
|
|
64
|
+
entry.healthy = false;
|
|
65
|
+
if (shuttingDown || entry._stopping) return;
|
|
66
|
+
log(tool.id + ' exited code ' + code);
|
|
67
|
+
const window = Date.now() - RESTART_WINDOW_MS;
|
|
68
|
+
entry.restarts = entry.restarts.filter(t => t > window);
|
|
69
|
+
if (entry.restarts.length < MAX_RESTARTS) {
|
|
70
|
+
const delay = Math.min(1000 * Math.pow(2, entry.restarts.length), 30000);
|
|
71
|
+
entry.restarts.push(Date.now());
|
|
72
|
+
setTimeout(() => startProcess(tool), delay);
|
|
73
|
+
} else {
|
|
74
|
+
log(tool.id + ' max restarts reached');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
processes.set(tool.id, entry);
|
|
79
|
+
log(tool.id + ' started port ' + tool.port + ' pid ' + entry.pid);
|
|
80
|
+
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
81
|
+
resetIdleTimer(tool.id);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
log(tool.id + ' spawn failed: ' + err.message);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resetIdleTimer(toolId) {
|
|
90
|
+
const entry = processes.get(toolId);
|
|
91
|
+
if (!entry) return;
|
|
92
|
+
entry.lastUsed = Date.now();
|
|
93
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
94
|
+
entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function stopTool(toolId) {
|
|
98
|
+
const entry = processes.get(toolId);
|
|
99
|
+
if (!entry) return;
|
|
100
|
+
log(toolId + ' idle, stopping to free RAM');
|
|
101
|
+
entry._stopping = true;
|
|
102
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
103
|
+
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
104
|
+
setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} }, 5000);
|
|
105
|
+
processes.delete(toolId);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function checkHealth(toolId) {
|
|
109
|
+
const entry = processes.get(toolId);
|
|
110
|
+
if (!entry || shuttingDown) return;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch('http://127.0.0.1:' + entry.port + '/provider', {
|
|
114
|
+
signal: AbortSignal.timeout(3000)
|
|
115
|
+
});
|
|
116
|
+
entry.healthy = res.ok;
|
|
117
|
+
if (res.ok) {
|
|
118
|
+
entry.providerInfo = await res.json();
|
|
119
|
+
}
|
|
120
|
+
} catch (_) {
|
|
121
|
+
entry.healthy = false;
|
|
122
|
+
}
|
|
123
|
+
entry.lastHealthCheck = Date.now();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function ensureRunning(agentId) {
|
|
127
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
128
|
+
if (!tool) return null;
|
|
129
|
+
let entry = processes.get(agentId);
|
|
130
|
+
if (entry?.healthy) { resetIdleTimer(agentId); return entry.port; }
|
|
131
|
+
if (!entry || entry._stopping) {
|
|
132
|
+
entry = startProcess(tool);
|
|
133
|
+
if (!entry) return null;
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < 20; i++) {
|
|
136
|
+
await new Promise(r => setTimeout(r, 500));
|
|
137
|
+
await checkHealth(agentId);
|
|
138
|
+
if (processes.get(agentId)?.healthy) { resetIdleTimer(agentId); return tool.port; }
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function touch(agentId) {
|
|
144
|
+
const entry = processes.get(agentId);
|
|
145
|
+
if (entry) resetIdleTimer(agentId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function startAll() {
|
|
149
|
+
log('ACP tools available (on-demand start)');
|
|
150
|
+
healthTimer = setInterval(() => {
|
|
151
|
+
for (const [id] of processes) checkHealth(id);
|
|
152
|
+
}, HEALTH_INTERVAL_MS);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function stopAll() {
|
|
156
|
+
shuttingDown = true;
|
|
157
|
+
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
158
|
+
const kills = [];
|
|
159
|
+
for (const [id, entry] of processes) {
|
|
160
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
161
|
+
log('stopping ' + id + ' pid ' + entry.pid);
|
|
162
|
+
kills.push(new Promise(resolve => {
|
|
163
|
+
const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
164
|
+
entry.process.on('close', () => { clearTimeout(t); resolve(); });
|
|
165
|
+
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
await Promise.all(kills);
|
|
169
|
+
processes.clear();
|
|
170
|
+
log('all stopped');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getStatus() {
|
|
174
|
+
return ACP_TOOLS.map(tool => {
|
|
175
|
+
const e = processes.get(tool.id);
|
|
176
|
+
return {
|
|
177
|
+
id: tool.id,
|
|
178
|
+
port: tool.port,
|
|
179
|
+
running: !!e,
|
|
180
|
+
healthy: e?.healthy || false,
|
|
181
|
+
pid: e?.pid,
|
|
182
|
+
uptime: e ? Date.now() - e.startedAt : 0,
|
|
183
|
+
restartCount: e?.restarts.length || 0,
|
|
184
|
+
idleMs: e ? Date.now() - e.lastUsed : 0,
|
|
185
|
+
providerInfo: e?.providerInfo || null,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getPort(agentId) {
|
|
191
|
+
const e = processes.get(agentId);
|
|
192
|
+
return e?.healthy ? e.port : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function getRunningPorts() {
|
|
196
|
+
const ports = {};
|
|
197
|
+
for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
|
|
198
|
+
return ports;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function restart(agentId) {
|
|
202
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
203
|
+
if (!tool) return false;
|
|
204
|
+
stopTool(agentId);
|
|
205
|
+
startProcess(tool);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function queryModels(agentId) {
|
|
210
|
+
const port = await ensureRunning(agentId);
|
|
211
|
+
if (!port) return [];
|
|
212
|
+
try {
|
|
213
|
+
const res = await fetch('http://127.0.0.1:' + port + '/models', {
|
|
214
|
+
signal: AbortSignal.timeout(3000)
|
|
215
|
+
});
|
|
216
|
+
if (!res.ok) return [];
|
|
217
|
+
const data = await res.json();
|
|
218
|
+
return data.models || [];
|
|
219
|
+
} catch (_) { return []; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function isAvailable(agentId) {
|
|
223
|
+
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
224
|
+
return !!tool;
|
|
225
|
+
}
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
|
-
import { ensureRunning, touch, queryModels } from './acp-manager.js';
|
|
5
|
+
import { ensureRunning, touch, queryModels } from './acp-sdk-manager.js';
|
|
6
6
|
|
|
7
7
|
function spawnScript(cmd, args, convId, scriptName, agentId, deps) {
|
|
8
8
|
const { activeScripts, broadcastSync, modelCache } = deps;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.584",
|
|
4
4
|
"description": "Multi-agent ACP client with real-time communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"postinstall": "node scripts/patch-fsbrowse.js"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@agentclientprotocol/sdk": "^0.4.1",
|
|
24
25
|
"@anthropic-ai/claude-code": "^2.1.37",
|
|
25
26
|
"@google/gemini-cli": "latest",
|
|
26
27
|
"@huggingface/transformers": "^3.8.1",
|
package/server.js
CHANGED
|
@@ -22,7 +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, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-manager.js';
|
|
25
|
+
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, ensureRunning, queryModels as queryACPModels, touch as touchACP } from './lib/acp-sdk-manager.js';
|
|
26
26
|
import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
|
|
27
27
|
import * as toolManager from './lib/tool-manager.js';
|
|
28
28
|
import { pm2Manager } from './lib/pm2-manager.js';
|
|
@@ -398,27 +398,54 @@ function findCommand(cmd) {
|
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
async function queryACPServerAgents(baseUrl) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
401
|
+
const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
|
|
402
|
+
try {
|
|
403
|
+
const response = await fetch(endpoint, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
406
|
+
body: JSON.stringify({}),
|
|
407
|
+
signal: AbortSignal.timeout(5000)
|
|
408
|
+
});
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
console.error(`Failed to query ACP agents from ${baseUrl}: ${response.status}`);
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
413
|
+
const data = await response.json();
|
|
414
|
+
if (!data?.agents || !Array.isArray(data.agents)) {
|
|
415
|
+
console.error(`Invalid agents response from ${baseUrl}`);
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
return data.agents.map(agent => ({
|
|
419
|
+
id: agent.agent_id || agent.id,
|
|
420
|
+
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
421
|
+
metadata: {
|
|
422
|
+
ref: {
|
|
423
|
+
name: agent.metadata?.ref?.name,
|
|
424
|
+
version: agent.metadata?.ref?.version,
|
|
425
|
+
url: agent.metadata?.ref?.url,
|
|
426
|
+
tags: agent.metadata?.ref?.tags
|
|
427
|
+
},
|
|
428
|
+
description: agent.metadata?.description,
|
|
429
|
+
author: agent.metadata?.author,
|
|
430
|
+
license: agent.metadata?.license
|
|
431
|
+
},
|
|
432
|
+
specs: agent.specs ? {
|
|
433
|
+
capabilities: agent.specs.capabilities,
|
|
434
|
+
input_schema: agent.specs.input_schema || agent.specs.input,
|
|
435
|
+
output_schema: agent.specs.output_schema || agent.specs.output,
|
|
436
|
+
thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
|
|
437
|
+
config_schema: agent.specs.config_schema || agent.specs.config,
|
|
438
|
+
custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
|
|
439
|
+
} : null,
|
|
440
|
+
custom_data: agent.custom_data,
|
|
441
|
+
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
442
|
+
protocol: 'acp',
|
|
443
|
+
path: baseUrl
|
|
444
|
+
}));
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(`ACP agents query failed for ${baseUrl}: ${error.message}`);
|
|
412
447
|
return [];
|
|
413
448
|
}
|
|
414
|
-
|
|
415
|
-
return result.data.agents.map(agent => {
|
|
416
|
-
const complete = extractCompleteAgentData(agent);
|
|
417
|
-
return {
|
|
418
|
-
...complete,
|
|
419
|
-
path: baseUrl
|
|
420
|
-
};
|
|
421
|
-
});
|
|
422
449
|
}
|
|
423
450
|
|
|
424
451
|
function discoverAgents() {
|
|
@@ -4117,7 +4144,7 @@ wss.on('connection', (ws, req) => {
|
|
|
4117
4144
|
|
|
4118
4145
|
const BROADCAST_TYPES = new Set([
|
|
4119
4146
|
'message_created', 'conversation_created', 'conversation_updated',
|
|
4120
|
-
'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
|
|
4147
|
+
'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated',
|
|
4121
4148
|
'rate_limit_hit', 'rate_limit_clear',
|
|
4122
4149
|
'script_started', 'script_stopped', 'script_output',
|
|
4123
4150
|
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
|
package/static/js/client.js
CHANGED
|
@@ -1317,12 +1317,19 @@ class AgentGUIClient {
|
|
|
1317
1317
|
this.enableControls();
|
|
1318
1318
|
}
|
|
1319
1319
|
|
|
1320
|
-
handleAllConversationsDeleted(data) {
|
|
1320
|
+
async handleAllConversationsDeleted(data) {
|
|
1321
1321
|
this.state.currentConversation = null;
|
|
1322
|
+
this.state.conversations = [];
|
|
1323
|
+
this.state.sessionEvents = [];
|
|
1324
|
+
this.conversationCache.clear();
|
|
1325
|
+
this.conversationListCache = { data: [], timestamp: 0, ttl: 30000 };
|
|
1326
|
+
this.draftPrompts.clear();
|
|
1322
1327
|
window.dispatchEvent(new CustomEvent('conversation-deselected'));
|
|
1323
1328
|
if (window.conversationManager) {
|
|
1324
|
-
|
|
1329
|
+
this.state.currentConversation = null;
|
|
1330
|
+
await window.conversationManager.loadConversations();
|
|
1325
1331
|
}
|
|
1332
|
+
this.clearOutput();
|
|
1326
1333
|
}
|
|
1327
1334
|
|
|
1328
1335
|
isHtmlContent(text) {
|
|
@@ -568,6 +568,11 @@ class ConversationManager {
|
|
|
568
568
|
this.updateConversation(msg.conversation.id, msg.conversation);
|
|
569
569
|
} else if (msg.type === 'conversation_deleted') {
|
|
570
570
|
this.deleteConversation(msg.conversationId);
|
|
571
|
+
} else if (msg.type === 'all_conversations_deleted') {
|
|
572
|
+
this.conversations = [];
|
|
573
|
+
this.activeId = null;
|
|
574
|
+
this.streamingConversations.clear();
|
|
575
|
+
this.showEmpty('No conversations yet');
|
|
571
576
|
} else if (msg.type === 'streaming_start' && msg.conversationId) {
|
|
572
577
|
this.streamingConversations.add(msg.conversationId);
|
|
573
578
|
this.render();
|
package/lib/acp-http-client.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ACP HTTP Client with comprehensive request/response logging
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
function logACPCall(method, url, requestData, responseData, error = null) {
|
|
6
|
-
const timestamp = new Date().toISOString();
|
|
7
|
-
const logEntry = {
|
|
8
|
-
timestamp,
|
|
9
|
-
method,
|
|
10
|
-
url,
|
|
11
|
-
request: requestData,
|
|
12
|
-
response: responseData,
|
|
13
|
-
error: error ? error.message : null
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
console.log('[ACP-HTTP]', JSON.stringify(logEntry, null, 2));
|
|
17
|
-
return logEntry;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function fetchACPProvider(baseUrl, port) {
|
|
21
|
-
const url = baseUrl + ':' + port + '/provider';
|
|
22
|
-
const startTime = Date.now();
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
console.log('[ACP-HTTP] → GET ' + url);
|
|
26
|
-
|
|
27
|
-
const response = await fetch(url, {
|
|
28
|
-
method: 'GET',
|
|
29
|
-
headers: { 'Accept': 'application/json' },
|
|
30
|
-
signal: AbortSignal.timeout(3000)
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const data = response.ok ? await response.json() : null;
|
|
34
|
-
const duration = Date.now() - startTime;
|
|
35
|
-
|
|
36
|
-
logACPCall('GET', url, {
|
|
37
|
-
headers: { 'Accept': 'application/json' },
|
|
38
|
-
timeout: 3000
|
|
39
|
-
}, {
|
|
40
|
-
status: response.status,
|
|
41
|
-
statusText: response.statusText,
|
|
42
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
43
|
-
body: data,
|
|
44
|
-
duration_ms: duration
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
return { ok: response.ok, status: response.status, data };
|
|
48
|
-
} catch (error) {
|
|
49
|
-
logACPCall('GET', url, { headers: { 'Accept': 'application/json' } }, null, error);
|
|
50
|
-
return { ok: false, status: 0, data: null, error: error.message };
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function fetchACPAgents(baseUrl) {
|
|
55
|
-
const endpoint = baseUrl.endsWith('/') ? baseUrl + 'agents/search' : baseUrl + '/agents/search';
|
|
56
|
-
const requestBody = {};
|
|
57
|
-
const startTime = Date.now();
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
console.log('[ACP-HTTP] → POST ' + endpoint);
|
|
61
|
-
console.log('[ACP-HTTP] Request body: ' + JSON.stringify(requestBody));
|
|
62
|
-
|
|
63
|
-
const response = await fetch(endpoint, {
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: {
|
|
66
|
-
'Content-Type': 'application/json',
|
|
67
|
-
'Accept': 'application/json'
|
|
68
|
-
},
|
|
69
|
-
body: JSON.stringify(requestBody),
|
|
70
|
-
signal: AbortSignal.timeout(5000)
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const data = response.ok ? await response.json() : null;
|
|
74
|
-
const duration = Date.now() - startTime;
|
|
75
|
-
|
|
76
|
-
logACPCall('POST', endpoint, {
|
|
77
|
-
headers: {
|
|
78
|
-
'Content-Type': 'application/json',
|
|
79
|
-
'Accept': 'application/json'
|
|
80
|
-
},
|
|
81
|
-
body: requestBody,
|
|
82
|
-
timeout: 5000
|
|
83
|
-
}, {
|
|
84
|
-
status: response.status,
|
|
85
|
-
statusText: response.statusText,
|
|
86
|
-
headers: Object.fromEntries(response.headers.entries()),
|
|
87
|
-
body: data,
|
|
88
|
-
duration_ms: duration
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
return { ok: response.ok, status: response.status, data };
|
|
92
|
-
} catch (error) {
|
|
93
|
-
logACPCall('POST', endpoint, { body: requestBody }, null, error);
|
|
94
|
-
return { ok: false, status: 0, data: null, error: error.message };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function extractCompleteAgentData(agent) {
|
|
99
|
-
return {
|
|
100
|
-
id: agent.agent_id || agent.id,
|
|
101
|
-
name: agent.metadata?.ref?.name || agent.name || 'Unknown Agent',
|
|
102
|
-
metadata: {
|
|
103
|
-
ref: {
|
|
104
|
-
name: agent.metadata?.ref?.name,
|
|
105
|
-
version: agent.metadata?.ref?.version,
|
|
106
|
-
url: agent.metadata?.ref?.url,
|
|
107
|
-
tags: agent.metadata?.ref?.tags
|
|
108
|
-
},
|
|
109
|
-
description: agent.metadata?.description,
|
|
110
|
-
author: agent.metadata?.author,
|
|
111
|
-
license: agent.metadata?.license
|
|
112
|
-
},
|
|
113
|
-
specs: agent.specs ? {
|
|
114
|
-
capabilities: agent.specs.capabilities,
|
|
115
|
-
input_schema: agent.specs.input_schema || agent.specs.input,
|
|
116
|
-
output_schema: agent.specs.output_schema || agent.specs.output,
|
|
117
|
-
thread_state_schema: agent.specs.thread_state_schema || agent.specs.thread_state,
|
|
118
|
-
config_schema: agent.specs.config_schema || agent.specs.config,
|
|
119
|
-
custom_streaming_update_schema: agent.specs.custom_streaming_update_schema || agent.specs.custom_streaming_update
|
|
120
|
-
} : null,
|
|
121
|
-
custom_data: agent.custom_data,
|
|
122
|
-
icon: agent.metadata?.ref?.name?.charAt(0) || 'A',
|
|
123
|
-
protocol: 'acp'
|
|
124
|
-
};
|
|
125
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
export const MAX_RESTARTS = 10;
|
|
12
|
-
export const RESTART_WINDOW_MS = 300000;
|
|
13
|
-
export const IDLE_TIMEOUT_MS = 120000;
|
|
14
|
-
|
|
15
|
-
export function resolveBinary(cmd) {
|
|
16
|
-
const ext = isWindows ? '.cmd' : '';
|
|
17
|
-
const localBin = path.join(projectRoot, 'node_modules', '.bin', cmd + ext);
|
|
18
|
-
if (fs.existsSync(localBin)) return localBin;
|
|
19
|
-
return cmd;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function startProcess(tool, log) {
|
|
23
|
-
const bin = resolveBinary(tool.cmd);
|
|
24
|
-
const args = [...tool.args, '--port', String(tool.port)];
|
|
25
|
-
const opts = { stdio: ['pipe', 'pipe', 'pipe'], cwd: process.cwd() };
|
|
26
|
-
if (isWindows) opts.shell = true;
|
|
27
|
-
|
|
28
|
-
let proc;
|
|
29
|
-
try { proc = spawn(bin, args, opts); }
|
|
30
|
-
catch (err) { log(tool.id + ' spawn failed: ' + err.message); return null; }
|
|
31
|
-
|
|
32
|
-
const entry = {
|
|
33
|
-
id: tool.id, port: tool.port, process: proc, pid: proc.pid,
|
|
34
|
-
startedAt: Date.now(), restarts: [], healthy: false, lastHealthCheck: 0,
|
|
35
|
-
lastUsed: Date.now(), idleTimer: null,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
proc.stdout.on('data', () => {});
|
|
39
|
-
proc.stderr.on('data', (d) => {
|
|
40
|
-
const t = d.toString().trim();
|
|
41
|
-
if (t) log(tool.id + ': ' + t.substring(0, 200));
|
|
42
|
-
});
|
|
43
|
-
proc.stdout.on('error', () => {});
|
|
44
|
-
proc.stderr.on('error', () => {});
|
|
45
|
-
proc.on('error', (err) => { log(tool.id + ' error: ' + err.message); entry.healthy = false; });
|
|
46
|
-
|
|
47
|
-
return entry;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function scheduleRestart(tool, prevRestarts, log, startProcessFn, shuttingDown) {
|
|
51
|
-
if (shuttingDown()) return;
|
|
52
|
-
const now = Date.now();
|
|
53
|
-
const recent = prevRestarts.filter(t => now - t < RESTART_WINDOW_MS);
|
|
54
|
-
if (recent.length >= MAX_RESTARTS) {
|
|
55
|
-
log(tool.id + ' exceeded restart limit, giving up');
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
const delay = Math.min(1000 * Math.pow(2, recent.length), 30000);
|
|
59
|
-
log(tool.id + ' restarting in ' + delay + 'ms');
|
|
60
|
-
setTimeout(() => {
|
|
61
|
-
if (shuttingDown()) return;
|
|
62
|
-
const entry = startProcessFn(tool);
|
|
63
|
-
if (entry) entry.restarts = [...recent, Date.now()];
|
|
64
|
-
}, delay);
|
|
65
|
-
}
|