tokmon 0.6.1 → 0.7.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 +113 -92
- package/package.json +2 -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
|
@@ -15,12 +15,12 @@ function configDir() {
|
|
|
15
15
|
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
16
16
|
return join(xdg, "tokmon");
|
|
17
17
|
}
|
|
18
|
-
function
|
|
18
|
+
function configLocation() {
|
|
19
19
|
return join(configDir(), "config.json");
|
|
20
20
|
}
|
|
21
21
|
async function loadConfig() {
|
|
22
22
|
try {
|
|
23
|
-
const raw = await readFile(
|
|
23
|
+
const raw = await readFile(configLocation(), "utf-8");
|
|
24
24
|
const parsed = JSON.parse(raw);
|
|
25
25
|
return { ...DEFAULTS, ...parsed };
|
|
26
26
|
} catch {
|
|
@@ -30,14 +30,11 @@ async function loadConfig() {
|
|
|
30
30
|
async function saveConfig(config2) {
|
|
31
31
|
const dir = configDir();
|
|
32
32
|
await mkdir(dir, { recursive: true });
|
|
33
|
-
await writeFile(
|
|
34
|
-
}
|
|
35
|
-
function configLocation() {
|
|
36
|
-
return configPath();
|
|
33
|
+
await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
// src/app.tsx
|
|
40
|
-
import { useState, useEffect, useRef } from "react";
|
|
37
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
41
38
|
import { Box, Text, useInput, useStdout, useApp } from "ink";
|
|
42
39
|
|
|
43
40
|
// src/data.ts
|
|
@@ -82,7 +79,7 @@ function costOf(model, u) {
|
|
|
82
79
|
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
80
|
}
|
|
84
81
|
function shortModel(model) {
|
|
85
|
-
return model.replace("claude-", "").replace(
|
|
82
|
+
return model.replace("claude-", "").replace(/-\d{8}$/, "");
|
|
86
83
|
}
|
|
87
84
|
async function parseFile(path, since) {
|
|
88
85
|
const entries = [];
|
|
@@ -110,7 +107,7 @@ async function parseFile(path, since) {
|
|
|
110
107
|
return entries;
|
|
111
108
|
}
|
|
112
109
|
async function loadEntries(since) {
|
|
113
|
-
const
|
|
110
|
+
const chunks = [];
|
|
114
111
|
const seen = /* @__PURE__ */ new Set();
|
|
115
112
|
for (const dir of getClaudeDirs()) {
|
|
116
113
|
let listing;
|
|
@@ -129,17 +126,17 @@ async function loadEntries(since) {
|
|
|
129
126
|
if (s.mtimeMs < since) return;
|
|
130
127
|
const cached = fileCache.get(path);
|
|
131
128
|
if (cached && cached.mtimeMs === s.mtimeMs) {
|
|
132
|
-
|
|
129
|
+
chunks.push(cached.data);
|
|
133
130
|
return;
|
|
134
131
|
}
|
|
135
132
|
const data = await parseFile(path, since);
|
|
136
133
|
fileCache.set(path, { mtimeMs: s.mtimeMs, data });
|
|
137
|
-
|
|
134
|
+
chunks.push(data);
|
|
138
135
|
} catch {
|
|
139
136
|
}
|
|
140
137
|
}));
|
|
141
138
|
}
|
|
142
|
-
return
|
|
139
|
+
return chunks.flat();
|
|
143
140
|
}
|
|
144
141
|
function sum(entries) {
|
|
145
142
|
let cost = 0, tokens2 = 0;
|
|
@@ -159,29 +156,36 @@ function groupBy(entries, keyFn) {
|
|
|
159
156
|
}
|
|
160
157
|
const rows = [];
|
|
161
158
|
for (const [label, group] of groups) {
|
|
162
|
-
const models = [...new Set(group.map((e) => shortModel(e.model)))];
|
|
163
159
|
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
|
|
160
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
164
161
|
for (const e of group) {
|
|
165
162
|
input += e.input;
|
|
166
163
|
output += e.output;
|
|
167
164
|
cacheCreate += e.cacheCreate;
|
|
168
165
|
cacheRead += e.cacheRead;
|
|
169
166
|
cost += e.cost;
|
|
170
|
-
}
|
|
171
|
-
const byModel = /* @__PURE__ */ new Map();
|
|
172
|
-
for (const e of group) {
|
|
173
167
|
const name = shortModel(e.model);
|
|
174
|
-
const m = byModel.get(name)
|
|
175
|
-
m
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
168
|
+
const m = byModel.get(name);
|
|
169
|
+
if (m) {
|
|
170
|
+
m.input += e.input;
|
|
171
|
+
m.output += e.output;
|
|
172
|
+
m.cacheCreate += e.cacheCreate;
|
|
173
|
+
m.cacheRead += e.cacheRead;
|
|
174
|
+
m.cost += e.cost;
|
|
175
|
+
} else {
|
|
176
|
+
byModel.set(name, {
|
|
177
|
+
name,
|
|
178
|
+
input: e.input,
|
|
179
|
+
output: e.output,
|
|
180
|
+
cacheCreate: e.cacheCreate,
|
|
181
|
+
cacheRead: e.cacheRead,
|
|
182
|
+
cost: e.cost
|
|
183
|
+
});
|
|
184
|
+
}
|
|
181
185
|
}
|
|
182
186
|
rows.push({
|
|
183
187
|
label,
|
|
184
|
-
models:
|
|
188
|
+
models: [...byModel.keys()].sort(),
|
|
185
189
|
input,
|
|
186
190
|
output,
|
|
187
191
|
cacheCreate,
|
|
@@ -215,9 +219,14 @@ async function fetchDashboard() {
|
|
|
215
219
|
const todayEntries = entries.filter((e) => e.ts >= todayStart);
|
|
216
220
|
let burnRate = 0;
|
|
217
221
|
if (todayEntries.length > 0) {
|
|
218
|
-
|
|
222
|
+
let oldest = todayEntries[0].ts;
|
|
223
|
+
let totalCost = 0;
|
|
224
|
+
for (const e of todayEntries) {
|
|
225
|
+
if (e.ts < oldest) oldest = e.ts;
|
|
226
|
+
totalCost += e.cost;
|
|
227
|
+
}
|
|
219
228
|
const hrs = (now - oldest) / 36e5;
|
|
220
|
-
if (hrs > 0) burnRate =
|
|
229
|
+
if (hrs > 0) burnRate = totalCost / hrs;
|
|
221
230
|
}
|
|
222
231
|
return {
|
|
223
232
|
today: sum(todayEntries),
|
|
@@ -326,10 +335,10 @@ function time(date) {
|
|
|
326
335
|
second: "2-digit"
|
|
327
336
|
});
|
|
328
337
|
}
|
|
338
|
+
var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
329
339
|
function shortDate(iso) {
|
|
330
340
|
const [, m, d] = iso.split("-");
|
|
331
|
-
|
|
332
|
-
return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
|
|
341
|
+
return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
|
|
333
342
|
}
|
|
334
343
|
function col(s, w, align = "right") {
|
|
335
344
|
if (s.length > w) return s.slice(0, w - 1) + "~";
|
|
@@ -341,6 +350,11 @@ function col(s, w, align = "right") {
|
|
|
341
350
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
342
351
|
var TABS = ["Dashboard", "Table"];
|
|
343
352
|
var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
353
|
+
var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
|
|
354
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
355
|
+
var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
356
|
+
var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
|
|
357
|
+
var IS_TTY = process.stdin.isTTY === true;
|
|
344
358
|
function App({ interval: cliInterval }) {
|
|
345
359
|
const [dashboard, setDashboard] = useState(null);
|
|
346
360
|
const [billing, setBilling] = useState(null);
|
|
@@ -352,6 +366,7 @@ function App({ interval: cliInterval }) {
|
|
|
352
366
|
const [view, setView] = useState(0);
|
|
353
367
|
const [cursor, setCursor] = useState(0);
|
|
354
368
|
const [expanded, setExpanded] = useState(-1);
|
|
369
|
+
const [sort, setSort] = useState(0);
|
|
355
370
|
const [showSettings, setShowSettings] = useState(false);
|
|
356
371
|
const [config2, setConfig] = useState(null);
|
|
357
372
|
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
@@ -361,7 +376,7 @@ function App({ interval: cliInterval }) {
|
|
|
361
376
|
const rows = stdout?.rows ?? 24;
|
|
362
377
|
const cols = stdout?.columns ?? 80;
|
|
363
378
|
const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
|
|
364
|
-
const cfg = config2 ??
|
|
379
|
+
const cfg = config2 ?? DEFAULT_CONFIG;
|
|
365
380
|
useEffect(() => {
|
|
366
381
|
loadConfig().then((c) => {
|
|
367
382
|
if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
|
|
@@ -435,30 +450,21 @@ function App({ interval: cliInterval }) {
|
|
|
435
450
|
clearInterval(id);
|
|
436
451
|
};
|
|
437
452
|
}, [tab, interval2]);
|
|
438
|
-
const
|
|
453
|
+
const resetView = useCallback(() => {
|
|
454
|
+
setCursor(0);
|
|
455
|
+
setExpanded(-1);
|
|
456
|
+
}, []);
|
|
439
457
|
useInput((input, key) => {
|
|
440
458
|
if (showSettings) {
|
|
441
459
|
if (key.escape || input === "s") setShowSettings(false);
|
|
442
460
|
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
443
461
|
if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
|
|
444
462
|
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
|
-
});
|
|
463
|
+
if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
|
|
464
|
+
if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
|
|
455
465
|
}
|
|
456
466
|
if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
|
|
457
|
-
|
|
458
|
-
const n = { ...c, clearScreen: !c.clearScreen };
|
|
459
|
-
saveConfig(n);
|
|
460
|
-
return n;
|
|
461
|
-
});
|
|
467
|
+
updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
|
|
462
468
|
}
|
|
463
469
|
return;
|
|
464
470
|
}
|
|
@@ -472,51 +478,48 @@ function App({ interval: cliInterval }) {
|
|
|
472
478
|
}
|
|
473
479
|
if (key.tab) {
|
|
474
480
|
setTab((t) => (t + 1) % TABS.length);
|
|
475
|
-
|
|
476
|
-
setExpanded(-1);
|
|
481
|
+
resetView();
|
|
477
482
|
return;
|
|
478
483
|
}
|
|
479
484
|
if (input === "1") {
|
|
480
485
|
setTab(0);
|
|
481
|
-
|
|
482
|
-
setExpanded(-1);
|
|
486
|
+
resetView();
|
|
483
487
|
return;
|
|
484
488
|
}
|
|
485
489
|
if (input === "2") {
|
|
486
490
|
setTab(1);
|
|
487
|
-
|
|
488
|
-
setExpanded(-1);
|
|
491
|
+
resetView();
|
|
489
492
|
return;
|
|
490
493
|
}
|
|
491
494
|
if (tab === 1) {
|
|
492
495
|
if (input === "d") {
|
|
493
496
|
setView(0);
|
|
494
|
-
|
|
495
|
-
setExpanded(-1);
|
|
497
|
+
resetView();
|
|
496
498
|
return;
|
|
497
499
|
}
|
|
498
500
|
if (input === "w") {
|
|
499
501
|
setView(1);
|
|
500
|
-
|
|
501
|
-
setExpanded(-1);
|
|
502
|
+
resetView();
|
|
502
503
|
return;
|
|
503
504
|
}
|
|
504
505
|
if (input === "m") {
|
|
505
506
|
setView(2);
|
|
506
|
-
|
|
507
|
-
setExpanded(-1);
|
|
507
|
+
resetView();
|
|
508
508
|
return;
|
|
509
509
|
}
|
|
510
510
|
if (key.leftArrow) {
|
|
511
511
|
setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
|
|
512
|
-
|
|
513
|
-
setExpanded(-1);
|
|
512
|
+
resetView();
|
|
514
513
|
return;
|
|
515
514
|
}
|
|
516
515
|
if (key.rightArrow) {
|
|
517
516
|
setView((v) => (v + 1) % VIEWS.length);
|
|
518
|
-
|
|
519
|
-
|
|
517
|
+
resetView();
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (input === "o") {
|
|
521
|
+
setSort((s) => (s + 1) % SORTS.length);
|
|
522
|
+
resetView();
|
|
520
523
|
return;
|
|
521
524
|
}
|
|
522
525
|
if (key.return) {
|
|
@@ -530,8 +533,7 @@ function App({ interval: cliInterval }) {
|
|
|
530
533
|
} else {
|
|
531
534
|
if (key.leftArrow || key.rightArrow) {
|
|
532
535
|
setTab((t) => (t + 1) % TABS.length);
|
|
533
|
-
|
|
534
|
-
setExpanded(-1);
|
|
536
|
+
resetView();
|
|
535
537
|
return;
|
|
536
538
|
}
|
|
537
539
|
}
|
|
@@ -551,10 +553,18 @@ function App({ interval: cliInterval }) {
|
|
|
551
553
|
setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
|
|
552
554
|
return;
|
|
553
555
|
}
|
|
554
|
-
}, { isActive:
|
|
556
|
+
}, { isActive: IS_TTY });
|
|
557
|
+
function updateConfig(fn) {
|
|
558
|
+
setConfig((prev) => {
|
|
559
|
+
const next = fn(prev ?? DEFAULT_CONFIG);
|
|
560
|
+
saveConfig(next);
|
|
561
|
+
return next;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
555
564
|
if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
|
|
556
565
|
if (!dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
|
|
557
|
-
const
|
|
566
|
+
const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
|
|
567
|
+
const tableData = sortRows(rawTableData, sort);
|
|
558
568
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: rows, children: [
|
|
559
569
|
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
560
570
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
@@ -578,18 +588,21 @@ function App({ interval: cliInterval }) {
|
|
|
578
588
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
579
589
|
tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
|
|
580
590
|
tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
581
|
-
/* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
|
|
591
|
+
/* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort] }),
|
|
582
592
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
583
593
|
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
594
|
] })
|
|
585
595
|
] }),
|
|
586
|
-
(tab === 0 || showSettings) && /* @__PURE__ */
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
596
|
+
(tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, {})
|
|
597
|
+
] });
|
|
598
|
+
}
|
|
599
|
+
function Footer() {
|
|
600
|
+
return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
601
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
|
|
602
|
+
/* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
|
|
603
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
|
|
604
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
|
|
605
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
|
|
593
606
|
] });
|
|
594
607
|
}
|
|
595
608
|
function TabBar({ tabs, active }) {
|
|
@@ -603,16 +616,34 @@ function TabBar({ tabs, active }) {
|
|
|
603
616
|
" "
|
|
604
617
|
] }) }, t)) });
|
|
605
618
|
}
|
|
606
|
-
function ViewBar({ views, active }) {
|
|
619
|
+
function ViewBar({ views, active, sort }) {
|
|
607
620
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
608
621
|
views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
609
622
|
"[",
|
|
610
623
|
v,
|
|
611
624
|
"]"
|
|
612
625
|
] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
|
|
613
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m
|
|
626
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m \xB7 sort: " }),
|
|
627
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: sort }),
|
|
628
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " o=cycle" })
|
|
614
629
|
] });
|
|
615
630
|
}
|
|
631
|
+
function sortRows(rows, sortIdx) {
|
|
632
|
+
if (rows.length === 0) return rows;
|
|
633
|
+
const sorted = [...rows];
|
|
634
|
+
switch (sortIdx) {
|
|
635
|
+
case 0:
|
|
636
|
+
return sorted.sort((a, b) => a.label.localeCompare(b.label));
|
|
637
|
+
case 1:
|
|
638
|
+
return sorted.sort((a, b) => b.label.localeCompare(a.label));
|
|
639
|
+
case 2:
|
|
640
|
+
return sorted.sort((a, b) => a.cost - b.cost);
|
|
641
|
+
case 3:
|
|
642
|
+
return sorted.sort((a, b) => b.cost - a.cost);
|
|
643
|
+
default:
|
|
644
|
+
return sorted;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
616
647
|
function SettingsView({ config: config2, cursor }) {
|
|
617
648
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
618
649
|
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
|
|
@@ -809,26 +840,18 @@ function TableView({ rows: allRows, cursor, expanded, maxRows, wide }) {
|
|
|
809
840
|
] }),
|
|
810
841
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
811
842
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
812
|
-
"\u2191\u2193 navigate \xB7 Enter detail \xB7
|
|
843
|
+
"\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
|
|
813
844
|
clampedCursor + 1,
|
|
814
845
|
"/",
|
|
815
846
|
allRows.length
|
|
816
847
|
] }),
|
|
817
848
|
/* @__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
|
-
] })
|
|
849
|
+
/* @__PURE__ */ jsx(Footer, {})
|
|
825
850
|
] });
|
|
826
851
|
}
|
|
827
852
|
function RowDetail({ row, indent }) {
|
|
828
|
-
const pad = " ".repeat(indent);
|
|
829
853
|
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";
|
|
854
|
+
const prefix = i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
832
855
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
833
856
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
834
857
|
prefix,
|
|
@@ -856,15 +879,14 @@ function RowDetail({ row, indent }) {
|
|
|
856
879
|
}) });
|
|
857
880
|
}
|
|
858
881
|
function Spinner({ label }) {
|
|
859
|
-
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
860
882
|
const [i, setI] = useState(0);
|
|
861
883
|
useEffect(() => {
|
|
862
|
-
const id = setInterval(() => setI((n) => (n + 1) %
|
|
884
|
+
const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
|
|
863
885
|
return () => clearInterval(id);
|
|
864
886
|
}, []);
|
|
865
887
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
866
888
|
/* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
867
|
-
|
|
889
|
+
SPINNER_FRAMES[i],
|
|
868
890
|
" "
|
|
869
891
|
] }),
|
|
870
892
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
|
|
@@ -873,9 +895,8 @@ function Spinner({ label }) {
|
|
|
873
895
|
function fmtLabel(label) {
|
|
874
896
|
if (label.length === 10 && label[4] === "-") return shortDate(label);
|
|
875
897
|
if (label.length === 7 && label[4] === "-") {
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
return `${months[Number(m)]} '${label.slice(2, 4)}`;
|
|
898
|
+
const m = label.slice(5, 7);
|
|
899
|
+
return `${MONTHS[Number(m)]} '${label.slice(2, 4)}`;
|
|
879
900
|
}
|
|
880
901
|
return shortDate(label);
|
|
881
902
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokmon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Terminal dashboard for Claude Code usage and costs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"react": "^18.0.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.5.2",
|
|
21
22
|
"@types/react": "^18.0.0",
|
|
22
23
|
"tsup": "^8.0.0",
|
|
23
24
|
"tsx": "^4.0.0",
|