claude-session-dashboard 0.4.2 → 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-CPud4KGP.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-75iTexM0.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 { a as parseDetail } from "./session-parser-75iTexM0.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
- parseDetail as a,
553
- parseSummary as p
667
+ getProjectsDir as a,
668
+ extractSessionId as b,
669
+ parseSummary as c,
670
+ decodeProjectDirName as d,
671
+ extractProjectName as e,
672
+ getStatsPath as g,
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 { p as parseSummary } from "./session-parser-75iTexM0.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;
@@ -85,7 +84,7 @@ async function scanSessionsInternal() {
85
84
  }
86
85
  async function scanAllSessions() {
87
86
  const results = await scanSessionsInternal();
88
- return results.map(({ filePath: _fp, ...rest }) => rest);
87
+ return results.map(({ filePath: _filePath, ...summary }) => summary);
89
88
  }
90
89
  async function scanAllSessionsWithPaths() {
91
90
  return scanSessionsInternal();
@@ -95,7 +94,7 @@ async function getActiveSessions() {
95
94
  return all.filter((s) => s.isActive);
96
95
  }
97
96
  export {
97
+ scanAllSessions as a,
98
98
  getActiveSessions as g,
99
- scanAllSessions as s,
100
- scanAllSessionsWithPaths as w
99
+ scanAllSessionsWithPaths as s
101
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-CPud4KGP.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-75iTexM0.js";
10
9
  import "node:readline";
11
10
  import "@tanstack/history";
12
11
  import "@tanstack/router-core/ssr/client";
@@ -1,12 +1,12 @@
1
1
  import { c as createServerRpc } from "./createServerRpc-Bd3B-Ah9.js";
2
2
  import * as fs from "node:fs";
3
- import { g as getStatsPath } from "./claude-path-BdwflgZ1.js";
4
- import { s as scanAllSessions, w as scanAllSessionsWithPaths } from "./session-scanner-CPud4KGP.js";
5
- import { a as parseDetail } from "./session-parser-75iTexM0.js";
3
+ import { g as getStatsPath, p as parseDetail } from "./session-parser-DxLcS8VW.js";
6
4
  import * as path from "node:path";
7
5
  import * as os from "node:os";
8
6
  import { z } from "zod";
7
+ import { s as scanAllSessionsWithPaths } from "./session-scanner-DRGzVO2T.js";
9
8
  import { c as createServerFn } from "../server.js";
9
+ import "node:readline";
10
10
  import "@tanstack/history";
11
11
  import "@tanstack/router-core/ssr/client";
12
12
  import "@tanstack/router-core";
@@ -113,25 +113,63 @@ const StatsCacheSchema = z.object({
113
113
  });
114
114
  let cachedStats = null;
115
115
  let mergedCache = null;
116
- const MERGE_STALENESS_MS = 60000;
116
+ const MERGE_STALENESS_MS = 6e4;
117
117
  function getTodayDateString() {
118
- return new Date().toISOString().split("T")[0];
118
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
119
119
  }
120
120
  function extractDateString(isoOrDate) {
121
121
  return isoOrDate.split("T")[0];
122
122
  }
123
- function updateHourCounts(hourCounts, session) {
124
- const startedAt = session.startedAt;
125
- if (!startedAt) return;
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
+ }
126
142
  try {
127
- const date = new Date(startedAt);
128
- const hour = date.getHours().toString();
129
- hourCounts[hour] = (hourCounts[hour] ?? 0) + 1;
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);
130
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;
131
169
  }
132
170
  }
