agent-tank 0.9.6 → 0.9.7
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 +16 -31
- package/bin/agent-tank.js +4 -12
- package/package.json +5 -3
- package/src/activity-monitor.js +18 -7
- package/src/agents/agy.js +301 -0
- package/src/agents/base.js +1 -1
- package/src/agents/claude.js +39 -10
- package/src/agents/pty-output-parser.js +10 -4
- 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 +5 -6
- package/src/logger.js +2 -2
- package/src/pace-attachment.js +7 -7
- 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 +10 -2
- package/src/usage-formatters.js +7 -8
- package/src/agents/gemini-direct.js +0 -234
- package/src/agents/gemini.js +0 -463
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Agent Tank is a local dashboard and HTTP API for monitoring usage limits in AI c
|
|
|
7
7
|
It supports:
|
|
8
8
|
|
|
9
9
|
- Claude Code
|
|
10
|
-
-
|
|
10
|
+
- Antigravity CLI
|
|
11
11
|
- OpenAI Codex
|
|
12
12
|
|
|
13
13
|
## How It Gets the Data
|
|
@@ -17,7 +17,7 @@ This is the part that matters.
|
|
|
17
17
|
Agent Tank reads usage directly from the local CLI tools you already use. It launches the installed CLIs locally, runs their built-in usage commands, and parses the output into a unified web UI and JSON API.
|
|
18
18
|
|
|
19
19
|
- Claude: runs `/usage`
|
|
20
|
-
-
|
|
20
|
+
- Antigravity: runs `/usage`
|
|
21
21
|
- Codex: prefers JSON-RPC `account/rateLimits/read`, falls back to `/status`
|
|
22
22
|
|
|
23
23
|
What it does not do:
|
|
@@ -37,7 +37,7 @@ Agent Tank is meant for subscription-based coding agent products where the CLI i
|
|
|
37
37
|
Examples:
|
|
38
38
|
|
|
39
39
|
- Claude Code Pro / Max
|
|
40
|
-
-
|
|
40
|
+
- Antigravity CLI with supported subscription plans
|
|
41
41
|
- ChatGPT Codex with supported plans
|
|
42
42
|
|
|
43
43
|
It is not for:
|
|
@@ -87,10 +87,7 @@ agent-tank --no-docker
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
# Monitor only specific agents
|
|
90
|
-
agent-tank --claude --
|
|
91
|
-
|
|
92
|
-
# Prefer direct Gemini quota fetching
|
|
93
|
-
agent-tank --gemini --gemini-mode direct
|
|
90
|
+
agent-tank --claude --agy
|
|
94
91
|
|
|
95
92
|
# Use a custom port
|
|
96
93
|
agent-tank --port 8080
|
|
@@ -130,7 +127,7 @@ agent-tank --refresh-cooldown 60
|
|
|
130
127
|
| Agent | Method | Metrics |
|
|
131
128
|
|---|---|---|
|
|
132
129
|
| Claude | PTY `/usage` or Anthropic OAuth API | Current session, weekly all-models, weekly Sonnet-only |
|
|
133
|
-
|
|
|
130
|
+
| Antigravity | PTY `/usage` | Per-model quota availability and reset windows when reported |
|
|
134
131
|
| Codex | JSON-RPC preferred, PTY fallback | 5-hour limits, weekly limits, model/account info |
|
|
135
132
|
|
|
136
133
|
## Basic Usage
|
|
@@ -186,9 +183,8 @@ Set it to `0` to disable the cooldown entirely.
|
|
|
186
183
|
```text
|
|
187
184
|
Options:
|
|
188
185
|
--claude Enable Claude monitoring
|
|
189
|
-
--
|
|
186
|
+
--agy Enable Antigravity monitoring
|
|
190
187
|
--codex Enable Codex monitoring
|
|
191
|
-
--gemini-mode <mode> Gemini strategy: fallback, direct, pty (default: fallback)
|
|
192
188
|
--port <port> HTTP server port (default: 3456)
|
|
193
189
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
194
190
|
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
@@ -228,7 +224,6 @@ Environment variables override CLI flags and config file values.
|
|
|
228
224
|
| `AGENT_TANK_DOCKER` | Enable/disable Docker bridge bind when `--host` is omitted |
|
|
229
225
|
| `AGENT_TANK_FRESH_PROCESS` | Use fresh process per refresh (`1`/`true`) |
|
|
230
226
|
| `AGENT_TANK_CLAUDE_API` | Use Claude API mode (`1`/`true`) |
|
|
231
|
-
| `AGENT_TANK_GEMINI_MODE` | Gemini strategy: `fallback`, `direct`, or `pty` |
|
|
232
227
|
| `AGENT_TANK_AUTO_REFRESH` | Enable/disable auto-refresh |
|
|
233
228
|
| `AGENT_TANK_AUTO_REFRESH_MODE` | `none`, `interval`, or `activity` |
|
|
234
229
|
| `AGENT_TANK_AUTO_REFRESH_INTERVAL` | Auto-refresh interval in seconds |
|
|
@@ -243,12 +238,11 @@ Environment variables override CLI flags and config file values.
|
|
|
243
238
|
```json
|
|
244
239
|
{
|
|
245
240
|
"claude": true,
|
|
246
|
-
"
|
|
241
|
+
"agy": true,
|
|
247
242
|
"codex": false,
|
|
248
243
|
"port": 8080,
|
|
249
244
|
"dockerAccess": true,
|
|
250
245
|
"claudeApi": false,
|
|
251
|
-
"geminiMode": "fallback",
|
|
252
246
|
"refreshCooldown": 30,
|
|
253
247
|
"autoRefresh": {
|
|
254
248
|
"mode": "activity",
|
|
@@ -272,26 +266,17 @@ Run with:
|
|
|
272
266
|
agent-tank -c config.json
|
|
273
267
|
```
|
|
274
268
|
|
|
275
|
-
##
|
|
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:
|
|
269
|
+
## Antigravity
|
|
283
270
|
|
|
284
|
-
|
|
285
|
-
- `direct`: use the direct quota API only
|
|
286
|
-
- `pty`: use interactive Gemini CLI `/stats` only
|
|
271
|
+
Antigravity support uses the `agy` CLI and reads the interactive `/usage` output.
|
|
287
272
|
|
|
288
273
|
In `--once` mode, Agent Tank prints the result and exits immediately after the output is flushed.
|
|
289
274
|
|
|
290
275
|
Examples:
|
|
291
276
|
|
|
292
277
|
```bash
|
|
293
|
-
agent-tank --
|
|
294
|
-
agent-tank --
|
|
278
|
+
agent-tank --agy
|
|
279
|
+
agent-tank --agy --once --json
|
|
295
280
|
```
|
|
296
281
|
|
|
297
282
|
## HTTP API
|
|
@@ -421,7 +406,7 @@ In activity mode, Agent Tank watches common local directories:
|
|
|
421
406
|
|
|
422
407
|
- Claude: `~/.config/claude/projects/`, `~/.claude/`
|
|
423
408
|
- Codex: `~/.codex/sessions/`, `~/.codex/`
|
|
424
|
-
-
|
|
409
|
+
- Antigravity: `~/.gemini/antigravity-cli/`
|
|
425
410
|
|
|
426
411
|
If no suitable directories are found, it falls back to interval mode.
|
|
427
412
|
|
|
@@ -509,15 +494,15 @@ You need at least one supported CLI installed and authenticated.
|
|
|
509
494
|
|
|
510
495
|
- [Claude Code](https://claude.ai/download) as `claude`
|
|
511
496
|
- Version 2.0+ required for `/usage`
|
|
512
|
-
-
|
|
513
|
-
-
|
|
497
|
+
- Antigravity CLI as `agy`
|
|
498
|
+
- `/usage` command support required
|
|
514
499
|
- OpenAI Codex as `codex`
|
|
515
500
|
|
|
516
501
|
Useful checks:
|
|
517
502
|
|
|
518
503
|
```bash
|
|
519
504
|
claude --version
|
|
520
|
-
|
|
505
|
+
agy --version
|
|
521
506
|
codex --version
|
|
522
507
|
```
|
|
523
508
|
|
|
@@ -527,7 +512,7 @@ codex --version
|
|
|
527
512
|
const { AgentTank } = require('agent-tank');
|
|
528
513
|
|
|
529
514
|
const watcher = new AgentTank({
|
|
530
|
-
agents: ['claude', '
|
|
515
|
+
agents: ['claude', 'agy'],
|
|
531
516
|
port: 3456,
|
|
532
517
|
autoDiscover: true
|
|
533
518
|
});
|
package/bin/agent-tank.js
CHANGED
|
@@ -8,10 +8,9 @@ const pkg = require('../package.json');
|
|
|
8
8
|
|
|
9
9
|
const options = {
|
|
10
10
|
claude: { type: 'boolean', default: false },
|
|
11
|
-
|
|
11
|
+
agy: { type: 'boolean', default: false },
|
|
12
12
|
codex: { type: 'boolean', default: false },
|
|
13
13
|
'claude-api': { type: 'boolean', default: false },
|
|
14
|
-
'gemini-mode': { type: 'string' },
|
|
15
14
|
port: { type: 'string', default: '3456' },
|
|
16
15
|
host: { type: 'string' },
|
|
17
16
|
docker: { type: 'boolean', default: true },
|
|
@@ -63,10 +62,9 @@ Usage: agent-tank [options]
|
|
|
63
62
|
|
|
64
63
|
Options:
|
|
65
64
|
--claude Enable Claude monitoring
|
|
66
|
-
--
|
|
65
|
+
--agy Enable Antigravity monitoring
|
|
67
66
|
--codex Enable Codex monitoring
|
|
68
67
|
--claude-api Use direct Anthropic API for Claude usage (faster, 60s refresh)
|
|
69
|
-
--gemini-mode <mode> Gemini strategy: fallback, direct, pty (default: fallback)
|
|
70
68
|
--port <port> HTTP server port (default: 3456)
|
|
71
69
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
72
70
|
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
@@ -103,7 +101,6 @@ Environment variables:
|
|
|
103
101
|
AGENT_TANK_DOCKER Enable/disable Docker bridge bind ("1"/"true" or "0"/"false")
|
|
104
102
|
AGENT_TANK_FRESH_PROCESS Use fresh process per refresh ("1" or "true")
|
|
105
103
|
AGENT_TANK_CLAUDE_API Use direct Anthropic API for Claude usage ("1" or "true")
|
|
106
|
-
AGENT_TANK_GEMINI_MODE Gemini strategy: fallback, direct, pty
|
|
107
104
|
AGENT_TANK_AUTO_REFRESH Enable/disable background auto-refresh ("1" or "true" / "0" or "false")
|
|
108
105
|
AGENT_TANK_AUTO_REFRESH_MODE Refresh mode: none, interval, activity
|
|
109
106
|
AGENT_TANK_AUTO_REFRESH_INTERVAL Auto-refresh interval in seconds
|
|
@@ -115,11 +112,10 @@ Environment variables:
|
|
|
115
112
|
|
|
116
113
|
Examples:
|
|
117
114
|
agent-tank # Auto-discover and monitor all available
|
|
118
|
-
agent-tank --claude --
|
|
115
|
+
agent-tank --claude --agy # Monitor specific agents
|
|
119
116
|
agent-tank --port 8080 # Use custom port
|
|
120
117
|
agent-tank --host 0.0.0.0 # Expose on all interfaces
|
|
121
118
|
agent-tank --no-docker # Bind localhost only when host is omitted
|
|
122
|
-
agent-tank --gemini-mode direct # Prefer direct Gemini quota API calls
|
|
123
119
|
agent-tank --auth-user admin --auth-pass secret # Enable basic auth
|
|
124
120
|
agent-tank --auth-token mykey # Enable API key auth
|
|
125
121
|
agent-tank -c ./config.json # Use config file
|
|
@@ -174,7 +170,7 @@ async function main() {
|
|
|
174
170
|
// Merge CLI options with config (env vars > CLI flags > config file)
|
|
175
171
|
const agents = [];
|
|
176
172
|
if (values.claude || config.claude) agents.push('claude');
|
|
177
|
-
if (values.
|
|
173
|
+
if (values.agy || config.agy) agents.push('agy');
|
|
178
174
|
if (values.codex || config.codex) agents.push('codex');
|
|
179
175
|
|
|
180
176
|
const auth = {
|
|
@@ -203,9 +199,6 @@ async function main() {
|
|
|
203
199
|
config.claudeApi ||
|
|
204
200
|
claudeApiEnv === '1' || claudeApiEnv === 'true';
|
|
205
201
|
|
|
206
|
-
const geminiModeEnv = process.env.AGENT_TANK_GEMINI_MODE;
|
|
207
|
-
const geminiMode = geminiModeEnv || values['gemini-mode'] || config.geminiMode || 'fallback';
|
|
208
|
-
|
|
209
202
|
// Auto-refresh configuration (env > CLI > config file)
|
|
210
203
|
const autoRefreshEnv = process.env.AGENT_TANK_AUTO_REFRESH;
|
|
211
204
|
let autoRefreshEnabled = values['auto-refresh'];
|
|
@@ -298,7 +291,6 @@ async function main() {
|
|
|
298
291
|
auth,
|
|
299
292
|
freshProcess,
|
|
300
293
|
claudeApi,
|
|
301
|
-
geminiMode,
|
|
302
294
|
autoRefreshEnabled: onceMode ? false : autoRefreshEnabled, // Disable auto-refresh in one-shot mode
|
|
303
295
|
autoRefreshInterval,
|
|
304
296
|
autoRefreshMode: onceMode ? 'none' : autoRefreshMode, // Disable auto-refresh in one-shot mode
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tank",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Monitor and query usage limits for LLM CLI tools (Claude,
|
|
3
|
+
"version": "0.9.7",
|
|
4
|
+
"description": "Monitor and query usage limits for LLM CLI tools (Claude, Antigravity, Codex) via HTTP API",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"agent-tank": "bin/agent-tank.js"
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"keywords": [
|
|
20
20
|
"llm",
|
|
21
21
|
"claude",
|
|
22
|
-
"
|
|
22
|
+
"antigravity",
|
|
23
|
+
"agy",
|
|
23
24
|
"codex",
|
|
24
25
|
"openai",
|
|
25
26
|
"anthropic",
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
},
|
|
43
44
|
"homepage": "https://agenttank.io",
|
|
44
45
|
"dependencies": {
|
|
46
|
+
"chokidar": "^3.6.0",
|
|
45
47
|
"node-pty": "^1.0.0",
|
|
46
48
|
"source-map-support": "^0.5.21"
|
|
47
49
|
},
|
package/src/activity-monitor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ActivityMonitor - Monitors local log directories for LLM CLI activity
|
|
3
3
|
*
|
|
4
|
-
* Watches known log directories for Claude, Codex, and
|
|
4
|
+
* Watches known log directories for Claude, Codex, and Antigravity using chokidar with polling.
|
|
5
5
|
* When activity is detected (new log entries), it triggers usage refresh callbacks
|
|
6
6
|
* with configurable debouncing to prevent excessive polling.
|
|
7
7
|
*/
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
const fs = require('node:fs');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const os = require('node:os');
|
|
12
|
+
const chokidar = require('chokidar');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Default log directories for each agent.
|
|
@@ -23,9 +24,8 @@ const DEFAULT_LOG_DIRECTORIES = {
|
|
|
23
24
|
path.join(os.homedir(), '.codex', 'sessions'),
|
|
24
25
|
path.join(os.homedir(), '.codex'),
|
|
25
26
|
],
|
|
26
|
-
|
|
27
|
-
path.join(os.homedir(), '.
|
|
28
|
-
path.join(os.homedir(), '.gemini'),
|
|
27
|
+
agy: [
|
|
28
|
+
path.join(os.homedir(), '.gemini', 'antigravity-cli'),
|
|
29
29
|
],
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -72,7 +72,7 @@ class ActivityMonitor {
|
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* Start monitoring log directories for activity.
|
|
75
|
-
* Sets up
|
|
75
|
+
* Sets up chokidar polling watchers on each agent's log directories.
|
|
76
76
|
*
|
|
77
77
|
* @returns {Object} Status object with monitoring details
|
|
78
78
|
*/
|
|
@@ -92,7 +92,18 @@ class ActivityMonitor {
|
|
|
92
92
|
for (const dir of dirs) {
|
|
93
93
|
if (fs.existsSync(dir)) {
|
|
94
94
|
try {
|
|
95
|
-
|
|
95
|
+
// Use chokidar with polling to avoid EMFILE errors from too many native watchers
|
|
96
|
+
const watcher = chokidar.watch(dir, {
|
|
97
|
+
persistent: true,
|
|
98
|
+
ignoreInitial: true,
|
|
99
|
+
usePolling: true,
|
|
100
|
+
interval: 1000,
|
|
101
|
+
depth: 99,
|
|
102
|
+
ignored: ['**/.git/**', '**/*.lock'],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
watcher.on('all', (eventType, filepath) => {
|
|
106
|
+
const filename = path.relative(dir, filepath);
|
|
96
107
|
this._handleFileChange(agent, eventType, filename);
|
|
97
108
|
});
|
|
98
109
|
|
|
@@ -158,7 +169,7 @@ class ActivityMonitor {
|
|
|
158
169
|
}
|
|
159
170
|
|
|
160
171
|
/**
|
|
161
|
-
* Handle a file change event from
|
|
172
|
+
* Handle a file change event from chokidar.
|
|
162
173
|
* Debounces activity detection per agent.
|
|
163
174
|
*
|
|
164
175
|
* @param {string} agent - Agent name
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
const { BaseAgent } = require('./base.js');
|
|
2
|
+
const { calculatePace } = require('../pace-evaluator.js');
|
|
3
|
+
const { CYCLE_DURATIONS } = require('../usage-formatters.js');
|
|
4
|
+
const logger = require('../logger.js');
|
|
5
|
+
|
|
6
|
+
class AgyAgent extends BaseAgent {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('agy', 'agy', ['--dangerously-skip-permissions']);
|
|
9
|
+
this._aboutSent = false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getTimeout() {
|
|
13
|
+
return 35000; // Antigravity can take 10+ seconds to start and sign in
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getEnv() {
|
|
17
|
+
// Use xterm-256color for proper terminal support
|
|
18
|
+
// Antigravity CLI needs good terminal emulation for its TUI
|
|
19
|
+
return {
|
|
20
|
+
...process.env,
|
|
21
|
+
TERM: 'xterm-256color',
|
|
22
|
+
COLORTERM: 'truecolor',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_hasReadyPrompt(output) {
|
|
27
|
+
const clean = this.stripAnsi(output);
|
|
28
|
+
return /Antigravity CLI[\s\S]*\n>\s*(?:\n|$)/i.test(clean)
|
|
29
|
+
|| /Google AI Pro[\s\S]*\n>\s*(?:\n|$)/i.test(clean)
|
|
30
|
+
|| /Ready\s*\(/.test(clean);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isReadyForCommands(output) {
|
|
34
|
+
// Don't consider ready if authentication is in progress
|
|
35
|
+
if (this._isAuthenticating(output)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return this._hasReadyPrompt(output);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_isAuthenticating(output) {
|
|
42
|
+
// Detect OAuth/authentication screens where Escape would cancel the flow
|
|
43
|
+
const clean = this.stripAnsi(output);
|
|
44
|
+
const hasAuthFlow = /waiting for authentication/i.test(clean)
|
|
45
|
+
|| /signing in/i.test(clean)
|
|
46
|
+
|| /press esc.*to cancel/i.test(clean)
|
|
47
|
+
|| /authenticating\.\.\./i.test(clean);
|
|
48
|
+
|
|
49
|
+
if (!hasAuthFlow) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Footer hints can render before the actual input prompt is available.
|
|
54
|
+
// Treat auth as complete only when the real prompt has returned.
|
|
55
|
+
return !this._hasReadyPrompt(clean);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
detectAuthenticationState(output) {
|
|
59
|
+
const clean = this.stripAnsi(output);
|
|
60
|
+
if (!clean) return null;
|
|
61
|
+
|
|
62
|
+
const hasAuthFailure = /failed to sign in\.\s*message:\s*([^\n]+)/i.test(clean)
|
|
63
|
+
|| /authentication consent could not be obtained/i.test(clean);
|
|
64
|
+
if (hasAuthFailure) {
|
|
65
|
+
const failureMatch = clean.match(/failed to sign in\.\s*message:\s*([^\n]+)/i);
|
|
66
|
+
const detail = failureMatch
|
|
67
|
+
? `Failed to sign in: ${failureMatch[1].trim()}`
|
|
68
|
+
: 'Antigravity CLI authentication failed.';
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
authenticated: false,
|
|
72
|
+
status: 'unauthenticated',
|
|
73
|
+
message: 'Authentication required',
|
|
74
|
+
detail,
|
|
75
|
+
action: 'Run agy in an interactive terminal to authenticate.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this._hasReadyPrompt(clean) || /signing in/i.test(clean)) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hasAuthMenu = /how would you like to authenticate for this project\?/i.test(clean)
|
|
84
|
+
|| /currently not signed in/i.test(clean);
|
|
85
|
+
|
|
86
|
+
if (!hasAuthMenu) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
authenticated: false,
|
|
92
|
+
status: 'unauthenticated',
|
|
93
|
+
message: 'Authentication required',
|
|
94
|
+
detail: 'Antigravity CLI is waiting for authentication.',
|
|
95
|
+
action: 'Run agy in an interactive terminal to authenticate.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
handleTrustPrompt(shell, output) {
|
|
100
|
+
// Check for trust prompt with various wordings
|
|
101
|
+
const trustPatterns = [
|
|
102
|
+
'Do you trust',
|
|
103
|
+
'trust the files',
|
|
104
|
+
'trust this folder',
|
|
105
|
+
'Trust this workspace',
|
|
106
|
+
'allow access',
|
|
107
|
+
'grant access',
|
|
108
|
+
'grant full access',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (trustPatterns.some(pattern => output.toLowerCase().includes(pattern.toLowerCase()))) {
|
|
112
|
+
logger.agent(this.name, 'Detected trust prompt, auto-accepting...');
|
|
113
|
+
// Numbered menu with "1. Trust folder" pre-selected; Enter accepts it.
|
|
114
|
+
shell.write('1\r');
|
|
115
|
+
|
|
116
|
+
// Antigravity can redraw after trust; clear transitional UI before readiness.
|
|
117
|
+
setTimeout(() => {
|
|
118
|
+
if (!shell) return;
|
|
119
|
+
logger.agent(this.name, 'Post-trust: sending Escape to clear restart UI...');
|
|
120
|
+
shell.write('\x1b');
|
|
121
|
+
}, 3000);
|
|
122
|
+
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
hasCompleteOutput(output) {
|
|
129
|
+
const clean = this.stripAnsi(output);
|
|
130
|
+
if (this.detectAuthenticationState(clean)) return true;
|
|
131
|
+
if (/Model\s+Quota/i.test(clean)) {
|
|
132
|
+
return /(?:Gemini|Claude|GPT)[\s\S]*\d+(?:\.\d+)?%/i.test(clean);
|
|
133
|
+
}
|
|
134
|
+
const hasModel = /(?:Gemini|Claude|GPT)[^\n]+/i.test(clean);
|
|
135
|
+
const hasPercent = /\d+(?:\.\d+)?%/i.test(clean);
|
|
136
|
+
return hasModel && hasPercent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Convert duration string like "3h 26m" or "5d 2h" to seconds
|
|
140
|
+
parseDurationToSeconds(durationStr) {
|
|
141
|
+
if (!durationStr) return null;
|
|
142
|
+
|
|
143
|
+
let totalSeconds = 0;
|
|
144
|
+
|
|
145
|
+
// Match days
|
|
146
|
+
const daysMatch = durationStr.match(/(\d+)\s*d/i);
|
|
147
|
+
if (daysMatch) {
|
|
148
|
+
totalSeconds += parseInt(daysMatch[1]) * 24 * 60 * 60;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Match hours
|
|
152
|
+
const hoursMatch = durationStr.match(/(\d+)\s*h/i);
|
|
153
|
+
if (hoursMatch) {
|
|
154
|
+
totalSeconds += parseInt(hoursMatch[1]) * 60 * 60;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Match minutes
|
|
158
|
+
const minsMatch = durationStr.match(/(\d+)\s*m/i);
|
|
159
|
+
if (minsMatch) {
|
|
160
|
+
totalSeconds += parseInt(minsMatch[1]) * 60;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return totalSeconds > 0 ? totalSeconds : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sendCommands(shell, output) {
|
|
167
|
+
// Check if authentication is still in progress - don't send commands that could interfere
|
|
168
|
+
if (this._isAuthenticating(output) || this.detectAuthenticationState(output)) {
|
|
169
|
+
logger.agent(this.name, 'Authentication required or in progress, cannot send /usage command');
|
|
170
|
+
// Don't retry - let the timeout handle it. Sending Escape would cancel auth.
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
logger.agent(this.name, 'Sending /usage command...');
|
|
174
|
+
setTimeout(() => shell.write('/usage'), 200);
|
|
175
|
+
setTimeout(() => shell.write('\r'), 600);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Calculate pace data for a model entry when reset timing is available.
|
|
179
|
+
_addPaceData(entry, resetsInSeconds) {
|
|
180
|
+
const cycleDuration = CYCLE_DURATIONS.sessionAgy;
|
|
181
|
+
if (!cycleDuration || resetsInSeconds == null) return;
|
|
182
|
+
const paceData = calculatePace({ usagePercent: entry.percentUsed, resetsInSeconds, cycleDurationSeconds: cycleDuration });
|
|
183
|
+
if (paceData) entry.pace = paceData;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
parseOutput(output) {
|
|
187
|
+
const clean = this.stripAnsi(output);
|
|
188
|
+
const usage = {
|
|
189
|
+
models: [],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this._parseAgyUsage(clean, usage);
|
|
193
|
+
|
|
194
|
+
// Detect version update notification in output
|
|
195
|
+
const updateMatch = clean.match(/[Uu]pdate\s+available[^\d]*([\d.]+)\s*(?:→|->|to)\s*([\d.]+)/);
|
|
196
|
+
if (updateMatch) {
|
|
197
|
+
usage.version = { current: updateMatch[1], latest: updateMatch[2] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return usage;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_parseAgyUsage(clean, usage) {
|
|
204
|
+
if (!/Model\s+Quota/i.test(clean)) return;
|
|
205
|
+
|
|
206
|
+
const lines = clean.split('\n').map(line => line.trim()).filter(Boolean);
|
|
207
|
+
const knownMarkers = new Set([
|
|
208
|
+
'Model Quota',
|
|
209
|
+
'Quota available',
|
|
210
|
+
'? for shortcuts',
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < lines.length; i++) {
|
|
214
|
+
const modelLine = lines[i];
|
|
215
|
+
if (knownMarkers.has(modelLine) || /^(?:[>└]|[-─↑/↓]|esc\s+to\s+cancel)/i.test(modelLine)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!/[A-Za-z]/.test(modelLine) || /\d+\s*%/.test(modelLine)) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const percentLine = lines[i + 1] || '';
|
|
223
|
+
const percentMatch = percentLine.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
224
|
+
if (!percentMatch) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const usageLeft = parseFloat(percentMatch[1]);
|
|
229
|
+
const percentUsed = Number((100 - usageLeft).toFixed(1));
|
|
230
|
+
const resetLine = lines[i + 2] || '';
|
|
231
|
+
const resetMatch = resetLine.match(/(?:resets?|available)\s*(?:in|at)?\s*([0-9dhm\s]+)?/i);
|
|
232
|
+
const resetsIn = resetMatch?.[1]?.trim() || null;
|
|
233
|
+
const resetsInSeconds = resetsIn ? this.parseDurationToSeconds(resetsIn) : null;
|
|
234
|
+
|
|
235
|
+
if (!usage.models.find(m => m.model === modelLine)) {
|
|
236
|
+
const modelEntry = { model: modelLine, usageLeft, percentUsed, resetsIn, resetsInSeconds };
|
|
237
|
+
this._addPaceData(modelEntry, resetsInSeconds);
|
|
238
|
+
usage.models.push(modelEntry);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async fetchMetadata() {
|
|
244
|
+
if (this._aboutSent) {
|
|
245
|
+
return this.metadata;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._aboutSent = true;
|
|
249
|
+
return this._parseAgyMetadata(this.output);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_parseAgyMetadata(output) {
|
|
253
|
+
const clean = this.stripAnsi(output);
|
|
254
|
+
const metadata = {};
|
|
255
|
+
|
|
256
|
+
const versionMatch = clean.match(/Antigravity\s+CLI\s+v?([\d.]+)/i);
|
|
257
|
+
if (versionMatch) metadata.version = versionMatch[1];
|
|
258
|
+
|
|
259
|
+
const emailMatch = clean.match(/(\S+@\S+)/);
|
|
260
|
+
if (emailMatch) metadata.email = this.stripBoxChars(emailMatch[1]);
|
|
261
|
+
|
|
262
|
+
const cwdMatch = clean.match(/\n\s*(\/[^\n]+)\n/);
|
|
263
|
+
if (cwdMatch) metadata.cwd = this.stripBoxChars(cwdMatch[1]);
|
|
264
|
+
|
|
265
|
+
const modelMatch = clean.match(/((?:Gemini|Claude|GPT)[^\n]+?)\s+\((?:Google AI|[^)]*Pro|[^)]*Free|[^)]*)\)/i)
|
|
266
|
+
|| clean.match(/\?\s+for\s+shortcuts\s+(.+?)(?:\n|$)/i);
|
|
267
|
+
if (modelMatch) metadata.model = this.stripBoxChars(modelMatch[1]);
|
|
268
|
+
|
|
269
|
+
metadata.cli = 'agy';
|
|
270
|
+
return Object.keys(metadata).length > 1 ? metadata : { cli: 'agy' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Lightweight keepalive to prevent session expiration.
|
|
275
|
+
* Sends escape key to maintain the PTY session without triggering API calls.
|
|
276
|
+
*
|
|
277
|
+
* @returns {Promise<boolean>} True if keepalive succeeded
|
|
278
|
+
*/
|
|
279
|
+
async keepalive() {
|
|
280
|
+
if (this.freshProcess) {
|
|
281
|
+
console.log(`[${this.name}] Keepalive skipped (fresh process mode)`);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!this.shell || !this.processReady) {
|
|
286
|
+
console.log(`[${this.name}] Keepalive: spawning process...`);
|
|
287
|
+
await this.spawnProcess();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (this.shell) {
|
|
291
|
+
console.log(`[${this.name}] Keepalive: sending ping...`);
|
|
292
|
+
// Send escape key to dismiss any UI and trigger activity
|
|
293
|
+
this.shell.write('\x1b');
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { AgyAgent };
|
package/src/agents/base.js
CHANGED
|
@@ -351,7 +351,7 @@ class BaseAgent {
|
|
|
351
351
|
this.output += data;
|
|
352
352
|
|
|
353
353
|
// Always respond to terminal queries — suppressing them causes deadlocks
|
|
354
|
-
// when CLIs
|
|
354
|
+
// when CLIs wait for cursor position responses before
|
|
355
355
|
// rendering output. Delay slightly during command-in-flight to avoid
|
|
356
356
|
// interleaving with command text being sent via setTimeout.
|
|
357
357
|
if (this._commandInFlight) {
|