agent-fuel 0.4.2 → 0.5.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 CHANGED
@@ -45,10 +45,11 @@ AI coding assistants are now integral to developer workflows. Tools like **Claud
45
45
  `agent-fuel` is a tiny modern CLI built with TypeScript that:
46
46
 
47
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.
48
+ 2. **Streams Consolidated Quota Live** — renders a weighted **Total** bar on top which calculates and updates in real-time as each provider finishes loading, showing the live calculated portion rather than waiting for all adapters to finish.
49
+ 3. **Normalises Quota Models** — standardises diverse limits into a uniform `0–100%` score.
50
+ 4. **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.
51
+ 5. **Caches AGY results** — AGY quota is cached for 5 minutes so repeated runs are instant (~1s).
52
+ 6. **Renders a clean dashboard** — colour-coded bars with reset times directly in your terminal.
52
53
 
53
54
  ### Project Architecture
54
55
 
@@ -57,6 +58,7 @@ agent-fuel/
57
58
  ├── src/
58
59
  │ ├── index.ts # CLI entry point — runs all adapters concurrently
59
60
  │ ├── render.ts # Colour-coded bar dashboard renderer
61
+ │ ├── config.ts # Config file manager & config command handler
60
62
  │ └── adapters/
61
63
  │ ├── index.ts # Shared UsageSnapshot type & QuotaAdapter interface
62
64
  │ ├── claude.ts # Claude Code (via ccusage blocks)
@@ -86,28 +88,67 @@ type UsageSnapshot = {
86
88
  ```
87
89
  ⚡️ Agent Fuel - CLI Quota Monitor
88
90
 
89
- Claude Code [██████████████████████░░░░░░░░] 72% remaining (resets 23:10 (Europe/Copenhagen))
90
- Codex [█████████████████████░░░░░░░░░] 69% remaining (resets 23:37)
91
+ Total [████████████████████████░░░░░░] 81% remaining (tune weights: agent-fuel config)
92
+
93
+ Claude Code [██████████████████████░░░░░░░░] 74% remaining (resets 13:19 (Europe/Copenhagen))
94
+ Codex [██████████████████████████████] 99% remaining (resets 13:56)
91
95
  AGY Gemini [██████████████████████████████] 100% remaining ✓ quota available [Gemini 3.5 Flash (Medium)]
92
- AGY Other [████████████░░░░░░░░░░░░░░░░░░] 40% remaining (resets in 122h 53m) [Claude Sonnet 4.6 (Thinking)]
96
+ AGY Other [████████████░░░░░░░░░░░░░░░░░░] 40% remaining (resets in 109h 12m) [Claude Sonnet 4.6 (Thinking)]
93
97
 
94
- agent-fuel v0.x.y
98
+ agent-fuel v0.5.0
95
99
  ```
96
100
 
97
- Rows appear as each adapter resolves Claude Code (instant) prints first, Codex and AGY follow as their TUI scrapes complete.
101
+ - **Total** bar prints on top (in TTY interactive mode) showing the weighted consolidated remaining quota. As adapters load, the bar fills up in real-time. When fully loaded, a helpful CLI reminder is displayed alongside the Total percentage.
102
+ - Rows appear as each adapter resolves — Claude Code (instant) prints first, Codex and AGY follow as their TUI scrapes complete.
103
+ - **AGY Gemini** shows the worst-case remaining across all `Gemini *` model tiers.
104
+ - **AGY Other** shows the worst-case across Claude and other non-Gemini models.
105
+ - **Codex** row tagged `[~est]` when quota has not been reached and the percentage is estimated from local session cost data (see fallback note below).
106
+
107
+ ---
108
+
109
+ ## ⚙️ Configuration & Custom Weights
110
+
111
+ Different developers operate under different quota sizes. By default, `agent-fuel` weights each provider bucket as standard proxies for monthly dollar subscription amounts:
112
+ - Claude Code (`claude-code`): `20`
113
+ - Codex CLI (`codex`): `20`
114
+ - AGY Gemini (`agy-gemini`): `10`
115
+ - AGY Other (`agy-other`): `10`
116
+
117
+ If a provider is completely unused or fails to return a quota percentage, its weight is **dynamically excluded** from the calculation, ensuring that missing/unused services don't break the consolidated bar.
98
118
 
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).
119
+ ### Managing Settings via the CLI
120
+
121
+ You can view or update your weights and settings directly using the CLI:
122
+
123
+ * **View Active Configuration**:
124
+ ```bash
125
+ agent-fuel config
126
+ ```
127
+ * **Change Provider Weight**:
128
+ ```bash
129
+ agent-fuel config set claude-code 50
130
+ ```
131
+ * **Disable/Enable Total Bar**:
132
+ ```bash
133
+ agent-fuel config set show-total false
134
+ ```
135
+
136
+ Settings are persistently saved to `~/.config/agent-fuel/config.json`.
102
137
 
