claude-session-dashboard 0.1.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.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/bin/cli.mjs +212 -0
  3. package/dist/client/assets/_dashboard-Oaur6UHf.js +1 -0
  4. package/dist/client/assets/_sessionId-CWavmGnC.js +12 -0
  5. package/dist/client/assets/createServerFn-Bmn60lX_.js +1 -0
  6. package/dist/client/assets/format-Bf-cSf6L.js +1 -0
  7. package/dist/client/assets/index-CN4cqOcf.js +1 -0
  8. package/dist/client/assets/main-Bssrw_E_.js +18 -0
  9. package/dist/client/assets/settings-Buc0ndXk.js +1 -0
  10. package/dist/client/assets/settings.types-4U9-Yxq3.js +1 -0
  11. package/dist/client/assets/stats-BEUCPbcP.js +4 -0
  12. package/dist/client/assets/useSessionCost-C7ox6YwA.js +61 -0
  13. package/dist/server/assets/_dashboard-CG6ub7j3.js +97 -0
  14. package/dist/server/assets/_sessionId-CcWGJarz.js +1582 -0
  15. package/dist/server/assets/_tanstack-start-manifest_v-DidrnaMJ.js +4 -0
  16. package/dist/server/assets/claude-path-CkuljM34.js +25 -0
  17. package/dist/server/assets/createServerRpc-Bd3B-Ah9.js +12 -0
  18. package/dist/server/assets/createSsrRpc-CVg2UDl0.js +17 -0
  19. package/dist/server/assets/format-CGmJnuhZ.js +57 -0
  20. package/dist/server/assets/index-DjrI63_C.js +409 -0
  21. package/dist/server/assets/router-ByIey__S.js +208 -0
  22. package/dist/server/assets/session-detail.server-azkRfON2.js +70 -0
  23. package/dist/server/assets/session-parser-CAEXxF1D.js +413 -0
  24. package/dist/server/assets/sessions.server-B8zbmvSM.js +187 -0
  25. package/dist/server/assets/settings-ko61yfVs.js +202 -0
  26. package/dist/server/assets/settings.queries-DSQd324O.js +33 -0
  27. package/dist/server/assets/settings.server-6B2PvLgf.js +89 -0
  28. package/dist/server/assets/settings.types-DntadCHo.js +111 -0
  29. package/dist/server/assets/start-HYkvq4Ni.js +4 -0
  30. package/dist/server/assets/stats-DItsFPp5.js +266 -0
  31. package/dist/server/assets/stats.server-BZWxV-mC.js +85 -0
  32. package/dist/server/assets/useSessionCost-EB0VxklP.js +76 -0
  33. package/dist/server/server.js +1428 -0
  34. package/package.json +69 -0
