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.
@@ -1,621 +1,99 @@
1
- import { spawn, execSync } from 'child_process';
2
- import os from 'os';
3
- import fs from 'fs';
4
- import path from 'path';
5
-
6
- const isWindows = os.platform() === 'win32';
7
- const TOOLS = [
8
- { id: 'cli-claude', name: 'Claude Code', pkg: '@anthropic-ai/claude-code', category: 'cli' },
9
- { id: 'cli-opencode', name: 'OpenCode', pkg: 'opencode-ai', category: 'cli' },
10
- { id: 'cli-gemini', name: 'Gemini CLI', pkg: '@google/gemini-cli', category: 'cli' },
11
- { id: 'cli-kilo', name: 'Kilo Code', pkg: '@kilocode/cli', category: 'cli' },
12
- { id: 'cli-codex', name: 'Codex CLI', pkg: '@openai/codex', category: 'cli' },
13
- { id: 'cli-agent-browser', name: 'Agent Browser', pkg: 'agent-browser', category: 'cli' },
14
- { id: 'gm-cc', name: 'GM Claude', pkg: 'gm-cc', installPkg: 'gm-cc@latest', pluginId: 'gm-cc', category: 'plugin', frameWork: 'claude' },
15
- { id: 'gm-oc', name: 'GM OpenCode', pkg: 'gm-oc', installPkg: 'gm-oc@latest', pluginId: 'gm', category: 'plugin', frameWork: 'opencode' },
16
- { id: 'gm-gc', name: 'GM Gemini', pkg: 'gm-gc', installPkg: 'gm-gc@latest', pluginId: 'gm', category: 'plugin', frameWork: 'gemini' },
17
- { id: 'gm-kilo', name: 'GM Kilo', pkg: 'gm-kilo', installPkg: 'gm-kilo@latest', pluginId: 'gm', category: 'plugin', frameWork: 'kilo' },
18
- { id: 'gm-codex', name: 'GM Codex', pkg: 'gm-codex', installPkg: 'gm-codex@latest', pluginId: 'gm', category: 'plugin', frameWork: 'codex' },
19
- ];
20
-
21
- const statusCache = new Map();
22
- const installLocks = new Map();
23
- const versionCache = new Map();
24
-
25
- const getTool = (id) => TOOLS.find(t => t.id === id);
26
-
27
- const getInstalledVersion = (pkg, pluginId = null, frameWork = null) => {
28
- try {
29
- const homeDir = os.homedir();
30
- const tool = TOOLS.find(t => t.pkg === pkg);
31
- const actualPluginId = pluginId || tool?.pluginId || pkg;
32
- const actualFrameWork = frameWork || tool?.frameWork;
33
-
34
- // Check Claude Code plugins using correct pluginId
35
- if (!frameWork || frameWork === 'claude') {
36
- const claudePath = path.join(homeDir, '.claude', 'plugins', actualPluginId, 'plugin.json');
37
- if (fs.existsSync(claudePath)) {
38
- try {
39
- const pluginJson = JSON.parse(fs.readFileSync(claudePath, 'utf-8'));
40
- if (pluginJson.version) return pluginJson.version;
41
- } catch (e) {
42
- console.warn(`[tool-manager] Failed to parse ${claudePath}:`, e.message);
43
- }
44
- }
45
- }
46
-
47
- // Check OpenCode agents using correct pluginId (stored as .md files)
48
- if (!frameWork || frameWork === 'opencode') {
49
- const opencodePath = path.join(homeDir, '.config', 'opencode', 'agents', actualPluginId + '.md');
50
- if (fs.existsSync(opencodePath)) {
51
- // Try to extract version from markdown front matter or try plugin.json in agent dir
52
- try {
53
- const agentDirPath = path.join(homeDir, '.config', 'opencode', 'agents', actualPluginId, 'plugin.json');
54
- if (fs.existsSync(agentDirPath)) {
55
- const pluginJson = JSON.parse(fs.readFileSync(agentDirPath, 'utf-8'));
56
- if (pluginJson.version) return pluginJson.version;
57
- }
58
- } catch (e) {
59
- // Fallback: skip
60
- }
61
- // For multi-framework bundles, try npm package.json in cache
62
- try {
63
- const pkgJsonPath = path.join(homeDir, '.gmweb/cache/.bun/install/cache');
64
- const cacheDirs = fs.readdirSync(pkgJsonPath).filter(d => d.startsWith(pkg + '@'));
65
- // Sort by version (get latest)
66
- const latestDir = cacheDirs.sort().reverse()[0];
67
- if (latestDir) {
68
- const pkgJsonFile = path.join(pkgJsonPath, latestDir, 'package.json');
69
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
70
- if (pkgJson.version) return pkgJson.version;
71
- }
72
- } catch (e) {
73
- // Fallback
74
- }
75
- // Last resort: try to extract from package name patterns
76
- return 'installed';
77
- }
78
- }
79
-
80
- // Check Gemini CLI agents (stored as 'gm' directory with gemini-extension.json)
81
- if (!frameWork || frameWork === 'gemini') {
82
- const geminiExtPath = path.join(homeDir, '.gemini', 'extensions', actualPluginId, 'gemini-extension.json');
83
- if (fs.existsSync(geminiExtPath)) {
84
- try {
85
- const extJson = JSON.parse(fs.readFileSync(geminiExtPath, 'utf-8'));
86
- if (extJson.version) return extJson.version;
87
- } catch (e) {
88
- console.warn(`[tool-manager] Failed to parse ${geminiExtPath}:`, e.message);
89
- }
90
- }
91
- }
92
-
93
- // Check Kilo agents (stored as .md files)
94
- if (!frameWork || frameWork === 'kilo') {
95
- const kiloPath = path.join(homeDir, '.config', 'kilo', 'agents', actualPluginId + '.md');
96
- if (fs.existsSync(kiloPath)) {
97
- // Try to extract version from markdown front matter or try plugin.json in agent dir
98
- try {
99
- const agentDirPath = path.join(homeDir, '.config', 'kilo', 'agents', actualPluginId, 'plugin.json');
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 };