@juliantanx/aiusage-widget 1.3.1

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,79 @@
1
+ # @juliantanx/aiusage-widget
2
+
3
+ A lightweight system tray widget for [aiusage](https://github.com/juliantanx/aiusage) that shows your AI coding assistant token usage at a glance.
4
+
5
+ English | [中文](./README_zh.md)
6
+
7
+ ## Features
8
+
9
+ - **System tray integration** — lives in your system tray, click to toggle a compact stats panel.
10
+ - **Today's token usage** — total tokens with input/output breakdown.
11
+ - **Monthly totals** — rolling token count for the current month.
12
+ - **Top model** — your most-used model today and its share.
13
+ - **Dashboard launcher** — one click to open the full aiusage web dashboard.
14
+ - **Auto-refresh** — updates every 60 seconds automatically.
15
+ - **Cross-platform** — Windows, macOS, and Linux.
16
+
17
+ ## Prerequisites
18
+
19
+ - [aiusage](https://github.com/juliantanx/aiusage) CLI installed and data parsed (`aiusage parse`)
20
+ - Node.js >= 18
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g @juliantanx/aiusage-widget
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```bash
31
+ # Start the widget (runs in background, adds a tray icon)
32
+ aiusage-widget
33
+ ```
34
+
35
+ The widget reads from `~/.aiusage/cache.db`. Make sure you have run `aiusage parse` at least once so the database exists.
36
+
37
+ ### Running with PM2
38
+
39
+ To keep the widget running after closing the terminal and auto-start on boot:
40
+
41
+ ```bash
42
+ npm install -g pm2 pm2-startup
43
+
44
+ pm2 start aiusage-widget -- --foreground
45
+ pm2 save
46
+ pm2-startup install
47
+ ```
48
+
49
+ PM2 works on Windows, macOS, and Linux.
50
+
51
+ **Tray interactions:**
52
+
53
+ - **Left-click** — toggle the stats panel.
54
+ - **Right-click** — context menu with Show Panel, Refresh, and Quit.
55
+
56
+ **Panel actions:**
57
+
58
+ - Click **Open Full Dashboard** to launch `aiusage serve` and open the web dashboard in your browser.
59
+
60
+ ## Build from Source
61
+
62
+ ```bash
63
+ git clone https://github.com/juliantanx/aiusage.git
64
+ cd aiusage
65
+ pnpm install
66
+ pnpm build
67
+ cd packages/widget
68
+ pnpm dev
69
+ ```
70
+
71
+ ## Tech Stack
72
+
73
+ - **Runtime:** Electron
74
+ - **UI:** Svelte + Vite
75
+ - **Database:** better-sqlite3 (reads aiusage's local SQLite database)
76
+
77
+ ## License
78
+
79
+ MIT
package/README_zh.md ADDED
@@ -0,0 +1,79 @@
1
+ # @juliantanx/aiusage-widget
2
+
3
+ 一个轻量级的系统托盘小组件,用于 [aiusage](https://github.com/juliantanx/aiusage),可以快速查看 AI 编程助手的 token 用量。
4
+
5
+ [English](./README.md) | 中文
6
+
7
+ ## 功能
8
+
9
+ - **系统托盘集成** — 常驻系统托盘,点击即可弹出简洁的统计面板。
10
+ - **今日 token 用量** — 总 token 数及输入/输出明细。
11
+ - **月度总量** — 当月累计 token 数。
12
+ - **热门模型** — 今日使用最多的模型及其占比。
13
+ - **仪表盘启动** — 一键打开完整的 aiusage Web 仪表盘。
14
+ - **自动刷新** — 每 60 秒自动更新数据。
15
+ - **跨平台** — 支持 Windows、macOS 和 Linux。
16
+
17
+ ## 前置条件
18
+
19
+ - 已安装 [aiusage](https://github.com/juliantanx/aiusage) CLI 并完成数据解析(`aiusage parse`)
20
+ - Node.js >= 18
21
+
22
+ ## 安装
23
+
24
+ ```bash
25
+ npm install -g @juliantanx/aiusage-widget
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ```bash
31
+ # 启动组件(后台运行,添加系统托盘图标)
32
+ aiusage-widget
33
+ ```
34
+
35
+ 组件会读取 `~/.aiusage/cache.db`。请确保至少执行过一次 `aiusage parse`,使数据库文件存在。
36
+
37
+ ### 使用 PM2 后台运行
38
+
39
+ 如需关闭终端后保持组件运行,并实现开机自启:
40
+
41
+ ```bash
42
+ npm install -g pm2 pm2-startup
43
+
44
+ pm2 start aiusage-widget -- --foreground
45
+ pm2 save
46
+ pm2-startup install
47
+ ```
48
+
49
+ PM2 支持 Windows、macOS 和 Linux。
50
+
51
+ **托盘操作:**
52
+
53
+ - **左键点击** — 切换统计面板的显示/隐藏。
54
+ - **右键点击** — 弹出上下文菜单,包含显示面板、刷新和退出。
55
+
56
+ **面板操作:**
57
+
58
+ - 点击 **Open Full Dashboard** 可启动 `aiusage serve` 并在浏览器中打开 Web 仪表盘。
59
+
60
+ ## 从源码构建
61
+
62
+ ```bash
63
+ git clone https://github.com/juliantanx/aiusage.git
64
+ cd aiusage
65
+ pnpm install
66
+ pnpm build
67
+ cd packages/widget
68
+ pnpm dev
69
+ ```
70
+
71
+ ## 技术栈
72
+
73
+ - **运行时:** Electron
74
+ - **UI:** Svelte + Vite
75
+ - **数据库:** better-sqlite3(读取 aiusage 的本地 SQLite 数据库)
76
+
77
+ ## 许可证
78
+
79
+ MIT
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require('child_process')
3
+ const { writeFileSync, mkdirSync } from 'fs'
4
+ const { homedir } = require('os')
5
+ const electron = require('electron')
6
+ const path = require('path')
7
+
8
+ const foreground = process.argv.includes('--foreground')
9
+ const aiusageDir = path.join(homedir(), '.aiusage')
10
+ const pidPath = path.join(aiusageDir, 'widget.pid')
11
+
12
+ if (foreground) {
13
+ // PM2 / service mode: run in foreground so the process manager can monitor it
14
+ const child = spawn(
15
+ String(electron),
16
+ [path.join(__dirname, '..', 'dist', 'main.js')],
17
+ { stdio: 'inherit' }
18
+ )
19
+ child.on('exit', (code) => process.exit(code ?? 0))
20
+ process.on('SIGINT', () => child.kill('SIGINT'))
21
+ process.on('SIGTERM', () => child.kill('SIGTERM'))
22
+ } else {
23
+ // Normal mode: detach so closing the terminal doesn't kill the widget
24
+ const child = spawn(
25
+ String(electron),
26
+ [path.join(__dirname, '..', 'dist', 'main.js')],
27
+ { detached: true, stdio: 'ignore' }
28
+ )
29
+
30
+ if (child.pid) {
31
+ try {
32
+ mkdirSync(aiusageDir, { recursive: true })
33
+ writeFileSync(pidPath, String(child.pid), { encoding: 'utf-8' })
34
+ } catch {
35
+ // Non-fatal: widget still starts, deduplication just won't work
36
+ }
37
+ }
38
+
39
+ child.unref()
40
+ }
package/dist/data.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queryWidgetData = queryWidgetData;
4
+ function queryWidgetData(db) {
5
+ const todayStart = getTodayStartMs();
6
+ const monthStart = getMonthStartMs();
7
+ const tomorrow = todayStart + 86400000;
8
+ const nextMonthStart = getNextMonthStartMs();
9
+ const todayRow = db.prepare(`
10
+ SELECT
11
+ COALESCE(SUM(input_tokens), 0) AS input,
12
+ COALESCE(SUM(output_tokens), 0) AS output,
13
+ COALESCE(SUM(cache_read_tokens + cache_write_tokens + thinking_tokens), 0) AS other
14
+ FROM records
15
+ WHERE ts >= ? AND ts < ?
16
+ `).get(todayStart, tomorrow);
17
+ const monthRow = db.prepare(`
18
+ SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens + thinking_tokens), 0) AS total
19
+ FROM records
20
+ WHERE ts >= ? AND ts < ?
21
+ `).get(monthStart, nextMonthStart);
22
+ const modelRows = db.prepare(`
23
+ SELECT
24
+ model,
25
+ SUM(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens + thinking_tokens) AS tokens
26
+ FROM records
27
+ WHERE ts >= ? AND ts < ?
28
+ GROUP BY model
29
+ ORDER BY tokens DESC
30
+ `).all(todayStart, tomorrow);
31
+ let topModel = null;
32
+ if (modelRows.length > 0) {
33
+ const totalTokens = modelRows.reduce((acc, r) => acc + r.tokens, 0);
34
+ const top = modelRows[0];
35
+ topModel = {
36
+ name: top.model,
37
+ share: totalTokens > 0 ? Math.round((top.tokens / totalTokens) * 100) : 0,
38
+ };
39
+ }
40
+ return {
41
+ todayTokens: {
42
+ total: todayRow.input + todayRow.output + todayRow.other,
43
+ input: todayRow.input,
44
+ output: todayRow.output,
45
+ },
46
+ monthTokens: {
47
+ total: monthRow.total,
48
+ },
49
+ topModel,
50
+ lastUpdated: Date.now(),
51
+ };
52
+ }
53
+ function getTodayStartMs() {
54
+ const now = new Date();
55
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
56
+ }
57
+ function getMonthStartMs() {
58
+ const now = new Date();
59
+ return new Date(now.getFullYear(), now.getMonth(), 1).getTime();
60
+ }
61
+ function getNextMonthStartMs() {
62
+ const now = new Date();
63
+ return new Date(now.getFullYear(), now.getMonth() + 1, 1).getTime();
64
+ }
package/dist/main.js ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
4
+ const node_path_1 = require("node:path");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_os_1 = require("node:os");
7
+ const node_module_1 = require("node:module");
8
+ const data_1 = require("./data");
9
+ const ui_1 = require("./ui");
10
+ const nodeRequire = (0, node_module_1.createRequire)(__filename);
11
+ const Database = nodeRequire('better-sqlite3');
12
+ const DB_PATH = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage', 'cache.db');
13
+ const PORT_FILE = (0, node_path_1.join)((0, node_os_1.homedir)(), '.aiusage', '.serve-port');
14
+ const DASHBOARD_PORT = 3847;
15
+ const REFRESH_INTERVAL_MS = 60000;
16
+ let tray = null;
17
+ let win = null;
18
+ let db = null;
19
+ electron_1.app.setName('aiusage Widget');
20
+ // Prevent dock icon on macOS
21
+ if (process.platform === 'darwin' && electron_1.app.dock) {
22
+ electron_1.app.dock.hide();
23
+ }
24
+ electron_1.app.whenReady().then(() => {
25
+ if ((0, node_fs_1.existsSync)(DB_PATH)) {
26
+ db = new Database(DB_PATH, { readonly: true });
27
+ }
28
+ createTray();
29
+ createWindow();
30
+ startAutoRefresh();
31
+ if ((0, ui_1.shouldShowWindowOnLaunch)(electron_1.app.isPackaged)) {
32
+ showWindow();
33
+ }
34
+ });
35
+ electron_1.app.on('window-all-closed', () => {
36
+ // Keep the app running in the tray — do not quit
37
+ });
38
+ electron_1.app.on('before-quit', () => {
39
+ db?.close();
40
+ });
41
+ function createTray() {
42
+ const icon = electron_1.nativeImage.createFromDataURL((0, ui_1.getTrayIconDataUrl)());
43
+ tray = new electron_1.Tray(icon);
44
+ tray.setToolTip('aiusage Widget');
45
+ // On macOS, SVG icons silently fail — use a text label as the visible entry
46
+ if (process.platform === 'darwin') {
47
+ tray.setTitle('⚡');
48
+ }
49
+ tray.on('click', () => toggleWindow());
50
+ tray.on('right-click', () => {
51
+ const menu = electron_1.Menu.buildFromTemplate([
52
+ { label: 'Show Panel', click: () => showWindow() },
53
+ { label: 'Refresh', click: () => pushDataUpdate() },
54
+ { type: 'separator' },
55
+ { label: 'Quit', click: () => { electron_1.app.exit(0); } },
56
+ ]);
57
+ tray.popUpContextMenu(menu);
58
+ });
59
+ }
60
+ function createWindow() {
61
+ win = new electron_1.BrowserWindow({
62
+ width: 320,
63
+ height: 300,
64
+ show: false,
65
+ frame: false,
66
+ resizable: false,
67
+ skipTaskbar: true,
68
+ alwaysOnTop: true,
69
+ transparent: true,
70
+ webPreferences: {
71
+ preload: (0, node_path_1.join)(__dirname, 'preload.js'),
72
+ contextIsolation: true,
73
+ nodeIntegration: false,
74
+ },
75
+ });
76
+ const rendererPath = (0, node_path_1.join)(__dirname, 'renderer', 'index.html');
77
+ win.loadFile(rendererPath);
78
+ if ((0, ui_1.shouldHideWindowOnBlur)(electron_1.app.isPackaged)) {
79
+ win.on('blur', () => win?.hide());
80
+ }
81
+ }
82
+ function showWindow() {
83
+ if (!win)
84
+ return;
85
+ // Position near tray icon
86
+ const trayBounds = tray.getBounds();
87
+ const winBounds = win.getBounds();
88
+ const x = Math.round(trayBounds.x + trayBounds.width / 2 - winBounds.width / 2);
89
+ const y = process.platform === 'darwin'
90
+ ? trayBounds.y + trayBounds.height + 4
91
+ : trayBounds.y - winBounds.height - 4;
92
+ win.setPosition(x, y, false);
93
+ win.show();
94
+ win.focus();
95
+ pushDataUpdate();
96
+ }
97
+ function toggleWindow() {
98
+ if (win?.isVisible()) {
99
+ win.hide();
100
+ }
101
+ else {
102
+ showWindow();
103
+ }
104
+ }
105
+ function pushDataUpdate() {
106
+ if (!win || !db)
107
+ return;
108
+ try {
109
+ const data = (0, data_1.queryWidgetData)(db);
110
+ win.webContents.send('widget:data-update', data);
111
+ }
112
+ catch {
113
+ // DB may not be initialized yet; silently skip
114
+ }
115
+ }
116
+ function startAutoRefresh() {
117
+ setInterval(() => pushDataUpdate(), REFRESH_INTERVAL_MS);
118
+ }
119
+ // IPC handlers
120
+ electron_1.ipcMain.handle('widget:get-data', () => {
121
+ if (!db)
122
+ return null;
123
+ return (0, data_1.queryWidgetData)(db);
124
+ });
125
+ electron_1.ipcMain.handle('widget:open-dashboard', async () => {
126
+ const port = getDashboardPort();
127
+ const reachable = await isDashboardReachable(port);
128
+ if (!reachable) {
129
+ const result = await launchDashboard();
130
+ if (!result.success) {
131
+ electron_1.dialog.showErrorBox('aiusage Not Found', 'The aiusage CLI is not installed.\n\nInstall it with:\n npm install -g @juliantanx/aiusage\n\nThen try again.');
132
+ return;
133
+ }
134
+ }
135
+ electron_1.shell.openExternal(`http://localhost:${getDashboardPort()}`);
136
+ });
137
+ electron_1.ipcMain.on('widget:hide-window', () => {
138
+ win?.hide();
139
+ });
140
+ function getDashboardPort() {
141
+ try {
142
+ if ((0, node_fs_1.existsSync)(PORT_FILE)) {
143
+ const port = parseInt((0, node_fs_1.readFileSync)(PORT_FILE, 'utf-8').trim(), 10);
144
+ if (!isNaN(port) && port > 0)
145
+ return port;
146
+ }
147
+ }
148
+ catch { }
149
+ return DASHBOARD_PORT;
150
+ }
151
+ async function isDashboardReachable(port) {
152
+ return new Promise((resolve) => {
153
+ const http = nodeRequire('http');
154
+ const req = http.get(`http://localhost:${port}`, (res) => {
155
+ res.destroy();
156
+ resolve(res.statusCode !== undefined && res.statusCode < 500);
157
+ });
158
+ req.on('error', () => resolve(false));
159
+ req.setTimeout(1000, () => { req.destroy(); resolve(false); });
160
+ });
161
+ }
162
+ async function launchDashboard() {
163
+ const { spawn } = nodeRequire('child_process');
164
+ return new Promise((resolve) => {
165
+ const child = spawn('aiusage', ['serve'], {
166
+ detached: true,
167
+ stdio: 'ignore',
168
+ shell: true,
169
+ });
170
+ let spawnError = null;
171
+ child.on('error', (err) => {
172
+ if (err.code === 'ENOENT') {
173
+ spawnError = 'aiusage command not found';
174
+ }
175
+ else {
176
+ spawnError = err.message;
177
+ }
178
+ });
179
+ child.unref();
180
+ // Wait up to 5 seconds for the server to be ready
181
+ let attempts = 0;
182
+ const check = async () => {
183
+ if (spawnError) {
184
+ resolve({ success: false, error: spawnError });
185
+ return;
186
+ }
187
+ if (await isDashboardReachable(getDashboardPort())) {
188
+ resolve({ success: true });
189
+ return;
190
+ }
191
+ attempts++;
192
+ if (attempts >= 10) {
193
+ resolve({ success: false, error: 'Server failed to start within 5 seconds' });
194
+ return;
195
+ }
196
+ setTimeout(check, 500);
197
+ };
198
+ check();
199
+ });
200
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const electron_1 = require("electron");
4
+ electron_1.contextBridge.exposeInMainWorld('widget', {
5
+ getData: () => electron_1.ipcRenderer.invoke('widget:get-data'),
6
+ openDashboard: () => electron_1.ipcRenderer.invoke('widget:open-dashboard'),
7
+ hideWindow: () => electron_1.ipcRenderer.send('widget:hide-window'),
8
+ onDataUpdate: (callback) => {
9
+ electron_1.ipcRenderer.removeAllListeners('widget:data-update');
10
+ electron_1.ipcRenderer.on('widget:data-update', (_event, data) => callback(data));
11
+ },
12
+ });
@@ -0,0 +1 @@
1
+ .header.svelte-14gtt2i{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid var(--border);-webkit-app-region:drag}.logo.svelte-14gtt2i{font-size:13px;font-weight:600;color:var(--text-primary);letter-spacing:.01em}.actions.svelte-14gtt2i{display:flex;gap:4px;-webkit-app-region:no-drag}.icon-btn.svelte-14gtt2i{width:24px;height:24px;border:none;border-radius:4px;background:transparent;color:var(--text-muted);cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:background .1s,color .1s;padding:0}.icon-btn.svelte-14gtt2i:hover{background:var(--bg-hover);color:var(--text-primary)}.section.svelte-savwyt{padding:12px 14px;border-bottom:1px solid var(--border)}.section-label.svelte-savwyt{font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--text-muted);text-transform:uppercase;margin-bottom:5px}.values.svelte-savwyt{display:flex;align-items:baseline;gap:8px}.primary.svelte-savwyt{font-size:16px;font-weight:600;color:var(--text-primary);font-variant-numeric:tabular-nums}.secondary.svelte-savwyt{font-size:11px;color:var(--text-muted);font-variant-numeric:tabular-nums}*{box-sizing:border-box;margin:0;padding:0}body{background:transparent;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;-webkit-font-smoothing:antialiased}:root{--bg:#1a1a1f;--bg-hover:rgba(255, 255, 255, .06);--border:rgba(255, 255, 255, .08);--text-primary:#f0f0f2;--text-muted:rgba(240, 240, 242, .45);--accent:#6c8eff}@media (prefers-color-scheme: light){:root{--bg:#ffffff;--bg-hover:rgba(0, 0, 0, .05);--border:rgba(0, 0, 0, .08);--text-primary:#0f0f12;--text-muted:rgba(15, 15, 18, .45);--accent:#3b5bdb}}.panel.svelte-z2p4qk{background:var(--bg);border-radius:12px;border:1px solid var(--border);overflow:hidden;width:320px;box-shadow:0 8px 32px #0000004d,0 2px 8px #0003;transition:opacity .15s}.panel.loading.svelte-z2p4qk{opacity:.7}.footer.svelte-z2p4qk{padding:10px 14px 14px}.open-btn.svelte-z2p4qk{width:100%;padding:9px 14px;border:1px solid var(--accent);border-radius:7px;background:transparent;color:var(--accent);font-size:12px;font-weight:600;cursor:pointer;transition:background .15s;letter-spacing:.01em}.open-btn.svelte-z2p4qk:hover{background:#6c8eff1a}
@@ -0,0 +1 @@
1
+ var se=Object.defineProperty;var ie=(e,t,n)=>t in e?se(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var x=(e,t,n)=>ie(e,typeof t!="symbol"?t+"":t,n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))o(r);new MutationObserver(r=>{for(const s of r)if(s.type==="childList")for(const u of s.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&o(u)}).observe(document,{childList:!0,subtree:!0});function n(r){const s={};return r.integrity&&(s.integrity=r.integrity),r.referrerPolicy&&(s.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?s.credentials="include":r.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function o(r){if(r.ep)return;r.ep=!0;const s=n(r);fetch(r.href,s)}})();function w(){}function re(e){return e()}function X(){return Object.create(null)}function M(e){e.forEach(re)}function F(e){return typeof e=="function"}function Y(e,t){return e!=e?t==t:e!==t||e&&typeof e=="object"||typeof e=="function"}function le(e){return Object.keys(e).length===0}function d(e,t){e.appendChild(t)}function j(e,t,n){e.insertBefore(t,n||null)}function N(e){e.parentNode&&e.parentNode.removeChild(e)}function h(e){return document.createElement(e)}function I(e){return document.createTextNode(e)}function _(){return I(" ")}function K(e,t,n,o){return e.addEventListener(t,n,o),()=>e.removeEventListener(t,n,o)}function p(e,t,n){n==null?e.removeAttribute(t):e.getAttribute(t)!==n&&e.setAttribute(t,n)}function ue(e){return Array.from(e.childNodes)}function U(e,t){t=""+t,e.data!==t&&(e.data=t)}function Z(e,t,n){e.classList.toggle(t,!!n)}let T;function L(e){T=e}function ae(){if(!T)throw new Error("Function called outside component initialization");return T}function fe(e){ae().$$.on_mount.push(e)}const v=[],ee=[];let S=[];const te=[],ce=Promise.resolve();let V=!1;function de(){V||(V=!0,ce.then(oe))}function W(e){S.push(e)}const B=new Set;let $=0;function oe(){if($!==0)return;const e=T;do{try{for(;$<v.length;){const t=v[$];$++,L(t),me(t.$$)}}catch(t){throw v.length=0,$=0,t}for(L(null),v.length=0,$=0;ee.length;)ee.pop()();for(let t=0;t<S.length;t+=1){const n=S[t];B.has(n)||(B.add(n),n())}S.length=0}while(v.length);for(;te.length;)te.pop()();V=!1,B.clear(),L(e)}function me(e){if(e.fragment!==null){e.update(),M(e.before_update);const t=e.dirty;e.dirty=[-1],e.fragment&&e.fragment.p(e.ctx,t),e.after_update.forEach(W)}}function pe(e){const t=[],n=[];S.forEach(o=>e.indexOf(o)===-1?t.push(o):n.push(o)),n.forEach(o=>o()),S=t}const R=new Set;let he;function O(e,t){e&&e.i&&(R.delete(e),e.i(t))}function A(e,t,n,o){if(e&&e.o){if(R.has(e))return;R.add(e),he.c.push(()=>{R.delete(e)}),e.o(t)}}function D(e){e&&e.c()}function C(e,t,n){const{fragment:o,after_update:r}=e.$$;o&&o.m(t,n),W(()=>{const s=e.$$.on_mount.map(re).filter(F);e.$$.on_destroy?e.$$.on_destroy.push(...s):M(s),e.$$.on_mount=[]}),r.forEach(W)}function E(e,t){const n=e.$$;n.fragment!==null&&(pe(n.after_update),M(n.on_destroy),n.fragment&&n.fragment.d(t),n.on_destroy=n.fragment=null,n.ctx=[])}function ge(e,t){e.$$.dirty[0]===-1&&(v.push(e),de(),e.$$.dirty.fill(0)),e.$$.dirty[t/31|0]|=1<<t%31}function G(e,t,n,o,r,s,u=null,m=[-1]){const l=T;L(e);const i=e.$$={fragment:null,ctx:[],props:s,update:w,not_equal:r,bound:X(),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(t.context||(l?l.$$.context:[])),callbacks:X(),dirty:m,skip_bound:!1,root:t.target||l.$$.root};u&&u(i.root);let f=!1;if(i.ctx=n?n(e,t.props||{},(a,g,...b)=>{const k=b.length?b[0]:g;return i.ctx&&r(i.ctx[a],i.ctx[a]=k)&&(!i.skip_bound&&i.bound[a]&&i.bound[a](k),f&&ge(e,a)),g}):[],i.update(),f=!0,M(i.before_update),i.fragment=o?o(i.ctx):!1,t.target){if(t.hydrate){const a=ue(t.target);i.fragment&&i.fragment.l(a),a.forEach(N)}else i.fragment&&i.fragment.c();t.intro&&O(e.$$.fragment),C(e,t.target,t.anchor),oe()}L(l)}class J{constructor(){x(this,"$$");x(this,"$$set")}$destroy(){E(this,1),this.$destroy=w}$on(t,n){if(!F(n))return w;const o=this.$$.callbacks[t]||(this.$$.callbacks[t]=[]);return o.push(n),()=>{const r=o.indexOf(n);r!==-1&&o.splice(r,1)}}$set(t){this.$$set&&!le(t)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const ye="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(ye);function _e(e){let t,n,o,r,s,u,m,l,i;return{c(){t=h("div"),n=h("span"),n.textContent="⚡ aiusage",o=_(),r=h("div"),s=h("button"),s.textContent="↻",u=_(),m=h("button"),m.textContent="✕",p(n,"class","logo svelte-14gtt2i"),p(s,"class","icon-btn svelte-14gtt2i"),p(s,"title","Refresh"),p(m,"class","icon-btn svelte-14gtt2i"),p(m,"title","Close"),p(r,"class","actions svelte-14gtt2i"),p(t,"class","header svelte-14gtt2i")},m(f,a){j(f,t,a),d(t,n),d(t,o),d(t,r),d(r,s),d(r,u),d(r,m),l||(i=[K(s,"click",function(){F(e[0])&&e[0].apply(this,arguments)}),K(m,"click",function(){F(e[1])&&e[1].apply(this,arguments)})],l=!0)},p(f,[a]){e=f},i:w,o:w,d(f){f&&N(t),l=!1,M(i)}}}function we(e,t,n){let{onRefresh:o}=t,{onClose:r}=t;return e.$$set=s=>{"onRefresh"in s&&n(0,o=s.onRefresh),"onClose"in s&&n(1,r=s.onClose)},[o,r]}class be extends J{constructor(t){super(),G(this,t,we,_e,Y,{onRefresh:0,onClose:1})}}function ne(e){let t,n;return{c(){t=h("span"),n=I(e[2]),p(t,"class","secondary svelte-savwyt")},m(o,r){j(o,t,r),d(t,n)},p(o,r){r&4&&U(n,o[2])},d(o){o&&N(t)}}}function $e(e){let t,n,o,r,s,u,m,l,i=e[2]&&ne(e);return{c(){t=h("div"),n=h("div"),o=I(e[0]),r=_(),s=h("div"),u=h("span"),m=I(e[1]),l=_(),i&&i.c(),p(n,"class","section-label svelte-savwyt"),p(u,"class","primary svelte-savwyt"),p(s,"class","values svelte-savwyt"),p(t,"class","section svelte-savwyt")},m(f,a){j(f,t,a),d(t,n),d(n,o),d(t,r),d(t,s),d(s,u),d(u,m),d(s,l),i&&i.m(s,null)},p(f,[a]){a&1&&U(o,f[0]),a&2&&U(m,f[1]),f[2]?i?i.p(f,a):(i=ne(f),i.c(),i.m(s,null)):i&&(i.d(1),i=null)},i:w,o:w,d(f){f&&N(t),i&&i.d()}}}function ve(e,t,n){let{label:o}=t,{primary:r}=t,{secondary:s=""}=t;return e.$$set=u=>{"label"in u&&n(0,o=u.label),"primary"in u&&n(1,r=u.primary),"secondary"in u&&n(2,s=u.secondary)},[o,r,s]}class H extends J{constructor(t){super(),G(this,t,ve,$e,Y,{label:0,primary:1,secondary:2})}}function Se(e){let t,n,o,r,s,u,m,l,i,f,a,g,b,k;return n=new be({props:{onRefresh:e[6],onClose:ke}}),r=new H({props:{label:"TODAY",primary:e[5],secondary:e[4]}}),u=new H({props:{label:"THIS MONTH",primary:e[3]}}),l=new H({props:{label:"TOP MODEL",primary:e[2],secondary:e[1]}}),{c(){t=h("div"),D(n.$$.fragment),o=_(),D(r.$$.fragment),s=_(),D(u.$$.fragment),m=_(),D(l.$$.fragment),i=_(),f=h("div"),a=h("button"),a.textContent="Open Full Dashboard →",p(a,"class","open-btn svelte-z2p4qk"),p(f,"class","footer svelte-z2p4qk"),p(t,"class","panel svelte-z2p4qk"),Z(t,"loading",e[0])},m(c,y){j(c,t,y),C(n,t,null),d(t,o),C(r,t,null),d(t,s),C(u,t,null),d(t,m),C(l,t,null),d(t,i),d(t,f),d(f,a),g=!0,b||(k=K(a,"click",Oe),b=!0)},p(c,[y]){const q={};y&32&&(q.primary=c[5]),y&16&&(q.secondary=c[4]),r.$set(q);const Q={};y&8&&(Q.primary=c[3]),u.$set(Q);const z={};y&4&&(z.primary=c[2]),y&2&&(z.secondary=c[1]),l.$set(z),(!g||y&1)&&Z(t,"loading",c[0])},i(c){g||(O(n.$$.fragment,c),O(r.$$.fragment,c),O(u.$$.fragment,c),O(l.$$.fragment,c),g=!0)},o(c){A(n.$$.fragment,c),A(r.$$.fragment,c),A(u.$$.fragment,c),A(l.$$.fragment,c),g=!1},d(c){c&&N(t),E(n),E(r),E(u),E(l),b=!1,k()}}}function P(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}K`:String(e)}function ke(){window.widget.hideWindow()}async function Oe(){await window.widget.openDashboard()}function Ce(e,t,n){let o,r,s,u,m,l=null,i=!0;async function f(){n(0,i=!0),n(7,l=await window.widget.getData()),n(0,i=!1)}return fe(()=>{f(),window.widget.onDataUpdate(a=>{n(7,l=a),n(0,i=!1)})}),e.$$.update=()=>{var a;e.$$.dirty&128&&n(5,o=l?P(l.todayTokens.total):"—"),e.$$.dirty&128&&n(4,r=l?`↑${P(l.todayTokens.input)} ↓${P(l.todayTokens.output)}`:""),e.$$.dirty&128&&n(3,s=l?P(l.monthTokens.total):"—"),e.$$.dirty&128&&n(2,u=((a=l==null?void 0:l.topModel)==null?void 0:a.name)??"—"),e.$$.dirty&128&&n(1,m=l!=null&&l.topModel?`${l.topModel.share}%`:"")},[i,m,u,s,r,o,f,l]}class Ee extends J{constructor(t){super(),G(this,t,Ce,Se,Y,{})}}new Ee({target:document.getElementById("app")});
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" />
7
+ <title>aiusage Widget</title>
8
+ <script type="module" crossorigin src="./assets/index-Bb9mog0W.js"></script>
9
+ <link rel="stylesheet" crossorigin href="./assets/index-2X4hbwJe.css">
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ </body>
14
+ </html>
package/dist/ui.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldShowWindowOnLaunch = shouldShowWindowOnLaunch;
4
+ exports.shouldHideWindowOnBlur = shouldHideWindowOnBlur;
5
+ exports.shouldHideWindowOnClose = shouldHideWindowOnClose;
6
+ exports.getTrayIconDataUrl = getTrayIconDataUrl;
7
+ const TRAY_ICON_SVG = `
8
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
9
+ <rect x="1" y="1" width="14" height="14" rx="4" fill="#111827"/>
10
+ <path d="M8.8 2.5 4.7 8.2h2.9L7 13.5l4.3-5.9H8.4l.4-5.1Z" fill="#f8fafc"/>
11
+ </svg>
12
+ `.trim();
13
+ // Pre-rendered 32x32 PNG of the same icon — Windows tray doesn't support SVG
14
+ const TRAY_ICON_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAyUlEQVR4nMWXMQ6DMAxFzRcX6NYu' +
15
+ 'XTtw/6N06NqFbhyhVYYsQUkT+zt+ExLI7zsCJ4gEs7RuXq6PL0NyfF5Vz+Ip7gmCWfJabcyS1xxg' +
16
+ 'yPf3Ux0CWqlWXoIy0SyyE5HdmwIw5OoApfx23+YGYIKopVcF2MnyoQAe8sTKKFIL1/NywiJo0ftl' +
17
+ 'rJZioYOoxchcgJAZHUqQYMAsphnJ0MpY+wEkGDCKmHfDo3Fu95JnJyQY5AvNKli7T5yk3gfUslH8e' +
18
+ '8BTngj/OQ3nB+9vTnl87+2CAAAAAElFTkSuQmCC';
19
+ function shouldShowWindowOnLaunch(isPackaged) {
20
+ return !isPackaged;
21
+ }
22
+ function shouldHideWindowOnBlur(isPackaged) {
23
+ return isPackaged;
24
+ }
25
+ function shouldHideWindowOnClose(_) {
26
+ return true;
27
+ }
28
+ function getTrayIconDataUrl() {
29
+ // Windows tray doesn't render SVG icons — use pre-rendered PNG
30
+ if (process.platform === 'win32') {
31
+ return `data:image/png;base64,${TRAY_ICON_PNG_BASE64}`;
32
+ }
33
+ return `data:image/svg+xml;base64,${Buffer.from(TRAY_ICON_SVG).toString('base64')}`;
34
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@juliantanx/aiusage-widget",
3
+ "version": "1.3.1",
4
+ "private": false,
5
+ "description": "System tray widget for aiusage — view AI token usage from your system tray",
6
+ "keywords": [
7
+ "ai",
8
+ "aiusage",
9
+ "widget",
10
+ "system-tray",
11
+ "electron",
12
+ "token-usage"
13
+ ],
14
+ "homepage": "https://github.com/juliantanx/aiusage/tree/main/packages/widget#readme",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/juliantanx/aiusage.git",
18
+ "directory": "packages/widget"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/juliantanx/aiusage/issues"
22
+ },
23
+ "license": "MIT",
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "main": "dist/main.js",
28
+ "files": [
29
+ "dist",
30
+ "bin",
31
+ "README.md",
32
+ "README_zh.md"
33
+ ],
34
+ "bin": {
35
+ "aiusage-widget": "bin/launcher.js"
36
+ },
37
+ "scripts": {
38
+ "rebuild": "pnpm run rebuild:electron",
39
+ "rebuild:electron": "node -e \"const {execSync}=require('child_process'); const electronVersion=require('electron/package.json').version; const cmd='npm rebuild better-sqlite3 --runtime=electron --target=' + electronVersion + ' --dist-url=https://electronjs.org/headers'; execSync(cmd, {stdio:'inherit'})\"",
40
+ "rebuild:node": "npm rebuild better-sqlite3",
41
+ "dev": "pnpm run rebuild:electron && concurrently --names \"vite,tsc,electron\" --kill-others --success command-electron \"vite build --watch\" \"tsc -p tsconfig.json --watch\" \"pnpm exec wait-on dist/main.js && pnpm exec electron .\"",
42
+ "build": "rm -rf dist/renderer && vite build && tsc -p tsconfig.json",
43
+ "pack": "pnpm build && electron-builder",
44
+ "test": "pnpm run rebuild:node && vitest run"
45
+ },
46
+ "dependencies": {
47
+ "better-sqlite3": "^11.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "electron": "^33.0.0",
51
+ "@sveltejs/vite-plugin-svelte": "^3.0.0",
52
+ "@types/better-sqlite3": "*",
53
+ "@types/node": "*",
54
+ "concurrently": "^8.0.0",
55
+ "electron-builder": "^25.0.0",
56
+ "svelte": "^4.0.0",
57
+ "typescript": "^5.0.0",
58
+ "vite": "^5.0.0",
59
+ "vitest": "^2.0.0",
60
+ "wait-on": "^7.0.0"
61
+ }
62
+ }