claude-session-dashboard 0.4.1 → 0.4.3

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.
@@ -0,0 +1,4 @@
1
+ const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/__root.tsx", "children": ["/", "/_dashboard"], "preloads": ["/assets/main-CV28H4XG.js"], "assets": [] }, "/": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/index.tsx" }, "/_dashboard": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/_dashboard.tsx", "children": ["/_dashboard/settings", "/_dashboard/stats", "/_dashboard/sessions/$sessionId", "/_dashboard/sessions/"], "assets": [], "preloads": ["/assets/_dashboard-t702m22X.js", "/assets/createServerFn-BYTDoNe-.js", "/assets/sessions.queries-tzrs5GhP.js"] }, "/_dashboard/settings": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/_dashboard/settings.tsx", "assets": [], "preloads": ["/assets/settings-D8yv1q93.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/stats": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/_dashboard/stats.tsx", "assets": [], "preloads": ["/assets/stats-C_6E4jyb.js", "/assets/format-Bsprb3az.js", "/assets/useSessionCost-BBu3AmcX.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/sessions/$sessionId": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/_dashboard/sessions/$sessionId.tsx", "assets": [], "preloads": ["/assets/_sessionId-D4Tpmmb5.js", "/assets/format-Bsprb3az.js", "/assets/useSessionCost-BBu3AmcX.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/sessions/": { "filePath": "/Users/dmytro.lupiak/GitHub/claude-session-dashboard/apps/web/src/routes/_dashboard/sessions/index.tsx", "assets": [], "preloads": ["/assets/index-DnK_zh3s.js", "/assets/format-Bsprb3az.js"] } }, "clientEntry": "/assets/main-CV28H4XG.js" });
2
+ export {
3
+ tsrStartManifest
4
+ };
@@ -1,11 +1,10 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
- import { s as scanAllSessions } from "./session-scanner-BzGf0Bqs.js";
2
+ import { a as scanAllSessions } from "./session-scanner-DRGzVO2T.js";
3
3
  import { c as createServerFn } from "../server.js";
4
4
  import "node:fs";
5
5
  import "node:path";
6
- import "./claude-path-BdwflgZ1.js";
6
+ import "./session-parser-DxLcS8VW.js";
7
7
  import "node:os";
8
- import "./session-parser-Bq8g2LOP.js";
9
8
  import "node:readline";
10
9
  import "@tanstack/history";
11
10
  import "@tanstack/router-core/ssr/client";
@@ -1,8 +1,7 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
2
  import * as path from "node:path";
3
3
  import * as fs from "node:fs";
4
- import { e as extractProjectName, a as getProjectsDir, d as decodeProjectDirName } from "./claude-path-BdwflgZ1.js";
5
- import { p as parseDetail } from "./session-parser-Bq8g2LOP.js";
4
+ import { e as extractProjectName, p as parseDetail, a as getProjectsDir, d as decodeProjectDirName } from "./session-parser-DxLcS8VW.js";
6
5
  import { c as createServerFn } from "../server.js";
7
6
  import "node:os";
8
7
  import "node:readline";
@@ -1,5 +1,54 @@
1
+ import * as path from "node:path";
2
+ import * as os from "node:os";
1
3
  import * as fs from "node:fs";
2
4
  import * as readline from "node:readline";
