agent-tank 0.9.0
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/LICENSE +21 -0
- package/README.md +675 -0
- package/bin/agent-tank.js +301 -0
- package/media/www-preview.png +0 -0
- package/package.json +65 -0
- package/src/activity-monitor.js +312 -0
- package/src/agents/api-response-parser.js +255 -0
- package/src/agents/base.js +396 -0
- package/src/agents/claude.js +388 -0
- package/src/agents/codex-pty-helpers.js +226 -0
- package/src/agents/codex-rpc-helpers.js +167 -0
- package/src/agents/codex.js +383 -0
- package/src/agents/gemini.js +302 -0
- package/src/agents/keepalive-helper.js +115 -0
- package/src/agents/oauth-helper.js +210 -0
- package/src/agents/pty-output-parser.js +301 -0
- package/src/auto-refresh-manager.js +246 -0
- package/src/client-auto-refresh-helpers.js +143 -0
- package/src/client-auto-refresh.js +321 -0
- package/src/client-favicon.js +198 -0
- package/src/client-monitor.js +242 -0
- package/src/client-script.js +147 -0
- package/src/discovery.js +128 -0
- package/src/history-store.js +273 -0
- package/src/http-handler.js +182 -0
- package/src/icons.js +67 -0
- package/src/index.js +376 -0
- package/src/json-rpc-client.js +285 -0
- package/src/keepalive-manager.js +163 -0
- package/src/logger.js +213 -0
- package/src/pace-attachment.js +104 -0
- package/src/pace-evaluator.js +269 -0
- package/src/public-status.js +276 -0
- package/src/server.js +144 -0
- package/src/snapshot-metrics.js +79 -0
- package/src/status-page.css +1149 -0
- package/src/status-page.js +128 -0
- package/src/usage-formatters.js +259 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Client-side JavaScript for background monitoring feature
|
|
2
|
+
// This module exports a string containing the monitor tracking code
|
|
3
|
+
|
|
4
|
+
const monitorScript = `
|
|
5
|
+
// ===== Background Monitor Feature =====
|
|
6
|
+
// Monitoring is ALWAYS ON - "Automatic" tracks highest usage, "Manual" pins a specific metric
|
|
7
|
+
|
|
8
|
+
// Find the metric with the highest usage percentage across all providers
|
|
9
|
+
function findHighestUsageMetric() {
|
|
10
|
+
const allTrackBtns = document.querySelectorAll('.track-btn');
|
|
11
|
+
let highestMetric = null;
|
|
12
|
+
let highestPercent = -1;
|
|
13
|
+
|
|
14
|
+
allTrackBtns.forEach(btn => {
|
|
15
|
+
const percent = parseInt(btn.dataset.percent, 10);
|
|
16
|
+
if (percent > highestPercent) {
|
|
17
|
+
highestPercent = percent;
|
|
18
|
+
highestMetric = {
|
|
19
|
+
metricId: btn.dataset.metricId,
|
|
20
|
+
agent: btn.dataset.agent,
|
|
21
|
+
label: btn.dataset.label,
|
|
22
|
+
percent: percent,
|
|
23
|
+
color: btn.dataset.color,
|
|
24
|
+
resetsIn: btn.dataset.resetsIn
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return highestMetric;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Initialize monitor on page load - always on
|
|
33
|
+
function initMonitor() {
|
|
34
|
+
// Restore pinned metric from localStorage (if user manually pinned one)
|
|
35
|
+
const savedPinned = localStorage.getItem('pinnedMetric');
|
|
36
|
+
if (savedPinned) {
|
|
37
|
+
try {
|
|
38
|
+
pinnedMetric = JSON.parse(savedPinned);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
pinnedMetric = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
notificationSent = localStorage.getItem('notificationSent') === 'true';
|
|
44
|
+
|
|
45
|
+
// Save original favicon
|
|
46
|
+
const existingFavicon = document.querySelector('link[rel="icon"]');
|
|
47
|
+
if (existingFavicon) {
|
|
48
|
+
originalFavicon = existingFavicon.href;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Request notification permission on first load
|
|
52
|
+
if ('Notification' in window && Notification.permission === 'default') {
|
|
53
|
+
Notification.requestPermission();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Determine which metric to track
|
|
57
|
+
if (pinnedMetric) {
|
|
58
|
+
// Manual mode: check if pinned metric still exists
|
|
59
|
+
const trackBtn = document.querySelector(\`.track-btn[data-metric-id="\${pinnedMetric.metricId}"]\`);
|
|
60
|
+
const trackRow = document.querySelector(\`.usage-item[data-metric-id="\${pinnedMetric.metricId}"]\`);
|
|
61
|
+
if (trackBtn) {
|
|
62
|
+
trackBtn.classList.add('tracking');
|
|
63
|
+
if (trackRow) {
|
|
64
|
+
trackRow.classList.add('tracking');
|
|
65
|
+
}
|
|
66
|
+
// Update pinnedMetric with fresh data from the page
|
|
67
|
+
pinnedMetric.percent = parseInt(trackBtn.dataset.percent, 10);
|
|
68
|
+
pinnedMetric.color = trackBtn.dataset.color;
|
|
69
|
+
pinnedMetric.resetsIn = trackBtn.dataset.resetsIn;
|
|
70
|
+
trackedMetric = pinnedMetric;
|
|
71
|
+
localStorage.setItem('pinnedMetric', JSON.stringify(pinnedMetric));
|
|
72
|
+
} else {
|
|
73
|
+
// Pinned metric no longer exists, fall back to automatic
|
|
74
|
+
pinnedMetric = null;
|
|
75
|
+
localStorage.removeItem('pinnedMetric');
|
|
76
|
+
trackedMetric = findHighestUsageMetric();
|
|
77
|
+
highlightTrackedMetric();
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
// Automatic mode: track highest usage
|
|
81
|
+
trackedMetric = findHighestUsageMetric();
|
|
82
|
+
highlightTrackedMetric();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Update favicon and title
|
|
86
|
+
updateFaviconAndTitle();
|
|
87
|
+
checkAndNotify();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Highlight the currently tracked metric (for automatic mode)
|
|
91
|
+
function highlightTrackedMetric() {
|
|
92
|
+
if (!trackedMetric) return;
|
|
93
|
+
|
|
94
|
+
const trackBtn = document.querySelector(\`.track-btn[data-metric-id="\${trackedMetric.metricId}"]\`);
|
|
95
|
+
const trackRow = document.querySelector(\`.usage-item[data-metric-id="\${trackedMetric.metricId}"]\`);
|
|
96
|
+
if (trackBtn) {
|
|
97
|
+
trackBtn.classList.add('tracking');
|
|
98
|
+
if (trackRow) {
|
|
99
|
+
trackRow.classList.add('tracking');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Toggle pinning on a specific metric (manual mode)
|
|
105
|
+
function toggleTracking(btn) {
|
|
106
|
+
const metricId = btn.dataset.metricId;
|
|
107
|
+
|
|
108
|
+
// If already pinned to this metric, unpin it (go back to automatic mode)
|
|
109
|
+
if (pinnedMetric && pinnedMetric.metricId === metricId) {
|
|
110
|
+
// Clear pinning
|
|
111
|
+
clearPinning();
|
|
112
|
+
// Fall back to automatic mode - track highest
|
|
113
|
+
trackedMetric = findHighestUsageMetric();
|
|
114
|
+
highlightTrackedMetric();
|
|
115
|
+
updateFaviconAndTitle();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Clear previous pinned state
|
|
120
|
+
clearPinning();
|
|
121
|
+
|
|
122
|
+
// Pin to this new metric
|
|
123
|
+
btn.classList.add('tracking');
|
|
124
|
+
const parentRow = btn.closest('.usage-item');
|
|
125
|
+
if (parentRow) {
|
|
126
|
+
parentRow.classList.add('tracking');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pinnedMetric = {
|
|
130
|
+
metricId: metricId,
|
|
131
|
+
agent: btn.dataset.agent,
|
|
132
|
+
label: btn.dataset.label,
|
|
133
|
+
percent: parseInt(btn.dataset.percent, 10),
|
|
134
|
+
color: btn.dataset.color,
|
|
135
|
+
resetsIn: btn.dataset.resetsIn
|
|
136
|
+
};
|
|
137
|
+
trackedMetric = pinnedMetric;
|
|
138
|
+
|
|
139
|
+
// Reset notification state for new metric
|
|
140
|
+
notificationSent = false;
|
|
141
|
+
localStorage.setItem('notificationSent', 'false');
|
|
142
|
+
|
|
143
|
+
// Save to localStorage
|
|
144
|
+
localStorage.setItem('pinnedMetric', JSON.stringify(pinnedMetric));
|
|
145
|
+
|
|
146
|
+
// Update favicon and title
|
|
147
|
+
updateFaviconAndTitle();
|
|
148
|
+
checkAndNotify();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Clear pinned metric state (returns to automatic mode)
|
|
152
|
+
function clearPinning() {
|
|
153
|
+
const prevTracked = document.querySelector('.track-btn.tracking');
|
|
154
|
+
if (prevTracked) {
|
|
155
|
+
prevTracked.classList.remove('tracking');
|
|
156
|
+
}
|
|
157
|
+
const prevTrackedRow = document.querySelector('.usage-item.tracking');
|
|
158
|
+
if (prevTrackedRow) {
|
|
159
|
+
prevTrackedRow.classList.remove('tracking');
|
|
160
|
+
}
|
|
161
|
+
pinnedMetric = null;
|
|
162
|
+
localStorage.removeItem('pinnedMetric');
|
|
163
|
+
notificationSent = false;
|
|
164
|
+
localStorage.removeItem('notificationSent');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Toggle tracking from clicking on the entire row
|
|
168
|
+
function toggleTrackingFromRow(row) {
|
|
169
|
+
// Find the track button within this row and delegate to toggleTracking
|
|
170
|
+
const trackBtn = row.querySelector('.track-btn');
|
|
171
|
+
if (trackBtn) {
|
|
172
|
+
toggleTracking(trackBtn);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Update favicon with Canvas-generated vertical tank
|
|
177
|
+
function updateFaviconAndTitle() {
|
|
178
|
+
if (!trackedMetric) {
|
|
179
|
+
generateDefaultFavicon();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { percent, color, agent, label } = trackedMetric;
|
|
184
|
+
|
|
185
|
+
// Update page title: XX% Agent | Agent Tank (clean format for squeezed tabs)
|
|
186
|
+
const agentName = agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
187
|
+
document.title = \`\${percent}% \${agentName} | Agent Tank\`;
|
|
188
|
+
|
|
189
|
+
// Generate favicon using Canvas API
|
|
190
|
+
generateProgressFavicon(percent, color);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check and send notification if metric hits 90%
|
|
194
|
+
function checkAndNotify() {
|
|
195
|
+
if (!trackedMetric) return;
|
|
196
|
+
if (notificationSent) return; // Only notify once per metric
|
|
197
|
+
|
|
198
|
+
const { percent, agent, label } = trackedMetric;
|
|
199
|
+
|
|
200
|
+
if (percent >= 90) {
|
|
201
|
+
// Mark as notified to prevent duplicates
|
|
202
|
+
notificationSent = true;
|
|
203
|
+
localStorage.setItem('notificationSent', 'true');
|
|
204
|
+
|
|
205
|
+
// Send browser notification
|
|
206
|
+
if ('Notification' in window && Notification.permission === 'granted') {
|
|
207
|
+
const agentName = agent.charAt(0).toUpperCase() + agent.slice(1);
|
|
208
|
+
new Notification('Agent Tank Warning', {
|
|
209
|
+
body: \`\${agentName} \${label} is at \${percent}% usage!\`,
|
|
210
|
+
icon: '/favicon.ico',
|
|
211
|
+
tag: 'agent-tank-warning'
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Initialize monitor on page load
|
|
218
|
+
initMonitor();
|
|
219
|
+
|
|
220
|
+
// ===== API Endpoint Copy Feature =====
|
|
221
|
+
function copyApiEndpoint(event) {
|
|
222
|
+
event.preventDefault();
|
|
223
|
+
event.stopPropagation();
|
|
224
|
+
|
|
225
|
+
const apiUrl = window.location.origin + '/status';
|
|
226
|
+
const copyBtn = event.target.closest('.copy-api-btn');
|
|
227
|
+
|
|
228
|
+
navigator.clipboard.writeText(apiUrl).then(() => {
|
|
229
|
+
// Show "Copied!" feedback on button
|
|
230
|
+
if (copyBtn) {
|
|
231
|
+
copyBtn.classList.add('copied');
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
copyBtn.classList.remove('copied');
|
|
234
|
+
}, 1500);
|
|
235
|
+
}
|
|
236
|
+
}).catch(err => {
|
|
237
|
+
console.error('Failed to copy API URL:', err);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
module.exports = { monitorScript };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Client-side JavaScript for the status page
|
|
2
|
+
// This module exports a string containing all client-side JS code
|
|
3
|
+
|
|
4
|
+
const { faviconScript } = require('./client-favicon');
|
|
5
|
+
const { autoRefreshScript } = require('./client-auto-refresh');
|
|
6
|
+
const { monitorScript } = require('./client-monitor');
|
|
7
|
+
|
|
8
|
+
const clientScript = faviconScript + `
|
|
9
|
+
// Theme management
|
|
10
|
+
function initTheme() {
|
|
11
|
+
const savedTheme = localStorage.getItem('theme') || 'dark';
|
|
12
|
+
if (savedTheme === 'light') {
|
|
13
|
+
document.body.classList.add('light-mode');
|
|
14
|
+
updateThemeButton(true);
|
|
15
|
+
} else {
|
|
16
|
+
updateThemeButton(false);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toggleTheme() {
|
|
21
|
+
const isLight = document.body.classList.toggle('light-mode');
|
|
22
|
+
localStorage.setItem('theme', isLight ? 'light' : 'dark');
|
|
23
|
+
updateThemeButton(isLight);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function updateThemeButton(isLight) {
|
|
27
|
+
const icon = document.getElementById('theme-icon');
|
|
28
|
+
const text = document.getElementById('theme-text');
|
|
29
|
+
if (isLight) {
|
|
30
|
+
icon.textContent = '🌙';
|
|
31
|
+
text.textContent = 'Dark';
|
|
32
|
+
} else {
|
|
33
|
+
icon.textContent = '☀️';
|
|
34
|
+
text.textContent = 'Light';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Initialize theme on page load
|
|
39
|
+
initTheme();
|
|
40
|
+
|
|
41
|
+
// ===== Auto-Refresh Configuration =====
|
|
42
|
+
// Frontend auto-refresh is controlled by backend settings fetched from /config
|
|
43
|
+
// These are fallback defaults if the backend config cannot be fetched
|
|
44
|
+
const autoRefreshConfig = {
|
|
45
|
+
enabled: true, // Will be overridden by backend config
|
|
46
|
+
interval: 60000 // Will be overridden by backend config (in milliseconds)
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Auto-refresh timer reference
|
|
50
|
+
let autoRefreshTimer = null;
|
|
51
|
+
|
|
52
|
+
// ===== Background Monitor Feature =====
|
|
53
|
+
// Monitoring is ALWAYS ON - "Automatic" tracks highest usage, "Manual" pins a specific metric
|
|
54
|
+
|
|
55
|
+
// State management (declared early for use in auto-refresh)
|
|
56
|
+
let pinnedMetric = null; // null = automatic mode (track highest), object = manual/pinned mode
|
|
57
|
+
let trackedMetric = null; // Currently tracked metric (either highest or pinned)
|
|
58
|
+
let notificationSent = false;
|
|
59
|
+
let originalFavicon = null;
|
|
60
|
+
|
|
61
|
+
function setButtonLoading(btn, loading) {
|
|
62
|
+
btn.disabled = loading;
|
|
63
|
+
if (loading) {
|
|
64
|
+
btn.dataset.originalText = btn.innerHTML;
|
|
65
|
+
// Check if this is the refresh-all-btn with sync icon
|
|
66
|
+
if (btn.classList.contains('refresh-all-btn')) {
|
|
67
|
+
btn.classList.add('syncing');
|
|
68
|
+
const textSpan = btn.querySelector('.btn-text');
|
|
69
|
+
if (textSpan) {
|
|
70
|
+
textSpan.textContent = 'Syncing...';
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
btn.innerHTML = '<span class="spinner"></span>Refreshing...';
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
if (btn.classList.contains('refresh-all-btn')) {
|
|
77
|
+
btn.classList.remove('syncing');
|
|
78
|
+
const textSpan = btn.querySelector('.btn-text');
|
|
79
|
+
if (textSpan) {
|
|
80
|
+
textSpan.textContent = 'Refresh All';
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
btn.innerHTML = btn.dataset.originalText || 'Refresh';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function setRefreshIconLoading(btn, loading) {
|
|
88
|
+
btn.disabled = loading;
|
|
89
|
+
if (loading) {
|
|
90
|
+
btn.classList.add('spinning');
|
|
91
|
+
} else {
|
|
92
|
+
btn.classList.remove('spinning');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function refresh(agent, event) {
|
|
96
|
+
// Handle clicks on the refresh icon button (may be SVG or button)
|
|
97
|
+
const btn = event.target.closest('button');
|
|
98
|
+
if (!btn) return;
|
|
99
|
+
|
|
100
|
+
// Check if it's a refresh-icon-btn (inline icon) or regular button
|
|
101
|
+
const isIconBtn = btn.classList.contains('refresh-icon-btn');
|
|
102
|
+
|
|
103
|
+
if (isIconBtn) {
|
|
104
|
+
setRefreshIconLoading(btn, true);
|
|
105
|
+
} else {
|
|
106
|
+
setButtonLoading(btn, true);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await fetch('/refresh/' + agent, { method: 'POST' });
|
|
111
|
+
// Use XHR update instead of page reload
|
|
112
|
+
await performAutoRefresh();
|
|
113
|
+
if (isIconBtn) {
|
|
114
|
+
setRefreshIconLoading(btn, false);
|
|
115
|
+
} else {
|
|
116
|
+
setButtonLoading(btn, false);
|
|
117
|
+
}
|
|
118
|
+
} catch (e) {
|
|
119
|
+
if (isIconBtn) {
|
|
120
|
+
setRefreshIconLoading(btn, false);
|
|
121
|
+
} else {
|
|
122
|
+
setButtonLoading(btn, false);
|
|
123
|
+
}
|
|
124
|
+
alert('Failed to refresh: ' + e.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function refreshAll(event) {
|
|
128
|
+
const btn = event.target.closest('button');
|
|
129
|
+
const refreshAllBtn = document.querySelector('.refresh-all-btn');
|
|
130
|
+
const allIconBtns = document.querySelectorAll('.refresh-icon-btn');
|
|
131
|
+
if (refreshAllBtn) setButtonLoading(refreshAllBtn, true);
|
|
132
|
+
allIconBtns.forEach(b => setRefreshIconLoading(b, true));
|
|
133
|
+
try {
|
|
134
|
+
await fetch('/refresh', { method: 'POST' });
|
|
135
|
+
// Use XHR update instead of page reload
|
|
136
|
+
await performAutoRefresh();
|
|
137
|
+
if (refreshAllBtn) setButtonLoading(refreshAllBtn, false);
|
|
138
|
+
allIconBtns.forEach(b => setRefreshIconLoading(b, false));
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (refreshAllBtn) setButtonLoading(refreshAllBtn, false);
|
|
141
|
+
allIconBtns.forEach(b => setRefreshIconLoading(b, false));
|
|
142
|
+
alert('Failed to refresh: ' + e.message);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
` + autoRefreshScript + monitorScript;
|
|
146
|
+
|
|
147
|
+
module.exports = { clientScript };
|
package/src/discovery.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { execSync } = require('node:child_process');
|
|
2
|
+
const logger = require('./logger.js');
|
|
3
|
+
|
|
4
|
+
async function discoverAgents() {
|
|
5
|
+
const found = [];
|
|
6
|
+
|
|
7
|
+
// Check for Claude with version requirement
|
|
8
|
+
if (commandExists('claude')) {
|
|
9
|
+
const version = getClaudeVersion();
|
|
10
|
+
if (version) {
|
|
11
|
+
if (version.major < 2) {
|
|
12
|
+
logger.warn(`Claude Code version ${version.full} detected. Version 2.0+ required for /usage command.`);
|
|
13
|
+
logger.warn('Please update: npm update -g @anthropic-ai/claude-code');
|
|
14
|
+
} else {
|
|
15
|
+
logger.success(`✅ Claude Code version ${version.full} detected`);
|
|
16
|
+
found.push('claude');
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
// If we can't determine version, add with warning
|
|
20
|
+
logger.warn('Claude found but version unknown. Version 2.0+ required for /usage command.');
|
|
21
|
+
found.push('claude');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for Gemini with version requirement
|
|
26
|
+
if (commandExists('gemini')) {
|
|
27
|
+
const version = getGeminiVersion();
|
|
28
|
+
if (version) {
|
|
29
|
+
if (!compareVersion(version, 0, 24, 5)) {
|
|
30
|
+
logger.warn(`Gemini CLI version ${version.full} detected. Version 0.24.5+ required for /stats command.`);
|
|
31
|
+
logger.warn('Please update: npm update -g gemini');
|
|
32
|
+
} else {
|
|
33
|
+
logger.success(`✅ Gemini CLI version ${version.full} detected`);
|
|
34
|
+
found.push('gemini');
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
// If we can't determine version, add with warning
|
|
38
|
+
logger.warn('Gemini found but version unknown. Version 0.24.5+ required for /stats command.');
|
|
39
|
+
found.push('gemini');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check for Codex with version info
|
|
44
|
+
if (commandExists('codex')) {
|
|
45
|
+
const version = getCodexVersion();
|
|
46
|
+
if (version) {
|
|
47
|
+
logger.success(`✅ Codex CLI version ${version.full} detected`);
|
|
48
|
+
} else {
|
|
49
|
+
logger.success('✅ Codex CLI detected (version unknown)');
|
|
50
|
+
}
|
|
51
|
+
found.push('codex');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function commandExists(cmd) {
|
|
58
|
+
try {
|
|
59
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getClaudeVersion() {
|
|
67
|
+
try {
|
|
68
|
+
const output = execSync('claude --version 2>&1', { encoding: 'utf-8' });
|
|
69
|
+
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
70
|
+
if (match) {
|
|
71
|
+
return {
|
|
72
|
+
major: parseInt(match[1]),
|
|
73
|
+
minor: parseInt(match[2]),
|
|
74
|
+
patch: parseInt(match[3]),
|
|
75
|
+
full: match[0]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Version check failed
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getGeminiVersion() {
|
|
85
|
+
try {
|
|
86
|
+
const output = execSync('gemini --version 2>&1', { encoding: 'utf-8' });
|
|
87
|
+
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
88
|
+
if (match) {
|
|
89
|
+
return {
|
|
90
|
+
major: parseInt(match[1]),
|
|
91
|
+
minor: parseInt(match[2]),
|
|
92
|
+
patch: parseInt(match[3]),
|
|
93
|
+
full: match[0]
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Version check failed
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getCodexVersion() {
|
|
103
|
+
try {
|
|
104
|
+
const output = execSync('codex --version 2>&1', { encoding: 'utf-8' });
|
|
105
|
+
const match = output.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
106
|
+
if (match) {
|
|
107
|
+
return {
|
|
108
|
+
major: parseInt(match[1]),
|
|
109
|
+
minor: parseInt(match[2]),
|
|
110
|
+
patch: parseInt(match[3]),
|
|
111
|
+
full: match[0]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
} catch {
|
|
115
|
+
// Version check failed
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function compareVersion(version, minMajor, minMinor, minPatch) {
|
|
121
|
+
if (version.major > minMajor) return true;
|
|
122
|
+
if (version.major < minMajor) return false;
|
|
123
|
+
if (version.minor > minMinor) return true;
|
|
124
|
+
if (version.minor < minMinor) return false;
|
|
125
|
+
return version.patch >= minPatch;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { discoverAgents };
|