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.
@@ -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 };
@@ -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 };