103
138
  ---
104
139
 
105
140
  ## ⚙️ Environment Overrides
106
141
 
142
+ Environment variables take highest precedence and override any values saved in the config JSON file:
143
+
107
144
  | Variable | Default | Description |
108
145
  |---|---|---|
109
146
  | `AGENT_FUEL_CLAUDE_BUDGET` | `20.0` | Claude Code rolling budget in USD |
110
147
  | `AGENT_FUEL_CODEX_BUDGET` | `20.0` | **Fallback estimate only** — Codex rolling budget in USD |
148
+ | `AGENT_FUEL_WEIGHT_CLAUDE` | `20` | Weight size ratio of the Claude Code quota pool |
149
+ | `AGENT_FUEL_WEIGHT_CODEX` | `20` | Weight size ratio of the Codex quota pool |
150
+ | `AGENT_FUEL_WEIGHT_AGY_GEMINI` | `10` | Weight size ratio of the AGY Gemini quota pool |
151
+ | `AGENT_FUEL_WEIGHT_AGY_OTHER` | `10` | Weight size ratio of the AGY Other quota pool |
152
+ | `AGENT_FUEL_SHOW_TOTAL` | `true` | Show or hide the consolidated Total quota bar (`true`/`false`) |
111
153
 
112
154
  > **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
-
@@ -1,9 +1,10 @@
1
1
  export interface UsageSnapshot {
2
- tool: 'codex' | 'claude-code' | 'agy-gemini' | 'agy-other';
2
+ tool: 'codex' | 'claude-code' | 'agy-gemini' | 'agy-other' | 'total';
3
3
  remainingPercent: number | null;
4
4
  usedPercent?: number | null;
5
5
  resetAt?: string | null;
6
6
  source: 'official-cli' | 'ccusage' | 'local-state' | 'provider-api' | 'cache' | 'unknown';
7
+ isLoading?: boolean;
7
8
  raw?: unknown;
8
9
  }
