@anibx/token-tracker 1.0.2

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,137 @@
1
+ # Token Tracker
2
+
3
+ A cross-platform desktop app that shows real-time and historical token usage + costs for **Anthropic (Claude)** and **OpenAI (GPT)** APIs.
4
+
5
+ Built with **Tauri v2** (Rust backend) + **React 19 + TypeScript** (frontend).
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install -g @anibx/token-tracker
13
+ token-tracker
14
+ ```
15
+
16
+ Or download a binary directly from the [latest release](https://github.com/ani0x53/token-tracker/releases/latest).
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ - **Real-time polling** — fetches usage from Anthropic and OpenAI APIs on a configurable interval (default 5 min)
23
+ - **30-day history** — stored locally in SQLite, persists across restarts
24
+ - **Daily line chart** — cost over time per provider
25
+ - **Model breakdown bar chart** — cost per model (Claude Sonnet, Opus, GPT-4o, etc.)
26
+ - **Spending alerts** — OS-level notifications when daily/monthly thresholds are exceeded
27
+ - **System tray** — shows today's total cost, click to open window
28
+ - **Dark UI** — Tailwind CSS dark theme
29
+
30
+ ---
31
+
32
+ ## Prerequisites
33
+
34
+ ### macOS / Windows
35
+ Tauri's prerequisites are automatically handled by the platform toolchain.
36
+
37
+ ### Linux (Ubuntu/Debian)
38
+ ```bash
39
+ sudo apt-get install -y \
40
+ libwebkit2gtk-4.1-dev \
41
+ libgtk-3-dev \
42
+ libayatana-appindicator3-dev \
43
+ librsvg2-dev \
44
+ pkg-config
45
+ ```
46
+
47
+ ### Rust
48
+ ```bash
49
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
50
+ source "$HOME/.cargo/env"
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Setup
56
+
57
+ ```bash
58
+ # 1. Install JS dependencies
59
+ npm install
60
+
61
+ # 2. Run in development mode
62
+ npm run tauri dev
63
+
64
+ # 3. Build a release binary
65
+ npm run tauri build
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Configuration
71
+
72
+ On first launch the Settings panel opens automatically. Enter your API keys there.
73
+
74
+ **Keys are stored only on your machine** — in your OS app data directory. They are never sent anywhere except the respective API provider.
75
+
76
+ | Setting | Where to get it |
77
+ |---------|----------------|
78
+ | **Anthropic Admin Key** | [console.anthropic.com/settings/admin-keys](https://console.anthropic.com/settings/admin-keys) — must be an **Admin** key, not a regular API key |
79
+ | **OpenAI API Key** | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
80
+ | **Poll Interval** | Seconds between data fetches (default 300) |
81
+ | **Daily Alert ($)** | OS notification when daily spend exceeds this amount |
82
+ | **Monthly Alert ($)** | OS notification when monthly spend exceeds this amount |
83
+
84
+ You can leave out either key if you only use one provider.
85
+
86
+ ---
87
+
88
+ ## Architecture
89
+
90
+ ```
91
+ token-tracker/
92
+ ├── src/ # React frontend
93
+ │ ├── App.tsx
94
+ │ ├── components/
95
+ │ │ ├── Dashboard.tsx # Main layout
96
+ │ │ ├── ProviderCard.tsx # Per-provider summary card
97
+ │ │ ├── UsageChart.tsx # Line chart — daily usage
98
+ │ │ ├── ModelBreakdown.tsx # Bar chart — cost per model
99
+ │ │ └── AlertSettings.tsx # Settings modal
100
+ │ ├── hooks/
101
+ │ │ ├── useUsageData.ts # SQLite query + event listeners
102
+ │ │ └── useAlerts.ts # Spending alert logic
103
+ │ └── store/
104
+ │ └── settingsStore.ts # Zustand store for settings
105
+ ├── src-tauri/
106
+ │ └── src/
107
+ │ ├── lib.rs # Tauri commands + app setup
108
+ │ ├── api/
109
+ │ │ ├── anthropic.rs # Anthropic usage API client
110
+ │ │ └── openai.rs # OpenAI usage API client
111
+ │ ├── poller.rs # Background polling loop
112
+ │ ├── storage.rs # SQLite schema constants
113
+ │ └── tray.rs # System tray setup
114
+ ```
115
+
116
+ ### Data flow
117
+
118
+ 1. Rust `poller` fetches both APIs every N seconds
119
+ 2. New snapshots are emitted as `new-snapshots` Tauri events
120
+ 3. Frontend `useUsageData` hook receives events and upserts into local SQLite
121
+ 4. React Query re-fetches from DB and re-renders charts
122
+ 5. Tray tooltip is updated with today's total cost
123
+
124
+ ---
125
+
126
+ ## Tech Stack
127
+
128
+ | Layer | Choice |
129
+ |-------|--------|
130
+ | Desktop shell | Tauri v2 |
131
+ | Frontend | React 19 + TypeScript |
132
+ | Styling | Tailwind CSS v4 |
133
+ | Charts | Recharts |
134
+ | State | Zustand |
135
+ | DB | SQLite (tauri-plugin-sql) |
136
+ | HTTP | reqwest (Rust) |
137
+ | Notifications | tauri-plugin-notification |
package/bin/launch.cjs ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn, execFileSync } = require('child_process');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const fs = require('fs');
8
+
9
+ function launch() {
10
+ const p = process.platform;
11
+
12
+ if (p === 'linux') {
13
+ const bin = path.join(os.homedir(), '.local', 'share', 'token-tracker', 'token-tracker.AppImage');
14
+ if (!fs.existsSync(bin)) {
15
+ console.error(`Binary not found at ${bin}`);
16
+ console.error('Try reinstalling: npm install -g @anibx/token-tracker');
17
+ process.exit(1);
18
+ }
19
+ spawn(bin, [], { detached: true, stdio: 'ignore' }).unref();
20
+
21
+ } else if (p === 'darwin') {
22
+ const local = path.join(os.homedir(), 'Applications', 'Token Tracker.app');
23
+ const system = '/Applications/Token Tracker.app';
24
+ if (!fs.existsSync(local) && !fs.existsSync(system)) {
25
+ console.error('Token Tracker.app not found in ~/Applications or /Applications.');
26
+ console.error('Try reinstalling: npm install -g @anibx/token-tracker');
27
+ process.exit(1);
28
+ }
29
+ const appPath = fs.existsSync(local) ? local : system;
30
+ execFileSync('open', [appPath]);
31
+
32
+ } else if (p === 'win32') {
33
+ const exe = path.join(
34
+ process.env['ProgramFiles'] || 'C:\\Program Files',
35
+ 'Token Tracker',
36
+ 'Token Tracker.exe'
37
+ );
38
+ if (!fs.existsSync(exe)) {
39
+ console.error(`Executable not found at ${exe}`);
40
+ console.error('Try reinstalling: npm install -g @anibx/token-tracker');
41
+ process.exit(1);
42
+ }
43
+ spawn(exe, [], { detached: true, stdio: 'ignore' }).unref();
44
+
45
+ } else {
46
+ console.error(`Unsupported platform: ${p}`);
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ launch();
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@anibx/token-tracker",
3
+ "version": "1.0.2",
4
+ "description": "Real-time token usage and cost tracker for Anthropic and OpenAI APIs",
5
+ "type": "module",
6
+ "bin": {
7
+ "token-tracker": "./bin/launch.cjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "scripts/"
12
+ ],
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "tsc && vite build",
16
+ "preview": "vite preview",
17
+ "tauri": "tauri",
18
+ "postinstall": "node scripts/postinstall.cjs"
19
+ },
20
+ "dependencies": {
21
+ "@tanstack/react-query": "^5.90.21",
22
+ "@tauri-apps/api": "^2",
23
+ "@tauri-apps/plugin-notification": "^2.3.3",
24
+ "@tauri-apps/plugin-opener": "^2",
25
+ "@tauri-apps/plugin-sql": "^2.3.2",
26
+ "lucide-react": "^0.575.0",
27
+ "react": "^19.1.0",
28
+ "react-dom": "^19.1.0",
29
+ "recharts": "^3.7.0",
30
+ "zustand": "^5.0.11"
31
+ },
32
+ "devDependencies": {
33
+ "@tailwindcss/vite": "^4.2.1",
34
+ "@tauri-apps/cli": "^2",
35
+ "@types/react": "^19.1.8",
36
+ "@types/react-dom": "^19.1.6",
37
+ "@vitejs/plugin-react": "^4.6.0",
38
+ "tailwindcss": "^4.2.1",
39
+ "typescript": "~5.8.3",
40
+ "vite": "^7.0.4"
41
+ }
42
+ }
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const https = require('https');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const os = require('os');
8
+ const { execFileSync } = require('child_process');
9
+
10
+ // Skip when running inside the source repository
11
+ if (fs.existsSync(path.join(__dirname, '..', 'src', 'App.tsx'))) {
12
+ process.exit(0);
13
+ }
14
+
15
+ // Skip if explicitly disabled (e.g. in CI during npm publish)
16
+ if (process.env.SKIP_POSTINSTALL) {
17
+ process.exit(0);
18
+ }
19
+
20
+ const REPO = 'ani0x53/token-tracker';
21
+ const { version } = require('../package.json');
22
+
23
+ function getInstallDir() {
24
+ if (process.platform === 'win32') {
25
+ return path.join(process.env.APPDATA || os.homedir(), 'token-tracker');
26
+ }
27
+ return path.join(os.homedir(), '.local', 'share', 'token-tracker');
28
+ }
29
+
30
+ function findAsset(assets) {
31
+ const p = process.platform;
32
+ const a = process.arch;
33
+
34
+ return assets.find(({ name }) => {
35
+ if (p === 'linux') {
36
+ if (!name.endsWith('.AppImage')) return false;
37
+ return a === 'arm64' ? name.includes('aarch64') : name.includes('amd64');
38
+ }
39
+ if (p === 'darwin') {
40
+ if (!name.endsWith('.dmg')) return false;
41
+ // universal dmg works for both arches; also match arch-specific ones
42
+ if (name.includes('universal')) return true;
43
+ return a === 'arm64' ? name.includes('aarch64') : name.includes('x64') && !name.includes('aarch64');
44
+ }
45
+ if (p === 'win32') {
46
+ return name.endsWith('-setup.exe') && name.includes('x64');
47
+ }
48
+ return false;
49
+ });
50
+ }
51
+
52
+ function get(url) {
53
+ return new Promise((resolve, reject) => {
54
+ https.get(url, { headers: { 'User-Agent': 'token-tracker-installer' } }, (res) => {
55
+ if (res.statusCode === 301 || res.statusCode === 302) {
56
+ resolve(get(res.headers.location));
57
+ return;
58
+ }
59
+ let body = '';
60
+ res.on('data', (chunk) => (body += chunk));
61
+ res.on('end', () => resolve(body));
62
+ res.on('error', reject);
63
+ }).on('error', reject);
64
+ });
65
+ }
66
+
67
+ function download(url, dest) {
68
+ return new Promise((resolve, reject) => {
69
+ function fetch(url) {
70
+ https.get(url, { headers: { 'User-Agent': 'token-tracker-installer' } }, (res) => {
71
+ if (res.statusCode === 301 || res.statusCode === 302) {
72
+ fetch(res.headers.location);
73
+ return;
74
+ }
75
+ if (res.statusCode !== 200) {
76
+ reject(new Error(`HTTP ${res.statusCode} from ${url}`));
77
+ return;
78
+ }
79
+ const total = parseInt(res.headers['content-length'] || '0', 10);
80
+ let received = 0;
81
+ const file = fs.createWriteStream(dest);
82
+
83
+ res.on('data', (chunk) => {
84
+ received += chunk.length;
85
+ if (total) {
86
+ const pct = Math.round((received / total) * 100);
87
+ process.stdout.write(`\r Downloading... ${pct}%`);
88
+ }
89
+ });
90
+ res.pipe(file);
91
+ file.on('finish', () => {
92
+ process.stdout.write('\n');
93
+ resolve();
94
+ });
95
+ file.on('error', reject);
96
+ }).on('error', reject);
97
+ }
98
+ fetch(url);
99
+ });
100
+ }
101
+
102
+ async function main() {
103
+ console.log(`\nToken Tracker v${version} — fetching release info...`);
104
+
105
+ let release;
106
+ try {
107
+ const body = await get(`https://api.github.com/repos/${REPO}/releases/latest`);
108
+ release = JSON.parse(body);
109
+ } catch (e) {
110
+ console.error('Could not fetch release info:', e.message);
111
+ console.error(`Download manually: https://github.com/${REPO}/releases`);
112
+ process.exit(1);
113
+ }
114
+
115
+ const asset = findAsset(release.assets || []);
116
+ if (!asset) {
117
+ const names = (release.assets || []).map((a) => a.name).join(', ') || 'none';
118
+ console.error(`No binary found for ${process.platform}/${process.arch}.`);
119
+ console.error(`Available: ${names}`);
120
+ process.exit(1);
121
+ }
122
+
123
+ const installDir = getInstallDir();
124
+ fs.mkdirSync(installDir, { recursive: true });
125
+
126
+ const ext = path.extname(asset.name);
127
+ const tmp = path.join(installDir, `download${ext}`);
128
+
129
+ console.log(`Downloading ${asset.name}...`);
130
+ await download(asset.browser_download_url, tmp);
131
+
132
+ if (process.platform === 'linux') {
133
+ const dest = path.join(installDir, 'token-tracker.AppImage');
134
+ fs.renameSync(tmp, dest);
135
+ fs.chmodSync(dest, 0o755);
136
+ console.log(`Installed to ${dest}`);
137
+ } else if (process.platform === 'darwin') {
138
+ console.log('Mounting disk image...');
139
+ execFileSync('hdiutil', ['attach', tmp, '-quiet', '-nobrowse']);
140
+ const appsDir = path.join(os.homedir(), 'Applications');
141
+ fs.mkdirSync(appsDir, { recursive: true });
142
+ execFileSync('cp', ['-r', '/Volumes/Token Tracker/Token Tracker.app', appsDir]);
143
+ execFileSync('hdiutil', ['detach', '/Volumes/Token Tracker', '-quiet']);
144
+ fs.unlinkSync(tmp);
145
+ console.log(`Installed to ~/Applications/Token Tracker.app`);
146
+ } else if (process.platform === 'win32') {
147
+ console.log('Running installer (silent)...');
148
+ execFileSync(tmp, ['/S'], { windowsHide: true });
149
+ fs.unlinkSync(tmp);
150
+ console.log('Token Tracker installed.');
151
+ }
152
+
153
+ console.log('Done! Run: token-tracker\n');
154
+ }
155
+
156
+ main().catch((e) => {
157
+ console.error('\nInstallation failed:', e.message);
158
+ process.exit(1);
159
+ });