Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Dependencies
node_modules/

# Build output
dist/
out/
*.tsbuildinfo

# Logs
*.log
npm-debug.log*

# OS / editor cruft
.DS_Store
Thumbs.db
.idea/
.vscode/

# Local runtime data / generated assets
*.local
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# LLM Usage Dashboard

> 一次看到 **Codex (OpenAI)****Claude Code (Anthropic)** 的訂閱用量、剩餘額度與重置時間,以及各專案的 token 消耗與預估花費。在本機桌面常駐執行,資料只讀你自己電腦上的檔案、不上傳任何雲端。
> 一次看到 **Codex (OpenAI)****Claude Code (Anthropic)** 與 **Antigravity (Google)** 的訂閱用量、剩餘額度與重置時間,以及各專案的 token 消耗與預估花費。支援 Electron 桌面視窗與 **CMD 終端模式**(`--cli`),資料只讀你自己電腦上的檔案、不上傳任何雲端。

[![npm version](https://img.shields.io/npm/v/llm-usage-dashboard.svg)](https://www.npmjs.com/package/llm-usage-dashboard)
[![node](https://img.shields.io/node/v/llm-usage-dashboard.svg)](https://nodejs.org)
Expand All @@ -13,15 +13,19 @@
只要電腦有 [Node.js](https://nodejs.org)(你在用 Codex CLI / Claude Code,通常都已經有了):

```bash
# 不安裝,直接執行
# 不安裝,直接執行(桌面 GUI)
npx llm-usage-dashboard

# 或裝成全域指令
npm i -g llm-usage-dashboard
llm-usage-dashboard

# 🆕 終端模式(不需 Electron,純 CMD 輸出)
llm-usage-dashboard --cli
```

> 首次執行會自動下載 Electron(約 100MB+),之後啟動就很快。
> GUI 模式首次執行會自動下載 Electron(約 100MB+),之後啟動就很快。
> 終端模式(`--cli`)不需要 Electron,任何有 Node.js 的環境都能直接使用。

📦 npm 套件頁:<https://www.npmjs.com/package/llm-usage-dashboard>

Expand All @@ -34,7 +38,8 @@ llm-usage-dashboard
| **總覽** | 本月預估花費、本月總 tokens、累計花費 |
| **Codex 卡片** | 5 小時 / 每週額度(官方精確 `used_percent` + 重置倒數)、方案、今日 / 本月 tokens、本月花費估算 |
| **Claude Code 卡片** | 5 小時區塊 / 近 7 天用量(依 token 記錄重建)、方案、今日 / 本月 tokens、本月花費估算 |
| **近 14 天圖** | 每日 Codex / Claude tokens 堆疊長條圖 |
| **Antigravity 卡片** | 方案、Prompts / Flows 點數額度、各模型配額進度條與重置倒數 |
| **近 14 天圖** | 每日 Codex / Claude / Antigravity tokens 堆疊長條圖 |
| **各專案用量** | 每個專案(依工作目錄)的來源、累計 / 本月 tokens、本月花費、最後活動時間 |

### 重點功能
Expand All @@ -43,23 +48,42 @@ llm-usage-dashboard
- 🟢 **Codex 額度為官方精確值**:直接讀取 Codex 寫入的 `rate_limits`(5 小時與每週的 `used_percent` + 重置時間)。
- 📈 **各專案分項**:自動依工作目錄歸戶,看出哪個專案吃掉最多額度。
- 💵 **花費估算**:依模型單價把 token 換算成約略美金成本(訂閱制非實際帳單,僅供參考)。
- ▶️ **一鍵啟動**:每張卡片的「▶ 啟動」按鈕(與系統匣 Launch 選單)可直接開啟 Codex / Claude / Antigravity。
- 📴 **Antigravity 離線快取**:IDE 沒開時面板改顯示上次成功讀取的額度(標示「快取」與時間),不再一片空白。
- 🔄 **常駐 + 自動更新**:縮到系統匣常駐,檔案變動即時刷新,另每 5 分鐘自動拉取一次。
- 🚀 **開機自動啟動**:一個勾選即可隨 Windows 開機背景啟動。
- 💻 **CMD 終端模式**:加上 `--cli` 即可在任何終端直接輸出 ANSI 彩色 Dashboard,不需要 Electron。

---

## 🖥 使用方式

### GUI 桌面模式(預設)

1. 執行 `npx llm-usage-dashboard`(或全域安裝後執行 `llm-usage-dashboard`)。
2. 主視窗開啟,立即看到所有用量。
3. **關閉視窗 = 縮到系統匣**(不會結束)。點系統匣圖示可再開啟。
4. 系統匣圖示右鍵選單:
- **Open Dashboard** — 開啟主視窗
- **Refresh now** — 立即重新整理
- **Launch** — 直接啟動 Codex / Claude / Antigravity
- **Start with Windows** — 開機自動啟動開關
- **Open data folders** — 開啟 Codex / Claude 的資料夾
- **Quit** — 結束程式

### 💻 CMD 終端模式

不想裝 Electron?只要加上 `--cli` 就能在 CMD / PowerShell / Terminal 直接看到完整 Dashboard:

```bash
# 任選一種
llm-usage-dashboard --cli
npx llm-usage-dashboard --cli
npm run cli # 在專案目錄內
```

終端模式會輸出 ANSI 彩色的 Dashboard,包含進度條、14 天 bar chart、專案表格等,與 GUI 顯示同樣的資料。

---

## 🔧 需求
Expand Down Expand Up @@ -93,6 +117,9 @@ Anthropic 沒有把官方配額數字寫進本機檔案,所以 Claude 的 5
**Q:執行 `llm-usage-dashboard` 沒反應?**
改用 `npx llm-usage-dashboard`(有時全域 bin 目錄不在系統 PATH 中)。

**Q:不想安裝 Electron,可以用嗎?**
可以!加上 `--cli` 參數即可使用純終端模式:`llm-usage-dashboard --cli`,只需要 Node.js,不會下載 Electron。

---

## 📜 授權與隱私
Expand Down
90 changes: 90 additions & 0 deletions assets/genicon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';
// Generates assets/icon.png (256x256) and assets/tray.png (32x32) without any
// native deps — a small PNG encoder over a hand-drawn RGBA gauge motif.
const zlib = require('zlib');
const fs = require('fs');
const path = require('path');

function crc32(buf) {
let c = ~0;
for (let i = 0; i < buf.length; i++) {
c ^= buf[i];
for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xEDB88320 & -(c & 1));
}
return ~c >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length, 0);
const t = Buffer.from(type, 'ascii');
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0);
return Buffer.concat([len, t, data, crc]);
}
function encodePNG(width, height, rgba) {
const sig = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; ihdr[9] = 6; // 8-bit, RGBA
const raw = Buffer.alloc((width * 4 + 1) * height);
for (let y = 0; y < height; y++) {
raw[y * (width * 4 + 1)] = 0;
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
}
const idat = zlib.deflateSync(raw, { level: 9 });
return Buffer.concat([sig, chunk('IHDR', ihdr), chunk('IDAT', idat), chunk('IEND', Buffer.alloc(0))]);
}

function draw(size) {
const buf = Buffer.alloc(size * size * 4);
const cx = size / 2, cy = size / 2;
const R = size * 0.40, ring = size * 0.11;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const dx = x + 0.5 - cx, dy = y + 0.5 - cy;
const d = Math.sqrt(dx * dx + dy * dy);
let r = 0, g = 0, b = 0, a = 0;
// rounded dark background
const bgR = size * 0.46;
if (d <= bgR) { r = 24; g = 27; b = 38; a = 255; }
// gauge ring: angle from -210deg..30deg, two-tone (codex green / claude orange)
if (d <= R + ring / 2 && d >= R - ring / 2) {
let ang = Math.atan2(dy, dx) * 180 / Math.PI; // -180..180
let t = (ang + 210) % 360; // 0 at start
if (t >= 0 && t <= 240) {
if (t < 120) { r = 52; g = 211; b = 153; } // teal/green
else { r = 251; g = 146; b = 60; } // orange
a = 255;
}
}
// center dot
if (d <= size * 0.12) { r = 96; g = 165; b = 250; a = 255; }
buf[i] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a;
}
}
return buf;
}

