agent-tank 0.9.6 → 0.9.8

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.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Fetches status information from public API status pages for Anthropic, OpenAI,
5
5
  * and Google Cloud. Uses Statuspage.io JSON API for Anthropic and OpenAI, and
6
- * Google Cloud's incidents JSON for Google/Gemini.
6
+ * Google Cloud's incidents JSON for Antigravity's Google-backed models.
7
7
  */
8
8
 
9
9
  const https = require('node:https');
@@ -20,11 +20,11 @@ const STATUS_PAGES = {
20
20
  type: 'statuspage',
21
21
  name: 'OpenAI',
22
22
  },
23
- gemini: {
23
+ agy: {
24
24
  url: 'https://status.cloud.google.com/incidents.json',
25
25
  type: 'google-cloud',
26
26
  name: 'Google Cloud',
27
- // Vertex AI Gemini product ID for filtering incidents
27
+ // Vertex AI Gemini product ID for filtering incidents that may affect Antigravity.
28
28
  productId: 'Z0FZJAMvEB4j3NbCJs6B',
29
29
  },
30
30
  };
@@ -99,7 +99,7 @@ function parseStatuspageResponse(json) {
99
99
  function parseGoogleCloudResponse(json, productId) {
100
100
  const incidents = JSON.parse(json);
101
101
 
102
- // Find active incidents affecting the specified product (e.g., Vertex AI Gemini)
102
+ // Find active incidents affecting the specified product.
103
103
  const now = new Date();
104
104
  const activeIncidents = incidents.filter((incident) => {
105
105
  // Check if incident is ongoing (no end date or recent)
@@ -171,7 +171,7 @@ function parseGoogleCloudResponse(json, productId) {
171
171
 
172
172
  /**
173
173
  * Fetch public status for a single agent
174
- * @param {string} agentName - Name of the agent (claude, gemini, codex)
174
+ * @param {string} agentName - Name of the agent (claude, agy, codex)
175
175
  * @returns {Promise<object>} - Status object with status, indicator, description
176
176
  */
177
177
  async function fetchAgentPublicStatus(agentName) {
@@ -20,11 +20,11 @@ function extractClaudeMetrics(usage) {
20
20
  }
21
21
 
22
22
  /**
23
- * Extract Gemini metrics from usage data.
24
- * @param {Object} usage - Gemini usage data
23
+ * Extract Antigravity metrics from usage data.
24
+ * @param {Object} usage - Antigravity usage data
25
25
  * @returns {Object|null} Extracted metrics or null
26
26
  */
27
- function extractGeminiMetrics(usage) {
27
+ function extractAgyMetrics(usage) {
28
28
  if (!usage.models || !Array.isArray(usage.models)) {
29
29
  return null;
30
30
  }
@@ -62,8 +62,8 @@ function extractSnapshotMetrics(agentName, usage) {
62
62
  switch (agentName) {
63
63
  case 'claude':
64
64
  return extractClaudeMetrics(usage);
65
- case 'gemini':
66
- return extractGeminiMetrics(usage);
65
+ case 'agy':
66
+ return extractAgyMetrics(usage);
67
67
  case 'codex':
68
68
  return extractCodexMetrics(usage);
69
69
  default:
@@ -74,6 +74,6 @@ function extractSnapshotMetrics(agentName, usage) {
74
74
  module.exports = {
75
75
  extractSnapshotMetrics,
76
76
  extractClaudeMetrics,
77
- extractGeminiMetrics,
77
+ extractAgyMetrics,
78
78
  extractCodexMetrics
79
79
  };
@@ -671,20 +671,20 @@ body.light-mode .status-badge-grey {
671
671
  /* Agent icons - neutral low-opacity white/grey for industrial look
672
672
  Status dots (LEDs) are now the primary color indicators */
673
673
  .agent-claude,
674
- .agent-gemini,
674
+ .agent-agy,
675
675
  .agent-codex {
676
676
  --agent-bg-subtle: rgba(255, 255, 255, 0.04);
677
677
  }
678
678
 
679
679
  .agent-claude .agent-icon,
680
- .agent-gemini .agent-icon,
680
+ .agent-agy .agent-icon,
681
681
  .agent-codex .agent-icon {
682
682
  color: var(--text-tertiary);
683
683
  opacity: 0.7;
684
684
  }
685
685
 
686
686
  body.light-mode .agent-claude .agent-icon,
687
- body.light-mode .agent-gemini .agent-icon,
687
+ body.light-mode .agent-agy .agent-icon,
688
688
  body.light-mode .agent-codex .agent-icon {
689
689
  color: var(--text-secondary);
690
690
  opacity: 0.6;
@@ -692,21 +692,21 @@ body.light-mode .agent-codex .agent-icon {
692
692
 
693
693
  /* Agent cards have simple borders - brand colors only on icons */
694
694
  .agent-claude,
695
- .agent-gemini,
695
+ .agent-agy,
696
696
  .agent-codex {
697
697
  background: var(--bg-card);
698
698
  }
699
699
 
700
700
  /* Light mode adjustments for subtle backgrounds */
701
701
  body.light-mode .agent-claude,
702
- body.light-mode .agent-gemini,
702
+ body.light-mode .agent-agy,
703
703
  body.light-mode .agent-codex {
704
704
  --agent-bg-subtle: rgba(0, 0, 0, 0.02);
705
705
  }
706
706
 
707
707
  /* Agent headings use neutral color, only icons have brand colors */
708
708
  .agent-claude h2,
709
- .agent-gemini h2,
709
+ .agent-agy h2,
710
710
  .agent-codex h2 {
711
711
  color: var(--text-primary);
712
712
  }
@@ -8,6 +8,14 @@ const { clientScript } = require('./client-script');
8
8
  const { getStatusBadgeClass, getStatusText } = require('./public-status');
9
9
 
10
10
  const styles = fs.readFileSync(path.join(__dirname, 'status-page.css'), 'utf8');
11
+ const AGENT_DISPLAY_NAMES = {
12
+ agy: 'Antigravity',
13
+ };
14
+
15
+ function getAgentDisplayName(name) {
16
+ if (AGENT_DISPLAY_NAMES[name]) return AGENT_DISPLAY_NAMES[name];
17
+ return name.charAt(0).toUpperCase() + name.slice(1);
18
+ }
11
19
 
12
20
  function escapeHtml(value) {
13
21
  return String(value)
@@ -18,10 +26,7 @@ function escapeHtml(value) {
18
26
  .replace(/'/g, '&#39;');
19
27
  }
20
28
 
21
- function statusPage(status) {
22
- const agents = Object.entries(status);
23
-
24
- // Calculate the maximum lastUpdated timestamp across all agents for global display
29
+ function getGlobalLastChecked(agents) {
25
30
  const maxLastUpdated = agents.reduce((max, [, data]) => {
26
31
  if (data.lastUpdated) {
27
32
  const ts = new Date(data.lastUpdated).getTime();
@@ -30,42 +35,50 @@ function statusPage(status) {
30
35
  return max;
31
36
  }, 0);
32
37
 
33
- const globalLastChecked = maxLastUpdated > 0
38
+ return maxLastUpdated > 0
34
39
  ? new Date(maxLastUpdated).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
35
40
  : null;
41
+ }
42
+
43
+ function renderPublicStatusBadge(publicStatus) {
44
+ const badgeClass = publicStatus ? getStatusBadgeClass(publicStatus.status) : 'status-badge-grey';
45
+ const badgeText = publicStatus ? getStatusText(publicStatus.status) : 'Unknown';
46
+ const badgeTitle = escapeHtml(publicStatus?.description || 'Unable to fetch status');
47
+
48
+ return `<span class="status-badge ${badgeClass}" title="${badgeTitle}">${badgeText}</span>`;
49
+ }
50
+
51
+ function renderVersionUpdate(data) {
52
+ const version = data.usage?.version || data.metadata?.updateAvailable;
53
+
54
+ if (!version || !version.current || !version.latest || version.current === version.latest) {
55
+ return '';
56
+ }
36
57
 
37
- const agentCards = agents.map(([name, data]) => {
38
- const usageHtml = formatUsage(name, data.usage);
39
- const statusClass = data.error ? 'error' : data.isRefreshing ? 'refreshing' : 'ok';
40
-
41
- const icon = agentIcons[name] || '';
42
- const displayName = name.charAt(0).toUpperCase() + name.slice(1);
43
-
44
- // Public API status badge
45
- const publicStatus = data.publicStatus;
46
- const badgeClass = publicStatus ? getStatusBadgeClass(publicStatus.status) : 'status-badge-grey';
47
- const badgeText = publicStatus ? getStatusText(publicStatus.status) : 'Unknown';
48
- const badgeTitle = escapeHtml(publicStatus?.description || 'Unable to fetch status');
49
- const statusBadgeHtml = `<span class="status-badge ${badgeClass}" title="${badgeTitle}">${badgeText}</span>`;
50
-
51
- // Version update notice - check usage.version (Codex) or metadata.updateAvailable (Gemini)
52
- const version = data.usage?.version || data.metadata?.updateAvailable;
53
- const updateHtml = (version && version.current && version.latest && version.current !== version.latest)
54
- ? `<div class="version-update-notice">Update available: v${version.current} → v${version.latest}</div>`
55
- : '';
56
-
57
- const authHtml = data.auth
58
- ? `<div class="auth-notice">${escapeHtml(data.auth.action || 'Please log in via the CLI.')}</div>`
59
- : '';
60
- const errorHtml = data.error && !data.auth ? `<span class="error-msg">${escapeHtml(data.error)}</span>` : '';
61
-
62
- // Only show card-footer if there's an error or update notice
63
- const hasFooterContent = errorHtml || authHtml || updateHtml;
64
- const footerHtml = hasFooterContent
65
- ? `<div class="card-footer">${errorHtml}${authHtml}${updateHtml}</div>`
66
- : '';
67
-
68
- return `
58
+ return `<div class="version-update-notice">Update available: v${version.current} → v${version.latest}</div>`;
59
+ }
60
+
61
+ function renderCardFooter(data) {
62
+ const authHtml = data.auth
63
+ ? `<div class="auth-notice">${escapeHtml(data.auth.action || 'Please log in via the CLI.')}</div>`
64
+ : '';
65
+ const errorHtml = data.error && !data.auth ? `<span class="error-msg">${escapeHtml(data.error)}</span>` : '';
66
+ const updateHtml = renderVersionUpdate(data);
67
+
68
+ return errorHtml || authHtml || updateHtml
69
+ ? `<div class="card-footer">${errorHtml}${authHtml}${updateHtml}</div>`
70
+ : '';
71
+ }
72
+
73
+ function renderAgentCard([name, data]) {
74
+ const usageHtml = formatUsage(name, data.usage);
75
+ const statusClass = data.error ? 'error' : data.isRefreshing ? 'refreshing' : 'ok';
76
+ const icon = agentIcons[name] || '';
77
+ const displayName = getAgentDisplayName(name);
78
+ const statusBadgeHtml = renderPublicStatusBadge(data.publicStatus);
79
+ const footerHtml = renderCardFooter(data);
80
+
81
+ return `
69
82
  <div class="agent-card ${statusClass} agent-${name}">
70
83
  <h2 class="agent-heading">
71
84
  ${icon}
@@ -79,7 +92,12 @@ function statusPage(status) {
79
92
  ${footerHtml}
80
93
  </div>
81
94
  `;
82
- }).join('');
95
+ }
96
+
97
+ function statusPage(status) {
98
+ const agents = Object.entries(status);
99
+ const globalLastChecked = getGlobalLastChecked(agents);
100
+ const agentCards = agents.map(renderAgentCard).join('');
83
101
 
84
102
  return `<!DOCTYPE html>
85
103
  <html>
@@ -4,7 +4,7 @@ const { formatPaceWarning } = require('./pace-evaluator');
4
4
  // Cycle duration constants in seconds
5
5
  const CYCLE_DURATIONS = {
6
6
  session: 5 * 60 * 60, // 5 hours = 18000 seconds
7
- sessionGemini: 24 * 60 * 60, // 24 hours = 86400 seconds (Gemini uses 24h sessions)
7
+ sessionAgy: 24 * 60 * 60, // 24 hours = 86400 seconds
8
8
  weekly: 7 * 24 * 60 * 60, // 7 days = 604800 seconds
9
9
  fiveHour: 5 * 60 * 60 // 5 hours = 18000 seconds
10
10
  };
@@ -169,22 +169,21 @@ function formatClaudeUsage(usage) {
169
169
  return html;
170
170
  }
171
171
 
172
- function formatGeminiUsage(usage) {
172
+ function formatAgyUsage(usage) {
173
173
  let html = '';
174
174
  if (usage.models && usage.models.length > 0) {
175
175
  for (const model of usage.models) {
176
176
  const percent = model.percentUsed ?? 0;
177
177
  const isZero = percent === 0;
178
- // Use lowercase model name with model-name styling
179
- const modelName = model.model.toLowerCase();
178
+ const modelName = model.model;
180
179
  const resetsIn = model.resetsIn || '';
181
180
 
182
181
  // Use pre-calculated pace data from the usage object
183
182
  const paceData = model.pace || null;
184
183
 
185
184
  html += '<div class="model-container">';
186
- html += usageItem(modelName, percent, '% used', { isZero, isModelName: true, agentName: 'gemini', resetsIn });
187
- html += resetInfoItem(model.resetsIn, null, 'sessionGemini', { isZero, paceData });
185
+ html += usageItem(modelName, percent, '% used', { isZero, isModelName: true, agentName: 'agy', resetsIn });
186
+ html += resetInfoItem(model.resetsIn, null, 'sessionAgy', { isZero, paceData });
188
187
  html += '</div>';
189
188
  }
190
189
  }
@@ -240,7 +239,7 @@ function formatUsage(agentName, usage) {
240
239
  return '<p class="usage-item">No data available</p>';
241
240
  }
242
241
 
243
- const formatters = { claude: formatClaudeUsage, gemini: formatGeminiUsage, codex: formatCodexUsage };
242
+ const formatters = { claude: formatClaudeUsage, agy: formatAgyUsage, codex: formatCodexUsage };
244
243
  const formatter = formatters[agentName];
245
244
  const html = formatter ? formatter(usage) : '';
246
245
 
@@ -250,7 +249,7 @@ function formatUsage(agentName, usage) {
250
249
  module.exports = {
251
250
  formatUsage,
252
251
  formatClaudeUsage,
253
- formatGeminiUsage,
252
+ formatAgyUsage,
254
253
  formatCodexUsage,
255
254
  usageItem,
256
255
  resetInfoItem,
@@ -1,234 +0,0 @@
1
- const fs = require('node:fs');
2
- const os = require('node:os');
3
- const path = require('node:path');
4
-
5
- const GEMINI_DIR = '.gemini';
6
- const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9' + 'e3aqf6av3hmdib135j.apps.googleusercontent.com';
7
- const OAUTH_CLIENT_SECRET = 'GOCSPX-4uHgMPm-1o7Sk-' + 'geV6Cu5clXFsxl';
8
- const TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
9
- const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com/v1internal';
10
- const REFRESH_SKEW_MS = 60 * 1000;
11
- const VALID_GEMINI_MODES = new Set(['pty', 'direct', 'fallback']);
12
-
13
- function getGeminiHomeDir() {
14
- return path.join(os.homedir(), GEMINI_DIR);
15
- }
16
-
17
- function getOAuthCredsPath() {
18
- return path.join(getGeminiHomeDir(), 'oauth_creds.json');
19
- }
20
-
21
- function getGoogleAccountsPath() {
22
- return path.join(getGeminiHomeDir(), 'google_accounts.json');
23
- }
24
-
25
- function normalizeGeminiMode(mode) {
26
- const normalized = typeof mode === 'string' ? mode.toLowerCase() : 'fallback';
27
- return VALID_GEMINI_MODES.has(normalized) ? normalized : 'fallback';
28
- }
29
-
30
- function readJsonIfExists(filePath) {
31
- try {
32
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
- } catch (err) {
34
- if (err && err.code === 'ENOENT') {
35
- return null;
36
- }
37
- throw err;
38
- }
39
- }
40
-
41
- function formatDurationFromSeconds(totalSeconds) {
42
- if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) {
43
- return '0m';
44
- }
45
-
46
- const days = Math.floor(totalSeconds / 86400);
47
- const hours = Math.floor((totalSeconds % 86400) / 3600);
48
- const minutes = Math.floor((totalSeconds % 3600) / 60);
49
-
50
- if (days > 0) {
51
- return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
52
- }
53
-
54
- if (hours > 0) {
55
- return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
56
- }
57
-
58
- return `${Math.max(1, minutes)}m`;
59
- }
60
-
61
- function parseResetTime(resetTime) {
62
- if (!resetTime) {
63
- return { resetsIn: null, resetsInSeconds: null };
64
- }
65
-
66
- const resetAtMs = Date.parse(resetTime);
67
- if (!Number.isFinite(resetAtMs)) {
68
- return { resetsIn: null, resetsInSeconds: null };
69
- }
70
-
71
- const resetsInSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
72
- return {
73
- resetsIn: formatDurationFromSeconds(resetsInSeconds),
74
- resetsInSeconds,
75
- };
76
- }
77
-
78
- function buildModelEntry(bucket) {
79
- if (!bucket || !bucket.modelId || bucket.remainingFraction == null) {
80
- return null;
81
- }
82
-
83
- const usageLeft = Number((bucket.remainingFraction * 100).toFixed(1));
84
- const percentUsed = Number((100 - usageLeft).toFixed(1));
85
- const { resetsIn, resetsInSeconds } = parseResetTime(bucket.resetTime);
86
-
87
- return {
88
- model: bucket.modelId,
89
- usageLeft,
90
- percentUsed,
91
- resetsIn,
92
- resetsInSeconds,
93
- };
94
- }
95
-
96
- function buildUsageFromQuota(quotaResponse) {
97
- const seenModels = new Set();
98
- const models = [];
99
-
100
- for (const bucket of quotaResponse?.buckets || []) {
101
- const entry = buildModelEntry(bucket);
102
- if (!entry || seenModels.has(entry.model)) {
103
- continue;
104
- }
105
- seenModels.add(entry.model);
106
- models.push(entry);
107
- }
108
-
109
- return { models };
110
- }
111
-
112
- class GeminiDirectQuotaClient {
113
- constructor(options = {}) {
114
- this.fetchImpl = options.fetchImpl || global.fetch;
115
- this.now = options.now || Date.now;
116
- }
117
-
118
- _readOAuthCredentials() {
119
- const creds = readJsonIfExists(getOAuthCredsPath());
120
- if (!creds) {
121
- throw new Error(`Gemini OAuth credentials not found at ${getOAuthCredsPath()}`);
122
- }
123
- return creds;
124
- }
125
-
126
- _readActiveAccountEmail() {
127
- const accounts = readJsonIfExists(getGoogleAccountsPath());
128
- return accounts?.active || null;
129
- }
130
-
131
- async _getAccessToken() {
132
- const creds = this._readOAuthCredentials();
133
- const expiryDate = Number(creds.expiry_date);
134
- const accessToken = creds.access_token;
135
-
136
- if (accessToken && Number.isFinite(expiryDate) && expiryDate - REFRESH_SKEW_MS > this.now()) {
137
- return accessToken;
138
- }
139
-
140
- if (!creds.refresh_token) {
141
- throw new Error('Gemini OAuth credentials are missing a refresh token');
142
- }
143
-
144
- const body = new URLSearchParams({
145
- client_id: OAUTH_CLIENT_ID,
146
- client_secret: OAUTH_CLIENT_SECRET,
147
- refresh_token: creds.refresh_token,
148
- grant_type: 'refresh_token',
149
- });
150
-
151
- const response = await this.fetchImpl(TOKEN_ENDPOINT, {
152
- method: 'POST',
153
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
154
- body,
155
- });
156
-
157
- if (!response.ok) {
158
- const text = await response.text();
159
- throw new Error(`Failed to refresh Gemini OAuth token (${response.status}): ${text || response.statusText}`);
160
- }
161
-
162
- const refreshed = await response.json();
163
- const updatedCreds = {
164
- ...creds,
165
- access_token: refreshed.access_token,
166
- expiry_date: refreshed.expires_in ? this.now() + (refreshed.expires_in * 1000) : creds.expiry_date,
167
- token_type: refreshed.token_type || creds.token_type,
168
- scope: refreshed.scope || creds.scope,
169
- };
170
-
171
- fs.writeFileSync(getOAuthCredsPath(), JSON.stringify(updatedCreds, null, 2));
172
- return updatedCreds.access_token;
173
- }
174
-
175
- async _post(method, token, payload) {
176
- const response = await this.fetchImpl(`${CODE_ASSIST_ENDPOINT}:${method}`, {
177
- method: 'POST',
178
- headers: {
179
- 'Content-Type': 'application/json',
180
- Authorization: `Bearer ${token}`,
181
- },
182
- body: JSON.stringify(payload),
183
- });
184
-
185
- if (!response.ok) {
186
- const text = await response.text();
187
- throw new Error(`Gemini ${method} request failed (${response.status}): ${text || response.statusText}`);
188
- }
189
-
190
- return response.json();
191
- }
192
-
193
- async fetchQuotaUsage() {
194
- const token = await this._getAccessToken();
195
- const configuredProject = process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_CLOUD_PROJECT_ID;
196
-
197
- const loadResponse = await this._post('loadCodeAssist', token, {
198
- cloudaicompanionProject: configuredProject,
199
- metadata: {
200
- ideType: 'IDE_UNSPECIFIED',
201
- platform: 'PLATFORM_UNSPECIFIED',
202
- pluginType: 'GEMINI',
203
- duetProject: configuredProject,
204
- },
205
- mode: 'HEALTH_CHECK',
206
- });
207
-
208
- const project = loadResponse?.cloudaicompanionProject || configuredProject;
209
- if (!project) {
210
- throw new Error('Gemini Code Assist project ID is unavailable');
211
- }
212
-
213
- const quotaResponse = await this._post('retrieveUserQuota', token, { project });
214
- const usage = buildUsageFromQuota(quotaResponse);
215
-
216
- return {
217
- usage,
218
- metadata: {
219
- email: this._readActiveAccountEmail(),
220
- authMethod: 'OAuth',
221
- tier: loadResponse?.paidTier?.name || loadResponse?.currentTier?.name,
222
- project,
223
- },
224
- };
225
- }
226
- }
227
-
228
- module.exports = {
229
- GeminiDirectQuotaClient,
230
- buildModelEntry,
231
- buildUsageFromQuota,
232
- formatDurationFromSeconds,
233
- normalizeGeminiMode,
234
- };