agentgui 1.0.510 → 1.0.512
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 +138 -74
- package/lib/ws-optimizer.js +2 -2
- package/package.json +1 -1
- package/server.js +110 -9
- package/static/js/tools-manager.js +1 -0
- package/test-http-tools.js +101 -0
package/lib/tool-manager.js
CHANGED
|
@@ -6,10 +6,10 @@ import path from 'path';
|
|
|
6
6
|
|
|
7
7
|
const isWindows = os.platform() === 'win32';
|
|
8
8
|
const TOOLS = [
|
|
9
|
-
{ id: 'gm-oc', name: 'OpenCode', pkg: '
|
|
10
|
-
{ id: 'gm-gc', name: 'Gemini CLI', pkg: '
|
|
11
|
-
{ id: 'gm-kilo', name: 'Kilo', pkg: '
|
|
12
|
-
{ id: 'gm-cc', name: 'Claude Code', pkg: '
|
|
9
|
+
{ id: 'gm-oc', name: 'OpenCode', pkg: 'opencode-ai' },
|
|
10
|
+
{ id: 'gm-gc', name: 'Gemini CLI', pkg: '@google/gemini-cli' },
|
|
11
|
+
{ id: 'gm-kilo', name: 'Kilo', pkg: '@kilocode/cli' },
|
|
12
|
+
{ id: 'gm-cc', name: 'Claude Code', pkg: '@anthropic-ai/claude-code' },
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
const statusCache = new Map();
|
|
@@ -26,12 +26,58 @@ const getNodeModulesPath = () => {
|
|
|
26
26
|
const getInstalledVersion = (pkg) => {
|
|
27
27
|
try {
|
|
28
28
|
const homeDir = os.homedir();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
|
|
30
|
+
// Check Claude Code plugins
|
|
31
|
+
const claudePath = path.join(homeDir, '.claude', 'plugins', pkg, 'plugin.json');
|
|
32
|
+
if (fs.existsSync(claudePath)) {
|
|
33
|
+
try {
|
|
34
|
+
const pluginJson = JSON.parse(fs.readFileSync(claudePath, 'utf-8'));
|
|
35
|
+
if (pluginJson.version) return pluginJson.version;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.warn(`[tool-manager] Failed to parse ${claudePath}:`, e.message);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check OpenCode agents
|
|
42
|
+
const opencodePath = path.join(homeDir, '.config', 'opencode', 'agents', pkg, 'plugin.json');
|
|
43
|
+
if (fs.existsSync(opencodePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pluginJson = JSON.parse(fs.readFileSync(opencodePath, 'utf-8'));
|
|
46
|
+
if (pluginJson.version) return pluginJson.version;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.warn(`[tool-manager] Failed to parse ${opencodePath}:`, e.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check Gemini CLI agents (stored as 'gm' directory)
|
|
53
|
+
const geminiPath = path.join(homeDir, '.gemini', 'extensions', 'gm', 'plugin.json');
|
|
54
|
+
if (fs.existsSync(geminiPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const pluginJson = JSON.parse(fs.readFileSync(geminiPath, 'utf-8'));
|
|
57
|
+
if (pluginJson.version) return pluginJson.version;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
console.warn(`[tool-manager] Failed to parse ${geminiPath}:`, e.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Try gemini-extension.json as fallback
|
|
63
|
+
const geminiExtPath = path.join(homeDir, '.gemini', 'extensions', 'gm', 'gemini-extension.json');
|
|
64
|
+
if (fs.existsSync(geminiExtPath)) {
|
|
65
|
+
try {
|
|
66
|
+
const extJson = JSON.parse(fs.readFileSync(geminiExtPath, 'utf-8'));
|
|
67
|
+
if (extJson.version) return extJson.version;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn(`[tool-manager] Failed to parse ${geminiExtPath}:`, e.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check Kilo agents
|
|
74
|
+
const kiloPath = path.join(homeDir, '.config', 'kilo', 'agents', pkg, 'plugin.json');
|
|
75
|
+
if (fs.existsSync(kiloPath)) {
|
|
76
|
+
try {
|
|
77
|
+
const pluginJson = JSON.parse(fs.readFileSync(kiloPath, 'utf-8'));
|
|
78
|
+
if (pluginJson.version) return pluginJson.version;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.warn(`[tool-manager] Failed to parse ${kiloPath}:`, e.message);
|
|
35
81
|
}
|
|
36
82
|
}
|
|
37
83
|
} catch (_) {}
|
|
@@ -119,75 +165,69 @@ const getPublishedVersion = async (pkg) => {
|
|
|
119
165
|
|
|
120
166
|
const checkToolInstalled = (pkg) => {
|
|
121
167
|
try {
|
|
168
|
+
const homeDir = os.homedir();
|
|
169
|
+
|
|
170
|
+
// Check Claude Code plugins
|
|
171
|
+
if (fs.existsSync(path.join(homeDir, '.claude', 'plugins', pkg))) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check OpenCode agents
|
|
176
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', pkg))) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check Gemini CLI agents (always stored as 'gm' directory)
|
|
181
|
+
if (fs.existsSync(path.join(homeDir, '.gemini', 'extensions', 'gm'))) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check Kilo agents
|
|
186
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pkg))) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check node_modules as fallback
|
|
122
191
|
const nodeModulesPath = getNodeModulesPath();
|
|
123
|
-
|
|
124
|
-
if (fs.existsSync(nodeModulesPackagePath)) {
|
|
192
|
+
if (fs.existsSync(path.join(nodeModulesPath, pkg))) {
|
|
125
193
|
return true;
|
|
126
194
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
return false;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const compareVersions = (v1, v2) => {
|
|
200
|
+
if (!v1 || !v2) return false;
|
|
201
|
+
const parts1 = v1.split('.').map(Number);
|
|
202
|
+
const parts2 = v2.split('.').map(Number);
|
|
203
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
204
|
+
const p1 = parts1[i] || 0;
|
|
205
|
+
const p2 = parts2[i] || 0;
|
|
206
|
+
if (p1 < p2) return true;
|
|
207
|
+
if (p1 > p2) return false;
|
|
132
208
|
}
|
|
209
|
+
return false;
|
|
133
210
|
};
|
|
134
211
|
|
|
135
212
|
const checkToolViaBunx = async (pkg) => {
|
|
136
213
|
try {
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
141
|
-
timeout: 10000,
|
|
142
|
-
shell: isWindows
|
|
143
|
-
});
|
|
144
|
-
let stdout = '', stderr = '';
|
|
145
|
-
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
146
|
-
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
147
|
-
const timer = setTimeout(() => {
|
|
148
|
-
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
149
|
-
const installed = checkToolInstalled(pkg);
|
|
150
|
-
const installedVersion = getInstalledVersion(pkg);
|
|
151
|
-
resolve({ installed, isUpToDate: installed, upgradeNeeded: false, output: 'timeout', installedVersion });
|
|
152
|
-
}, 10000);
|
|
153
|
-
proc.on('close', (code) => {
|
|
154
|
-
clearTimeout(timer);
|
|
155
|
-
const output = stdout + stderr;
|
|
156
|
-
const installed = code === 0 || checkToolInstalled(pkg);
|
|
157
|
-
const upgradeNeeded = output.includes('Upgrading') || output.includes('upgrade');
|
|
158
|
-
const isUpToDate = installed && !upgradeNeeded;
|
|
159
|
-
const installedVersion = getInstalledVersion(pkg);
|
|
160
|
-
resolve({ installed, isUpToDate, upgradeNeeded, output, installedVersion });
|
|
161
|
-
});
|
|
162
|
-
proc.on('error', () => {
|
|
163
|
-
clearTimeout(timer);
|
|
164
|
-
const installed = checkToolInstalled(pkg);
|
|
165
|
-
const installedVersion = getInstalledVersion(pkg);
|
|
166
|
-
resolve({ installed, isUpToDate: false, upgradeNeeded: false, output: '', installedVersion });
|
|
167
|
-
});
|
|
168
|
-
});
|
|
214
|
+
const installed = checkToolInstalled(pkg);
|
|
215
|
+
const installedVersion = getInstalledVersion(pkg);
|
|
216
|
+
const publishedVersion = await getPublishedVersion(pkg);
|
|
169
217
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
218
|
+
// Determine if update is needed by comparing versions
|
|
219
|
+
// Do NOT run bunx --version as it triggers installation/upgrade
|
|
220
|
+
const needsUpdate = installed && publishedVersion && compareVersions(installedVersion, publishedVersion);
|
|
221
|
+
const isUpToDate = installed && !needsUpdate;
|
|
174
222
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const p2 = parts2[i] || 0;
|
|
183
|
-
if (p1 < p2) return true;
|
|
184
|
-
if (p1 > p2) return false;
|
|
185
|
-
}
|
|
186
|
-
return false;
|
|
223
|
+
return {
|
|
224
|
+
installed,
|
|
225
|
+
isUpToDate,
|
|
226
|
+
upgradeNeeded: needsUpdate,
|
|
227
|
+
output: 'version-check',
|
|
228
|
+
installedVersion,
|
|
229
|
+
publishedVersion
|
|
187
230
|
};
|
|
188
|
-
|
|
189
|
-
const needsUpdate = checkResult.installed && publishedVersion && compareVersions(finalInstalledVersion, publishedVersion);
|
|
190
|
-
return { ...checkResult, installedVersion: finalInstalledVersion, publishedVersion, upgradeNeeded: needsUpdate };
|
|
191
231
|
} catch (_) {
|
|
192
232
|
const installedVersion = getInstalledVersion(pkg);
|
|
193
233
|
return { installed: checkToolInstalled(pkg), isUpToDate: false, upgradeNeeded: false, output: '', installedVersion, publishedVersion: null };
|
|
@@ -331,11 +371,23 @@ export async function install(toolId, onProgress) {
|
|
|
331
371
|
installLocks.set(toolId, true);
|
|
332
372
|
try {
|
|
333
373
|
const result = await spawnBunxProc(tool.pkg, onProgress);
|
|
334
|
-
statusCache.delete(toolId);
|
|
335
|
-
versionCache.delete(`published-${tool.pkg}`);
|
|
336
374
|
if (result.success) {
|
|
375
|
+
// Give the filesystem a moment to settle after bunx install
|
|
376
|
+
await new Promise(r => setTimeout(r, 500));
|
|
377
|
+
|
|
378
|
+
// Aggressively clear all version caches to force fresh detection
|
|
379
|
+
statusCache.delete(toolId);
|
|
380
|
+
versionCache.clear();
|
|
381
|
+
|
|
337
382
|
const version = getInstalledVersion(tool.pkg);
|
|
338
|
-
|
|
383
|
+
if (!version) {
|
|
384
|
+
console.warn(`[tool-manager] Install succeeded but version detection failed for ${toolId}. Attempting CLI check...`);
|
|
385
|
+
const cliVersion = await getCliToolVersion(tool.pkg);
|
|
386
|
+
const freshStatus = await checkToolStatusAsync(toolId);
|
|
387
|
+
return { success: true, error: null, version: cliVersion || 'unknown', ...freshStatus };
|
|
388
|
+
}
|
|
389
|
+
const freshStatus = await checkToolStatusAsync(toolId);
|
|
390
|
+
return { success: true, error: null, version, ...freshStatus };
|
|
339
391
|
}
|
|
340
392
|
return result;
|
|
341
393
|
} finally {
|
|
@@ -353,11 +405,23 @@ export async function update(toolId, onProgress) {
|
|
|
353
405
|
installLocks.set(toolId, true);
|
|
354
406
|
try {
|
|
355
407
|
const result = await spawnBunxProc(tool.pkg, onProgress);
|
|
356
|
-
statusCache.delete(toolId);
|
|
357
|
-
versionCache.delete(`published-${tool.pkg}`);
|
|
358
408
|
if (result.success) {
|
|
409
|
+
// Give the filesystem a moment to settle after bunx update
|
|
410
|
+
await new Promise(r => setTimeout(r, 500));
|
|
411
|
+
|
|
412
|
+
// Aggressively clear all version caches to force fresh detection
|
|
413
|
+
statusCache.delete(toolId);
|
|
414
|
+
versionCache.clear();
|
|
415
|
+
|
|
359
416
|
const version = getInstalledVersion(tool.pkg);
|
|
360
|
-
|
|
417
|
+
if (!version) {
|
|
418
|
+
console.warn(`[tool-manager] Update succeeded but version detection failed for ${toolId}. Attempting CLI check...`);
|
|
419
|
+
const cliVersion = await getCliToolVersion(tool.pkg);
|
|
420
|
+
const freshStatus = await checkToolStatusAsync(toolId);
|
|
421
|
+
return { success: true, error: null, version: cliVersion || 'unknown', ...freshStatus };
|
|
422
|
+
}
|
|
423
|
+
const freshStatus = await checkToolStatusAsync(toolId);
|
|
424
|
+
return { success: true, error: null, version, ...freshStatus };
|
|
361
425
|
}
|
|
362
426
|
return result;
|
|
363
427
|
} finally {
|
package/lib/ws-optimizer.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import zlib from 'zlib';
|
|
2
2
|
|
|
3
3
|
const MESSAGE_PRIORITY = {
|
|
4
|
-
high: ['streaming_error', 'streaming_complete', 'rate_limit_hit', 'streaming_cancelled', 'run_cancelled'],
|
|
5
|
-
normal: ['streaming_progress', 'streaming_start', 'message_created', 'queue_status'],
|
|
4
|
+
high: ['streaming_error', 'streaming_complete', 'rate_limit_hit', 'streaming_cancelled', 'run_cancelled', 'tool_install_complete', 'tool_update_complete', 'tool_install_failed', 'tool_update_failed'],
|
|
5
|
+
normal: ['streaming_progress', 'streaming_start', 'message_created', 'queue_status', 'tool_install_progress', 'tool_update_progress'],
|
|
6
6
|
low: ['model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list', 'tts_audio']
|
|
7
7
|
};
|
|
8
8
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -1817,15 +1817,28 @@ const server = http.createServer(async (req, res) => {
|
|
|
1817
1817
|
|
|
1818
1818
|
if (pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)) {
|
|
1819
1819
|
const toolId = pathOnly.match(/^\/api\/tools\/([^/]+)\/status$/)[1];
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1820
|
+
const dbStatus = queries.getToolStatus(toolId);
|
|
1821
|
+
const tmStatus = toolManager.checkToolStatus(toolId);
|
|
1822
|
+
if (!tmStatus && !dbStatus) {
|
|
1822
1823
|
sendJSON(req, res, 404, { error: 'Tool not found' });
|
|
1823
1824
|
return;
|
|
1824
1825
|
}
|
|
1826
|
+
|
|
1827
|
+
// Merge database status with tool manager status
|
|
1828
|
+
const status = {
|
|
1829
|
+
toolId,
|
|
1830
|
+
installed: tmStatus?.installed || (dbStatus?.status === 'installed'),
|
|
1831
|
+
isUpToDate: tmStatus?.isUpToDate || false,
|
|
1832
|
+
upgradeNeeded: tmStatus?.upgradeNeeded || false,
|
|
1833
|
+
status: dbStatus?.status || (tmStatus?.installed ? 'installed' : 'not_installed'),
|
|
1834
|
+
installedVersion: dbStatus?.version || tmStatus?.installedVersion || null,
|
|
1835
|
+
timestamp: Date.now(),
|
|
1836
|
+
error_message: dbStatus?.error_message || null
|
|
1837
|
+
};
|
|
1838
|
+
|
|
1825
1839
|
if (status.installed) {
|
|
1826
|
-
const updates = await toolManager.checkForUpdates(toolId
|
|
1827
|
-
status.hasUpdate = updates.
|
|
1828
|
-
status.latestVersion = updates.latestVersion;
|
|
1840
|
+
const updates = await toolManager.checkForUpdates(toolId);
|
|
1841
|
+
status.hasUpdate = updates.needsUpdate || false;
|
|
1829
1842
|
}
|
|
1830
1843
|
sendJSON(req, res, 200, status);
|
|
1831
1844
|
return;
|
|
@@ -1867,12 +1880,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
1867
1880
|
installCompleted = true;
|
|
1868
1881
|
if (result.success) {
|
|
1869
1882
|
const version = result.version || null;
|
|
1883
|
+
console.log(`[TOOLS-API] Install succeeded for ${toolId}, version: ${version}`);
|
|
1870
1884
|
queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
|
|
1885
|
+
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
1886
|
+
console.log(`[TOOLS-API] Fresh status after install for ${toolId}:`, JSON.stringify(freshStatus));
|
|
1871
1887
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1872
|
-
wsOptimizer.broadcast({ type: 'tool_install_complete', toolId, data: { success: true } });
|
|
1888
|
+
wsOptimizer.broadcast({ type: 'tool_install_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1873
1889
|
}
|
|
1874
1890
|
queries.addToolInstallHistory(toolId, 'install', 'success', null);
|
|
1875
1891
|
} else {
|
|
1892
|
+
console.error(`[TOOLS-API] Install failed for ${toolId}:`, result.error);
|
|
1876
1893
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1877
1894
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1878
1895
|
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: result });
|
|
@@ -1884,6 +1901,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1884
1901
|
if (installCompleted) return;
|
|
1885
1902
|
installCompleted = true;
|
|
1886
1903
|
const error = err?.message || 'Unknown error';
|
|
1904
|
+
console.error(`[TOOLS-API] Install error for ${toolId}:`, error);
|
|
1887
1905
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
|
|
1888
1906
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1889
1907
|
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: { success: false, error } });
|
|
@@ -1901,7 +1919,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1901
1919
|
sendJSON(req, res, 404, { error: 'Tool not found' });
|
|
1902
1920
|
return;
|
|
1903
1921
|
}
|
|
1904
|
-
const current = toolManager.
|
|
1922
|
+
const current = await toolManager.checkToolStatusAsync(toolId);
|
|
1905
1923
|
if (!current || !current.installed) {
|
|
1906
1924
|
sendJSON(req, res, 400, { error: 'Tool not installed' });
|
|
1907
1925
|
return;
|
|
@@ -1931,12 +1949,16 @@ const server = http.createServer(async (req, res) => {
|
|
|
1931
1949
|
updateCompleted = true;
|
|
1932
1950
|
if (result.success) {
|
|
1933
1951
|
const version = result.version || null;
|
|
1952
|
+
console.log(`[TOOLS-API] Update succeeded for ${toolId}, version: ${version}`);
|
|
1934
1953
|
queries.updateToolStatus(toolId, { status: 'installed', version, installed_at: Date.now() });
|
|
1954
|
+
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
1955
|
+
console.log(`[TOOLS-API] Fresh status after update for ${toolId}:`, JSON.stringify(freshStatus));
|
|
1935
1956
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1936
|
-
wsOptimizer.broadcast({ type: 'tool_update_complete', toolId, data: { success: true } });
|
|
1957
|
+
wsOptimizer.broadcast({ type: 'tool_update_complete', toolId, data: { success: true, ...freshStatus } });
|
|
1937
1958
|
}
|
|
1938
1959
|
queries.addToolInstallHistory(toolId, 'update', 'success', null);
|
|
1939
1960
|
} else {
|
|
1961
|
+
console.error(`[TOOLS-API] Update failed for ${toolId}:`, result.error);
|
|
1940
1962
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
1941
1963
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1942
1964
|
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: result });
|
|
@@ -1948,6 +1970,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1948
1970
|
if (updateCompleted) return;
|
|
1949
1971
|
updateCompleted = true;
|
|
1950
1972
|
const error = err?.message || 'Unknown error';
|
|
1973
|
+
console.error(`[TOOLS-API] Update error for ${toolId}:`, error);
|
|
1951
1974
|
queries.updateToolStatus(toolId, { status: 'failed', error_message: error });
|
|
1952
1975
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
1953
1976
|
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: { success: false, error } });
|
|
@@ -1967,6 +1990,81 @@ const server = http.createServer(async (req, res) => {
|
|
|
1967
1990
|
return;
|
|
1968
1991
|
}
|
|
1969
1992
|
|
|
1993
|
+
|
|
1994
|
+
// Handle POST /api/tools/{toolId}/install - individual tool install
|
|
1995
|
+
const installMatch = pathOnly.match(/^\/api\/tools\/([^\/]+)\/install$/);
|
|
1996
|
+
if (installMatch && req.method === 'POST') {
|
|
1997
|
+
const toolId = installMatch[1];
|
|
1998
|
+
sendJSON(req, res, 200, { installing: true, toolId });
|
|
1999
|
+
setImmediate(async () => {
|
|
2000
|
+
try {
|
|
2001
|
+
const result = await toolManager.install(toolId, (msg) => {
|
|
2002
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2003
|
+
wsOptimizer.broadcast({ type: 'tool_install_progress', toolId, data: msg });
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
if (result.success) {
|
|
2007
|
+
queries.updateToolStatus(toolId, { status: 'installed', installed_at: Date.now() });
|
|
2008
|
+
queries.addToolInstallHistory(toolId, 'install', 'success', null);
|
|
2009
|
+
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
2010
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2011
|
+
wsOptimizer.broadcast({ type: 'tool_install_complete', toolId, data: { ...result, ...freshStatus } });
|
|
2012
|
+
}
|
|
2013
|
+
} else {
|
|
2014
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
2015
|
+
queries.addToolInstallHistory(toolId, 'install', 'failed', result.error);
|
|
2016
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2017
|
+
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: result });
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
} catch (err) {
|
|
2021
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: err.message });
|
|
2022
|
+
queries.addToolInstallHistory(toolId, 'install', 'failed', err.message);
|
|
2023
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2024
|
+
wsOptimizer.broadcast({ type: 'tool_install_failed', toolId, data: { success: false, error: err.message } });
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// Handle POST /api/tools/{toolId}/update - individual tool update
|
|
2032
|
+
const updateMatch = pathOnly.match(/^\/api\/tools\/([^\/]+)\/update$/);
|
|
2033
|
+
if (updateMatch && req.method === 'POST') {
|
|
2034
|
+
const toolId = updateMatch[1];
|
|
2035
|
+
sendJSON(req, res, 200, { updating: true, toolId });
|
|
2036
|
+
setImmediate(async () => {
|
|
2037
|
+
try {
|
|
2038
|
+
const result = await toolManager.update(toolId, (msg) => {
|
|
2039
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2040
|
+
wsOptimizer.broadcast({ type: 'tool_update_progress', toolId, data: msg });
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
if (result.success) {
|
|
2044
|
+
queries.updateToolStatus(toolId, { status: 'installed', installed_at: Date.now() });
|
|
2045
|
+
queries.addToolInstallHistory(toolId, 'update', 'success', null);
|
|
2046
|
+
const freshStatus = await toolManager.checkToolStatusAsync(toolId);
|
|
2047
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2048
|
+
wsOptimizer.broadcast({ type: 'tool_update_complete', toolId, data: { ...result, ...freshStatus } });
|
|
2049
|
+
}
|
|
2050
|
+
} else {
|
|
2051
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: result.error });
|
|
2052
|
+
queries.addToolInstallHistory(toolId, 'update', 'failed', result.error);
|
|
2053
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2054
|
+
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: result });
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
} catch (err) {
|
|
2058
|
+
queries.updateToolStatus(toolId, { status: 'failed', error_message: err.message });
|
|
2059
|
+
queries.addToolInstallHistory(toolId, 'update', 'failed', err.message);
|
|
2060
|
+
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
2061
|
+
wsOptimizer.broadcast({ type: 'tool_update_failed', toolId, data: { success: false, error: err.message } });
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
1970
2068
|
if (pathOnly === '/api/tools/update' && req.method === 'POST') {
|
|
1971
2069
|
sendJSON(req, res, 200, { updating: true, toolCount: 4 });
|
|
1972
2070
|
if (wsOptimizer && wsOptimizer.broadcast) {
|
|
@@ -4087,7 +4185,10 @@ const BROADCAST_TYPES = new Set([
|
|
|
4087
4185
|
'rate_limit_hit', 'rate_limit_clear',
|
|
4088
4186
|
'script_started', 'script_stopped', 'script_output',
|
|
4089
4187
|
'model_download_progress', 'stt_progress', 'tts_setup_progress', 'voice_list',
|
|
4090
|
-
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error'
|
|
4188
|
+
'streaming_start', 'streaming_progress', 'streaming_complete', 'streaming_error',
|
|
4189
|
+
'tool_install_started', 'tool_install_progress', 'tool_install_complete', 'tool_install_failed',
|
|
4190
|
+
'tool_update_progress', 'tool_update_complete', 'tool_update_failed',
|
|
4191
|
+
'tools_update_started', 'tools_update_complete', 'tools_refresh_complete'
|
|
4091
4192
|
]);
|
|
4092
4193
|
|
|
4093
4194
|
const wsOptimizer = new WSOptimizer();
|
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
tool.hasUpdate = (data.data?.upgradeNeeded && data.data?.installed) ?? false;
|
|
176
176
|
tool.progress = 100;
|
|
177
177
|
operationInProgress.delete(data.toolId);
|
|
178
|
+
render();
|
|
178
179
|
setTimeout(fetchTools, 1000);
|
|
179
180
|
}
|
|
180
181
|
} else if (data.type === 'tool_install_failed' || data.type === 'tool_update_failed') {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = 'http://localhost:3000/gm';
|
|
4
|
+
|
|
5
|
+
async function httpGet(path) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const url = new URL(path, BASE_URL);
|
|
8
|
+
http.get(url, (res) => {
|
|
9
|
+
let data = '';
|
|
10
|
+
res.on('data', chunk => data += chunk);
|
|
11
|
+
res.on('end', () => {
|
|
12
|
+
try {
|
|
13
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
14
|
+
} catch {
|
|
15
|
+
resolve({ status: res.statusCode, data });
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}).on('error', reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function httpPost(path, body) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const url = new URL(path, BASE_URL);
|
|
25
|
+
const bodyStr = JSON.stringify(body);
|
|
26
|
+
const options = {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': bodyStr.length }
|
|
29
|
+
};
|
|
30
|
+
const req = http.request(url, options, (res) => {
|
|
31
|
+
let data = '';
|
|
32
|
+
res.on('data', chunk => data += chunk);
|
|
33
|
+
res.on('end', () => {
|
|
34
|
+
try {
|
|
35
|
+
resolve({ status: res.statusCode, data: JSON.parse(data) });
|
|
36
|
+
} catch {
|
|
37
|
+
resolve({ status: res.statusCode, data });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}).on('error', reject);
|
|
41
|
+
req.write(bodyStr);
|
|
42
|
+
req.end();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runTests() {
|
|
47
|
+
console.log('=== HTTP API TESTS ===\n');
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
console.log('TEST 1: GET /api/tools');
|
|
51
|
+
const tools = await httpGet('/api/tools');
|
|
52
|
+
console.log('Status:', tools.status);
|
|
53
|
+
console.log('Tools:', tools.data?.tools?.length || 0);
|
|
54
|
+
if (tools.data?.tools) {
|
|
55
|
+
tools.data.tools.forEach(t => {
|
|
56
|
+
console.log(` ${t.id}: status=${t.status}, installed=${t.installed}, hasUpdate=${t.hasUpdate}`);
|
|
57
|
+
console.log(` versions: installed=${t.installedVersion}, published=${t.publishedVersion}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
|
|
62
|
+
if (tools.data?.tools?.length > 0) {
|
|
63
|
+
const firstTool = tools.data.tools[0];
|
|
64
|
+
|
|
65
|
+
console.log(`TEST 2: GET /api/tools/${firstTool.id}/status`);
|
|
66
|
+
const status = await httpGet(`/api/tools/${firstTool.id}/status`);
|
|
67
|
+
console.log('Status code:', status.status);
|
|
68
|
+
console.log('Status data:', JSON.stringify(status.data, null, 2));
|
|
69
|
+
console.log('');
|
|
70
|
+
|
|
71
|
+
if (firstTool.installed && firstTool.hasUpdate) {
|
|
72
|
+
console.log(`TEST 3: POST /api/tools/${firstTool.id}/update (DRY RUN - not actually posting)`);
|
|
73
|
+
console.log(`Would update: ${firstTool.id}`);
|
|
74
|
+
console.log(`Current version: ${firstTool.installedVersion}`);
|
|
75
|
+
console.log(`Available version: ${firstTool.publishedVersion}`);
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
console.log('FLOW WOULD BE:');
|
|
79
|
+
console.log(' 1. Frontend sends POST /api/tools/{id}/update');
|
|
80
|
+
console.log(' 2. Backend sets status to "updating" in DB');
|
|
81
|
+
console.log(' 3. Backend sends immediate 200 response');
|
|
82
|
+
console.log(' 4. Backend spawns bunx process async');
|
|
83
|
+
console.log(' 5. Backend broadcasts WebSocket "tool_update_progress" events');
|
|
84
|
+
console.log(' 6. When bunx completes:');
|
|
85
|
+
console.log(' - Clears caches');
|
|
86
|
+
console.log(' - Calls checkToolStatusAsync() for fresh status');
|
|
87
|
+
console.log(' - Broadcasts "tool_update_complete" with new status');
|
|
88
|
+
console.log(' - Updates DB with new version');
|
|
89
|
+
console.log(' 7. Frontend receives event and updates UI');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('\n=== TESTS COMPLETED ===');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('ERROR:', err.message);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
runTests();
|