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.
- package/README.md +77 -34
- package/bin/agent-tank.js +61 -16
- package/package.json +5 -3
- package/src/activity-monitor.js +18 -7
- package/src/agents/agy.js +301 -0
- package/src/agents/base.js +27 -4
- package/src/agents/claude.js +39 -10
- package/src/agents/codex.js +1 -1
- package/src/agents/pty-output-parser.js +10 -4
- package/src/cli-background.js +198 -0
- package/src/client-auto-refresh-helpers.js +7 -7
- package/src/client-auto-refresh.js +2 -2
- package/src/client-monitor.js +2 -2
- package/src/discovery.js +9 -24
- package/src/history-store.js +1 -1
- package/src/icons.js +2 -2
- package/src/index.js +7 -6
- package/src/logger.js +2 -2
- package/src/pace-attachment.js +7 -7
- package/src/process-utils.js +215 -0
- package/src/public-status.js +5 -5
- package/src/snapshot-metrics.js +6 -6
- package/src/status-page.css +6 -6
- package/src/status-page.js +56 -38
- package/src/usage-formatters.js +7 -8
- package/src/agents/gemini-direct.js +0 -234
- package/src/agents/gemini.js +0 -463
package/src/public-status.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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) {
|
package/src/snapshot-metrics.js
CHANGED
|
@@ -20,11 +20,11 @@ function extractClaudeMetrics(usage) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Extract
|
|
24
|
-
* @param {Object} usage -
|
|
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
|
|
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 '
|
|
66
|
-
return
|
|
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
|
-
|
|
77
|
+
extractAgyMetrics,
|
|
78
78
|
extractCodexMetrics
|
|
79
79
|
};
|
package/src/status-page.css
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
709
|
+
.agent-agy h2,
|
|
710
710
|
.agent-codex h2 {
|
|
711
711
|
color: var(--text-primary);
|
|
712
712
|
}
|
package/src/status-page.js
CHANGED
|
@@ -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, ''');
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// 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
|
-
|
|
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>
|
package/src/usage-formatters.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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: '
|
|
187
|
-
html += resetInfoItem(model.resetsIn, null, '
|
|
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,
|
|
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
|
-
|
|
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
|
-
};
|