agentgui 1.0.615 → 1.0.617
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/README.md +0 -1
- package/TOOLS-FIX-SUMMARY.md +122 -0
- package/TOOLS_COMPLETION_REPORT.md +199 -0
- package/TOOLS_DEBUG_SUMMARY.md +105 -0
- package/database.js +5 -2
- package/docs/index.html +0 -1
- package/lib/speech.js +0 -1
- package/lib/tool-manager.js +178 -119
- package/lib/ws-handlers-conv.js +16 -2
- package/lib/ws-handlers-session.js +32 -39
- package/lib/ws-handlers-util.js +33 -13
- package/package.json +1 -1
- package/scripts/patch-fsbrowse.js +131 -4
- package/scripts/take-screenshots.sh +14 -1
- package/server.js +187 -122
- package/static/index.html +13 -22
- package/static/js/client.js +61 -2
- package/static/js/streaming-renderer.js +3 -1
- package/static/js/tools-manager.js +11 -0
- package/test-cli-detection.mjs +37 -0
- package/test-system-validation.js +188 -0
package/lib/tool-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn, execSync } from 'child_process';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
@@ -10,10 +10,10 @@ const TOOLS = [
|
|
|
10
10
|
{ id: 'cli-gemini', name: 'Gemini CLI', pkg: '@google/gemini-cli', category: 'cli' },
|
|
11
11
|
{ id: 'cli-kilo', name: 'Kilo Code', pkg: '@kilocode/cli', category: 'cli' },
|
|
12
12
|
{ id: 'cli-codex', name: 'Codex CLI', pkg: '@openai/codex', category: 'cli' },
|
|
13
|
-
{ id: 'gm-cc', name: 'GM Claude', pkg: 'gm-cc', pluginId: 'gm-cc', category: 'plugin' },
|
|
14
|
-
{ id: 'gm-oc', name: 'GM OpenCode', pkg: 'gm-oc', pluginId: 'gm
|
|
15
|
-
{ id: 'gm-gc', name: 'GM Gemini', pkg: 'gm-gc', pluginId: 'gm', category: 'plugin' },
|
|
16
|
-
{ id: 'gm-kilo', name: 'GM Kilo', pkg: 'gm-kilo', pluginId: 'gm
|
|
13
|
+
{ id: 'gm-cc', name: 'GM Claude', pkg: 'gm-cc', pluginId: 'gm-cc', category: 'plugin', frameWork: 'claude' },
|
|
14
|
+
{ id: 'gm-oc', name: 'GM OpenCode', pkg: 'gm-oc', pluginId: 'gm', category: 'plugin', frameWork: 'opencode' },
|
|
15
|
+
{ id: 'gm-gc', name: 'GM Gemini', pkg: 'gm-gc', pluginId: 'gm', category: 'plugin', frameWork: 'gemini' },
|
|
16
|
+
{ id: 'gm-kilo', name: 'GM Kilo', pkg: 'gm-kilo', pluginId: 'gm', category: 'plugin', frameWork: 'kilo' },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
const statusCache = new Map();
|
|
@@ -22,74 +22,115 @@ const versionCache = new Map();
|
|
|
22
22
|
|
|
23
23
|
const getTool = (id) => TOOLS.find(t => t.id === id);
|
|
24
24
|
|
|
25
|
-
const getInstalledVersion = (pkg, pluginId = null) => {
|
|
25
|
+
const getInstalledVersion = (pkg, pluginId = null, frameWork = null) => {
|
|
26
26
|
try {
|
|
27
27
|
const homeDir = os.homedir();
|
|
28
|
-
const tool =
|
|
29
|
-
const actualPluginId = pluginId ||
|
|
28
|
+
const tool = TOOLS.find(t => t.pkg === pkg);
|
|
29
|
+
const actualPluginId = pluginId || tool?.pluginId || pkg;
|
|
30
|
+
const actualFrameWork = frameWork || tool?.frameWork;
|
|
30
31
|
|
|
31
32
|
// Check Claude Code plugins using correct pluginId
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
if (!frameWork || frameWork === 'claude') {
|
|
34
|
+
const claudePath = path.join(homeDir, '.claude', 'plugins', actualPluginId, 'plugin.json');
|
|
35
|
+
if (fs.existsSync(claudePath)) {
|
|
36
|
+
try {
|
|
37
|
+
const pluginJson = JSON.parse(fs.readFileSync(claudePath, 'utf-8'));
|
|
38
|
+
if (pluginJson.version) return pluginJson.version;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.warn(`[tool-manager] Failed to parse ${claudePath}:`, e.message);
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
42
|
-
// Check OpenCode agents using correct pluginId
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// Check OpenCode agents using correct pluginId (stored as .md files)
|
|
46
|
+
if (!frameWork || frameWork === 'opencode') {
|
|
47
|
+
const opencodePath = path.join(homeDir, '.config', 'opencode', 'agents', actualPluginId + '.md');
|
|
48
|
+
if (fs.existsSync(opencodePath)) {
|
|
49
|
+
// Try to extract version from markdown front matter or try plugin.json in agent dir
|
|
50
|
+
try {
|
|
51
|
+
const agentDirPath = path.join(homeDir, '.config', 'opencode', 'agents', actualPluginId, 'plugin.json');
|
|
52
|
+
if (fs.existsSync(agentDirPath)) {
|
|
53
|
+
const pluginJson = JSON.parse(fs.readFileSync(agentDirPath, 'utf-8'));
|
|
54
|
+
if (pluginJson.version) return pluginJson.version;
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// Fallback: skip
|
|
58
|
+
}
|
|
59
|
+
// For multi-framework bundles, try npm package.json in cache
|
|
60
|
+
try {
|
|
61
|
+
const pkgJsonPath = path.join(homeDir, '.gmweb/cache/.bun/install/cache');
|
|
62
|
+
const cacheDirs = fs.readdirSync(pkgJsonPath).filter(d => d.startsWith(pkg + '@'));
|
|
63
|
+
// Sort by version (get latest)
|
|
64
|
+
const latestDir = cacheDirs.sort().reverse()[0];
|
|
65
|
+
if (latestDir) {
|
|
66
|
+
const pkgJsonFile = path.join(pkgJsonPath, latestDir, 'package.json');
|
|
67
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
|
|
68
|
+
if (pkgJson.version) return pkgJson.version;
|
|
69
|
+
}
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// Fallback
|
|
72
|
+
}
|
|
73
|
+
// Last resort: try to extract from package name patterns
|
|
74
|
+
return 'installed';
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
|
|
53
|
-
// Check Gemini CLI agents (stored as 'gm' directory)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Try gemini-extension.json as fallback
|
|
64
|
-
const geminiExtPath = path.join(homeDir, '.gemini', 'extensions', actualPluginId, 'gemini-extension.json');
|
|
65
|
-
if (fs.existsSync(geminiExtPath)) {
|
|
66
|
-
try {
|
|
67
|
-
const extJson = JSON.parse(fs.readFileSync(geminiExtPath, 'utf-8'));
|
|
68
|
-
if (extJson.version) return extJson.version;
|
|
69
|
-
} catch (e) {
|
|
70
|
-
console.warn(`[tool-manager] Failed to parse ${geminiExtPath}:`, e.message);
|
|
78
|
+
// Check Gemini CLI agents (stored as 'gm' directory with gemini-extension.json)
|
|
79
|
+
if (!frameWork || frameWork === 'gemini') {
|
|
80
|
+
const geminiExtPath = path.join(homeDir, '.gemini', 'extensions', actualPluginId, 'gemini-extension.json');
|
|
81
|
+
if (fs.existsSync(geminiExtPath)) {
|
|
82
|
+
try {
|
|
83
|
+
const extJson = JSON.parse(fs.readFileSync(geminiExtPath, 'utf-8'));
|
|
84
|
+
if (extJson.version) return extJson.version;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.warn(`[tool-manager] Failed to parse ${geminiExtPath}:`, e.message);
|
|
87
|
+
}
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
90
|
|
|
74
|
-
// Check Kilo agents
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
// Check Kilo agents (stored as .md files)
|
|
92
|
+
if (!frameWork || frameWork === 'kilo') {
|
|
93
|
+
const kiloPath = path.join(homeDir, '.config', 'kilo', 'agents', actualPluginId + '.md');
|
|
94
|
+
if (fs.existsSync(kiloPath)) {
|
|
95
|
+
// Try to extract version from markdown front matter or try plugin.json in agent dir
|
|
96
|
+
try {
|
|
97
|
+
const agentDirPath = path.join(homeDir, '.config', 'kilo', 'agents', actualPluginId, 'plugin.json');
|
|
98
|
+
if (fs.existsSync(agentDirPath)) {
|
|
99
|
+
const pluginJson = JSON.parse(fs.readFileSync(agentDirPath, 'utf-8'));
|
|
100
|
+
if (pluginJson.version) return pluginJson.version;
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// Fallback: skip
|
|
104
|
+
}
|
|
105
|
+
// For multi-framework bundles, try npm package.json in cache
|
|
106
|
+
try {
|
|
107
|
+
const pkgJsonPath = path.join(homeDir, '.gmweb/cache/.bun/install/cache');
|
|
108
|
+
const cacheDirs = fs.readdirSync(pkgJsonPath).filter(d => d.startsWith(pkg + '@'));
|
|
109
|
+
// Sort by version (get latest)
|
|
110
|
+
const latestDir = cacheDirs.sort().reverse()[0];
|
|
111
|
+
if (latestDir) {
|
|
112
|
+
const pkgJsonFile = path.join(pkgJsonPath, latestDir, 'package.json');
|
|
113
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
|
|
114
|
+
if (pkgJson.version) return pkgJson.version;
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// Fallback
|
|
118
|
+
}
|
|
119
|
+
// Last resort: try to extract from package name patterns
|
|
120
|
+
return 'installed';
|
|
82
121
|
}
|
|
83
122
|
}
|
|
84
123
|
|
|
85
124
|
// Check Codex CLI (stored at ~/.codex)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
125
|
+
if (!frameWork || frameWork === 'codex') {
|
|
126
|
+
const codexPath = path.join(homeDir, '.codex', 'plugin.json');
|
|
127
|
+
if (fs.existsSync(codexPath)) {
|
|
128
|
+
try {
|
|
129
|
+
const pluginJson = JSON.parse(fs.readFileSync(codexPath, 'utf-8'));
|
|
130
|
+
if (pluginJson.version) return pluginJson.version;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.warn(`[tool-manager] Failed to parse ${codexPath}:`, e.message);
|
|
133
|
+
}
|
|
93
134
|
}
|
|
94
135
|
}
|
|
95
136
|
} catch (_) {}
|
|
@@ -97,43 +138,16 @@ const getInstalledVersion = (pkg, pluginId = null) => {
|
|
|
97
138
|
};
|
|
98
139
|
|
|
99
140
|
const getPublishedVersion = async (pkg) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const cmd = isWindows ? 'npm.cmd' : 'npm';
|
|
108
|
-
const result = await new Promise((resolve) => {
|
|
109
|
-
const proc = spawn(cmd, ['view', pkg, 'version'], {
|
|
110
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
111
|
-
timeout: 5000,
|
|
112
|
-
shell: isWindows
|
|
113
|
-
});
|
|
114
|
-
let stdout = '';
|
|
115
|
-
proc.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
116
|
-
const timer = setTimeout(() => {
|
|
117
|
-
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
118
|
-
resolve(null);
|
|
119
|
-
}, 5000);
|
|
120
|
-
proc.on('close', (code) => {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
resolve(code === 0 ? stdout.trim() : null);
|
|
123
|
-
});
|
|
124
|
-
proc.on('error', () => {
|
|
125
|
-
clearTimeout(timer);
|
|
126
|
-
resolve(null);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (result) {
|
|
131
|
-
versionCache.set(cacheKey, { version: result, timestamp: Date.now() });
|
|
132
|
-
}
|
|
133
|
-
return result;
|
|
134
|
-
} catch (_) {
|
|
135
|
-
return null;
|
|
141
|
+
const cacheKey = `published-${pkg}`;
|
|
142
|
+
const cached = versionCache.get(cacheKey);
|
|
143
|
+
// Use very aggressive caching - 24 hours
|
|
144
|
+
if (cached && Date.now() - cached.timestamp < 86400000) {
|
|
145
|
+
return cached.version;
|
|
136
146
|
}
|
|
147
|
+
|
|
148
|
+
// Return null immediately if npm view would block - never block on published versions
|
|
149
|
+
// The server should prioritize installed detection over update availability
|
|
150
|
+
return null;
|
|
137
151
|
};
|
|
138
152
|
|
|
139
153
|
const checkCliInstalled = (pkg) => {
|
|
@@ -142,7 +156,6 @@ const checkCliInstalled = (pkg) => {
|
|
|
142
156
|
const binMap = { '@anthropic-ai/claude-code': 'claude', 'opencode-ai': 'opencode', '@google/gemini-cli': 'gemini', '@kilocode/cli': 'kilo', '@openai/codex': 'codex' };
|
|
143
157
|
const bin = binMap[pkg];
|
|
144
158
|
if (bin) {
|
|
145
|
-
const { execSync } = require('child_process');
|
|
146
159
|
execSync(`${cmd} ${bin}`, { stdio: 'pipe', timeout: 3000 });
|
|
147
160
|
return true;
|
|
148
161
|
}
|
|
@@ -155,24 +168,42 @@ const getCliVersion = (pkg) => {
|
|
|
155
168
|
const binMap = { '@anthropic-ai/claude-code': 'claude', 'opencode-ai': 'opencode', '@google/gemini-cli': 'gemini', '@kilocode/cli': 'kilo', '@openai/codex': 'codex' };
|
|
156
169
|
const bin = binMap[pkg];
|
|
157
170
|
if (bin) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
try {
|
|
172
|
+
// Use short timeout - we already know the binary exists from checkCliInstalled
|
|
173
|
+
const out = execSync(`${bin} --version`, { stdio: 'pipe', timeout: 1000, encoding: 'utf8' });
|
|
174
|
+
const match = out.match(/(\d+\.\d+\.\d+)/);
|
|
175
|
+
if (match) {
|
|
176
|
+
console.log(`[tool-manager] CLI ${pkg} (${bin}) version: ${match[1]}`);
|
|
177
|
+
return match[1];
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// If version detection times out or fails, return null (binary exists but version unknown)
|
|
181
|
+
console.log(`[tool-manager] CLI ${pkg} (${bin}) version detection failed: ${err.message.split('\n')[0]}`);
|
|
182
|
+
}
|
|
162
183
|
}
|
|
163
|
-
} catch (
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.log(`[tool-manager] Error in getCliVersion for ${pkg}:`, err.message);
|
|
186
|
+
}
|
|
164
187
|
return null;
|
|
165
188
|
};
|
|
166
189
|
|
|
167
|
-
const checkToolInstalled = (pluginId) => {
|
|
190
|
+
const checkToolInstalled = (pluginId, frameWork = null) => {
|
|
168
191
|
try {
|
|
169
192
|
const homeDir = os.homedir();
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
193
|
+
if (!frameWork || frameWork === 'claude') {
|
|
194
|
+
if (fs.existsSync(path.join(homeDir, '.claude', 'plugins', pluginId))) return true;
|
|
195
|
+
}
|
|
196
|
+
if (!frameWork || frameWork === 'gemini') {
|
|
197
|
+
if (fs.existsSync(path.join(homeDir, '.gemini', 'extensions', pluginId))) return true;
|
|
198
|
+
}
|
|
199
|
+
if (!frameWork || frameWork === 'opencode') {
|
|
200
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', pluginId + '.md'))) return true;
|
|
201
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', pluginId))) return true;
|
|
202
|
+
}
|
|
203
|
+
if (!frameWork || frameWork === 'kilo') {
|
|
204
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pluginId + '.md'))) return true;
|
|
205
|
+
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pluginId))) return true;
|
|
206
|
+
}
|
|
176
207
|
} catch (_) {}
|
|
177
208
|
return false;
|
|
178
209
|
};
|
|
@@ -190,19 +221,26 @@ const compareVersions = (v1, v2) => {
|
|
|
190
221
|
return false;
|
|
191
222
|
};
|
|
192
223
|
|
|
193
|
-
const checkToolViaBunx = async (pkg, pluginId = null, category = 'plugin') => {
|
|
224
|
+
const checkToolViaBunx = async (pkg, pluginId = null, category = 'plugin', frameWork = null, skipPublishedVersion = false) => {
|
|
194
225
|
try {
|
|
195
226
|
const isCli = category === 'cli';
|
|
196
|
-
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg);
|
|
197
|
-
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId);
|
|
198
|
-
|
|
227
|
+
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
|
|
228
|
+
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork);
|
|
229
|
+
|
|
230
|
+
// Skip published version check if requested (for faster initial detection during startup)
|
|
231
|
+
let publishedVersion = null;
|
|
232
|
+
if (!skipPublishedVersion) {
|
|
233
|
+
publishedVersion = await getPublishedVersion(pkg);
|
|
234
|
+
}
|
|
235
|
+
|
|
199
236
|
const needsUpdate = installed && publishedVersion && compareVersions(installedVersion, publishedVersion);
|
|
200
237
|
const isUpToDate = installed && !needsUpdate;
|
|
201
238
|
return { installed, isUpToDate, upgradeNeeded: needsUpdate, output: 'version-check', installedVersion, publishedVersion };
|
|
202
|
-
} catch (
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.log(`[tool-manager] Error checking ${pkg}:`, err.message);
|
|
203
241
|
const isCli = category === 'cli';
|
|
204
|
-
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg);
|
|
205
|
-
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId);
|
|
242
|
+
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
|
|
243
|
+
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork);
|
|
206
244
|
return { installed, isUpToDate: false, upgradeNeeded: false, output: '', installedVersion, publishedVersion: null };
|
|
207
245
|
}
|
|
208
246
|
};
|
|
@@ -225,7 +263,7 @@ export function checkToolStatus(toolId) {
|
|
|
225
263
|
return { toolId, installed: false, isUpToDate: false, upgradeNeeded: false, timestamp: Date.now() };
|
|
226
264
|
}
|
|
227
265
|
|
|
228
|
-
export async function checkToolStatusAsync(toolId) {
|
|
266
|
+
export async function checkToolStatusAsync(toolId, skipPublishedVersion = true) {
|
|
229
267
|
const tool = getTool(toolId);
|
|
230
268
|
if (!tool) return null;
|
|
231
269
|
|
|
@@ -242,7 +280,8 @@ export async function checkToolStatusAsync(toolId) {
|
|
|
242
280
|
};
|
|
243
281
|
}
|
|
244
282
|
|
|
245
|
-
|
|
283
|
+
// Skip published version check by default for faster responses during initial tool detection
|
|
284
|
+
const result = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, skipPublishedVersion);
|
|
246
285
|
const status = {
|
|
247
286
|
toolId,
|
|
248
287
|
category: tool.category,
|
|
@@ -376,7 +415,7 @@ export async function install(toolId, onProgress) {
|
|
|
376
415
|
await new Promise(r => setTimeout(r, 500));
|
|
377
416
|
statusCache.delete(toolId);
|
|
378
417
|
versionCache.clear();
|
|
379
|
-
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId);
|
|
418
|
+
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId, tool.frameWork);
|
|
380
419
|
const freshStatus = await checkToolStatusAsync(toolId);
|
|
381
420
|
return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
|
|
382
421
|
}
|
|
@@ -400,7 +439,7 @@ export async function update(toolId, onProgress) {
|
|
|
400
439
|
await new Promise(r => setTimeout(r, 500));
|
|
401
440
|
statusCache.delete(toolId);
|
|
402
441
|
versionCache.clear();
|
|
403
|
-
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId);
|
|
442
|
+
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId, tool.frameWork);
|
|
404
443
|
const freshStatus = await checkToolStatusAsync(toolId);
|
|
405
444
|
return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
|
|
406
445
|
}
|
|
@@ -426,14 +465,32 @@ export function getAllTools() {
|
|
|
426
465
|
});
|
|
427
466
|
}
|
|
428
467
|
|
|
429
|
-
export async function getAllToolsAsync() {
|
|
430
|
-
const results = await Promise.all(TOOLS.map(tool => checkToolStatusAsync(tool.id)));
|
|
468
|
+
export async function getAllToolsAsync(skipPublishedVersion = false) {
|
|
469
|
+
const results = await Promise.all(TOOLS.map(tool => checkToolStatusAsync(tool.id, skipPublishedVersion)));
|
|
431
470
|
return results.map((status, idx) => ({
|
|
432
471
|
...TOOLS[idx],
|
|
433
472
|
...status
|
|
434
473
|
}));
|
|
435
474
|
}
|
|
436
475
|
|
|
476
|
+
export function clearStatusCache() {
|
|
477
|
+
statusCache.clear();
|
|
478
|
+
versionCache.clear();
|
|
479
|
+
console.log('[tool-manager] Caches cleared, forcing fresh tool detection');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function refreshAllToolsAsync() {
|
|
483
|
+
clearStatusCache();
|
|
484
|
+
return getAllToolsAsync();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function getAllToolsSync() {
|
|
488
|
+
return TOOLS.map(tool => {
|
|
489
|
+
const cached = statusCache.get(tool.id);
|
|
490
|
+
return { ...tool, ...cached };
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
437
494
|
export function getToolConfig(toolId) {
|
|
438
495
|
return getTool(toolId) || null;
|
|
439
496
|
}
|
|
@@ -443,7 +500,8 @@ export async function autoProvision(broadcast) {
|
|
|
443
500
|
log('Starting background tool provisioning...');
|
|
444
501
|
for (const tool of TOOLS) {
|
|
445
502
|
try {
|
|
446
|
-
|
|
503
|
+
// Skip published version check initially for faster startup - agents need to be available immediately
|
|
504
|
+
const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, true);
|
|
447
505
|
statusCache.set(tool.id, { ...status, toolId: tool.id, timestamp: Date.now() });
|
|
448
506
|
if (!status.installed) {
|
|
449
507
|
log(`${tool.id} not installed, installing...`);
|
|
@@ -473,6 +531,7 @@ export async function autoProvision(broadcast) {
|
|
|
473
531
|
}
|
|
474
532
|
} else {
|
|
475
533
|
log(`${tool.id} v${status.installedVersion} up-to-date`);
|
|
534
|
+
broadcast({ type: 'tool_status_update', toolId: tool.id, data: { installed: true, isUpToDate: true, installedVersion: status.installedVersion, status: 'installed' } });
|
|
476
535
|
}
|
|
477
536
|
} catch (err) {
|
|
478
537
|
log(`${tool.id} error: ${err.message}`);
|
package/lib/ws-handlers-conv.js
CHANGED
|
@@ -127,8 +127,22 @@ export function register(router, deps) {
|
|
|
127
127
|
broadcastSync({ type: 'message_created', conversationId: p.id, message, timestamp: Date.now() });
|
|
128
128
|
|
|
129
129
|
try {
|
|
130
|
-
//
|
|
131
|
-
|
|
130
|
+
// Send steering prompt using JSON-RPC format so agent processes it immediately
|
|
131
|
+
// Get the active session ID from the entry
|
|
132
|
+
const sessionId = entry.sessionId;
|
|
133
|
+
|
|
134
|
+
// Send JSON-RPC prompt request with requestId for tracking
|
|
135
|
+
const promptRequest = {
|
|
136
|
+
jsonrpc: '2.0',
|
|
137
|
+
id: Date.now(), // Use timestamp as unique request ID
|
|
138
|
+
method: 'session/prompt',
|
|
139
|
+
params: {
|
|
140
|
+
sessionId,
|
|
141
|
+
prompt: [{ type: 'text', text: p.content }]
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
proc.stdin.write(JSON.stringify(promptRequest) + '\n');
|
|
132
146
|
return { ok: true, steered: true, conversationId: p.id, messageId: message.id };
|
|
133
147
|
} catch (err) {
|
|
134
148
|
fail(500, `Failed to steer: ${err.message}`);
|
|
@@ -111,48 +111,41 @@ export function register(router, deps) {
|
|
|
111
111
|
router.handle('agent.subagents', async (p) => {
|
|
112
112
|
const agent = discoveredAgents.find(x => x.id === p.id);
|
|
113
113
|
if (p.id === 'claude-code') {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
encoding: 'utf-8',
|
|
117
|
-
timeout: 2000,
|
|
118
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
119
|
-
});
|
|
120
|
-
let agents = [];
|
|
121
|
-
try {
|
|
122
|
-
agents = JSON.parse(output);
|
|
123
|
-
} catch {
|
|
124
|
-
agents = output.split('\n')
|
|
125
|
-
.map(line => line.trim())
|
|
126
|
-
.filter(line => {
|
|
127
|
-
if (!line || line.startsWith('-') || line.startsWith('#')) return false;
|
|
128
|
-
if (/active agents?$/.test(line)) return false;
|
|
129
|
-
if (line.endsWith(':') && (line.includes('agents') || line.includes('agent'))) return false;
|
|
130
|
-
return true;
|
|
131
|
-
})
|
|
132
|
-
.map(line => {
|
|
133
|
-
let name = line.replace(/\s*-\s*inherit\s*$/, '').trim();
|
|
134
|
-
return { id: name.toLowerCase().replace(/\s+/g, '-'), name };
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
if (Array.isArray(agents) && agents.length > 0) {
|
|
138
|
-
return { subAgents: agents };
|
|
139
|
-
}
|
|
140
|
-
} catch (_) {}
|
|
114
|
+
// Directly enumerate skills from ~/.claude/skills directory
|
|
115
|
+
// Don't try to run 'claude agents list' as it gets intercepted by the running Claude Code instance
|
|
141
116
|
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
142
117
|
try {
|
|
143
118
|
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
144
|
-
const skills = entries
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
119
|
+
const skills = entries
|
|
120
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
121
|
+
.map(e => {
|
|
122
|
+
// Try to read SKILL.md to get the actual name
|
|
123
|
+
const md = path.join(skillsDir, e.name, 'SKILL.md');
|
|
124
|
+
let name = e.name;
|
|
125
|
+
try {
|
|
126
|
+
const content = fs.readFileSync(md, 'utf-8');
|
|
127
|
+
const m = content.match(/^name:\s*(.+)$/m);
|
|
128
|
+
if (m) name = m[1].trim();
|
|
129
|
+
} catch (_) {
|
|
130
|
+
// Fallback: format the directory name as the skill name
|
|
131
|
+
name = e.name
|
|
132
|
+
.split(/[\-_]/)
|
|
133
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
134
|
+
.join(' ');
|
|
135
|
+
}
|
|
136
|
+
return { id: e.name, name };
|
|
137
|
+
})
|
|
138
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
139
|
+
|
|
140
|
+
if (skills.length > 0) {
|
|
141
|
+
console.log(`[agent.subagents] Found ${skills.length} Claude Code skills:`, skills.map(s => s.name).join(', '));
|
|
142
|
+
return { subAgents: skills };
|
|
143
|
+
}
|
|
144
|
+
return { subAgents: [] };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`[agent.subagents] Error reading skills directory:`, err.message);
|
|
147
|
+
return { subAgents: [] };
|
|
148
|
+
}
|
|
156
149
|
}
|
|
157
150
|
if (!agent || agent.protocol !== 'acp') return { subAgents: [] };
|
|
158
151
|
const port = await ensureRunning(p.id);
|
package/lib/ws-handlers-util.js
CHANGED
|
@@ -9,10 +9,15 @@ export function register(router, deps) {
|
|
|
9
9
|
const { queries, wsOptimizer, modelDownloadState, ensureModelsDownloaded,
|
|
10
10
|
broadcastSync, getSpeech, getProviderConfigs, saveProviderConfig,
|
|
11
11
|
startGeminiOAuth, exchangeGeminiOAuthCode, geminiOAuthState,
|
|
12
|
-
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager } = deps;
|
|
12
|
+
STARTUP_CWD, activeScripts, voiceCacheManager, toolManager, discoveredAgents } = deps;
|
|
13
13
|
|
|
14
14
|
router.handle('home', () => ({ home: os.homedir(), cwd: STARTUP_CWD }));
|
|
15
15
|
|
|
16
|
+
router.handle('agent.ls', () => {
|
|
17
|
+
const agents = discoveredAgents || [];
|
|
18
|
+
return { agents };
|
|
19
|
+
});
|
|
20
|
+
|
|
16
21
|
router.handle('folders', (p) => {
|
|
17
22
|
const folderPath = p.path || STARTUP_CWD;
|
|
18
23
|
try {
|
|
@@ -79,19 +84,8 @@ export function register(router, deps) {
|
|
|
79
84
|
try {
|
|
80
85
|
const { getStatus } = await getSpeech();
|
|
81
86
|
const base = getStatus();
|
|
82
|
-
let pythonDetected = false, pythonVersion = null;
|
|
83
|
-
try {
|
|
84
|
-
const { createRequire } = await import('module');
|
|
85
|
-
const r = createRequire(import.meta.url);
|
|
86
|
-
const serverTTS = r('webtalk/server-tts');
|
|
87
|
-
if (typeof serverTTS.detectPython === 'function') {
|
|
88
|
-
const py = serverTTS.detectPython();
|
|
89
|
-
pythonDetected = py.found;
|
|
90
|
-
pythonVersion = py.version || null;
|
|
91
|
-
}
|
|
92
|
-
} catch {}
|
|
93
87
|
return {
|
|
94
|
-
...base,
|
|
88
|
+
...base,
|
|
95
89
|
setupMessage: base.ttsReady ? 'pocket-tts ready' : 'Will setup on first TTS request',
|
|
96
90
|
modelsDownloading: modelDownloadState.downloading,
|
|
97
91
|
modelsComplete: modelDownloadState.complete,
|
|
@@ -275,4 +269,30 @@ export function register(router, deps) {
|
|
|
275
269
|
err(500, e.message);
|
|
276
270
|
}
|
|
277
271
|
});
|
|
272
|
+
|
|
273
|
+
router.handle('agent.subagents', async (p) => {
|
|
274
|
+
const { id } = p;
|
|
275
|
+
if (!id) err(400, 'Missing agent id');
|
|
276
|
+
|
|
277
|
+
// Map CLI agent IDs to their corresponding plugin sub-agents
|
|
278
|
+
const subAgentMap = {
|
|
279
|
+
'cli-claude': [{ id: 'gm-cc', name: 'GM Claude' }],
|
|
280
|
+
'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
|
|
281
|
+
'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
|
|
282
|
+
'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
|
|
283
|
+
'cli-codex': []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const subAgents = subAgentMap[id] || [];
|
|
287
|
+
|
|
288
|
+
// Filter to only installed sub-agents
|
|
289
|
+
try {
|
|
290
|
+
const tools = await toolManager.getAllToolsAsync();
|
|
291
|
+
const installedPlugins = new Set(tools.filter(t => t.category === 'plugin' && t.installed).map(t => t.id));
|
|
292
|
+
const availableSubAgents = subAgents.filter(sa => installedPlugins.has(sa.id));
|
|
293
|
+
return { subAgents: availableSubAgents };
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return { subAgents };
|
|
296
|
+
}
|
|
297
|
+
});
|
|
278
298
|
}
|