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.
@@ -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: 'gm-oc' },
10
- { id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc' },
11
- { id: 'gm-kilo', name: 'Kilo', pkg: 'gm-kilo' },
12
- { id: 'gm-cc', name: 'Claude Code', pkg: 'gm-cc' },
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
- const pluginPath = path.join(homeDir, '.claude', 'plugins', pkg);
30
- const pluginJsonPath = path.join(pluginPath, 'plugin.json');
31
- if (fs.existsSync(pluginJsonPath)) {
32
- const pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf-8'));
33
- if (pluginJson.version) {
34
- return pluginJson.version;
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
- const nodeModulesPackagePath = path.join(nodeModulesPath, pkg);
124
- if (fs.existsSync(nodeModulesPackagePath)) {
192
+ if (fs.existsSync(path.join(nodeModulesPath, pkg))) {
125
193
  return true;
126
194
  }
127
- const homeDir = os.homedir();
128
- const pluginPath = path.join(homeDir, '.claude', 'plugins', pkg);
129
- return fs.existsSync(pluginPath);
130
- } catch (_) {
131
- return false;
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 cmd = isWindows ? 'bunx.cmd' : 'bunx';
138
- const checkResult = await new Promise((resolve) => {
139
- const proc = spawn(cmd, [pkg, '--version'], {
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
- let finalInstalledVersion = checkResult.installedVersion;
171
- if (!finalInstalledVersion && checkResult.installed) {
172
- finalInstalledVersion = await getCliToolVersion(pkg);
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
- const publishedVersion = await getPublishedVersion(pkg);
176
- const compareVersions = (v1, v2) => {
177
- if (!v1 || !v2) return false;
178
- const parts1 = v1.split('.').map(Number);
179
- const parts2 = v2.split('.').map(Number);
180
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
181
- const p1 = parts1[i] || 0;
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
- return { success: true, error: null, version };
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
- return { success: true, error: null, version };
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 {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.510",
3
+ "version": "1.0.512",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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 status = toolManager.checkToolStatus(toolId);
1821
- if (!status) {
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, status.version);
1827
- status.hasUpdate = updates.hasUpdate;
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.checkToolStatus(toolId);
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();