agent-tank 0.9.9 → 0.9.11
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/CHANGELOG.md +157 -0
- package/README.md +84 -9
- package/bin/agent-tank.js +26 -8
- package/favicon.svg +7 -0
- package/package.json +3 -1
- package/src/activity-monitor.js +6 -1
- package/src/agent-config.js +105 -0
- package/src/agents/agy.js +48 -10
- package/src/agents/api-response-parser.js +17 -17
- package/src/agents/base.js +29 -14
- package/src/agents/claude.js +149 -22
- package/src/agents/codex-rpc-helpers.js +166 -34
- package/src/agents/codex.js +42 -9
- package/src/agents/oauth-helper.js +12 -4
- package/src/agents/pty-output-parser.js +28 -10
- package/src/auto-refresh-manager.js +6 -1
- package/src/client-auto-refresh-helpers.js +13 -13
- package/src/client-auto-refresh.js +9 -7
- package/src/client-script.js +1 -1
- package/src/index.js +49 -22
- package/src/pace-attachment.js +2 -2
- package/src/snapshot-metrics.js +1 -1
- package/src/status-page.js +11 -6
- package/src/usage-formatters.js +14 -12
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.9.11] - 2026-09-29
|
|
9
|
+
|
|
10
|
+
### Verified
|
|
11
|
+
|
|
12
|
+
Verified with the following agent versions (versions checked on 2026-09-29):
|
|
13
|
+
|
|
14
|
+
- Claude Code `2.1.284`
|
|
15
|
+
- Antigravity CLI (`agy`) `1.2.12`
|
|
16
|
+
- Codex CLI `0.158.0`
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Monitor multiple accounts from the same provider using the repeatable `--agent provider[:alias][=config-path]` flag or the config file's `agents` array, with separate config directories, dashboard labels, history, and refresh endpoints
|
|
21
|
+
- Parse Codex model-specific rate-limit buckets alongside the main five-hour and weekly limits
|
|
22
|
+
- Include absolute `resetsAt` timestamps and limit `cycle` identifiers in Antigravity usage entries
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Replace Claude's `weeklySonnet` usage field with `weeklyFable` in PTY and OAuth API parsing, dashboard display, history, and pace calculations; API consumers using `weeklySonnet` must update to the new field
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Recover swallowed Claude `/usage` commands without interrupting partially rendered usage or an active refresh, and parse metadata correctly from compact `/status` output
|
|
31
|
+
- Submit Codex slash commands through the newer command picker and accept weekly-only usage output without waiting for a missing five-hour limit
|
|
32
|
+
- Initialize the Codex app-server JSON-RPC connection and parse its current rate-limit response, including used percentages, window durations, and Unix reset timestamps
|
|
33
|
+
- Preserve Claude Fable allowances at zero usage and wait for delayed allowance rows before dismissing the usage dialog, while allowing accounts without Fable to complete
|
|
34
|
+
- Parse Antigravity's `Refreshes in` reset countdowns, including lines with leftover terminal box borders, and use the correct weekly or five-hour window for pace calculations
|
|
35
|
+
|
|
36
|
+
## [0.9.10] - 2026-06-15
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Recover Claude session usage from the `/usage` dialog even when its "Current session" row is captured mid-render in a corrupted state, instead of returning only weekly and Sonnet data (and timing out)
|
|
41
|
+
- Stop treating a partial Claude rate limit (e.g. the per-model/Sonnet breakdown) as an error: keep the fresh session and weekly data, omit only the rate-limited section, and surface a rate-limit error only when no usable data is returned
|
|
42
|
+
|
|
43
|
+
## [0.9.9] - 2026-06-15
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- Parse the newer Antigravity "Models & Quota" usage screen, keeping each model group's weekly and five-hour limits as distinct, readable entries instead of collapsing them into two generic limits
|
|
48
|
+
- Dismiss the Antigravity usage modal before re-running `/usage`, fixing refreshes that alternated between success and a 35s timeout on the reused session
|
|
49
|
+
- Force-kill agent PTY processes that ignore `SIGTERM`/`SIGHUP` (e.g. `agy`, `codex`) so they no longer survive as orphaned processes after respawns or shutdown
|
|
50
|
+
- Clean up agent processes on `SIGINT` and `SIGHUP` in addition to `SIGTERM`, so detached/background runs shut down cleanly
|
|
51
|
+
|
|
52
|
+
## [0.9.8] - 2026-06-14
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- Add `--background` (and `AGENT_TANK_BACKGROUND`) to start Agent Tank as a detached process that prints its PID and log path, then exits the parent command
|
|
57
|
+
- Add `AGENT_TANK_BACKGROUND_LOG` to set the background child's stdout/stderr log path, and `AGENT_TANK_BACKGROUND_GRACE_MS` to configure the parent's startup grace period before reporting success
|
|
58
|
+
- Warn at startup when other Agent Tank processes are already running, listing each PID with credential flags redacted, and including `kill` / `kill -9` instructions to stop them; the warning header is shown in bold red on an interactive terminal
|
|
59
|
+
- Document running Agent Tank from source in the README (clone, install, run, flags, prerequisites)
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Harden the debug output dump on the status page so malformed agent output can't crash the process
|
|
64
|
+
|
|
65
|
+
## [0.9.7] - 2026-06-05
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Use chokidar polling for activity monitoring to reduce native watcher pressure on large CLI log trees
|
|
70
|
+
- Replace Gemini CLI support with Antigravity CLI (`agy`) using the interactive `/usage` command
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- Fix Claude Code PTY one-shot stats checks against Claude Code 2.1 compact usage output
|
|
75
|
+
- Prevent Claude metadata fetch from crashing when the CLI exits before delayed PTY writes run
|
|
76
|
+
- Dismiss Claude `/status` metadata dialogs before sending `/usage` so one-shot usage checks can complete
|
|
77
|
+
- Parse compact Claude reset timestamps such as `Jun10,6pm(Europe/Berlin)`
|
|
78
|
+
- Wait for Claude's separate weekly Sonnet usage section before completing `/usage` parsing
|
|
79
|
+
|
|
80
|
+
## [0.9.5] - 2026-04-13
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- Add a per-agent refresh cooldown with a default of 30 seconds, applied to manual refreshes, startup refreshes, and auto-refresh activity
|
|
85
|
+
- Add `--refresh-cooldown` and `AGENT_TANK_REFRESH_COOLDOWN` to reconfigure or disable the refresh cooldown
|
|
86
|
+
|
|
87
|
+
### Changed
|
|
88
|
+
|
|
89
|
+
- Prefer Docker bridge gateway discovery via `docker network inspect`, falling back to local interface detection only when Docker is unavailable
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- Stop interval-mode auto-refresh from bypassing the central refresh guard
|
|
94
|
+
- Tighten Gemini CLI readiness detection so auth and agent-thinking screens are less likely to be misread as ready prompts
|
|
95
|
+
|
|
96
|
+
## [0.9.4] - 2026-04-12
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- Bind to the local Docker bridge by default when `--host` is omitted, so containers on the same host can reach Agent Tank without exposing it publicly
|
|
101
|
+
- Add `--no-docker` and `AGENT_TANK_DOCKER` to disable Docker bridge binding explicitly
|
|
102
|
+
|
|
103
|
+
### Fixed
|
|
104
|
+
|
|
105
|
+
- Keep zero-usage reset times visible in the UI for Claude, Gemini, and Codex when reset information exists
|
|
106
|
+
- Remove trailing slashes from startup banner status page URLs
|
|
107
|
+
|
|
108
|
+
## [0.9.3] - 2026-04-11
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- Add `--version` to the `agent-tank` CLI for quick version checks
|
|
113
|
+
- Log PTY and JSON-RPC subprocess PIDs when agent processes start
|
|
114
|
+
|
|
115
|
+
### Fixed
|
|
116
|
+
|
|
117
|
+
- Make `Ctrl+C` exits immediate again by avoiding reliance on a JS `SIGINT` handler
|
|
118
|
+
- Prevent shutdown races from respawning agent processes after teardown begins
|
|
119
|
+
- Guard PTY refresh, metadata fetch, and keepalive paths against restart during shutdown
|
|
120
|
+
|
|
121
|
+
## [0.9.2] - 2026-04-09
|
|
122
|
+
|
|
123
|
+
### Fixed
|
|
124
|
+
|
|
125
|
+
- Prevent Gemini agent crashes when the CLI process exits during authentication flows
|
|
126
|
+
- Avoid sending `Escape` while Gemini OAuth authentication is in progress, which could cancel login
|
|
127
|
+
- Improve Gemini trust prompt handling during command execution and slow startup scenarios
|
|
128
|
+
- Detect Gemini CLI unauthenticated/setup screens and report them clearly instead of surfacing a generic timeout
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- Regression tests covering Gemini shell null-safety during delayed callbacks
|
|
133
|
+
- Gemini authentication-required status in the API response and a concise login instruction in the UI
|
|
134
|
+
|
|
135
|
+
## [0.9.0] - 2024-01-20
|
|
136
|
+
|
|
137
|
+
### Added
|
|
138
|
+
|
|
139
|
+
- Initial release
|
|
140
|
+
- Support for Claude Code CLI (`/usage` command)
|
|
141
|
+
- Session usage percentage
|
|
142
|
+
- Weekly usage (all models)
|
|
143
|
+
- Weekly usage (Sonnet only)
|
|
144
|
+
- Reset times
|
|
145
|
+
- Support for Gemini CLI (`/stats` command)
|
|
146
|
+
- Per-model usage percentages
|
|
147
|
+
- Reset times for each model
|
|
148
|
+
- Support for OpenAI Codex CLI (`/status` command)
|
|
149
|
+
- 5-hour limit percentage
|
|
150
|
+
- Weekly limit percentage
|
|
151
|
+
- Model and account info
|
|
152
|
+
- Auto-discovery of installed LLM CLI tools
|
|
153
|
+
- HTTP API with JSON responses
|
|
154
|
+
- Built-in HTML status page dashboard
|
|
155
|
+
- Refresh endpoints for on-demand updates
|
|
156
|
+
- CORS support for cross-origin requests
|
|
157
|
+
- Cursor position query response for Codex compatibility
|
package/README.md
CHANGED
|
@@ -170,7 +170,7 @@ agent-tank --refresh-cooldown 60
|
|
|
170
170
|
|
|
171
171
|
| Agent | Method | Metrics |
|
|
172
172
|
|---|---|---|
|
|
173
|
-
| Claude | PTY `/usage` or Anthropic OAuth API | Current session, weekly all-models, weekly
|
|
173
|
+
| Claude | PTY `/usage` or Anthropic OAuth API | Current session, weekly all-models, weekly Fable allowance (Max accounts only) |
|
|
174
174
|
| Antigravity | PTY `/usage` | Per-model quota availability and reset windows when reported |
|
|
175
175
|
| Codex | JSON-RPC preferred, PTY fallback | 5-hour limits, weekly limits, model/account info |
|
|
176
176
|
|
|
@@ -237,6 +237,7 @@ Options:
|
|
|
237
237
|
--claude Enable Claude monitoring
|
|
238
238
|
--agy Enable Antigravity monitoring
|
|
239
239
|
--codex Enable Codex monitoring
|
|
240
|
+
--agent <spec> Add an account as provider[:alias][=config-path] (repeatable)
|
|
240
241
|
--port <port> HTTP server port (default: 3456)
|
|
241
242
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
242
243
|
--docker Enable Docker bridge bind when --host is omitted (default: true)
|
|
@@ -293,11 +294,32 @@ three exceptions: `--no-background`, `--once`, and `--json` suppress
|
|
|
293
294
|
|
|
294
295
|
### Config File
|
|
295
296
|
|
|
297
|
+
Use the `agents` array to monitor multiple accounts, including multiple accounts
|
|
298
|
+
from the same provider. Each entry accepts a `provider`, an optional unique `id`
|
|
299
|
+
and display `alias`, and an optional `configPath` directory:
|
|
300
|
+
|
|
296
301
|
```json
|
|
297
302
|
{
|
|
298
|
-
"
|
|
299
|
-
|
|
300
|
-
|
|
303
|
+
"agents": [
|
|
304
|
+
{
|
|
305
|
+
"provider": "codex",
|
|
306
|
+
"id": "work",
|
|
307
|
+
"alias": "Work",
|
|
308
|
+
"configPath": "/srv/accounts/codex-work"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"provider": "codex",
|
|
312
|
+
"id": "personal",
|
|
313
|
+
"alias": "Personal",
|
|
314
|
+
"configPath": "/srv/accounts/codex-personal"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"provider": "claude",
|
|
318
|
+
"id": "client-a",
|
|
319
|
+
"alias": "Client A",
|
|
320
|
+
"configPath": "./accounts/claude-client-a"
|
|
321
|
+
}
|
|
322
|
+
],
|
|
301
323
|
"port": 8080,
|
|
302
324
|
"dockerAccess": true,
|
|
303
325
|
"claudeApi": false,
|
|
@@ -318,12 +340,32 @@ three exceptions: `--no-background`, `--once`, and `--json` suppress
|
|
|
318
340
|
}
|
|
319
341
|
```
|
|
320
342
|
|
|
343
|
+
Relative account paths are resolved relative to the JSON config file, and `~/`
|
|
344
|
+
paths are expanded to the current user's home directory. The path
|
|
345
|
+
is passed to the provider CLI as its config home: `CLAUDE_CONFIG_DIR` for Claude,
|
|
346
|
+
`CODEX_HOME` for Codex, and `GEMINI_CLI_HOME` for Antigravity. Legacy top-level
|
|
347
|
+
settings such as `"claude": true` remain supported for single default accounts.
|
|
348
|
+
|
|
321
349
|
Run with:
|
|
322
350
|
|
|
323
351
|
```bash
|
|
324
352
|
agent-tank -c config.json
|
|
325
353
|
```
|
|
326
354
|
|
|
355
|
+
The same setup can be supplied directly on the command line. Repeat `--agent`
|
|
356
|
+
once per account:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
agent-tank \
|
|
360
|
+
--agent codex:work=/srv/accounts/codex-work \
|
|
361
|
+
--agent codex:personal=/srv/accounts/codex-personal
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The `id` (or CLI alias after `:`) is the key returned by `/status` and is used
|
|
365
|
+
by account-specific status, history, and refresh endpoints. IDs may contain
|
|
366
|
+
letters, numbers, dots, underscores, and hyphens. If both `id` and `alias` are
|
|
367
|
+
set in JSON, `id` is the API key and `alias` is the dashboard label.
|
|
368
|
+
|
|
327
369
|
## Antigravity
|
|
328
370
|
|
|
329
371
|
Antigravity support uses the `agy` CLI and reads the interactive `/usage` output.
|
|
@@ -337,6 +379,25 @@ agent-tank --agy
|
|
|
337
379
|
agent-tank --agy --once --json
|
|
338
380
|
```
|
|
339
381
|
|
|
382
|
+
Each entry in `usage.models` reports the quota left, the countdown Antigravity
|
|
383
|
+
displays (`Resets in` on older builds, `Refreshes in` on the grouped
|
|
384
|
+
`Models & Quota` screen), the absolute instant that countdown points at, and the
|
|
385
|
+
cycle the limit belongs to. A limit showing `Quota available` has no pending
|
|
386
|
+
reset, so its reset fields are `null`.
|
|
387
|
+
|
|
388
|
+
```json
|
|
389
|
+
{
|
|
390
|
+
"model": "Gemini · Five Hour Limit Remaining",
|
|
391
|
+
"usageLeft": 90.31,
|
|
392
|
+
"percentUsed": 9.7,
|
|
393
|
+
"resetsIn": "58m",
|
|
394
|
+
"resetsInSeconds": 3480,
|
|
395
|
+
"resetsAt": "2026-09-23T11:52:00.000Z",
|
|
396
|
+
"cycle": "fiveHour",
|
|
397
|
+
"group": "Gemini"
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
340
401
|
## HTTP API
|
|
341
402
|
|
|
342
403
|
### Endpoints
|
|
@@ -345,12 +406,12 @@ agent-tank --agy --once --json
|
|
|
345
406
|
|---|---|---|
|
|
346
407
|
| GET | `/` | HTML status page |
|
|
347
408
|
| GET | `/status` | JSON status for all agents |
|
|
348
|
-
| GET | `/status/:
|
|
409
|
+
| GET | `/status/:id` | JSON status for one configured account |
|
|
349
410
|
| GET | `/config` | Auto-refresh and history config |
|
|
350
411
|
| GET | `/history` | History summary |
|
|
351
|
-
| GET | `/history/:
|
|
412
|
+
| GET | `/history/:id` | History for one configured account |
|
|
352
413
|
| POST | `/refresh` | Refresh all agents |
|
|
353
|
-
| POST | `/refresh/:
|
|
414
|
+
| POST | `/refresh/:id` | Refresh one configured account |
|
|
354
415
|
|
|
355
416
|
### Example `GET /status`
|
|
356
417
|
|
|
@@ -358,6 +419,9 @@ agent-tank --agy --once --json
|
|
|
358
419
|
{
|
|
359
420
|
"claude": {
|
|
360
421
|
"name": "claude",
|
|
422
|
+
"id": "claude",
|
|
423
|
+
"alias": null,
|
|
424
|
+
"provider": "claude",
|
|
361
425
|
"usage": {
|
|
362
426
|
"session": {
|
|
363
427
|
"label": "Current session",
|
|
@@ -372,6 +436,13 @@ agent-tank --agy --once --json
|
|
|
372
436
|
"resetsAt": "Mar 13, 3am (Europe/London)",
|
|
373
437
|
"resetsIn": "4d 5h",
|
|
374
438
|
"resetsInSeconds": 364364
|
|
439
|
+
},
|
|
440
|
+
"weeklyFable": {
|
|
441
|
+
"label": "Current week (Fable)",
|
|
442
|
+
"percent": 82,
|
|
443
|
+
"resetsAt": "Mar 13, 3am (Europe/London)",
|
|
444
|
+
"resetsIn": "4d 5h",
|
|
445
|
+
"resetsInSeconds": 364364
|
|
375
446
|
}
|
|
376
447
|
},
|
|
377
448
|
"metadata": {
|
|
@@ -570,7 +641,11 @@ codex --version
|
|
|
570
641
|
const { AgentTank } = require('agent-tank');
|
|
571
642
|
|
|
572
643
|
const watcher = new AgentTank({
|
|
573
|
-
agents: [
|
|
644
|
+
agents: [
|
|
645
|
+
{ provider: 'codex', id: 'work', configPath: '/srv/accounts/codex-work' },
|
|
646
|
+
{ provider: 'codex', id: 'personal', configPath: '/srv/accounts/codex-personal' },
|
|
647
|
+
'agy'
|
|
648
|
+
],
|
|
574
649
|
port: 3456,
|
|
575
650
|
autoDiscover: true
|
|
576
651
|
});
|
|
@@ -580,7 +655,7 @@ await watcher.start();
|
|
|
580
655
|
const status = watcher.getStatus();
|
|
581
656
|
console.log(status);
|
|
582
657
|
|
|
583
|
-
await watcher.refreshAgent('
|
|
658
|
+
await watcher.refreshAgent('work');
|
|
584
659
|
|
|
585
660
|
watcher.stop();
|
|
586
661
|
```
|
package/bin/agent-tank.js
CHANGED
|
@@ -10,12 +10,14 @@ const {
|
|
|
10
10
|
spawnBackgroundProcess,
|
|
11
11
|
warnAboutRunningProcesses,
|
|
12
12
|
} = require('../src/cli-background.js');
|
|
13
|
+
const { normalizeAgentSpecs, parseAgentOption } = require('../src/agent-config.js');
|
|
13
14
|
const pkg = require('../package.json');
|
|
14
15
|
|
|
15
16
|
const options = {
|
|
16
17
|
claude: { type: 'boolean', default: false },
|
|
17
18
|
agy: { type: 'boolean', default: false },
|
|
18
19
|
codex: { type: 'boolean', default: false },
|
|
20
|
+
agent: { type: 'string', multiple: true, default: [] },
|
|
19
21
|
'claude-api': { type: 'boolean', default: false },
|
|
20
22
|
port: { type: 'string', default: '3456' },
|
|
21
23
|
host: { type: 'string' },
|
|
@@ -76,6 +78,7 @@ Options:
|
|
|
76
78
|
--claude Enable Claude monitoring
|
|
77
79
|
--agy Enable Antigravity monitoring
|
|
78
80
|
--codex Enable Codex monitoring
|
|
81
|
+
--agent <spec> Add an account as provider[:alias][=config-path] (repeatable)
|
|
79
82
|
--claude-api Use direct Anthropic API for Claude usage (faster, 60s refresh)
|
|
80
83
|
--port <port> HTTP server port (default: 3456)
|
|
81
84
|
--host <host> Bind address (default: 127.0.0.1 + Docker bridge when available)
|
|
@@ -131,6 +134,7 @@ Examples:
|
|
|
131
134
|
agent-tank # Auto-discover and monitor all available
|
|
132
135
|
agent-tank --background # Start in the background and print the PID
|
|
133
136
|
agent-tank --claude --agy # Monitor specific agents
|
|
137
|
+
agent-tank --agent codex:work=/srv/codex-work --agent codex:personal=/srv/codex-personal
|
|
134
138
|
agent-tank --port 8080 # Use custom port
|
|
135
139
|
agent-tank --host 0.0.0.0 # Expose on all interfaces
|
|
136
140
|
agent-tank --no-docker # Bind localhost only when host is omitted
|
|
@@ -152,12 +156,12 @@ Examples:
|
|
|
152
156
|
HTTP Endpoints:
|
|
153
157
|
GET / Status page (HTML)
|
|
154
158
|
GET /status All agent statuses (JSON)
|
|
155
|
-
GET /status/:
|
|
159
|
+
GET /status/:id Status for a specific agent account (JSON)
|
|
156
160
|
GET /config Auto-refresh and history configuration (JSON)
|
|
157
161
|
GET /history Usage history statistics (JSON)
|
|
158
|
-
GET /history/:
|
|
162
|
+
GET /history/:id Usage history for a specific agent account (JSON)
|
|
159
163
|
POST /refresh Trigger refresh for all agents
|
|
160
|
-
POST /refresh/:
|
|
164
|
+
POST /refresh/:id Trigger refresh for a specific agent account
|
|
161
165
|
`);
|
|
162
166
|
}
|
|
163
167
|
|
|
@@ -207,9 +211,12 @@ async function main() {
|
|
|
207
211
|
|
|
208
212
|
// Load config file if specified
|
|
209
213
|
let config = {};
|
|
214
|
+
let configBaseDir = process.cwd();
|
|
210
215
|
if (values.config) {
|
|
211
216
|
try {
|
|
212
|
-
|
|
217
|
+
const configPath = require('path').resolve(values.config);
|
|
218
|
+
config = require(configPath);
|
|
219
|
+
configBaseDir = require('path').dirname(configPath);
|
|
213
220
|
} catch (err) {
|
|
214
221
|
exitWithCode(1, `Failed to load config file: ${err.message}`);
|
|
215
222
|
return;
|
|
@@ -217,10 +224,21 @@ async function main() {
|
|
|
217
224
|
}
|
|
218
225
|
|
|
219
226
|
// Merge CLI options with config (env vars > CLI flags > config file)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
let agents;
|
|
228
|
+
try {
|
|
229
|
+
const configAgents = config.agents == null
|
|
230
|
+
? []
|
|
231
|
+
: normalizeAgentSpecs(config.agents, { baseDir: configBaseDir });
|
|
232
|
+
const cliAgents = normalizeAgentSpecs(values.agent.map(parseAgentOption));
|
|
233
|
+
const legacyAgents = [];
|
|
234
|
+
if (values.claude || config.claude) legacyAgents.push('claude');
|
|
235
|
+
if (values.agy || config.agy) legacyAgents.push('agy');
|
|
236
|
+
if (values.codex || config.codex) legacyAgents.push('codex');
|
|
237
|
+
agents = normalizeAgentSpecs([...configAgents, ...cliAgents, ...legacyAgents]);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
exitWithCode(1, `Invalid agent configuration: ${err.message}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
224
242
|
|
|
225
243
|
const auth = {
|
|
226
244
|
user: process.env.AGENT_TANK_USER || values['auth-user'] || config.auth?.user,
|
package/favicon.svg
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="#48bb78" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="24" height="24" rx="5" fill="#0d1117" stroke="none"/>
|
|
3
|
+
<path d="M6 4v12a4 4 0 0 0 4 4h4a4 4 0 0 0 4-4V4"/>
|
|
4
|
+
<line x1="6" y1="4" x2="18" y2="4"/>
|
|
5
|
+
<line x1="8" y1="8" x2="16" y2="8" opacity="0.3"/>
|
|
6
|
+
<line x1="8" y1="12" x2="16" y2="12" opacity="0.3"/>
|
|
7
|
+
</svg>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tank",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.11",
|
|
4
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": {
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
"bin/",
|
|
62
62
|
"src/",
|
|
63
63
|
"media/",
|
|
64
|
+
"favicon.svg",
|
|
65
|
+
"CHANGELOG.md",
|
|
64
66
|
"LICENSE",
|
|
65
67
|
"README.md"
|
|
66
68
|
]
|
package/src/activity-monitor.js
CHANGED
|
@@ -29,6 +29,11 @@ const DEFAULT_LOG_DIRECTORIES = {
|
|
|
29
29
|
],
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
function getAgentLogDirectories(provider, configPath = null) {
|
|
33
|
+
if (configPath) return [configPath];
|
|
34
|
+
return DEFAULT_LOG_DIRECTORIES[provider] || [];
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
function normalizeFilename(filename) {
|
|
33
38
|
return String(filename || '').replace(/\\/g, '/');
|
|
34
39
|
}
|
|
@@ -320,4 +325,4 @@ class ActivityMonitor {
|
|
|
320
325
|
}
|
|
321
326
|
}
|
|
322
327
|
|
|
323
|
-
module.exports = { ActivityMonitor, DEFAULT_LOG_DIRECTORIES };
|
|
328
|
+
module.exports = { ActivityMonitor, DEFAULT_LOG_DIRECTORIES, getAgentLogDirectories };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/* eslint-disable complexity -- Validation keeps all supported agent config forms in one place. */
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_PROVIDERS = new Set(['claude', 'agy', 'codex']);
|
|
7
|
+
const AGENT_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
8
|
+
|
|
9
|
+
function resolveConfigPath(configPath, baseDir) {
|
|
10
|
+
if (configPath === '~') return os.homedir();
|
|
11
|
+
if (configPath.startsWith('~/') || configPath.startsWith('~\\')) {
|
|
12
|
+
return path.resolve(os.homedir(), configPath.slice(2));
|
|
13
|
+
}
|
|
14
|
+
return path.resolve(baseDir, configPath);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseAgentOption(value) {
|
|
18
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
19
|
+
throw new Error('Agent option must be a non-empty string');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const equalsIndex = value.indexOf('=');
|
|
23
|
+
const identity = (equalsIndex === -1 ? value : value.slice(0, equalsIndex)).trim();
|
|
24
|
+
const configPath = equalsIndex === -1 ? null : value.slice(equalsIndex + 1).trim();
|
|
25
|
+
const colonIndex = identity.indexOf(':');
|
|
26
|
+
const provider = (colonIndex === -1 ? identity : identity.slice(0, colonIndex)).trim();
|
|
27
|
+
const alias = colonIndex === -1 ? null : identity.slice(colonIndex + 1).trim();
|
|
28
|
+
|
|
29
|
+
if (equalsIndex !== -1 && !configPath) {
|
|
30
|
+
throw new Error(`Agent option "${value}" has an empty config path`);
|
|
31
|
+
}
|
|
32
|
+
if (colonIndex !== -1 && !alias) {
|
|
33
|
+
throw new Error(`Agent option "${value}" has an empty alias`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
provider,
|
|
38
|
+
...(alias && { id: alias, alias }),
|
|
39
|
+
...(configPath && { configPath }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeAgentSpec(entry, baseDir) {
|
|
44
|
+
const raw = typeof entry === 'string' ? { provider: entry } : entry;
|
|
45
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
46
|
+
throw new Error('Each agent must be a provider name or an agent configuration object');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const provider = String(raw.provider || raw.name || '').trim().toLowerCase();
|
|
50
|
+
if (!SUPPORTED_PROVIDERS.has(provider)) {
|
|
51
|
+
throw new Error(`Unsupported agent provider: ${provider || '(missing)'}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const explicitId = raw.id == null ? null : String(raw.id).trim();
|
|
55
|
+
const alias = raw.alias == null ? null : String(raw.alias).trim();
|
|
56
|
+
const id = explicitId || alias || provider;
|
|
57
|
+
if (!AGENT_ID_PATTERN.test(id)) {
|
|
58
|
+
throw new Error(`Invalid agent id "${id}"; use letters, numbers, dots, underscores, or hyphens`);
|
|
59
|
+
}
|
|
60
|
+
if (raw.alias != null && !alias) {
|
|
61
|
+
throw new Error(`Alias for agent "${id}" must not be empty`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const rawConfigPath = raw.configPath ?? raw.path ?? null;
|
|
65
|
+
let configPath = null;
|
|
66
|
+
if (rawConfigPath != null) {
|
|
67
|
+
if (typeof rawConfigPath !== 'string' || !rawConfigPath.trim()) {
|
|
68
|
+
throw new Error(`Config path for agent "${id}" must be a non-empty string`);
|
|
69
|
+
}
|
|
70
|
+
configPath = resolveConfigPath(rawConfigPath.trim(), baseDir);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
provider,
|
|
75
|
+
id,
|
|
76
|
+
alias: alias || (id !== provider ? id : null),
|
|
77
|
+
configPath,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeAgentSpecs(entries, options = {}) {
|
|
82
|
+
if (!Array.isArray(entries)) {
|
|
83
|
+
throw new Error('agents must be an array');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const baseDir = options.baseDir || process.cwd();
|
|
87
|
+
const normalized = entries.map(entry => normalizeAgentSpec(entry, baseDir));
|
|
88
|
+
const ids = new Set();
|
|
89
|
+
|
|
90
|
+
for (const spec of normalized) {
|
|
91
|
+
if (ids.has(spec.id)) {
|
|
92
|
+
throw new Error(`Duplicate agent id "${spec.id}"; give each account a unique id or alias`);
|
|
93
|
+
}
|
|
94
|
+
ids.add(spec.id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return normalized;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
AGENT_ID_PATTERN,
|
|
102
|
+
SUPPORTED_PROVIDERS,
|
|
103
|
+
normalizeAgentSpecs,
|
|
104
|
+
parseAgentOption,
|
|
105
|
+
};
|
package/src/agents/agy.js
CHANGED
|
@@ -8,8 +8,15 @@ class AgyAgent extends BaseAgent {
|
|
|
8
8
|
// "Models & Quota" screen reported by recent Antigravity CLI builds.
|
|
9
9
|
static QUOTA_HEADER = /Models?\s*&?\s*Quota/i;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Antigravity words its countdown line differently across builds: older
|
|
12
|
+
// screens say "Resets in 3h 15m", the grouped "Models & Quota" screen says
|
|
13
|
+
// "Refreshes in 160h 12m". A fully replenished limit renders "Quota
|
|
14
|
+
// available" instead, which carries no countdown and yields a null reset.
|
|
15
|
+
static RESET_LINE = /^(?:resets?|refresh(?:es)?|renew(?:s|als)?)\s*(?:in|at)?\s*([0-9dhm\s]+)?/i;
|
|
16
|
+
|
|
17
|
+
constructor(options = {}) {
|
|
12
18
|
super('agy', 'agy', ['--dangerously-skip-permissions']);
|
|
19
|
+
this.configPath = options.configPath || null;
|
|
13
20
|
this._aboutSent = false;
|
|
14
21
|
}
|
|
15
22
|
|
|
@@ -20,11 +27,13 @@ class AgyAgent extends BaseAgent {
|
|
|
20
27
|
getEnv() {
|
|
21
28
|
// Use xterm-256color for proper terminal support
|
|
22
29
|
// Antigravity CLI needs good terminal emulation for its TUI
|
|
23
|
-
|
|
30
|
+
const env = {
|
|
24
31
|
...process.env,
|
|
25
32
|
TERM: 'xterm-256color',
|
|
26
33
|
COLORTERM: 'truecolor',
|
|
27
34
|
};
|
|
35
|
+
if (this.configPath) env.GEMINI_CLI_HOME = this.configPath;
|
|
36
|
+
return env;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
_hasReadyPrompt(output) {
|
|
@@ -203,9 +212,35 @@ class AgyAgent extends BaseAgent {
|
|
|
203
212
|
.join(' ');
|
|
204
213
|
}
|
|
205
214
|
|
|
215
|
+
// Read the countdown line that follows a limit's percentage bar. Returns
|
|
216
|
+
// nulls when the limit is fully replenished ("Quota available") or the line
|
|
217
|
+
// is something else entirely.
|
|
218
|
+
_parseResetLine(line) {
|
|
219
|
+
// A redrawn frame can leave box borders on the line, so normalise before
|
|
220
|
+
// matching the countdown's leading keyword.
|
|
221
|
+
const resetMatch = this.stripBoxChars(line || '').match(AgyAgent.RESET_LINE);
|
|
222
|
+
const resetsIn = resetMatch?.[1]?.trim() || null;
|
|
223
|
+
const resetsInSeconds = resetsIn ? this.parseDurationToSeconds(resetsIn) : null;
|
|
224
|
+
// The countdown goes stale as soon as it is stored, so also record the
|
|
225
|
+
// absolute instant it refers to.
|
|
226
|
+
const resetsAt = resetsInSeconds == null
|
|
227
|
+
? null
|
|
228
|
+
: new Date(Date.now() + resetsInSeconds * 1000).toISOString();
|
|
229
|
+
return { resetsIn, resetsInSeconds, resetsAt };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Map an Antigravity limit label to the cycle its countdown belongs to, so
|
|
233
|
+
// pace math compares usage against the right window. A weekly limit reports
|
|
234
|
+
// "Refreshes in 160h 12m", which is nonsense against a 24h cycle.
|
|
235
|
+
_cycleTypeFor(label) {
|
|
236
|
+
if (/\bweek(?:ly)?\b/i.test(label)) return 'weekly';
|
|
237
|
+
if (/\b(?:five|5)[\s-]?hour\b/i.test(label)) return 'fiveHour';
|
|
238
|
+
return 'sessionAgy';
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
// Calculate pace data for a model entry when reset timing is available.
|
|
207
|
-
_addPaceData(entry, resetsInSeconds) {
|
|
208
|
-
const cycleDuration = CYCLE_DURATIONS
|
|
242
|
+
_addPaceData(entry, resetsInSeconds, cycleType = 'sessionAgy') {
|
|
243
|
+
const cycleDuration = CYCLE_DURATIONS[cycleType];
|
|
209
244
|
if (!cycleDuration || resetsInSeconds == null) return;
|
|
210
245
|
const paceData = calculatePace({ usagePercent: entry.percentUsed, resetsInSeconds, cycleDurationSeconds: cycleDuration });
|
|
211
246
|
if (paceData) entry.pace = paceData;
|
|
@@ -265,6 +300,11 @@ class AgyAgent extends BaseAgent {
|
|
|
265
300
|
if (!/[A-Za-z]/.test(line) || /\d+\s*%/.test(line)) {
|
|
266
301
|
continue;
|
|
267
302
|
}
|
|
303
|
+
// A countdown line ("Refreshes in 58m") belongs to the entry above it and
|
|
304
|
+
// is never a model label of its own.
|
|
305
|
+
if (AgyAgent.RESET_LINE.test(line)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
268
308
|
|
|
269
309
|
const percentLine = lines[i + 1] || '';
|
|
270
310
|
const percentMatch = percentLine.match(/(\d+(?:\.\d+)?)\s*%/);
|
|
@@ -274,10 +314,7 @@ class AgyAgent extends BaseAgent {
|
|
|
274
314
|
|
|
275
315
|
const usageLeft = parseFloat(percentMatch[1]);
|
|
276
316
|
const percentUsed = Number((100 - usageLeft).toFixed(1));
|
|
277
|
-
const
|
|
278
|
-
const resetMatch = resetLine.match(/(?:resets?|available)\s*(?:in|at)?\s*([0-9dhm\s]+)?/i);
|
|
279
|
-
const resetsIn = resetMatch?.[1]?.trim() || null;
|
|
280
|
-
const resetsInSeconds = resetsIn ? this.parseDurationToSeconds(resetsIn) : null;
|
|
317
|
+
const reset = this._parseResetLine(lines[i + 2]);
|
|
281
318
|
|
|
282
319
|
// Qualify shared limit labels with their group so entries from different
|
|
283
320
|
// groups stay distinct; per-model labels are already unique on their own.
|
|
@@ -287,9 +324,10 @@ class AgyAgent extends BaseAgent {
|
|
|
287
324
|
: line;
|
|
288
325
|
|
|
289
326
|
if (!usage.models.find(m => m.model === modelName)) {
|
|
290
|
-
const
|
|
327
|
+
const cycle = this._cycleTypeFor(line);
|
|
328
|
+
const modelEntry = { model: modelName, usageLeft, percentUsed, ...reset, cycle };
|
|
291
329
|
if (currentGroup) modelEntry.group = currentGroup;
|
|
292
|
-
this._addPaceData(modelEntry, resetsInSeconds);
|
|
330
|
+
this._addPaceData(modelEntry, reset.resetsInSeconds, cycle);
|
|
293
331
|
usage.models.push(modelEntry);
|
|
294
332
|
}
|
|
295
333
|
}
|