@zhangferry-dev/tokendash 1.4.2 → 1.6.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.
@@ -48,9 +48,56 @@ function countLines(text) {
48
48
  // Claude Code session scanning & tool extraction
49
49
  // ---------------------------------------------------------------------------
50
50
  const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
51
+ const projectNameCache = new Map();
52
+ /** Decode Claude's encoded project directory name.
53
+ * Claude encodes paths: /Users/foo/bar → -Users-foo-bar
54
+ * Since '-' replaces '/' and project names can contain '-',
55
+ * we use filesystem checks to find the correct last segment.
56
+ */
51
57
  function extractProjectName(dirName) {
52
- const parts = dirName.replace(/^-/, '').split('-');
53
- return parts[parts.length - 1] || dirName;
58
+ if (!dirName.startsWith('-'))
59
+ return dirName;
60
+ const cached = projectNameCache.get(dirName);
61
+ if (cached)
62
+ return cached;
63
+ const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
64
+ if (segments.length === 0) {
65
+ projectNameCache.set(dirName, dirName);
66
+ return dirName;
67
+ }
68
+ if (segments.length === 1) {
69
+ projectNameCache.set(dirName, segments[0]);
70
+ return segments[0];
71
+ }
72
+ let bestName = segments[segments.length - 1];
73
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
74
+ const parentSegments = segments.slice(0, splitAt);
75
+ const candidateName = segments.slice(splitAt).join('-');
76
+ let parentPath = '/';
77
+ let valid = true;
78
+ for (const seg of parentSegments) {
79
+ const regular = join(parentPath, seg);
80
+ const hidden = join(parentPath, '.' + seg);
81
+ if (existsSync(regular)) {
82
+ parentPath = regular;
83
+ }
84
+ else if (existsSync(hidden)) {
85
+ parentPath = hidden;
86
+ }
87
+ else {
88
+ valid = false;
89
+ break;
90
+ }
91
+ }
92
+ if (!valid)
93
+ continue;
94
+ if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
95
+ bestName = candidateName;
96
+ break;
97
+ }
98
+ }
99
+ projectNameCache.set(dirName, bestName);
100
+ return bestName;
54
101
  }
55
102
  function matchesProject(dirName, filter) {
56
103
  return extractProjectName(dirName) === extractProjectName(filter);
@@ -273,5 +320,22 @@ export function computeAnalytics(toolCalls, timezone = 'Asia/Shanghai') {
273
320
  toolCallTrend.push(entry);
274
321
  }
275
322
  toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
323
+ // Fill missing tool values with 0 so chart lines don't break
324
+ if (toolCallTrend.length > 0) {
325
+ const allTools = new Set();
326
+ for (const entry of toolCallTrend) {
327
+ for (const key of Object.keys(entry)) {
328
+ if (key !== 'date')
329
+ allTools.add(key);
330
+ }
331
+ }
332
+ for (const entry of toolCallTrend) {
333
+ for (const tool of allTools) {
334
+ if (entry[tool] === undefined) {
335
+ entry[tool] = 0;
336
+ }
337
+ }
338
+ }
339
+ }
276
340
  return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
277
341
  }
