agentgui 1.0.466 → 1.0.468
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/lib/tool-manager.js +129 -87
- package/lib/ws-handlers-util.js +19 -0
- package/package.json +1 -1
- package/server.js +12 -25
- package/static/js/tools-manager.js +7 -6
package/lib/tool-manager.js
CHANGED
|
@@ -1,114 +1,124 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import fetch from 'node-fetch';
|
|
7
4
|
|
|
8
5
|
const isWindows = os.platform() === 'win32';
|
|
9
6
|
const TOOLS = [
|
|
10
|
-
{ id: 'gm-oc', name: 'OpenCode', pkg: 'gm-oc'
|
|
11
|
-
{ id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc'
|
|
12
|
-
{ id: 'gm-kilo', name: 'Kilo', pkg: 'gm-kilo'
|
|
13
|
-
{ id: 'gm-cc', name: 'Claude Code', pkg: 'gm-cc'
|
|
7
|
+
{ id: 'gm-oc', name: 'OpenCode', pkg: 'gm-oc' },
|
|
8
|
+
{ id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc' },
|
|
9
|
+
{ id: 'gm-kilo', name: 'Kilo', pkg: 'gm-kilo' },
|
|
10
|
+
{ id: 'gm-cc', name: 'Claude Code', pkg: 'gm-cc' },
|
|
14
11
|
];
|
|
15
12
|
|
|
16
|
-
const
|
|
13
|
+
const statusCache = new Map();
|
|
17
14
|
const installLocks = new Map();
|
|
18
15
|
|
|
19
16
|
const getTool = (id) => TOOLS.find(t => t.id === id);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (fs.existsSync(path.join(process.cwd(), 'node_modules', '.bin', tool.binary + ext))) return true;
|
|
23
|
-
try { execSync(`${isWindows ? 'where' : 'which'} ${tool.binary}`, { stdio: 'pipe', timeout: 2000 }); return true; } catch (_) { return false; }
|
|
24
|
-
};
|
|
25
|
-
const detectVersion = (binary) => {
|
|
26
|
-
const versionPatterns = ['--version', '-v', '-V', '--help'];
|
|
27
|
-
for (const flag of versionPatterns) {
|
|
28
|
-
try {
|
|
29
|
-
const o = execSync(`${binary} ${flag} 2>&1`, { timeout: 2000, encoding: 'utf8', stdio: 'pipe' });
|
|
30
|
-
const match = o.match(/(\d+\.\d+\.\d+)/);
|
|
31
|
-
if (match?.[1]) return match[1];
|
|
32
|
-
} catch (_) {}
|
|
33
|
-
}
|
|
17
|
+
|
|
18
|
+
const checkToolViaBunx = async (pkg) => {
|
|
34
19
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
20
|
+
const cmd = isWindows ? 'bunx.cmd' : 'bunx';
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const proc = spawn(cmd, [pkg, '--version'], {
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
shell: isWindows
|
|
26
|
+
});
|
|
27
|
+
let stdout = '', stderr = '';
|
|
28
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
29
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
32
|
+
resolve({ installed: false, isUpToDate: false, upgradeNeeded: true, output: 'timeout' });
|
|
33
|
+
}, 10000);
|
|
34
|
+
proc.on('close', (code) => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
const output = stdout + stderr;
|
|
37
|
+
const installed = output.length > 0;
|
|
38
|
+
const upgradeNeeded = output.includes('Upgrading') || output.includes('upgrade');
|
|
39
|
+
const isUpToDate = installed && !upgradeNeeded;
|
|
40
|
+
resolve({ installed, isUpToDate, upgradeNeeded, output });
|
|
41
|
+
});
|
|
42
|
+
proc.on('error', () => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve({ installed: false, isUpToDate: false, upgradeNeeded: false, output: '' });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
} catch (_) {
|
|
48
|
+
return { installed: false, isUpToDate: false, upgradeNeeded: false, output: '' };
|
|
49
|
+
}
|
|
51
50
|
};
|
|
52
|
-
const cmpVer = (v1, v2) => { const [a,b] = [v1?.split('.')?.map(Number) || [], v2?.split('.')?.map(Number) || []]; for(let i=0;i<3;i++) { const n1=a[i]||0, n2=b[i]||0; if(n1<n2)return true; if(n1>n2)return false; } return false; };
|
|
53
51
|
|
|
54
52
|
export function checkToolStatus(toolId) {
|
|
55
53
|
const tool = getTool(toolId);
|
|
56
54
|
if (!tool) return null;
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
55
|
+
|
|
56
|
+
const cached = statusCache.get(toolId);
|
|
57
|
+
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
58
|
+
return {
|
|
59
|
+
toolId,
|
|
60
|
+
installed: cached.installed,
|
|
61
|
+
isUpToDate: cached.isUpToDate,
|
|
62
|
+
upgradeNeeded: cached.upgradeNeeded,
|
|
63
|
+
timestamp: cached.timestamp
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { toolId, installed: false, isUpToDate: false, upgradeNeeded: false, timestamp: Date.now() };
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
export async function
|
|
63
|
-
if (!currentVersion) return { hasUpdate: false, latestVersion: null };
|
|
70
|
+
export async function checkToolStatusAsync(toolId) {
|
|
64
71
|
const tool = getTool(toolId);
|
|
65
|
-
if (!tool) return
|
|
72
|
+
if (!tool) return null;
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
const cached = statusCache.get(toolId);
|
|
75
|
+
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
76
|
+
return {
|
|
77
|
+
toolId,
|
|
78
|
+
installed: cached.installed,
|
|
79
|
+
isUpToDate: cached.isUpToDate,
|
|
80
|
+
upgradeNeeded: cached.upgradeNeeded,
|
|
81
|
+
timestamp: cached.timestamp
|
|
82
|
+
};
|
|
83
|
+
}
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
const result = await checkToolViaBunx(tool.pkg);
|
|
86
|
+
const status = {
|
|
87
|
+
toolId,
|
|
88
|
+
installed: result.installed,
|
|
89
|
+
isUpToDate: result.isUpToDate,
|
|
90
|
+
upgradeNeeded: result.upgradeNeeded,
|
|
91
|
+
timestamp: Date.now()
|
|
92
|
+
};
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (latest) { versionCache.set(toolId, { version: latest, timestamp: Date.now() }); return { hasUpdate: cmpVer(currentVersion, latest), latestVersion: latest }; }
|
|
77
|
-
return { hasUpdate: false, latestVersion: null };
|
|
78
|
-
} catch (_) { return { hasUpdate: false, latestVersion: null }; }
|
|
94
|
+
statusCache.set(toolId, status);
|
|
95
|
+
return status;
|
|
79
96
|
}
|
|
80
97
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fs.mkdirSync(tool.marker, { recursive: true });
|
|
89
|
-
}
|
|
90
|
-
} catch (_) {}
|
|
91
|
-
};
|
|
98
|
+
export async function checkForUpdates(toolId) {
|
|
99
|
+
const tool = getTool(toolId);
|
|
100
|
+
if (!tool) return { needsUpdate: false };
|
|
101
|
+
|
|
102
|
+
const status = await checkToolStatusAsync(toolId);
|
|
103
|
+
return { needsUpdate: status.upgradeNeeded && status.installed };
|
|
104
|
+
}
|
|
92
105
|
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
106
|
+
const spawnBunxProc = (pkg, onProgress) => new Promise((resolve) => {
|
|
107
|
+
const cmd = isWindows ? 'bunx.cmd' : 'bunx';
|
|
108
|
+
const proc = spawn(cmd, [pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
|
|
109
|
+
let completed = false, stderr = '', stdout = '';
|
|
96
110
|
const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); }}, 300000);
|
|
97
|
-
proc.stdout.on('data', (d) => { if (onProgress) onProgress({ type: 'progress', data: d.toString() }); });
|
|
111
|
+
proc.stdout.on('data', (d) => { stdout += d.toString(); if (onProgress) onProgress({ type: 'progress', data: d.toString() }); });
|
|
98
112
|
proc.stderr.on('data', (d) => { stderr += d.toString(); if (onProgress) onProgress({ type: 'error', data: d.toString() }); });
|
|
99
113
|
proc.on('close', (code) => {
|
|
100
114
|
clearTimeout(timer);
|
|
101
115
|
if (completed) return;
|
|
102
116
|
completed = true;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (!s?.installed) {
|
|
107
|
-
setTimeout(() => { s = checkToolStatus(toolId); }, 500);
|
|
108
|
-
}
|
|
109
|
-
resolve(s?.installed ? { success: true, error: null, version: s.version } : { success: false, error: 'Tool not detected' });
|
|
117
|
+
const output = stdout + stderr;
|
|
118
|
+
if (code === 0 || output.includes('upgraded') || output.includes('registered') || output.includes('Hooks registered')) {
|
|
119
|
+
resolve({ success: true, error: null });
|
|
110
120
|
} else {
|
|
111
|
-
resolve({ success: false, error:
|
|
121
|
+
resolve({ success: false, error: output.substring(0, 1000) || 'Failed' });
|
|
112
122
|
}
|
|
113
123
|
});
|
|
114
124
|
proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); }});
|
|
@@ -119,22 +129,54 @@ export async function install(toolId, onProgress) {
|
|
|
119
129
|
if (!tool) return { success: false, error: 'Tool not found' };
|
|
120
130
|
if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
|
|
121
131
|
installLocks.set(toolId, true);
|
|
122
|
-
try {
|
|
132
|
+
try {
|
|
133
|
+
const result = await spawnBunxProc(tool.pkg, onProgress);
|
|
134
|
+
statusCache.delete(toolId);
|
|
135
|
+
return result;
|
|
136
|
+
} finally {
|
|
137
|
+
installLocks.delete(toolId);
|
|
138
|
+
}
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
export async function update(toolId,
|
|
141
|
+
export async function update(toolId, onProgress) {
|
|
126
142
|
const tool = getTool(toolId);
|
|
127
143
|
if (!tool) return { success: false, error: 'Tool not found' };
|
|
128
|
-
const current =
|
|
144
|
+
const current = await checkToolStatusAsync(toolId);
|
|
129
145
|
if (!current?.installed) return { success: false, error: 'Tool not installed' };
|
|
130
146
|
if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
|
|
131
147
|
|
|
132
|
-
const target = targetVersion || (await checkForUpdates(toolId, current.version)).latestVersion;
|
|
133
|
-
if (!target) return { success: false, error: 'Unable to determine target version' };
|
|
134
|
-
|
|
135
148
|
installLocks.set(toolId, true);
|
|
136
|
-
try {
|
|
149
|
+
try {
|
|
150
|
+
const result = await spawnBunxProc(tool.pkg, onProgress);
|
|
151
|
+
statusCache.delete(toolId);
|
|
152
|
+
return result;
|
|
153
|
+
} finally {
|
|
154
|
+
installLocks.delete(toolId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function getAllTools() {
|
|
159
|
+
return TOOLS.map(tool => {
|
|
160
|
+
const cached = statusCache.get(tool.id);
|
|
161
|
+
return {
|
|
162
|
+
...tool,
|
|
163
|
+
toolId: tool.id,
|
|
164
|
+
installed: cached?.installed ?? false,
|
|
165
|
+
isUpToDate: cached?.isUpToDate ?? false,
|
|
166
|
+
upgradeNeeded: cached?.upgradeNeeded ?? false,
|
|
167
|
+
timestamp: cached?.timestamp ?? 0
|
|
168
|
+
};
|
|
169
|
+
});
|
|
137
170
|
}
|
|
138
171
|
|
|
139
|
-
export function
|
|
140
|
-
|
|
172
|
+
export async function getAllToolsAsync() {
|
|
173
|
+
const results = await Promise.all(TOOLS.map(tool => checkToolStatusAsync(tool.id)));
|
|
174
|
+
return results.map((status, idx) => ({
|
|
175
|
+
...TOOLS[idx],
|
|
176
|
+
...status
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getToolConfig(toolId) {
|
|
181
|
+
return getTool(toolId) || null;
|
|
182
|
+
}
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -252,4 +252,23 @@ export function register(router, deps) {
|
|
|
252
252
|
return { ok: true, byteSize: result.byteSize, cached: true };
|
|
253
253
|
} catch (e) { err(500, e.message); }
|
|
254
254
|
});
|
|
255
|
+
|
|
256
|
+
router.handle('tools.list', async () => {
|
|
257
|
+
try {
|
|
258
|
+
const tools = await toolManager.getAllToolsAsync();
|
|
259
|
+
const result = tools.map((t) => ({
|
|
260
|
+
id: t.id,
|
|
261
|
+
name: t.name,
|
|
262
|
+
pkg: t.pkg,
|
|
263
|
+
installed: t.installed,
|
|
264
|
+
status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
|
|
265
|
+
isUpToDate: t.isUpToDate,
|
|
266
|
+
upgradeNeeded: t.upgradeNeeded,
|
|
267
|
+
hasUpdate: t.upgradeNeeded && t.installed
|
|
268
|
+
}));
|
|
269
|
+
return { tools: result };
|
|
270
|
+
} catch (e) {
|
|
271
|
+
err(500, e.message);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
255
274
|
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1783,31 +1783,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
1783
1783
|
|
|
1784
1784
|
if (pathOnly === '/api/tools' && req.method === 'GET') {
|
|
1785
1785
|
console.log('[TOOLS-API] Handling GET /api/tools');
|
|
1786
|
-
const tools = toolManager.
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
version: dbStatus?.version || t.version,
|
|
1797
|
-
status: status,
|
|
1798
|
-
error_message: dbStatus?.error_message,
|
|
1799
|
-
hasUpdate: false,
|
|
1800
|
-
latestVersion: null
|
|
1801
|
-
};
|
|
1802
|
-
});
|
|
1803
|
-
const toolsWithUpdates = await Promise.all(toolsWithStatus.map(async (t) => {
|
|
1804
|
-
if (t.installed && t.version) {
|
|
1805
|
-
const updates = await toolManager.checkForUpdates(t.id, t.version);
|
|
1806
|
-
return { ...t, hasUpdate: updates.hasUpdate, latestVersion: updates.latestVersion };
|
|
1807
|
-
}
|
|
1808
|
-
return t;
|
|
1786
|
+
const tools = await toolManager.getAllToolsAsync();
|
|
1787
|
+
const result = tools.map((t) => ({
|
|
1788
|
+
id: t.id,
|
|
1789
|
+
name: t.name,
|
|
1790
|
+
pkg: t.pkg,
|
|
1791
|
+
installed: t.installed,
|
|
1792
|
+
status: t.installed ? (t.isUpToDate ? 'installed' : 'needs_update') : 'not_installed',
|
|
1793
|
+
isUpToDate: t.isUpToDate,
|
|
1794
|
+
upgradeNeeded: t.upgradeNeeded,
|
|
1795
|
+
hasUpdate: t.upgradeNeeded && t.installed
|
|
1809
1796
|
}));
|
|
1810
|
-
sendJSON(req, res, 200, { tools:
|
|
1797
|
+
sendJSON(req, res, 200, { tools: result });
|
|
1811
1798
|
return;
|
|
1812
1799
|
}
|
|
1813
1800
|
|
|
@@ -4032,7 +4019,7 @@ registerUtilHandlers(wsRouter, {
|
|
|
4032
4019
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
4033
4020
|
startGeminiOAuth, exchangeGeminiOAuthCode,
|
|
4034
4021
|
geminiOAuthState: () => geminiOAuthState,
|
|
4035
|
-
STARTUP_CWD, activeScripts, voiceCacheManager
|
|
4022
|
+
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager
|
|
4036
4023
|
});
|
|
4037
4024
|
|
|
4038
4025
|
wsRouter.onLegacy((data, ws) => {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
function getStatusColor(tool) {
|
|
43
43
|
if (tool.status === 'installing' || tool.status === 'updating') return '#3b82f6';
|
|
44
|
-
if (tool.status === 'installed' && tool.hasUpdate) return '#f59e0b';
|
|
44
|
+
if (tool.status === 'needs_update' || (tool.status === 'installed' && tool.hasUpdate)) return '#f59e0b';
|
|
45
45
|
if (tool.status === 'installed') return '#10b981';
|
|
46
46
|
if (tool.status === 'failed') return '#ef4444';
|
|
47
47
|
return '#6b7280';
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
function getStatusText(tool) {
|
|
51
51
|
if (tool.status === 'installing') return 'Installing...';
|
|
52
52
|
if (tool.status === 'updating') return 'Updating...';
|
|
53
|
+
if (tool.status === 'needs_update') return 'Update available';
|
|
53
54
|
if (tool.status === 'installed') {
|
|
54
|
-
return tool.hasUpdate ?
|
|
55
|
+
return tool.hasUpdate ? 'Update available' : 'Up-to-date';
|
|
55
56
|
}
|
|
56
57
|
if (tool.status === 'failed') return 'Installation failed';
|
|
57
58
|
return 'Not installed';
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
|
|
60
61
|
function getStatusClass(tool) {
|
|
61
62
|
if (tool.status === 'installing' || tool.status === 'updating') return 'installing';
|
|
62
|
-
if (tool.status === 'installed' && tool.hasUpdate) return 'updating';
|
|
63
|
+
if (tool.status === 'needs_update' || (tool.status === 'installed' && tool.hasUpdate)) return 'updating';
|
|
63
64
|
if (tool.status === 'installed') return 'installed';
|
|
64
65
|
if (tool.status === 'failed') return 'failed';
|
|
65
66
|
return 'not-installed';
|
|
@@ -180,11 +181,11 @@
|
|
|
180
181
|
'<div class="tool-actions">' +
|
|
181
182
|
(tool.status === 'not_installed' ?
|
|
182
183
|
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Install</button>' :
|
|
183
|
-
tool.hasUpdate ?
|
|
184
|
-
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.update(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Update
|
|
184
|
+
(tool.hasUpdate || tool.status === 'needs_update') ?
|
|
185
|
+
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.update(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Update</button>' :
|
|
185
186
|
tool.status === 'failed' ?
|
|
186
187
|
'<button class="tool-btn tool-btn-primary" onclick="window.toolsManager.install(\'' + tool.id + '\')" ' + (operationInProgress.has(tool.id) ? 'disabled' : '') + '>Retry</button>' :
|
|
187
|
-
'<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>
|
|
188
|
+
'<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>Refresh</button>'
|
|
188
189
|
) +
|
|
189
190
|
'</div>' +
|
|
190
191
|
'</div>';
|