agent-tank 0.9.5 → 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 +25 -12
- package/bin/agent-tank.js +33 -15
- package/package.json +5 -3
- package/src/activity-monitor.js +18 -7
- package/src/agents/agy.js +301 -0
- package/src/agents/base.js +18 -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 -5
- 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.js +0 -412
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,7 +87,7 @@ agent-tank --no-docker
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
# Monitor only specific agents
|
|
90
|
-
agent-tank --claude --
|
|
90
|
+
agent-tank --claude --agy
|
|
91
91
|
|
|
92
92
|
# Use a custom port
|
|
93
93
|
agent-tank --port 8080
|
|
@@ -127,7 +127,7 @@ agent-tank --refresh-cooldown 60
|
|
|
127
127
|
| Agent | Method | Metrics |
|
|
128
128
|
|---|---|---|
|
|
129
129
|
| Claude | PTY `/usage` or Anthropic OAuth API | Current session, weekly all-models, weekly Sonnet-only |
|
|
130
|
-
|
|
|
130
|
+
| Antigravity | PTY `/usage` | Per-model quota availability and reset windows when reported |
|
|
131
131
|
| Codex | JSON-RPC preferred, PTY fallback | 5-hour limits, weekly limits, model/account info |
|
|
132
132
|
|
|
133
133
|
## Basic Usage
|
|
@@ -183,7 +183,7 @@ Set it to `0` to disable the cooldown entirely.
|
|
|
183
183
|
```text
|
|
184
184
|
Options:
|
|
185
185
|
--claude Enable Claude monitoring
|
|
186
|
-
--
|
|
186
|
+
--agy Enable Antigravity monitoring
|
|
187
187
|
--codex Enable Codex monitoring
|
|
188
188
|
--port <port> HTTP server port (default: 3456)
|
|
189
189
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
@@ -238,7 +238,7 @@ Environment variables override CLI flags and config file values.
|
|
|
238
238
|
```json
|
|
239
239
|
{
|
|
240
240
|
"claude": true,
|
|
241
|
-
"
|
|
241
|
+
"agy": true,
|
|
242
242
|
"codex": false,
|
|
243
243
|
"port": 8080,
|
|
244
244
|
"dockerAccess": true,
|
|
@@ -266,6 +266,19 @@ Run with:
|
|
|
266
266
|
agent-tank -c config.json
|
|
267
267
|
```
|
|
268
268
|
|
|
269
|
+
## Antigravity
|
|
270
|
+
|
|
271
|
+
Antigravity support uses the `agy` CLI and reads the interactive `/usage` output.
|
|
272
|
+
|
|
273
|
+
In `--once` mode, Agent Tank prints the result and exits immediately after the output is flushed.
|
|
274
|
+
|
|
275
|
+
Examples:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
agent-tank --agy
|
|
279
|
+
agent-tank --agy --once --json
|
|
280
|
+
```
|
|
281
|
+
|
|
269
282
|
## HTTP API
|
|
270
283
|
|
|
271
284
|
### Endpoints
|
|
@@ -393,7 +406,7 @@ In activity mode, Agent Tank watches common local directories:
|
|
|
393
406
|
|
|
394
407
|
- Claude: `~/.config/claude/projects/`, `~/.claude/`
|
|
395
408
|
- Codex: `~/.codex/sessions/`, `~/.codex/`
|
|
396
|
-
-
|
|
409
|
+
- Antigravity: `~/.gemini/antigravity-cli/`
|
|
397
410
|
|
|
398
411
|
If no suitable directories are found, it falls back to interval mode.
|
|
399
412
|
|
|
@@ -481,15 +494,15 @@ You need at least one supported CLI installed and authenticated.
|
|
|
481
494
|
|
|
482
495
|
- [Claude Code](https://claude.ai/download) as `claude`
|
|
483
496
|
- Version 2.0+ required for `/usage`
|
|
484
|
-
-
|
|
485
|
-
-
|
|
497
|
+
- Antigravity CLI as `agy`
|
|
498
|
+
- `/usage` command support required
|
|
486
499
|
- OpenAI Codex as `codex`
|
|
487
500
|
|
|
488
501
|
Useful checks:
|
|
489
502
|
|
|
490
503
|
```bash
|
|
491
504
|
claude --version
|
|
492
|
-
|
|
505
|
+
agy --version
|
|
493
506
|
codex --version
|
|
494
507
|
```
|
|
495
508
|
|
|
@@ -499,7 +512,7 @@ codex --version
|
|
|
499
512
|
const { AgentTank } = require('agent-tank');
|
|
500
513
|
|
|
501
514
|
const watcher = new AgentTank({
|
|
502
|
-
agents: ['claude', '
|
|
515
|
+
agents: ['claude', 'agy'],
|
|
503
516
|
port: 3456,
|
|
504
517
|
autoDiscover: true
|
|
505
518
|
});
|
package/bin/agent-tank.js
CHANGED
|
@@ -8,7 +8,7 @@ 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
14
|
port: { type: 'string', default: '3456' },
|
|
@@ -43,6 +43,17 @@ function exitWithCode(code, message, stream = process.stderr) {
|
|
|
43
43
|
process.exitCode = code;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function writeAndExit(stream, text, exitCode, { cleanup, watcher, jsonMode, originalLog }) {
|
|
47
|
+
stream.write(text, () => {
|
|
48
|
+
cleanup();
|
|
49
|
+
watcher.stop();
|
|
50
|
+
if (jsonMode) {
|
|
51
|
+
console.log = originalLog;
|
|
52
|
+
}
|
|
53
|
+
process.exit(exitCode);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
function printHelp() {
|
|
47
58
|
process.stdout.write(`
|
|
48
59
|
agent-tank - Monitor LLM CLI usage limits
|
|
@@ -51,7 +62,7 @@ Usage: agent-tank [options]
|
|
|
51
62
|
|
|
52
63
|
Options:
|
|
53
64
|
--claude Enable Claude monitoring
|
|
54
|
-
--
|
|
65
|
+
--agy Enable Antigravity monitoring
|
|
55
66
|
--codex Enable Codex monitoring
|
|
56
67
|
--claude-api Use direct Anthropic API for Claude usage (faster, 60s refresh)
|
|
57
68
|
--port <port> HTTP server port (default: 3456)
|
|
@@ -101,7 +112,7 @@ Environment variables:
|
|
|
101
112
|
|
|
102
113
|
Examples:
|
|
103
114
|
agent-tank # Auto-discover and monitor all available
|
|
104
|
-
agent-tank --claude --
|
|
115
|
+
agent-tank --claude --agy # Monitor specific agents
|
|
105
116
|
agent-tank --port 8080 # Use custom port
|
|
106
117
|
agent-tank --host 0.0.0.0 # Expose on all interfaces
|
|
107
118
|
agent-tank --no-docker # Bind localhost only when host is omitted
|
|
@@ -159,7 +170,7 @@ async function main() {
|
|
|
159
170
|
// Merge CLI options with config (env vars > CLI flags > config file)
|
|
160
171
|
const agents = [];
|
|
161
172
|
if (values.claude || config.claude) agents.push('claude');
|
|
162
|
-
if (values.
|
|
173
|
+
if (values.agy || config.agy) agents.push('agy');
|
|
163
174
|
if (values.codex || config.codex) agents.push('codex');
|
|
164
175
|
|
|
165
176
|
const auth = {
|
|
@@ -311,21 +322,28 @@ async function main() {
|
|
|
311
322
|
await watcher.start();
|
|
312
323
|
const status = watcher.getStatus();
|
|
313
324
|
if (jsonMode) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
325
|
+
const output = `${JSON.stringify(status, null, 2)}\n`;
|
|
326
|
+
writeAndExit(process.stdout, output, 0, {
|
|
327
|
+
cleanup: cleanupShutdownHandlers,
|
|
328
|
+
watcher,
|
|
329
|
+
jsonMode,
|
|
330
|
+
originalLog,
|
|
331
|
+
});
|
|
332
|
+
return;
|
|
319
333
|
} else {
|
|
334
|
+
let output = '';
|
|
320
335
|
for (const [name, agentStatus] of Object.entries(status)) {
|
|
321
|
-
|
|
322
|
-
|
|
336
|
+
output += `\n=== ${name.toUpperCase()} ===\n`;
|
|
337
|
+
output += `${JSON.stringify(agentStatus, null, 2)}\n`;
|
|
323
338
|
}
|
|
339
|
+
writeAndExit(process.stdout, output, 0, {
|
|
340
|
+
cleanup: cleanupShutdownHandlers,
|
|
341
|
+
watcher,
|
|
342
|
+
jsonMode,
|
|
343
|
+
originalLog,
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
324
346
|
}
|
|
325
|
-
cleanupShutdownHandlers();
|
|
326
|
-
watcher.stop();
|
|
327
|
-
process.exitCode = 0;
|
|
328
|
-
return;
|
|
329
347
|
}
|
|
330
348
|
|
|
331
349
|
await watcher.start();
|
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
|
@@ -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,11 +343,15 @@ 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
|
|
|
340
353
|
// Always respond to terminal queries — suppressing them causes deadlocks
|
|
341
|
-
// when CLIs
|
|
354
|
+
// when CLIs wait for cursor position responses before
|
|
342
355
|
// rendering output. Delay slightly during command-in-flight to avoid
|
|
343
356
|
// interleaving with command text being sent via setTimeout.
|
|
344
357
|
if (this._commandInFlight) {
|
|
@@ -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 = [];
|