@@ -24,13 +24,56 @@ const ClaudeEventSchema = z.object({
24
24
  // Helpers
25
25
  // ---------------------------------------------------------------------------
26
26
  const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
27
- /** Extract project display name from encoded directory path.
28
- * -Users-zhangferry-AI-Ideas Ideas
29
- * -Users-zhangferry-Desktop-Develop-DailyNewsReport → DailyNewsReport
27
+ const projectNameCache = new Map();
28
+ /** Decode Claude's encoded project directory name.
29
+ * Claude encodes paths: /Users/foo/bar → -Users-foo-bar
30
+ * Since '-' replaces '/' and project names can contain '-',
31
+ * we use filesystem checks to find the correct last segment.
30
32
  */
31
33
  function extractProjectName(dirName) {
32
- const parts = dirName.replace(/^-/, '').split('-');
33
- return parts[parts.length - 1] || dirName;
34
+ if (!dirName.startsWith('-'))
35
+ return dirName;
36
+ const cached = projectNameCache.get(dirName);
37
+ if (cached)
38
+ return cached;
39
+ const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
40
+ if (segments.length === 0) {
41
+ projectNameCache.set(dirName, dirName);
42
+ return dirName;
43
+ }
44
+ if (segments.length === 1) {
45
+ projectNameCache.set(dirName, segments[0]);
46
+ return segments[0];
47
+ }
48
+ let bestName = segments[segments.length - 1];
49
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
50
+ const parentSegments = segments.slice(0, splitAt);
51
+ const candidateName = segments.slice(splitAt).join('-');
52
+ let parentPath = '/';
53
+ let valid = true;
54
+ for (const seg of parentSegments) {
55
+ const regular = join(parentPath, seg);
56
+ const hidden = join(parentPath, '.' + seg);
57
+ if (existsSync(regular)) {
58
+ parentPath = regular;
59
+ }
60
+ else if (existsSync(hidden)) {
61
+ parentPath = hidden;
62
+ }
63
+ else {
64
+ valid = false;
65
+ break;
66
+ }
67
+ }
68
+ if (!valid)
69
+ continue;
70
+ if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
71
+ bestName = candidateName;
72
+ break;
73
+ }
74
+ }
75
+ projectNameCache.set(dirName, bestName);
76
+ return bestName;
34
77
  }
35
78
  /** Match project display name against a filter (also normalizes the filter) */
