agentgui 1.0.462 → 1.0.464

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prd CHANGED
@@ -0,0 +1,74 @@
1
+ {
2
+ "project": "agentgui-tools-detection",
3
+ "created": "2026-03-01",
4
+ "objective": "Fix tools view to correctly detect, install, and upgrade CLI tools (OpenCode, Gemini CLI, Kilo, Claude Code)",
5
+ "problem": "Tools show 'Not installed' even after installation via npm/npx. Detection logic fails to recognize installed tools.",
6
+ "root_causes": [
7
+ "isInstalled() checks PATH but npm-installed binaries may not be in PATH immediately",
8
+ "Version detection uses --version flag which may not work for all tools",
9
+ "Marker directories are not created during installation, only checked",
10
+ "Race condition: binary checked before npm finishes fully writing files",
11
+ "No fallback detection method if which/where command fails"
12
+ ],
13
+ "items": [
14
+ {
15
+ "id": "9",
16
+ "subject": "Verify git commit and all work is pushed",
17
+ "status": "pending",
18
+ "description": "Ensure all changes are committed and pushed to remote repository.",
19
+ "blocking": [],
20
+ "blockedBy": [
21
+ "8"
22
+ ],
23
+ "effort": "small",
24
+ "category": "infra",
25
+ "acceptance": [
26
+ "git status shows no uncommitted changes",
27
+ "All commits are pushed to remote",
28
+ "CI/build passes if configured"
29
+ ]
30
+ }
31
+ ],
32
+ "completed": [
33
+ {
34
+ "id": "1",
35
+ "subject": "Understand current tool detection and installation flow",
36
+ "status": "completed"
37
+ },
38
+ {
39
+ "id": "2",
40
+ "subject": "Implement reliable tool installation detection with multiple fallbacks",
41
+ "status": "completed"
42
+ },
43
+ {
44
+ "id": "3",
45
+ "subject": "Improve version detection with multiple fallback strategies",
46
+ "status": "completed"
47
+ },
48
+ {
49
+ "id": "4",
50
+ "subject": "Add installation verification and marker directory creation",
51
+ "status": "completed"
52
+ },
53
+ {
54
+ "id": "5",
55
+ "subject": "Test tool installation and detection for all 4 tools",
56
+ "status": "completed"
57
+ },
58
+ {
59
+ "id": "6",
60
+ "subject": "Fix tools UI to show correct installation status",
61
+ "status": "completed"
62
+ },
63
+ {
64
+ "id": "7",
65
+ "subject": "Implement upgrade mechanism for installed tools",
66
+ "status": "completed"
67
+ },
68
+ {
69
+ "id": "8",
70
+ "subject": "Test upgrade path for each tool",
71
+ "status": "completed"
72
+ }
73
+ ]
74
+ }
package/database.js CHANGED
@@ -1607,6 +1607,28 @@ export const queries = {
1607
1607
  return stmt.all();
1608
1608
  },
1609
1609
 
