@zhangferry-dev/tokendash 1.1.4 → 1.2.1

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.
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>TokenDash</title>
7
7
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><text y='28' font-size='28'>⚡</text></svg>" />
8
- <script type="module" crossorigin src="/assets/index-BJbeEwyn.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DI_qK8jk.css">
8
+ <script type="module" crossorigin src="/assets/index-DohuMiQc.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-92lvfG3S.css">
10
10
  </head>
11
11
  <body class="antialiased" style="background:#faf9f7">
12
12
  <div id="root"></div>
@@ -0,0 +1,6 @@
1
+ export declare function isClaudeCodeAvailable(): boolean;
2
+ export declare function isCodexAvailable(): boolean;
3
+ export declare function detectAvailableAgents(): {
4
+ claude: boolean;
5
+ codex: boolean;
6
+ };
@@ -0,0 +1,25 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
5
+ const CODEX_SESSIONS_DIR = join(homedir(), '.codex', 'sessions');
6
+ export function isClaudeCodeAvailable() {
7
+ if (!existsSync(CLAUDE_PROJECTS_DIR))
8
+ return false;
9
+ try {
10
+ const dirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
11
+ return dirs.some(d => d.isDirectory());
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ export function isCodexAvailable() {
18
+ return existsSync(CODEX_SESSIONS_DIR);
19
+ }
20
+ export function detectAvailableAgents() {
21
+ return {
22
+ claude: isClaudeCodeAvailable(),
23
+ codex: isCodexAvailable(),
24
+ };
25
+ }
@@ -0,0 +1,13 @@
1
+ import type { AnalyticsResponse } from '../shared/types.js';
2
+ interface ToolCallRecord {
3
+ toolName: string;
4
+ timestamp: number;
5
+ filePath?: string;
6
+ linesAdded: number;
7
+ linesDeleted: number;
8
+ }
9
+ export declare function normalizeToolName(name: string): string;
10
+ export declare function extractClaudeToolCalls(project?: string | null): ToolCallRecord[];
11
+ export declare function extractOpenClawToolCalls(project?: string | null): ToolCallRecord[];
12
+ export declare function computeAnalytics(toolCalls: ToolCallRecord[], timezone?: string): AnalyticsResponse;
13
+ export {};
@@ -0,0 +1,277 @@
1
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { scanOpenClawSessions } from './openclawParser.js';
5
+ // ---------------------------------------------------------------------------
6
+ // Timezone helpers (same as other parsers)
7
+ // ---------------------------------------------------------------------------
8
+ const TZ_OFFSETS = {
9
+ 'Asia/Shanghai': 8,
10
+ 'Asia/Tokyo': 9,
11
+ 'America/New_York': -5,
12
+ 'America/Los_Angeles': -8,
13
+ 'Europe/London': 0,
14
+ 'UTC': 0,
15
+ };
16
+ function getDateKey(ms, tz) {
17
+ const offset = (TZ_OFFSETS[tz] ?? 8) * 3_600_000;
18
+ const d = new Date(ms + offset);
19
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(d.getUTCDate()).padStart(2, '0')}`;
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // Tool name normalization
23
+ // ---------------------------------------------------------------------------
24
+ export function normalizeToolName(name) {
25
+ const lower = name.toLowerCase();
26
+ if (lower.startsWith('mcp__')) {
27
+ const parts = name.split('__');
28
+ const serverPart = parts.length >= 3 ? parts[2] : 'mcp';
29
+ return `MCP:${serverPart}`;
30
+ }
31
+ const mapping = {
32
+ 'exec': 'Bash',
33
+ 'read': 'Read',
34
+ 'edit': 'Edit',
35
+ 'write': 'Write',
36
+ };
37
+ return mapping[lower] || name;
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Line counting
41
+ // ---------------------------------------------------------------------------
42
+ function countLines(text) {
43
+ if (!text)
44
+ return 0;
45
+ return text.split('\n').length;
46
+ }
47
+ // ---------------------------------------------------------------------------
48
+ // Claude Code session scanning & tool extraction
49
+ // ---------------------------------------------------------------------------
50
+ const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
51
+ function extractProjectName(dirName) {
52
+ const parts = dirName.replace(/^-/, '').split('-');
53
+ return parts[parts.length - 1] || dirName;
54
+ }
55
+ function matchesProject(dirName, filter) {
56
+ return extractProjectName(dirName) === extractProjectName(filter);
57
+ }
58
+ // Session-level cache (mtime-based)
59
+ const claudeSessionCache = new Map();
60
+ export function extractClaudeToolCalls(project) {
61
+ if (!existsSync(CLAUDE_PROJECTS_DIR))
62
+ return [];
63
+ const results = [];
64
+ const projectDirs = readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true })
65
+ .filter(d => d.isDirectory())
66
+ .map(d => d.name);
67
+ for (const dirName of projectDirs) {
68
+ if (project && !matchesProject(dirName, project))
69
+ continue;
70
+ const dirPath = join(CLAUDE_PROJECTS_DIR, dirName);
71
+ let files;
72
+ try {
73
+ files = readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
74
+ }
75
+ catch {
76
+ continue;
77
+ }
78
+ for (const file of files) {
79
+ const filePath = join(dirPath, file);
80
+ let mtime = 0;
81
+ try {
82
+ mtime = statSync(filePath).mtimeMs;
83
+ }
84
+ catch { /* ok */ }
85
+ const cached = claudeSessionCache.get(filePath);
86
+ if (cached && cached.mtime === mtime) {
87
+ results.push(...cached.toolCalls);
88
+ continue;
89
+ }
90
+ const toolCalls = [];
91
+ let content;
92
+ try {
93
+ content = readFileSync(filePath, 'utf-8');
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ for (const line of content.split('\n')) {
99
+ const trimmed = line.trim();
100
+ if (!trimmed)
101
+ continue;
102
+ let obj;
103
+ try {
104
+ obj = JSON.parse(trimmed);
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ if (obj.type !== 'assistant' || !obj.message)
110
+ continue;
111
+ const msg = obj.message;
112
+ const timestamp = new Date(obj.timestamp).getTime();
113
+ const content_arr = msg.content;
114
+ if (!content_arr)
115
+ continue;
116
+ for (const item of content_arr) {
117
+ if (item.type !== 'tool_use')
118
+ continue;
119
+ const toolName = normalizeToolName(item.name);
120
+ const input = item.input || {};
121
+ let linesAdded = 0;
122
+ let linesDeleted = 0;
123
+ const filePath2 = input.file_path || undefined;
124
+ if (toolName === 'Edit') {
125
+ linesDeleted = countLines(input.old_string || '');
126
+ linesAdded = countLines(input.new_string || '');
127
+ }
128
+ else if (toolName === 'Write') {
129
+ linesAdded = countLines(input.content || '');
130
+ }
131
+ toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
132
+ }
133
+ }
134
+ claudeSessionCache.set(filePath, { mtime, toolCalls });
135
+ results.push(...toolCalls);
136
+ }
137
+ }
138
+ return results;
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // OpenClaw tool extraction
142
+ // ---------------------------------------------------------------------------
143
+ const openclawSessionCache = new Map();
144
+ export function extractOpenClawToolCalls(project) {
145
+ const results = [];
146
+ const refs = scanOpenClawSessions();
147
+ for (const ref of refs) {
148
+ if (project && ref.agentId !== project)
149
+ continue;
150
+ let mtime = 0;
151
+ try {
152
+ mtime = statSync(ref.sessionFile).mtimeMs;
153
+ }
154
+ catch { /* ok */ }
155
+ const cached = openclawSessionCache.get(ref.sessionFile);
156
+ if (cached && cached.mtime === mtime) {
157
+ results.push(...cached.toolCalls);
158
+ continue;
159
+ }
160
+ const toolCalls = [];
161
+ let content;
162
+ try {
163
+ content = readFileSync(ref.sessionFile, 'utf-8');
164
+ }
165
+ catch {
166
+ continue;
167
+ }
168
+ for (const line of content.split('\n')) {
169
+ const trimmed = line.trim();
170
+ if (!trimmed)
171
+ continue;
172
+ let obj;
173
+ try {
174
+ obj = JSON.parse(trimmed);
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ if (obj.type !== 'message')
180
+ continue;
181
+ const msg = obj.message;
182
+ if (msg.role !== 'assistant')
183
+ continue;
184
+ const timestamp = Number(msg.timestamp ?? 0);
185
+ const content_arr = msg.content;
186
+ if (!content_arr)
187
+ continue;
188
+ for (const item of content_arr) {
189
+ if (item.type !== 'toolCall')
190
+ continue;
191
+ const toolName = normalizeToolName(item.name);
192
+ const args = item.arguments || {};
193
+ let linesAdded = 0;
194
+ let linesDeleted = 0;
195
+ const filePath2 = args.path || undefined;
196
+ if (toolName === 'Edit') {
197
+ linesDeleted = countLines(args.oldText || '');
198
+ linesAdded = countLines(args.newText || '');
199
+ }
200
+ else if (toolName === 'Write') {
201
+ linesAdded = countLines(args.content || '');
202
+ }
203
+ toolCalls.push({ toolName, timestamp, filePath: filePath2, linesAdded, linesDeleted });
204
+ }
205
+ }
206
+ openclawSessionCache.set(ref.sessionFile, { mtime, toolCalls });
207
+ results.push(...toolCalls);
208
+ }
209
+ return results;
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Analytics computation
213
+ // ---------------------------------------------------------------------------
214
+ export function computeAnalytics(toolCalls, timezone = 'Asia/Shanghai') {
215
+ // 1. Code Change Trend — group edit/write calls by date
216
+ const changeMap = new Map();
217
+ for (const tc of toolCalls) {
218
+ if (tc.linesAdded === 0 && tc.linesDeleted === 0)
219
+ continue;
220
+ const key = getDateKey(tc.timestamp, timezone);
221
+ if (!changeMap.has(key))
222
+ changeMap.set(key, { added: 0, deleted: 0, files: new Set() });
223
+ const entry = changeMap.get(key);
224
+ entry.added += tc.linesAdded;
225
+ entry.deleted += tc.linesDeleted;
226
+ if (tc.filePath)
227
+ entry.files.add(tc.filePath);
228
+ }
229
+ const codeChangeTrend = [];
230
+ for (const [date, { added, deleted, files }] of changeMap) {
231
+ codeChangeTrend.push({ date, linesAdded: added, linesDeleted: deleted, netChange: added - deleted, filesModified: files.size });
232
+ }
233
+ codeChangeTrend.sort((a, b) => a.date.localeCompare(b.date));
234
+ // 2. Tool Usage Distribution — count per tool
235
+ const toolCountMap = new Map();
236
+ for (const tc of toolCalls) {
237
+ toolCountMap.set(tc.toolName, (toolCountMap.get(tc.toolName) || 0) + 1);
238
+ }
239
+ const toolUsageDistribution = [...toolCountMap.entries()]
240
+ .map(([name, count]) => ({ name, count }))
241
+ .sort((a, b) => b.count - a.count);
242
+ // 3. Productivity KPIs
243
+ const editCalls = toolCalls.filter(tc => tc.toolName === 'Edit' || tc.toolName === 'Write');
244
+ const totalEdits = editCalls.length;
245
+ const totalLinesChanged = editCalls.reduce((s, tc) => s + tc.linesAdded + tc.linesDeleted, 0);
246
+ const totalLinesAdded = editCalls.reduce((s, tc) => s + tc.linesAdded, 0);
247
+ const totalLinesDeleted = editCalls.reduce((s, tc) => s + tc.linesDeleted, 0);
248
+ const uniqueFiles = new Set(editCalls.filter(tc => tc.filePath).map(tc => tc.filePath));
249
+ const editDates = new Set(editCalls.map(tc => getDateKey(tc.timestamp, timezone)));
250
+ const productivityKPIs = {
251
+ avgLinesPerEdit: totalEdits > 0 ? Math.round(totalLinesChanged / totalEdits) : 0,
252
+ filesModifiedPerDay: editDates.size > 0 ? Math.round(uniqueFiles.size / editDates.size) : 0,
253
+ addDeleteRatio: totalLinesDeleted > 0 ? Math.round((totalLinesAdded / totalLinesDeleted) * 100) / 100 : totalLinesAdded > 0 ? 1 : 0,
254
+ totalEdits,
255
+ totalFilesModified: uniqueFiles.size,
256
+ activeDaysWithEdits: editDates.size,
257
+ };
258
+ // 4. Tool Call Trend — group all calls by (date, toolName)
259
+ const trendMap = new Map();
260
+ for (const tc of toolCalls) {
261
+ const date = getDateKey(tc.timestamp, timezone);
262
+ if (!trendMap.has(date))
263
+ trendMap.set(date, new Map());
264
+ const dayMap = trendMap.get(date);
265
+ dayMap.set(tc.toolName, (dayMap.get(tc.toolName) || 0) + 1);
266
+ }
267
+ const toolCallTrend = [];
268
+ for (const [date, dayMap] of trendMap) {
269
+ const entry = { date };
270
+ for (const [tool, count] of dayMap) {
271
+ entry[tool] = count;
272
+ }
273
+ toolCallTrend.push(entry);
274
+ }
275
+ toolCallTrend.sort((a, b) => a.date.localeCompare(b.date));
276
+ return { codeChangeTrend, toolUsageDistribution, productivityKPIs, toolCallTrend };
277
+ }
@@ -1,14 +1,19 @@
1
1
  interface CacheEntry<T> {
2
2
  data: T;
3
3
  expiresAt: number;
4
+ updatedAt: number;
4
5
  }
5
6
  declare class Cache {
6
7
  private store;
7
8
  get<T>(key: string): T | null;
9
+ /** Get data even if stale (for stale-while-revalidate) */
10
+ getStale<T>(key: string): T | null;
8
11
  set<T>(key: string, data: T, ttl?: number): void;
9
12
  clear(): void;
10
13
  delete(key: string): boolean;
11
14
  has(key: string): boolean;
15
+ private writeToDisk;
16
+ private readFromDisk;
12
17
  }
13
18
  export declare const cache: Cache;
14
19
  export type { CacheEntry };
@@ -1,23 +1,39 @@
1
- const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ const DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes (fresh)
5
+ const DISK_TTL = 60 * 60 * 1000; // 1 hour (stale but usable)
6
+ const CACHE_DIR = join(tmpdir(), 'tokendash-cache');
7
+ function diskPath(key) {
8
+ const safe = key.replace(/[^a-zA-Z0-9_-]/g, '_');
9
+ return join(CACHE_DIR, `${safe}.json`);
10
+ }
2
11
  class Cache {
3
12
  store = new Map();
4
13
  get(key) {
5
14
  const entry = this.store.get(key);
6
- if (!entry) {
7
- return null;
8
- }
9
- if (Date.now() > entry.expiresAt) {
10
- this.store.delete(key);
11
- return null;
15
+ if (entry && Date.now() <= entry.expiresAt) {
16
+ return entry.data;
12
17
  }
13
- return entry.data;
18
+ return null;
19
+ }
20
+ /** Get data even if stale (for stale-while-revalidate) */
21
+ getStale(key) {
22
+ // Try memory first
23
+ const entry = this.store.get(key);
24
+ if (entry)
25
+ return entry.data;
26
+ // Try disk
27
+ return this.readFromDisk(key);
14
28
  }
15
29
  set(key, data, ttl = DEFAULT_TTL) {
16
30
  const entry = {
17
31
  data,
18
32
  expiresAt: Date.now() + ttl,
33
+ updatedAt: Date.now(),
19
34
  };
20
35
  this.store.set(key, entry);
36
+ this.writeToDisk(key, entry);
21
37
  }
22
38
  clear() {
23
39
  this.store.clear();
@@ -27,14 +43,42 @@ class Cache {
27
43
  }
28
44
  has(key) {
29
45
  const entry = this.store.get(key);
30
- if (!entry) {
46
+ if (!entry)
31
47
  return false;
32
- }
33
48
  if (Date.now() > entry.expiresAt) {
34
49
  this.store.delete(key);
35
50
  return false;
36
51
  }
37
52
  return true;
38
53
  }
54
+ writeToDisk(key, entry) {
55
+ try {
56
+ if (!existsSync(CACHE_DIR))
57
+ mkdirSync(CACHE_DIR, { recursive: true });
58
+ writeFileSync(diskPath(key), JSON.stringify(entry), 'utf-8');
59
+ }
60
+ catch {
61
+ // Disk cache is best-effort
62
+ }
63
+ }
64
+ readFromDisk(key) {
65
+ try {
66
+ const path = diskPath(key);
67
+ if (!existsSync(path))
68
+ return null;
69
+ const raw = readFileSync(path, 'utf-8');
70
+ const entry = JSON.parse(raw);
71
+ // Only use disk cache if less than DISK_TTL old
72
+ if (Date.now() - entry.updatedAt < DISK_TTL) {
73
+ // Promote to memory cache (with 0 TTL so it'll be treated as stale)
74
+ this.store.set(key, { ...entry, expiresAt: 0 });
75
+ return entry.data;
76
+ }
77
+ }
78
+ catch {
79
+ // Disk cache is best-effort
80
+ }
81
+ return null;
82
+ }
39
83
  }
40
84
  export const cache = new Cache();
@@ -0,0 +1,9 @@
1
+ import type { DailyResponse, ProjectsResponse, BlockEntry } from '../shared/types.js';
2
+ export declare function calculateCost(inputTokens: number, cacheReadTokens: number, outputTokens: number, model: string): number;
3
+ export declare function getDateKey(timestamp: string, tz: string): string;
4
+ export declare function getHourKey(timestamp: string, tz: string): string;
5
+ export declare function getDailyResponse(project?: string | null, tz?: string): DailyResponse;
6
+ export declare function getProjectsResponse(tz?: string): ProjectsResponse;
7
+ export declare function getBlocksResponse(project?: string | null, tz?: string): {
8
+ blocks: BlockEntry[];
9
+ };