agent-tank 0.9.5 → 0.9.6
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 +28 -0
- package/bin/agent-tank.js +37 -11
- package/package.json +1 -1
- package/src/agents/base.js +17 -0
- package/src/agents/gemini-direct.js +234 -0
- package/src/agents/gemini.js +53 -2
- package/src/index.js +2 -1
package/README.md
CHANGED
|
@@ -89,6 +89,9 @@ agent-tank --no-docker
|
|
|
89
89
|
# Monitor only specific agents
|
|
90
90
|
agent-tank --claude --gemini
|
|
91
91
|
|
|
92
|
+
# Prefer direct Gemini quota fetching
|
|
93
|
+
agent-tank --gemini --gemini-mode direct
|
|
94
|
+
|
|
92
95
|
# Use a custom port
|
|
93
96
|
agent-tank --port 8080
|
|
94
97
|
|
|
@@ -185,6 +188,7 @@ Options:
|
|
|
185
188
|
--claude Enable Claude monitoring
|
|
186
189
|
--gemini Enable Gemini monitoring
|
|
187
190
|
--codex Enable Codex monitoring
|
|
191
|
+
--gemini-mode <mode> Gemini strategy: fallback, direct, pty (default: fallback)
|
|
188
192
|
--port <port> HTTP server port (default: 3456)
|
|
189
193
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
190
194
|
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
@@ -224,6 +228,7 @@ Environment variables override CLI flags and config file values.
|
|
|
224
228
|
| `AGENT_TANK_DOCKER` | Enable/disable Docker bridge bind when `--host` is omitted |
|
|
225
229
|
| `AGENT_TANK_FRESH_PROCESS` | Use fresh process per refresh (`1`/`true`) |
|
|
226
230
|
| `AGENT_TANK_CLAUDE_API` | Use Claude API mode (`1`/`true`) |
|
|
231
|
+
| `AGENT_TANK_GEMINI_MODE` | Gemini strategy: `fallback`, `direct`, or `pty` |
|
|
227
232
|
| `AGENT_TANK_AUTO_REFRESH` | Enable/disable auto-refresh |
|
|
228
233
|
| `AGENT_TANK_AUTO_REFRESH_MODE` | `none`, `interval`, or `activity` |
|
|
229
234
|
| `AGENT_TANK_AUTO_REFRESH_INTERVAL` | Auto-refresh interval in seconds |
|
|
@@ -243,6 +248,7 @@ Environment variables override CLI flags and config file values.
|
|
|
243
248
|
"port": 8080,
|
|
244
249
|
"dockerAccess": true,
|
|
245
250
|
"claudeApi": false,
|
|
251
|
+
"geminiMode": "fallback",
|
|
246
252
|
"refreshCooldown": 30,
|
|
247
253
|
"autoRefresh": {
|
|
248
254
|
"mode": "activity",
|
|
@@ -266,6 +272,28 @@ Run with:
|
|
|
266
272
|
agent-tank -c config.json
|
|
267
273
|
```
|
|
268
274
|
|
|
275
|
+
## Gemini Modes
|
|
276
|
+
|
|
277
|
+
By default, Gemini runs in `fallback` mode:
|
|
278
|
+
|
|
279
|
+
- try a direct quota fetch using the local Gemini OAuth session
|
|
280
|
+
- fall back to PTY `/stats` if the direct path fails
|
|
281
|
+
|
|
282
|
+
Available modes:
|
|
283
|
+
|
|
284
|
+
- `fallback`: direct first, PTY fallback
|
|
285
|
+
- `direct`: use the direct quota API only
|
|
286
|
+
- `pty`: use interactive Gemini CLI `/stats` only
|
|
287
|
+
|
|
288
|
+
In `--once` mode, Agent Tank prints the result and exits immediately after the output is flushed.
|
|
289
|
+
|
|
290
|
+
Examples:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
agent-tank --gemini --gemini-mode direct
|
|
294
|
+
agent-tank --gemini --gemini-mode pty
|
|
295
|
+
```
|
|
296
|
+
|
|
269
297
|
## HTTP API
|
|
270
298
|
|
|
271
299
|
### Endpoints
|
package/bin/agent-tank.js
CHANGED
|
@@ -11,6 +11,7 @@ const options = {
|
|
|
11
11
|
gemini: { type: 'boolean', default: false },
|
|
12
12
|
codex: { type: 'boolean', default: false },
|
|
13
13
|
'claude-api': { type: 'boolean', default: false },
|
|
14
|
+
'gemini-mode': { type: 'string' },
|
|
14
15
|
port: { type: 'string', default: '3456' },
|
|
15
16
|
host: { type: 'string' },
|
|
16
17
|
docker: { type: 'boolean', default: true },
|
|
@@ -43,6 +44,17 @@ function exitWithCode(code, message, stream = process.stderr) {
|
|
|
43
44
|
process.exitCode = code;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function writeAndExit(stream, text, exitCode, { cleanup, watcher, jsonMode, originalLog }) {
|
|
48
|
+
stream.write(text, () => {
|
|
49
|
+
cleanup();
|
|
50
|
+
watcher.stop();
|
|
51
|
+
if (jsonMode) {
|
|
52
|
+
console.log = originalLog;
|
|
53
|
+
}
|
|
54
|
+
process.exit(exitCode);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
function printHelp() {
|
|
47
59
|
process.stdout.write(`
|
|
48
60
|
agent-tank - Monitor LLM CLI usage limits
|
|
@@ -54,6 +66,7 @@ Options:
|
|
|
54
66
|
--gemini Enable Gemini monitoring
|
|
55
67
|
--codex Enable Codex monitoring
|
|
56
68
|
--claude-api Use direct Anthropic API for Claude usage (faster, 60s refresh)
|
|
69
|
+
--gemini-mode <mode> Gemini strategy: fallback, direct, pty (default: fallback)
|
|
57
70
|
--port <port> HTTP server port (default: 3456)
|
|
58
71
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
59
72
|
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
@@ -90,6 +103,7 @@ Environment variables:
|
|
|
90
103
|
AGENT_TANK_DOCKER Enable/disable Docker bridge bind ("1"/"true" or "0"/"false")
|
|
91
104
|
AGENT_TANK_FRESH_PROCESS Use fresh process per refresh ("1" or "true")
|
|
92
105
|
AGENT_TANK_CLAUDE_API Use direct Anthropic API for Claude usage ("1" or "true")
|
|
106
|
+
AGENT_TANK_GEMINI_MODE Gemini strategy: fallback, direct, pty
|
|
93
107
|
AGENT_TANK_AUTO_REFRESH Enable/disable background auto-refresh ("1" or "true" / "0" or "false")
|
|
94
108
|
AGENT_TANK_AUTO_REFRESH_MODE Refresh mode: none, interval, activity
|
|
95
109
|
AGENT_TANK_AUTO_REFRESH_INTERVAL Auto-refresh interval in seconds
|
|
@@ -105,6 +119,7 @@ Examples:
|
|
|
105
119
|
agent-tank --port 8080 # Use custom port
|
|
106
120
|
agent-tank --host 0.0.0.0 # Expose on all interfaces
|
|
107
121
|
agent-tank --no-docker # Bind localhost only when host is omitted
|
|
122
|
+
agent-tank --gemini-mode direct # Prefer direct Gemini quota API calls
|
|
108
123
|
agent-tank --auth-user admin --auth-pass secret # Enable basic auth
|
|
109
124
|
agent-tank --auth-token mykey # Enable API key auth
|
|
110
125
|
agent-tank -c ./config.json # Use config file
|
|
@@ -188,6 +203,9 @@ async function main() {
|
|
|
188
203
|
config.claudeApi ||
|
|
189
204
|
claudeApiEnv === '1' || claudeApiEnv === 'true';
|
|
190
205
|
|
|
206
|
+
const geminiModeEnv = process.env.AGENT_TANK_GEMINI_MODE;
|
|
207
|
+
const geminiMode = geminiModeEnv || values['gemini-mode'] || config.geminiMode || 'fallback';
|
|
208
|
+
|
|
191
209
|
// Auto-refresh configuration (env > CLI > config file)
|
|
192
210
|
const autoRefreshEnv = process.env.AGENT_TANK_AUTO_REFRESH;
|
|
193
211
|
let autoRefreshEnabled = values['auto-refresh'];
|
|
@@ -280,6 +298,7 @@ async function main() {
|
|
|
280
298
|
auth,
|
|
281
299
|
freshProcess,
|
|
282
300
|
claudeApi,
|
|
301
|
+
geminiMode,
|
|
283
302
|
autoRefreshEnabled: onceMode ? false : autoRefreshEnabled, // Disable auto-refresh in one-shot mode
|
|
284
303
|
autoRefreshInterval,
|
|
285
304
|
autoRefreshMode: onceMode ? 'none' : autoRefreshMode, // Disable auto-refresh in one-shot mode
|
|
@@ -311,21 +330,28 @@ async function main() {
|
|
|
311
330
|
await watcher.start();
|
|
312
331
|
const status = watcher.getStatus();
|
|
313
332
|
if (jsonMode) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
const output = `${JSON.stringify(status, null, 2)}\n`;
|
|
334
|
+
writeAndExit(process.stdout, output, 0, {
|
|
335
|
+
cleanup: cleanupShutdownHandlers,
|
|
336
|
+
watcher,
|
|
337
|
+
jsonMode,
|
|
338
|
+
originalLog,
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
319
341
|
} else {
|
|
342
|
+
let output = '';
|
|
320
343
|
for (const [name, agentStatus] of Object.entries(status)) {
|
|
321
|
-
|
|
322
|
-
|
|
344
|
+
output += `\n=== ${name.toUpperCase()} ===\n`;
|
|
345
|
+
output += `${JSON.stringify(agentStatus, null, 2)}\n`;
|
|
323
346
|
}
|
|
347
|
+
writeAndExit(process.stdout, output, 0, {
|
|
348
|
+
cleanup: cleanupShutdownHandlers,
|
|
349
|
+
watcher,
|
|
350
|
+
jsonMode,
|
|
351
|
+
originalLog,
|
|
352
|
+
});
|
|
353
|
+
return;
|
|
324
354
|
}
|
|
325
|
-
cleanupShutdownHandlers();
|
|
326
|
-
watcher.stop();
|
|
327
|
-
process.exitCode = 0;
|
|
328
|
-
return;
|
|
329
355
|
}
|
|
330
356
|
|
|
331
357
|
await watcher.start();
|
package/package.json
CHANGED
package/src/agents/base.js
CHANGED
|
@@ -231,6 +231,10 @@ class BaseAgent {
|
|
|
231
231
|
clearTimeout(timer);
|
|
232
232
|
spawnDataHandler.dispose();
|
|
233
233
|
spawnExitHandler.dispose();
|
|
234
|
+
if (!this.shell) {
|
|
235
|
+
reject(new Error('Process exited during spawn before handlers were attached'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
234
238
|
this.processReady = true;
|
|
235
239
|
this.output = spawnOutput;
|
|
236
240
|
this._setupPersistentDataHandler();
|
|
@@ -243,6 +247,11 @@ class BaseAgent {
|
|
|
243
247
|
logger.agent(this.name, 'Process ready for commands');
|
|
244
248
|
clearTimeout(timer);
|
|
245
249
|
spawnDataHandler.dispose();
|
|
250
|
+
spawnExitHandler.dispose();
|
|
251
|
+
if (!this.shell) {
|
|
252
|
+
reject(new Error('Process exited during spawn before handlers were attached'));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
246
255
|
this.processReady = true;
|
|
247
256
|
this._setupPersistentDataHandler();
|
|
248
257
|
this._setupPersistentExitHandler();
|
|
@@ -334,6 +343,10 @@ class BaseAgent {
|
|
|
334
343
|
}
|
|
335
344
|
|
|
336
345
|
_setupPersistentDataHandler() {
|
|
346
|
+
if (!this.shell) {
|
|
347
|
+
logger.agent(this.name, 'Skipping persistent data handler setup: shell already exited');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
337
350
|
const handler = this.shell.onData((data) => {
|
|
338
351
|
this.output += data;
|
|
339
352
|
|
|
@@ -356,6 +369,10 @@ class BaseAgent {
|
|
|
356
369
|
}
|
|
357
370
|
|
|
358
371
|
_setupPersistentExitHandler() {
|
|
372
|
+
if (!this.shell) {
|
|
373
|
+
logger.agent(this.name, 'Skipping persistent exit handler setup: shell already exited');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
359
376
|
const handler = this.shell.onExit(({ exitCode }) => {
|
|
360
377
|
logger.agent(this.name, 'Persistent process exited with code', logger.dim(exitCode));
|
|
361
378
|
this.shell = null; this.processReady = false; this._onDataCallback = null; this._disposables = [];
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
};
|
package/src/agents/gemini.js
CHANGED
|
@@ -1,14 +1,60 @@
|
|
|
1
1
|
const { BaseAgent } = require('./base.js');
|
|
2
2
|
const { calculatePace } = require('../pace-evaluator.js');
|
|
3
3
|
const { CYCLE_DURATIONS } = require('../usage-formatters.js');
|
|
4
|
+
const { GeminiDirectQuotaClient, normalizeGeminiMode } = require('./gemini-direct.js');
|
|
4
5
|
const logger = require('../logger.js');
|
|
5
6
|
|
|
6
7
|
class GeminiAgent extends BaseAgent {
|
|
7
|
-
constructor() {
|
|
8
|
-
super('gemini', 'gemini');
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
super('gemini', 'gemini', ['--skip-trust']);
|
|
10
|
+
this.mode = normalizeGeminiMode(options.mode);
|
|
11
|
+
this.directClient = new GeminiDirectQuotaClient(options.directClientOptions);
|
|
9
12
|
this._aboutSent = false;
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
async refresh() {
|
|
16
|
+
if (this.mode === 'pty') {
|
|
17
|
+
return super.refresh();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.isStopping()) {
|
|
21
|
+
logger.agent(this.name, 'Skipping refresh while stopping');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (this.isRefreshing) {
|
|
26
|
+
logger.agent(this.name, 'Already refreshing, skipping...');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.agent(this.name, `Starting refresh${this.mode === 'direct' ? ' (direct quota mode)' : ' (direct quota mode with PTY fallback)'}...`);
|
|
31
|
+
this.isRefreshing = true;
|
|
32
|
+
this.error = null;
|
|
33
|
+
this.auth = null;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await this.directClient.fetchQuotaUsage();
|
|
37
|
+
this.usage = result.usage;
|
|
38
|
+
this.metadata = result.metadata || this.metadata;
|
|
39
|
+
this._metadataFetched = !!this.metadata;
|
|
40
|
+
this.lastUpdated = new Date().toISOString();
|
|
41
|
+
logger.agent(this.name, 'Parsed direct quota usage:', logger.json(this.usage));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (this.mode === 'fallback') {
|
|
44
|
+
logger.agent(this.name, `Direct quota fetch failed, falling back to PTY: ${err.message}`);
|
|
45
|
+
this.isRefreshing = false;
|
|
46
|
+
return super.refresh();
|
|
47
|
+
}
|
|
48
|
+
logger.error(`[${this.name}] Error during direct quota refresh:`, err.message);
|
|
49
|
+
this.error = err.message;
|
|
50
|
+
} finally {
|
|
51
|
+
if (this.isRefreshing) {
|
|
52
|
+
this.isRefreshing = false;
|
|
53
|
+
logger.agent(this.name, 'Refresh complete');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
12
58
|
getTimeout() {
|
|
13
59
|
return 35000; // 35 seconds - Gemini can take 10+ seconds to start
|
|
14
60
|
}
|
|
@@ -388,6 +434,11 @@ class GeminiAgent extends BaseAgent {
|
|
|
388
434
|
* @returns {Promise<boolean>} True if keepalive succeeded
|
|
389
435
|
*/
|
|
390
436
|
async keepalive() {
|
|
437
|
+
if (this.mode === 'direct' || (this.mode === 'fallback' && (!this.shell || !this.processReady))) {
|
|
438
|
+
console.log(`[${this.name}] Keepalive skipped (${this.mode} mode)`);
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
391
442
|
if (this.freshProcess) {
|
|
392
443
|
console.log(`[${this.name}] Keepalive skipped (fresh process mode)`);
|
|
393
444
|
return true;
|
package/src/index.js
CHANGED
|
@@ -121,6 +121,7 @@ class AgentTank {
|
|
|
121
121
|
this.requestedAgents = options.agents || null;
|
|
122
122
|
this.freshProcess = options.freshProcess || false;
|
|
123
123
|
this.claudeApi = options.claudeApi || false; // Use direct Anthropic API for Claude
|
|
124
|
+
this.geminiMode = options.geminiMode || 'fallback';
|
|
124
125
|
this.auth = options.auth || {};
|
|
125
126
|
this.agents = new Map();
|
|
126
127
|
this.server = null;
|
|
@@ -327,7 +328,7 @@ class AgentTank {
|
|
|
327
328
|
case 'claude':
|
|
328
329
|
return new ClaudeAgent({ useApi: this.claudeApi });
|
|
329
330
|
case 'gemini':
|
|
330
|
-
return new GeminiAgent();
|
|
331
|
+
return new GeminiAgent({ mode: this.geminiMode });
|
|
331
332
|
case 'codex':
|
|
332
333
|
return new CodexAgent();
|
|
333
334
|
default:
|