@notenkidev/claude-token-dashboard 0.1.6 → 0.1.9
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 +18 -9
- package/components/ProjectTable.tsx +50 -10
- package/components/SummaryCards.tsx +13 -3
- package/lib/pricing.ts +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
> Visualize your [Claude Code](https://claude.ai/code) token usage by project and date — built with Next.js + Tailwind.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
## Quick Start (No Install)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
|
|
12
|
+
npm install -g @notenkidev/claude-token-dashboard
|
|
13
|
+
claude-token-dashboard
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
Open http://localhost:3000
|
|
@@ -18,26 +19,34 @@ Open http://localhost:3000
|
|
|
18
19
|
|
|
19
20
|
| Section | Details |
|
|
20
21
|
|---|---|
|
|
21
|
-
| **Summary cards** | Output tokens · Input tokens · Effective input · Cache read ratio ·
|
|
22
|
+
| **Summary cards** | Estimated total cost · Output tokens · Input tokens · Effective input · Cache read ratio · Session count |
|
|
22
23
|
| **Daily chart** | Input / output tokens per day (bar chart) |
|
|
23
|
-
| **Project table** | Per-project breakdown sorted by
|
|
24
|
+
| **Project table** | Per-project breakdown sorted by cost, with inline bar |
|
|
24
25
|
|
|
25
26
|
Cache read tokens from prompt caching are tracked separately — you can see at a glance how much of your effective input Claude is serving from cache (typically 90%+).
|
|
26
27
|
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Token usage per project (output / input / cache read / cache create)
|
|
31
|
+
- Daily usage chart
|
|
32
|
+
- Cache read ratio
|
|
33
|
+
- Estimated cost in USD per project (claude-sonnet-4-6 pricing)
|
|
34
|
+
- Cost share % with warning for projects over 20% of total spend
|
|
35
|
+
- Total estimated cost since first session
|
|
36
|
+
|
|
27
37
|
## Quick start
|
|
28
38
|
|
|
29
39
|
```bash
|
|
30
|
-
|
|
40
|
+
npm install -g @notenkidev/claude-token-dashboard
|
|
41
|
+
claude-token-dashboard
|
|
31
42
|
```
|
|
32
43
|
|
|
33
44
|
Open [http://localhost:3000](http://localhost:3000).
|
|
34
45
|
|
|
35
|
-
On first run, dependencies are installed automatically (~30s). After that it starts instantly.
|
|
36
|
-
|
|
37
46
|
**Custom port:**
|
|
38
47
|
|
|
39
48
|
```bash
|
|
40
|
-
|
|
49
|
+
claude-token-dashboard -p 4000
|
|
41
50
|
```
|
|
42
51
|
|
|
43
52
|
Reads `~/.claude/projects/**/*.jsonl` directly — no config, no API key needed.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TokenStats } from "@/lib/collect"
|
|
2
|
+
import { calcCost, fmtCost } from "@/lib/pricing"
|
|
2
3
|
|
|
3
4
|
interface Props {
|
|
4
5
|
byProject: Record<string, TokenStats>
|
|
@@ -12,10 +13,17 @@ function fmt(n: number): string {
|
|
|
12
13
|
|
|
13
14
|
export default function ProjectTable({ byProject }: Props) {
|
|
14
15
|
const rows = Object.entries(byProject)
|
|
15
|
-
.map(([project, stats]) => ({
|
|
16
|
-
|
|
16
|
+
.map(([project, stats]) => ({
|
|
17
|
+
project,
|
|
18
|
+
...stats,
|
|
19
|
+
total: stats.input + stats.output,
|
|
20
|
+
cost: calcCost(stats),
|
|
21
|
+
}))
|
|
22
|
+
.sort((a, b) => b.cost - a.cost)
|
|
17
23
|
|
|
18
|
-
const maxTotal = rows
|
|
24
|
+
const maxTotal = Math.max(...rows.map((r) => r.total), 1)
|
|
25
|
+
const totalCost = rows.reduce((s, r) => s + r.cost, 0)
|
|
26
|
+
const WARNING_THRESHOLD = 0.20
|
|
19
27
|
|
|
20
28
|
return (
|
|
21
29
|
<div className="overflow-x-auto rounded-xl border border-border">
|
|
@@ -23,24 +31,47 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
23
31
|
<thead>
|
|
24
32
|
<tr className="border-b border-border bg-muted/40">
|
|
25
33
|
<th className="px-4 py-3 text-left font-medium text-muted-foreground">Project</th>
|
|
34
|
+
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Cost</th>
|
|
26
35
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Output</th>
|
|
27
36
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Input</th>
|
|
28
37
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground hidden md:table-cell">Cache Read</th>
|
|
29
38
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground hidden lg:table-cell">Cache Create</th>
|
|
30
|
-
<th className="px-4 py-3 text-right font-medium text-muted-foreground">
|
|
31
|
-
<th className="px-4 py-3 w-
|
|
39
|
+
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Tokens</th>
|
|
40
|
+
<th className="px-4 py-3 w-28 hidden sm:table-cell"></th>
|
|
32
41
|
</tr>
|
|
33
42
|
</thead>
|
|
34
43
|
<tbody>
|
|
35
44
|
{rows.map((row, i) => {
|
|
36
45
|
const pct = (row.total / maxTotal) * 100
|
|
46
|
+
const costShare = totalCost > 0 ? row.cost / totalCost : 0
|
|
47
|
+
const isHighCost = costShare >= WARNING_THRESHOLD
|
|
48
|
+
|
|
37
49
|
return (
|
|
38
50
|
<tr
|
|
39
51
|
key={row.project}
|
|
40
|
-
className={`border-b border-border last:border-0 hover:bg-muted/20 transition-colors ${
|
|
52
|
+
className={`border-b border-border last:border-0 hover:bg-muted/20 transition-colors ${
|
|
53
|
+
isHighCost ? "bg-amber-950/10" : i === 0 ? "bg-muted/10" : ""
|
|
54
|
+
}`}
|
|
41
55
|
>
|
|
42
|
-
<td className="px-4 py-3 font-mono text-xs max-w-[
|
|
43
|
-
{row.project}
|
|
56
|
+
<td className="px-4 py-3 font-mono text-xs max-w-[180px]">
|
|
57
|
+
<div className="flex items-center gap-1.5 truncate" title={row.project}>
|
|
58
|
+
{isHighCost && (
|
|
59
|
+
<span className="shrink-0 text-amber-400" title={`${(costShare * 100).toFixed(1)}% of total cost`}>
|
|
60
|
+
⚠
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
<span className="truncate">{row.project}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</td>
|
|
66
|
+
<td className="px-4 py-3 text-right font-mono text-xs">
|
|
67
|
+
<span className={isHighCost ? "text-amber-400 font-semibold" : "text-emerald-400"}>
|
|
68
|
+
{fmtCost(row.cost)}
|
|
69
|
+
</span>
|
|
70
|
+
{totalCost > 0 && (
|
|
71
|
+
<span className="ml-1 text-muted-foreground text-[10px]">
|
|
72
|
+
{(costShare * 100).toFixed(1)}%
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
44
75
|
</td>
|
|
45
76
|
<td className="px-4 py-3 text-right font-mono text-xs text-violet-400">{fmt(row.output)}</td>
|
|
46
77
|
<td className="px-4 py-3 text-right font-mono text-xs text-blue-400">{fmt(row.input)}</td>
|
|
@@ -50,11 +81,11 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
50
81
|
<td className="px-4 py-3 text-right font-mono text-xs text-muted-foreground hidden lg:table-cell">
|
|
51
82
|
{fmt(row.cacheCreate)}
|
|
52
83
|
</td>
|
|
53
|
-
<td className="px-4 py-3 text-right font-mono text-xs
|
|
84
|
+
<td className="px-4 py-3 text-right font-mono text-xs text-muted-foreground">{fmt(row.total)}</td>
|
|
54
85
|
<td className="px-4 py-3 hidden sm:table-cell">
|
|
55
86
|
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
|
|
56
87
|
<div
|
|
57
|
-
className=
|
|
88
|
+
className={`h-full rounded-full ${isHighCost ? "bg-amber-500/70" : "bg-violet-500/70"}`}
|
|
58
89
|
style={{ width: `${pct}%` }}
|
|
59
90
|
/>
|
|
60
91
|
</div>
|
|
@@ -63,6 +94,15 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
63
94
|
)
|
|
64
95
|
})}
|
|
65
96
|
</tbody>
|
|
97
|
+
<tfoot>
|
|
98
|
+
<tr className="border-t border-border bg-muted/20">
|
|
99
|
+
<td className="px-4 py-2 text-xs font-medium text-muted-foreground">TOTAL</td>
|
|
100
|
+
<td className="px-4 py-2 text-right font-mono text-xs font-semibold text-emerald-400">
|
|
101
|
+
{fmtCost(totalCost)}
|
|
102
|
+
</td>
|
|
103
|
+
<td colSpan={6} />
|
|
104
|
+
</tr>
|
|
105
|
+
</tfoot>
|
|
66
106
|
</table>
|
|
67
107
|
</div>
|
|
68
108
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
|
2
2
|
import type { TokenStats } from "@/lib/collect"
|
|
3
|
+
import { calcCost, fmtCost } from "@/lib/pricing"
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
byProject: Record<string, TokenStats>
|
|
@@ -25,24 +26,33 @@ export default function SummaryCards({ byProject, totalFiles, totalEntries, skip
|
|
|
25
26
|
const effectiveInput = totalInput + totalCacheRead + totalCacheCreate
|
|
26
27
|
const cacheRatio = effectiveInput > 0 ? (totalCacheRead / effectiveInput) * 100 : 0
|
|
27
28
|
|
|
29
|
+
const totalCost = allStats.reduce((s, v) => s + calcCost(v), 0)
|
|
30
|
+
|
|
28
31
|
const cards = [
|
|
32
|
+
{
|
|
33
|
+
title: "Estimated Cost",
|
|
34
|
+
value: fmtCost(totalCost),
|
|
35
|
+
sub: "claude-sonnet-4-6",
|
|
36
|
+
highlight: true,
|
|
37
|
+
},
|
|
29
38
|
{ title: "Output Tokens", value: fmtBig(totalOutput), sub: "generated" },
|
|
30
39
|
{ title: "Input Tokens", value: fmtBig(totalInput), sub: "sent" },
|
|
31
40
|
{ title: "Effective Input", value: fmtBig(effectiveInput), sub: "input + cache" },
|
|
32
41
|
{ title: "Cache Read Ratio", value: `${cacheRatio.toFixed(1)}%`, sub: "of effective input" },
|
|
33
|
-
{ title: "Projects", value: String(Object.keys(byProject).length), sub: "unique" },
|
|
34
42
|
{ title: "Sessions", value: fmtBig(totalFiles), sub: `${fmtBig(totalEntries)} entries · ${fmtBig(skippedDup)} deduped` },
|
|
35
43
|
]
|
|
36
44
|
|
|
37
45
|
return (
|
|
38
46
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
|
39
47
|
{cards.map((c) => (
|
|
40
|
-
<Card key={c.title}>
|
|
48
|
+
<Card key={c.title} className={c.highlight ? "border-emerald-500/40 bg-emerald-950/20" : ""}>
|
|
41
49
|
<CardHeader className="pb-2">
|
|
42
50
|
<CardTitle>{c.title}</CardTitle>
|
|
43
51
|
</CardHeader>
|
|
44
52
|
<CardContent>
|
|
45
|
-
<p className=
|
|
53
|
+
<p className={`text-2xl font-bold font-mono tracking-tight ${c.highlight ? "text-emerald-400" : ""}`}>
|
|
54
|
+
{c.value}
|
|
55
|
+
</p>
|
|
46
56
|
<p className="mt-1 text-xs text-muted-foreground">{c.sub}</p>
|
|
47
57
|
</CardContent>
|
|
48
58
|
</Card>
|
package/lib/pricing.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TokenStats } from "./collect"
|
|
2
|
+
|
|
3
|
+
// claude-sonnet-4-6 pricing (USD per token)
|
|
4
|
+
export const PRICING = {
|
|
5
|
+
input: 3.00 / 1_000_000,
|
|
6
|
+
output: 15.00 / 1_000_000,
|
|
7
|
+
cacheRead: 0.30 / 1_000_000,
|
|
8
|
+
cacheCreate: 3.75 / 1_000_000,
|
|
9
|
+
} as const
|
|
10
|
+
|
|
11
|
+
export function calcCost(stats: TokenStats): number {
|
|
12
|
+
return (
|
|
13
|
+
stats.input * PRICING.input +
|
|
14
|
+
stats.output * PRICING.output +
|
|
15
|
+
stats.cacheRead * PRICING.cacheRead +
|
|
16
|
+
stats.cacheCreate * PRICING.cacheCreate
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function fmtCost(usd: number): string {
|
|
21
|
+
if (usd >= 1000) return `$${(usd / 1000).toFixed(1)}K`
|
|
22
|
+
if (usd >= 100) return `$${usd.toFixed(0)}`
|
|
23
|
+
if (usd >= 1) return `$${usd.toFixed(2)}`
|
|
24
|
+
if (usd >= 0.01) return `$${usd.toFixed(3)}`
|
|
25
|
+
return "<$0.01"
|
|
26
|
+
}
|