@@ -0,0 +1,266 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
3
+ import { queryOptions, useQuery } from "@tanstack/react-query";
4
+ import { c as createSsrRpc } from "./createSsrRpc-CVg2UDl0.js";
5
+ import { c as createServerFn } from "../server.js";
6
+ import { ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Bar, PieChart, Pie, Cell, Legend } from "recharts";
7
+ import { format } from "date-fns";
8
+ import { f as formatTokenCount, a as formatUSD, b as formatDuration } from "./format-CGmJnuhZ.js";
9
+ import { u as useSessionCost } from "./useSessionCost-EB0VxklP.js";
10
+ import "@tanstack/history";
11
+ import "@tanstack/router-core/ssr/client";
12
+ import "@tanstack/router-core";
13
+ import "node:async_hooks";
14
+ import "@tanstack/router-core/ssr/server";
15
+ import "h3-v2";
16
+ import "tiny-invariant";
17
+ import "seroval";
18
+ import "@tanstack/react-router/ssr/server";
19
+ import "@tanstack/react-router";
20
+ import "./settings.queries-DSQd324O.js";
21
+ import "./settings.types-DntadCHo.js";
22
+ import "zod";
23
+ const getStats = createServerFn({
24
+ method: "GET"
25
+ }).handler(createSsrRpc("4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799"));
26
+ const statsQuery = queryOptions({
27
+ queryKey: ["stats"],
28
+ queryFn: () => getStats(),
29
+ refetchInterval: 6e4
30
+ });
31
+ function ActivityChart({ data }) {
32
+ const chartData = data.map((d) => ({
33
+ ...d,
34
+ dateLabel: format(new Date(d.date), "MMM d")
35
+ }));
36
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
37
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Daily Activity" }),
38
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Messages, sessions, and tool calls per day" }),
39
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(BarChart, { data: chartData, children: [
40
+ /* @__PURE__ */ jsx(CartesianGrid, { strokeDasharray: "3 3", stroke: "#1f2937" }),
41
+ /* @__PURE__ */ jsx(
42
+ XAxis,
43
+ {
44
+ dataKey: "dateLabel",
45
+ tick: { fill: "#6b7280", fontSize: 10 },
46
+ tickLine: false
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsx(
50
+ YAxis,
51
+ {
52
+ tick: { fill: "#6b7280", fontSize: 10 },
53
+ tickLine: false,
54
+ axisLine: false
55
+ }
56
+ ),
57
+ /* @__PURE__ */ jsx(
58
+ Tooltip,
59
+ {
60
+ contentStyle: {
61
+ backgroundColor: "#111827",
62
+ border: "1px solid #374151",
63
+ borderRadius: "8px",
64
+ fontSize: "12px"
65
+ }
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsx(
69
+ Bar,
70
+ {
71
+ dataKey: "messageCount",
72
+ name: "Messages",
73
+ fill: "#3b82f6",
74
+ radius: [2, 2, 0, 0]
75
+ }
76
+ ),
77
+ /* @__PURE__ */ jsx(
78
+ Bar,
79
+ {
80
+ dataKey: "toolCallCount",
81
+ name: "Tool Calls",
82
+ fill: "#8b5cf6",
83
+ radius: [2, 2, 0, 0]
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx(
87
+ Bar,
88
+ {
89
+ dataKey: "sessionCount",
90
+ name: "Sessions",
91
+ fill: "#10b981",
92
+ radius: [2, 2, 0, 0]
93
+ }
94
+ )
95
+ ] }) }) })
96
+ ] });
97
+ }
98
+ const COLORS = ["#3b82f6", "#8b5cf6", "#10b981", "#f59e0b", "#ef4444", "#6366f1"];
99
+ function ModelUsageChart({ data }) {
100
+ const chartData = Object.entries(data).map(([model, usage]) => ({
101
+ name: model.replace(/^claude-/, "").split("-202")[0],
102
+ fullName: model,
103
+ totalTokens: usage.inputTokens + usage.outputTokens,
104
+ outputTokens: usage.outputTokens
105
+ }));
106
+ chartData.sort((a, b) => b.totalTokens - a.totalTokens);
107
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
108
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Model Usage" }),
109
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Token usage by model" }),
110
+ /* @__PURE__ */ jsx("div", { className: "mt-4 h-64", children: /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: /* @__PURE__ */ jsxs(PieChart, { children: [
111
+ /* @__PURE__ */ jsx(
112
+ Pie,
113
+ {
114
+ data: chartData,
115
+ cx: "50%",
116
+ cy: "50%",
117
+ innerRadius: 50,
118
+ outerRadius: 80,
119
+ dataKey: "totalTokens",
120
+ nameKey: "name",
121
+ strokeWidth: 0,
122
+ children: chartData.map((_, i) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length], opacity: 0.8 }, i))
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsx(
126
+ Tooltip,
127
+ {
128
+ formatter: (value) => formatTokenCount(value),
129
+ contentStyle: {
130
+ backgroundColor: "#111827",
131
+ border: "1px solid #374151",
132
+ borderRadius: "8px",
133
+ fontSize: "12px"
134
+ }
135
+ }
136
+ ),
137
+ /* @__PURE__ */ jsx(
138
+ Legend,
139
+ {
140
+ wrapperStyle: { fontSize: "11px" }
141
+ }
142
+ )
143
+ ] }) }) })
144
+ ] });
145
+ }
146
+ function HourlyDistribution({
147
+ hourCounts
148
+ }) {
149
+ const hours = Array.from({ length: 24 }, (_, i) => ({
150
+ hour: i,
151
+ label: `${i.toString().padStart(2, "0")}:00`,
152
+ count: hourCounts[String(i)] ?? 0
153
+ }));
154
+ const maxCount = Math.max(...hours.map((h) => h.count), 1);
155
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
156
+ /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-300", children: "Hourly Distribution" }),
157
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-gray-500", children: "Session starts by hour of day" }),
158
+ /* @__PURE__ */ jsx("div", { className: "mt-4 flex items-end gap-0.5", style: { height: "120px" }, children: hours.map((h) => {
159
+ const height = h.count > 0 ? Math.max(h.count / maxCount * 100, 4) : 0;
160
+ const intensity = h.count / maxCount;
161
+ return /* @__PURE__ */ jsxs(
162
+ "div",
163
+ {
164
+ className: "group relative flex-1",
165
+ style: { height: "100%" },
166
+ children: [
167
+ /* @__PURE__ */ jsx(
168
+ "div",
169
+ {
170
+ className: "absolute bottom-0 w-full rounded-t transition-colors",
171
+ style: {
172
+ height: `${height}%`,
173
+ backgroundColor: `rgba(59, 130, 246, ${0.2 + intensity * 0.6})`
174
+ }
175
+ }
176
+ ),
177
+ /* @__PURE__ */ jsxs("div", { className: "absolute -top-8 left-1/2 hidden -translate-x-1/2 whitespace-nowrap rounded bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-300 group-hover:block", children: [
178
+ h.label,
179
+ ": ",
180
+ h.count
181
+ ] })
182
+ ]
183
+ },
184
+ h.hour
185
+ );
186
+ }) }),
187
+ /* @__PURE__ */ jsxs("div", { className: "mt-1 flex justify-between text-[10px] text-gray-600", children: [
188
+ /* @__PURE__ */ jsx("span", { children: "00:00" }),
189
+ /* @__PURE__ */ jsx("span", { children: "06:00" }),
190
+ /* @__PURE__ */ jsx("span", { children: "12:00" }),
191
+ /* @__PURE__ */ jsx("span", { children: "18:00" }),
192
+ /* @__PURE__ */ jsx("span", { children: "23:00" })
193
+ ] })
194
+ ] });
195
+ }
196
+ const EMPTY_TOKENS_BY_MODEL = {};
197
+ function StatsPage() {
198
+ const {
199
+ data: stats,
200
+ isLoading
201
+ } = useQuery(statsQuery);
202
+ const tokensByModel = useMemo(() => {
203
+ if (!stats) return EMPTY_TOKENS_BY_MODEL;
204
+ const result = {};
205
+ for (const [model, usage] of Object.entries(stats.modelUsage)) {
206
+ result[model] = {
207
+ inputTokens: usage.inputTokens,
208
+ outputTokens: usage.outputTokens,
209
+ cacheReadInputTokens: usage.cacheReadInputTokens,
210
+ cacheCreationInputTokens: usage.cacheCreationInputTokens
211
+ };
212
+ }
213
+ return result;
214
+ }, [stats]);
215
+ const {
216
+ cost
217
+ } = useSessionCost(tokensByModel);
218
+ if (isLoading) {
219
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
220
+ /* @__PURE__ */ jsx("div", { className: "h-8 w-32 animate-pulse rounded bg-gray-800" }),
221
+ Array.from({
222
+ length: 3
223
+ }).map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-64 animate-pulse rounded-xl bg-gray-900/50" }, i))
224
+ ] });
225
+ }
226
+ if (!stats) {
227
+ return /* @__PURE__ */ jsx("div", { className: "py-12 text-center text-sm text-gray-500", children: "No stats data found. Check ~/.claude/stats-cache.json" });
228
+ }
229
+ const totalTokens = Object.values(stats.modelUsage).reduce((sum, m) => sum + m.inputTokens + m.outputTokens, 0);
230
+ return /* @__PURE__ */ jsxs("div", { children: [
231
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold text-white", children: "Stats" }),
232
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-gray-400", children: [
233
+ "Aggregate usage from ",
234
+ stats.firstSessionDate?.split("T")[0],
235
+ " to",
236
+ " ",
237
+ stats.lastComputedDate
238
+ ] }),
239
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-2 gap-3 md:grid-cols-5", children: [
240
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Sessions", value: String(stats.totalSessions) }),
241
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Messages", value: stats.totalMessages.toLocaleString() }),
242
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Tokens", value: formatTokenCount(totalTokens), sub: cost ? `~${formatUSD(cost.totalUSD)}` : void 0 }),
243
+ /* @__PURE__ */ jsx(StatCard, { label: "Total Estimated Cost", value: cost ? `~${formatUSD(cost.totalUSD)}` : "N/A" }),
244
+ /* @__PURE__ */ jsx(StatCard, { label: "Longest Session", value: formatDuration(stats.longestSession.duration), sub: `${stats.longestSession.messageCount} messages` })
245
+ ] }),
246
+ /* @__PURE__ */ jsx("div", { className: "mt-6", children: /* @__PURE__ */ jsx(ActivityChart, { data: stats.dailyActivity }) }),
247
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 grid grid-cols-1 gap-4 md:grid-cols-2", children: [
248
+ /* @__PURE__ */ jsx(ModelUsageChart, { data: stats.modelUsage }),
249
+ /* @__PURE__ */ jsx(HourlyDistribution, { hourCounts: stats.hourCounts })
250
+ ] })
251
+ ] });
252
+ }
253
+ function StatCard({
254
+ label,
255
+ value,
256
+ sub
257
+ }) {
258
+ return /* @__PURE__ */ jsxs("div", { className: "rounded-xl border border-gray-800 bg-gray-900/50 p-4", children: [
259
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400", children: label }),
260
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-xl font-bold text-white", children: value }),
261
+ sub && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-gray-500", children: sub })
262
+ ] });
263
+ }
264
+ export {
265
+ StatsPage as component
266
+ };
@@ -0,0 +1,85 @@
1
+ import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
+ import * as fs from "node:fs";
3
+ import { a as getStatsPath } from "./claude-path-CkuljM34.js";
4
+ import { z } from "zod";
5
+ import { c as createServerFn } from "../server.js";
6
+ import "node:path";
7
+ import "node:os";
8
+ import "@tanstack/history";
9
+ import "@tanstack/router-core/ssr/client";
10
+ import "@tanstack/router-core";
11
+ import "node:async_hooks";
12
+ import "@tanstack/router-core/ssr/server";
13
+ import "h3-v2";
14
+ import "tiny-invariant";
15
+ import "seroval";
16
+ import "react/jsx-runtime";
17
+ import "@tanstack/react-router/ssr/server";
18
+ import "@tanstack/react-router";
19
+ const DailyActivitySchema = z.object({
20
+ date: z.string(),
21
+ messageCount: z.number(),
22
+ sessionCount: z.number(),
23
+ toolCallCount: z.number()
24
+ });
25
+ const DailyModelTokensSchema = z.object({
26
+ date: z.string(),
27
+ tokensByModel: z.record(z.string(), z.number())
28
+ });
29
+ const ModelUsageSchema = z.record(
30
+ z.string(),
31
+ z.object({
32
+ inputTokens: z.number(),
33
+ outputTokens: z.number(),
34
+ cacheReadInputTokens: z.number(),
35
+ cacheCreationInputTokens: z.number(),
36
+ webSearchRequests: z.number().optional(),
37
+ costUSD: z.number().optional()
38
+ })
39
+ );
40
+ const LongestSessionSchema = z.object({
41
+ sessionId: z.string(),
42
+ duration: z.number(),
43
+ messageCount: z.number(),
44
+ timestamp: z.string()
45
+ });
46
+ const StatsCacheSchema = z.object({
47
+ version: z.number(),
48
+ lastComputedDate: z.string(),
49
+ dailyActivity: z.array(DailyActivitySchema),
50
+ dailyModelTokens: z.array(DailyModelTokensSchema),
51
+ modelUsage: ModelUsageSchema,
52
+ totalSessions: z.number(),
53
+ totalMessages: z.number(),
54
+ longestSession: LongestSessionSchema,
55
+ firstSessionDate: z.string(),
56
+ hourCounts: z.record(z.string(), z.number()),
57
+ totalSpeculationTimeSavedMs: z.number().optional()
58
+ });
59
+ let cachedStats = null;
60
+ async function parseStats() {
61
+ const statsPath = getStatsPath();
62
+ const stat = await fs.promises.stat(statsPath).catch(() => null);
63
+ if (!stat) return null;
64
+ if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
65
+ return cachedStats.data;
66
+ }
67
+ const raw = await fs.promises.readFile(statsPath, "utf-8");
68
+ const parsed = JSON.parse(raw);
69
+ const result = StatsCacheSchema.parse(parsed);
70
+ cachedStats = { mtimeMs: stat.mtimeMs, data: result };
71
+ return result;
72
+ }
73
+ const getStats_createServerFn_handler = createServerRpc({
74
+ id: "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799",
75
+ name: "getStats",
76
+ filename: "src/features/stats/stats.server.ts"
77
+ }, (opts) => getStats.__executeServer(opts));
78
+ const getStats = createServerFn({
79
+ method: "GET"
80
+ }).handler(getStats_createServerFn_handler, async () => {
81
+ return parseStats();
82
+ });
83
+ export {
84
+ getStats_createServerFn_handler
85
+ };
@@ -0,0 +1,76 @@
1
+ import { useMemo } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
3
+ import { s as settingsQuery } from "./settings.queries-DSQd324O.js";
4
+ import { b as DEFAULT_PRICING, n as normalizeModelId } from "./settings.types-DntadCHo.js";
5
+ function getMergedPricing(settings) {
6
+ const table = {};
7
+ for (const model of DEFAULT_PRICING) {
8
+ const override = settings.pricingOverrides[model.modelId];
9
+ table[model.modelId] = override ? { ...model, ...override } : { ...model };
10
+ }
11
+ return table;
12
+ }
13
+ const FALLBACK_MODEL_ID = "claude-sonnet-4";
14
+ function calculateSessionCost(tokensByModel, pricingTable) {
15
+ const byModel = {};
16
+ const byCategory = {
17
+ input: 0,
18
+ output: 0,
19
+ cacheRead: 0,
20
+ cacheWrite: 0
21
+ };
22
+ for (const [rawModelId, tokens] of Object.entries(tokensByModel)) {
23
+ const normalized = normalizeModelId(rawModelId);
24
+ const pricing = pricingTable[normalized] ?? pricingTable[FALLBACK_MODEL_ID];
25
+ if (!pricing) continue;
26
+ const inputCost = tokens.inputTokens / 1e6 * pricing.inputPerMTok;
27
+ const outputCost = tokens.outputTokens / 1e6 * pricing.outputPerMTok;
28
+ const cacheReadCost = tokens.cacheReadInputTokens / 1e6 * pricing.cacheReadPerMTok;
29
+ const cacheWriteCost = tokens.cacheCreationInputTokens / 1e6 * pricing.cacheWritePerMTok;
30
+ const totalCost = inputCost + outputCost + cacheReadCost + cacheWriteCost;
31
+ const existing = byModel[normalized];
32
+ if (existing) {
33
+ existing.inputCost += inputCost;
34
+ existing.outputCost += outputCost;
35
+ existing.cacheReadCost += cacheReadCost;
36
+ existing.cacheWriteCost += cacheWriteCost;
37
+ existing.totalCost += totalCost;
38
+ existing.tokens.inputTokens += tokens.inputTokens;
39
+ existing.tokens.outputTokens += tokens.outputTokens;
40
+ existing.tokens.cacheReadInputTokens += tokens.cacheReadInputTokens;
41
+ existing.tokens.cacheCreationInputTokens += tokens.cacheCreationInputTokens;
42
+ } else {
43
+ byModel[normalized] = {
44
+ modelId: normalized,
45
+ displayName: pricing.displayName,
46
+ inputCost,
47
+ outputCost,
48
+ cacheReadCost,
49
+ cacheWriteCost,
50
+ totalCost,
51
+ tokens: { ...tokens }
52
+ };
53
+ }
54
+ byCategory.input += inputCost;
55
+ byCategory.output += outputCost;
56
+ byCategory.cacheRead += cacheReadCost;
57
+ byCategory.cacheWrite += cacheWriteCost;
58
+ }
59
+ const totalUSD = byCategory.input + byCategory.output + byCategory.cacheRead + byCategory.cacheWrite;
60
+ return { totalUSD, byModel, byCategory };
61
+ }
62
+ function useSessionCost(tokensByModel) {
63
+ const { data: settings, isLoading } = useQuery(settingsQuery);
64
+ const cost = useMemo(() => {
65
+ if (!settings) return null;
66
+ if (Object.keys(tokensByModel).length === 0) return null;
67
+ const pricingTable = getMergedPricing(settings);
68
+ return calculateSessionCost(tokensByModel, pricingTable);
69
+ }, [settings, tokensByModel]);
70
+ return { cost, isLoading };
71
+ }
72
+ export {
73
+ calculateSessionCost as c,
74
+ getMergedPricing as g,
75
+ useSessionCost as u
76
+ };