9
10
  export interface QuotaAdapter {
@@ -0,0 +1,15 @@
1
+ export interface Config {
2
+ weights: {
3
+ 'claude-code': number;
4
+ 'codex': number;
5
+ 'agy-gemini': number;
6
+ 'agy-other': number;
7
+ };
8
+ showTotal: boolean;
9
+ }
10
+ export declare const CONFIG_DIR: string;
11
+ export declare const CONFIG_FILE: string;
12
+ export declare const DEFAULT_CONFIG: Config;
13
+ export declare function loadConfig(): Config;
14
+ export declare function saveConfig(config: Config): void;
15
+ export declare function handleConfigCommand(args: string[]): boolean;
package/dist/config.js ADDED
@@ -0,0 +1,177 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ export const CONFIG_DIR = path.join(os.homedir(), '.config', 'agent-fuel');
5
+ export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
6
+ export const DEFAULT_CONFIG = {
7
+ weights: {
8
+ 'claude-code': 20,
9
+ 'codex': 20,
10
+ 'agy-gemini': 10,
11
+ 'agy-other': 10,
12
+ },
13
+ showTotal: true,
14
+ };
15
+ function ensureDir(dir) {
16
+ try {
17
+ if (!fs.existsSync(dir)) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+ }
21
+ catch {
22
+ // Ignore, let write fail if it must
23
+ }
24
+ }
25
+ export function loadConfig() {
26
+ const config = { ...DEFAULT_CONFIG, weights: { ...DEFAULT_CONFIG.weights } };
27
+ // 1. Read from config file
28
+ try {
29
+ if (fs.existsSync(CONFIG_FILE)) {
30
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
31
+ const parsed = JSON.parse(content);
32
+ if (parsed && typeof parsed === 'object') {
33
+ if (parsed.weights && typeof parsed.weights === 'object') {
34
+ for (const key of ['claude-code', 'codex', 'agy-gemini', 'agy-other']) {
35
+ const w = parsed.weights[key];
36
+ if (typeof w === 'number' && Number.isFinite(w) && w >= 0) {
37
+ config.weights[key] = w;
38
+ }
39
+ }
40
+ }
41
+ if (typeof parsed.showTotal === 'boolean') {
42
+ config.showTotal = parsed.showTotal;
43
+ }
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // Fail silently, use defaults
49
+ }
50
+ // 2. Read from Environment Variables
51
+ const envClaude = process.env.AGENT_FUEL_WEIGHT_CLAUDE_CODE ?? process.env.AGENT_FUEL_WEIGHT_CLAUDE;
52
+ if (envClaude) {
53
+ const val = Number(envClaude);
54
+ if (Number.isFinite(val) && val >= 0)
55
+ config.weights['claude-code'] = val;
56
+ }
57
+ const envCodex = process.env.AGENT_FUEL_WEIGHT_CODEX;
58
+ if (envCodex) {
59
+ const val = Number(envCodex);
60
+ if (Number.isFinite(val) && val >= 0)
61
+ config.weights['codex'] = val;
62
+ }
63
+ const envGemini = process.env.AGENT_FUEL_WEIGHT_AGY_GEMINI ?? process.env.AGENT_FUEL_WEIGHT_GEMINI;
64
+ if (envGemini) {
65
+ const val = Number(envGemini);
66
+ if (Number.isFinite(val) && val >= 0)
67
+ config.weights['agy-gemini'] = val;
68
+ }
69
+ const envOther = process.env.AGENT_FUEL_WEIGHT_AGY_OTHER ?? process.env.AGENT_FUEL_WEIGHT_OTHER;
70
+ if (envOther) {
71
+ const val = Number(envOther);
72
+ if (Number.isFinite(val) && val >= 0)
73
+ config.weights['agy-other'] = val;
74
+ }
75
+ const envShowTotal = process.env.AGENT_FUEL_SHOW_TOTAL;
76
+ if (envShowTotal) {
77
+ if (envShowTotal.toLowerCase() === 'true')
78
+ config.showTotal = true;
79
+ if (envShowTotal.toLowerCase() === 'false')
80
+ config.showTotal = false;
81
+ }
82
+ return config;
83
+ }
84
+ export function saveConfig(config) {
85
+ ensureDir(CONFIG_DIR);
86
+ try {
87
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
88
+ }
89
+ catch (err) {
90
+ const msg = err instanceof Error ? err.message : String(err);
91
+ throw new Error(`Could not write config file ${CONFIG_FILE}: ${msg}`);
92
+ }
93
+ }
94
+ export function handleConfigCommand(args) {
95
+ if (args.length === 0)
96
+ return false;
97
+ const firstArg = args[0].toLowerCase();
98
+ if (firstArg !== 'config')
99
+ return false;
100
+ const BOLD = '\x1b[1m';
101
+ const CYAN = '\x1b[36m';
102
+ const RED = '\x1b[31m';
103
+ const GREEN = '\x1b[32m';
104
+ const R = '\x1b[0m';
105
+ const GRAY = '\x1b[90m';
106
+ const config = loadConfig();
107
+ const subCommand = args[1]?.toLowerCase();
108
+ if (!subCommand || subCommand === 'list') {
109
+ console.log(`\n${BOLD}${CYAN}⚡️ Agent Fuel Configuration${R}`);
110
+ console.log(`${GRAY}Config file: ${CONFIG_FILE}${R}\n`);
111
+ console.log(`${BOLD}Weights:${R}`);
112
+ console.log(` claude-code : ${config.weights['claude-code']}`);
113
+ console.log(` codex : ${config.weights['codex']}`);
114
+ console.log(` agy-gemini : ${config.weights['agy-gemini']}`);
115
+ console.log(` agy-other : ${config.weights['agy-other']}`);
116
+ console.log();
117
+ console.log(`${BOLD}Settings:${R}`);
118
+ console.log(` show-total : ${config.showTotal}`);
119
+ console.log();
120
+ console.log(`${BOLD}Examples:${R}`);
121
+ console.log(` agent-fuel config set claude-code 50`);
122
+ console.log(` agent-fuel config set show-total false`);
123
+ console.log();
124
+ return true;
125
+ }
126
+ if (subCommand === 'set') {
127
+ const key = args[2]?.toLowerCase();
128
+ const rawVal = args[3];
129
+ if (!key || !rawVal) {
130
+ console.error(`\n${BOLD}${RED}Error:${R} Usage: agent-fuel config set <key> <value>`);
131
+ console.error(`Keys: claude, claude-code, codex, gemini, agy-gemini, other, agy-other, show-total\n`);
132
+ process.exit(1);
133
+ }
134
+ if (key === 'show-total') {
135
+ const lowerVal = rawVal.toLowerCase();
136
+ if (lowerVal !== 'true' && lowerVal !== 'false') {
137
+ console.error(`\n${BOLD}${RED}Error:${R} show-total must be true or false\n`);
138
+ process.exit(1);
139
+ }
140
+ config.showTotal = lowerVal === 'true';
141
+ saveConfig(config);
142
+ console.log(`\n${BOLD}${GREEN}✓${R} Set show-total to ${config.showTotal}\n`);
143
+ return true;
144
+ }
145
+ // Handle weights keys
146
+ let targetKey = null;
147
+ if (key === 'claude' || key === 'claude-code') {
148
+ targetKey = 'claude-code';
149
+ }
150
+ else if (key === 'codex') {
151
+ targetKey = 'codex';
152
+ }
153
+ else if (key === 'gemini' || key === 'agy-gemini') {
154
+ targetKey = 'agy-gemini';
155
+ }
156
+ else if (key === 'other' || key === 'agy-other') {
157
+ targetKey = 'agy-other';
158
+ }
159
+ if (!targetKey) {
160
+ console.error(`\n${BOLD}${RED}Error:${R} Unknown key "${key}".`);
161
+ console.error(`Valid keys: claude, claude-code, codex, gemini, agy-gemini, other, agy-other, show-total\n`);
162
+ process.exit(1);
163
+ }
164
+ const val = Number(rawVal);
165
+ if (!Number.isFinite(val) || val < 0) {
166
+ console.error(`\n${BOLD}${RED}Error:${R} Weight must be a positive number or 0.\n`);
167
+ process.exit(1);
168
+ }
169
+ config.weights[targetKey] = val;
170
+ saveConfig(config);
171
+ console.log(`\n${BOLD}${GREEN}✓${R} Set weight.${targetKey} to ${val}\n`);
172
+ return true;
173
+ }
174
+ console.error(`\n${BOLD}${RED}Error:${R} Unknown config sub-command "${subCommand}".`);
175
+ console.error(`Usage: agent-fuel config [list|set]\n`);
176
+ process.exit(1);
177
+ }
package/dist/index.js CHANGED
@@ -3,23 +3,78 @@ import { debugEnabled, debugLogFile } from './debug.js';
3
3
  import { ClaudeQuotaAdapter } from './adapters/claude.js';
4
4
  import { CodexQuotaAdapter } from './adapters/codex.js';
5
5
  import { AgyQuotaAdapter } from './adapters/agy.js';
6
- import { printHeader, printFooter, formatRow, getDisplayName } from './render.js';
6
+ import { printHeader, printFooter, formatRow, getDisplayName, SHADE_CHAR } from './render.js';
7
+ import { loadConfig, handleConfigCommand } from './config.js';
7
8
  // Fixed display order — never changes regardless of which adapter resolves first
8
9
  const SLOT_ORDER = ['claude-code', 'codex', 'agy-gemini', 'agy-other'];
9
10
  const BOLD = '\x1b[1m';
10
11
  const DIM = '\x1b[2m';
11
12
  const R = '\x1b[0m';
12
13
  const GRAY = '\x1b[90m';
14
+ const CYAN = '\x1b[36m';
13
15
  const isTTY = Boolean(process.stdout.isTTY);
14
16
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
15
17
  let spinnerTick = 0;
18
+ const config = loadConfig();
16
19
  function spinnerLine(tool) {
17
20
  const frame = SPINNER[spinnerTick % SPINNER.length];
18
21
  return `${BOLD}${getDisplayName(tool).padEnd(13)}${R} ${DIM}${GRAY}${frame} loading...${R}\x1b[K`;
19
22
  }
23
+ function calculateTotalLine(snapshots) {
24
+ let totalWeight = 0;
25
+ let totalRemainingWeight = 0;
26
+ let hasActive = false;
27
+ let isAnyLoading = false;
28
+ for (const tool of SLOT_ORDER) {
29
+ const snap = snapshots.get(tool);
30
+ if (snap === undefined || snap === null) {
31
+ isAnyLoading = true;
32
+ continue;
33
+ }
34
+ if (snap.remainingPercent !== null) {
35
+ const w = config.weights[tool] ?? 0;
36
+ totalWeight += w;
37
+ totalRemainingWeight += (snap.remainingPercent / 100) * w;
38
+ hasActive = true;
39
+ }
40
+ }
41
+ if (!hasActive) {
42
+ if (isAnyLoading) {
43
+ const frame = SPINNER[spinnerTick % SPINNER.length];
44
+ return `${BOLD}${CYAN}Total${R} [${GRAY}${SHADE_CHAR.repeat(30)}${R}] ${DIM}${GRAY}${frame} loading...${R}\x1b[K`;
45
+ }
46
+ const totalSnap = {
47
+ tool: 'total',
48
+ remainingPercent: null,
49
+ usedPercent: null,
50
+ source: 'unknown',
51
+ isLoading: true
52
+ };
53
+ return formatRow(totalSnap);
54
+ }
55
+ const pct = totalWeight > 0
56
+ ? Math.max(0, Math.min(100, Math.round((totalRemainingWeight / totalWeight) * 100)))
57
+ : null;
58
+ const totalSnap = {
59
+ tool: 'total',
60
+ remainingPercent: pct,
61
+ usedPercent: pct !== null ? 100 - pct : null,
62
+ source: 'local-state',
63
+ isLoading: isAnyLoading
64
+ };
65
+ let formatted = formatRow(totalSnap);
66
+ if (isAnyLoading) {
67
+ const frame = SPINNER[spinnerTick % SPINNER.length];
68
+ formatted += ` ${DIM}${GRAY}${frame} loading...${R}`;
69
+ }
70
+ else {
71
+ formatted += ` ${DIM}${GRAY}(tune weights: agent-fuel config)${R}`;
72
+ }
73
+ return formatted;
74
+ }
20
75
  // In TTY mode: restore cursor to saved position and repaint all slots.
21
76
  // In pipe mode: emit each newly-resolved line exactly once (tracked via emitted set).
22
- function redraw(slots, emitted) {
77
+ function redraw(slots, emitted, snapshots) {
23
78
  if (!isTTY) {
24
79
  for (const tool of SLOT_ORDER) {
25
80
  const line = slots.get(tool);
@@ -28,9 +83,20 @@ function redraw(slots, emitted) {
28
83
  emitted.add(tool);
29
84
  }
30
85
  }
86
+ if (config.showTotal && emitted.size === SLOT_ORDER.length && !emitted.has('total')) {
87
+ const totalLine = calculateTotalLine(snapshots);
88
+ process.stdout.write('\n' + totalLine + '\n');
89
+ emitted.add('total');
90
+ }
31
91
  return;
32
92
  }
33
93
  process.stdout.write('\x1b8'); // DEC restore-cursor — teleports back to saved position
94
+ if (config.showTotal) {
95
+ process.stdout.write('\x1b[2K\r');
96
+ const totalLine = calculateTotalLine(snapshots);
97
+ process.stdout.write(totalLine + '\n');
98
+ process.stdout.write('\x1b[2K\r\n'); // spacer line
99
+ }
34
100
  for (const tool of SLOT_ORDER) {
35
101
  process.stdout.write('\x1b[2K\r');
36
102
  const line = slots.get(tool);
@@ -38,6 +104,10 @@ function redraw(slots, emitted) {
38
104
  }
39
105
  }
40
106
  async function main() {
107
+ const args = process.argv.slice(2);
108
+ if (handleConfigCommand(args)) {
109
+ return;
110
+ }
41
111
  const claudeAdapter = new ClaudeQuotaAdapter();
42
112
  const codexAdapter = new CodexQuotaAdapter();
43
113
  const agyAdapter = new AgyQuotaAdapter();
@@ -46,24 +116,30 @@ async function main() {
46
116
  printHeader();
47
117
  // Save cursor before the placeholder rows so redraw() can teleport back and overwrite them
48
118
  const slots = new Map(SLOT_ORDER.map(t => [t, null]));
119
+ const snapshots = new Map(SLOT_ORDER.map(t => [t, null]));
49
120
  const emitted = new Set(); // pipe-mode: tracks which lines have been printed
50
- if (isTTY)
121
+ if (isTTY) {
51
122
  process.stdout.write('\x1b7'); // DEC save-cursor
123
+ if (config.showTotal) {
124
+ process.stdout.write(calculateTotalLine(snapshots) + '\n\n');
125
+ }
126
+ }
52
127
  for (const tool of SLOT_ORDER) {
53
128
  process.stdout.write(spinnerLine(tool) + '\n');
54
129
  }
55
130
  // Animate spinner at 80ms while any slot is still loading
56
131
  const spinnerTimer = isTTY
57
- ? setInterval(() => { spinnerTick++; redraw(slots, emitted); }, 80)
132
+ ? setInterval(() => { spinnerTick++; redraw(slots, emitted, snapshots); }, 80)
58
133
  : null;
59
134
  // Each adapter fills its slot(s) and triggers a redraw; order is always fixed
60
135
  function fill(snaps) {
61
136
  for (const snap of snaps) {
62
137
  if (SLOT_ORDER.includes(snap.tool)) {
138
+ snapshots.set(snap.tool, snap);
63
139
  slots.set(snap.tool, formatRow(snap));
64
140
  }
65
141
  }
66
- redraw(slots, emitted);
142
+ redraw(slots, emitted, snapshots);
67
143
  }
68
144
  await Promise.allSettled([
69
145
  claudeAdapter.fetchSnapshots().then(fill),
@@ -72,7 +148,7 @@ async function main() {
72
148
  ]);
73
149
  if (spinnerTimer)
74
150
  clearInterval(spinnerTimer);
75
- redraw(slots, emitted); // final clean repaint with all data
151
+ redraw(slots, emitted, snapshots); // final clean repaint with all data
76
152
  printFooter();
77
153
  }
78
154
  main().catch((error) => {
package/dist/render.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { UsageSnapshot } from './adapters/index.js';
2
+ export declare const SHADE_CHAR = "\u2591";
2
3
  export declare function getDisplayName(tool: string): string;
3
4
  export declare function formatRow(snap: UsageSnapshot): string;
4
5
  export declare function printHeader(): void;
package/dist/render.js CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  // ── Constants ──────────────────────────────────────────────────────────────
5
5
  const BLOCK_CHAR = '█';
6
- const SHADE_CHAR = '░';
6
+ export const SHADE_CHAR = '░';
7
7
  const BAR_WIDTH = 30;
8
8
  // ── ANSI colour helpers ────────────────────────────────────────────────────
9
9
  const R = '\x1b[0m';
@@ -21,10 +21,13 @@ export function getDisplayName(tool) {
21
21
  case 'claude-code': return 'Claude Code';
22
22
  case 'agy-gemini': return 'AGY Gemini';
23
23
  case 'agy-other': return 'AGY Other';
24
+ case 'total': return 'Total';
24
25
  default: return tool;
25
26
  }
26
27
  }
27
- function pickColour(remaining) {
28
+ function pickColour(remaining, isLoading) {
29
+ if (isLoading)
30
+ return CYAN;
28
31
  if (remaining < 20)
29
32
  return RED;
30
33
  if (remaining < 50)
@@ -69,7 +72,7 @@ export function formatRow(snap) {
69
72
  percentStr = `${GRAY}unknown${R}`;
70
73
  }
71
74
  else {
72
- const colour = pickColour(remaining);
75
+ const colour = pickColour(remaining, snap.isLoading);
73
76
  const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round((remaining * BAR_WIDTH) / 100)));
74
77
  const empty = BAR_WIDTH - filled;
75
78
  barStr = `${colour}${BLOCK_CHAR.repeat(filled)}${R}${GRAY}${SHADE_CHAR.repeat(empty)}${R}`;
@@ -89,7 +92,9 @@ export function formatRow(snap) {
89
92
  parts.push(`${DIM}${GRAY}[~est]${R}`);
90
93
  }
91
94
  const detailStr = parts.length > 0 ? ` ${parts.join(' ')}` : '';
92
- return `${BOLD}${displayName.padEnd(13)}${R} [${barStr}] ${percentStr}${detailStr}`;
95
+ const isTotal = snap.tool === 'total';
96
+ const labelPrefix = isTotal ? `${BOLD}${CYAN}` : BOLD;
97
+ return `${labelPrefix}${displayName.padEnd(13)}${R} [${barStr}] ${percentStr}${detailStr}`;
93
98
  }
94
99
  // ── Public render functions ────────────────────────────────────────────────
95
100
  export function printHeader() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-fuel",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Sleek term-based dashboard for AI coding CLI quotas",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",