agentgui 1.0.583 → 1.0.585
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/screenshot-chat.png +0 -0
- package/docs/screenshot-conversation.png +0 -0
- package/docs/screenshot-files.png +0 -0
- package/docs/screenshot-main.png +0 -0
- package/docs/screenshot-terminal.png +0 -0
- package/docs/screenshot-tools-popup.png +0 -0
- package/package.json +2 -1
- package/scripts/take-screenshots.sh +60 -0
- package/server.js +1 -1
- package/static/js/client.js +7 -0
- package/static/js/conversations.js +5 -0
- package/lib/acp-http-client.js +0 -125
- package/lib/acp-manager.js +0 -164
- 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/screenshot-chat.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/screenshot-main.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentgui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.585",
|
|
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",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
DOCS_DIR="$SCRIPT_DIR/../docs"
|
|
6
|
+
SESSION="agentgui-screenshots"
|
|
7
|
+
LOG_FILE="/config/logs/services/agentgui.log"
|
|
8
|
+
|
|
9
|
+
PORT=$(grep -oP 'localhost:\K[0-9]+(?=/gm/)' "$LOG_FILE" 2>/dev/null | tail -1)
|
|
10
|
+
PORT="${PORT:-9897}"
|
|
11
|
+
BASE_URL="http://localhost:$PORT/gm/"
|
|
12
|
+
|
|
13
|
+
ab() {
|
|
14
|
+
agent-browser --session "$SESSION" "$@"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
cleanup() {
|
|
18
|
+
agent-browser --session "$SESSION" close 2>/dev/null || true
|
|
19
|
+
}
|
|
20
|
+
trap cleanup EXIT
|
|
21
|
+
|
|
22
|
+
echo "Taking screenshots from $BASE_URL"
|
|
23
|
+
echo "Saving to $DOCS_DIR"
|
|
24
|
+
|
|
25
|
+
ab open "$BASE_URL"
|
|
26
|
+
ab wait --load networkidle
|
|
27
|
+
ab wait ".conversation-item"
|
|
28
|
+
|
|
29
|
+
ab screenshot --full "$DOCS_DIR/screenshot-main.png"
|
|
30
|
+
echo "Saved screenshot-main.png"
|
|
31
|
+
|
|
32
|
+
ab eval 'document.querySelector(".conversation-item").click()'
|
|
33
|
+
ab wait --load networkidle
|
|
34
|
+
ab wait ".message, .event-block, .streaming-event, #chatView, .chat-messages"
|
|
35
|
+
|
|
36
|
+
ab screenshot --full "$DOCS_DIR/screenshot-chat.png"
|
|
37
|
+
echo "Saved screenshot-chat.png"
|
|
38
|
+
|
|
39
|
+
ab screenshot --full "$DOCS_DIR/screenshot-conversation.png"
|
|
40
|
+
echo "Saved screenshot-conversation.png"
|
|
41
|
+
|
|
42
|
+
ab eval 'document.getElementById("toolsManagerBtn")?.click()'
|
|
43
|
+
ab wait "#toolsPopup.open, .tools-popup.open"
|
|
44
|
+
|
|
45
|
+
ab screenshot --full "$DOCS_DIR/screenshot-tools-popup.png"
|
|
46
|
+
echo "Saved screenshot-tools-popup.png"
|
|
47
|
+
|
|
48
|
+
ab eval 'document.querySelector("[data-view=files]")?.click()'
|
|
49
|
+
ab wait "[data-view=files].active, .files-view, #filesView"
|
|
50
|
+
|
|
51
|
+
ab screenshot --full "$DOCS_DIR/screenshot-files.png"
|
|
52
|
+
echo "Saved screenshot-files.png"
|
|
53
|
+
|
|
54
|
+
ab eval 'document.querySelector("[data-view=terminal]")?.click()'
|
|
55
|
+
ab wait "#terminalContainer:not([style*=\"display:none\"]), .terminal-container:not([style*=\"display:none\"])"
|
|
56
|
+
|
|
57
|
+
ab screenshot --full "$DOCS_DIR/screenshot-terminal.png"
|
|
58
|
+
echo "Saved screenshot-terminal.png"
|
|
59
|
+
|
|
60
|
+
echo "All screenshots saved to $DOCS_DIR"
|
package/server.js
CHANGED
|
@@ -4144,7 +4144,7 @@ wss.on('connection', (ws, req) => {
|
|
|
4144
4144
|
|
|
4145
4145
|
const BROADCAST_TYPES = new Set([
|
|
4146
4146
|
'message_created', 'conversation_created', 'conversation_updated',
|
|
4147
|
-
'conversations_updated', 'conversation_deleted', 'queue_status', 'queue_updated',
|
|
4147
|
+
'conversations_updated', 'conversation_deleted', 'all_conversations_deleted', 'queue_status', 'queue_updated',
|
|
4148
4148
|
'rate_limit_hit', 'rate_limit_clear',
|
|
4149
4149
|
'script_started', 'script_stopped', 'script_output',
|
|
4150
4150
|
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
|
package/static/js/client.js
CHANGED
|
@@ -1319,10 +1319,17 @@ class AgentGUIClient {
|
|
|
1319
1319
|
|
|
1320
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) {
|
|
1329
|
+
this.state.currentConversation = null;
|
|
1324
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
|
-
}
|
package/lib/acp-manager.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { startProcess as startProc, scheduleRestart as scheduleRestart, MAX_RESTARTS, RESTART_WINDOW_MS, IDLE_TIMEOUT_MS } from './acp-process-lifecycle.js';
|
|
2
|
-
|
|
3
|
-
const ACP_TOOLS = [
|
|
4
|
-
{ id: 'opencode', cmd: 'opencode', args: ['acp'], port: 18100, npxPkg: 'opencode-ai' },
|
|
5
|
-
{ id: 'kilo', cmd: 'kilo', args: ['acp'], port: 18101, npxPkg: '@kilocode/cli' },
|
|
6
|
-
];
|
|
7
|
-
|
|
8
|
-
const HEALTH_INTERVAL_MS = 30000;
|
|
9
|
-
const STARTUP_GRACE_MS = 5000;
|
|
10
|
-
const processes = new Map();
|
|
11
|
-
let healthTimer = null;
|
|
12
|
-
let shuttingDown = false;
|
|
13
|
-
|
|
14
|
-
function log(msg) { console.log('[ACP] ' + msg); }
|
|
15
|
-
|
|
16
|
-
function startProcess(tool) {
|
|
17
|
-
if (shuttingDown) return null;
|
|
18
|
-
const existing = processes.get(tool.id);
|
|
19
|
-
if (existing?.process && !existing.process.killed) return existing;
|
|
20
|
-
|
|
21
|
-
const entry = startProc(tool, log);
|
|
22
|
-
if (!entry) return null;
|
|
23
|
-
|
|
24
|
-
entry.process.on('close', (code) => {
|
|
25
|
-
entry.healthy = false;
|
|
26
|
-
if (shuttingDown || entry._stopping) return;
|
|
27
|
-
log(tool.id + ' exited code ' + code);
|
|
28
|
-
scheduleRestart(tool, entry.restarts, log, startProcess, () => shuttingDown);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
processes.set(tool.id, entry);
|
|
32
|
-
log(tool.id + ' started port ' + tool.port + ' pid ' + entry.process.pid);
|
|
33
|
-
setTimeout(() => checkHealth(tool.id), STARTUP_GRACE_MS);
|
|
34
|
-
resetIdleTimer(tool.id);
|
|
35
|
-
return entry;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function resetIdleTimer(toolId) {
|
|
39
|
-
const entry = processes.get(toolId);
|
|
40
|
-
if (!entry) return;
|
|
41
|
-
entry.lastUsed = Date.now();
|
|
42
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
43
|
-
entry.idleTimer = setTimeout(() => stopTool(toolId), IDLE_TIMEOUT_MS);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function stopTool(toolId) {
|
|
47
|
-
const entry = processes.get(toolId);
|
|
48
|
-
if (!entry) return;
|
|
49
|
-
log(toolId + ' idle, stopping to free RAM');
|
|
50
|
-
entry._stopping = true;
|
|
51
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
52
|
-
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
53
|
-
setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} }, 5000);
|
|
54
|
-
processes.delete(toolId);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function checkHealth(toolId) {
|
|
58
|
-
const entry = processes.get(toolId);
|
|
59
|
-
if (!entry || shuttingDown) return;
|
|
60
|
-
|
|
61
|
-
const { fetchACPProvider } = await import('./acp-http-client.js');
|
|
62
|
-
const result = await fetchACPProvider('http://127.0.0.1', entry.port);
|
|
63
|
-
|
|
64
|
-
entry.healthy = result.ok;
|
|
65
|
-
entry.lastHealthCheck = Date.now();
|
|
66
|
-
|
|
67
|
-
if (result.data) {
|
|
68
|
-
entry.providerInfo = result.data;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function ensureRunning(agentId) {
|
|
73
|
-
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
74
|
-
if (!tool) return null;
|
|
75
|
-
let entry = processes.get(agentId);
|
|
76
|
-
if (entry?.healthy) { resetIdleTimer(agentId); return entry.port; }
|
|
77
|
-
if (!entry || entry._stopping) {
|
|
78
|
-
entry = startProcess(tool);
|
|
79
|
-
if (!entry) return null;
|
|
80
|
-
}
|
|
81
|
-
for (let i = 0; i < 20; i++) {
|
|
82
|
-
await new Promise(r => setTimeout(r, 500));
|
|
83
|
-
await checkHealth(agentId);
|
|
84
|
-
if (processes.get(agentId)?.healthy) { resetIdleTimer(agentId); return tool.port; }
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function touch(agentId) {
|
|
90
|
-
const entry = processes.get(agentId);
|
|
91
|
-
if (entry) resetIdleTimer(agentId);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function startAll() {
|
|
95
|
-
log('ACP tools available (on-demand start)');
|
|
96
|
-
healthTimer = setInterval(() => {
|
|
97
|
-
for (const [id] of processes) checkHealth(id);
|
|
98
|
-
}, HEALTH_INTERVAL_MS);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export async function stopAll() {
|
|
102
|
-
shuttingDown = true;
|
|
103
|
-
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
|
104
|
-
const kills = [];
|
|
105
|
-
for (const [id, entry] of processes) {
|
|
106
|
-
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
107
|
-
log('stopping ' + id + ' pid ' + entry.pid);
|
|
108
|
-
kills.push(new Promise(resolve => {
|
|
109
|
-
const t = setTimeout(() => { try { entry.process.kill('SIGKILL'); } catch (_) {} resolve(); }, 5000);
|
|
110
|
-
entry.process.on('close', () => { clearTimeout(t); resolve(); });
|
|
111
|
-
try { entry.process.kill('SIGTERM'); } catch (_) {}
|
|
112
|
-
}));
|
|
113
|
-
}
|
|
114
|
-
await Promise.all(kills);
|
|
115
|
-
processes.clear();
|
|
116
|
-
log('all stopped');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getStatus() {
|
|
120
|
-
return ACP_TOOLS.map(tool => {
|
|
121
|
-
const e = processes.get(tool.id);
|
|
122
|
-
return {
|
|
123
|
-
id: tool.id, port: tool.port, running: !!e, healthy: e?.healthy || false,
|
|
124
|
-
pid: e?.pid, uptime: e ? Date.now() - e.startedAt : 0,
|
|
125
|
-
restartCount: e?.restarts.length || 0, idleMs: e ? Date.now() - e.lastUsed : 0,
|
|
126
|
-
providerInfo: e?.providerInfo || null,
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export function getPort(agentId) {
|
|
132
|
-
const e = processes.get(agentId);
|
|
133
|
-
return e?.healthy ? e.port : null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function getRunningPorts() {
|
|
137
|
-
const ports = {};
|
|
138
|
-
for (const [id, e] of processes) if (e.healthy) ports[id] = e.port;
|
|
139
|
-
return ports;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export async function restart(agentId) {
|
|
143
|
-
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
144
|
-
if (!tool) return false;
|
|
145
|
-
stopTool(agentId);
|
|
146
|
-
startProcess(tool);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function queryModels(agentId) {
|
|
151
|
-
const port = await ensureRunning(agentId);
|
|
152
|
-
if (!port) return [];
|
|
153
|
-
try {
|
|
154
|
-
const res = await fetch('http://127.0.0.1:' + port + '/models');
|
|
155
|
-
if (!res.ok) return [];
|
|
156
|
-
const data = await res.json();
|
|
157
|
-
return data.models || [];
|
|
158
|
-
} catch (_) { return []; }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function isAvailable(agentId) {
|
|
162
|
-
const tool = ACP_TOOLS.find(t => t.id === agentId);
|
|
163
|
-
return !!tool;
|
|
164
|
-
}
|
|
@@ -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
|
-
}
|