agent-fuel 0.2.0 → 0.4.0
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 +62 -28
- package/dist/adapters/agy.d.ts +1 -3
- package/dist/adapters/agy.js +157 -90
- package/dist/adapters/claude.d.ts +2 -3
- package/dist/adapters/claude.js +76 -61
- package/dist/adapters/codex.d.ts +3 -2
- package/dist/adapters/codex.js +217 -87
- package/dist/adapters/index.d.ts +4 -3
- package/dist/debug.d.ts +4 -0
- package/dist/debug.js +18 -0
- package/dist/index.js +70 -18
- package/dist/render.d.ts +7 -0
- package/dist/render.js +100 -63
- package/dist/tmux.d.ts +13 -0
- package/dist/tmux.js +72 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,20 +6,20 @@ A sleek, unified CLI dashboard to monitor your AI coding assistant quotas, credi
|
|
|
6
6
|
|
|
7
7
|
## 🚀 Installation & Running
|
|
8
8
|
|
|
9
|
-
Install **Agent Fuel** globally
|
|
9
|
+
Install **Agent Fuel** globally:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install -g agent-fuel
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Then run from any directory:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
agent-fuel
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
### Development Setup
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
```bash
|
|
24
24
|
git clone https://github.com/jperod/agent-fuel.git
|
|
25
25
|
cd agent-fuel
|
|
@@ -32,63 +32,97 @@ npm link
|
|
|
32
32
|
|
|
33
33
|
## 💡 The Motivation
|
|
34
34
|
|
|
35
|
-
AI coding assistants are now integral to developer workflows. Tools like **Claude Code**, **Codex CLI**, and **AGY (Google Antigravity CLI)** supercharge productivity but operate under tight, separate quota bounds.
|
|
35
|
+
AI coding assistants are now integral to developer workflows. Tools like **Claude Code**, **Codex CLI**, and **AGY (Google Antigravity CLI)** supercharge productivity but operate under tight, separate quota bounds. Developers are forced to jump through interactive prompts or scrape configuration screens just to answer:
|
|
36
36
|
|
|
37
37
|
> **"How much agent fuel do I have left before starting this massive refactor?"**
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
**Agent Fuel** solves this by acting as a lightweight, adapter-based abstraction layer that normalizes all coding agent quotas into a single metric: **Percent Remaining**.
|
|
39
|
+
**Agent Fuel** solves this by acting as a lightweight, adapter-based abstraction layer that normalises all coding agent quotas into a single metric: **Percent Remaining**.
|
|
42
40
|
|
|
43
41
|
---
|
|
44
42
|
|
|
45
|
-
## 🎯
|
|
43
|
+
## 🎯 How It Works
|
|
44
|
+
|
|
45
|
+
`agent-fuel` is a tiny modern CLI built with TypeScript that:
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
1. **Dispatches Adapters concurrently** — all adapters run in parallel and each row is printed the moment its adapter resolves; you never wait for the slowest tool.
|
|
48
|
+
2. **Normalises Quota Models** — standardises diverse limits into a uniform `0–100%` score.
|
|
49
|
+
3. **Scrapes TUI output directly** — Codex and AGY quotas are read by spawning the real CLIs via `expect` and parsing terminal output, so the numbers match what the tools themselves show.
|
|
50
|
+
4. **Caches AGY results** — AGY quota is cached for 5 minutes so repeated runs are instant (~1s).
|
|
51
|
+
5. **Renders a clean dashboard** — colour-coded bars with reset times directly in your terminal.
|
|
51
52
|
|
|
52
53
|
### Project Architecture
|
|
53
54
|
|
|
54
55
|
```text
|
|
55
56
|
agent-fuel/
|
|
56
57
|
├── src/
|
|
57
|
-
│ ├── index.ts
|
|
58
|
-
│ ├── render.ts
|
|
58
|
+
│ ├── index.ts # CLI entry point — runs all adapters concurrently
|
|
59
|
+
│ ├── render.ts # Colour-coded bar dashboard renderer
|
|
59
60
|
│ └── adapters/
|
|
60
|
-
│ ├──
|
|
61
|
-
│ ├──
|
|
62
|
-
│
|
|
61
|
+
│ ├── index.ts # Shared UsageSnapshot type & QuotaAdapter interface
|
|
62
|
+
│ ├── claude.ts # Claude Code (via ccusage blocks)
|
|
63
|
+
│ ├── codex.ts # Codex CLI (expect TUI scrape; ccusage as fallback estimate)
|
|
64
|
+
│ └── agy.ts # AGY — split into Gemini + Other buckets
|
|
63
65
|
├── package.json
|
|
64
66
|
└── README.md
|
|
65
67
|
```
|
|
66
68
|
|
|
67
|
-
###
|
|
69
|
+
### Type Shape
|
|
68
70
|
|
|
69
71
|
```typescript
|
|
70
72
|
type UsageSnapshot = {
|
|
71
|
-
tool: 'codex' | 'claude-code' | 'agy';
|
|
72
|
-
remainingPercent: number | null;
|
|
73
|
+
tool: 'codex' | 'claude-code' | 'agy-gemini' | 'agy-other';
|
|
74
|
+
remainingPercent: number | null; // Unified 0–100 scale
|
|
73
75
|
usedPercent?: number | null;
|
|
74
76
|
resetAt?: string | null;
|
|
75
|
-
source: 'official-cli' | 'ccusage' | 'local-state' | 'provider-api' | 'unknown';
|
|
77
|
+
source: 'official-cli' | 'ccusage' | 'local-state' | 'provider-api' | 'cache' | 'unknown';
|
|
76
78
|
raw?: unknown;
|
|
77
79
|
};
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
---
|
|
81
83
|
|
|
82
|
-
## 📊 Terminal Dashboard
|
|
84
|
+
## 📊 Terminal Dashboard
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```text
|
|
86
|
+
```
|
|
87
87
|
⚡️ Agent Fuel - CLI Quota Monitor
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
AGY
|
|
89
|
+
Claude Code [███████████████████░░░░░░░░░░░] 64% remaining (resets 01:00 PM)
|
|
90
|
+
Codex [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% remaining (resets in 4h 33m)
|
|
91
|
+
AGY Gemini [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% remaining (resets in 3h 16m) [Gemini 3.5 Flash (Medium)]
|
|
92
|
+
AGY Other [██████████████████░░░░░░░░░░░░] 60% remaining (resets in 4h 47m) [Claude Sonnet 4.6 (Thinking)]
|
|
93
|
+
|
|
94
|
+
agent-fuel v0.3.0
|
|
92
95
|
```
|
|
93
96
|
|
|
97
|
+
Rows appear as each adapter resolves — Claude Code (instant) prints first, Codex and AGY follow as their TUI scrapes complete.
|
|
98
|
+
|
|
99
|
+
**AGY Gemini** shows the worst-case remaining across all `Gemini *` model tiers.
|
|
100
|
+
**AGY Other** shows the worst-case across Claude and other non-Gemini models.
|
|
101
|
+
**Codex** row tagged `[~est]` when quota has not been reached and the percentage is estimated from local session cost data (see fallback note below).
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## ⚙️ Environment Overrides
|
|
106
|
+
|
|
107
|
+
| Variable | Default | Description |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `AGENT_FUEL_CLAUDE_BUDGET` | `20.0` | Claude Code rolling budget in USD |
|
|
110
|
+
| `AGENT_FUEL_CODEX_BUDGET` | `20.0` | **Fallback estimate only** — Codex rolling budget in USD |
|
|
111
|
+
|
|
112
|
+
> **Note on `AGENT_FUEL_CODEX_BUDGET`:** Codex quota is read directly from the Codex TUI via `expect` scraping. This variable is only used as a rough fallback estimate (shown as `[~est]`) when the TUI reports no quota warning and a percentage cannot be determined. It is a guess based on local session cost data — not an official Codex quota signal. The TUI scrape is always preferred.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 📦 Changelog
|
|
117
|
+
|
|
118
|
+
### v0.3.0
|
|
119
|
+
- **Codex TUI scrape**: replaced inaccurate `ccusage` cost estimate with an `expect` wrapper that reads the real Codex quota warning (`"Individual quota reached. Resets in Xh Ym"`) — same pattern as AGY. `ccusage` kept as a labelled `[~est]` fallback when quota has not yet been exhausted.
|
|
120
|
+
- **Streaming render with fixed order**: placeholder rows print immediately; each bar overwrites in-place as its adapter resolves. Row order is always `Claude Code → Codex → AGY Gemini → AGY Other`.
|
|
121
|
+
- **AGY split view**: Gemini and non-Gemini (Claude, etc.) quota buckets shown as separate rows
|
|
122
|
+
- **5-minute disk cache** for AGY quota — repeated runs complete in ~1s instead of ~20s
|
|
123
|
+
- Output size cap, typed `any` removal, env validation hardening
|
|
94
124
|
|
|
125
|
+
### v0.2.x
|
|
126
|
+
- AGY quota now scraped live from `agy /usage` panel via `expect` wrapper (zero token cost)
|
|
127
|
+
- Claude Code budget corrected to $20 rolling limit
|
|
128
|
+
- Replaced token-consuming `claude -p` calls with offline `ccusage` scraping
|
package/dist/adapters/agy.d.ts
CHANGED
package/dist/adapters/agy.js
CHANGED
|
@@ -1,104 +1,171 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { TuiScraper } from '../tmux.js';
|
|
5
|
+
const CACHE_PATH = path.join(os.homedir(), '.gemini/antigravity-cli/.agent-fuel-quota-cache.json');
|
|
6
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
|
+
// ── Scraping ───────────────────────────────────────────────────────────────
|
|
8
|
+
async function sleep(ms) {
|
|
9
|
+
return new Promise(res => setTimeout(res, ms));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Launches `agy` in a tmux session, opens the `/usage` panel, waits for
|
|
13
|
+
* the Model Quota list to render, then returns clean rendered screen text.
|
|
14
|
+
*/
|
|
15
|
+
async function runAgyUsage() {
|
|
16
|
+
const tui = new TuiScraper('agy');
|
|
17
|
+
try {
|
|
18
|
+
tui.start();
|
|
19
|
+
// Wait for AGY main menu ready
|
|
20
|
+
await tui.waitFor(/for shortcuts/, 20_000);
|
|
21
|
+
// Navigate to /usage panel
|
|
22
|
+
tui.send('/usage');
|
|
23
|
+
await tui.waitFor(/Model Quota/, 10_000);
|
|
24
|
+
// Brief pause for all model rows to finish rendering
|
|
25
|
+
await sleep(500);
|
|
26
|
+
return tui.capture();
|
|
9
27
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
finally {
|
|
29
|
+
tui.kill();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── Parsing ────────────────────────────────────────────────────────────────
|
|
33
|
+
/**
|
|
34
|
+
* Parse the Model Quota panel into an array of entries.
|
|
35
|
+
*
|
|
36
|
+
* Panel format (tmux rendered — no ANSI codes):
|
|
37
|
+
*
|
|
38
|
+
* └ Model Quota
|
|
39
|
+
*
|
|
40
|
+
* Gemini 3.5 Flash (High)
|
|
41
|
+
* ░░░░░░░░░░░ ... 20%
|
|
42
|
+
* Refreshes in 3h 28m
|
|
43
|
+
*
|
|
44
|
+
* Claude Sonnet 4.6 (Thinking)
|
|
45
|
+
* ███████████ ... 100%
|
|
46
|
+
* Quota available
|
|
47
|
+
*/
|
|
48
|
+
function parseQuotaPanel(raw) {
|
|
49
|
+
// tmux capture-pane returns clean rendered text — no ANSI stripping needed
|
|
50
|
+
const lines = raw.split(/\r?\n/);
|
|
51
|
+
const results = [];
|
|
52
|
+
const headerIdx = lines.findIndex(l => l.includes('Model Quota'));
|
|
53
|
+
if (headerIdx === -1)
|
|
54
|
+
return results;
|
|
55
|
+
const panelLines = lines.slice(headerIdx + 1);
|
|
56
|
+
let i = 0;
|
|
57
|
+
while (i < panelLines.length) {
|
|
58
|
+
const line = panelLines[i].trim();
|
|
59
|
+
const isModelName = line.length > 0 &&
|
|
60
|
+
!line.startsWith('░') && !line.startsWith('█') &&
|
|
61
|
+
!line.startsWith('↑') && !line.startsWith('(') &&
|
|
62
|
+
!line.startsWith('┘') && !line.startsWith('└') &&
|
|
63
|
+
!line.startsWith('?') && !line.startsWith('esc') &&
|
|
64
|
+
!/^\d+%/.test(line) &&
|
|
65
|
+
!line.includes('Refreshes') && !line.includes('Quota available') &&
|
|
66
|
+
!line.includes('──');
|
|
67
|
+
if (isModelName) {
|
|
68
|
+
let barLine = null;
|
|
69
|
+
let refreshLine = null;
|
|
70
|
+
let j = i + 1;
|
|
71
|
+
while (j < panelLines.length) {
|
|
72
|
+
const candidate = panelLines[j].trim();
|
|
73
|
+
if (candidate.length === 0) {
|
|
74
|
+
j++;
|
|
75
|
+
continue;
|
|
21
76
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// 2. Read history.jsonl to detect active prompts within the rolling 5-hour window
|
|
27
|
-
let todayPromptsCount = 0;
|
|
28
|
-
let latestPromptTimestamp = null;
|
|
29
|
-
const fiveHoursAgo = Date.now() - 5 * 60 * 60 * 1000;
|
|
30
|
-
try {
|
|
31
|
-
const historyContent = await fs.readFile(historyPath, 'utf-8');
|
|
32
|
-
const historyLines = historyContent.trim().split('\n');
|
|
33
|
-
for (const line of historyLines) {
|
|
34
|
-
if (!line.trim())
|
|
35
|
-
continue;
|
|
36
|
-
const entry = JSON.parse(line);
|
|
37
|
-
if (entry && entry.timestamp) {
|
|
38
|
-
// Check if the prompt falls within the 5-hour rolling window
|
|
39
|
-
if (entry.timestamp >= fiveHoursAgo) {
|
|
40
|
-
todayPromptsCount++;
|
|
41
|
-
if (!latestPromptTimestamp || entry.timestamp > latestPromptTimestamp) {
|
|
42
|
-
latestPromptTimestamp = entry.timestamp;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
77
|
+
if (barLine === null && (candidate.includes('░') || candidate.includes('█') || /^\d+%/.test(candidate))) {
|
|
78
|
+
barLine = candidate;
|
|
79
|
+
j++;
|
|
80
|
+
continue;
|
|
46
81
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// 3. Calculate remaining percent based on active usage and model tier
|
|
52
|
-
// Support dynamic overrides using AGENT_FUEL_AGY_PERCENT environment variable
|
|
53
|
-
let remainingPercent = 100;
|
|
54
|
-
const isProModel = activeModel.toLowerCase().includes('pro');
|
|
55
|
-
let calculatedPercent = 100;
|
|
56
|
-
if (isProModel) {
|
|
57
|
-
// Pro models: limit is 10 prompts, steps of 2 prompts (each step is 20%)
|
|
58
|
-
calculatedPercent = Math.max(0, 100 - (Math.floor(todayPromptsCount / 2) * 20));
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
// Flash models: limit is 25 prompts, steps of 5 prompts (each step is 20%)
|
|
62
|
-
calculatedPercent = Math.max(0, 100 - (Math.floor(todayPromptsCount / 5) * 20));
|
|
63
|
-
}
|
|
64
|
-
if (process.env.AGENT_FUEL_AGY_PERCENT) {
|
|
65
|
-
const envVal = Number(process.env.AGENT_FUEL_AGY_PERCENT);
|
|
66
|
-
remainingPercent = !isNaN(envVal) ? Math.max(0, Math.min(100, envVal)) : calculatedPercent;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
remainingPercent = calculatedPercent;
|
|
70
|
-
}
|
|
71
|
-
// 4. Calculate rolling reset time (5 hours rolling or resets in 4h 37m from latest prompt, giving ~01:30 PM resets)
|
|
72
|
-
let resetAt = null;
|
|
73
|
-
if (latestPromptTimestamp) {
|
|
74
|
-
try {
|
|
75
|
-
const lastActivityDate = new Date(latestPromptTimestamp);
|
|
76
|
-
// Roll forward 5 hours (refreshes in ~4h 37m from active run)
|
|
77
|
-
const resetDate = new Date(lastActivityDate.getTime() + 5 * 60 * 60 * 1000);
|
|
78
|
-
resetAt = resetDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
82
|
+
if (barLine !== null && (candidate.includes('Refreshes') || candidate.includes('Quota available'))) {
|
|
83
|
+
const m = candidate.match(/(Refreshes in [^\r\n]+|Quota available)/);
|
|
84
|
+
refreshLine = m ? m[1] : candidate;
|
|
85
|
+
j++;
|
|
79
86
|
}
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (barLine !== null) {
|
|
90
|
+
const percentMatch = barLine.match(/(\d+)%/);
|
|
91
|
+
if (percentMatch) {
|
|
92
|
+
results.push({ model: line, percent: parseInt(percentMatch[1], 10), refreshLine });
|
|
82
93
|
}
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
i = j;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
// ── Cache helpers ──────────────────────────────────────────────────────────
|
|
104
|
+
async function readCache() {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(await fs.readFile(CACHE_PATH, 'utf-8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function writeCache(entries) {
|
|
113
|
+
try {
|
|
114
|
+
await fs.writeFile(CACHE_PATH, JSON.stringify({ fetchedAt: Date.now(), entries }), 'utf-8');
|
|
115
|
+
}
|
|
116
|
+
catch { /* non-fatal */ }
|
|
117
|
+
}
|
|
118
|
+
// ── Bucket aggregation ────────────────────────────────────────────────────
|
|
119
|
+
function buildSnapshots(entries, fromCache) {
|
|
120
|
+
const source = fromCache ? 'cache' : 'official-cli';
|
|
121
|
+
const geminiEntries = entries.filter(e => /gemini/i.test(e.model));
|
|
122
|
+
const otherEntries = entries.filter(e => !/gemini/i.test(e.model));
|
|
123
|
+
function worstCase(bucket, tool) {
|
|
124
|
+
if (bucket.length === 0) {
|
|
125
|
+
return { tool, remainingPercent: null, usedPercent: null, resetAt: null, source: 'unknown' };
|
|
126
|
+
}
|
|
127
|
+
const worst = bucket.reduce((a, b) => a.percent <= b.percent ? a : b);
|
|
128
|
+
return {
|
|
129
|
+
tool,
|
|
130
|
+
remainingPercent: worst.percent,
|
|
131
|
+
usedPercent: 100 - worst.percent,
|
|
132
|
+
resetAt: worst.refreshLine ?? null,
|
|
133
|
+
source,
|
|
134
|
+
raw: { matchedModel: worst.model, allModels: bucket.map(e => `${e.model}: ${e.percent}%`) },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
worstCase(geminiEntries, 'agy-gemini'),
|
|
139
|
+
worstCase(otherEntries, 'agy-other'),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
// ── Adapter ───────────────────────────────────────────────────────────────
|
|
143
|
+
export class AgyQuotaAdapter {
|
|
144
|
+
async fetchSnapshots() {
|
|
145
|
+
// Fast path: serve from cache if fresh enough
|
|
146
|
+
const cached = await readCache();
|
|
147
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
148
|
+
return buildSnapshots(cached.entries, true);
|
|
149
|
+
}
|
|
150
|
+
// Slow path: spawn agy via tmux, scrape the quota panel
|
|
151
|
+
try {
|
|
152
|
+
const raw = await runAgyUsage();
|
|
153
|
+
const entries = parseQuotaPanel(raw);
|
|
154
|
+
if (entries.length > 0) {
|
|
155
|
+
await writeCache(entries);
|
|
156
|
+
return buildSnapshots(entries, false);
|
|
157
|
+
}
|
|
158
|
+
return [
|
|
159
|
+
{ tool: 'agy-gemini', remainingPercent: null, usedPercent: null, resetAt: null, source: 'unknown' },
|
|
160
|
+
{ tool: 'agy-other', remainingPercent: null, usedPercent: null, resetAt: null, source: 'unknown' },
|
|
161
|
+
];
|
|
92
162
|
}
|
|
93
163
|
catch (error) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
remainingPercent: null,
|
|
97
|
-
usedPercent: null,
|
|
98
|
-
|
|
99
|
-
source: 'unknown',
|
|
100
|
-
raw: error instanceof Error ? error.message : String(error)
|
|
101
|
-
};
|
|
164
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
165
|
+
return [
|
|
166
|
+
{ tool: 'agy-gemini', remainingPercent: null, usedPercent: null, resetAt: null, source: 'unknown', raw: msg },
|
|
167
|
+
{ tool: 'agy-other', remainingPercent: null, usedPercent: null, resetAt: null, source: 'unknown', raw: msg },
|
|
168
|
+
];
|
|
102
169
|
}
|
|
103
170
|
}
|
|
104
171
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { QuotaAdapter, UsageSnapshot } from './index.js';
|
|
2
2
|
export declare class ClaudeQuotaAdapter implements QuotaAdapter {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
fetchSnapshot(): Promise<UsageSnapshot>;
|
|
3
|
+
fetchSnapshots(): Promise<UsageSnapshot[]>;
|
|
4
|
+
private _fetch;
|
|
6
5
|
}
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,72 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { TuiScraper } from '../tmux.js';
|
|
2
|
+
import { debug } from '../debug.js';
|
|
3
|
+
// ── TUI scraper ────────────────────────────────────────────────────────────
|
|
4
|
+
async function sleep(ms) {
|
|
5
|
+
return new Promise(res => setTimeout(res, ms));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Launches `claude` in a tmux session, opens /status, navigates to the
|
|
9
|
+
* Status tab (which shows real quota usage bars), and returns the captured
|
|
10
|
+
* screen text.
|
|
11
|
+
*
|
|
12
|
+
* The Status tab renders persistently (not transient), so regular
|
|
13
|
+
* capture-pane is sufficient — no pipe-pane needed.
|
|
14
|
+
*/
|
|
15
|
+
async function runClaudeScrape() {
|
|
16
|
+
const tui = new TuiScraper('claude');
|
|
17
|
+
try {
|
|
18
|
+
tui.start();
|
|
19
|
+
// Wait for TUI ready — welcome banner or prompt hint visible
|
|
20
|
+
await tui.waitFor(/Welcome back|Try "/i, 15_000, 0);
|
|
21
|
+
// Open the /status panel
|
|
22
|
+
tui.send('/status');
|
|
23
|
+
// Wait for the status panel tabs to appear
|
|
24
|
+
await tui.waitFor(/Settings\s+Status/i, 10_000, 0);
|
|
25
|
+
// Navigate to the Status tab (second tab after Settings)
|
|
26
|
+
tui.sendKey('Tab');
|
|
27
|
+
await sleep(300);
|
|
28
|
+
tui.sendKey('Tab');
|
|
29
|
+
// Wait for the usage bars — shows "XX% used"
|
|
30
|
+
return await tui.waitFor(/\d+%\s+used/i, 8_000, 0);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
tui.kill();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseScrapeOutput(screen) {
|
|
37
|
+
debug('claude:parse', `screen length: ${screen.length}`);
|
|
38
|
+
debug('claude:parse', 'screen', screen);
|
|
39
|
+
// Match "XX% used" occurrences in order:
|
|
40
|
+
// First = current session (5h block), second = current week
|
|
41
|
+
const usedMatches = [...screen.matchAll(/(\d+)%\s+used/gi)];
|
|
42
|
+
debug('claude:parse', `found ${usedMatches.length} "% used" matches`);
|
|
43
|
+
const sessionUsedPct = usedMatches[0] ? parseInt(usedMatches[0][1], 10) : null;
|
|
44
|
+
const weeklyUsedPct = usedMatches[1] ? parseInt(usedMatches[1][1], 10) : null;
|
|
45
|
+
// Reset time: "Resets H:MMam" or "Resets May 30 at 6am" — grab the first occurrence
|
|
46
|
+
const resetMatch = screen.match(/Resets\s+([^\n\r]+)/i);
|
|
47
|
+
const sessionResetAt = resetMatch ? resetMatch[1].trim() : null;
|
|
48
|
+
debug('claude:parse', 'result', { sessionUsedPct, sessionResetAt, weeklyUsedPct });
|
|
49
|
+
return { sessionUsedPct, sessionResetAt, weeklyUsedPct };
|
|
50
|
+
}
|
|
51
|
+
// ── Adapter ────────────────────────────────────────────────────────────────
|
|
4
52
|
export class ClaudeQuotaAdapter {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Default to $10.00 for the rolling 5-hour window, allow env override
|
|
8
|
-
this.budgetLimit = Number(process.env.AGENT_FUEL_CLAUDE_BUDGET) || 10.0;
|
|
53
|
+
async fetchSnapshots() {
|
|
54
|
+
return [await this._fetch()];
|
|
9
55
|
}
|
|
10
|
-
async
|
|
56
|
+
async _fetch() {
|
|
57
|
+
const unknown = () => ({
|
|
58
|
+
tool: 'claude-code',
|
|
59
|
+
remainingPercent: null,
|
|
60
|
+
usedPercent: null,
|
|
61
|
+
resetAt: null,
|
|
62
|
+
source: 'unknown',
|
|
63
|
+
});
|
|
64
|
+
debug('claude:fetch', 'starting TUI scrape via tmux');
|
|
11
65
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
stdout = result.stdout;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
throw new Error('ccusage package is not installed or available locally. Please run "npm install -g ccusage" to use this tool.');
|
|
21
|
-
}
|
|
22
|
-
const data = JSON.parse(stdout);
|
|
23
|
-
const blocks = data && Array.isArray(data.blocks) ? data.blocks : data;
|
|
24
|
-
if (!blocks || !Array.isArray(blocks)) {
|
|
25
|
-
throw new Error('Invalid JSON format returned from ccusage blocks');
|
|
26
|
-
}
|
|
27
|
-
// Find the active billing block
|
|
28
|
-
const activeBlock = blocks.find((block) => block.isActive === true);
|
|
29
|
-
if (!activeBlock) {
|
|
66
|
+
const screen = await runClaudeScrape();
|
|
67
|
+
const result = parseScrapeOutput(screen);
|
|
68
|
+
if (result.sessionUsedPct !== null) {
|
|
69
|
+
const remainingPercent = Math.max(0, 100 - result.sessionUsedPct);
|
|
70
|
+
debug('claude:fetch', `parsed Usage tab → ${result.sessionUsedPct}% used (${remainingPercent}% remaining)`);
|
|
30
71
|
return {
|
|
31
72
|
tool: 'claude-code',
|
|
32
|
-
remainingPercent
|
|
33
|
-
usedPercent:
|
|
34
|
-
resetAt:
|
|
35
|
-
source: '
|
|
73
|
+
remainingPercent,
|
|
74
|
+
usedPercent: result.sessionUsedPct,
|
|
75
|
+
resetAt: result.sessionResetAt,
|
|
76
|
+
source: 'official-cli',
|
|
36
77
|
};
|
|
37
78
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const remainingPercent = Math.max(0, Math.min(100, Math.round(100 - usedPercent)));
|
|
41
|
-
let resetAt = null;
|
|
42
|
-
if (activeBlock.endTime) {
|
|
43
|
-
try {
|
|
44
|
-
const endDate = new Date(activeBlock.endTime);
|
|
45
|
-
resetAt = endDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
resetAt = activeBlock.endTime;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
tool: 'claude-code',
|
|
53
|
-
remainingPercent,
|
|
54
|
-
usedPercent: Math.round(usedPercent),
|
|
55
|
-
resetAt,
|
|
56
|
-
source: 'ccusage',
|
|
57
|
-
raw: activeBlock
|
|
58
|
-
};
|
|
79
|
+
debug('claude:fetch', 'parse failed → unknown');
|
|
80
|
+
return unknown();
|
|
59
81
|
}
|
|
60
|
-
catch (
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
tool: 'claude-code',
|
|
64
|
-
remainingPercent: null,
|
|
65
|
-
usedPercent: null,
|
|
66
|
-
resetAt: null,
|
|
67
|
-
source: 'unknown',
|
|
68
|
-
raw: error instanceof Error ? error.message : String(error)
|
|
69
|
-
};
|
|
82
|
+
catch (err) {
|
|
83
|
+
debug('claude:fetch', 'caught error', String(err));
|
|
84
|
+
return unknown();
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
}
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QuotaAdapter, UsageSnapshot } from './index.js';
|
|
2
2
|
export declare class CodexQuotaAdapter implements QuotaAdapter {
|
|
3
|
-
private budgetLimit;
|
|
3
|
+
private readonly budgetLimit;
|
|
4
4
|
constructor();
|
|
5
|
-
|
|
5
|
+
fetchSnapshots(): Promise<UsageSnapshot[]>;
|
|
6
|
+
private _fetch;
|
|
6
7
|
}
|