// Wrap a 256x256 PNG inside an .ico container (valid for modern Windows).
function encodeICO(png) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserved
header.writeUInt16LE(1, 2); // type = icon
header.writeUInt16LE(1, 4); // image count
const entry = Buffer.alloc(16);
entry[0] = 0; // width 0 => 256
entry[1] = 0; // height 0 => 256
entry[2] = 0; // palette
entry[3] = 0; // reserved
entry.writeUInt16LE(1, 4); // color planes
entry.writeUInt16LE(32, 6); // bits per pixel
entry.writeUInt32LE(png.length, 8); // size of PNG data
entry.writeUInt32LE(6 + 16, 12); // offset to PNG data
return Buffer.concat([header, entry, png]);
}

const outDir = __dirname;
const png256 = encodePNG(256, 256, draw(256));
fs.writeFileSync(path.join(outDir, 'icon.png'), png256);
fs.writeFileSync(path.join(outDir, 'tray.png'), encodePNG(32, 32, draw(32)));
fs.writeFileSync(path.join(outDir, 'icon.ico'), encodeICO(png256));
console.log('icons written (png + tray + ico)');
Binary file added assets/icon.ico
Binary file not shown.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions assets/pricing.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"_comment": "把這個檔複製成 pricing.json 即可覆寫預設模型單價。單位:USD / 1,000,000 tokens。key 以模型 id 子字串比對,最長相符者優先。只需列出想覆寫的項目。",
"gpt-5": { "input": 1.25, "output": 10, "cacheWrite": 1.25, "cacheRead": 0.125 },
"codex": { "input": 1.25, "output": 10, "cacheWrite": 1.25, "cacheRead": 0.125 },
"claude-opus": { "input": 15, "output": 75, "cacheWrite": 18.75, "cacheRead": 1.5 },
"claude-sonnet": { "input": 3, "output": 15, "cacheWrite": 3.75, "cacheRead": 0.3 }
}
Binary file added assets/tray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node
'use strict';
// Launcher so the app can be run via `npx ai-usage-dashboard` or, after
// `npm i -g ai-usage-dashboard`, simply `ai-usage-dashboard`.
//
// Modes:
// --cli Terminal dashboard (no Electron required)
// (none) Full Electron GUI (default)

const args = process.argv.slice(2);

// ── CLI mode: pure Node.js terminal dashboard ──
if (args.includes('--cli')) {
const { main } = require('../src/main/cli-render');
main();
} else {
// ── GUI mode: launch Electron ──
const { spawn } = require('child_process');
const path = require('path');

let electronPath;
try {
electronPath = require('electron');
} catch (e) {
console.error('找不到 Electron。請先安裝:npm i -g ai-usage-dashboard(或在專案內 npm install)。');
console.error('提示:若只需終端模式,請加上 --cli 參數:llm-usage-dashboard --cli');
process.exit(1);
}

const appDir = path.join(__dirname, '..');
const child = spawn(electronPath, [appDir, ...args], {
stdio: 'inherit',
windowsHide: false,
});
child.on('close', (code) => process.exit(code == null ? 0 : code));
child.on('error', (err) => { console.error('啟動失敗:', err.message); process.exit(1); });
}
Loading