tokmon 0.1.0 → 0.3.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 +51 -17
- package/dist/cli.js +435 -77
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,52 +1,86 @@
|
|
|
1
1
|
# tokmon
|
|
2
2
|
|
|
3
|
-
Terminal dashboard for Claude Code usage and costs.
|
|
3
|
+
Terminal dashboard for Claude Code usage and costs. Tabbed interface with auto-refresh.
|
|
4
4
|
|
|
5
5
|
Built with [Ink](https://github.com/vadimdemedes/ink), TypeScript.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
◉ tokmon ·
|
|
8
|
+
◉ tokmon · 2s 01:17:09 AM
|
|
9
|
+
|
|
10
|
+
Dashboard Daily ←→ or 1-2
|
|
9
11
|
|
|
10
12
|
┃ Claude
|
|
11
13
|
┃
|
|
12
|
-
┃ Today $
|
|
13
|
-
┃ This Week $
|
|
14
|
-
┃ This Month $
|
|
14
|
+
┃ Today $166.10 252.7M tokens
|
|
15
|
+
┃ This Week $399.79 608.2M tokens
|
|
16
|
+
┃ This Month $1337.03 2.2B tokens
|
|
15
17
|
|
|
16
|
-
┃ Active Block
|
|
18
|
+
┃ Active Block 12m remaining
|
|
17
19
|
┃
|
|
18
|
-
┃
|
|
20
|
+
┃ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━─ 96%
|
|
19
21
|
┃
|
|
20
|
-
┃ $
|
|
22
|
+
┃ $314.37 spent · ~$328.01 proj · $65.60/hr
|
|
21
23
|
|
|
22
24
|
──────────────────────────────────────────────────
|
|
23
|
-
Total $
|
|
25
|
+
Total $1337.03
|
|
26
|
+
|
|
27
|
+
by David Ilie (davidilie.com)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx tokmon
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or with pnpm:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm dlx tokmon
|
|
24
40
|
```
|
|
25
41
|
|
|
26
|
-
|
|
42
|
+
### Global Install
|
|
27
43
|
|
|
28
44
|
```bash
|
|
29
45
|
npm install -g tokmon
|
|
30
46
|
```
|
|
31
47
|
|
|
32
|
-
|
|
48
|
+
Then just run `tokmon`. Press `Ctrl+C` to exit.
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
-i, --interval <seconds> Refresh interval in seconds (default: 2)
|
|
54
|
+
-h, --help Show help
|
|
55
|
+
```
|
|
33
56
|
|
|
34
57
|
```bash
|
|
35
|
-
tokmon
|
|
58
|
+
tokmon -i 5 # refresh every 5 seconds
|
|
36
59
|
```
|
|
37
60
|
|
|
38
|
-
|
|
61
|
+
## Views
|
|
39
62
|
|
|
40
|
-
|
|
63
|
+
Navigate between views with `←` `→` arrow keys, `Tab`, or number keys `1` `2`.
|
|
41
64
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
| View | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| **Dashboard** | Today / week / month cost summaries, active 5-hour block with burn rate |
|
|
68
|
+
| **Daily** | Per-day breakdown table with model, token, and cost columns (scrollable with `↑` `↓`) |
|
|
45
69
|
|
|
46
70
|
## How It Works
|
|
47
71
|
|
|
48
72
|
Reads Claude Code's JSONL session logs directly from `~/.claude/projects/`. Calculates costs using Claude model pricing (Opus, Sonnet, Haiku). Caches file reads by mtime so subsequent refreshes are near-instant.
|
|
49
73
|
|
|
74
|
+
Cross-platform: supports macOS, Linux, and Windows (`%APPDATA%`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`).
|
|
75
|
+
|
|
76
|
+
## CI/CD
|
|
77
|
+
|
|
78
|
+
Publishes to npm automatically via GitHub Actions when a version tag is pushed:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git tag v0.2.0 && git push --tags
|
|
82
|
+
```
|
|
83
|
+
|
|
50
84
|
## Requirements
|
|
51
85
|
|
|
52
86
|
- Node.js 20+
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { render } from "ink";
|
|
|
5
5
|
|
|
6
6
|
// src/app.tsx
|
|
7
7
|
import { useState, useEffect } from "react";
|
|
8
|
-
import { Box, Text } from "ink";
|
|
8
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
9
9
|
|
|
10
10
|
// src/data.ts
|
|
11
11
|
import { readdir, stat as fsStat } from "fs/promises";
|
|
@@ -36,6 +36,16 @@ function minutes(mins) {
|
|
|
36
36
|
const m = Math.round(mins % 60);
|
|
37
37
|
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
38
38
|
}
|
|
39
|
+
function shortDate(iso) {
|
|
40
|
+
const [, m, d] = iso.split("-");
|
|
41
|
+
const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
42
|
+
return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
|
|
43
|
+
}
|
|
44
|
+
function col(s, w, align = "right") {
|
|
45
|
+
if (s.length > w) return s.slice(0, w - 1) + "~";
|
|
46
|
+
const spaces = " ".repeat(w - s.length);
|
|
47
|
+
return align === "right" ? spaces + s : s + spaces;
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
// src/data.ts
|
|
41
51
|
var PRICING = {
|
|
@@ -73,6 +83,9 @@ function costOf(model, u) {
|
|
|
73
83
|
const p = priceFor(model);
|
|
74
84
|
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;
|
|
75
85
|
}
|
|
86
|
+
function shortModel(model) {
|
|
87
|
+
return model.replace("claude-", "").replace("-20251001", "").replace("-20250514", "").replace("-20251101", "").replace("-20250805", "");
|
|
88
|
+
}
|
|
76
89
|
async function parseFile(path, since) {
|
|
77
90
|
const entries = [];
|
|
78
91
|
const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
|
|
@@ -86,8 +99,12 @@ async function parseFile(path, since) {
|
|
|
86
99
|
const u = obj.message.usage;
|
|
87
100
|
entries.push({
|
|
88
101
|
ts,
|
|
102
|
+
model: obj.message.model ?? "unknown",
|
|
89
103
|
cost: costOf(obj.message.model ?? "", u),
|
|
90
|
-
|
|
104
|
+
input: u.input_tokens ?? 0,
|
|
105
|
+
output: u.output_tokens ?? 0,
|
|
106
|
+
cacheCreate: u.cache_creation_input_tokens ?? 0,
|
|
107
|
+
cacheRead: u.cache_read_input_tokens ?? 0
|
|
91
108
|
});
|
|
92
109
|
} catch {
|
|
93
110
|
}
|
|
@@ -130,18 +147,62 @@ function sum(entries) {
|
|
|
130
147
|
let cost = 0, tokens2 = 0;
|
|
131
148
|
for (const e of entries) {
|
|
132
149
|
cost += e.cost;
|
|
133
|
-
tokens2 += e.
|
|
150
|
+
tokens2 += e.input + e.output + e.cacheCreate + e.cacheRead;
|
|
134
151
|
}
|
|
135
152
|
return { cost, tokens: tokens2 };
|
|
136
153
|
}
|
|
137
|
-
|
|
154
|
+
function groupBy(entries, keyFn) {
|
|
155
|
+
const groups = /* @__PURE__ */ new Map();
|
|
156
|
+
for (const e of entries) {
|
|
157
|
+
const key = keyFn(e);
|
|
158
|
+
const arr = groups.get(key);
|
|
159
|
+
if (arr) arr.push(e);
|
|
160
|
+
else groups.set(key, [e]);
|
|
161
|
+
}
|
|
162
|
+
const rows = [];
|
|
163
|
+
for (const [label, group] of groups) {
|
|
164
|
+
const models = [...new Set(group.map((e) => shortModel(e.model)))];
|
|
165
|
+
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
|
|
166
|
+
for (const e of group) {
|
|
167
|
+
input += e.input;
|
|
168
|
+
output += e.output;
|
|
169
|
+
cacheCreate += e.cacheCreate;
|
|
170
|
+
cacheRead += e.cacheRead;
|
|
171
|
+
cost += e.cost;
|
|
172
|
+
}
|
|
173
|
+
rows.push({
|
|
174
|
+
label,
|
|
175
|
+
models: models.sort(),
|
|
176
|
+
input,
|
|
177
|
+
output,
|
|
178
|
+
cacheCreate,
|
|
179
|
+
cacheRead,
|
|
180
|
+
total: input + output + cacheCreate + cacheRead,
|
|
181
|
+
cost
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return rows.sort((a, b) => a.label.localeCompare(b.label));
|
|
185
|
+
}
|
|
186
|
+
function isoWeekLabel(ts) {
|
|
187
|
+
const d = new Date(ts);
|
|
188
|
+
const day = d.getDay();
|
|
189
|
+
const mondayOffset = day === 0 ? 6 : day - 1;
|
|
190
|
+
const monday = new Date(d);
|
|
191
|
+
monday.setDate(d.getDate() - mondayOffset);
|
|
192
|
+
return monday.toISOString().slice(0, 10);
|
|
193
|
+
}
|
|
194
|
+
function monthLabel(ts) {
|
|
195
|
+
return new Date(ts).toISOString().slice(0, 7);
|
|
196
|
+
}
|
|
197
|
+
async function fetchData() {
|
|
138
198
|
const now = Date.now();
|
|
139
199
|
const d = /* @__PURE__ */ new Date();
|
|
200
|
+
const lookback = new Date(d.getFullYear(), d.getMonth() - 6, 1).getTime();
|
|
140
201
|
const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
141
202
|
const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
142
203
|
const weekDay = d.getDay();
|
|
143
204
|
const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
|
|
144
|
-
const entries = await loadEntries(
|
|
205
|
+
const entries = await loadEntries(lookback);
|
|
145
206
|
const fiveHoursAgo = now - 5 * 36e5;
|
|
146
207
|
const blockEntries = entries.filter((e) => e.ts >= fiveHoursAgo);
|
|
147
208
|
let block = null;
|
|
@@ -154,26 +215,173 @@ async function fetchUsage() {
|
|
|
154
215
|
const percent = Math.min(100, (now - oldest) / (5 * 36e5) * 100);
|
|
155
216
|
block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
|
|
156
217
|
}
|
|
218
|
+
const daily = groupBy(entries, (e) => new Date(e.ts).toISOString().slice(0, 10));
|
|
219
|
+
const weekly = groupBy(entries, (e) => isoWeekLabel(e.ts));
|
|
220
|
+
const monthly = groupBy(entries, (e) => monthLabel(e.ts));
|
|
157
221
|
return {
|
|
158
222
|
today: sum(entries.filter((e) => e.ts >= todayStart)),
|
|
159
223
|
week: sum(entries.filter((e) => e.ts >= weekStart)),
|
|
160
|
-
month: sum(entries),
|
|
161
|
-
block
|
|
224
|
+
month: sum(entries.filter((e) => e.ts >= monthStart)),
|
|
225
|
+
block,
|
|
226
|
+
daily,
|
|
227
|
+
weekly,
|
|
228
|
+
monthly
|
|
162
229
|
};
|
|
163
230
|
}
|
|
164
231
|
|
|
232
|
+
// src/config.ts
|
|
233
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
234
|
+
import { join as join2 } from "path";
|
|
235
|
+
import { homedir as homedir2 } from "os";
|
|
236
|
+
var DEFAULTS = { interval: 2, clearScreen: true };
|
|
237
|
+
function configDir() {
|
|
238
|
+
if (process.platform === "win32") {
|
|
239
|
+
return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "tokmon");
|
|
240
|
+
}
|
|
241
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config");
|
|
242
|
+
return join2(xdg, "tokmon");
|
|
243
|
+
}
|
|
244
|
+
function configPath() {
|
|
245
|
+
return join2(configDir(), "config.json");
|
|
246
|
+
}
|
|
247
|
+
async function loadConfig() {
|
|
248
|
+
try {
|
|
249
|
+
const raw = await readFile(configPath(), "utf-8");
|
|
250
|
+
const parsed = JSON.parse(raw);
|
|
251
|
+
return { ...DEFAULTS, ...parsed };
|
|
252
|
+
} catch {
|
|
253
|
+
return { ...DEFAULTS };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
async function saveConfig(config) {
|
|
257
|
+
const dir = configDir();
|
|
258
|
+
await mkdir(dir, { recursive: true });
|
|
259
|
+
await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n");
|
|
260
|
+
}
|
|
261
|
+
function configLocation() {
|
|
262
|
+
return configPath();
|
|
263
|
+
}
|
|
264
|
+
|
|
165
265
|
// src/app.tsx
|
|
166
266
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
167
|
-
var
|
|
168
|
-
|
|
267
|
+
var TABS = ["Dashboard", "Table"];
|
|
268
|
+
var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
269
|
+
function App({ interval: cliInterval }) {
|
|
169
270
|
const [data, setData] = useState(null);
|
|
170
271
|
const [error, setError] = useState(null);
|
|
171
272
|
const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
|
|
273
|
+
const [tab, setTab] = useState(0);
|
|
274
|
+
const [view, setView] = useState(0);
|
|
275
|
+
const [scroll, setScroll] = useState(0);
|
|
276
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
277
|
+
const [config, setConfig] = useState(null);
|
|
278
|
+
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
279
|
+
const { stdout } = useStdout();
|
|
280
|
+
const rows = stdout?.rows ?? 24;
|
|
281
|
+
const cols = stdout?.columns ?? 80;
|
|
282
|
+
const interval2 = cliInterval ?? (config?.interval ?? 2) * 1e3;
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
loadConfig().then((c) => {
|
|
285
|
+
if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
|
|
286
|
+
setConfig(c);
|
|
287
|
+
if (c.clearScreen && stdout) stdout.write("\x1B[2J\x1B[H");
|
|
288
|
+
});
|
|
289
|
+
}, []);
|
|
290
|
+
const isTTY = process.stdin.isTTY === true;
|
|
291
|
+
const settingsItems = 2;
|
|
292
|
+
const cfg = config ?? { interval: 2, clearScreen: true };
|
|
293
|
+
useInput((input, key) => {
|
|
294
|
+
if (showSettings) {
|
|
295
|
+
if (key.escape || input === "s") setShowSettings(false);
|
|
296
|
+
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
297
|
+
if (key.downArrow) setSettingsCursor((c) => Math.min(settingsItems - 1, c + 1));
|
|
298
|
+
if (settingsCursor === 0) {
|
|
299
|
+
if (key.leftArrow) {
|
|
300
|
+
setConfig((c) => {
|
|
301
|
+
const next = { ...c, interval: Math.max(1, c.interval - 1) };
|
|
302
|
+
saveConfig(next);
|
|
303
|
+
return next;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (key.rightArrow) {
|
|
307
|
+
setConfig((c) => {
|
|
308
|
+
const next = { ...c, interval: c.interval + 1 };
|
|
309
|
+
saveConfig(next);
|
|
310
|
+
return next;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
|
|
315
|
+
setConfig((c) => {
|
|
316
|
+
const next = { ...c, clearScreen: !c.clearScreen };
|
|
317
|
+
saveConfig(next);
|
|
318
|
+
return next;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (input === "s") {
|
|
324
|
+
setShowSettings(true);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (key.tab) {
|
|
328
|
+
setTab((t) => (t + 1) % TABS.length);
|
|
329
|
+
setScroll(0);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (input === "1") {
|
|
333
|
+
setTab(0);
|
|
334
|
+
setScroll(0);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (input === "2") {
|
|
338
|
+
setTab(1);
|
|
339
|
+
setScroll(0);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (tab === 1) {
|
|
343
|
+
if (input === "d") {
|
|
344
|
+
setView(0);
|
|
345
|
+
setScroll(0);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (input === "w") {
|
|
349
|
+
setView(1);
|
|
350
|
+
setScroll(0);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (input === "m") {
|
|
354
|
+
setView(2);
|
|
355
|
+
setScroll(0);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (key.leftArrow) {
|
|
359
|
+
setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
|
|
360
|
+
setScroll(0);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (key.rightArrow) {
|
|
364
|
+
setView((v) => (v + 1) % VIEWS.length);
|
|
365
|
+
setScroll(0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
if (key.leftArrow || key.rightArrow) {
|
|
370
|
+
setTab((t) => (t + 1) % TABS.length);
|
|
371
|
+
setScroll(0);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (key.upArrow) setScroll((s) => Math.max(0, s - 1));
|
|
376
|
+
if (key.downArrow) setScroll((s) => s + 1);
|
|
377
|
+
if (key.pageDown) setScroll((s) => s + Math.max(1, rows - 12));
|
|
378
|
+
if (key.pageUp) setScroll((s) => Math.max(0, s - Math.max(1, rows - 12)));
|
|
379
|
+
}, { isActive: isTTY });
|
|
172
380
|
useEffect(() => {
|
|
173
381
|
let active = true;
|
|
174
382
|
const load = async () => {
|
|
175
383
|
try {
|
|
176
|
-
const result = await
|
|
384
|
+
const result = await fetchData();
|
|
177
385
|
if (active) {
|
|
178
386
|
setData(result);
|
|
179
387
|
setError(null);
|
|
@@ -184,62 +392,143 @@ function App() {
|
|
|
184
392
|
}
|
|
185
393
|
};
|
|
186
394
|
load();
|
|
187
|
-
const id = setInterval(load,
|
|
395
|
+
const id = setInterval(load, interval2);
|
|
188
396
|
return () => {
|
|
189
397
|
active = false;
|
|
190
398
|
clearInterval(id);
|
|
191
399
|
};
|
|
192
|
-
}, []);
|
|
400
|
+
}, [interval2]);
|
|
193
401
|
if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
|
|
194
402
|
if (!data) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
|
|
403
|
+
const tableData = [data.daily, data.weekly, data.monthly][view];
|
|
195
404
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
196
|
-
/* @__PURE__ */
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
405
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
406
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
407
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
|
|
408
|
+
"\u25C9",
|
|
409
|
+
" tokmon"
|
|
410
|
+
] }),
|
|
411
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
412
|
+
" \xB7 ",
|
|
413
|
+
cliInterval ? cliInterval / 1e3 : cfg.interval,
|
|
414
|
+
"s"
|
|
415
|
+
] })
|
|
416
|
+
] }),
|
|
417
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
|
|
418
|
+
] }),
|
|
419
|
+
showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
420
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
421
|
+
/* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab }),
|
|
422
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab s=settings" })
|
|
423
|
+
] }),
|
|
200
424
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
201
|
-
/* @__PURE__ */ jsx(
|
|
425
|
+
tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data }),
|
|
426
|
+
tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
427
|
+
/* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
|
|
428
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
429
|
+
/* @__PURE__ */ jsx(TableView, { rows: tableData, scroll, maxRows: rows - 12, wide: cols > 90 })
|
|
430
|
+
] })
|
|
202
431
|
] }),
|
|
203
|
-
/* @__PURE__ */
|
|
204
|
-
|
|
205
|
-
|
|
432
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
433
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
|
|
434
|
+
/* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
|
|
435
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
|
|
436
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
|
|
437
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ")" })
|
|
438
|
+
] })
|
|
206
439
|
] });
|
|
207
440
|
}
|
|
208
|
-
function
|
|
441
|
+
function TabBar({ tabs, active }) {
|
|
442
|
+
return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(Box, { marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
|
|
443
|
+
" ",
|
|
444
|
+
t,
|
|
445
|
+
" "
|
|
446
|
+
] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
447
|
+
" ",
|
|
448
|
+
t,
|
|
449
|
+
" "
|
|
450
|
+
] }) }, t)) });
|
|
451
|
+
}
|
|
452
|
+
function ViewBar({ views, active }) {
|
|
209
453
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
210
|
-
/* @__PURE__ */ jsxs(Text, { bold: true, color: "
|
|
211
|
-
"
|
|
212
|
-
|
|
454
|
+
views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
455
|
+
"[",
|
|
456
|
+
v,
|
|
457
|
+
"]"
|
|
458
|
+
] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
|
|
459
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m or \u2190\u2192" })
|
|
460
|
+
] });
|
|
461
|
+
}
|
|
462
|
+
function SettingsView({ config, cursor }) {
|
|
463
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
464
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
|
|
465
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
|
|
466
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
467
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
468
|
+
/* @__PURE__ */ jsxs(Text, { color: cursor === 0 ? "green" : void 0, children: [
|
|
469
|
+
cursor === 0 ? "\u25B8" : " ",
|
|
470
|
+
" "
|
|
471
|
+
] }),
|
|
472
|
+
/* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
|
|
473
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
474
|
+
"\u25C2",
|
|
475
|
+
" "
|
|
476
|
+
] }),
|
|
477
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
|
|
478
|
+
config.interval,
|
|
479
|
+
"s"
|
|
480
|
+
] }),
|
|
481
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
482
|
+
" ",
|
|
483
|
+
"\u25B8"
|
|
484
|
+
] })
|
|
213
485
|
] }),
|
|
214
|
-
/* @__PURE__ */ jsxs(
|
|
215
|
-
"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
486
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
487
|
+
/* @__PURE__ */ jsxs(Text, { color: cursor === 1 ? "green" : void 0, children: [
|
|
488
|
+
cursor === 1 ? "\u25B8" : " ",
|
|
489
|
+
" "
|
|
490
|
+
] }),
|
|
491
|
+
/* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
|
|
492
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: config.clearScreen ? "green" : "red", children: config.clearScreen ? "on" : "off" })
|
|
493
|
+
] }),
|
|
494
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
495
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
|
|
219
496
|
] });
|
|
220
497
|
}
|
|
221
|
-
function
|
|
222
|
-
return /* @__PURE__ */ jsxs(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
498
|
+
function DashboardView({ data }) {
|
|
499
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
500
|
+
/* @__PURE__ */ jsxs(
|
|
501
|
+
Box,
|
|
502
|
+
{
|
|
503
|
+
flexDirection: "column",
|
|
504
|
+
paddingLeft: 1,
|
|
505
|
+
borderStyle: "bold",
|
|
506
|
+
borderColor: "green",
|
|
507
|
+
borderRight: false,
|
|
508
|
+
borderTop: false,
|
|
509
|
+
borderBottom: false,
|
|
510
|
+
children: [
|
|
511
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
|
|
512
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
513
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
|
|
514
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
|
|
515
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month })
|
|
516
|
+
]
|
|
517
|
+
}
|
|
518
|
+
),
|
|
519
|
+
data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
520
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
521
|
+
/* @__PURE__ */ jsx(BlockView, { block: data.block })
|
|
522
|
+
] }),
|
|
523
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
524
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
|
|
525
|
+
/* @__PURE__ */ jsxs(Box, { width: 50, children: [
|
|
526
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
|
|
527
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(data.month.cost) })
|
|
528
|
+
] })
|
|
529
|
+
] });
|
|
241
530
|
}
|
|
242
|
-
function
|
|
531
|
+
function BlockView({ block }) {
|
|
243
532
|
return /* @__PURE__ */ jsxs(
|
|
244
533
|
Box,
|
|
245
534
|
{
|
|
@@ -261,7 +550,7 @@ function BlockSection({ block }) {
|
|
|
261
550
|
] }),
|
|
262
551
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
263
552
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
264
|
-
/* @__PURE__ */ jsx(
|
|
553
|
+
/* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
|
|
265
554
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
266
555
|
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
267
556
|
Math.round(block.percent),
|
|
@@ -270,12 +559,9 @@ function BlockSection({ block }) {
|
|
|
270
559
|
] }),
|
|
271
560
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
272
561
|
/* @__PURE__ */ jsx(Text, { color: "yellow", children: currency(block.spent) }),
|
|
273
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent" }),
|
|
274
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
275
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "~" }),
|
|
562
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
|
|
276
563
|
/* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
|
|
277
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj" }),
|
|
278
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
564
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
|
|
279
565
|
/* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
|
|
280
566
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
|
|
281
567
|
] })
|
|
@@ -283,7 +569,7 @@ function BlockSection({ block }) {
|
|
|
283
569
|
}
|
|
284
570
|
);
|
|
285
571
|
}
|
|
286
|
-
function
|
|
572
|
+
function SummaryRow({ label, summary }) {
|
|
287
573
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
288
574
|
/* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
|
|
289
575
|
/* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
|
|
@@ -293,36 +579,108 @@ function Row({ label, summary }) {
|
|
|
293
579
|
] }) })
|
|
294
580
|
] });
|
|
295
581
|
}
|
|
296
|
-
function
|
|
582
|
+
function ProgressBar({ percent, width = 36 }) {
|
|
297
583
|
const filled = Math.round(percent / 100 * width);
|
|
298
584
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
299
585
|
/* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
|
|
300
586
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
|
|
301
587
|
] });
|
|
302
588
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
589
|
+
function TableView({ rows: allRows, scroll, maxRows, wide }) {
|
|
590
|
+
const W = wide ? { label: 10, models: 18, input: 8, output: 8, cc: 8, cr: 9, total: 9, cost: 10 } : { label: 8, models: 14, input: 7, output: 7, cc: 7, cr: 8, total: 0, cost: 9 };
|
|
591
|
+
const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
|
|
592
|
+
const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
|
|
593
|
+
for (const r of allRows) {
|
|
594
|
+
totals.input += r.input;
|
|
595
|
+
totals.output += r.output;
|
|
596
|
+
totals.cacheCreate += r.cacheCreate;
|
|
597
|
+
totals.cacheRead += r.cacheRead;
|
|
598
|
+
totals.cost += r.cost;
|
|
599
|
+
}
|
|
600
|
+
const clampedScroll = Math.min(scroll, Math.max(0, allRows.length - maxRows));
|
|
601
|
+
const visible = allRows.slice(clampedScroll, clampedScroll + maxRows);
|
|
602
|
+
const more = allRows.length - clampedScroll - maxRows;
|
|
603
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
604
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
605
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.label, "left") }),
|
|
606
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
|
|
607
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
|
|
608
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
|
|
609
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("CchCrt", W.cc) }),
|
|
610
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("CchRd", W.cr) }),
|
|
611
|
+
W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, children: col("Total", W.total) }),
|
|
612
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
|
|
314
613
|
] }),
|
|
315
|
-
/* @__PURE__ */
|
|
316
|
-
|
|
317
|
-
/* @__PURE__ */ jsx(Text, {
|
|
318
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "
|
|
319
|
-
/* @__PURE__ */ jsx(Text, {
|
|
320
|
-
/* @__PURE__ */ jsx(Text, {
|
|
321
|
-
|
|
614
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
|
|
615
|
+
visible.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
616
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: col(fmtLabel(r.label), W.label, "left") }),
|
|
617
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: col(r.models.join(", "), W.models, "left") }),
|
|
618
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
|
|
619
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
|
|
620
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
|
|
621
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheRead), W.cr) }),
|
|
622
|
+
W.total > 0 && /* @__PURE__ */ jsx(Text, { children: col(tokens(r.total), W.total) }),
|
|
623
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(currency(r.cost), W.cost) })
|
|
624
|
+
] }, r.label)),
|
|
625
|
+
more > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
626
|
+
" \u2193 ",
|
|
627
|
+
more,
|
|
628
|
+
" more"
|
|
629
|
+
] }),
|
|
630
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
|
|
631
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
632
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.label, "left") }),
|
|
633
|
+
/* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
|
|
634
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
|
|
635
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
|
|
636
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
|
|
637
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
|
|
638
|
+
W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
|
|
639
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
|
|
640
|
+
] }),
|
|
641
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
642
|
+
"\u2191\u2193 PgUp/Dn scroll \xB7 ",
|
|
643
|
+
allRows.length,
|
|
644
|
+
" rows \xB7 ",
|
|
645
|
+
clampedScroll + 1,
|
|
646
|
+
"-",
|
|
647
|
+
Math.min(clampedScroll + maxRows, allRows.length)
|
|
648
|
+
] }) })
|
|
322
649
|
] });
|
|
323
650
|
}
|
|
651
|
+
function fmtLabel(label) {
|
|
652
|
+
if (label.length === 10 && label[4] === "-") return shortDate(label);
|
|
653
|
+
if (label.length === 7 && label[4] === "-") {
|
|
654
|
+
const [, m] = label.split("-");
|
|
655
|
+
const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
656
|
+
return `${months[Number(m)]} '${label.slice(2, 4)}`;
|
|
657
|
+
}
|
|
658
|
+
return shortDate(label);
|
|
659
|
+
}
|
|
324
660
|
|
|
325
661
|
// src/cli.tsx
|
|
326
662
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
327
|
-
var
|
|
663
|
+
var args = process.argv.slice(2);
|
|
664
|
+
var interval;
|
|
665
|
+
for (let i = 0; i < args.length; i++) {
|
|
666
|
+
if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
|
|
667
|
+
interval = Math.max(500, Number(args[i + 1]) * 1e3);
|
|
668
|
+
i++;
|
|
669
|
+
}
|
|
670
|
+
if (args[i] === "--help" || args[i] === "-h") {
|
|
671
|
+
console.log("tokmon - Terminal dashboard for Claude Code usage\n");
|
|
672
|
+
console.log("Usage: tokmon [options]\n");
|
|
673
|
+
console.log("Options:");
|
|
674
|
+
console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
|
|
675
|
+
console.log(" -h, --help Show this help\n");
|
|
676
|
+
console.log("Keybindings:");
|
|
677
|
+
console.log(" Tab / \u2190\u2192 Switch views");
|
|
678
|
+
console.log(" \u2191\u2193 Scroll table");
|
|
679
|
+
console.log(" 1-2 Jump to view");
|
|
680
|
+
console.log(" s Settings");
|
|
681
|
+
console.log(" Ctrl+C Quit");
|
|
682
|
+
process.exit(0);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
|
|
328
686
|
await waitUntilExit();
|