@notenkidev/claude-token-dashboard 0.1.10 โ 0.1.12
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/app/page.tsx +6 -2
- package/components/InsightPanel.tsx +64 -0
- package/components/ProjectTable.tsx +25 -1
- package/lib/collect.ts +30 -2
- package/lib/insights.ts +86 -0
- package/package.json +1 -1
package/app/page.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { collect } from "@/lib/collect"
|
|
2
2
|
import SummaryCards from "@/components/SummaryCards"
|
|
3
|
+
import InsightPanel from "@/components/InsightPanel"
|
|
3
4
|
import ProjectTable from "@/components/ProjectTable"
|
|
4
5
|
import DailyChart from "@/components/DailyChart"
|
|
5
6
|
import RefreshButton from "@/components/RefreshButton"
|
|
@@ -8,7 +9,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
|
|
8
9
|
export const dynamic = "force-dynamic"
|
|
9
10
|
|
|
10
11
|
export default function Page() {
|
|
11
|
-
const { byProject, byDay, totalFiles, totalEntries, skippedDup } = collect()
|
|
12
|
+
const { byProject, byDay, byProjectClaudeMd, totalFiles, totalEntries, skippedDup } = collect()
|
|
12
13
|
|
|
13
14
|
const dayData = Object.entries(byDay)
|
|
14
15
|
.filter(([date]) => date !== "unknown")
|
|
@@ -46,6 +47,9 @@ export default function Page() {
|
|
|
46
47
|
skippedDup={skippedDup}
|
|
47
48
|
/>
|
|
48
49
|
|
|
50
|
+
{/* Insights */}
|
|
51
|
+
<InsightPanel byProject={byProject} byProjectClaudeMd={byProjectClaudeMd} />
|
|
52
|
+
|
|
49
53
|
{/* Daily chart */}
|
|
50
54
|
<Card>
|
|
51
55
|
<CardHeader>
|
|
@@ -66,7 +70,7 @@ export default function Page() {
|
|
|
66
70
|
</CardTitle>
|
|
67
71
|
</CardHeader>
|
|
68
72
|
<CardContent className="p-0">
|
|
69
|
-
<ProjectTable byProject={byProject} />
|
|
73
|
+
<ProjectTable byProject={byProject} byProjectClaudeMd={byProjectClaudeMd} />
|
|
70
74
|
</CardContent>
|
|
71
75
|
</Card>
|
|
72
76
|
</main>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
|
2
|
+
import type { TokenStats } from "@/lib/collect"
|
|
3
|
+
import { generateInsights, type InsightLevel } from "@/lib/insights"
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
byProject: Record<string, TokenStats>
|
|
7
|
+
byProjectClaudeMd: Record<string, number | null>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LEVEL_STYLES: Record<InsightLevel, { bar: string; icon: string; badge: string }> = {
|
|
11
|
+
alert: { bar: "bg-red-500", icon: "๐ด", badge: "text-red-400" },
|
|
12
|
+
warning: { bar: "bg-amber-500", icon: "โ ๏ธ", badge: "text-amber-400" },
|
|
13
|
+
tip: { bar: "bg-emerald-500",icon: "๐ก", badge: "text-emerald-400" },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const LEVEL_LABEL: Record<InsightLevel, string> = {
|
|
17
|
+
alert: "้่ฆ",
|
|
18
|
+
warning: "ๆณจๆ",
|
|
19
|
+
tip: "ๆๆก",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function InsightPanel({ byProject, byProjectClaudeMd }: Props) {
|
|
23
|
+
const insights = generateInsights(byProject, byProjectClaudeMd)
|
|
24
|
+
|
|
25
|
+
if (insights.length === 0) return null
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Card>
|
|
29
|
+
<CardHeader className="pb-3">
|
|
30
|
+
<CardTitle className="text-base font-semibold text-foreground">
|
|
31
|
+
ๅๆธๆๆก
|
|
32
|
+
</CardTitle>
|
|
33
|
+
</CardHeader>
|
|
34
|
+
<CardContent className="space-y-3">
|
|
35
|
+
{insights.map((insight, i) => {
|
|
36
|
+
const s = LEVEL_STYLES[insight.level]
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
key={i}
|
|
40
|
+
className="flex gap-3 rounded-lg border border-border bg-muted/20 overflow-hidden"
|
|
41
|
+
>
|
|
42
|
+
{/* ๅทฆใซใฉใผใใผ */}
|
|
43
|
+
<div className={`w-1 shrink-0 ${s.bar}`} />
|
|
44
|
+
<div className="flex items-start gap-2 py-3 pr-4">
|
|
45
|
+
<span className="text-base leading-none mt-0.5">{s.icon}</span>
|
|
46
|
+
<div>
|
|
47
|
+
<span className={`text-xs font-semibold ${s.badge} mr-2`}>
|
|
48
|
+
{LEVEL_LABEL[insight.level]}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-xs font-medium text-foreground mr-2">
|
|
51
|
+
{insight.title}
|
|
52
|
+
</span>
|
|
53
|
+
<span className="text-xs text-muted-foreground">
|
|
54
|
+
{insight.message}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
})}
|
|
61
|
+
</CardContent>
|
|
62
|
+
</Card>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { TokenStats } from "@/lib/collect"
|
|
2
2
|
import { calcCost, fmtCost } from "@/lib/pricing"
|
|
3
3
|
|
|
4
|
+
const CLAUDE_MD_WARN_BYTES = 5 * 1024 // 5 KB
|
|
5
|
+
|
|
4
6
|
interface Props {
|
|
5
7
|
byProject: Record<string, TokenStats>
|
|
8
|
+
byProjectClaudeMd: Record<string, number | null>
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
function fmt(n: number): string {
|
|
@@ -11,13 +14,14 @@ function fmt(n: number): string {
|
|
|
11
14
|
return n.toLocaleString()
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
export default function ProjectTable({ byProject }: Props) {
|
|
17
|
+
export default function ProjectTable({ byProject, byProjectClaudeMd }: Props) {
|
|
15
18
|
const rows = Object.entries(byProject)
|
|
16
19
|
.map(([project, stats]) => ({
|
|
17
20
|
project,
|
|
18
21
|
...stats,
|
|
19
22
|
total: stats.input + stats.output,
|
|
20
23
|
cost: calcCost(stats),
|
|
24
|
+
claudeMdBytes: byProjectClaudeMd[project] ?? null,
|
|
21
25
|
}))
|
|
22
26
|
.sort((a, b) => b.cost - a.cost)
|
|
23
27
|
|
|
@@ -36,6 +40,7 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
36
40
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Input</th>
|
|
37
41
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground hidden md:table-cell">Cache Read</th>
|
|
38
42
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground hidden lg:table-cell">Cache Create</th>
|
|
43
|
+
<th className="px-4 py-3 text-right font-medium text-muted-foreground hidden xl:table-cell">CLAUDE.md</th>
|
|
39
44
|
<th className="px-4 py-3 text-right font-medium text-muted-foreground">Tokens</th>
|
|
40
45
|
<th className="px-4 py-3 w-28 hidden sm:table-cell"></th>
|
|
41
46
|
</tr>
|
|
@@ -45,6 +50,11 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
45
50
|
const pct = (row.total / maxTotal) * 100
|
|
46
51
|
const costShare = totalCost > 0 ? row.cost / totalCost : 0
|
|
47
52
|
const isHighCost = costShare >= WARNING_THRESHOLD
|
|
53
|
+
const isHeavyClaudeMd = row.claudeMdBytes !== null && row.claudeMdBytes >= CLAUDE_MD_WARN_BYTES
|
|
54
|
+
const claudeMdLabel =
|
|
55
|
+
row.claudeMdBytes === null
|
|
56
|
+
? "-"
|
|
57
|
+
: `${(row.claudeMdBytes / 1024).toFixed(1)} KB`
|
|
48
58
|
|
|
49
59
|
return (
|
|
50
60
|
<tr
|
|
@@ -81,6 +91,20 @@ export default function ProjectTable({ byProject }: Props) {
|
|
|
81
91
|
<td className="px-4 py-3 text-right font-mono text-xs text-muted-foreground hidden lg:table-cell">
|
|
82
92
|
{fmt(row.cacheCreate)}
|
|
83
93
|
</td>
|
|
94
|
+
<td className="px-4 py-3 text-right font-mono text-xs hidden xl:table-cell">
|
|
95
|
+
{isHeavyClaudeMd ? (
|
|
96
|
+
<span
|
|
97
|
+
className="text-orange-400 font-semibold"
|
|
98
|
+
title="CLAUDE.mdใ้ใๅฏ่ฝๆงใใใใพใ๏ผ5KBไปฅไธ๏ผ"
|
|
99
|
+
>
|
|
100
|
+
โ {claudeMdLabel}
|
|
101
|
+
</span>
|
|
102
|
+
) : (
|
|
103
|
+
<span className={row.claudeMdBytes !== null ? "text-muted-foreground" : "text-muted-foreground/40"}>
|
|
104
|
+
{claudeMdLabel}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</td>
|
|
84
108
|
<td className="px-4 py-3 text-right font-mono text-xs text-muted-foreground">{fmt(row.total)}</td>
|
|
85
109
|
<td className="px-4 py-3 hidden sm:table-cell">
|
|
86
110
|
<div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
|
package/lib/collect.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface TokenStats {
|
|
|
12
12
|
export interface DashboardData {
|
|
13
13
|
byProject: Record<string, TokenStats>
|
|
14
14
|
byDay: Record<string, TokenStats>
|
|
15
|
+
byProjectClaudeMd: Record<string, number | null> // bytes, null = not found
|
|
15
16
|
totalFiles: number
|
|
16
17
|
totalEntries: number
|
|
17
18
|
skippedDup: number
|
|
@@ -50,15 +51,34 @@ function addStats(target: TokenStats, input: number, output: number, cacheCreate
|
|
|
50
51
|
target.cacheRead += cacheRead
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function claudeMdBytes(cwd: string): number | null {
|
|
55
|
+
// Walk up from cwd to find CLAUDE.md, stopping at home directory
|
|
56
|
+
const home = os.homedir()
|
|
57
|
+
let dir = cwd
|
|
58
|
+
while (dir.startsWith(home) && dir !== home) {
|
|
59
|
+
try {
|
|
60
|
+
const stat = fs.statSync(path.join(dir, "CLAUDE.md"))
|
|
61
|
+
if (stat.isFile()) return stat.size
|
|
62
|
+
} catch { /* not found at this level */ }
|
|
63
|
+
const parent = path.dirname(dir)
|
|
64
|
+
if (parent === dir) break
|
|
65
|
+
dir = parent
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export function collect(): DashboardData {
|
|
54
71
|
const byProject: Record<string, TokenStats> = {}
|
|
55
72
|
const byDay: Record<string, TokenStats> = {}
|
|
73
|
+
const cwdByLabel: Record<string, string> = {} // label โ first seen cwd
|
|
56
74
|
const seenMessageIds = new Set<string>()
|
|
57
75
|
let totalFiles = 0
|
|
58
76
|
let totalEntries = 0
|
|
59
77
|
let skippedDup = 0
|
|
60
78
|
|
|
61
|
-
if (!fs.existsSync(BASE))
|
|
79
|
+
if (!fs.existsSync(BASE)) {
|
|
80
|
+
return { byProject, byDay, byProjectClaudeMd: {}, totalFiles, totalEntries, skippedDup }
|
|
81
|
+
}
|
|
62
82
|
|
|
63
83
|
const projectDirs = fs.readdirSync(BASE).sort()
|
|
64
84
|
|
|
@@ -101,6 +121,7 @@ export function collect(): DashboardData {
|
|
|
101
121
|
|
|
102
122
|
const cwd = (d.cwd as string) || ""
|
|
103
123
|
const label = cwd ? projectLabelFromCwd(cwd) : fallbackLabel
|
|
124
|
+
if (cwd && !cwdByLabel[label]) cwdByLabel[label] = cwd
|
|
104
125
|
|
|
105
126
|
const ts = (d.timestamp as string) || ""
|
|
106
127
|
let date = "unknown"
|
|
@@ -121,5 +142,12 @@ export function collect(): DashboardData {
|
|
|
121
142
|
}
|
|
122
143
|
}
|
|
123
144
|
|
|
124
|
-
|
|
145
|
+
// Resolve CLAUDE.md sizes after all entries are processed
|
|
146
|
+
const byProjectClaudeMd: Record<string, number | null> = {}
|
|
147
|
+
for (const label of Object.keys(byProject)) {
|
|
148
|
+
const cwd = cwdByLabel[label]
|
|
149
|
+
byProjectClaudeMd[label] = cwd ? claudeMdBytes(cwd) : null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { byProject, byDay, byProjectClaudeMd, totalFiles, totalEntries, skippedDup }
|
|
125
153
|
}
|
package/lib/insights.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { TokenStats } from "./collect"
|
|
2
|
+
import { calcCost, PRICING } from "./pricing"
|
|
3
|
+
|
|
4
|
+
export type InsightLevel = "warning" | "alert" | "tip"
|
|
5
|
+
|
|
6
|
+
export interface Insight {
|
|
7
|
+
level: InsightLevel
|
|
8
|
+
title: string
|
|
9
|
+
message: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CLAUDE_MD_WARN_BYTES = 5 * 1024
|
|
13
|
+
const HIGH_COST_SHARE_THRESHOLD = 0.20 // 20% โ alert
|
|
14
|
+
const HIGH_CACHE_CREATE_RATIO = 0.50 // cache_create > 50% of own cost โ warning
|
|
15
|
+
|
|
16
|
+
function cacheCreateCost(stats: TokenStats): number {
|
|
17
|
+
return stats.cacheCreate * PRICING.cacheCreate
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function generateInsights(
|
|
21
|
+
byProject: Record<string, TokenStats>,
|
|
22
|
+
byProjectClaudeMd: Record<string, number | null>,
|
|
23
|
+
): Insight[] {
|
|
24
|
+
const insights: Insight[] = []
|
|
25
|
+
|
|
26
|
+
const entries = Object.entries(byProject)
|
|
27
|
+
if (entries.length === 0) return insights
|
|
28
|
+
|
|
29
|
+
const totalCost = entries.reduce((s, [, v]) => s + calcCost(v), 0)
|
|
30
|
+
if (totalCost === 0) return insights
|
|
31
|
+
|
|
32
|
+
// โโ Insight 1: ๆใใณในใใ้ซใใใญใธใงใฏใ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
33
|
+
const [topProject, topStats] = entries.reduce(
|
|
34
|
+
(best, cur) => (calcCost(cur[1]) > calcCost(best[1]) ? cur : best),
|
|
35
|
+
)
|
|
36
|
+
const topShare = calcCost(topStats) / totalCost
|
|
37
|
+
insights.push({
|
|
38
|
+
level: topShare >= HIGH_COST_SHARE_THRESHOLD ? "alert" : "tip",
|
|
39
|
+
title: "ๆๅคงใณในใใใญใธใงใฏใ",
|
|
40
|
+
message: `${topProject} ใๅ
จไฝใฎ ${(topShare * 100).toFixed(1)}% ใๅ ใใฆใใพใใCLAUDE.mdใฎใตใคใบใ็ขบ่ชใใฆใใ ใใใ`,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// โโ Insight 2: cache_creation ใ้ซใใใญใธใงใฏใ โโโโโโโโโโโโโโโโโโโโโโ
|
|
44
|
+
const highCacheProjects = entries
|
|
45
|
+
.filter(([, stats]) => {
|
|
46
|
+
const own = calcCost(stats)
|
|
47
|
+
return own > 0 && cacheCreateCost(stats) / own >= HIGH_CACHE_CREATE_RATIO
|
|
48
|
+
})
|
|
49
|
+
.sort(([, a], [, b]) => cacheCreateCost(b) - cacheCreateCost(a))
|
|
50
|
+
.slice(0, 3)
|
|
51
|
+
|
|
52
|
+
for (const [project] of highCacheProjects) {
|
|
53
|
+
insights.push({
|
|
54
|
+
level: "warning",
|
|
55
|
+
title: "cache_creation ้ๅค",
|
|
56
|
+
message: `${project} ใฎ cache_creation ใ้ซใใงใใCLAUDE.mdใ่ฅๅคงๅใใฆใใๅฏ่ฝๆงใใใใพใใ`,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// โโ Insight 3: ๅ
จไฝๆ้ฉๅใใใณใทใฃใซ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
61
|
+
// ๅคงใใช CLAUDE.md๏ผโฅ5KB๏ผใๆใคใใญใธใงใฏใใฎ cache_create ใณในใใ
|
|
62
|
+
// 50% ๅๆธใงใใใจไปฎๅฎใใๅ ดๅใฎ็ฏ็ด้ก
|
|
63
|
+
const heavyProjects = entries.filter(
|
|
64
|
+
([label]) => (byProjectClaudeMd[label] ?? 0) >= CLAUDE_MD_WARN_BYTES,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
const targetProjects = heavyProjects.length > 0
|
|
68
|
+
? heavyProjects
|
|
69
|
+
: entries.sort(([, a], [, b]) => cacheCreateCost(b) - cacheCreateCost(a)).slice(0, 3)
|
|
70
|
+
|
|
71
|
+
const optimizableCost = targetProjects.reduce(
|
|
72
|
+
(s, [, stats]) => s + cacheCreateCost(stats),
|
|
73
|
+
0,
|
|
74
|
+
)
|
|
75
|
+
const savingPct = (optimizableCost * 0.5) / totalCost * 100
|
|
76
|
+
|
|
77
|
+
if (savingPct >= 1) {
|
|
78
|
+
insights.push({
|
|
79
|
+
level: "tip",
|
|
80
|
+
title: "ๆ้ฉๅใใใณใทใฃใซ",
|
|
81
|
+
message: `ๆ้ฉๅใใใฐๆจๅฎ ${savingPct.toFixed(1)}% ใฎใณในใๅๆธใๅฏ่ฝใงใใ`,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return insights
|
|
86
|
+
}
|