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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.453",
3
+ "version": "1.0.454",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
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>
@@ -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;