agentgui 1.0.453 → 1.0.454
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/.prd +428 -0
- package/database.js +119 -0
- package/lib/tool-manager.js +285 -0
- package/package.json +1 -1
- package/server.js +151 -0
- package/static/index.html +17 -0
- package/static/js/features.js +2 -0
- package/static/js/tool-status.js +323 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import fetch from 'node-fetch';
|
|
7
|
+
|
|
8
|
+
const isWindows = os.platform() === 'win32';
|
|
9
|
+
const INSTALL_TIMEOUT_MS = 300000;
|
|
10
|
+
const VERSION_TIMEOUT_MS = 3000;
|
|
11
|
+
const REGISTRY_TIMEOUT_MS = 5000;
|
|
12
|
+
const VERSION_CACHE_MS = 3600000;
|
|
13
|
+
|
|
14
|
+
const TOOLS = [
|
|
15
|
+
{ id: 'gm-oc', name: 'OpenCode', pkg: 'gm-oc', binary: 'opencode', marker: path.join(os.homedir(), '.config', 'opencode', 'agents') },
|
|
16
|
+
{ id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc', binary: 'gemini', marker: path.join(os.homedir(), '.gemini', 'extensions', 'gm', 'agents') },
|
|
17
|
+
{ id: 'gm-kilo', name: 'Kilo', pkg: '@kilocode/cli', binary: 'kilo', marker: path.join(os.homedir(), '.config', 'kilo', 'agents') },
|
|
18
|
+
{ id: 'gm-cc', name: 'Claude Code', pkg: '@anthropic-sdk/claude-code', binary: 'claude', marker: path.join(os.homedir(), '.config', 'claude', 'agents') },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const versionCache = new Map();
|
|
22
|
+
const installLocks = new Map();
|
|
23
|
+
|
|
24
|
+
function log(msg) { console.log('[TOOL-MANAGER] ' + msg); }
|
|
25
|
+
|
|
26
|
+
function getTool(toolId) {
|
|
27
|
+
return TOOLS.find(t => t.id === toolId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function checkToolStatus(toolId) {
|
|
31
|
+
const tool = getTool(toolId);
|
|
32
|
+
if (!tool) return null;
|
|
33
|
+
|
|
34
|
+
const timestamp = Date.now();
|
|
35
|
+
let installed = false;
|
|
36
|
+
let hasConfig = false;
|
|
37
|
+
let version = null;
|
|
38
|
+
|
|
39
|
+
const ext = isWindows ? '.cmd' : '';
|
|
40
|
+
const localBin = path.join(process.cwd(), 'node_modules', '.bin', tool.binary + ext);
|
|
41
|
+
if (fs.existsSync(localBin)) {
|
|
42
|
+
installed = true;
|
|
43
|
+
} else {
|
|
44
|
+
try {
|
|
45
|
+
const which = isWindows ? 'where' : 'which';
|
|
46
|
+
execSync(`${which} ${tool.binary}`, { stdio: 'pipe', timeout: 2000 });
|
|
47
|
+
installed = true;
|
|
48
|
+
} catch (_) {
|
|
49
|
+
installed = false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (fs.existsSync(tool.marker)) {
|
|
54
|
+
hasConfig = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (installed && !hasConfig) {
|
|
58
|
+
installed = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (installed) {
|
|
62
|
+
version = detectVersionSync(tool);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { toolId, installed, version, hasConfig, timestamp };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detectVersionSync(tool) {
|
|
69
|
+
try {
|
|
70
|
+
const output = execSync(`${tool.binary} --version 2>&1 || ${tool.binary} -v 2>&1`, {
|
|
71
|
+
timeout: VERSION_TIMEOUT_MS,
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: 'pipe'
|
|
74
|
+
});
|
|
75
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
76
|
+
return match ? match[1] : null;
|
|
77
|
+
} catch (_) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function checkForUpdates(toolId, currentVersion) {
|
|
83
|
+
const tool = getTool(toolId);
|
|
84
|
+
if (!tool || !currentVersion) return { hasUpdate: false, latestVersion: null };
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const cached = versionCache.get(toolId);
|
|
88
|
+
if (cached && Date.now() - cached.timestamp < VERSION_CACHE_MS) {
|
|
89
|
+
return compareVersions(currentVersion, cached.version) ? { hasUpdate: true, latestVersion: cached.version } : { hasUpdate: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await fetch(`https://registry.npmjs.org/${tool.pkg}`, {
|
|
93
|
+
timeout: REGISTRY_TIMEOUT_MS,
|
|
94
|
+
headers: { 'Accept': 'application/json' }
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!response.ok) return { hasUpdate: false, latestVersion: null };
|
|
98
|
+
|
|
99
|
+
const data = await response.json();
|
|
100
|
+
const latestVersion = data['dist-tags']?.latest;
|
|
101
|
+
|
|
102
|
+
if (latestVersion) {
|
|
103
|
+
versionCache.set(toolId, { version: latestVersion, timestamp: Date.now() });
|
|
104
|
+
return { hasUpdate: compareVersions(currentVersion, latestVersion), latestVersion };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { hasUpdate: false, latestVersion: null };
|
|
108
|
+
} catch (_) {
|
|
109
|
+
return { hasUpdate: false, latestVersion: null };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compareVersions(v1, v2) {
|
|
114
|
+
const p1 = v1.split('.').map(Number);
|
|
115
|
+
const p2 = v2.split('.').map(Number);
|
|
116
|
+
for (let i = 0; i < 3; i++) {
|
|
117
|
+
const n1 = p1[i] || 0;
|
|
118
|
+
const n2 = p2[i] || 0;
|
|
119
|
+
if (n1 > n2) return false;
|
|
120
|
+
if (n1 < n2) return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function install(toolId, onProgress) {
|
|
126
|
+
const tool = getTool(toolId);
|
|
127
|
+
if (!tool) return { success: false, error: 'Tool not found' };
|
|
128
|
+
|
|
129
|
+
if (installLocks.get(toolId)) {
|
|
130
|
+
return { success: false, error: 'Install already in progress' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
installLocks.set(toolId, true);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
138
|
+
const proc = spawn(npxCmd, ['--yes', tool.pkg], {
|
|
139
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
140
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
141
|
+
shell: isWindows
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let stdout = '';
|
|
145
|
+
let stderr = '';
|
|
146
|
+
let completed = false;
|
|
147
|
+
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
if (!completed) {
|
|
150
|
+
completed = true;
|
|
151
|
+
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
152
|
+
resolve({ success: false, error: 'Installation timeout (5 minutes)' });
|
|
153
|
+
}
|
|
154
|
+
}, INSTALL_TIMEOUT_MS);
|
|
155
|
+
|
|
156
|
+
proc.stdout.on('data', (d) => {
|
|
157
|
+
stdout += d.toString();
|
|
158
|
+
if (onProgress) onProgress({ type: 'progress', data: d.toString() });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
proc.stderr.on('data', (d) => {
|
|
162
|
+
stderr += d.toString();
|
|
163
|
+
if (onProgress) onProgress({ type: 'error', data: d.toString() });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
proc.on('close', (code) => {
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
if (completed) return;
|
|
169
|
+
completed = true;
|
|
170
|
+
|
|
171
|
+
if (code === 0) {
|
|
172
|
+
const status = checkToolStatus(toolId);
|
|
173
|
+
if (status && status.installed) {
|
|
174
|
+
resolve({ success: true, error: null, version: status.version });
|
|
175
|
+
} else {
|
|
176
|
+
resolve({ success: false, error: 'Install completed but tool not detected' });
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
const error = stderr.substring(0, 1000) || 'Installation failed';
|
|
180
|
+
resolve({ success: false, error });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
proc.on('error', (err) => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
if (completed) return;
|
|
187
|
+
completed = true;
|
|
188
|
+
resolve({ success: false, error: err.message });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
installLocks.delete(toolId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function update(toolId, targetVersion, onProgress) {
|
|
197
|
+
const tool = getTool(toolId);
|
|
198
|
+
if (!tool) return { success: false, error: 'Tool not found' };
|
|
199
|
+
|
|
200
|
+
const current = checkToolStatus(toolId);
|
|
201
|
+
if (!current || !current.installed) {
|
|
202
|
+
return { success: false, error: 'Tool not installed' };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (installLocks.get(toolId)) {
|
|
206
|
+
return { success: false, error: 'Install already in progress' };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const target = targetVersion || await checkForUpdates(toolId, current.version).then(r => r.latestVersion);
|
|
210
|
+
if (!target) {
|
|
211
|
+
return { success: false, error: 'Unable to determine target version' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
installLocks.set(toolId, true);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
return new Promise((resolve) => {
|
|
218
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
219
|
+
const pkg = `${tool.pkg}@${target}`;
|
|
220
|
+
const proc = spawn(npxCmd, ['--yes', pkg], {
|
|
221
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
222
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
223
|
+
shell: isWindows
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
let stderr = '';
|
|
227
|
+
let completed = false;
|
|
228
|
+
|
|
229
|
+
const timer = setTimeout(() => {
|
|
230
|
+
if (!completed) {
|
|
231
|
+
completed = true;
|
|
232
|
+
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
233
|
+
resolve({ success: false, error: 'Update timeout (5 minutes)' });
|
|
234
|
+
}
|
|
235
|
+
}, INSTALL_TIMEOUT_MS);
|
|
236
|
+
|
|
237
|
+
proc.stdout.on('data', (d) => {
|
|
238
|
+
if (onProgress) onProgress({ type: 'progress', data: d.toString() });
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
proc.stderr.on('data', (d) => {
|
|
242
|
+
stderr += d.toString();
|
|
243
|
+
if (onProgress) onProgress({ type: 'error', data: d.toString() });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
proc.on('close', (code) => {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
if (completed) return;
|
|
249
|
+
completed = true;
|
|
250
|
+
|
|
251
|
+
if (code === 0) {
|
|
252
|
+
const status = checkToolStatus(toolId);
|
|
253
|
+
if (status && status.installed) {
|
|
254
|
+
resolve({ success: true, error: null, version: status.version });
|
|
255
|
+
} else {
|
|
256
|
+
resolve({ success: false, error: 'Update completed but tool not detected' });
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const error = stderr.substring(0, 1000) || 'Update failed';
|
|
260
|
+
resolve({ success: false, error });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
proc.on('error', (err) => {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
if (completed) return;
|
|
267
|
+
completed = true;
|
|
268
|
+
resolve({ success: false, error: err.message });
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
} finally {
|
|
272
|
+
installLocks.delete(toolId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function getAllTools() {
|
|
277
|
+
return TOOLS.map(tool => {
|
|
278
|
+
const status = checkToolStatus(tool.id);
|
|
279
|
+
return { ...tool, ...status };
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function getToolConfig(toolId) {
|
|
284
|
+
return getTool(toolId) || null;
|
|
285
|
+
}
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { register as registerRunHandlers } from './lib/ws-handlers-run.js';
|
|
|
24
24
|
import { register as registerUtilHandlers } from './lib/ws-handlers-util.js';
|
|
25
25
|
import { startAll as startACPTools, stopAll as stopACPTools, getStatus as getACPStatus, getPort as getACPPort, queryModels as queryACPModels, touch as touchACP } from './lib/acp-manager.js';
|
|
26
26
|
import { installGMAgentConfigs } from './lib/gm-agent-configs.js';
|
|
27
|
+
import * as toolManager from './lib/tool-manager.js';
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
process.on('uncaughtException', (err, origin) => {
|
|
@@ -1780,6 +1781,141 @@ const server = http.createServer(async (req, res) => {
|
|
|
1780
1781
|
return;
|
|
1781
1782
|
}
|
|
1782
1783
|
|
|
1784
|
+
if (pathOnly === '/api/tools' && req.method === 'GET') {
|
|
1785
|
+
console.log('[TOOLS-API] Handling GET /api/tools');
|
|
1786
|
+
const tools = toolManager.getAllTools();
|
|
1787
|
+
const toolsWithUpdates = await Promise.all(tools.map(async (t) => {
|
|
1788
|
+
if (t.installed) {
|
|
1789
|
+
const updates = await toolManager.checkForUpdates(t.id, t.version);
|
|
1790
|
+
return { ...t, hasUpdate: updates.hasUpdate, latestVersion: updates.latestVersion };
|
|
1791
|
+
}
|
|
1792
|
+
return { ...t, hasUpdate: false, latestVersion: null };
|
|
1793
|
+
}));
|
|
1794
|
+
sendJSON(req, res, 200, { tools: toolsWithUpdates });
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)) {
|
|
1799
|
+
const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)[1];
|
|
1800
|
+
const status = toolManager.checkToolStatus(toolId);
|
|
1801
|
+
if (!status) {
|
|
1802
|
+
sendJSON(req, res, 404, { error: 'Tool not found' });
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (status.installed) {
|
|
1806
|
+
const updates = await toolManager.checkForUpdates(toolId, status.version);
|
|
1807
|
+
status.hasUpdate = updates.hasUpdate;
|
|
1808
|
+
status.latestVersion = updates.latestVersion;
|
|
1809
|
+
}
|
|
1810
|
+
sendJSON(req, res, 200, status);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
if (pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/) && req.method === 'POST') {
|
|
1815
|
+
const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/install$/)[1];
|
|
1816
|
+
const tool = toolManager.getToolConfig(toolId);
|
|
1817
|
+
if (!tool) {
|
|
1818
|
+
sendJSON(req, res, 404, { error: 'Tool not found' });
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
queries.updateToolStatus(toolId, { status: 'installing' });
|
|
1822
|
+
sendJSON(req, res, 200, { success: true, installing: true, estimatedTime: 60000 });
|
|
1823
|
+
toolManager.install(toolId, (msg) => {
|
|
1824
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1825
|
+
wsOptimizer.broadcast({ type: 'tool_install_progress', toolId, data: msg });
|
|
1826
|
+
}
|
|
1827
|
+
}).then((result) => {
|
|
1828
|
+
if (result.success) {
|
|
1829
|
+
queries.updateToolStatus(toolId, { status: 'installed', version: result.version, installed_at: Date.now() });
|
|
1830
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1831
|
+
wsOptimizer.broadcast({ type: 'tool_install_complete', toolId, data: result });
|
|
1832
|
+
}
|
|
1833
|
+
queries.addToolInstallHistory(toolId, 'install', 'success', null);
|
|
1834
|
+
} else {
|
|
1835
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1836
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1837
|
+
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: result });
|
|
1838
|
+
}
|
|
1839
|
+
queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
if (pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/) && req.method === 'POST') {
|
|
1846
|
+
const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/update$/)[1];
|
|
1847
|
+
const body = await parseBody(req);
|
|
1848
|
+
const tool = toolManager.getToolConfig(toolId);
|
|
1849
|
+
if (!tool) {
|
|
1850
|
+
sendJSON(req, res, 404, { error: 'Tool not found' });
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const current = toolManager.checkToolStatus(toolId);
|
|
1854
|
+
if (!current || !current.installed) {
|
|
1855
|
+
sendJSON(req, res, 400, { error: 'Tool not installed' });
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
queries.updateToolStatus(toolId, { status: 'updating' });
|
|
1859
|
+
sendJSON(req, res, 200, { success: true, updating: true });
|
|
1860
|
+
toolManager.update(toolId, body.targetVersion, (msg) => {
|
|
1861
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1862
|
+
wsOptimizer.broadcast({ type: 'tool_update_progress', toolId, data: msg });
|
|
1863
|
+
}
|
|
1864
|
+
}).then((result) => {
|
|
1865
|
+
if (result.success) {
|
|
1866
|
+
queries.updateToolStatus(toolId, { status: 'installed', version: result.version, installed_at: Date.now() });
|
|
1867
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1868
|
+
wsOptimizer.broadcast({ type: 'tool_update_complete', toolId, data: result });
|
|
1869
|
+
}
|
|
1870
|
+
queries.addToolInstallHistory(toolId, 'update', 'success', null);
|
|
1871
|
+
} else {
|
|
1872
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1873
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1874
|
+
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: result });
|
|
1875
|
+
}
|
|
1876
|
+
queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/) && req.method === 'GET') {
|
|
1883
|
+
const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/history$/)[1];
|
|
1884
|
+
const url = new URL(req.url, 'http://localhost');
|
|
1885
|
+
const limit = Math.min(parseInt(url.searchParams.get('limit')) || 20, 100);
|
|
1886
|
+
const offset = parseInt(url.searchParams.get('offset')) || 0;
|
|
1887
|
+
const history = queries.getToolInstallHistory(toolId, limit, offset);
|
|
1888
|
+
sendJSON(req, res, 200, { history });
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
if (pathOnly === '/api/tools/refresh-all' && req.method === 'POST') {
|
|
1893
|
+
sendJSON(req, res, 200, { refreshing: true, toolCount: 4 });
|
|
1894
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1895
|
+
wsOptimizer.broadcast({ type: 'tools_refresh_started' });
|
|
1896
|
+
}
|
|
1897
|
+
setImmediate(async () => {
|
|
1898
|
+
const tools = toolManager.getAllTools();
|
|
1899
|
+
for (const tool of tools) {
|
|
1900
|
+
queries.updateToolStatus(tool.id, {
|
|
1901
|
+
status: tool.installed ? 'installed' : 'not_installed',
|
|
1902
|
+
version: tool.version,
|
|
1903
|
+
last_check_at: Date.now()
|
|
1904
|
+
});
|
|
1905
|
+
if (tool.installed) {
|
|
1906
|
+
const updates = await toolManager.checkForUpdates(tool.id, tool.version);
|
|
1907
|
+
if (updates.hasUpdate) {
|
|
1908
|
+
queries.updateToolStatus(tool.id, { update_available: 1, latest_version: updates.latestVersion });
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1913
|
+
wsOptimizer.broadcast({ type: 'tools_refresh_complete', data: tools });
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1783
1919
|
if (pathOnly === '/api/ws-stats' && req.method === 'GET') {
|
|
1784
1920
|
const stats = wsOptimizer.getStats();
|
|
1785
1921
|
sendJSON(req, res, 200, stats);
|
|
@@ -4213,6 +4349,7 @@ function onServerReady() {
|
|
|
4213
4349
|
installGMAgentConfigs().catch(err => console.error('[GM-CONFIG] Startup error:', err.message));
|
|
4214
4350
|
|
|
4215
4351
|
startACPTools().then(() => {
|
|
4352
|
+
console.log('[ACP] On-demand startup enabled (ACP tools start when first used)');
|
|
4216
4353
|
setTimeout(() => {
|
|
4217
4354
|
const acpStatus = getACPStatus();
|
|
4218
4355
|
for (const s of acpStatus) {
|
|
@@ -4227,6 +4364,20 @@ function onServerReady() {
|
|
|
4227
4364
|
}, 6000);
|
|
4228
4365
|
}).catch(err => console.error('[ACP] Startup error:', err.message));
|
|
4229
4366
|
|
|
4367
|
+
const toolIds = ['gm-oc', 'gm-gc', 'gm-kilo', 'gm-cc'];
|
|
4368
|
+
queries.initializeToolInstallations(toolIds.map(id => ({ id })));
|
|
4369
|
+
for (const toolId of toolIds) {
|
|
4370
|
+
const status = toolManager.checkToolStatus(toolId);
|
|
4371
|
+
if (status) {
|
|
4372
|
+
queries.updateToolStatus(toolId, {
|
|
4373
|
+
status: status.installed ? 'installed' : 'not_installed',
|
|
4374
|
+
version: status.version,
|
|
4375
|
+
last_check_at: Date.now()
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
console.log('[TOOLS] Initialization complete');
|
|
4380
|
+
|
|
4230
4381
|
ensureModelsDownloaded().then(async ok => {
|
|
4231
4382
|
if (ok) console.log('[MODELS] Speech models ready');
|
|
4232
4383
|
else console.log('[MODELS] Speech model download failed');
|
package/static/index.html
CHANGED
|
@@ -1238,6 +1238,16 @@
|
|
|
1238
1238
|
100% { border-color: var(--color-border); }
|
|
1239
1239
|
}
|
|
1240
1240
|
|
|
1241
|
+
/* ===== TOOLS VIEW ===== */
|
|
1242
|
+
.tools-container {
|
|
1243
|
+
flex: 1;
|
|
1244
|
+
min-height: 0;
|
|
1245
|
+
overflow-y: auto;
|
|
1246
|
+
overflow-x: hidden;
|
|
1247
|
+
padding: 1.5rem 2rem;
|
|
1248
|
+
-webkit-overflow-scrolling: touch;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1241
1251
|
.voice-mic-btn.recording {
|
|
1242
1252
|
background: var(--color-error);
|
|
1243
1253
|
border-color: var(--color-error);
|
|
@@ -3111,6 +3121,7 @@
|
|
|
3111
3121
|
<button class="view-toggle-btn" data-view="files">Files</button>
|
|
3112
3122
|
<button class="view-toggle-btn" data-view="voice" id="voiceTabBtn" style="display:none;">Voice</button>
|
|
3113
3123
|
<button class="view-toggle-btn" data-view="terminal" id="terminalTabBtn">Terminal</button>
|
|
3124
|
+
<button class="view-toggle-btn" data-view="tools">Tools</button>
|
|
3114
3125
|
</div>
|
|
3115
3126
|
|
|
3116
3127
|
<!-- Messages scroll area -->
|
|
@@ -3174,6 +3185,11 @@
|
|
|
3174
3185
|
</div>
|
|
3175
3186
|
</div>
|
|
3176
3187
|
|
|
3188
|
+
<!-- Tools management view -->
|
|
3189
|
+
<div id="toolsContainer" class="tools-container" style="display:none;">
|
|
3190
|
+
<div id="tool-status-container"></div>
|
|
3191
|
+
</div>
|
|
3192
|
+
|
|
3177
3193
|
<!-- Input area: fixed at bottom -->
|
|
3178
3194
|
<div class="input-section">
|
|
3179
3195
|
<div class="input-wrapper">
|
|
@@ -3243,6 +3259,7 @@
|
|
|
3243
3259
|
<script defer src="/gm/js/conversations.js"></script>
|
|
3244
3260
|
<script defer src="/gm/js/terminal.js"></script>
|
|
3245
3261
|
<script defer src="/gm/js/script-runner.js"></script>
|
|
3262
|
+
<script defer src="/gm/js/tool-status.js"></script>
|
|
3246
3263
|
<script defer src="/gm/js/client.js"></script>
|
|
3247
3264
|
<script type="module" src="/gm/js/voice.js"></script>
|
|
3248
3265
|
<script defer src="/gm/js/features.js"></script>
|
package/static/js/features.js
CHANGED
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
var fileIframe = document.getElementById('fileBrowserIframe');
|
|
150
150
|
var voiceContainer = document.getElementById('voiceContainer');
|
|
151
151
|
var terminalContainer = document.getElementById('terminalContainer');
|
|
152
|
+
var toolsContainer = document.getElementById('toolsContainer');
|
|
152
153
|
if (!bar) return;
|
|
153
154
|
bar.querySelectorAll('.view-toggle-btn').forEach(function(btn) {
|
|
154
155
|
btn.classList.toggle('active', btn.dataset.view === view);
|
|
@@ -158,6 +159,7 @@
|
|
|
158
159
|
if (fileBrowser) fileBrowser.style.display = view === 'files' ? 'flex' : 'none';
|
|
159
160
|
if (voiceContainer) voiceContainer.style.display = view === 'voice' ? 'flex' : 'none';
|
|
160
161
|
if (terminalContainer) terminalContainer.style.display = view === 'terminal' ? 'flex' : 'none';
|
|
162
|
+
if (toolsContainer) toolsContainer.style.display = view === 'tools' ? 'flex' : 'none';
|
|
161
163
|
if (view === 'files' && fileIframe && currentConversation) {
|
|
162
164
|
var src = BASE + '/files/' + currentConversation + '/';
|
|
163
165
|
if (fileIframe.src !== location.origin + src) fileIframe.src = src;
|