@notenkidev/claude-token-dashboard 0.1.4 → 0.1.8

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/bin/cli.js CHANGED
@@ -1,5 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * npx インストールパスの構造:
5
+ * ~/.npm/_npx/<hash>/node_modules/@notenkidev/claude-token-dashboard/ ← root (__dirname/..)
6
+ * ~/.npm/_npx/<hash>/node_modules/.bin/next ← flat install (2 levels up)
7
+ *
8
+ * ローカル開発 / npm install の場合:
9
+ * <project>/node_modules/.bin/next ← nested install
10
+ */
11
+
3
12
  const { execSync, spawn } = require('child_process')
4
13
  const { existsSync } = require('fs')
5
14
  const { join } = require('path')
@@ -7,15 +16,32 @@ const { join } = require('path')
7
16
  const root = join(__dirname, '..')
8
17
  const args = process.argv.slice(2)
9
18
 
10
- // Install dependencies on first run (node_modules not included in npm package)
11
- if (!existsSync(join(root, 'node_modules', 'next'))) {
12
- console.log('📦 Installing dependencies (first run, this takes ~30s)...')
13
- execSync('npm install', { cwd: root, stdio: 'inherit' })
19
+ function findNext () {
20
+ return [
21
+ // npx flat install (scoped packages live 2 dirs deep inside node_modules)
22
+ join(root, '..', '..', '.bin', 'next'),
23
+ // local nested install (git clone → npm install)
24
+ join(root, 'node_modules', '.bin', 'next'),
25
+ ].find(existsSync) ?? null
26
+ }
27
+
28
+ let nextBin = findNext()
29
+
30
+ // Fallback: run npm install locally (e.g. running directly from a git clone
31
+ // without having done npm install first)
32
+ if (!nextBin) {
33
+ console.log('📦 Installing dependencies (~30s)...')
34
+ execSync('npm install --prefer-offline', { cwd: root, stdio: 'inherit' })
35
+ nextBin = findNext()
36
+ }
37
+
38
+ if (!nextBin) {
39
+ console.error('❌ Could not locate the next binary. Run `npm install` in the project directory.')
40
+ process.exit(1)
14
41
  }
15
42
 
16
43
  console.log('🚀 Starting Claude Token Dashboard → http://localhost:3000\n')
17
44
 
18
- const nextBin = join(root, 'node_modules', '.bin', 'next')
19
45
  const child = spawn(process.execPath, [nextBin, 'dev', ...args], {
20
46
  cwd: root,
21
47
  stdio: 'inherit',
@@ -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]) => ({ project, ...stats, total: stats.input + stats.output }))
16
- .sort((a, b) => b.total - a.total)
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[0]?.total ?? 1
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">Total</th>
31
- <th className="px-4 py-3 w-32 hidden sm:table-cell"></th>
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 ${i === 0 ? "bg-muted/10" : ""}`}
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-[200px] truncate" title={row.project}>
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 font-semibold">{fmt(row.total)}</td>
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="h-full rounded-full bg-violet-500/70"
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="text-2xl font-bold font-mono tracking-tight">{c.value}</p>
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notenkidev/claude-token-dashboard",
3
- "version": "0.1.4",
3
+ "version": "0.1.8",
4
4
  "description": "Visualize your Claude Code token usage by project and date",
5
5
  "bin": {
6
6
  "claude-token-dashboard": "bin/cli.js"