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 +74 -0
- package/database.js +22 -0
- package/lib/tool-manager.js +54 -2
- package/package.json +1 -1
- package/server.js +23 -3
- package/static/index.html +3 -1
- package/static/js/client.js +3 -2
- package/static/js/voice.js +46 -0
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(`
|
package/lib/tool-manager.js
CHANGED
|
@@ -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) => {
|
|
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) => {
|
|
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
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
|
|
1788
|
-
|
|
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
|
|
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">
|
package/static/js/client.js
CHANGED
|
@@ -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
|
}
|
package/static/js/voice.js
CHANGED
|
@@ -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() {
|