tokmon 0.6.1 → 0.8.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 +57 -28
- package/dist/cli.js +158 -117
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -35,57 +35,86 @@ Then just run `tokmon`. Press `q` to quit.
|
|
|
35
35
|
|
|
36
36
|
## Keybindings
|
|
37
37
|
|
|
38
|
+
### Global
|
|
39
|
+
|
|
38
40
|
| Key | Action |
|
|
39
41
|
|-----|--------|
|
|
40
|
-
| `Tab`
|
|
41
|
-
| `1` `2` | Jump to
|
|
42
|
-
| `
|
|
43
|
-
| `↑` `↓` | Scroll table |
|
|
44
|
-
| `PgUp` `PgDn` | Scroll table fast |
|
|
45
|
-
| `s` | Settings |
|
|
42
|
+
| `Tab` | Cycle between Dashboard and Table |
|
|
43
|
+
| `1` `2` | Jump to Dashboard / Table |
|
|
44
|
+
| `s` | Open settings |
|
|
46
45
|
| `q` | Quit |
|
|
47
46
|
|
|
47
|
+
### Table View
|
|
48
|
+
|
|
49
|
+
| Key | Action |
|
|
50
|
+
|-----|--------|
|
|
51
|
+
| `d` `w` `m` | Switch to Daily / Weekly / Monthly |
|
|
52
|
+
| `←` `→` | Cycle sub-view |
|
|
53
|
+
| `↑` `↓` | Move cursor / navigate rows |
|
|
54
|
+
| `Enter` | Expand row — per-model cost breakdown |
|
|
55
|
+
| `Esc` | Collapse expanded row |
|
|
56
|
+
| `o` | Cycle sort: date ↑, date ↓, cost ↑, cost ↓ |
|
|
57
|
+
| `g` | Jump to top |
|
|
58
|
+
| `G` | Jump to bottom |
|
|
59
|
+
| `PgUp` `PgDn` | Page scroll |
|
|
60
|
+
|
|
61
|
+
### Settings
|
|
62
|
+
|
|
63
|
+
| Key | Action |
|
|
64
|
+
|-----|--------|
|
|
65
|
+
| `↑` `↓` | Select option |
|
|
66
|
+
| `←` `→` | Adjust value |
|
|
67
|
+
| `s` / `Esc` | Close |
|
|
68
|
+
|
|
48
69
|
## Views
|
|
49
70
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
| **Table → Monthly** | Grouped by month |
|
|
71
|
+
### Dashboard
|
|
72
|
+
|
|
73
|
+
- **Today / This Week / This Month** — cost and token summaries
|
|
74
|
+
- **Burn rate** — current $/hr
|
|
75
|
+
- **Rate Limits** — real-time session (5h), weekly (7d), and Sonnet utilization with reset countdowns, fetched from Anthropic's OAuth API
|
|
56
76
|
|
|
57
|
-
|
|
77
|
+
### Table
|
|
58
78
|
|
|
59
|
-
|
|
79
|
+
Interactive table with 3 sub-views:
|
|
60
80
|
|
|
61
|
-
- **
|
|
62
|
-
- **Weekly** —
|
|
63
|
-
- **
|
|
64
|
-
- **Extra usage** — spend vs monthly limit
|
|
81
|
+
- **Daily** — per-day breakdown (6 months of history)
|
|
82
|
+
- **Weekly** — grouped by ISO week
|
|
83
|
+
- **Monthly** — grouped by month
|
|
65
84
|
|
|
66
|
-
|
|
85
|
+
Each row shows models used, input/output/cache tokens, and cost. Press `Enter` on any row to expand a per-model breakdown:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
▸ Apr 7 haiku-4-5, op~ 7.6K 487.0K 10.1M 1.1B $603.89
|
|
89
|
+
├─ opus-4-6 7.5K 485.0K 10.0M 1.1B $601.50
|
|
90
|
+
└─ haiku-4-5 100 2.0K 100K 5.0M $2.39
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Sort by date or cost with `o`.
|
|
67
94
|
|
|
68
95
|
## Settings
|
|
69
96
|
|
|
70
|
-
Press `s` to open
|
|
97
|
+
Press `s` to open. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
|
|
71
98
|
|
|
72
|
-
- **Refresh interval** — adjust with
|
|
73
|
-
- **Clear screen** — on
|
|
99
|
+
- **Refresh interval** — adjust with `←` `→`
|
|
100
|
+
- **Clear screen** — clears terminal on launch (like `watch`)
|
|
74
101
|
|
|
75
102
|
## How It Works
|
|
76
103
|
|
|
77
|
-
Reads Claude Code's JSONL session logs
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
- Reads Claude Code's JSONL session logs from `~/.claude/projects/`
|
|
105
|
+
- Calculates costs using Claude model pricing (Opus, Sonnet, Haiku)
|
|
106
|
+
- Caches file reads by mtime — subsequent refreshes are near-instant
|
|
107
|
+
- Dashboard loads current month only (fast). Table loads 6 months lazily.
|
|
108
|
+
- Rate limits fetched from Anthropic OAuth API every 2 minutes (token from macOS Keychain)
|
|
80
109
|
|
|
81
|
-
Cross-platform:
|
|
110
|
+
Cross-platform: macOS, Linux, Windows.
|
|
82
111
|
|
|
83
112
|
## CI/CD
|
|
84
113
|
|
|
85
|
-
Publishes to npm via GitHub Actions on version tags:
|
|
114
|
+
Publishes to npm and GitHub Packages via GitHub Actions on version tags:
|
|
86
115
|
|
|
87
116
|
```bash
|
|
88
|
-
git tag v0.
|
|
117
|
+
git tag v0.7.0 && git push --tags
|
|
89
118
|
```
|
|
90
119
|
|
|
91
120
|
## Requirements
|
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.tsx
|
|
4
4
|
import { render } from "ink";
|
|
5
|
+
import { MouseProvider as MouseProvider2 } from "@zenobius/ink-mouse";
|
|
5
6
|
|
|
6
7
|
// src/config.ts
|
|
7
8
|
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
@@ -15,12 +16,12 @@ function configDir() {
|
|
|
15
16
|
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
16
17
|
return join(xdg, "tokmon");
|
|
17
18
|
}
|
|
18
|
-
function
|
|
19
|
+
function configLocation() {
|
|
19
20
|
return join(configDir(), "config.json");
|
|
20
21
|
}
|
|
21
22
|
async function loadConfig() {
|
|
22
23
|
try {
|
|
23
|
-
const raw = await readFile(
|
|
24
|
+
const raw = await readFile(configLocation(), "utf-8");
|
|
24
25
|
const parsed = JSON.parse(raw);
|
|
25
26
|
return { ...DEFAULTS, ...parsed };
|
|
26
27
|
} catch {
|
|
@@ -30,15 +31,13 @@ async function loadConfig() {
|
|
|
30
31
|
async function saveConfig(config2) {
|
|
31
32
|
const dir = configDir();
|
|
32
33
|
await mkdir(dir, { recursive: true });
|
|
33
|
-
await writeFile(
|
|
34
|
-
}
|
|
35
|
-
function configLocation() {
|
|
36
|
-
return configPath();
|
|
34
|
+
await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
// src/app.tsx
|
|
40
|
-
import { useState, useEffect, useRef } from "react";
|
|
38
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
41
39
|
import { Box, Text, useInput, useStdout, useApp } from "ink";
|
|
40
|
+
import { useMouse } from "@zenobius/ink-mouse";
|
|
42
41
|
|
|
43
42
|
// src/data.ts
|
|
44
43
|
import { readdir, stat as fsStat } from "fs/promises";
|
|
@@ -82,7 +81,7 @@ function costOf(model, u) {
|
|
|
82
81
|
return (u.input_tokens ?? 0) * p.i + (u.output_tokens ?? 0) * p.o + (u.cache_creation_input_tokens ?? 0) * p.cc + (u.cache_read_input_tokens ?? 0) * p.cr;
|
|
83
82
|
}
|
|
84
83
|
function shortModel(model) {
|
|
85
|
-
return model.replace("claude-", "").replace(
|
|
84
|
+
return model.replace("claude-", "").replace(/-\d{8}$/, "");
|
|
86
85
|
}
|
|
87
86
|
async function parseFile(path, since) {
|
|
88
87
|
const entries = [];
|
|
@@ -110,7 +109,7 @@ async function parseFile(path, since) {
|
|
|
110
109
|
return entries;
|
|
111
110
|
}
|
|
112
111
|
async function loadEntries(since) {
|
|
113
|
-
const
|
|
112
|
+
const chunks = [];
|
|
114
113
|
const seen = /* @__PURE__ */ new Set();
|
|
115
114
|
for (const dir of getClaudeDirs()) {
|
|
116
115
|
let listing;
|
|
@@ -129,17 +128,17 @@ async function loadEntries(since) {
|
|
|
129
128
|
if (s.mtimeMs < since) return;
|
|
130
129
|
const cached = fileCache.get(path);
|
|
131
130
|
if (cached && cached.mtimeMs === s.mtimeMs) {
|
|
132
|
-
|
|
131
|
+
chunks.push(cached.data);
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
134
|
const data = await parseFile(path, since);
|
|
136
135
|
fileCache.set(path, { mtimeMs: s.mtimeMs, data });
|
|
137
|
-
|
|
136
|
+
chunks.push(data);
|
|
138
137
|
} catch {
|
|
139
138
|
}
|
|
140
139
|
}));
|
|
141
140
|
}
|
|
142
|
-
return
|
|
141
|
+
return chunks.flat();
|
|
143
142
|
}
|
|
144
143
|
function sum(entries) {
|
|
145
144
|
let cost = 0, tokens2 = 0;
|
|
@@ -159,29 +158,36 @@ function groupBy(entries, keyFn) {
|
|
|
159
158
|
}
|
|
160
159
|
const rows = [];
|
|
161
160
|
for (const [label, group] of groups) {
|
|
162
|
-
const models = [...new Set(group.map((e) => shortModel(e.model)))];
|
|
163
161
|
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
|
|
162
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
164
163
|
for (const e of group) {
|
|
165
164
|
input += e.input;
|
|
166
165
|
output += e.output;
|
|
167
166
|
cacheCreate += e.cacheCreate;
|
|
168
167
|
cacheRead += e.cacheRead;
|
|
169
168
|
cost += e.cost;
|
|
170
|
-
}
|
|
171
|
-
const byModel = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const e of group) {
|
|
173
169
|
const name = shortModel(e.model);
|
|
174
|
-
const m = byModel.get(name)
|
|
175
|
-
m
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
170
|
+
const m = byModel.get(name);
|
|
171
|
+
if (m) {
|
|
172
|
+
m.input += e.input;
|
|
173
|
+
m.output += e.output;
|
|
174
|
+
m.cacheCreate += e.cacheCreate;
|
|
175
|
+
m.cacheRead += e.cacheRead;
|
|
176
|
+
m.cost += e.cost;
|
|
177
|
+
} else {
|
|
178
|
+
byModel.set(name, {
|
|
179
|
+
name,
|
|
180
|
+
input: e.input,
|
|
181
|
+
output: e.output,
|
|
182
|
+
cacheCreate: e.cacheCreate,
|
|
183
|
+
cacheRead: e.cacheRead,
|
|
184
|
+
cost: e.cost
|
|
185
|
+
});
|
|
186
|
+
}
|
|
181
187
|
}
|
|
182
188
|
rows.push({
|
|
183
189
|
label,
|
|
184
|
-
models:
|
|
190
|
+
models: [...byModel.keys()].sort(),
|
|
185
191
|
input,
|
|
186
192
|
output,
|
|
187
193
|
cacheCreate,
|
|
@@ -215,9 +221,14 @@ async function fetchDashboard() {
|
|
|
215
221
|
const todayEntries = entries.filter((e) => e.ts >= todayStart);
|
|
216
222
|
let burnRate = 0;
|
|
217
223
|
if (todayEntries.length > 0) {
|
|
218
|
-
|
|
224
|
+
let oldest = todayEntries[0].ts;
|
|
225
|
+
let totalCost = 0;
|
|
226
|
+
for (const e of todayEntries) {
|
|
227
|
+
if (e.ts < oldest) oldest = e.ts;
|
|
228
|
+
totalCost += e.cost;
|
|
229
|
+
}
|
|
219
230
|
const hrs = (now - oldest) / 36e5;
|
|
220
|
-
if (hrs > 0) burnRate =
|
|
231
|
+
if (hrs > 0) burnRate = totalCost / hrs;
|
|
221
232
|
}
|
|
222
233
|
return {
|
|
223
234
|
today: sum(todayEntries),
|
|
@@ -258,9 +269,10 @@ async function getAccessToken() {
|
|
|
258
269
|
}
|
|
259
270
|
return null;
|
|
260
271
|
}
|
|
272
|
+
var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
|
|
261
273
|
async function fetchBilling() {
|
|
262
274
|
const token = await getAccessToken();
|
|
263
|
-
if (!token) return
|
|
275
|
+
if (!token) return { ...EMPTY, error: "No OAuth token found (macOS Keychain)" };
|
|
264
276
|
try {
|
|
265
277
|
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
266
278
|
headers: {
|
|
@@ -270,7 +282,9 @@ async function fetchBilling() {
|
|
|
270
282
|
},
|
|
271
283
|
signal: AbortSignal.timeout(1e4)
|
|
272
284
|
});
|
|
273
|
-
if (
|
|
285
|
+
if (res.status === 429) return { ...EMPTY, error: "Rate limited \u2014 retrying in 2m" };
|
|
286
|
+
if (res.status === 401) return { ...EMPTY, error: "Token expired \u2014 restart Claude Code" };
|
|
287
|
+
if (!res.ok) return { ...EMPTY, error: `API ${res.status}` };
|
|
274
288
|
const data = await res.json();
|
|
275
289
|
return {
|
|
276
290
|
session: data.five_hour ? {
|
|
@@ -288,10 +302,11 @@ async function fetchBilling() {
|
|
|
288
302
|
extraUsage: data.extra_usage?.is_enabled ? {
|
|
289
303
|
limit: data.extra_usage.monthly_limit / 100,
|
|
290
304
|
used: data.extra_usage.used_credits / 100
|
|
291
|
-
} : null
|
|
305
|
+
} : null,
|
|
306
|
+
error: null
|
|
292
307
|
};
|
|
293
308
|
} catch {
|
|
294
|
-
return
|
|
309
|
+
return { ...EMPTY, error: "Network error" };
|
|
295
310
|
}
|
|
296
311
|
}
|
|
297
312
|
function formatReset(iso) {
|
|
@@ -326,10 +341,10 @@ function time(date) {
|
|
|
326
341
|
second: "2-digit"
|
|
327
342
|
});
|
|
328
343
|
}
|
|
344
|
+
var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
329
345
|
function shortDate(iso) {
|
|
330
346
|
const [, m, d] = iso.split("-");
|
|
331
|
-
|
|
332
|
-
return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
|
|
347
|
+
return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
|
|
333
348
|
}
|
|
334
349
|
function col(s, w, align = "right") {
|
|
335
350
|
if (s.length > w) return s.slice(0, w - 1) + "~";
|
|
@@ -341,6 +356,11 @@ function col(s, w, align = "right") {
|
|
|
341
356
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
342
357
|
var TABS = ["Dashboard", "Table"];
|
|
343
358
|
var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
359
|
+
var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
|
|
360
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
361
|
+
var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
362
|
+
var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
|
|
363
|
+
var IS_TTY = process.stdin.isTTY === true;
|
|
344
364
|
function App({ interval: cliInterval }) {
|
|
345
365
|
const [dashboard, setDashboard] = useState(null);
|
|
346
366
|
const [billing, setBilling] = useState(null);
|
|
@@ -352,6 +372,7 @@ function App({ interval: cliInterval }) {
|
|
|
352
372
|
const [view, setView] = useState(0);
|
|
353
373
|
const [cursor, setCursor] = useState(0);
|
|
354
374
|
const [expanded, setExpanded] = useState(-1);
|
|
375
|
+
const [sort, setSort] = useState(0);
|
|
355
376
|
const [showSettings, setShowSettings] = useState(false);
|
|
356
377
|
const [config2, setConfig] = useState(null);
|
|
357
378
|
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
@@ -361,7 +382,7 @@ function App({ interval: cliInterval }) {
|
|
|
361
382
|
const rows = stdout?.rows ?? 24;
|
|
362
383
|
const cols = stdout?.columns ?? 80;
|
|
363
384
|
const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
|
|
364
|
-
const cfg = config2 ??
|
|
385
|
+
const cfg = config2 ?? DEFAULT_CONFIG;
|
|
365
386
|
useEffect(() => {
|
|
366
387
|
loadConfig().then((c) => {
|
|
367
388
|
if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
|
|
@@ -392,7 +413,7 @@ function App({ interval: cliInterval }) {
|
|
|
392
413
|
useEffect(() => {
|
|
393
414
|
let active = true;
|
|
394
415
|
const load = () => fetchBilling().then((b) => {
|
|
395
|
-
if (active
|
|
416
|
+
if (active) setBilling(b);
|
|
396
417
|
}).catch(() => {
|
|
397
418
|
});
|
|
398
419
|
load();
|
|
@@ -435,30 +456,35 @@ function App({ interval: cliInterval }) {
|
|
|
435
456
|
clearInterval(id);
|
|
436
457
|
};
|
|
437
458
|
}, [tab, interval2]);
|
|
438
|
-
const
|
|
459
|
+
const resetView = useCallback(() => {
|
|
460
|
+
setCursor(0);
|
|
461
|
+
setExpanded(-1);
|
|
462
|
+
}, []);
|
|
463
|
+
const mouse = useMouse();
|
|
464
|
+
useEffect(() => {
|
|
465
|
+
if (!IS_TTY) return;
|
|
466
|
+
mouse.enable();
|
|
467
|
+
const onScroll = (_pos, dir) => {
|
|
468
|
+
if (tab === 1) {
|
|
469
|
+
setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
mouse.events.on("scroll", onScroll);
|
|
473
|
+
return () => {
|
|
474
|
+
mouse.events.off("scroll", onScroll);
|
|
475
|
+
};
|
|
476
|
+
}, [tab]);
|
|
439
477
|
useInput((input, key) => {
|
|
440
478
|
if (showSettings) {
|
|
441
479
|
if (key.escape || input === "s") setShowSettings(false);
|
|
442
480
|
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
443
481
|
if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
|
|
444
482
|
if (settingsCursor === 0) {
|
|
445
|
-
if (key.leftArrow)
|
|
446
|
-
|
|
447
|
-
saveConfig(n);
|
|
448
|
-
return n;
|
|
449
|
-
});
|
|
450
|
-
if (key.rightArrow) setConfig((c) => {
|
|
451
|
-
const n = { ...c, interval: c.interval + 1 };
|
|
452
|
-
saveConfig(n);
|
|
453
|
-
return n;
|
|
454
|
-
});
|
|
483
|
+
if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
|
|
484
|
+
if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
|
|
455
485
|
}
|
|
456
486
|
if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
|
|
457
|
-
|
|
458
|
-
const n = { ...c, clearScreen: !c.clearScreen };
|
|
459
|
-
saveConfig(n);
|
|
460
|
-
return n;
|
|
461
|
-
});
|
|
487
|
+
updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
|
|
462
488
|
}
|
|
463
489
|
return;
|
|
464
490
|
}
|
|
@@ -472,51 +498,48 @@ function App({ interval: cliInterval }) {
|
|
|
472
498
|
}
|
|
473
499
|
if (key.tab) {
|
|
474
500
|
setTab((t) => (t + 1) % TABS.length);
|
|
475
|
-
|
|
476
|
-
setExpanded(-1);
|
|
501
|
+
resetView();
|
|
477
502
|
return;
|
|
478
503
|
}
|
|
479
504
|
if (input === "1") {
|
|
480
505
|
setTab(0);
|
|
481
|
-
|
|
482
|
-
setExpanded(-1);
|
|
506
|
+
resetView();
|
|
483
507
|
return;
|
|
484
508
|
}
|
|
485
509
|
if (input === "2") {
|
|
486
510
|
setTab(1);
|
|
487
|
-
|
|
488
|
-
setExpanded(-1);
|
|
511
|
+
resetView();
|
|
489
512
|
return;
|
|
490
513
|
}
|
|
491
514
|
if (tab === 1) {
|
|
492
515
|
if (input === "d") {
|
|
493
516
|
setView(0);
|
|
494
|
-
|
|
495
|
-
setExpanded(-1);
|
|
517
|
+
resetView();
|
|
496
518
|
return;
|
|
497
519
|
}
|
|
498
520
|
if (input === "w") {
|
|
499
521
|
setView(1);
|
|
500
|
-
|
|
501
|
-
setExpanded(-1);
|
|
522
|
+
resetView();
|
|
502
523
|
return;
|
|
503
524
|
}
|
|
504
525
|
if (input === "m") {
|
|
505
526
|
setView(2);
|
|
506
|
-
|
|
507
|
-
setExpanded(-1);
|
|
527
|
+
resetView();
|
|
508
528
|
return;
|
|
509
529
|
}
|
|
510
530
|
if (key.leftArrow) {
|
|
511
531
|
setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
|
|
512
|
-
|
|
513
|
-
setExpanded(-1);
|
|
532
|
+
resetView();
|
|
514
533
|
return;
|
|
515
534
|
}
|
|
516
535
|
if (key.rightArrow) {
|
|
517
536
|
setView((v) => (v + 1) % VIEWS.length);
|
|
518
|
-
|
|
519
|
-
|
|
537
|
+
resetView();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (input === "o") {
|
|
541
|
+
setSort((s) => (s + 1) % SORTS.length);
|
|
542
|
+
resetView();
|
|
520
543
|
return;
|
|
521
544
|
}
|
|
522
545
|
if (key.return) {
|
|
@@ -530,8 +553,7 @@ function App({ interval: cliInterval }) {
|
|
|
530
553
|
} else {
|
|
531
554
|
if (key.leftArrow || key.rightArrow) {
|
|
532
555
|
setTab((t) => (t + 1) % TABS.length);
|
|
533
|
-
|
|
534
|
-
setExpanded(-1);
|
|
556
|
+
resetView();
|
|
535
557
|
return;
|
|
536
558
|
}
|
|
537
559
|
}
|
|
@@ -551,10 +573,18 @@ function App({ interval: cliInterval }) {
|
|
|
551
573
|
setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
|
|
552
574
|
return;
|
|
553
575
|
}
|
|
554
|
-
}, { isActive:
|
|
576
|
+
}, { isActive: IS_TTY });
|
|
577
|
+
function updateConfig(fn) {
|
|
578
|
+
setConfig((prev) => {
|
|
579
|
+
const next = fn(prev ?? DEFAULT_CONFIG);
|
|
580
|
+
saveConfig(next);
|
|
581
|
+
return next;
|
|
582
|
+
});
|
|
583
|
+
}
|
|
555
584
|
if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
|
|
556
585
|
if (!dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
|
|
557
|
-
const
|
|
586
|
+
const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
|
|
587
|
+
const tableData = sortRows(rawTableData, sort);
|
|
558
588
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: rows, children: [
|
|
559
589
|
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
560
590
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
@@ -578,18 +608,21 @@ function App({ interval: cliInterval }) {
|
|
|
578
608
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
579
609
|
tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
|
|
580
610
|
tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
581
|
-
/* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
|
|
611
|
+
/* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort] }),
|
|
582
612
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
583
613
|
tableLoading && !table ? /* @__PURE__ */ jsx(Spinner, { label: "Loading 6 months of history" }) : /* @__PURE__ */ jsx(TableView, { rows: tableData, cursor, expanded, maxRows: rows - 12, wide: cols > 90 })
|
|
584
614
|
] })
|
|
585
615
|
] }),
|
|
586
|
-
(tab === 0 || showSettings) && /* @__PURE__ */
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
616
|
+
(tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, {})
|
|
617
|
+
] });
|
|
618
|
+
}
|
|
619
|
+
function Footer() {
|
|
620
|
+
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
621
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
|
|
622
|
+
/* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
|
|
623
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
|
|
624
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
|
|
625
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
|
|
593
626
|
] });
|
|
594
627
|
}
|
|
595
628
|
function TabBar({ tabs, active }) {
|
|
@@ -603,16 +636,34 @@ function TabBar({ tabs, active }) {
|
|
|
603
636
|
" "
|
|
604
637
|
] }) }, t)) });
|
|
605
638
|
}
|
|
606
|
-
function ViewBar({ views, active }) {
|
|
639
|
+
function ViewBar({ views, active, sort }) {
|
|
607
640
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
608
641
|
views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
609
642
|
"[",
|
|
610
643
|
v,
|
|
611
644
|
"]"
|
|
612
645
|
] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
|
|
613
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m
|
|
646
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m \xB7 sort: " }),
|
|
647
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: sort }),
|
|
648
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " o=cycle" })
|
|
614
649
|
] });
|
|
615
650
|
}
|
|
651
|
+
function sortRows(rows, sortIdx) {
|
|
652
|
+
if (rows.length === 0) return rows;
|
|
653
|
+
const sorted = [...rows];
|
|
654
|
+
switch (sortIdx) {
|
|
655
|
+
case 0:
|
|
656
|
+
return sorted.sort((a, b) => a.label.localeCompare(b.label));
|
|
657
|
+
case 1:
|
|
658
|
+
return sorted.sort((a, b) => b.label.localeCompare(a.label));
|
|
659
|
+
case 2:
|
|
660
|
+
return sorted.sort((a, b) => a.cost - b.cost);
|
|
661
|
+
case 3:
|
|
662
|
+
return sorted.sort((a, b) => b.cost - a.cost);
|
|
663
|
+
default:
|
|
664
|
+
return sorted;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
616
667
|
function SettingsView({ config: config2, cursor }) {
|
|
617
668
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
618
669
|
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
|
|
@@ -678,21 +729,21 @@ function DashboardView({ data, billing }) {
|
|
|
678
729
|
]
|
|
679
730
|
}
|
|
680
731
|
),
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
children:
|
|
694
|
-
|
|
695
|
-
|
|
732
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
733
|
+
/* @__PURE__ */ jsxs(
|
|
734
|
+
Box,
|
|
735
|
+
{
|
|
736
|
+
flexDirection: "column",
|
|
737
|
+
paddingLeft: 1,
|
|
738
|
+
borderStyle: "bold",
|
|
739
|
+
borderColor: billing?.error ? "red" : "yellow",
|
|
740
|
+
borderRight: false,
|
|
741
|
+
borderTop: false,
|
|
742
|
+
borderBottom: false,
|
|
743
|
+
children: [
|
|
744
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
|
|
745
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
746
|
+
billing?.error ? /* @__PURE__ */ jsx(Text, { color: "red", children: billing.error }) : billing?.session || billing?.weekly ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
696
747
|
billing.session && /* @__PURE__ */ jsx(LimitBar, { label: "Session", pct: billing.session.utilization, resets: billing.session.resetsAt }),
|
|
697
748
|
billing.weekly && /* @__PURE__ */ jsx(LimitBar, { label: "Weekly", pct: billing.weekly.utilization, resets: billing.weekly.resetsAt }),
|
|
698
749
|
billing.sonnet && /* @__PURE__ */ jsx(LimitBar, { label: "Sonnet", pct: billing.sonnet.utilization, resets: billing.sonnet.resetsAt }),
|
|
@@ -708,10 +759,10 @@ function DashboardView({ data, billing }) {
|
|
|
708
759
|
" limit"
|
|
709
760
|
] })
|
|
710
761
|
] })
|
|
711
|
-
]
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
762
|
+
] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fetching..." })
|
|
763
|
+
]
|
|
764
|
+
}
|
|
765
|
+
)
|
|
715
766
|
] });
|
|
716
767
|
}
|
|
717
768
|
function LimitBar({ label, pct, resets }) {
|
|
@@ -809,26 +860,18 @@ function TableView({ rows: allRows, cursor, expanded, maxRows, wide }) {
|
|
|
809
860
|
] }),
|
|
810
861
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
811
862
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
812
|
-
"\u2191\u2193 navigate \xB7 Enter detail \xB7
|
|
863
|
+
"\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
|
|
813
864
|
clampedCursor + 1,
|
|
814
865
|
"/",
|
|
815
866
|
allRows.length
|
|
816
867
|
] }),
|
|
817
868
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
818
|
-
/* @__PURE__ */
|
|
819
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
|
|
820
|
-
/* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
|
|
821
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
|
|
822
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
|
|
823
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
|
|
824
|
-
] })
|
|
869
|
+
/* @__PURE__ */ jsx(Footer, {})
|
|
825
870
|
] });
|
|
826
871
|
}
|
|
827
872
|
function RowDetail({ row, indent }) {
|
|
828
|
-
const pad = " ".repeat(indent);
|
|
829
873
|
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: indent, marginY: 0, children: row.breakdown.map((m, i) => {
|
|
830
|
-
const
|
|
831
|
-
const prefix = last ? "\u2514\u2500" : "\u251C\u2500";
|
|
874
|
+
const prefix = i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
832
875
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
833
876
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
834
877
|
prefix,
|
|
@@ -856,15 +899,14 @@ function RowDetail({ row, indent }) {
|
|
|
856
899
|
}) });
|
|
857
900
|
}
|
|
858
901
|
function Spinner({ label }) {
|
|
859
|
-
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
860
902
|
const [i, setI] = useState(0);
|
|
861
903
|
useEffect(() => {
|
|
862
|
-
const id = setInterval(() => setI((n) => (n + 1) %
|
|
904
|
+
const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
|
|
863
905
|
return () => clearInterval(id);
|
|
864
906
|
}, []);
|
|
865
907
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
866
908
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
867
|
-
|
|
909
|
+
SPINNER_FRAMES[i],
|
|
868
910
|
" "
|
|
869
911
|
] }),
|
|
870
912
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
|
|
@@ -873,9 +915,8 @@ function Spinner({ label }) {
|
|
|
873
915
|
function fmtLabel(label) {
|
|
874
916
|
if (label.length === 10 && label[4] === "-") return shortDate(label);
|
|
875
917
|
if (label.length === 7 && label[4] === "-") {
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
return `${months[Number(m)]} '${label.slice(2, 4)}`;
|
|
918
|
+
const m = label.slice(5, 7);
|
|
919
|
+
return `${MONTHS[Number(m)]} '${label.slice(2, 4)}`;
|
|
879
920
|
}
|
|
880
921
|
return shortDate(label);
|
|
881
922
|
}
|
|
@@ -908,5 +949,5 @@ var config = await loadConfig();
|
|
|
908
949
|
if (config.clearScreen && process.stdout.isTTY) {
|
|
909
950
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
910
951
|
}
|
|
911
|
-
var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
|
|
952
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx2(MouseProvider2, { children: /* @__PURE__ */ jsx2(App, { interval }) }));
|
|
912
953
|
await waitUntilExit();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokmon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Terminal dashboard for Claude Code usage and costs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
"dev": "tsx src/cli.tsx"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
+
"@zenobius/ink-mouse": "^1.0.3",
|
|
17
18
|
"ink": "^5.0.0",
|
|
18
19
|
"react": "^18.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
22
|
+
"@types/node": "^25.5.2",
|
|
21
23
|
"@types/react": "^18.0.0",
|
|
22
24
|
"tsup": "^8.0.0",
|
|
23
25
|
"tsx": "^4.0.0",
|