133
171
  async function parseDetailsInBatches(sessions, batchSize = 10) {
134
- const results = new Map();
172
+ const results = /* @__PURE__ */ new Map();
135
173
  for (let i = 0; i < sessions.length; i += batchSize) {
136
174
  const batch = sessions.slice(i, i + batchSize);
137
175
  const details = await Promise.all(
@@ -139,7 +177,12 @@ async function parseDetailsInBatches(sessions, batchSize = 10) {
139
177
  try {
140
178
  return {
141
179
  sessionId: s.sessionId,
142
- detail: await parseDetail(s.filePath, s.sessionId, s.projectPath, s.projectName),
180
+ detail: await parseDetail(
181
+ s.filePath,
182
+ s.sessionId,
183
+ s.projectPath,
184
+ s.projectName
185
+ )
143
186
  };
144
187
  } catch {
145
188
  return null;
@@ -152,23 +195,6 @@ async function parseDetailsInBatches(sessions, batchSize = 10) {
152
195
  }
153
196
  return results;
154
197
  }
155
- async function maybeEnrichWithRecentSessions(stats, mtimeMs) {
156
- const today = getTodayDateString();
157
- const lastComputed = extractDateString(stats.lastComputedDate);
158
- if (lastComputed >= today) {
159
- return stats;
160
- }
161
- if (mergedCache && mergedCache.mtimeMs === mtimeMs && Date.now() - mergedCache.mergedAt < MERGE_STALENESS_MS) {
162
- return mergedCache.data;
163
- }
164
- try {
165
- const merged = await mergeRecentSessions(stats);
166
- mergedCache = { mtimeMs, mergedAt: Date.now(), data: merged };
167
- return merged;
168
- } catch {
169
- return stats;
170
- }
171
- }
172
198
  async function mergeRecentSessions(stats) {
173
199
  const summaries = await scanAllSessionsWithPaths();
174
200
  const cutoffDate = extractDateString(stats.lastComputedDate);
@@ -180,7 +206,7 @@ async function mergeRecentSessions(stats) {
180
206
  return stats;
181
207
  }
182
208
  const detailMap = await parseDetailsInBatches(recentSessions);
183
- const activityMap = new Map();
209
+ const activityMap = /* @__PURE__ */ new Map();
184
210
  for (const entry of stats.dailyActivity) {
185
211
  activityMap.set(entry.date, {
186
212
  messageCount: entry.messageCount,
@@ -188,7 +214,7 @@ async function mergeRecentSessions(stats) {
188
214
  toolCallCount: entry.toolCallCount
189
215
  });
190
216
  }
191
- const modelTokensMap = new Map();
217
+ const modelTokensMap = /* @__PURE__ */ new Map();
192
218
  for (const entry of stats.dailyModelTokens) {
193
219
  modelTokensMap.set(entry.date, { ...entry.tokensByModel });
194
220
  }
@@ -208,8 +234,7 @@ async function mergeRecentSessions(stats) {
208
234
  cur.sessionCount += 1;
209
235
  if (detail) {
210
236
  cur.messageCount += detail.turns.length;
211
- cur.toolCallCount += Object.values(detail.toolFrequency)
212
- .reduce((sum, n) => sum + n, 0);
237
+ cur.toolCallCount += Object.values(detail.toolFrequency).reduce((sum, n) => sum + n, 0);
213
238
  const dayTokens = modelTokensMap.get(date) ?? {};
214
239
  for (const [model, usage] of Object.entries(detail.tokensByModel)) {
215
240
  const total = usage.inputTokens + usage.outputTokens;
@@ -218,8 +243,10 @@ async function mergeRecentSessions(stats) {
218
243
  modelTokensMap.set(date, dayTokens);
219
244
  for (const [model, usage] of Object.entries(detail.tokensByModel)) {
220
245
  const existing = modelUsage[model] ?? {
221
- inputTokens: 0, outputTokens: 0,
222
- cacheReadInputTokens: 0, cacheCreationInputTokens: 0,
246
+ inputTokens: 0,
247
+ outputTokens: 0,
248
+ cacheReadInputTokens: 0,
249
+ cacheCreationInputTokens: 0
223
250
  };
224
251
  existing.inputTokens += usage.inputTokens;
225
252
  existing.outputTokens += usage.outputTokens;
@@ -264,12 +291,22 @@ async function mergeRecentSessions(stats) {
264
291
  hourCounts
265
292
  };
266
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
+ }
267
304
  async function computeStatsFromSessions() {
268
305
  try {
269
306
  const summaries = await scanAllSessionsWithPaths();
270
307
  const detailMap = await parseDetailsInBatches(summaries);
271
- const activityMap = new Map();
272
- const modelTokensMap = new Map();
308
+ const activityMap = /* @__PURE__ */ new Map();
309
+ const modelTokensMap = /* @__PURE__ */ new Map();
273
310
  const modelUsage = {};
274
311
  const hourCounts = {};
275
312
  let totalMessages = 0;
@@ -282,8 +319,7 @@ async function computeStatsFromSessions() {
282
319
  cur.sessionCount += 1;
283
320
  if (detail) {
284
321
  cur.messageCount += detail.turns.length;
285
- cur.toolCallCount += Object.values(detail.toolFrequency)
286
- .reduce((sum, n) => sum + n, 0);
322
+ cur.toolCallCount += Object.values(detail.toolFrequency).reduce((sum, n) => sum + n, 0);
287
323
  totalMessages += detail.turns.length;
288
324
  const dayTokens = modelTokensMap.get(d) ?? {};
289
325
  for (const [model, usage] of Object.entries(detail.tokensByModel)) {
@@ -293,8 +329,10 @@ async function computeStatsFromSessions() {
293
329
  modelTokensMap.set(d, dayTokens);
294
330
  for (const [model, usage] of Object.entries(detail.tokensByModel)) {
295
331
  const existing = modelUsage[model] ?? {
296
- inputTokens: 0, outputTokens: 0,
297
- cacheReadInputTokens: 0, cacheCreationInputTokens: 0,
332
+ inputTokens: 0,
333
+ outputTokens: 0,
334
+ cacheReadInputTokens: 0,
335
+ cacheCreationInputTokens: 0
298
336
  };
299
337
  existing.inputTokens += usage.inputTokens;
300
338
  existing.outputTokens += usage.outputTokens;
@@ -322,15 +360,11 @@ async function computeStatsFromSessions() {
322
360
  firstSessionDate = s.startedAt;
323
361
  }
324
362
  }
325
- const dailyActivity = Array.from(activityMap.entries())
326
- .map(([date, v]) => ({ date, ...v }))
327
- .sort((a, b) => a.date < b.date ? -1 : 1);
328
- const dailyModelTokens = Array.from(modelTokensMap.entries())
329
- .map(([date, tokensByModel]) => ({ date, tokensByModel }))
330
- .sort((a, b) => a.date < b.date ? -1 : 1);
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);
331
365
  return {
332
366
  version: 1,
333
- lastComputedDate: new Date().toISOString(),
367
+ lastComputedDate: (/* @__PURE__ */ new Date()).toISOString(),
334
368
  dailyActivity,
335
369
  dailyModelTokens,
336
370
  modelUsage,
@@ -340,44 +374,13 @@ async function computeStatsFromSessions() {
340
374
  sessionId: longestSession.sessionId,
341
375
  duration: longestSession.duration,
342
376
  messageCount: longestSession.messageCount,
343
- timestamp: longestSession.timestamp || new Date().toISOString()
377
+ timestamp: longestSession.timestamp || (/* @__PURE__ */ new Date()).toISOString()
344
378
  },
345
- firstSessionDate: firstSessionDate ?? new Date().toISOString(),
379
+ firstSessionDate: firstSessionDate ?? (/* @__PURE__ */ new Date()).toISOString(),
346
380
  hourCounts
347
381
  };
348
- } catch (error) {
349
- return null;
350
- }
351
- }
352
- async function parseStats() {
353
- const statsPath = getStatsPath();
354
- const stat = await fs.promises.stat(statsPath).catch(() => null);
355
- if (!stat) {
356
- try {
357
- const computed = await computeStatsFromSessions();
358
- return computed;
359
- } catch {
360
- return null;
361
- }
362
- }
363
- if (cachedStats && cachedStats.mtimeMs === stat.mtimeMs) {
364
- return maybeEnrichWithRecentSessions(cachedStats.data, stat.mtimeMs);
365
- }
366
- const diskResult = readDiskCache("stats", stat.mtimeMs, StatsCacheSchema);
367
- if (diskResult) {
368
- cachedStats = { mtimeMs: stat.mtimeMs, data: diskResult };
369
- return maybeEnrichWithRecentSessions(diskResult, stat.mtimeMs);
370
- }
371
- try {
372
- const raw = await fs.promises.readFile(statsPath, "utf-8");
373
- const parsed = JSON.parse(raw);
374
- const result = StatsCacheSchema.parse(parsed);
375
- writeDiskCache("stats", statsPath, stat.mtimeMs, result);
376
- cachedStats = { mtimeMs: stat.mtimeMs, data: result };
377
- return maybeEnrichWithRecentSessions(result, stat.mtimeMs);
378
382
  } catch {
379
- const computed = await computeStatsFromSessions();
380
- return computed;
383
+ return null;
381
384
  }
382
385
  }
383
386
  const getStats_createServerFn_handler = createServerRpc({
@@ -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-RaGuPsWw.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,10 +579,10 @@ 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")
583
- }, "64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb": {
584
- functionName: "getProjectAnalytics_createServerFn_handler",
585
- importer: () => import("./assets/project-analytics.server-BsmZ4xil.js")
582
+ importer: () => import("./assets/stats.server-iJ_z7eUN.js")
583
+ }, "ff8a3161afdfa175e9c519e4146a56ab5bce6e80745e99cfc2191ebbb7a859bb": {
584
+ functionName: "getSessionDetail_createServerFn_handler",
585
+ importer: () => import("./assets/session-detail.server-CO5XwLSv.js")
586
586
  }, "810657681a273df5b4e58f0d8fcc6a5451598b489431b9bcaa98eea0ad815da8": {
587
587
  functionName: "getSettings_createServerFn_handler",
588
588
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
@@ -591,16 +591,16 @@ const manifest = { "4b9a58c176f487b49800a372100037cdf33cf048f3592a449f115c7e3f5e
591
591
  importer: () => import("./assets/settings.server-6B2PvLgf.js")
592
592
  }, "bf8e4a7901f1843bdc9c46be1ad5ad59c615b8bbe611b73eb3ff28f20e43ee0d": {
593
593
  functionName: "getSessionList_createServerFn_handler",
594
- importer: () => import("./assets/sessions.server-60puUvjv.js")
594
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
595
595
  }, "839d29fe93dfa2a6d506af7b48ca25197190a5ff4c796e970ddfdc6e8c98827f": {
596
596
  functionName: "getActiveSessionList_createServerFn_handler",
597
- importer: () => import("./assets/sessions.server-60puUvjv.js")
597
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
598
598
  }, "a3f42f9012fd83586787da8f7cb90649da739dd947d867eb67572f68735ff495": {
599
599
  functionName: "getPaginatedSessions_createServerFn_handler",
600
- importer: () => import("./assets/sessions.server-60puUvjv.js")
601
- }, "ff8a3161afdfa175e9c519e4146a56ab5bce6e80745e99cfc2191ebbb7a859bb": {
602
- functionName: "getSessionDetail_createServerFn_handler",
603
- importer: () => import("./assets/session-detail.server-BDup9xb0.js")
600
+ importer: () => import("./assets/sessions.server-CTeIkXCV.js")
601
+ }, "64052f224a1d6696436e5d3deeee2b798f0742e1292ffabd038c3a7bf75e6fcb": {
602
+ functionName: "getProjectAnalytics_createServerFn_handler",
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.2",
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": "/Users/dmytro.lupiak/Documents/GitHub/claude-session-dashboard/apps/web/src/routes/__root.tsx", "children": ["/", "/_dashboard"], "preloads": ["/assets/main-CV28H4XG.js"], "assets": [] }, "/": { "filePath": "/Users/dmytro.lupiak/Documents/GitHub/claude-session-dashboard/apps/web/src/routes/index.tsx" }, "/_dashboard": { "filePath": "/Users/dmytro.lupiak/Documents/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/Documents/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/Documents/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/Documents/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/Documents/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,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
- };