agentgui 1.0.714 → 1.0.716
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/CLAUDE.md +20 -7
- package/lib/acp-protocol.js +91 -0
- package/lib/acp-runner.js +136 -0
- package/lib/acp-sdk-manager.js +20 -64
- package/lib/agent-descriptors.js +47 -332
- package/lib/agent-registry-configs.js +125 -0
- package/lib/claude-runner.js +189 -1247
- package/lib/jsonl-watcher.js +61 -35
- package/lib/plugin-loader.js +3 -15
- package/lib/tool-manager.js +99 -621
- package/lib/tool-provisioner.js +93 -0
- package/lib/tool-spawner.js +121 -0
- package/lib/tool-version.js +196 -0
- package/lib/ws-handlers-conv.js +5 -198
- package/lib/ws-handlers-msg.js +119 -0
- package/lib/ws-handlers-oauth.js +76 -0
- package/lib/ws-handlers-queue.js +56 -0
- package/lib/ws-handlers-scripts.js +58 -0
- package/lib/ws-handlers-util.js +22 -206
- package/package.json +1 -1
- package/server.js +21 -3
- package/static/js/client.js +34 -35
- package/static/js/streaming-renderer.js +30 -49
package/lib/tool-manager.js
CHANGED
|
@@ -1,621 +1,99 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{ id: 'cli-
|
|
9
|
-
{ id: 'cli-
|
|
10
|
-
{ id: 'cli-
|
|
11
|
-
{ id: 'cli-
|
|
12
|
-
{ id: '
|
|
13
|
-
{ id: '
|
|
14
|
-
{ id: 'gm-
|
|
15
|
-
{ id: 'gm-
|
|
16
|
-
{ id: 'gm-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (fs.existsSync(agentDirPath)) {
|
|
101
|
-
const pluginJson = JSON.parse(fs.readFileSync(agentDirPath, 'utf-8'));
|
|
102
|
-
if (pluginJson.version) return pluginJson.version;
|
|
103
|
-
}
|
|
104
|
-
} catch (e) {
|
|
105
|
-
// Fallback: skip
|
|
106
|
-
}
|
|
107
|
-
// For multi-framework bundles, try npm package.json in cache
|
|
108
|
-
try {
|
|
109
|
-
const pkgJsonPath = path.join(homeDir, '.gmweb/cache/.bun/install/cache');
|
|
110
|
-
const cacheDirs = fs.readdirSync(pkgJsonPath).filter(d => d.startsWith(pkg + '@'));
|
|
111
|
-
// Sort by version (get latest)
|
|
112
|
-
const latestDir = cacheDirs.sort().reverse()[0];
|
|
113
|
-
if (latestDir) {
|
|
114
|
-
const pkgJsonFile = path.join(pkgJsonPath, latestDir, 'package.json');
|
|
115
|
-
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
|
|
116
|
-
if (pkgJson.version) return pkgJson.version;
|
|
117
|
-
}
|
|
118
|
-
} catch (e) {
|
|
119
|
-
// Fallback
|
|
120
|
-
}
|
|
121
|
-
// Last resort: try to extract from package name patterns
|
|
122
|
-
return 'installed';
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Check Codex CLI (stored at ~/.codex)
|
|
127
|
-
if (!frameWork || frameWork === 'codex') {
|
|
128
|
-
const codexPluginPath = path.join(homeDir, '.codex', 'plugins', actualPluginId, 'plugin.json');
|
|
129
|
-
if (fs.existsSync(codexPluginPath)) {
|
|
130
|
-
try {
|
|
131
|
-
const pluginJson = JSON.parse(fs.readFileSync(codexPluginPath, 'utf-8'));
|
|
132
|
-
if (pluginJson.version) return pluginJson.version;
|
|
133
|
-
} catch (e) {
|
|
134
|
-
console.warn(`[tool-manager] Failed to parse ${codexPluginPath}:`, e.message);
|
|
135
|
-
}
|
|
136
|
-
return 'installed';
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} catch (_) {}
|
|
140
|
-
return null;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const getPublishedVersion = async (pkg) => {
|
|
144
|
-
const cacheKey = `published-${pkg}`;
|
|
145
|
-
const cached = versionCache.get(cacheKey);
|
|
146
|
-
// Use very aggressive caching - 24 hours
|
|
147
|
-
if (cached && Date.now() - cached.timestamp < 86400000) {
|
|
148
|
-
return cached.version;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Return null immediately if npm view would block - never block on published versions
|
|
152
|
-
// The server should prioritize installed detection over update availability
|
|
153
|
-
return null;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const checkCliInstalled = (pkg) => {
|
|
157
|
-
try {
|
|
158
|
-
const cmd = isWindows ? 'where' : 'which';
|
|
159
|
-
const binMap = { '@anthropic-ai/claude-code': 'claude', 'opencode-ai': 'opencode', '@google/gemini-cli': 'gemini', '@kilocode/cli': 'kilo', '@openai/codex': 'codex', 'agent-browser': 'agent-browser' };
|
|
160
|
-
const bin = binMap[pkg];
|
|
161
|
-
if (bin) {
|
|
162
|
-
execSync(`${cmd} ${bin}`, { stdio: 'pipe', timeout: 3000, windowsHide: true });
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
} catch (_) {}
|
|
166
|
-
return false;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const getCliVersion = (pkg) => {
|
|
170
|
-
try {
|
|
171
|
-
const binMap = { '@anthropic-ai/claude-code': 'claude', 'opencode-ai': 'opencode', '@google/gemini-cli': 'gemini', '@kilocode/cli': 'kilo', '@openai/codex': 'codex', 'agent-browser': 'agent-browser' };
|
|
172
|
-
const bin = binMap[pkg];
|
|
173
|
-
if (bin) {
|
|
174
|
-
try {
|
|
175
|
-
// Use short timeout - we already know the binary exists from checkCliInstalled
|
|
176
|
-
// agent-browser uses -V (--version prints help); others use --version
|
|
177
|
-
const versionFlag = pkg === 'agent-browser' ? '-V' : '--version';
|
|
178
|
-
const out = execSync(`${bin} ${versionFlag}`, { stdio: 'pipe', timeout: 1000, encoding: 'utf8', windowsHide: true });
|
|
179
|
-
const match = out.match(/(\d+\.\d+\.\d+)/);
|
|
180
|
-
if (match) {
|
|
181
|
-
console.log(`[tool-manager] CLI ${pkg} (${bin}) version: ${match[1]}`);
|
|
182
|
-
return match[1];
|
|
183
|
-
}
|
|
184
|
-
} catch (err) {
|
|
185
|
-
// If version detection times out or fails, return null (binary exists but version unknown)
|
|
186
|
-
console.log(`[tool-manager] CLI ${pkg} (${bin}) version detection failed: ${err.message.split('\n')[0]}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.log(`[tool-manager] Error in getCliVersion for ${pkg}:`, err.message);
|
|
191
|
-
}
|
|
192
|
-
return null;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
const checkToolInstalled = (pluginId, frameWork = null) => {
|
|
196
|
-
try {
|
|
197
|
-
const homeDir = os.homedir();
|
|
198
|
-
if (!frameWork || frameWork === 'claude') {
|
|
199
|
-
if (fs.existsSync(path.join(homeDir, '.claude', 'plugins', pluginId))) return true;
|
|
200
|
-
}
|
|
201
|
-
if (!frameWork || frameWork === 'gemini') {
|
|
202
|
-
if (fs.existsSync(path.join(homeDir, '.gemini', 'extensions', pluginId))) return true;
|
|
203
|
-
}
|
|
204
|
-
if (!frameWork || frameWork === 'opencode') {
|
|
205
|
-
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', pluginId + '.md'))) return true;
|
|
206
|
-
if (fs.existsSync(path.join(homeDir, '.config', 'opencode', 'agents', pluginId))) return true;
|
|
207
|
-
}
|
|
208
|
-
if (!frameWork || frameWork === 'kilo') {
|
|
209
|
-
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pluginId + '.md'))) return true;
|
|
210
|
-
if (fs.existsSync(path.join(homeDir, '.config', 'kilo', 'agents', pluginId))) return true;
|
|
211
|
-
}
|
|
212
|
-
if (!frameWork || frameWork === 'codex') {
|
|
213
|
-
if (fs.existsSync(path.join(homeDir, '.codex', 'plugins', pluginId))) return true;
|
|
214
|
-
}
|
|
215
|
-
} catch (_) {}
|
|
216
|
-
return false;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const compareVersions = (v1, v2) => {
|
|
220
|
-
if (!v1 || !v2) return false;
|
|
221
|
-
const parts1 = v1.split('.').map(Number);
|
|
222
|
-
const parts2 = v2.split('.').map(Number);
|
|
223
|
-
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
224
|
-
const p1 = parts1[i] || 0;
|
|
225
|
-
const p2 = parts2[i] || 0;
|
|
226
|
-
if (p1 < p2) return true;
|
|
227
|
-
if (p1 > p2) return false;
|
|
228
|
-
}
|
|
229
|
-
return false;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const checkToolViaBunx = async (pkg, pluginId = null, category = 'plugin', frameWork = null, skipPublishedVersion = false) => {
|
|
233
|
-
try {
|
|
234
|
-
const isCli = category === 'cli';
|
|
235
|
-
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
|
|
236
|
-
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork);
|
|
237
|
-
|
|
238
|
-
// Skip published version check if requested (for faster initial detection during startup)
|
|
239
|
-
let publishedVersion = null;
|
|
240
|
-
if (!skipPublishedVersion) {
|
|
241
|
-
publishedVersion = await getPublishedVersion(pkg);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const needsUpdate = installed && publishedVersion && compareVersions(installedVersion, publishedVersion);
|
|
245
|
-
const isUpToDate = installed && !needsUpdate;
|
|
246
|
-
return { installed, isUpToDate, upgradeNeeded: needsUpdate, output: 'version-check', installedVersion, publishedVersion };
|
|
247
|
-
} catch (err) {
|
|
248
|
-
console.log(`[tool-manager] Error checking ${pkg}:`, err.message);
|
|
249
|
-
const isCli = category === 'cli';
|
|
250
|
-
const installed = isCli ? checkCliInstalled(pkg) : checkToolInstalled(pluginId || pkg, frameWork);
|
|
251
|
-
const installedVersion = isCli ? getCliVersion(pkg) : getInstalledVersion(pkg, pluginId, frameWork);
|
|
252
|
-
return { installed, isUpToDate: false, upgradeNeeded: false, output: '', installedVersion, publishedVersion: null };
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
export function checkToolStatus(toolId) {
|
|
257
|
-
const tool = getTool(toolId);
|
|
258
|
-
if (!tool) return null;
|
|
259
|
-
|
|
260
|
-
const cached = statusCache.get(toolId);
|
|
261
|
-
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
262
|
-
return {
|
|
263
|
-
toolId,
|
|
264
|
-
installed: cached.installed,
|
|
265
|
-
isUpToDate: cached.isUpToDate,
|
|
266
|
-
upgradeNeeded: cached.upgradeNeeded,
|
|
267
|
-
timestamp: cached.timestamp
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return { toolId, installed: false, isUpToDate: false, upgradeNeeded: false, timestamp: Date.now() };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export async function checkToolStatusAsync(toolId, skipPublishedVersion = true) {
|
|
275
|
-
const tool = getTool(toolId);
|
|
276
|
-
if (!tool) return null;
|
|
277
|
-
|
|
278
|
-
const cached = statusCache.get(toolId);
|
|
279
|
-
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
280
|
-
return {
|
|
281
|
-
toolId,
|
|
282
|
-
installed: cached.installed,
|
|
283
|
-
isUpToDate: cached.isUpToDate,
|
|
284
|
-
upgradeNeeded: cached.upgradeNeeded,
|
|
285
|
-
installedVersion: cached.installedVersion,
|
|
286
|
-
publishedVersion: cached.publishedVersion,
|
|
287
|
-
timestamp: cached.timestamp
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Skip published version check by default for faster responses during initial tool detection
|
|
292
|
-
const result = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, skipPublishedVersion);
|
|
293
|
-
const status = {
|
|
294
|
-
toolId,
|
|
295
|
-
category: tool.category,
|
|
296
|
-
installed: result.installed,
|
|
297
|
-
isUpToDate: result.isUpToDate,
|
|
298
|
-
upgradeNeeded: result.upgradeNeeded,
|
|
299
|
-
installedVersion: result.installedVersion,
|
|
300
|
-
publishedVersion: result.publishedVersion,
|
|
301
|
-
timestamp: Date.now()
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
statusCache.set(toolId, status);
|
|
305
|
-
return status;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export async function checkForUpdates(toolId) {
|
|
309
|
-
const tool = getTool(toolId);
|
|
310
|
-
if (!tool) return { needsUpdate: false };
|
|
311
|
-
|
|
312
|
-
const status = await checkToolStatusAsync(toolId);
|
|
313
|
-
return { needsUpdate: status.upgradeNeeded && status.installed };
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const spawnNpmInstall = (pkg, onProgress) => new Promise((resolve) => {
|
|
317
|
-
const cmd = isWindows ? 'npm.cmd' : 'npm';
|
|
318
|
-
let completed = false, stderr = '', stdout = '';
|
|
319
|
-
let proc;
|
|
320
|
-
try {
|
|
321
|
-
proc = spawn(cmd, ['install', '-g', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows, windowsHide: true });
|
|
322
|
-
} catch (err) {
|
|
323
|
-
return resolve({ success: false, error: `Failed to spawn npm install: ${err.message}` });
|
|
324
|
-
}
|
|
325
|
-
if (!proc) return resolve({ success: false, error: 'Failed to spawn npm process' });
|
|
326
|
-
const timer = setTimeout(() => { if (!completed) { completed = true; try { proc.kill('SIGKILL'); } catch (_) {} resolve({ success: false, error: 'Timeout (5min)' }); } }, 300000);
|
|
327
|
-
const onData = (d) => { if (onProgress) onProgress({ type: 'progress', data: d.toString() }); };
|
|
328
|
-
if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d.toString(); onData(d); });
|
|
329
|
-
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d.toString(); onData(d); });
|
|
330
|
-
proc.on('close', (code) => {
|
|
331
|
-
clearTimeout(timer);
|
|
332
|
-
if (completed) return;
|
|
333
|
-
completed = true;
|
|
334
|
-
resolve(code === 0 ? { success: true, error: null, pkg } : { success: false, error: (stdout + stderr).substring(0, 1000) || 'Failed' });
|
|
335
|
-
});
|
|
336
|
-
proc.on('error', (err) => { clearTimeout(timer); if (!completed) { completed = true; resolve({ success: false, error: err.message }); } });
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
const spawnBunxProc = (pkg, onProgress) => new Promise((resolve) => {
|
|
340
|
-
const cmd = isWindows ? 'bun.cmd' : 'bun';
|
|
341
|
-
let completed = false, stderr = '', stdout = '';
|
|
342
|
-
let lastDataTime = Date.now();
|
|
343
|
-
let proc;
|
|
344
|
-
|
|
345
|
-
try {
|
|
346
|
-
proc = spawn(cmd, ['x', pkg], { stdio: ['pipe', 'pipe', 'pipe'], timeout: 300000, shell: isWindows, windowsHide: true });
|
|
347
|
-
} catch (err) {
|
|
348
|
-
return resolve({ success: false, error: `Failed to spawn bun x: ${err.message}` });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (!proc) {
|
|
352
|
-
return resolve({ success: false, error: 'Failed to spawn bun x process' });
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const timer = setTimeout(() => {
|
|
356
|
-
if (!completed) {
|
|
357
|
-
completed = true;
|
|
358
|
-
try { proc.kill('SIGKILL'); } catch (_) {}
|
|
359
|
-
resolve({ success: false, error: 'Timeout (5min)' });
|
|
360
|
-
}
|
|
361
|
-
}, 300000);
|
|
362
|
-
|
|
363
|
-
const heartbeatTimer = setInterval(() => {
|
|
364
|
-
if (completed) { clearInterval(heartbeatTimer); return; }
|
|
365
|
-
const timeSinceLastData = Date.now() - lastDataTime;
|
|
366
|
-
if (timeSinceLastData > 30000) {
|
|
367
|
-
console.warn(`[tool-manager] No output from bun x ${pkg} for ${timeSinceLastData}ms - process may be hung`);
|
|
368
|
-
}
|
|
369
|
-
}, 30000);
|
|
370
|
-
|
|
371
|
-
const onData = (d) => {
|
|
372
|
-
lastDataTime = Date.now();
|
|
373
|
-
if (onProgress) onProgress({ type: 'progress', data: d.toString() });
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d.toString(); onData(d); });
|
|
377
|
-
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d.toString(); onData(d); });
|
|
378
|
-
|
|
379
|
-
proc.on('close', (code) => {
|
|
380
|
-
clearTimeout(timer);
|
|
381
|
-
clearInterval(heartbeatTimer);
|
|
382
|
-
if (completed) return;
|
|
383
|
-
completed = true;
|
|
384
|
-
const output = stdout + stderr;
|
|
385
|
-
const successPatterns = [
|
|
386
|
-
code === 0,
|
|
387
|
-
output.includes('upgraded'),
|
|
388
|
-
output.includes('registered'),
|
|
389
|
-
output.includes('Hooks registered'),
|
|
390
|
-
output.includes('successfully'),
|
|
391
|
-
output.includes('Done'),
|
|
392
|
-
code === 0 && !output.includes('error')
|
|
393
|
-
];
|
|
394
|
-
if (successPatterns.some(p => p)) {
|
|
395
|
-
resolve({ success: true, error: null, pkg });
|
|
396
|
-
} else {
|
|
397
|
-
resolve({ success: false, error: output.substring(0, 1000) || 'Failed' });
|
|
398
|
-
}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
proc.on('error', (err) => {
|
|
402
|
-
clearTimeout(timer);
|
|
403
|
-
clearInterval(heartbeatTimer);
|
|
404
|
-
if (!completed) {
|
|
405
|
-
completed = true;
|
|
406
|
-
resolve({ success: false, error: `Process error: ${err.message}` });
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
const spawnForTool = (tool, onProgress) => {
|
|
412
|
-
const pkg = tool.installPkg || tool.pkg;
|
|
413
|
-
return tool.category === 'cli' ? spawnNpmInstall(pkg, onProgress) : spawnBunxProc(pkg, onProgress);
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
export async function install(toolId, onProgress) {
|
|
417
|
-
const tool = getTool(toolId);
|
|
418
|
-
if (!tool) return { success: false, error: 'Tool not found' };
|
|
419
|
-
if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
|
|
420
|
-
installLocks.set(toolId, true);
|
|
421
|
-
try {
|
|
422
|
-
const result = await spawnForTool(tool, onProgress);
|
|
423
|
-
if (result.success) {
|
|
424
|
-
await new Promise(r => setTimeout(r, 500));
|
|
425
|
-
statusCache.delete(toolId);
|
|
426
|
-
versionCache.clear();
|
|
427
|
-
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId, tool.frameWork);
|
|
428
|
-
const freshStatus = await checkToolStatusAsync(toolId);
|
|
429
|
-
return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
|
|
430
|
-
}
|
|
431
|
-
return result;
|
|
432
|
-
} finally {
|
|
433
|
-
installLocks.delete(toolId);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export async function update(toolId, onProgress) {
|
|
438
|
-
const tool = getTool(toolId);
|
|
439
|
-
if (!tool) return { success: false, error: 'Tool not found' };
|
|
440
|
-
const current = await checkToolStatusAsync(toolId);
|
|
441
|
-
if (!current?.installed) return { success: false, error: 'Tool not installed' };
|
|
442
|
-
if (installLocks.get(toolId)) return { success: false, error: 'Install in progress' };
|
|
443
|
-
|
|
444
|
-
installLocks.set(toolId, true);
|
|
445
|
-
try {
|
|
446
|
-
const result = await spawnForTool(tool, onProgress);
|
|
447
|
-
if (result.success) {
|
|
448
|
-
await new Promise(r => setTimeout(r, 500));
|
|
449
|
-
statusCache.delete(toolId);
|
|
450
|
-
versionCache.clear();
|
|
451
|
-
const version = tool.category === 'cli' ? getCliVersion(tool.pkg) : getInstalledVersion(tool.pkg, tool.pluginId, tool.frameWork);
|
|
452
|
-
const freshStatus = await checkToolStatusAsync(toolId);
|
|
453
|
-
return { success: true, error: null, version: version || freshStatus.publishedVersion || 'unknown', ...freshStatus };
|
|
454
|
-
}
|
|
455
|
-
return result;
|
|
456
|
-
} finally {
|
|
457
|
-
installLocks.delete(toolId);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function getAllTools() {
|
|
462
|
-
return TOOLS.map(tool => {
|
|
463
|
-
const cached = statusCache.get(tool.id);
|
|
464
|
-
return {
|
|
465
|
-
...tool,
|
|
466
|
-
toolId: tool.id,
|
|
467
|
-
installed: cached?.installed ?? false,
|
|
468
|
-
isUpToDate: cached?.isUpToDate ?? false,
|
|
469
|
-
upgradeNeeded: cached?.upgradeNeeded ?? false,
|
|
470
|
-
installedVersion: cached?.installedVersion ?? null,
|
|
471
|
-
publishedVersion: cached?.publishedVersion ?? null,
|
|
472
|
-
timestamp: cached?.timestamp ?? 0
|
|
473
|
-
};
|
|
474
|
-
});
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
export async function getAllToolsAsync(skipPublishedVersion = false) {
|
|
478
|
-
const results = await Promise.all(TOOLS.map(tool => checkToolStatusAsync(tool.id, skipPublishedVersion)));
|
|
479
|
-
return results.map((status, idx) => ({
|
|
480
|
-
...TOOLS[idx],
|
|
481
|
-
...status
|
|
482
|
-
}));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export function clearStatusCache() {
|
|
486
|
-
statusCache.clear();
|
|
487
|
-
versionCache.clear();
|
|
488
|
-
console.log('[tool-manager] Caches cleared, forcing fresh tool detection');
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export async function refreshAllToolsAsync() {
|
|
492
|
-
clearStatusCache();
|
|
493
|
-
return getAllToolsAsync();
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export function getAllToolsSync() {
|
|
497
|
-
return TOOLS.map(tool => {
|
|
498
|
-
const cached = statusCache.get(tool.id);
|
|
499
|
-
return { ...tool, ...cached };
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
export function getToolConfig(toolId) {
|
|
504
|
-
return getTool(toolId) || null;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
export async function autoProvision(broadcast) {
|
|
508
|
-
const log = (msg) => console.log('[TOOLS-AUTO] ' + msg);
|
|
509
|
-
log('Starting background tool provisioning...');
|
|
510
|
-
for (const tool of TOOLS) {
|
|
511
|
-
try {
|
|
512
|
-
// Skip published version check initially for faster startup - agents need to be available immediately
|
|
513
|
-
const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, true);
|
|
514
|
-
statusCache.set(tool.id, { ...status, toolId: tool.id, timestamp: Date.now() });
|
|
515
|
-
if (!status.installed) {
|
|
516
|
-
log(`${tool.id} not installed, installing...`);
|
|
517
|
-
broadcast({ type: 'tool_install_started', toolId: tool.id });
|
|
518
|
-
const result = await install(tool.id, (msg) => {
|
|
519
|
-
broadcast({ type: 'tool_install_progress', toolId: tool.id, data: msg });
|
|
520
|
-
});
|
|
521
|
-
if (result.success) {
|
|
522
|
-
log(`${tool.id} installed v${result.version}`);
|
|
523
|
-
broadcast({ type: 'tool_install_complete', toolId: tool.id, data: result });
|
|
524
|
-
} else {
|
|
525
|
-
log(`${tool.id} install failed: ${result.error}`);
|
|
526
|
-
broadcast({ type: 'tool_install_failed', toolId: tool.id, data: result });
|
|
527
|
-
}
|
|
528
|
-
} else if (status.upgradeNeeded) {
|
|
529
|
-
log(`${tool.id} needs update (${status.installedVersion} -> ${status.publishedVersion})`);
|
|
530
|
-
broadcast({ type: 'tool_install_started', toolId: tool.id });
|
|
531
|
-
const result = await update(tool.id, (msg) => {
|
|
532
|
-
broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
|
|
533
|
-
});
|
|
534
|
-
if (result.success) {
|
|
535
|
-
log(`${tool.id} updated to v${result.version}`);
|
|
536
|
-
broadcast({ type: 'tool_update_complete', toolId: tool.id, data: result });
|
|
537
|
-
} else {
|
|
538
|
-
log(`${tool.id} update failed: ${result.error}`);
|
|
539
|
-
broadcast({ type: 'tool_update_failed', toolId: tool.id, data: result });
|
|
540
|
-
}
|
|
541
|
-
} else {
|
|
542
|
-
log(`${tool.id} v${status.installedVersion} up-to-date`);
|
|
543
|
-
broadcast({ type: 'tool_status_update', toolId: tool.id, data: { installed: true, isUpToDate: true, installedVersion: status.installedVersion, status: 'installed' } });
|
|
544
|
-
}
|
|
545
|
-
} catch (err) {
|
|
546
|
-
log(`${tool.id} error: ${err.message}`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
log('Provisioning complete');
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Periodic tool update checker - runs in background every 6 hours
|
|
553
|
-
let updateCheckInterval = null;
|
|
554
|
-
const UPDATE_CHECK_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
|
555
|
-
|
|
556
|
-
export function startPeriodicUpdateCheck(broadcast) {
|
|
557
|
-
const log = (msg) => console.log('[TOOLS-PERIODIC] ' + msg);
|
|
558
|
-
|
|
559
|
-
if (updateCheckInterval) {
|
|
560
|
-
log('Update check already running');
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
log('Starting periodic tool update checker (every 6 hours)');
|
|
565
|
-
|
|
566
|
-
// Run check immediately on startup (non-blocking)
|
|
567
|
-
setImmediate(() => {
|
|
568
|
-
checkAndUpdateTools(broadcast).catch(err => {
|
|
569
|
-
log(`Initial check failed: ${err.message}`);
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
// Then run periodically every 6 hours
|
|
574
|
-
updateCheckInterval = setInterval(() => {
|
|
575
|
-
checkAndUpdateTools(broadcast).catch(err => {
|
|
576
|
-
log(`Periodic check failed: ${err.message}`);
|
|
577
|
-
});
|
|
578
|
-
}, UPDATE_CHECK_INTERVAL);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function stopPeriodicUpdateCheck() {
|
|
582
|
-
if (updateCheckInterval) {
|
|
583
|
-
clearInterval(updateCheckInterval);
|
|
584
|
-
updateCheckInterval = null;
|
|
585
|
-
console.log('[TOOLS-PERIODIC] Update check stopped');
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
async function checkAndUpdateTools(broadcast) {
|
|
590
|
-
const log = (msg) => console.log('[TOOLS-PERIODIC] ' + msg);
|
|
591
|
-
log('Checking for tool updates...');
|
|
592
|
-
|
|
593
|
-
for (const tool of TOOLS) {
|
|
594
|
-
try {
|
|
595
|
-
const status = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, false);
|
|
596
|
-
|
|
597
|
-
if (status.upgradeNeeded) {
|
|
598
|
-
log(`Update available for ${tool.id}: ${status.installedVersion} -> ${status.publishedVersion}`);
|
|
599
|
-
broadcast({ type: 'tool_update_available', toolId: tool.id, data: { installedVersion: status.installedVersion, publishedVersion: status.publishedVersion } });
|
|
600
|
-
|
|
601
|
-
// Auto-update in background (non-blocking)
|
|
602
|
-
log(`Auto-updating ${tool.id}...`);
|
|
603
|
-
const result = await update(tool.id, (msg) => {
|
|
604
|
-
broadcast({ type: 'tool_update_progress', toolId: tool.id, data: msg });
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
if (result.success) {
|
|
608
|
-
log(`${tool.id} auto-updated to v${result.version}`);
|
|
609
|
-
broadcast({ type: 'tool_update_complete', toolId: tool.id, data: { ...result, autoUpdated: true } });
|
|
610
|
-
} else {
|
|
611
|
-
log(`${tool.id} auto-update failed: ${result.error}`);
|
|
612
|
-
broadcast({ type: 'tool_update_failed', toolId: tool.id, data: { ...result, autoUpdated: true } });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
} catch (err) {
|
|
616
|
-
log(`Error checking ${tool.id}: ${err.message}`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
log('Update check complete');
|
|
621
|
-
}
|
|
1
|
+
import { checkToolViaBunx, clearVersionCache } from './tool-version.js';
|
|
2
|
+
import { createInstaller } from './tool-spawner.js';
|
|
3
|
+
import { autoProvision as _autoProvision, startPeriodicUpdateCheck as _startPeriodicUpdateCheck, stopPeriodicUpdateCheck } from './tool-provisioner.js';
|
|
4
|
+
|
|
5
|
+
const TOOLS = [
|
|
6
|
+
{ id: 'cli-claude', name: 'Claude Code', pkg: '@anthropic-ai/claude-code', category: 'cli' },
|
|
7
|
+
{ id: 'cli-opencode', name: 'OpenCode', pkg: 'opencode-ai', category: 'cli' },
|
|
8
|
+
{ id: 'cli-gemini', name: 'Gemini CLI', pkg: '@google/gemini-cli', category: 'cli' },
|
|
9
|
+
{ id: 'cli-kilo', name: 'Kilo Code', pkg: '@kilocode/cli', category: 'cli' },
|
|
10
|
+
{ id: 'cli-codex', name: 'Codex CLI', pkg: '@openai/codex', category: 'cli' },
|
|
11
|
+
{ id: 'cli-agent-browser', name: 'Agent Browser', pkg: 'agent-browser', category: 'cli' },
|
|
12
|
+
{ id: 'gm-cc', name: 'GM Claude', pkg: 'gm-cc', installPkg: 'gm-cc@latest', pluginId: 'gm-cc', category: 'plugin', frameWork: 'claude' },
|
|
13
|
+
{ id: 'gm-oc', name: 'GM OpenCode', pkg: 'gm-oc', installPkg: 'gm-oc@latest', pluginId: 'gm', category: 'plugin', frameWork: 'opencode' },
|
|
14
|
+
{ id: 'gm-gc', name: 'GM Gemini', pkg: 'gm-gc', installPkg: 'gm-gc@latest', pluginId: 'gm', category: 'plugin', frameWork: 'gemini' },
|
|
15
|
+
{ id: 'gm-kilo', name: 'GM Kilo', pkg: 'gm-kilo', installPkg: 'gm-kilo@latest', pluginId: 'gm', category: 'plugin', frameWork: 'kilo' },
|
|
16
|
+
{ id: 'gm-codex', name: 'GM Codex', pkg: 'gm-codex', installPkg: 'gm-codex@latest', pluginId: 'gm', category: 'plugin', frameWork: 'codex' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const statusCache = new Map();
|
|
20
|
+
const installLocks = new Map();
|
|
21
|
+
|
|
22
|
+
const getTool = (id) => TOOLS.find(t => t.id === id);
|
|
23
|
+
|
|
24
|
+
export function checkToolStatus(toolId) {
|
|
25
|
+
const tool = getTool(toolId);
|
|
26
|
+
if (!tool) return null;
|
|
27
|
+
const cached = statusCache.get(toolId);
|
|
28
|
+
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
29
|
+
return { toolId, installed: cached.installed, isUpToDate: cached.isUpToDate, upgradeNeeded: cached.upgradeNeeded, timestamp: cached.timestamp };
|
|
30
|
+
}
|
|
31
|
+
return { toolId, installed: false, isUpToDate: false, upgradeNeeded: false, timestamp: Date.now() };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function checkToolStatusAsync(toolId, skipPublishedVersion = true) {
|
|
35
|
+
const tool = getTool(toolId);
|
|
36
|
+
if (!tool) return null;
|
|
37
|
+
const cached = statusCache.get(toolId);
|
|
38
|
+
if (cached && Date.now() - cached.timestamp < 1800000) {
|
|
39
|
+
return { toolId, installed: cached.installed, isUpToDate: cached.isUpToDate, upgradeNeeded: cached.upgradeNeeded, installedVersion: cached.installedVersion, publishedVersion: cached.publishedVersion, timestamp: cached.timestamp };
|
|
40
|
+
}
|
|
41
|
+
const result = await checkToolViaBunx(tool.pkg, tool.pluginId, tool.category, tool.frameWork, skipPublishedVersion, TOOLS);
|
|
42
|
+
const status = { toolId, category: tool.category, installed: result.installed, isUpToDate: result.isUpToDate, upgradeNeeded: result.upgradeNeeded, installedVersion: result.installedVersion, publishedVersion: result.publishedVersion, timestamp: Date.now() };
|
|
43
|
+
statusCache.set(toolId, status);
|
|
44
|
+
return status;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function checkForUpdates(toolId) {
|
|
48
|
+
const tool = getTool(toolId);
|
|
49
|
+
if (!tool) return { needsUpdate: false };
|
|
50
|
+
const status = await checkToolStatusAsync(toolId);
|
|
51
|
+
return { needsUpdate: status.upgradeNeeded && status.installed };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { install, update } = createInstaller(getTool, installLocks, statusCache, checkToolStatusAsync);
|
|
55
|
+
export { install, update };
|
|
56
|
+
|
|
57
|
+
export function getAllTools() {
|
|
58
|
+
return TOOLS.map(tool => {
|
|
59
|
+
const cached = statusCache.get(tool.id);
|
|
60
|
+
return { ...tool, toolId: tool.id, installed: cached?.installed ?? false, isUpToDate: cached?.isUpToDate ?? false, upgradeNeeded: cached?.upgradeNeeded ?? false, installedVersion: cached?.installedVersion ?? null, publishedVersion: cached?.publishedVersion ?? null, timestamp: cached?.timestamp ?? 0 };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function getAllToolsAsync(skipPublishedVersion = false) {
|
|
65
|
+
const results = await Promise.all(TOOLS.map(tool => checkToolStatusAsync(tool.id, skipPublishedVersion)));
|
|
66
|
+
return results.map((status, idx) => ({ ...TOOLS[idx], ...status }));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function clearStatusCache() {
|
|
70
|
+
statusCache.clear();
|
|
71
|
+
clearVersionCache();
|
|
72
|
+
console.log('[tool-manager] Caches cleared, forcing fresh tool detection');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function refreshAllToolsAsync() {
|
|
76
|
+
clearStatusCache();
|
|
77
|
+
return getAllToolsAsync();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getAllToolsSync() {
|
|
81
|
+
return TOOLS.map(tool => {
|
|
82
|
+
const cached = statusCache.get(tool.id);
|
|
83
|
+
return { ...tool, ...cached };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getToolConfig(toolId) {
|
|
88
|
+
return getTool(toolId) || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function autoProvision(broadcast) {
|
|
92
|
+
return _autoProvision(TOOLS, statusCache, install, broadcast);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function startPeriodicUpdateCheck(broadcast) {
|
|
96
|
+
return _startPeriodicUpdateCheck(TOOLS, statusCache, update, broadcast);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { stopPeriodicUpdateCheck };
|