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 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
- - Gemini CLI
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
- - Gemini: runs `/stats`
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
- - Gemini CLI with supported subscription plans
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 --gemini
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
- | Gemini | PTY `/stats` | Per-model usage and reset windows |
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
- --gemini Enable Gemini monitoring
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
- "gemini": true,
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
- ## 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:
269
+ ## Antigravity
283
270
 
284
- - `fallback`: direct first, PTY fallback
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 --gemini --gemini-mode direct
294
- agent-tank --gemini --gemini-mode pty
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
- - Gemini: `~/.config/gemini/`, `~/.gemini/`
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
- - Gemini CLI as `gemini`
513
- - Version 0.24.5+ required for `/stats`
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
- gemini --version
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', 'gemini'],
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
- gemini: { type: 'boolean', default: false },
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
- --gemini Enable Gemini monitoring
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 --gemini # Monitor specific agents
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.gemini || config.gemini) agents.push('gemini');
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.6",
4
- "description": "Monitor and query usage limits for LLM CLI tools (Claude, Gemini, Codex) via HTTP API",
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
- "gemini",
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
  },
@@ -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 Gemini using fs.watch.
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
- gemini: [
27
- path.join(os.homedir(), '.config', 'gemini'),
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 fs.watch on each agent's log directories.
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
- const watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
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 fs.watch.
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 };
@@ -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 (e.g. Gemini) wait for cursor position responses before
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) {