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 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,6 @@
1
+ import { QuotaAdapter, UsageSnapshot } from './index.js';
2
+ export declare class AgyQuotaAdapter implements QuotaAdapter {
3
+ private configDir;
4
+ constructor();
5
+ fetchSnapshot(): Promise<UsageSnapshot>;
6
+ }
@@ -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,6 @@
1
+ import { QuotaAdapter, UsageSnapshot } from './index.js';
2
+ export declare class ClaudeQuotaAdapter implements QuotaAdapter {
3
+ private budgetLimit;
4
+ constructor();
5
+ fetchSnapshot(): Promise<UsageSnapshot>;
6
+ }
@@ -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,6 @@
1
+ import { QuotaAdapter, UsageSnapshot } from './index.js';
2
+ export declare class CodexQuotaAdapter implements QuotaAdapter {
3
+ private budgetLimit;
4
+ constructor();
5
+ fetchSnapshot(): Promise<UsageSnapshot>;
6
+ }
@@ -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 {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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();
@@ -0,0 +1,2 @@
1
+ import { UsageSnapshot } from './adapters/index.js';
2
+ export declare function renderDashboard(snapshots: UsageSnapshot[]): void;
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
+ }