36
79
  function matchesProject(dirName, filter) {
@@ -99,7 +142,7 @@ export function getClaudeBlocksByProject(project) {
99
142
  const outputTokens = usage.output_tokens;
100
143
  const cacheCreationTokens = usage.cache_creation_input_tokens;
101
144
  const cacheReadTokens = usage.cache_read_input_tokens;
102
- const totalTokens = inputTokens + outputTokens + cacheReadTokens;
145
+ const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
103
146
  if (totalTokens === 0)
104
147
  continue;
105
148
  const hourKey = getHourKey(event.timestamp);
@@ -124,7 +167,7 @@ export function getClaudeBlocksByProject(project) {
124
167
  const blocks = [];
125
168
  let idx = 0;
126
169
  for (const [hourKey, bucket] of hourMap) {
127
- const totalTokens = bucket.inputTokens + bucket.outputTokens + bucket.cacheReadTokens;
170
+ const totalTokens = bucket.inputTokens + bucket.outputTokens + bucket.cacheCreationTokens + bucket.cacheReadTokens;
128
171
  blocks.push({
129
172
  id: `claude-project-${idx}`,
130
173
  startTime: `${hourKey}:00:00.000Z`,
@@ -1,5 +1,11 @@
1
1
  import type { DailyResponse, ProjectsResponse, BlockEntry } from '../shared/types.js';
2
- export declare function calculateCost(inputTokens: number, cacheReadTokens: number, outputTokens: number, model: string): number;
2
+ export declare function calculateCost(inputTokens: number, cacheReadTokens: number, outputTokens: number, model: string, cacheCreationTokens?: number): number;
3
+ /** Decode Claude's encoded project directory name.
4
+ * Claude encodes paths: /Users/foo/bar → -Users-foo-bar
5
+ * Since '-' replaces '/' and project names can contain '-',
6
+ * we use filesystem checks to find the correct last segment.
7
+ */
8
+ export declare function extractProjectName(dirName: string): string;
3
9
  export declare function getDateKey(timestamp: string, tz: string): string;
4
10
  export declare function getHourKey(timestamp: string, tz: string): string;
5
11
  export declare function getDailyResponse(project?: string | null, tz?: string): DailyResponse;
@@ -3,18 +3,18 @@ import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  const MODEL_PRICING = {
5
5
  // Claude 4.6
6
- 'claude-opus-4-6': { inputPer1M: 15, cacheReadPer1M: 1.50, outputPer1M: 75 },
7
- 'claude-sonnet-4-6': { inputPer1M: 3, cacheReadPer1M: 0.30, outputPer1M: 15 },
6
+ 'claude-opus-4-6': { inputPer1M: 15, cacheCreationPer1M: 18.75, cacheReadPer1M: 1.50, outputPer1M: 75 },
7
+ 'claude-sonnet-4-6': { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.30, outputPer1M: 15 },
8
8
  // Claude 4.5
9
- 'claude-sonnet-4-5-20250514': { inputPer1M: 3, cacheReadPer1M: 0.30, outputPer1M: 15 },
10
- 'claude-haiku-4-5-20251001': { inputPer1M: 0.80, cacheReadPer1M: 0.08, outputPer1M: 4 },
9
+ 'claude-sonnet-4-5-20250514': { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.30, outputPer1M: 15 },
10
+ 'claude-haiku-4-5-20251001': { inputPer1M: 0.80, cacheCreationPer1M: 1, cacheReadPer1M: 0.08, outputPer1M: 4 },
11
11
  // Older Claude models
12
- 'claude-3-5-sonnet-20241022': { inputPer1M: 3, cacheReadPer1M: 0.30, outputPer1M: 15 },
13
- 'claude-3-5-haiku-20241022': { inputPer1M: 0.80, cacheReadPer1M: 0.08, outputPer1M: 4 },
14
- 'claude-3-opus-20240229': { inputPer1M: 15, cacheReadPer1M: 1.50, outputPer1M: 75 },
15
- 'claude-3-haiku-20240307': { inputPer1M: 0.25, cacheReadPer1M: 0.03, outputPer1M: 1.25 },
12
+ 'claude-3-5-sonnet-20241022': { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.30, outputPer1M: 15 },
13
+ 'claude-3-5-haiku-20241022': { inputPer1M: 0.80, cacheCreationPer1M: 1, cacheReadPer1M: 0.08, outputPer1M: 4 },
14
+ 'claude-3-opus-20240229': { inputPer1M: 15, cacheCreationPer1M: 18.75, cacheReadPer1M: 1.50, outputPer1M: 75 },
15
+ 'claude-3-haiku-20240307': { inputPer1M: 0.25, cacheCreationPer1M: 0.30, cacheReadPer1M: 0.03, outputPer1M: 1.25 },
16
16
  };
17
- const DEFAULT_PRICING = { inputPer1M: 3, cacheReadPer1M: 0.30, outputPer1M: 15 };
17
+ const DEFAULT_PRICING = { inputPer1M: 3, cacheCreationPer1M: 3.75, cacheReadPer1M: 0.30, outputPer1M: 15 };
18
18
  function getPricing(model) {
19
19
  // Try exact match first, then prefix match
20
20
  if (MODEL_PRICING[model])
@@ -26,21 +26,73 @@ function getPricing(model) {
26
26
  }
27
27
  return DEFAULT_PRICING;
28
28
  }
29
- export function calculateCost(inputTokens, cacheReadTokens, outputTokens, model) {
29
+ export function calculateCost(inputTokens, cacheReadTokens, outputTokens, model, cacheCreationTokens = 0) {
30
30
  const p = getPricing(model);
31
- const nonCachedInput = Math.max(inputTokens - cacheReadTokens, 0);
32
- return (nonCachedInput / 1_000_000) * p.inputPer1M
31
+ return (inputTokens / 1_000_000) * p.inputPer1M
32
+ + (cacheCreationTokens / 1_000_000) * p.cacheCreationPer1M
33
33
  + (cacheReadTokens / 1_000_000) * p.cacheReadPer1M
34
34
  + (outputTokens / 1_000_000) * p.outputPer1M;
35
35
  }
36
+ function totalClaudeTokens(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens) {
37
+ return inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
38
+ }
36
39
  // ---------------------------------------------------------------------------
37
40
  // JSONL parsing with mtime cache
38
41
  // ---------------------------------------------------------------------------
39
42
  const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
40
43
  const fileCache = new Map();
41
- function extractProjectName(dirName) {
42
- const parts = dirName.replace(/^-/, '').split('-');
43
- return parts[parts.length - 1] || dirName;
44
+ const projectNameCache = new Map();
45
+ /** Decode Claude's encoded project directory name.
46
+ * Claude encodes paths: /Users/foo/bar → -Users-foo-bar
47
+ * Since '-' replaces '/' and project names can contain '-',
48
+ * we use filesystem checks to find the correct last segment.
49
+ */
50
+ export function extractProjectName(dirName) {
51
+ if (!dirName.startsWith('-'))
52
+ return dirName;
53
+ const cached = projectNameCache.get(dirName);
54
+ if (cached)
55
+ return cached;
56
+ const segments = dirName.replace(/^-/, '').split('-').filter(Boolean);
57
+ if (segments.length === 0) {
58
+ projectNameCache.set(dirName, dirName);
59
+ return dirName;
60
+ }
61
+ if (segments.length === 1) {
62
+ projectNameCache.set(dirName, segments[0]);
63
+ return segments[0];
64
+ }
65
+ let bestName = segments[segments.length - 1];
66
+ // Try from right: find the longest last segment that forms a valid path
67
+ for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
68
+ const parentSegments = segments.slice(0, splitAt);
69
+ const candidateName = segments.slice(splitAt).join('-');
70
+ // Build parent path, handling hidden directories (dot prefix)
71
+ let parentPath = '/';
72
+ let valid = true;
73
+ for (const seg of parentSegments) {
74
+ const regular = join(parentPath, seg);
75
+ const hidden = join(parentPath, '.' + seg);
76
+ if (existsSync(regular)) {
77
+ parentPath = regular;
78
+ }
79
+ else if (existsSync(hidden)) {
80
+ parentPath = hidden;
81
+ }
82
+ else {
83
+ valid = false;
84
+ break;
85
+ }
86
+ }
87
+ if (!valid)
88
+ continue;
89
+ if (existsSync(join(parentPath, candidateName)) || existsSync(join(parentPath, '.' + candidateName))) {
90
+ bestName = candidateName;
91
+ break;
92
+ }
93
+ }
94
+ projectNameCache.set(dirName, bestName);
95
+ return bestName;
44
96
  }
45
97
  function matchesProject(dirName, filter) {
46
98
  return extractProjectName(dirName) === extractProjectName(filter);
@@ -111,7 +163,7 @@ function parseAllSessions(project) {
111
163
  const outputTokens = usage.output_tokens || 0;
112
164
  const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
113
165
  const cacheReadTokens = usage.cache_read_input_tokens || 0;
114
- const totalTokens = inputTokens + outputTokens + cacheReadTokens;
166
+ const totalTokens = totalClaudeTokens(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens);
115
167
  if (totalTokens === 0)
116
168
  continue;
117
169
  entries.push({
@@ -199,8 +251,8 @@ export function getDailyResponse(project, tz = DEFAULT_TZ) {
199
251
  agg.outputTokens += e.outputTokens;
200
252
  agg.cacheCreationTokens += e.cacheCreationTokens;
201
253
  agg.cacheReadTokens += e.cacheReadTokens;
202
- agg.totalTokens += e.inputTokens + e.outputTokens + e.cacheReadTokens;
203
- const cost = calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
254
+ agg.totalTokens += totalClaudeTokens(e.inputTokens, e.outputTokens, e.cacheCreationTokens, e.cacheReadTokens);
255
+ const cost = calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
204
256
  agg.totalCost += cost;
205
257
  if (!agg.models.has(e.model)) {
206
258
  agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
@@ -228,7 +280,7 @@ export function getProjectsResponse(tz = DEFAULT_TZ) {
228
280
  const projectMap = new Map();
229
281
  for (const e of entries) {
230
282
  const date = getDateKey(e.timestamp, tz);
231
- const projectName = e.projectDir;
283
+ const projectName = extractProjectName(e.projectDir);
232
284
  if (!projectMap.has(projectName)) {
233
285
  projectMap.set(projectName, new Map());
234
286
  }
@@ -245,8 +297,8 @@ export function getProjectsResponse(tz = DEFAULT_TZ) {
245
297
  agg.outputTokens += e.outputTokens;
246
298
  agg.cacheCreationTokens += e.cacheCreationTokens;
247
299
  agg.cacheReadTokens += e.cacheReadTokens;
248
- agg.totalTokens += e.inputTokens + e.outputTokens + e.cacheReadTokens;
249
- const cost = calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
300
+ agg.totalTokens += totalClaudeTokens(e.inputTokens, e.outputTokens, e.cacheCreationTokens, e.cacheReadTokens);
301
+ const cost = calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
250
302
  agg.totalCost += cost;
251
303
  if (!agg.models.has(e.model)) {
252
304
  agg.models.set(e.model, { input: 0, output: 0, cacheCreation: 0, cacheRead: 0, cost: 0 });
@@ -282,13 +334,13 @@ export function getBlocksResponse(project, tz = DEFAULT_TZ) {
282
334
  bucket.outputTokens += e.outputTokens;
283
335
  bucket.cacheCreationTokens += e.cacheCreationTokens;
284
336
  bucket.cacheReadTokens += e.cacheReadTokens;
285
- bucket.costUSD += calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model);
337
+ bucket.costUSD += calculateCost(e.inputTokens, e.cacheReadTokens, e.outputTokens, e.model, e.cacheCreationTokens);
286
338
  bucket.models.add(e.model);
287
339
  }
288
340
  const blocks = [];
289
341
  let idx = 0;
290
342
  for (const [hourKey, bucket] of hourMap) {
291
- const totalTokens = bucket.inputTokens + bucket.outputTokens + bucket.cacheReadTokens;
343
+ const totalTokens = totalClaudeTokens(bucket.inputTokens, bucket.outputTokens, bucket.cacheCreationTokens, bucket.cacheReadTokens);
292
344
  blocks.push({
293
345
  id: `claude-${idx}`,
294
346
  startTime: `${hourKey}:00:00`,
@@ -1,5 +1,5 @@
1
1
  import type { DailyResponse, ProjectsResponse, BlocksResponse } from '../shared/types.js';
2
- interface ParsedTokenEvent {
2
+ export interface ParsedTokenEvent {
3
3
  timestamp: string;
4
4
  inputTokens: number;
5
5
  cachedInputTokens: number;
@@ -37,10 +37,14 @@ export declare function scanCodexSessions(): string[];
37
37
  export declare function parseCodexSession(filepath: string): ParsedSession | null;
38
38
  /** Parse all Codex sessions. */
39
39
  export declare function parseAllSessions(): ParsedSession[];
40
+ export declare function buildCodexResponsesFromSessions(sessions: ParsedSession[], options?: Partial<AggregateOptions>): {
41
+ daily: DailyResponse;
42
+ projects: ProjectsResponse;
43
+ blocks: BlocksResponse;
44
+ };
40
45
  /** Aggregate and return DailyResponse format (for /daily?agent=codex) */
41
46
  export declare function getDailyResponse(options?: Partial<AggregateOptions>): DailyResponse;
42
47
  /** Aggregate and return ProjectsResponse format (for /projects?agent=codex) */
43
48
  export declare function getProjectsResponse(options?: Partial<AggregateOptions>): ProjectsResponse;
44
49
  /** Aggregate and return BlocksResponse format (hourly, for /blocks?agent=codex) */
45
50
  export declare function getBlocksResponse(options?: Partial<AggregateOptions>): BlocksResponse;
46
- export {};