agent-fuel 0.1.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 +85 -0
- package/dist/adapters/agy.d.ts +6 -0
- package/dist/adapters/agy.js +108 -0
- package/dist/adapters/claude.d.ts +6 -0
- package/dist/adapters/claude.js +72 -0
- package/dist/adapters/codex.d.ts +6 -0
- package/dist/adapters/codex.js +105 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +29 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +61 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# ⚡️ Agent Fuel (`agent-fuel`)
|
|
2
|
+
|
|
3
|
+
A sleek, unified CLI dashboard to monitor your AI coding assistant quotas, credits, and token usage in real-time.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Start
|
|
8
|
+
|
|
9
|
+
Get **Agent Fuel** installed, globally linked, and running on your system with a single one-liner command:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install && npm run build && npm link && agent-fuel
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Once globally linked, you can run the dashboard at any time from **any directory** on your machine by simply typing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
agent-fuel
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 💡 The Motivation
|
|
24
|
+
|
|
25
|
+
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. Whether it is a daily dollar limit, token ceilings, or monthly credits, developers are forced to jump through interactive prompts or scrape configuration screens just to answer a simple question:
|
|
26
|
+
|
|
27
|
+
> **"How much agent fuel do I have left before starting this massive refactor?"**
|
|
28
|
+
|
|
29
|
+
Because each CLI exposes quota information differently (some via strict JSON, others through human-readable prompts, and some in local state files), there is no centralized way to monitor your resource consumption.
|
|
30
|
+
|
|
31
|
+
**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**.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 🎯 The Idea
|
|
36
|
+
|
|
37
|
+
`agent-fuel` is a tiny, modern local CLI built with TypeScript that:
|
|
38
|
+
1. **Dispatches Adapters**: Queries each configured AI coding tool (Claude Code, Codex, AGY) using native CLI calls, helper utilities (like `ccusage`), or local config parsing.
|
|
39
|
+
2. **Normalizes Quota Models**: Standardizes diverse limits into a uniform percentage score (`0` to `100%`).
|
|
40
|
+
3. **Renders an Elegant CLI Dashboard**: Displays a high-fidelity 3-bar ASCII progress dashboard directly in your terminal.
|
|
41
|
+
|
|
42
|
+
### Proposed Architecture
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
agent-fuel/
|
|
46
|
+
├── src/
|
|
47
|
+
│ ├── index.ts # CLI entry point
|
|
48
|
+
│ ├── render.ts # Beautiful 3-bar dashboard renderer
|
|
49
|
+
│ └── adapters/
|
|
50
|
+
│ ├── claude.ts # Adapter for Claude Code (ccusage/native)
|
|
51
|
+
│ ├── codex.ts # Adapter for Codex (local session/status)
|
|
52
|
+
│ ├── agy.ts # Adapter for AGY (Antigravity config parser)
|
|
53
|
+
│ └── ccusage.ts # Shared JSON parser helper
|
|
54
|
+
├── package.json
|
|
55
|
+
└── README.md
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### High-Fidelity API Type Shape
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
type UsageSnapshot = {
|
|
62
|
+
tool: 'codex' | 'claude-code' | 'agy';
|
|
63
|
+
remainingPercent: number | null; // Unified 0-100 scale
|
|
64
|
+
usedPercent?: number | null;
|
|
65
|
+
resetAt?: string | null;
|
|
66
|
+
source: 'official-cli' | 'ccusage' | 'local-state' | 'provider-api' | 'unknown';
|
|
67
|
+
raw?: unknown;
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 📊 Terminal Dashboard Preview
|
|
74
|
+
|
|
75
|
+
Running `agent-fuel` will immediately output a clean, colored visual summary of your current agent capacity:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
⚡️ Agent Fuel - CLI Quota Monitor
|
|
79
|
+
|
|
80
|
+
Codex [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] unknown
|
|
81
|
+
Claude Code [██████████████████████████░░░░] 86% remaining (resets 01:00 PM)
|
|
82
|
+
AGY [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] unknown
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
export class AgyQuotaAdapter {
|
|
5
|
+
configDir;
|
|
6
|
+
constructor() {
|
|
7
|
+
// Default to the standard AGY CLI config directory
|
|
8
|
+
this.configDir = path.join(os.homedir(), '.gemini/antigravity-cli');
|
|
9
|
+
}
|
|
10
|
+
async fetchSnapshot() {
|
|
11
|
+
try {
|
|
12
|
+
const settingsPath = path.join(this.configDir, 'settings.json');
|
|
13
|
+
const historyPath = path.join(this.configDir, 'history.jsonl');
|
|
14
|
+
let activeModel = 'Gemini 3.5 Flash';
|
|
15
|
+
// 1. Read active model from settings.json if it exists
|
|
16
|
+
try {
|
|
17
|
+
const settingsContent = await fs.readFile(settingsPath, 'utf-8');
|
|
18
|
+
const settings = JSON.parse(settingsContent);
|
|
19
|
+
if (settings && settings.model) {
|
|
20
|
+
activeModel = settings.model;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Fallback to default model name if reading or parsing settings failed
|
|
25
|
+
}
|
|
26
|
+
// 2. Read history.jsonl to detect active prompts today
|
|
27
|
+
let todayPromptsCount = 0;
|
|
28
|
+
let latestPromptTimestamp = null;
|
|
29
|
+
// Construct local todayPrefix in YYYY-MM-DD format (timezone aware)
|
|
30
|
+
const now = new Date();
|
|
31
|
+
const year = now.getFullYear();
|
|
32
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
33
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
34
|
+
const todayPrefix = `${year}-${month}-${day}`;
|
|
35
|
+
try {
|
|
36
|
+
const historyContent = await fs.readFile(historyPath, 'utf-8');
|
|
37
|
+
const historyLines = historyContent.trim().split('\n');
|
|
38
|
+
for (const line of historyLines) {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
continue;
|
|
41
|
+
const entry = JSON.parse(line);
|
|
42
|
+
if (entry && entry.timestamp) {
|
|
43
|
+
// Get local date YYYY-MM-DD for the entry's timestamp
|
|
44
|
+
const entryDateObj = new Date(entry.timestamp);
|
|
45
|
+
const eYear = entryDateObj.getFullYear();
|
|
46
|
+
const eMonth = String(entryDateObj.getMonth() + 1).padStart(2, '0');
|
|
47
|
+
const eDay = String(entryDateObj.getDate()).padStart(2, '0');
|
|
48
|
+
const entryDate = `${eYear}-${eMonth}-${eDay}`;
|
|
49
|
+
if (entryDate === todayPrefix) {
|
|
50
|
+
todayPromptsCount++;
|
|
51
|
+
if (!latestPromptTimestamp || entry.timestamp > latestPromptTimestamp) {
|
|
52
|
+
latestPromptTimestamp = entry.timestamp;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Fallback if reading or parsing history failed (e.g. file doesn't exist)
|
|
60
|
+
}
|
|
61
|
+
// 3. Calculate remaining percent based on active usage and model tier
|
|
62
|
+
// Support dynamic overrides using AGENT_FUEL_AGY_PERCENT environment variable
|
|
63
|
+
let remainingPercent = 100;
|
|
64
|
+
const isProModel = activeModel.toLowerCase().includes('pro');
|
|
65
|
+
const limit = isProModel ? 3 : 5; // Pro models have a tighter limit of 3, Flash has 5
|
|
66
|
+
const costPerPrompt = 100 / limit;
|
|
67
|
+
const calculatedPercent = Math.max(0, Math.round(100 - (todayPromptsCount * costPerPrompt)));
|
|
68
|
+
if (process.env.AGENT_FUEL_AGY_PERCENT) {
|
|
69
|
+
const envVal = Number(process.env.AGENT_FUEL_AGY_PERCENT);
|
|
70
|
+
remainingPercent = !isNaN(envVal) ? Math.max(0, Math.min(100, envVal)) : calculatedPercent;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
remainingPercent = calculatedPercent;
|
|
74
|
+
}
|
|
75
|
+
// 4. Calculate rolling reset time (5 hours rolling or resets in 4h 37m from latest prompt, giving ~01:30 PM resets)
|
|
76
|
+
let resetAt = null;
|
|
77
|
+
if (latestPromptTimestamp) {
|
|
78
|
+
try {
|
|
79
|
+
const lastActivityDate = new Date(latestPromptTimestamp);
|
|
80
|
+
// Roll forward 5 hours (refreshes in ~4h 37m from active run)
|
|
81
|
+
const resetDate = new Date(lastActivityDate.getTime() + 5 * 60 * 60 * 1000);
|
|
82
|
+
resetAt = resetDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
resetAt = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
tool: 'agy',
|
|
90
|
+
remainingPercent,
|
|
91
|
+
usedPercent: 100 - remainingPercent,
|
|
92
|
+
resetAt,
|
|
93
|
+
source: 'local-state',
|
|
94
|
+
raw: { activeModel, todayPromptsCount }
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
tool: 'agy',
|
|
100
|
+
remainingPercent: null,
|
|
101
|
+
usedPercent: null,
|
|
102
|
+
resetAt: null,
|
|
103
|
+
source: 'unknown',
|
|
104
|
+
raw: error instanceof Error ? error.message : String(error)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
export class ClaudeQuotaAdapter {
|
|
5
|
+
budgetLimit;
|
|
6
|
+
constructor() {
|
|
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;
|
|
9
|
+
}
|
|
10
|
+
async fetchSnapshot() {
|
|
11
|
+
try {
|
|
12
|
+
// Execute ccusage to get billing block information in JSON format
|
|
13
|
+
// We run npx --no-install first to see if it's already cached/available, otherwise fall back to regular npx
|
|
14
|
+
let stdout;
|
|
15
|
+
try {
|
|
16
|
+
const result = await execAsync('npx --no-install ccusage blocks --json');
|
|
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) {
|
|
30
|
+
return {
|
|
31
|
+
tool: 'claude-code',
|
|
32
|
+
remainingPercent: null,
|
|
33
|
+
usedPercent: null,
|
|
34
|
+
resetAt: null,
|
|
35
|
+
source: 'unknown'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const cost = activeBlock.costUSD || 0.0;
|
|
39
|
+
const usedPercent = (cost / this.budgetLimit) * 100;
|
|
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
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Fallback in case of execution errors
|
|
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
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { exec } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
export class CodexQuotaAdapter {
|
|
5
|
+
budgetLimit;
|
|
6
|
+
constructor() {
|
|
7
|
+
// Default budget limit of $20.00 for the rolling 5h window (Standard Team/Plus limit)
|
|
8
|
+
// Allows dynamic override using environment variable AGENT_FUEL_CODEX_BUDGET
|
|
9
|
+
this.budgetLimit = Number(process.env.AGENT_FUEL_CODEX_BUDGET) || 20.0;
|
|
10
|
+
}
|
|
11
|
+
async fetchSnapshot() {
|
|
12
|
+
try {
|
|
13
|
+
// Execute ccusage to get Codex session data
|
|
14
|
+
let stdout;
|
|
15
|
+
try {
|
|
16
|
+
const result = await execAsync('npx --no-install ccusage codex session --json');
|
|
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 sessions = data && Array.isArray(data.sessions) ? data.sessions : (data && Array.isArray(data.session) ? data.session : data);
|
|
24
|
+
if (!sessions || !Array.isArray(sessions)) {
|
|
25
|
+
throw new Error('Invalid JSON format returned from ccusage codex session');
|
|
26
|
+
}
|
|
27
|
+
// Filter sessions for today's date in local time
|
|
28
|
+
const now = new Date();
|
|
29
|
+
const year = now.getFullYear();
|
|
30
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
31
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
32
|
+
const todayPrefix = `${year}-${month}-${day}`;
|
|
33
|
+
const todaySessions = sessions.filter((s) => {
|
|
34
|
+
if (!s.lastActivity)
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
const dateObj = new Date(s.lastActivity);
|
|
38
|
+
const sYear = dateObj.getFullYear();
|
|
39
|
+
const sMonth = String(dateObj.getMonth() + 1).padStart(2, '0');
|
|
40
|
+
const sDay = String(dateObj.getDate()).padStart(2, '0');
|
|
41
|
+
const sLocalDate = `${sYear}-${sMonth}-${sDay}`;
|
|
42
|
+
return sLocalDate === todayPrefix;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (todaySessions.length === 0) {
|
|
49
|
+
// No activity today, so 100% fuel remaining
|
|
50
|
+
return {
|
|
51
|
+
tool: 'codex',
|
|
52
|
+
remainingPercent: 100,
|
|
53
|
+
usedPercent: 0,
|
|
54
|
+
resetAt: null,
|
|
55
|
+
source: 'ccusage'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Sum today's cost
|
|
59
|
+
const totalCost = todaySessions.reduce((acc, s) => acc + (s.costUSD || 0.0), 0.0);
|
|
60
|
+
const usedPercent = (totalCost / this.budgetLimit) * 100;
|
|
61
|
+
// Calculate remaining percentage
|
|
62
|
+
let remainingPercent = 100 - usedPercent;
|
|
63
|
+
if (usedPercent > 0 && remainingPercent > 99) {
|
|
64
|
+
// Micro-interaction: if they burned any credits, show 99% instead of rounding to 100%
|
|
65
|
+
remainingPercent = 99;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
remainingPercent = Math.max(0, Math.min(100, Math.round(remainingPercent)));
|
|
69
|
+
}
|
|
70
|
+
// Calculate rolling 5-hour reset time based on the most recent session's activity
|
|
71
|
+
let resetAt = null;
|
|
72
|
+
const sortedSessions = [...todaySessions].sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
|
|
73
|
+
const latestSession = sortedSessions[0];
|
|
74
|
+
if (latestSession && latestSession.lastActivity) {
|
|
75
|
+
try {
|
|
76
|
+
const lastActivityDate = new Date(latestSession.lastActivity);
|
|
77
|
+
// Roll forward 5 hours for the rolling limit window
|
|
78
|
+
const resetDate = new Date(lastActivityDate.getTime() + 5 * 60 * 60 * 1000);
|
|
79
|
+
resetAt = resetDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
resetAt = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
tool: 'codex',
|
|
87
|
+
remainingPercent,
|
|
88
|
+
usedPercent: Math.round(usedPercent),
|
|
89
|
+
resetAt,
|
|
90
|
+
source: 'ccusage',
|
|
91
|
+
raw: { totalCost, todaySessionsCount: todaySessions.length }
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return {
|
|
96
|
+
tool: 'codex',
|
|
97
|
+
remainingPercent: null,
|
|
98
|
+
usedPercent: null,
|
|
99
|
+
resetAt: null,
|
|
100
|
+
source: 'unknown',
|
|
101
|
+
raw: error instanceof Error ? error.message : String(error)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface UsageSnapshot {
|
|
2
|
+
tool: 'codex' | 'claude-code' | 'agy';
|
|
3
|
+
remainingPercent: number | null;
|
|
4
|
+
usedPercent?: number | null;
|
|
5
|
+
resetAt?: string | null;
|
|
6
|
+
source: 'official-cli' | 'ccusage' | 'local-state' | 'provider-api' | 'unknown';
|
|
7
|
+
raw?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface QuotaAdapter {
|
|
10
|
+
fetchSnapshot(): Promise<UsageSnapshot>;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { ClaudeQuotaAdapter } from './adapters/claude.js';
|
|
3
|
+
import { CodexQuotaAdapter } from './adapters/codex.js';
|
|
4
|
+
import { AgyQuotaAdapter } from './adapters/agy.js';
|
|
5
|
+
import { renderDashboard } from './render.js';
|
|
6
|
+
async function main() {
|
|
7
|
+
const claudeAdapter = new ClaudeQuotaAdapter();
|
|
8
|
+
const codexAdapter = new CodexQuotaAdapter();
|
|
9
|
+
const agyAdapter = new AgyQuotaAdapter();
|
|
10
|
+
try {
|
|
11
|
+
// Run all adapters concurrently to minimize startup latency
|
|
12
|
+
const [claudeSnap, codexSnap, agySnap] = await Promise.all([
|
|
13
|
+
claudeAdapter.fetchSnapshot(),
|
|
14
|
+
codexAdapter.fetchSnapshot(),
|
|
15
|
+
agyAdapter.fetchSnapshot()
|
|
16
|
+
]);
|
|
17
|
+
// Render the beautiful 3-bar ASCII progress dashboard
|
|
18
|
+
renderDashboard([
|
|
19
|
+
codexSnap,
|
|
20
|
+
claudeSnap,
|
|
21
|
+
agySnap
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error('\x1b[31mFatal error orchestrating Agent Fuel CLI:\x1b[0m', error);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
main();
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function renderDashboard(snapshots) {
|
|
2
|
+
const reset = '\x1b[0m';
|
|
3
|
+
const bold = '\x1b[1m';
|
|
4
|
+
const dim = '\x1b[2m';
|
|
5
|
+
const cyan = '\x1b[36m';
|
|
6
|
+
const green = '\x1b[32m';
|
|
7
|
+
const yellow = '\x1b[33m';
|
|
8
|
+
const red = '\x1b[31m';
|
|
9
|
+
const gray = '\x1b[90m';
|
|
10
|
+
console.log(`\n${bold}${cyan}⚡️ Agent Fuel - CLI Quota Monitor${reset}\n`);
|
|
11
|
+
for (const snap of snapshots) {
|
|
12
|
+
const displayName = getDisplayName(snap.tool);
|
|
13
|
+
const remaining = snap.remainingPercent;
|
|
14
|
+
const width = 30;
|
|
15
|
+
let barStr = '';
|
|
16
|
+
let percentStr = '';
|
|
17
|
+
if (remaining === null || remaining === undefined) {
|
|
18
|
+
// Unknown/Unconfigured quota
|
|
19
|
+
barStr = `${gray}${'░'.repeat(width)}${reset}`;
|
|
20
|
+
percentStr = `${gray}unknown${reset}`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const filled = Math.max(0, Math.min(width, Math.round((remaining * width) / 100)));
|
|
24
|
+
const empty = width - filled;
|
|
25
|
+
// Color scheme based on remaining percentage
|
|
26
|
+
let color = green;
|
|
27
|
+
if (remaining < 20) {
|
|
28
|
+
color = red;
|
|
29
|
+
}
|
|
30
|
+
else if (remaining < 50) {
|
|
31
|
+
color = yellow;
|
|
32
|
+
}
|
|
33
|
+
const blockChar = '█';
|
|
34
|
+
const shadeChar = '░';
|
|
35
|
+
barStr = `${color}${blockChar.repeat(filled)}${reset}${gray}${shadeChar.repeat(empty)}${reset}`;
|
|
36
|
+
percentStr = `${bold}${color}${remaining.toString().padStart(3)}% remaining${reset}`;
|
|
37
|
+
}
|
|
38
|
+
// Add metadata/reset times if available
|
|
39
|
+
let detailStr = '';
|
|
40
|
+
if (snap.resetAt) {
|
|
41
|
+
detailStr = ` ${dim}${gray}(resets ${snap.resetAt})${reset}`;
|
|
42
|
+
}
|
|
43
|
+
if (snap.tool === 'agy' && snap.raw && typeof snap.raw === 'object' && 'activeModel' in snap.raw) {
|
|
44
|
+
detailStr += ` ${dim}${gray}[${snap.raw.activeModel}]${reset}`;
|
|
45
|
+
}
|
|
46
|
+
console.log(`${bold}${displayName.padEnd(12)}${reset} [${barStr}] ${percentStr}${detailStr}`);
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
function getDisplayName(tool) {
|
|
51
|
+
switch (tool) {
|
|
52
|
+
case 'codex':
|
|
53
|
+
return 'Codex';
|
|
54
|
+
case 'claude-code':
|
|
55
|
+
return 'Claude Code';
|
|
56
|
+
case 'agy':
|
|
57
|
+
return 'AGY';
|
|
58
|
+
default:
|
|
59
|
+
return tool;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-fuel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sleek term-based dashboard for AI coding CLI quotas",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-fuel": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc -w",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"agent",
|
|
22
|
+
"quota",
|
|
23
|
+
"dashboard",
|
|
24
|
+
"claude",
|
|
25
|
+
"codex",
|
|
26
|
+
"agy"
|
|
27
|
+
],
|
|
28
|
+
"author": "Pedro Rodrigues",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.11.24",
|
|
32
|
+
"typescript": "^5.3.3"
|
|
33
|
+
}
|
|
34
|
+
}
|