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.
@@ -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', binary: 'opencode', marker: path.join(os.homedir(), '.config', 'opencode', 'agents') },
11
- { id: 'gm-gc', name: 'Gemini CLI', pkg: 'gm-gc', binary: 'gemini', marker: path.join(os.homedir(), '.gemini', 'extensions', 'gm', 'agents') },
12
- { id: 'gm-kilo', name: 'Kilo', pkg: 'gm-kilo', binary: 'kilo', marker: path.join(os.homedir(), '.config', 'kilo', 'agents') },
13
- { id: 'gm-cc', name: 'Claude Code', pkg: 'gm-cc', binary: 'claude', marker: path.join(os.homedir(), '.config', 'claude', 'agents') },
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 versionCache = new Map();
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
- const isInstalled = (tool) => {
21
- const ext = isWindows ? '.cmd' : '';
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 binPath = execSync(`${isWindows ? 'where' : 'which'} ${binary}`, { timeout: 2000, encoding: 'utf8', stdio: 'pipe' }).trim();
36
- let pkgDir = path.dirname(binPath);
37
- for (let i = 0; i < 5; i++) {
38
- const pkgPath = path.join(pkgDir, 'package.json');
39
- if (fs.existsSync(pkgPath)) {
40
- try {
41
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
42
- if (pkg.version && pkg.version.match(/\d+\.\d+\.\d+/)) {
43
- return pkg.version.match(/(\d+\.\d+\.\d+)/)[1];
44
- }
45
- } catch (_) {}
46
- }
47
- pkgDir = path.dirname(pkgDir);
48
- }
49
- } catch (_) {}
50
- return null;
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
- const installed = isInstalled(tool) || fs.existsSync(tool.marker);
58
- const version = installed ? detectVersion(tool.binary) : null;
59
- return { toolId, installed, version, timestamp: Date.now() };
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 checkForUpdates(toolId, currentVersion) {
63
- if (!currentVersion) return { hasUpdate: false, latestVersion: null };
70
+ export async function checkToolStatusAsync(toolId) {
64
71
  const tool = getTool(toolId);
65
- if (!tool) return { hasUpdate: false, latestVersion: null };
72
+ if (!tool) return null;
66
73
 
67
- try {
68
- const cached = versionCache.get(toolId);
69
- if (cached && Date.now() - cached.timestamp < 3600000) return { hasUpdate: cmpVer(currentVersion, cached.version), latestVersion: cached.version };
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
- const res = await fetch(`https://registry.npmjs.org/${tool.pkg}`, { timeout: 5000, headers: { 'Accept': 'application/json' } });
72
- if (!res.ok) return { hasUpdate: false, latestVersion: null };
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
- const data = await res.json();
75
- const latest = data['dist-tags']?.latest;
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
- const createMarkerDir = (tool) => {
82
- try {
83
- const markerDir = path.dirname(tool.marker);
84
- if (!fs.existsSync(markerDir)) {
85
- fs.mkdirSync(markerDir, { recursive: true });
86
- }
87
- if (!fs.existsSync(tool.marker)) {
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 spawnProc = (toolId, tool, pkg, onProgress) => new Promise((resolve) => {
94
- const proc = spawn(isWindows ? 'npx.cmd' : 'npx', ['--yes', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
95
- let completed = false, stderr = '';
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
- if (code === 0) {
104
- createMarkerDir(tool);
105
- let s = checkToolStatus(toolId);
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: stderr.substring(0, 1000) || 'Failed' });
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 { return await spawnProc(toolId, tool, tool.pkg, onProgress); } finally { installLocks.delete(toolId); }
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, targetVersion, onProgress) {
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 = checkToolStatus(toolId);
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 { return await spawnProc(toolId, tool, `${tool.pkg}@${target}`, onProgress); } finally { installLocks.delete(toolId); }
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 getAllTools() { return TOOLS.map(tool => ({ ...tool, ...checkToolStatus(tool.id) })); }
140
- export function getToolConfig(toolId) { return getTool(toolId) || null; }
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.466",
3
+ "version": "1.0.468",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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.getAllTools();
1787
- const toolsWithStatus = tools.map((t) => {
1788
- const dbStatus = queries.getToolStatus(t.id);
1789
- const status = dbStatus?.status || (t.installed ? 'installed' : 'not_installed');
1790
- return {
1791
- id: t.id,
1792
- name: t.name,
1793
- pkg: t.pkg,
1794
- binary: t.binary,
1795
- installed: t.installed,
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: toolsWithUpdates });
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 ? `v${tool.version || '?'} (update available)` : `v${tool.version || '?'}`;
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 to v' + esc(tool.latestVersion || '?') + '</button>' :
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' : '') + '>Check for updates</button>'
188
+ '<button class="tool-btn tool-btn-secondary" onclick="window.toolsManager.refresh()" ' + (isRefreshing ? 'disabled' : '') + '>Refresh</button>'
188
189
  ) +
189
190
  '</div>' +
190
191
  '</div>';