tokmon 0.1.0 → 0.2.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 +338 -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 });
|
|
@@ -84,10 +97,15 @@ async function parseFile(path, since) {
|
|
|
84
97
|
const ts = new Date(obj.timestamp ?? 0).getTime();
|
|
85
98
|
if (ts < since) continue;
|
|
86
99
|
const u = obj.message.usage;
|
|
100
|
+
const model = obj.message.model ?? "unknown";
|
|
87
101
|
entries.push({
|
|
88
102
|
ts,
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
model,
|
|
104
|
+
cost: costOf(model, u),
|
|
105
|
+
input: u.input_tokens ?? 0,
|
|
106
|
+
output: u.output_tokens ?? 0,
|
|
107
|
+
cacheCreate: u.cache_creation_input_tokens ?? 0,
|
|
108
|
+
cacheRead: u.cache_read_input_tokens ?? 0
|
|
91
109
|
});
|
|
92
110
|
} catch {
|
|
93
111
|
}
|
|
@@ -130,11 +148,43 @@ function sum(entries) {
|
|
|
130
148
|
let cost = 0, tokens2 = 0;
|
|
131
149
|
for (const e of entries) {
|
|
132
150
|
cost += e.cost;
|
|
133
|
-
tokens2 += e.
|
|
151
|
+
tokens2 += e.input + e.output + e.cacheCreate + e.cacheRead;
|
|
134
152
|
}
|
|
135
153
|
return { cost, tokens: tokens2 };
|
|
136
154
|
}
|
|
137
|
-
|
|
155
|
+
function buildDaily(entries) {
|
|
156
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const e of entries) {
|
|
158
|
+
const date = new Date(e.ts).toISOString().slice(0, 10);
|
|
159
|
+
const arr = byDate.get(date);
|
|
160
|
+
if (arr) arr.push(e);
|
|
161
|
+
else byDate.set(date, [e]);
|
|
162
|
+
}
|
|
163
|
+
const rows = [];
|
|
164
|
+
for (const [date, dayEntries] of byDate) {
|
|
165
|
+
const models = [...new Set(dayEntries.map((e) => shortModel(e.model)))];
|
|
166
|
+
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
|
|
167
|
+
for (const e of dayEntries) {
|
|
168
|
+
input += e.input;
|
|
169
|
+
output += e.output;
|
|
170
|
+
cacheCreate += e.cacheCreate;
|
|
171
|
+
cacheRead += e.cacheRead;
|
|
172
|
+
cost += e.cost;
|
|
173
|
+
}
|
|
174
|
+
rows.push({
|
|
175
|
+
date,
|
|
176
|
+
models: models.sort(),
|
|
177
|
+
input,
|
|
178
|
+
output,
|
|
179
|
+
cacheCreate,
|
|
180
|
+
cacheRead,
|
|
181
|
+
total: input + output + cacheCreate + cacheRead,
|
|
182
|
+
cost
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return rows.sort((a, b) => a.date.localeCompare(b.date));
|
|
186
|
+
}
|
|
187
|
+
async function fetchData() {
|
|
138
188
|
const now = Date.now();
|
|
139
189
|
const d = /* @__PURE__ */ new Date();
|
|
140
190
|
const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
|
|
@@ -158,22 +208,115 @@ async function fetchUsage() {
|
|
|
158
208
|
today: sum(entries.filter((e) => e.ts >= todayStart)),
|
|
159
209
|
week: sum(entries.filter((e) => e.ts >= weekStart)),
|
|
160
210
|
month: sum(entries),
|
|
161
|
-
block
|
|
211
|
+
block,
|
|
212
|
+
daily: buildDaily(entries)
|
|
162
213
|
};
|
|
163
214
|
}
|
|
164
215
|
|
|
216
|
+
// src/config.ts
|
|
217
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
218
|
+
import { join as join2 } from "path";
|
|
219
|
+
import { homedir as homedir2 } from "os";
|
|
220
|
+
var DEFAULTS = { interval: 2 };
|
|
221
|
+
function configDir() {
|
|
222
|
+
if (process.platform === "win32") {
|
|
223
|
+
return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "tokmon");
|
|
224
|
+
}
|
|
225
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config");
|
|
226
|
+
return join2(xdg, "tokmon");
|
|
227
|
+
}
|
|
228
|
+
function configPath() {
|
|
229
|
+
return join2(configDir(), "config.json");
|
|
230
|
+
}
|
|
231
|
+
async function loadConfig() {
|
|
232
|
+
try {
|
|
233
|
+
const raw = await readFile(configPath(), "utf-8");
|
|
234
|
+
const parsed = JSON.parse(raw);
|
|
235
|
+
return { ...DEFAULTS, ...parsed };
|
|
236
|
+
} catch {
|
|
237
|
+
return { ...DEFAULTS };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function saveConfig(config) {
|
|
241
|
+
const dir = configDir();
|
|
242
|
+
await mkdir(dir, { recursive: true });
|
|
243
|
+
await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n");
|
|
244
|
+
}
|
|
245
|
+
function configLocation() {
|
|
246
|
+
return configPath();
|
|
247
|
+
}
|
|
248
|
+
|
|
165
249
|
// src/app.tsx
|
|
166
250
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
167
|
-
var
|
|
168
|
-
function App() {
|
|
251
|
+
var TABS = ["Dashboard", "Daily"];
|
|
252
|
+
function App({ interval: initialInterval }) {
|
|
169
253
|
const [data, setData] = useState(null);
|
|
170
254
|
const [error, setError] = useState(null);
|
|
171
255
|
const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
|
|
256
|
+
const [tab, setTab] = useState(0);
|
|
257
|
+
const [scroll, setScroll] = useState(0);
|
|
258
|
+
const [showSettings, setShowSettings] = useState(false);
|
|
259
|
+
const [config, setConfig] = useState({ interval: (initialInterval ?? 2e3) / 1e3 });
|
|
260
|
+
const [settingsCursor, setSettingsCursor] = useState(0);
|
|
261
|
+
const { stdout } = useStdout();
|
|
262
|
+
const rows = stdout?.rows ?? 24;
|
|
263
|
+
const interval2 = config.interval * 1e3;
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
loadConfig().then((c) => {
|
|
266
|
+
if (!initialInterval) setConfig(c);
|
|
267
|
+
else setConfig({ ...c, interval: initialInterval / 1e3 });
|
|
268
|
+
});
|
|
269
|
+
}, []);
|
|
270
|
+
const isTTY = process.stdin.isTTY === true;
|
|
271
|
+
useInput((input, key) => {
|
|
272
|
+
if (showSettings) {
|
|
273
|
+
if (key.escape || input === "s") setShowSettings(false);
|
|
274
|
+
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
275
|
+
if (key.downArrow) setSettingsCursor((c) => Math.min(0, c + 1));
|
|
276
|
+
if (key.leftArrow && settingsCursor === 0) {
|
|
277
|
+
setConfig((c) => {
|
|
278
|
+
const next = { ...c, interval: Math.max(1, c.interval - 1) };
|
|
279
|
+
saveConfig(next);
|
|
280
|
+
return next;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (key.rightArrow && settingsCursor === 0) {
|
|
284
|
+
setConfig((c) => {
|
|
285
|
+
const next = { ...c, interval: c.interval + 1 };
|
|
286
|
+
saveConfig(next);
|
|
287
|
+
return next;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (input === "s") {
|
|
293
|
+
setShowSettings(true);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (key.tab || key.rightArrow) {
|
|
297
|
+
setTab((t) => (t + 1) % TABS.length);
|
|
298
|
+
setScroll(0);
|
|
299
|
+
}
|
|
300
|
+
if (key.leftArrow) {
|
|
301
|
+
setTab((t) => (t - 1 + TABS.length) % TABS.length);
|
|
302
|
+
setScroll(0);
|
|
303
|
+
}
|
|
304
|
+
if (key.upArrow) setScroll((s) => Math.max(0, s - 1));
|
|
305
|
+
if (key.downArrow) setScroll((s) => s + 1);
|
|
306
|
+
if (input === "1") {
|
|
307
|
+
setTab(0);
|
|
308
|
+
setScroll(0);
|
|
309
|
+
}
|
|
310
|
+
if (input === "2") {
|
|
311
|
+
setTab(1);
|
|
312
|
+
setScroll(0);
|
|
313
|
+
}
|
|
314
|
+
}, { isActive: isTTY });
|
|
172
315
|
useEffect(() => {
|
|
173
316
|
let active = true;
|
|
174
317
|
const load = async () => {
|
|
175
318
|
try {
|
|
176
|
-
const result = await
|
|
319
|
+
const result = await fetchData();
|
|
177
320
|
if (active) {
|
|
178
321
|
setData(result);
|
|
179
322
|
setError(null);
|
|
@@ -184,62 +327,124 @@ function App() {
|
|
|
184
327
|
}
|
|
185
328
|
};
|
|
186
329
|
load();
|
|
187
|
-
const id = setInterval(load,
|
|
330
|
+
const id = setInterval(load, interval2);
|
|
188
331
|
return () => {
|
|
189
332
|
active = false;
|
|
190
333
|
clearInterval(id);
|
|
191
334
|
};
|
|
192
|
-
}, []);
|
|
335
|
+
}, [interval2]);
|
|
193
336
|
if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
|
|
194
337
|
if (!data) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
|
|
195
338
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
196
|
-
/* @__PURE__ */
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
339
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
340
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
341
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
|
|
342
|
+
"\u25C9",
|
|
343
|
+
" tokmon"
|
|
344
|
+
] }),
|
|
345
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
346
|
+
" \xB7 ",
|
|
347
|
+
config.interval,
|
|
348
|
+
"s"
|
|
349
|
+
] })
|
|
350
|
+
] }),
|
|
351
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
|
|
352
|
+
] }),
|
|
353
|
+
showSettings ? /* @__PURE__ */ jsx(SettingsView, { config, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
354
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
355
|
+
/* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab }),
|
|
356
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192 s=settings" })
|
|
357
|
+
] }),
|
|
200
358
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
201
|
-
/* @__PURE__ */ jsx(
|
|
359
|
+
TABS[tab] === "Dashboard" && /* @__PURE__ */ jsx(DashboardView, { data }),
|
|
360
|
+
TABS[tab] === "Daily" && /* @__PURE__ */ jsx(DailyView, { daily: data.daily, scroll, maxRows: rows - 10 })
|
|
202
361
|
] }),
|
|
203
|
-
/* @__PURE__ */
|
|
204
|
-
|
|
205
|
-
|
|
362
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
363
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
|
|
364
|
+
/* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
|
|
365
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
|
|
366
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
|
|
367
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: ")" })
|
|
368
|
+
] })
|
|
206
369
|
] });
|
|
207
370
|
}
|
|
208
|
-
function
|
|
209
|
-
return /* @__PURE__ */
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
371
|
+
function TabBar({ tabs, active }) {
|
|
372
|
+
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: [
|
|
373
|
+
" ",
|
|
374
|
+
t,
|
|
375
|
+
" "
|
|
376
|
+
] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
377
|
+
" ",
|
|
378
|
+
t,
|
|
379
|
+
" "
|
|
380
|
+
] }) }, t)) });
|
|
381
|
+
}
|
|
382
|
+
function SettingsView({ config, cursor }) {
|
|
383
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
384
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
|
|
214
385
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
215
|
-
"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
386
|
+
"Saved to ",
|
|
387
|
+
configLocation()
|
|
388
|
+
] }),
|
|
389
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
390
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
391
|
+
cursor === 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
392
|
+
"\u25B8",
|
|
393
|
+
" "
|
|
394
|
+
] }) : /* @__PURE__ */ jsx(Text, { children: " " }),
|
|
395
|
+
/* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
|
|
396
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
397
|
+
"\u25C2",
|
|
398
|
+
" "
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
|
|
401
|
+
config.interval,
|
|
402
|
+
"s"
|
|
403
|
+
] }),
|
|
404
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
405
|
+
" ",
|
|
406
|
+
"\u25B8"
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " (\u2190\u2192 to adjust)" })
|
|
409
|
+
] }),
|
|
410
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
411
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press s or Esc to close" })
|
|
219
412
|
] });
|
|
220
413
|
}
|
|
221
|
-
function
|
|
222
|
-
return /* @__PURE__ */ jsxs(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
414
|
+
function DashboardView({ data }) {
|
|
415
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
416
|
+
/* @__PURE__ */ jsxs(
|
|
417
|
+
Box,
|
|
418
|
+
{
|
|
419
|
+
flexDirection: "column",
|
|
420
|
+
paddingLeft: 1,
|
|
421
|
+
borderStyle: "bold",
|
|
422
|
+
borderColor: "green",
|
|
423
|
+
borderRight: false,
|
|
424
|
+
borderTop: false,
|
|
425
|
+
borderBottom: false,
|
|
426
|
+
children: [
|
|
427
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
|
|
428
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
429
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
|
|
430
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
|
|
431
|
+
/* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month })
|
|
432
|
+
]
|
|
433
|
+
}
|
|
434
|
+
),
|
|
435
|
+
data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
436
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
437
|
+
/* @__PURE__ */ jsx(BlockView, { block: data.block })
|
|
438
|
+
] }),
|
|
439
|
+
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
440
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
|
|
441
|
+
/* @__PURE__ */ jsx(Box, { justifyContent: "space-between", width: 50, children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
442
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
|
|
443
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(data.month.cost) })
|
|
444
|
+
] }) })
|
|
445
|
+
] });
|
|
241
446
|
}
|
|
242
|
-
function
|
|
447
|
+
function BlockView({ block }) {
|
|
243
448
|
return /* @__PURE__ */ jsxs(
|
|
244
449
|
Box,
|
|
245
450
|
{
|
|
@@ -261,7 +466,7 @@ function BlockSection({ block }) {
|
|
|
261
466
|
] }),
|
|
262
467
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
263
468
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
264
|
-
/* @__PURE__ */ jsx(
|
|
469
|
+
/* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
|
|
265
470
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
266
471
|
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
267
472
|
Math.round(block.percent),
|
|
@@ -270,12 +475,9 @@ function BlockSection({ block }) {
|
|
|
270
475
|
] }),
|
|
271
476
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
272
477
|
/* @__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: "~" }),
|
|
478
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
|
|
276
479
|
/* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
|
|
277
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj" }),
|
|
278
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
|
|
480
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
|
|
279
481
|
/* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
|
|
280
482
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
|
|
281
483
|
] })
|
|
@@ -283,7 +485,7 @@ function BlockSection({ block }) {
|
|
|
283
485
|
}
|
|
284
486
|
);
|
|
285
487
|
}
|
|
286
|
-
function
|
|
488
|
+
function SummaryRow({ label, summary }) {
|
|
287
489
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
288
490
|
/* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
|
|
289
491
|
/* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
|
|
@@ -293,36 +495,95 @@ function Row({ label, summary }) {
|
|
|
293
495
|
] }) })
|
|
294
496
|
] });
|
|
295
497
|
}
|
|
296
|
-
function
|
|
498
|
+
function ProgressBar({ percent, width = 36 }) {
|
|
297
499
|
const filled = Math.round(percent / 100 * width);
|
|
298
500
|
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
299
501
|
/* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
|
|
300
502
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
|
|
301
503
|
] });
|
|
302
504
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
505
|
+
function DailyView({ daily, scroll, maxRows }) {
|
|
506
|
+
const W = { date: 7, models: 16, input: 8, output: 8, cc: 8, cr: 8, cost: 10 };
|
|
507
|
+
const lineW = W.date + W.models + W.input + W.output + W.cc + W.cr + W.cost;
|
|
508
|
+
const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
|
|
509
|
+
for (const r of daily) {
|
|
510
|
+
totals.input += r.input;
|
|
511
|
+
totals.output += r.output;
|
|
512
|
+
totals.cacheCreate += r.cacheCreate;
|
|
513
|
+
totals.cacheRead += r.cacheRead;
|
|
514
|
+
totals.cost += r.cost;
|
|
515
|
+
}
|
|
516
|
+
const visible = daily.slice(scroll, scroll + maxRows);
|
|
517
|
+
const more = daily.length - scroll - maxRows;
|
|
518
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
519
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
520
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.date, "left") }),
|
|
521
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
|
|
522
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
|
|
523
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
|
|
524
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("CchCrt", W.cc) }),
|
|
525
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("CchRd", W.cr) }),
|
|
526
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
|
|
314
527
|
] }),
|
|
315
|
-
/* @__PURE__ */
|
|
316
|
-
|
|
317
|
-
/* @__PURE__ */ jsx(Text, {
|
|
318
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "
|
|
319
|
-
/* @__PURE__ */ jsx(Text, {
|
|
320
|
-
/* @__PURE__ */ jsx(Text, {
|
|
321
|
-
|
|
528
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
|
|
529
|
+
visible.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
530
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: col(shortDate(r.date), W.date, "left") }),
|
|
531
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: col(r.models.join(", "), W.models, "left") }),
|
|
532
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
|
|
533
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
|
|
534
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
|
|
535
|
+
/* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheRead), W.cr) }),
|
|
536
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(currency(r.cost), W.cost) })
|
|
537
|
+
] }, r.date)),
|
|
538
|
+
more > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
539
|
+
" \u2193 ",
|
|
540
|
+
more,
|
|
541
|
+
" more"
|
|
542
|
+
] }),
|
|
543
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
|
|
544
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
545
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.date, "left") }),
|
|
546
|
+
/* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
|
|
547
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
|
|
548
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
|
|
549
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
|
|
550
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
|
|
551
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
|
|
552
|
+
] }),
|
|
553
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
554
|
+
"\u2191\u2193 scroll \xB7 ",
|
|
555
|
+
daily.length,
|
|
556
|
+
" days \xB7 ",
|
|
557
|
+
scroll + 1,
|
|
558
|
+
"-",
|
|
559
|
+
Math.min(scroll + maxRows, daily.length)
|
|
560
|
+
] }) })
|
|
322
561
|
] });
|
|
323
562
|
}
|
|
324
563
|
|
|
325
564
|
// src/cli.tsx
|
|
326
565
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
327
|
-
var
|
|
566
|
+
var args = process.argv.slice(2);
|
|
567
|
+
var interval;
|
|
568
|
+
for (let i = 0; i < args.length; i++) {
|
|
569
|
+
if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
|
|
570
|
+
interval = Math.max(500, Number(args[i + 1]) * 1e3);
|
|
571
|
+
i++;
|
|
572
|
+
}
|
|
573
|
+
if (args[i] === "--help" || args[i] === "-h") {
|
|
574
|
+
console.log("tokmon - Terminal dashboard for Claude Code usage\n");
|
|
575
|
+
console.log("Usage: tokmon [options]\n");
|
|
576
|
+
console.log("Options:");
|
|
577
|
+
console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
|
|
578
|
+
console.log(" -h, --help Show this help\n");
|
|
579
|
+
console.log("Keybindings:");
|
|
580
|
+
console.log(" Tab / \u2190\u2192 Switch views");
|
|
581
|
+
console.log(" \u2191\u2193 Scroll table");
|
|
582
|
+
console.log(" 1-2 Jump to view");
|
|
583
|
+
console.log(" s Settings");
|
|
584
|
+
console.log(" Ctrl+C Quit");
|
|
585
|
+
process.exit(0);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
|
|
328
589
|
await waitUntilExit();
|