1610
+ insertToolInstallation(toolId, data) {
1611
+ const now = Date.now();
1612
+ const stmt = prep(`
1613
+ INSERT OR IGNORE INTO tool_installations
1614
+ (id, tool_id, version, installed_at, status, last_check_at, error_message, update_available, latest_version, created_at, updated_at)
1615
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1616
+ `);
1617
+ stmt.run(
1618
+ generateId('ti'),
1619
+ toolId,
1620
+ data.version || null,
1621
+ data.installed_at || null,
1622
+ data.status || 'not_installed',
1623
+ now,
1624
+ data.error_message || null,
1625
+ 0,
1626
+ null,
1627
+ now,
1628
+ now
1629
+ );
1630
+ },
1631
+
1610
1632
  updateToolStatus(toolId, data) {
1611
1633
  const now = Date.now();
1612
1634
  const stmt = prep(`
@@ -22,7 +22,33 @@ const isInstalled = (tool) => {
22
22
  if (fs.existsSync(path.join(process.cwd(), 'node_modules', '.bin', tool.binary + ext))) return true;
23
23
  try { execSync(`${isWindows ? 'where' : 'which'} ${tool.binary}`, { stdio: 'pipe', timeout: 2000 }); return true; } catch (_) { return false; }
24
24
  };
25
- const detectVersion = (binary) => { try { const o = execSync(`${binary} --version 2>&1 || ${binary} -v 2>&1`, { timeout: 3000, encoding: 'utf8', stdio: 'pipe' }); return o.match(/(\d+\.\d+\.\d+)/)?.[1] || null; } catch (_) { return null; } };
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
+ }
34
+ 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;
51
+ };
26
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; };
27
53
 
28
54
  export function checkToolStatus(toolId) {
@@ -52,13 +78,39 @@ export async function checkForUpdates(toolId, currentVersion) {
52
78
  } catch (_) { return { hasUpdate: false, latestVersion: null }; }
53
79
  }
54
80
 
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
+ };
92
+
55
93
  const spawnProc = (toolId, tool, pkg, onProgress) => new Promise((resolve) => {
56
94
  const proc = spawn(isWindows ? 'npx.cmd' : 'npx', ['--yes', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows });
57
95
  let completed = false, stderr = '';
58
96
  const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); }}, 300000);
59
97
  proc.stdout.on('data', (d) => { if (onProgress) onProgress({ type: 'progress', data: d.toString() }); });
60
98
  proc.stderr.on('data', (d) => { stderr += d.toString(); if (onProgress) onProgress({ type: 'error', data: d.toString() }); });
61
- proc.on('close', (code) => { clearTimeout(timer); if (completed) return; completed = true; if (code === 0) { const s = checkToolStatus(toolId); resolve(s?.installed ? { success: true, error: null, version: s.version } : { success: false, error: 'Tool not detected' }); } else { resolve({ success: false, error: stderr.substring(0, 1000) || 'Failed' }); } });
99
+ proc.on('close', (code) => {
100
+ clearTimeout(timer);
101
+ if (completed) return;
102
+ 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' });
110
+ } else {
111
+ resolve({ success: false, error: stderr.substring(0, 1000) || 'Failed' });
112
+ }
113
+ });
62
114
  proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); }});
63
115
  });
64
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.462",
3
+ "version": "1.0.464",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1784,12 +1784,28 @@ const server = http.createServer(async (req, res) => {
1784
1784
  if (pathOnly === '/api/tools' && req.method === 'GET') {
1785
1785
  console.log('[TOOLS-API] Handling GET /api/tools');
1786
1786
  const tools = toolManager.getAllTools();
1787
- const toolsWithUpdates = await Promise.all(tools.map(async (t) => {
1788
- if (t.installed) {
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) {
1789
1805
  const updates = await toolManager.checkForUpdates(t.id, t.version);
1790
1806
  return { ...t, hasUpdate: updates.hasUpdate, latestVersion: updates.latestVersion };
1791
1807
  }
1792
- return { ...t, hasUpdate: false, latestVersion: null };
1808
+ return t;
1793
1809
  }));
1794
1810
  sendJSON(req, res, 200, { tools: toolsWithUpdates });
1795
1811
  return;
@@ -1818,6 +1834,10 @@ const server = http.createServer(async (req, res) => {
1818
1834
  sendJSON(req, res, 404, { error: 'Tool not found' });
1819
1835
  return;
1820
1836
  }
1837
+ const existing = queries.getToolStatus(toolId);
1838
+ if (!existing) {
1839
+ queries.insertToolInstallation(toolId, { status: 'not_installed' });
1840
+ }
1821
1841
  queries.updateToolStatus(toolId, { status: 'installing' });
1822
1842
  sendJSON(req, res, 200, { success: true, installing: true, estimatedTime: 60000 });
1823
1843
  toolManager.install(toolId, (msg) => {
package/static/index.html CHANGED
@@ -3169,7 +3169,9 @@
3169
3169
  </div>
3170
3170
  <div class="voice-input-section">
3171
3171
  <div class="voice-input-wrapper">
3172
- <select class="agent-selector voice-agent-selector" data-voice-agent-selector title="Select agent"></select>
3172
+ <select class="agent-selector voice-agent-selector" data-voice-agent-selector title="Select agent"></select>
3173
+ <select class="agent-selector voice-cli-selector" data-voice-cli-selector title="Select CLI tool"></select>
3174
+ <select class="agent-selector voice-model-selector" data-voice-model-selector title="Select model"></select>
3173
3175
  <div class="voice-transcript" id="voiceTranscript" data-placeholder="Tap mic and speak..."></div>
3174
3176
  <button class="voice-mic-btn" id="voiceMicBtn" title="Toggle recording" aria-label="Voice input">
3175
3177
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -360,7 +360,9 @@ class AgentGUIClient {
360
360
  this.ui.sendButton = document.querySelector('[data-send-button]');
361
361
  this.ui.cliSelector = document.querySelector('[data-cli-selector]');
362
362
  this.ui.agentSelector = document.querySelector('[data-agent-selector]');
363
- this.ui.modelSelector = document.querySelector('[data-model-selector]');
363
+ this.ui.modelSelector = document.querySelector('[data-model-selector]');
364
+ this.ui.voiceCliSelector = document.querySelector('[data-voice-cli-selector]');
365
+ this.ui.voiceModelSelector = document.querySelector('[data-voice-model-selector]');
364
366
 
365
367
  if (this.ui.cliSelector) {
366
368
  this.ui.cliSelector.addEventListener('change', () => {
@@ -1984,7 +1986,6 @@ class AgentGUIClient {
1984
1986
  this.loadModelsForAgent(agentId).then(() => {
1985
1987
  if (this.ui.modelSelector && model) {
1986
1988
  this.ui.modelSelector.value = model;
1987
- this.ui.modelSelector.disabled = true;
1988
1989
  }
1989
1990
  });
1990
1991
  }
@@ -125,6 +125,22 @@
125
125
  if (mainSelector.value) voiceSelector.value = mainSelector.value;
126
126
  }
127
127
 
128
+ function syncVoiceCliSelector() {
129
+ var voiceCliSelector = document.querySelector('[data-voice-cli-selector]');
130
+ var mainCliSelector = document.querySelector('[data-cli-selector]');
131
+ if (!voiceCliSelector || !mainCliSelector) return;
132
+ voiceCliSelector.innerHTML = mainCliSelector.innerHTML;
133
+ if (mainCliSelector.value) voiceCliSelector.value = mainCliSelector.value;
134
+ }
135
+
136
+ function syncVoiceModelSelector() {
137
+ var voiceModelSelector = document.querySelector('[data-voice-model-selector]');
138
+ var mainModelSelector = document.querySelector('[data-model-selector]');
139
+ if (!voiceModelSelector || !mainModelSelector) return;
140
+ voiceModelSelector.innerHTML = mainModelSelector.innerHTML;
141
+ if (mainModelSelector.value) voiceModelSelector.value = mainModelSelector.value;
142
+ }
143
+
128
144
  function setupAgentSelector() {
129
145
  var voiceSelector = document.querySelector('[data-voice-agent-selector]');
130
146
  if (!voiceSelector) return;
@@ -139,6 +155,36 @@
139
155
  });
140
156
  }
141
157
  window.addEventListener('agents-loaded', syncVoiceSelector);
158
+
159
+ var mainCliSelector = document.querySelector('[data-cli-selector]');
160
+ if (mainCliSelector) {
161
+ syncVoiceCliSelector();
162
+ mainCliSelector.addEventListener('change', function() {
163
+ var voiceCliSelector = document.querySelector('[data-voice-cli-selector]');
164
+ if (voiceCliSelector) voiceCliSelector.value = mainCliSelector.value;
165
+ });
166
+ var voiceCliSelector = document.querySelector('[data-voice-cli-selector]');
167
+ if (voiceCliSelector) {
168
+ voiceCliSelector.addEventListener('change', function() {
169
+ mainCliSelector.value = voiceCliSelector.value;
170
+ });
171
+ }
172
+ }
173
+
174
+ var mainModelSelector = document.querySelector('[data-model-selector]');
175
+ if (mainModelSelector) {
176
+ syncVoiceModelSelector();
177
+ mainModelSelector.addEventListener('change', function() {
178
+ var voiceModelSelector = document.querySelector('[data-voice-model-selector]');
179
+ if (voiceModelSelector) voiceModelSelector.value = mainModelSelector.value;
180
+ });
181
+ var voiceModelSelector = document.querySelector('[data-voice-model-selector]');
182
+ if (voiceModelSelector) {
183
+ voiceModelSelector.addEventListener('change', function() {
184
+ mainModelSelector.value = voiceModelSelector.value;
185
+ });
186
+ }
187
+ }
142
188
  }
143
189
 
144
190
  function setupTTSToggle() {