5
+ function resolveClaudeDir() {
6
+ if (process.env.CLAUDE_HOME) {
7
+ return path.resolve(process.env.CLAUDE_HOME);
8
+ }
9
+ return path.join(os.homedir(), ".claude");
10
+ }
11
+ const CLAUDE_DIR = resolveClaudeDir();
12
+ function getProjectsDir() {
13
+ return path.join(CLAUDE_DIR, "projects");
14
+ }
15
+ function getStatsPath() {
16
+ return path.join(CLAUDE_DIR, "stats-cache.json");
17
+ }
18
+ function decodeProjectDirName(dirName) {
19
+ return dirName.replace(/^-/, "/").replace(/-/g, "/");
20
+ }
21
+ function extractProjectName(decodedPath) {
22
+ return path.basename(decodedPath);
23
+ }
24
+ function extractSessionId(filename) {
25
+ return filename.replace(/\.jsonl$/, "");
26
+ }
27
+ const AGENT_FILE_PATTERN = /^agent-(.+)\.jsonl$/;
28
+ async function discoverSubagentFiles(sessionDir) {
29
+ const result = /* @__PURE__ */ new Map();
30
+ const candidateDirs = [
31
+ path.join(sessionDir, "subagents"),
32
+ path.join(sessionDir, "agents")
33
+ ];
34
+ for (const dir of candidateDirs) {
35
+ try {
36
+ const entries = await fs.promises.readdir(dir);
37
+ for (const entry of entries) {
38
+ const match = AGENT_FILE_PATTERN.exec(entry);
39
+ if (match) {
40
+ const agentId = match[1];
41
+ if (!result.has(agentId)) {
42
+ result.set(agentId, path.join(dir, entry));
43
+ }
44
+ }
45
+ }
46
+ } catch {
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+ const AGENT_DISPATCH_TOOL_NAMES = /* @__PURE__ */ new Set(["Task", "Agent"]);
3
52
  const HEAD_LINES = 15;
4
53
  async function parseSummary(filePath, sessionId, projectPath, projectName, fileSizeBytes) {
5
54
  const headLines = await readHeadLines(filePath, HEAD_LINES);
@@ -159,12 +208,13 @@ async function parseDetail(filePath, sessionId, projectPath, projectName) {
159
208
  input: block.input
160
209
  });
161
210
  toolFrequency[block.name] = (toolFrequency[block.name] ?? 0) + 1;
162
- if (block.name === "Task" && block.input) {
211
+ if (AGENT_DISPATCH_TOOL_NAMES.has(block.name) && block.input) {
163
212
  const inp = block.input;
164
- if (inp.subagent_type) {
213
+ const subagentType = inp.subagent_type ?? inp.agent_type;
214
+ if (subagentType) {
165
215
  const agent = {
166
- subagentType: String(inp.subagent_type),
167
- description: String(inp.description ?? ""),
216
+ subagentType: String(subagentType),
217
+ description: String(inp.description ?? inp.prompt ?? ""),
168
218
  timestamp: msg.timestamp ?? "",
169
219
  toolUseId: block.id ?? "",
170
220
  model: inp.model ? String(inp.model) : void 0
@@ -275,24 +325,24 @@ async function parseDetail(filePath, sessionId, projectPath, projectName) {
275
325
  }
276
326
  }
277
327
  if (resultText && toolUseId) {
278
- const agentIdMatch = resultText.match(/agentId:\s*(\w+)/);
328
+ const agentIdMatch = resultText.match(/agentId:\s*([\w-]+)/);
279
329
  if (agentIdMatch) {
280
330
  agentIdByToolUseId.set(String(toolUseId), agentIdMatch[1]);
281
331
  }
282
332
  }
283
333
  if (msg.toolUseResult && toolUseId) {
334
+ const result = msg.toolUseResult;
335
+ if (result.agentId) {
336
+ agentIdByToolUseId.set(String(toolUseId), result.agentId);
337
+ }
338
+ if (result.retrieval_status && result.task?.task_id) {
339
+ agentIdByToolUseId.set(String(toolUseId), result.task.task_id);
340
+ }
284
341
  const agent = agentByToolUseId.get(String(toolUseId));
285
342
  if (agent) {
286
- const result = msg.toolUseResult;
287
343
  if (result.totalTokens) agent.totalTokens = result.totalTokens;
288
344
  if (result.totalToolUseCount) agent.totalToolUseCount = result.totalToolUseCount;
289
345
  if (result.totalDurationMs) agent.durationMs = result.totalDurationMs;
290
- if (result.isAsync === true && result.agentId) {
291
- agentIdByToolUseId.set(String(toolUseId), result.agentId);
292
- }
293
- if (result.retrieval_status && result.task?.task_id) {
294
- agentIdByToolUseId.set(String(toolUseId), result.task.task_id);
295
- }
296
346
  }
297
347
  }
298
348
  }
@@ -330,50 +380,64 @@ async function parseDetail(filePath, sessionId, projectPath, projectName) {
330
380
  agent.model = actualModel;
331
381
  }
332
382
  }
333
- const subagentDir = filePath.replace(/\.jsonl$/, "");
383
+ const sessionDir = filePath.replace(/\.jsonl$/, "");
384
+ const subagentFileMap = await discoverSubagentFiles(sessionDir);
385
+ const matchedAgentIds = /* @__PURE__ */ new Set();
334
386
  await Promise.all(
335
387
  agents.map(async (agent) => {
336
388
  const agentId = agentIdByToolUseId.get(agent.toolUseId);
337
389
  if (!agentId) return;
338
390
  agent.agentId = agentId;
339
- const subagentFilePath = `${subagentDir}/subagents/agent-${agentId}.jsonl`;
391
+ matchedAgentIds.add(agentId);
392
+ const subagentFilePath = subagentFileMap.get(agentId);
393
+ if (!subagentFilePath) return;
340
394
  try {
341
395
  const detail = await parseSubagentDetail(subagentFilePath);
342
- agent.skills = detail.skills;
343
- if (!agent.tokens) {
344
- agent.tokens = detail.tokens;
345
- totalTokens.inputTokens += detail.tokens.inputTokens;
346
- totalTokens.outputTokens += detail.tokens.outputTokens;
347
- totalTokens.cacheReadInputTokens += detail.tokens.cacheReadInputTokens;
348
- totalTokens.cacheCreationInputTokens += detail.tokens.cacheCreationInputTokens;
349
- if (detail.model) {
350
- const modelId = detail.model;
351
- const existing = tokensByModel[modelId] ?? {
352
- inputTokens: 0,
353
- outputTokens: 0,
354
- cacheReadInputTokens: 0,
355
- cacheCreationInputTokens: 0
356
- };
357
- existing.inputTokens += detail.tokens.inputTokens;
358
- existing.outputTokens += detail.tokens.outputTokens;
359
- existing.cacheReadInputTokens += detail.tokens.cacheReadInputTokens;
360
- existing.cacheCreationInputTokens += detail.tokens.cacheCreationInputTokens;
361
- tokensByModel[modelId] = existing;
362
- }
363
- }
364
- if (!agent.toolCalls) {
365
- agent.toolCalls = detail.toolCalls;
366
- }
367
- if (!agent.model && detail.model) {
368
- agent.model = detail.model;
369
- }
370
- if (!agent.totalToolUseCount && detail.totalToolUseCount > 0) {
371
- agent.totalToolUseCount = detail.totalToolUseCount;
372
- }
396
+ mergeSubagentData(
397
+ agent,
398
+ detail,
399
+ agentProgressTokens.get(agent.toolUseId),
400
+ totalTokens,
401
+ tokensByModel
402
+ );
373
403
  } catch {
374
404
  }
375
405
  })
376
406
  );
407
+ for (const [agentId, subagentFilePath] of subagentFileMap) {
408
+ if (matchedAgentIds.has(agentId)) continue;
409
+ try {
410
+ const detail = await parseSubagentDetail(subagentFilePath);
411
+ const hasTokens = detail.tokens.inputTokens > 0 || detail.tokens.outputTokens > 0;
412
+ const hasActivity = hasTokens || detail.skills.length > 0 || detail.totalToolUseCount > 0;
413
+ if (hasTokens) {
414
+ addTokens(totalTokens, detail.tokens);
415
+ if (detail.model) {
416
+ const existing = tokensByModel[detail.model] ?? createEmptyTokenUsage();
417
+ addTokens(existing, detail.tokens);
418
+ tokensByModel[detail.model] = existing;
419
+ }
420
+ }
421
+ for (const [toolName, count] of Object.entries(detail.toolCalls)) {
422
+ toolFrequency[toolName] = (toolFrequency[toolName] ?? 0) + count;
423
+ }
424
+ if (hasActivity) {
425
+ agents.push({
426
+ subagentType: "unknown",
427
+ description: "",
428
+ timestamp: "",
429
+ toolUseId: `orphan-${agentId}`,
430
+ agentId,
431
+ tokens: detail.tokens,
432
+ toolCalls: detail.toolCalls,
433
+ model: detail.model,
434
+ totalToolUseCount: detail.totalToolUseCount,
435
+ skills: detail.skills
436
+ });
437
+ }
438
+ } catch {
439
+ }
440
+ }
377
441
  const modelName = modelsSet.size > 0 ? Array.from(modelsSet)[0] : "unknown";
378
442
  const contextWindow = buildContextWindowData(
379
443
  contextSnapshots,
@@ -430,7 +494,7 @@ async function parseSubagentDetail(subagentFilePath) {
430
494
  skill: skillName,
431
495
  args: null,
432
496
  timestamp: msg.timestamp ?? "",
433
- toolUseId: `injected-${skillName}-${msg.timestamp ?? lineCount}`,
497
+ toolUseId: `injected-${skillName}-${lineCount}`,
434
498
  source: "injected"
435
499
  });
436
500
  }
@@ -500,6 +564,56 @@ function buildContextWindowData(snapshots, modelName) {
500
564
  snapshots
501
565
  };
502
566
  }
567
+ function createEmptyTokenUsage() {
568
+ return {
569
+ inputTokens: 0,
570
+ outputTokens: 0,
571
+ cacheReadInputTokens: 0,
572
+ cacheCreationInputTokens: 0
573
+ };
574
+ }
575
+ function addTokens(target, source) {
576
+ target.inputTokens += source.inputTokens;
577
+ target.outputTokens += source.outputTokens;
578
+ target.cacheReadInputTokens += source.cacheReadInputTokens;
579
+ target.cacheCreationInputTokens += source.cacheCreationInputTokens;
580
+ }
581
+ function subtractTokens(target, source) {
582
+ target.inputTokens -= source.inputTokens;
583
+ target.outputTokens -= source.outputTokens;
584
+ target.cacheReadInputTokens -= source.cacheReadInputTokens;
585
+ target.cacheCreationInputTokens -= source.cacheCreationInputTokens;
586
+ }
587
+ function mergeSubagentData(agent, detail, progressTokens, totalTokens, tokensByModel) {
588
+ agent.skills = detail.skills;
589
+ if (detail.tokens.inputTokens > 0 || detail.tokens.outputTokens > 0) {
590
+ if (progressTokens) {
591
+ subtractTokens(totalTokens, progressTokens);
592
+ const progressModel = agent.model;
593
+ if (progressModel && tokensByModel[progressModel]) {
594
+ subtractTokens(tokensByModel[progressModel], progressTokens);
595
+ }
596
+ }
597
+ agent.tokens = detail.tokens;
598
+ addTokens(totalTokens, detail.tokens);
599
+ if (detail.model) {
600
+ const existing = tokensByModel[detail.model] ?? createEmptyTokenUsage();
601
+ addTokens(existing, detail.tokens);
602
+ tokensByModel[detail.model] = existing;
603
+ }
604
+ } else if (!agent.tokens && progressTokens) {
605
+ agent.tokens = progressTokens;
606
+ }
607
+ if (!agent.toolCalls && Object.keys(detail.toolCalls).length > 0) {
608
+ agent.toolCalls = detail.toolCalls;
609
+ }
610
+ if (!agent.model && detail.model) {
611
+ agent.model = detail.model;
612
+ }
613
+ if (!agent.totalToolUseCount && detail.totalToolUseCount > 0) {
614
+ agent.totalToolUseCount = detail.totalToolUseCount;
615
+ }
616
+ }
503
617
  async function readHeadLines(filePath, count) {
504
618
  const lines = [];
505
619
  const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
@@ -509,6 +623,7 @@ async function readHeadLines(filePath, count) {
509
623
  if (lines.length >= count) break;
510
624
  }
511
625
  stream.destroy();
626
+ rl.close();
512
627
  return lines;
513
628
  }
514
629
  async function readTailLines(filePath, count) {
@@ -549,6 +664,11 @@ function extractTextContent(msg) {
549
664
  return texts.length > 0 ? texts.join("\n").slice(0, 500) : void 0;
550
665
  }
551
666
  export {
552
- parseSummary as a,
667
+ getProjectsDir as a,
668
+ extractSessionId as b,
669
+ parseSummary as c,
670
+ decodeProjectDirName as d,
671
+ extractProjectName as e,
672
+ getStatsPath as g,
553
673
  parseDetail as p
554
674
  };
@@ -1,7 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { a as getProjectsDir, d as decodeProjectDirName, e as extractProjectName, b as extractSessionId } from "./claude-path-BdwflgZ1.js";
4
- import { a as parseSummary } from "./session-parser-Bq8g2LOP.js";
3
+ import { a as getProjectsDir, d as decodeProjectDirName, e as extractProjectName, b as extractSessionId, c as parseSummary } from "./session-parser-DxLcS8VW.js";
5
4
  async function scanProjects() {
6
5
  const projectsDir = getProjectsDir();
7
6
  let entries;
@@ -41,7 +40,7 @@ async function isSessionActive(projectDirName, sessionId) {
41
40
  return lockStat?.isDirectory() ?? false;
42
41
  }
43
42
  const summaryCache = /* @__PURE__ */ new Map();
44
- async function scanAllSessions() {
43
+ async function scanSessionsInternal() {
45
44
  const projects = await scanProjects();
46
45
  const summaries = [];
47
46
  for (const project of projects) {
@@ -57,7 +56,7 @@ async function scanAllSessions() {
57
56
  const cached = summaryCache.get(sessionId);
58
57
  if (cached && cached.mtimeMs === stat.mtimeMs) {
59
58
  const active = await isSessionActive(project.dirName, sessionId);
60
- summaries.push({ ...cached.summary, isActive: active });
59
+ summaries.push({ ...cached.summary, isActive: active, filePath });
61
60
  continue;
62
61
  }
63
62
  const summary = await parseSummary(
@@ -74,7 +73,7 @@ async function scanAllSessions() {
74
73
  mtimeMs: stat.mtimeMs,
75
74
  summary
76
75
  });
77
- summaries.push(summary);
76
+ summaries.push({ ...summary, filePath });
78
77
  }
79
78
  }
80
79
  }
@@ -83,11 +82,19 @@ async function scanAllSessions() {
83
82
  );
84
83
  return summaries;
85
84
  }
85
+ async function scanAllSessions() {
86
+ const results = await scanSessionsInternal();
87
+ return results.map(({ filePath: _filePath, ...summary }) => summary);
88
+ }
89
+ async function scanAllSessionsWithPaths() {
90
+ return scanSessionsInternal();
91
+ }
86
92
  async function getActiveSessions() {
87
93
  const all = await scanAllSessions();
88
94
  return all.filter((s) => s.isActive);
89
95
  }
90
96
  export {
97
+ scanAllSessions as a,
91
98
  getActiveSessions as g,
92
- scanAllSessions as s
99
+ scanAllSessionsWithPaths as s
93
100
  };
@@ -1,12 +1,11 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
2
  import { z } from "zod";
3
- import { s as scanAllSessions, g as getActiveSessions } from "./session-scanner-BzGf0Bqs.js";
3
+ import { a as scanAllSessions, g as getActiveSessions } from "./session-scanner-DRGzVO2T.js";
4
4
  import { c as createServerFn } from "../server.js";
5
5
  import "node:fs";
6
6
  import "node:path";
7
- import "./claude-path-BdwflgZ1.js";
7
+ import "./session-parser-DxLcS8VW.js";
8
8
  import "node:os";
9
- import "./session-parser-Bq8g2LOP.js";
10
9
  import "node:readline";
11
10
  import "@tanstack/history";
12
11
  import "@tanstack/router-core/ssr/client";
@@ -0,0 +1,398 @@
1
+ import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
+ import * as fs from "node:fs";
3
+ import { g as getStatsPath, p as parseDetail } from "./session-parser-DxLcS8VW.js";
4
+ import * as path from "node:path";
5
+ import * as os from "node:os";
6
+ import { z } from "zod";
7
+ import { s as scanAllSessionsWithPaths } from "./session-scanner-DRGzVO2T.js";
8
+ import { c as createServerFn } from "../server.js";
9
+ import "node:readline";
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 "react/jsx-runtime";
19
+ import "@tanstack/react-router/ssr/server";
20
+ import "@tanstack/react-router";
21
+ const CACHE_VERSION = 1;
22
+ function getCacheDir() {
23
+ return path.join(os.homedir(), ".claude-dashboard", "cache");
24
+ }
25
+ function getCachePath(cacheKey) {
26
+ return path.join(getCacheDir(), `${cacheKey}.cache.json`);
27
+ }
28
+ function readDiskCache(cacheKey, sourceMtimeMs, schema) {
29
+ try {
30
+ const cachePath = getCachePath(cacheKey);
31
+ if (!fs.existsSync(cachePath)) {
32
+ return null;
33
+ }
34
+ const raw = fs.readFileSync(cachePath, "utf-8");
35
+ const parsed = JSON.parse(raw);
36
+ if (parsed.version !== CACHE_VERSION) {
37
+ return null;
38
+ }
39
+ if (parsed.sourceMtimeMs !== sourceMtimeMs) {
40
+ return null;
41
+ }
42
+ const result = schema.safeParse(parsed.data);
43
+ if (!result.success) {
44
+ console.warn(`[disk-cache] Zod validation failed for "${cacheKey}":`, result.error.message);
45
+ return null;
46
+ }
47
+ return result.data;
48
+ } catch (error) {
49
+ const message = error instanceof Error ? error.message : String(error);
50
+ console.warn(`[disk-cache] Read failed for "${cacheKey}":`, message);
51
+ return null;
52
+ }
53
+ }
54
+ function writeDiskCache(cacheKey, sourceFile, sourceMtimeMs, data) {
55
+ try {
56
+ const cacheDir = getCacheDir();
57
+ fs.mkdirSync(cacheDir, { recursive: true });
58
+ const cachePath = getCachePath(cacheKey);
59
+ const tmpPath = `${cachePath}.tmp`;
60
+ const entry = {
61
+ version: CACHE_VERSION,
62
+ sourceFile,
63
+ sourceMtimeMs,
64
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
65
+ data
66
+ };
67
+ fs.writeFileSync(tmpPath, JSON.stringify(entry), "utf-8");
68
+ fs.renameSync(tmpPath, cachePath);
69
+ } catch (error) {
70
+ const message = error instanceof Error ? error.message : String(error);
71
+ console.warn(`[disk-cache] Write failed for "${cacheKey}":`, message);
72
+ }
73
+ }
74
+ const DailyActivitySchema = z.object({
75
+ date: z.string(),
76
+ messageCount: z.number(),
77
+ sessionCount: z.number(),
78
+ toolCallCount: z.number()
79
+ });
80
+ const DailyModelTokensSchema = z.object({
81
+ date: z.string(),
82
+ tokensByModel: z.record(z.string(), z.number())
83
+ });
84
+ const ModelUsageSchema = z.record(
85
+ z.string(),
86
+ z.object({
87
+ inputTokens: z.number(),
88
+ outputTokens: z.number(),
89
+ cacheReadInputTokens: z.number(),
90
+ cacheCreationInputTokens: z.number(),
91
+ webSearchRequests: z.number().optional(),
92
+ costUSD: z.number().optional()
93
+ })
94
+ );
95
+ const LongestSessionSchema = z.object({
96
+ sessionId: z.string(),
97
+ duration: z.number(),
98
+ messageCount: z.number(),
99
+ timestamp: z.string()
100
+ });
101
+ const StatsCacheSchema = z.object({
102
+ version: z.number(),
103
+ lastComputedDate: z.string(),
104
+ dailyActivity: z.array(DailyActivitySchema),
105
+ dailyModelTokens: z.array(DailyModelTokensSchema),
106
+ modelUsage: ModelUsageSchema,
107
+ totalSessions: z.number(),
108
+ totalMessages: z.number(),
109
+ longestSession: LongestSessionSchema,
110
+ firstSessionDate: z.string(),
111
+ hourCounts: z.record(z.string(), z.number()),
112
+ totalSpeculationTimeSavedMs: z.number().optional()
113
+ });
114
+ let cachedStats = null;
115
+ let mergedCache = null;
116
+ const MERGE_STALENESS_MS = 6e4;
117
+ function getTodayDateString() {
118
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
119
+ }
120
+ function extractDateString(isoOrDate) {
121
+ return isoOrDate.split("T")[0];
122
+ }
123
+ async function parseStats() {
124
+ const statsPath = getStatsPath();
125
+ const stat = await fs.promises.stat(statsPath).catch(() => null);
126
+ if (!stat) {
127
+ try {
128
+ const computed = await computeStatsFromSessions();
129
+ return computed;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
135
+ return maybeEnrichWithRecentSessions(cachedStats.data, stat.mtimeMs);
136
+ }
137
+ const diskResult = readDiskCache("stats", stat.mtimeMs, StatsCacheSchema);
138
+ if (diskResult) {
139
+ cachedStats = { mtimeMs: stat.mtimeMs, data: diskResult };
140
+ return maybeEnrichWithRecentSessions(diskResult, stat.mtimeMs);
141
+ }
142
+ try {
143
+ const raw = await fs.promises.readFile(statsPath, "utf-8");
144
+ const parsed = JSON.parse(raw);
145
+ const result = StatsCacheSchema.parse(parsed);
146
+ writeDiskCache("stats", statsPath, stat.mtimeMs, result);
147
+ cachedStats = { mtimeMs: stat.mtimeMs, data: result };
148
+ return maybeEnrichWithRecentSessions(result, stat.mtimeMs);
149
+ } catch {
150
+ const computed = await computeStatsFromSessions();
151
+ return computed;
152
+ }
153
+ }
154
+ async function maybeEnrichWithRecentSessions(stats, mtimeMs) {
155
+ const today = getTodayDateString();
156
+ const lastComputed = extractDateString(stats.lastComputedDate);
157
+ if (lastComputed >= today) {
158
+ return stats;
159
+ }
160
+ if (mergedCache && mergedCache.mtimeMs === mtimeMs && Date.now() - mergedCache.mergedAt < MERGE_STALENESS_MS) {
161
+ return mergedCache.data;
162
+ }
163
+ try {
164
+ const merged = await mergeRecentSessions(stats);
165
+ mergedCache = { mtimeMs, mergedAt: Date.now(), data: merged };
166
+ return merged;
167
+ } catch {
168
+ return stats;
169
+ }
170
+ }
171
+ async function parseDetailsInBatches(sessions, batchSize = 10) {
172
+ const results = /* @__PURE__ */ new Map();
173
+ for (let i = 0; i < sessions.length; i += batchSize) {
174
+ const batch = sessions.slice(i, i + batchSize);
175
+ const details = await Promise.all(
176
+ batch.map(async (s) => {
177
+ try {
178
+ return {
179
+ sessionId: s.sessionId,
180
+ detail: await parseDetail(
181
+ s.filePath,
182
+ s.sessionId,
183
+ s.projectPath,
184
+ s.projectName
185
+ )
186
+ };
187
+ } catch {
188
+ return null;
189
+ }
190
+ })
191
+ );
192
+ for (const result of details) {
193
+ if (result) results.set(result.sessionId, result.detail);
194
+ }
195
+ }
196
+ return results;
197
+ }
198
+ async function mergeRecentSessions(stats) {
199
+ const summaries = await scanAllSessionsWithPaths();
200
+ const cutoffDate = extractDateString(stats.lastComputedDate);
201
+ const recentSessions = summaries.filter((s) => {
202
+ const sessionDate = extractDateString(s.lastActiveAt ?? s.startedAt);
203
+ return sessionDate > cutoffDate;
204
+ });
205
+ if (recentSessions.length === 0) {
206
+ return stats;
207
+ }
208
+ const detailMap = await parseDetailsInBatches(recentSessions);
209
+ const activityMap = /* @__PURE__ */ new Map();
210
+ for (const entry of stats.dailyActivity) {
211
+ activityMap.set(entry.date, {
212
+ messageCount: entry.messageCount,
213
+ sessionCount: entry.sessionCount,
214
+ toolCallCount: entry.toolCallCount
215
+ });
216
+ }
217
+ const modelTokensMap = /* @__PURE__ */ new Map();
218
+ for (const entry of stats.dailyModelTokens) {
219
+ modelTokensMap.set(entry.date, { ...entry.tokensByModel });
220
+ }
221
+ const hourCounts = { ...stats.hourCounts };
222
+ const modelUsage = {};
223
+ for (const [model, usage] of Object.entries(stats.modelUsage)) {
224
+ modelUsage[model] = { ...usage };
225
+ }
226
+ let additionalMessages = 0;
227
+ const additionalSessions = recentSessions.length;
228
+ let longestSession = { ...stats.longestSession };
229
+ const existingSessionCount = stats.totalSessions;
230
+ for (const s of recentSessions) {
231
+ const date = extractDateString(s.lastActiveAt ?? s.startedAt);
232
+ const detail = detailMap.get(s.sessionId);
233
+ const cur = activityMap.get(date) ?? { messageCount: 0, sessionCount: 0, toolCallCount: 0 };
234
+ cur.sessionCount += 1;
235
+ if (detail) {
236
+ cur.messageCount += detail.turns.length;
237
+ cur.toolCallCount += Object.values(detail.toolFrequency).reduce((sum, n) => sum + n, 0);
238
+ const dayTokens = modelTokensMap.get(date) ?? {};
239
+ for (const [model, usage] of Object.entries(detail.tokensByModel)) {
240
+ const total = usage.inputTokens + usage.outputTokens;
241
+ dayTokens[model] = (dayTokens[model] ?? 0) + total;
242
+ }
243
+ modelTokensMap.set(date, dayTokens);
244
+ for (const [model, usage] of Object.entries(detail.tokensByModel)) {
245
+ const existing = modelUsage[model] ?? {
246
+ inputTokens: 0,
247
+ outputTokens: 0,
248
+ cacheReadInputTokens: 0,
249
+ cacheCreationInputTokens: 0
250
+ };
251
+ existing.inputTokens += usage.inputTokens;
252
+ existing.outputTokens += usage.outputTokens;
253
+ existing.cacheReadInputTokens += usage.cacheReadInputTokens;
254
+ existing.cacheCreationInputTokens += usage.cacheCreationInputTokens;
255
+ modelUsage[model] = existing;
256
+ }
257
+ additionalMessages += detail.turns.length;
258
+ } else {
259
+ cur.messageCount += s.messageCount;
260
+ additionalMessages += s.messageCount;
261
+ if (!modelTokensMap.has(date)) {
262
+ modelTokensMap.set(date, {});
263
+ }
264
+ }
265
+ activityMap.set(date, cur);
266
+ updateHourCounts(hourCounts, s);
267
+ if (s.durationMs > longestSession.duration) {
268
+ longestSession = {
269
+ sessionId: s.sessionId,
270
+ duration: s.durationMs,
271
+ messageCount: detail?.turns.length ?? s.messageCount,
272
+ timestamp: s.lastActiveAt ?? s.startedAt
273
+ };
274
+ }
275
+ }
276
+ const dailyActivity = Array.from(activityMap.entries()).map(([date, v]) => ({
277
+ date,
278
+ messageCount: v.messageCount,
279
+ sessionCount: v.sessionCount,
280
+ toolCallCount: v.toolCallCount
281
+ })).sort((a, b) => a.date < b.date ? -1 : 1);
282
+ const dailyModelTokens = Array.from(modelTokensMap.entries()).map(([date, tokensByModel]) => ({ date, tokensByModel })).sort((a, b) => a.date < b.date ? -1 : 1);
283
+ return {
284
+ ...stats,
285
+ dailyActivity,
286
+ dailyModelTokens,
287
+ modelUsage,
288
+ totalSessions: existingSessionCount + additionalSessions,
289
+ totalMessages: stats.totalMessages + additionalMessages,
290
+ longestSession,
291
+ hourCounts
292
+ };
293
+ }
294
+ function updateHourCounts(hourCounts, session) {
295
+ const startedAt = session.startedAt;
296
+ if (!startedAt) return;
297
+ try {
298
+ const date = new Date(startedAt);
299
+ const hour = date.getHours().toString();
300
+ hourCounts[hour] = (hourCounts[hour] ?? 0) + 1;
301
+ } catch {
302
+ }
303
+ }
304
+ async function computeStatsFromSessions() {
305
+ try {
306
+ const summaries = await scanAllSessionsWithPaths();
307
+ const detailMap = await parseDetailsInBatches(summaries);
308
+ const activityMap = /* @__PURE__ */ new Map();
309
+ const modelTokensMap = /* @__PURE__ */ new Map();
310
+ const modelUsage = {};
311
+ const hourCounts = {};
312
+ let totalMessages = 0;
313
+ let longestSession = { sessionId: "", duration: 0, messageCount: 0, timestamp: "" };
314
+ let firstSessionDate = null;
315
+ for (const s of summaries) {
316
+ const d = (s.lastActiveAt ?? s.startedAt).split("T")[0];
317
+ const detail = detailMap.get(s.sessionId);
318
+ const cur = activityMap.get(d) ?? { messageCount: 0, sessionCount: 0, toolCallCount: 0 };
319
+ cur.sessionCount += 1;
320
+ if (detail) {
321
+ cur.messageCount += detail.turns.length;
322
+ cur.toolCallCount += Object.values(detail.toolFrequency).reduce((sum, n) => sum + n, 0);
323
+ totalMessages += detail.turns.length;
324
+ const dayTokens = modelTokensMap.get(d) ?? {};
325
+ for (const [model, usage] of Object.entries(detail.tokensByModel)) {
326
+ const total = usage.inputTokens + usage.outputTokens;
327
+ dayTokens[model] = (dayTokens[model] ?? 0) + total;
328
+ }
329
+ modelTokensMap.set(d, dayTokens);
330
+ for (const [model, usage] of Object.entries(detail.tokensByModel)) {
331
+ const existing = modelUsage[model] ?? {
332
+ inputTokens: 0,
333
+ outputTokens: 0,
334
+ cacheReadInputTokens: 0,
335
+ cacheCreationInputTokens: 0
336
+ };
337
+ existing.inputTokens += usage.inputTokens;
338
+ existing.outputTokens += usage.outputTokens;
339
+ existing.cacheReadInputTokens += usage.cacheReadInputTokens;
340
+ existing.cacheCreationInputTokens += usage.cacheCreationInputTokens;
341
+ modelUsage[model] = existing;
342
+ }
343
+ } else {
344
+ cur.messageCount += s.messageCount;
345
+ totalMessages += s.messageCount;
346
+ if (!modelTokensMap.has(d)) modelTokensMap.set(d, {});
347
+ }
348
+ activityMap.set(d, cur);
349
+ updateHourCounts(hourCounts, s);
350
+ const msgCount = detail?.turns.length ?? s.messageCount;
351
+ if (s.durationMs > longestSession.duration) {
352
+ longestSession = {
353
+ sessionId: s.sessionId,
354
+ duration: s.durationMs,
355
+ messageCount: msgCount,
356
+ timestamp: s.lastActiveAt ?? s.startedAt
357
+ };
358
+ }
359
+ if (!firstSessionDate || s.startedAt < firstSessionDate) {
360
+ firstSessionDate = s.startedAt;
361
+ }
362
+ }
363
+ const dailyActivity = Array.from(activityMap.entries()).map(([date, v]) => ({ date, ...v })).sort((a, b) => a.date < b.date ? -1 : 1);
364
+ const dailyModelTokens = Array.from(modelTokensMap.entries()).map(([date, tokensByModel]) => ({ date, tokensByModel })).sort((a, b) => a.date < b.date ? -1 : 1);
365
+ return {
366
+ version: 1,
367
+ lastComputedDate: (/* @__PURE__ */ new Date()).toISOString(),
368
+ dailyActivity,
369
+ dailyModelTokens,
370
+ modelUsage,
371
+ totalSessions: summaries.length,
372
+ totalMessages,
373
+ longestSession: {
374
+ sessionId: longestSession.sessionId,
375
+ duration: longestSession.duration,
376
+ messageCount: longestSession.messageCount,
377
+ timestamp: longestSession.timestamp || (/* @__PURE__ */ new Date()).toISOString()
378
+ },
379
+ firstSessionDate: firstSessionDate ?? (/* @__PURE__ */ new Date()).toISOString(),
380
+ hourCounts
381
+ };
382
+ } catch {
383
+ return null;
384
+ }
385
+ }
386
+ const getStats_createServerFn_handler = createServerRpc({
387
+ id: "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799",
388
+ name: "getStats",
389
+ filename: "src/features/stats/stats.server.ts"
390
+ }, (opts) => getStats.__executeServer(opts));
391
+ const getStats = createServerFn({
392
+ method: "GET"
393
+ }).handler(getStats_createServerFn_handler, async () => {
394
+ return parseStats();
395
+ });
396
+ export {
397
+ getStats_createServerFn_handler
398
+ };
@@ -423,7 +423,7 @@ function getResponse() {
423
423
  return event.res;
424
424
  }
425
425
  async function getStartManifest(matchedRoutes) {
426
- const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-JNF_5-F_.js");
426
+ const { tsrStartManifest } = await import("./assets/_tanstack-start-manifest_v-Bzp9kpGN.js");
427
427
  const startManifest = tsrStartManifest();
428
428
  const rootRoute = startManifest.routes[rootRouteId] = startManifest.routes[rootRouteId] || {};
429
429
  rootRoute.assets = rootRoute.assets || [];
@@ -579,28 +579,28 @@ function createMultiplexedStream(jsonStream, rawStreams) {
579
579
  }
580
580
  const manifest = { "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799": {
581
581
  functionName: "getStats_createServerFn_handler",
582
- importer: () => import("./assets/stats.server-qTOvID9-.js")
582
+ importer: () => import("./assets/stats.server-iJ_z7eUN.js")
583
583
  }, "ff8a3161afdfa175e9c519e4146a56ab5bce6e80745e99cfc2191ebbb7a859bb": {
584
584
  functionName: "getSessionDetail_createServerFn_handler",
585
- importer: () => import("./assets/session-detail.server-4Cp5Zyo1.js")
586
- }, "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d": {
587
- functionName: "getSessionList_createServerFn_handler",
588
- importer: () => import("./assets/sessions.server-XaGOdz4f.js")
589
- }, "839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f": {
590
- functionName: "getActiveSessionList_createServerFn_handler",
591
- importer: () => import("./assets/sessions.server-XaGOdz4f.js")
592
- }, "a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495": {
593
- functionName: "getPaginatedSessions_createServerFn_handler",
594
- importer: () => import("./assets/sessions.server-XaGOdz4f.js")
585
+ importer: () => import("./assets/session-detail.server-CO5XwLSv.js")
595
586
  }, "810657681a273df5b4e58f0d8fcc6a5451598b489431b9bcaa98eea0ad815da8": {
596
587
  functionName: "getSettings_createServerFn_handler",
597
588
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
598
589
  }, "3050115d92ca91ab1fd8fd698e33076328aae80dc64ca27c088eee16cebccc1a": {
599
590
  functionName: "saveSettings_createServerFn_handler",
600
591
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
592
+ }, "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d": {
593
+ functionName: "getSessionList_createServerFn_handler",
594
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
595
+ }, "839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f": {
596
+ functionName: "getActiveSessionList_createServerFn_handler",
597
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
598
+ }, "a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495": {
599
+ functionName: "getPaginatedSessions_createServerFn_handler",
600
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
601
601
  }, "64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb": {
602
602
  functionName: "getProjectAnalytics_createServerFn_handler",
603
- importer: () => import("./assets/project-analytics.server-aftsdOgf.js")
603
+ importer: () => import("./assets/project-analytics.server-kTooOGl-.js")
604
604
  } };
605
605
  async function getServerFnById(id) {
606
606
  const serverFnInfo = manifest[id];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-dashboard",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Local observability dashboard for Claude Code sessions",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,4 +0,0 @@
1
- const tsrStartManifest = () => ({ "routes": { "__root__": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/__root.tsx", "children": ["/", "/_dashboard"], "preloads": ["/assets/main-CV28H4XG.js"], "assets": [] }, "/": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/index.tsx" }, "/_dashboard": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/_dashboard.tsx", "children": ["/_dashboard/settings", "/_dashboard/stats", "/_dashboard/sessions/$sessionId", "/_dashboard/sessions/"], "assets": [], "preloads": ["/assets/_dashboard-t702m22X.js", "/assets/createServerFn-BYTDoNe-.js", "/assets/sessions.queries-tzrs5GhP.js"] }, "/_dashboard/settings": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/_dashboard/settings.tsx", "assets": [], "preloads": ["/assets/settings-D8yv1q93.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/stats": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/_dashboard/stats.tsx", "assets": [], "preloads": ["/assets/stats-C_6E4jyb.js", "/assets/format-Bsprb3az.js", "/assets/useSessionCost-BBu3AmcX.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/sessions/$sessionId": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/_dashboard/sessions/$sessionId.tsx", "assets": [], "preloads": ["/assets/_sessionId-D4Tpmmb5.js", "/assets/format-Bsprb3az.js", "/assets/useSessionCost-BBu3AmcX.js", "/assets/settings.types-CMYAW0cQ.js"] }, "/_dashboard/sessions/": { "filePath": "/home/runner/work/claude-session-dashboard/claude-session-dashboard/apps/web/src/routes/_dashboard/sessions/index.tsx", "assets": [], "preloads": ["/assets/index-DnK_zh3s.js", "/assets/format-Bsprb3az.js"] } }, "clientEntry": "/assets/main-CV28H4XG.js" });
2
- export {
3
- tsrStartManifest
4
- };
@@ -1,31 +0,0 @@
1
- import * as path from "node:path";
2
- import * as os from "node:os";
3
- function resolveClaudeDir() {
4
- if (process.env.CLAUDE_HOME) {
5
- return path.resolve(process.env.CLAUDE_HOME);
6
- }
7
- return path.join(os.homedir(), ".claude");
8
- }
9
- const CLAUDE_DIR = resolveClaudeDir();
10
- function getProjectsDir() {
11
- return path.join(CLAUDE_DIR, "projects");
12
- }
13
- function getStatsPath() {
14
- return path.join(CLAUDE_DIR, "stats-cache.json");
15
- }
16
- function decodeProjectDirName(dirName) {
17
- return dirName.replace(/^-/, "/").replace(/-/g, "/");
18
- }
19
- function extractProjectName(decodedPath) {
20
- return path.basename(decodedPath);
21
- }
22
- function extractSessionId(filename) {
23
- return filename.replace(/\.jsonl$/, "");
24
- }
25
- export {
26
- getProjectsDir as a,
27
- extractSessionId as b,
28
- decodeProjectDirName as d,
29
- extractProjectName as e,
30
- getStatsPath as g
31
- };
@@ -1,144 +0,0 @@
1
- import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
- import * as fs from "node:fs";
3
- import { g as getStatsPath } from "./claude-path-BdwflgZ1.js";
4
- import * as path from "node:path";
5
- import * as os from "node:os";
6
- import { z } from "zod";
7
- import { c as createServerFn } from "../server.js";
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 CACHE_VERSION = 1;
20
- function getCacheDir() {
21
- return path.join(os.homedir(), ".claude-dashboard", "cache");
22
- }
23
- function getCachePath(cacheKey) {
24
- return path.join(getCacheDir(), `${cacheKey}.cache.json`);
25
- }
26
- function readDiskCache(cacheKey, sourceMtimeMs, schema) {
27
- try {
28
- const cachePath = getCachePath(cacheKey);
29
- if (!fs.existsSync(cachePath)) {
30
- return null;
31
- }
32
- const raw = fs.readFileSync(cachePath, "utf-8");
33
- const parsed = JSON.parse(raw);
34
- if (parsed.version !== CACHE_VERSION) {
35
- return null;
36
- }
37
- if (parsed.sourceMtimeMs !== sourceMtimeMs) {
38
- return null;
39
- }
40
- const result = schema.safeParse(parsed.data);
41
- if (!result.success) {
42
- console.warn(`[disk-cache] Zod validation failed for "${cacheKey}":`, result.error.message);
43
- return null;
44
- }
45
- return result.data;
46
- } catch (error) {
47
- const message = error instanceof Error ? error.message : String(error);
48
- console.warn(`[disk-cache] Read failed for "${cacheKey}":`, message);
49
- return null;
50
- }
51
- }
52
- function writeDiskCache(cacheKey, sourceFile, sourceMtimeMs, data) {
53
- try {
54
- const cacheDir = getCacheDir();
55
- fs.mkdirSync(cacheDir, { recursive: true });
56
- const cachePath = getCachePath(cacheKey);
57
- const tmpPath = `${cachePath}.tmp`;
58
- const entry = {
59
- version: CACHE_VERSION,
60
- sourceFile,
61
- sourceMtimeMs,
62
- cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
63
- data
64
- };
65
- fs.writeFileSync(tmpPath, JSON.stringify(entry), "utf-8");
66
- fs.renameSync(tmpPath, cachePath);
67
- } catch (error) {
68
- const message = error instanceof Error ? error.message : String(error);
69
- console.warn(`[disk-cache] Write failed for "${cacheKey}":`, message);
70
- }
71
- }
72
- const DailyActivitySchema = z.object({
73
- date: z.string(),
74
- messageCount: z.number(),
75
- sessionCount: z.number(),
76
- toolCallCount: z.number()
77
- });
78
- const DailyModelTokensSchema = z.object({
79
- date: z.string(),
80
- tokensByModel: z.record(z.string(), z.number())
81
- });
82
- const ModelUsageSchema = z.record(
83
- z.string(),
84
- z.object({
85
- inputTokens: z.number(),
86
- outputTokens: z.number(),
87
- cacheReadInputTokens: z.number(),
88
- cacheCreationInputTokens: z.number(),
89
- webSearchRequests: z.number().optional(),
90
- costUSD: z.number().optional()
91
- })
92
- );
93
- const LongestSessionSchema = z.object({
94
- sessionId: z.string(),
95
- duration: z.number(),
96
- messageCount: z.number(),
97
- timestamp: z.string()
98
- });
99
- const StatsCacheSchema = z.object({
100
- version: z.number(),
101
- lastComputedDate: z.string(),
102
- dailyActivity: z.array(DailyActivitySchema),
103
- dailyModelTokens: z.array(DailyModelTokensSchema),
104
- modelUsage: ModelUsageSchema,
105
- totalSessions: z.number(),
106
- totalMessages: z.number(),
107
- longestSession: LongestSessionSchema,
108
- firstSessionDate: z.string(),
109
- hourCounts: z.record(z.string(), z.number()),
110
- totalSpeculationTimeSavedMs: z.number().optional()
111
- });
112
- let cachedStats = null;
113
- async function parseStats() {
114
- const statsPath = getStatsPath();
115
- const stat = await fs.promises.stat(statsPath).catch(() => null);
116
- if (!stat) return null;
117
- if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
118
- return cachedStats.data;
119
- }
120
- const diskResult = readDiskCache("stats", stat.mtimeMs, StatsCacheSchema);
121
- if (diskResult) {
122
- cachedStats = { mtimeMs: stat.mtimeMs, data: diskResult };
123
- return diskResult;
124
- }
125
- const raw = await fs.promises.readFile(statsPath, "utf-8");
126
- const parsed = JSON.parse(raw);
127
- const result = StatsCacheSchema.parse(parsed);
128
- writeDiskCache("stats", statsPath, stat.mtimeMs, result);
129
- cachedStats = { mtimeMs: stat.mtimeMs, data: result };
130
- return result;
131
- }
132
- const getStats_createServerFn_handler = createServerRpc({
133
- id: "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5ea799",
134
- name: "getStats",
135
- filename: "src/features/stats/stats.server.ts"
136
- }, (opts) => getStats.__executeServer(opts));
137
- const getStats = createServerFn({
138
- method: "GET"
139
- }).handler(getStats_createServerFn_handler, async () => {
140
- return parseStats();
141
- });
142
- export {
143
- getStats_createServerFn_handler
144
- };