claude-session-viewer 0.3.2 → 0.3.4
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.
- package/dist/client/assets/index-DvK33tag.css +1 -0
- package/dist/client/assets/{index-cY4kqyQm.js → index-KEbXAXOS.js} +15 -15
- package/dist/client/index.html +2 -2
- package/dist/server/claude/config.js +36 -0
- package/dist/server/claude/projects/repository.js +40 -0
- package/dist/server/claude/projects/service.js +41 -0
- package/dist/server/claude/sessions/agentMapping.js +100 -0
- package/dist/server/claude/sessions/agents.js +100 -0
- package/dist/server/claude/sessions/filters.js +23 -0
- package/dist/server/claude/sessions/repository.js +33 -0
- package/dist/server/claude/sessions/service.js +86 -0
- package/dist/server/claude/sessions/title.js +52 -0
- package/dist/server/constants.js +11 -0
- package/dist/server/index.js +6 -782
- package/dist/server/routes/sessions.js +95 -0
- package/dist/server/routes/statistics.js +328 -0
- package/dist/server/shared/constants.js +11 -0
- package/dist/server/types.js +1 -0
- package/dist/server/utils/jsonl.js +12 -0
- package/dist/server/utils/text.js +34 -0
- package/dist/server/utils/tokenStats.js +66 -0
- package/package.json +1 -1
- package/dist/client/assets/index-i0yZUese.css +0 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readdir, stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
+
import { parseJsonl } from '../utils/jsonl.js';
|
|
5
|
+
import { getAllProjectsWithSessions } from '../claude/projects/service.js';
|
|
6
|
+
import { getProjectDisplayName } from '../claude/projects/repository.js';
|
|
7
|
+
import { loadAgentSessionsFromFiles } from '../claude/sessions/service.js';
|
|
8
|
+
import { collectAgentDescriptions, injectAgentIdsIntoMessages, findAgentTitleFromParentMessages } from '../claude/sessions/agents.js';
|
|
9
|
+
import { isAgentSession } from '../claude/sessions/filters.js';
|
|
10
|
+
import { extractSessionTitle } from '../claude/sessions/title.js';
|
|
11
|
+
/**
|
|
12
|
+
* Session routes
|
|
13
|
+
*/
|
|
14
|
+
export async function registerSessionRoutes(server) {
|
|
15
|
+
// API: Get all sessions grouped by project
|
|
16
|
+
server.get('/api/sessions', async (request, reply) => {
|
|
17
|
+
try {
|
|
18
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
19
|
+
const projectGroups = await getAllProjectsWithSessions(projectsDir);
|
|
20
|
+
return { projects: projectGroups };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
console.error('Error reading sessions:', error);
|
|
24
|
+
return { projects: [] };
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
// API: Get session by ID
|
|
28
|
+
server.get('/api/sessions/:id', async (request, reply) => {
|
|
29
|
+
try {
|
|
30
|
+
const { id } = request.params;
|
|
31
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
32
|
+
const projects = await readdir(projectsDir);
|
|
33
|
+
const isAgent = isAgentSession(id);
|
|
34
|
+
for (const project of projects) {
|
|
35
|
+
const projectPath = join(projectsDir, project);
|
|
36
|
+
const sessionFile = join(projectPath, `${id}.jsonl`);
|
|
37
|
+
try {
|
|
38
|
+
const messages = await parseJsonl(sessionFile);
|
|
39
|
+
const fileStat = await stat(sessionFile);
|
|
40
|
+
const projectName = getProjectDisplayName(project);
|
|
41
|
+
let title = extractSessionTitle(messages);
|
|
42
|
+
// For agent sessions, find description from parent
|
|
43
|
+
if (isAgent) {
|
|
44
|
+
const agentId = id.replace('agent-', '');
|
|
45
|
+
const files = await readdir(projectPath);
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
if (!file.startsWith('agent-') && file.endsWith('.jsonl')) {
|
|
48
|
+
try {
|
|
49
|
+
const parentMessages = await parseJsonl(join(projectPath, file));
|
|
50
|
+
const description = findAgentTitleFromParentMessages(parentMessages, agentId);
|
|
51
|
+
if (description) {
|
|
52
|
+
title = description;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Attach agent sessions for main sessions
|
|
63
|
+
let agentSessions;
|
|
64
|
+
if (!isAgent) {
|
|
65
|
+
const agentDescriptions = collectAgentDescriptions(messages);
|
|
66
|
+
if (agentDescriptions.size > 0) {
|
|
67
|
+
agentSessions = await loadAgentSessionsFromFiles(projectPath, projectName, agentDescriptions);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const messagesWithAgentIds = injectAgentIdsIntoMessages(messages);
|
|
71
|
+
return {
|
|
72
|
+
session: {
|
|
73
|
+
id,
|
|
74
|
+
project: projectName,
|
|
75
|
+
timestamp: fileStat.mtime.toISOString(),
|
|
76
|
+
messages: messagesWithAgentIds,
|
|
77
|
+
messageCount: messages.length,
|
|
78
|
+
title,
|
|
79
|
+
isAgent,
|
|
80
|
+
agentSessions
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return reply.code(404).send({ error: 'Session not found' });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Error reading session:', error);
|
|
92
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { readdir, stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
+
import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
|
|
5
|
+
import { parseJsonl } from '../utils/jsonl.js';
|
|
6
|
+
import { extractTokenUsage, aggregateTokenUsage, calculateCost, createEmptyTokenUsage } from '../utils/tokenStats.js';
|
|
7
|
+
import { getProjectDisplayName } from '../claude/projects/repository.js';
|
|
8
|
+
import { collectAgentDescriptions } from '../claude/sessions/agents.js';
|
|
9
|
+
import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
|
|
10
|
+
/**
|
|
11
|
+
* Statistics routes
|
|
12
|
+
*/
|
|
13
|
+
export async function registerStatisticsRoutes(server) {
|
|
14
|
+
server.get('/api/statistics/tokens', async (request, reply) => {
|
|
15
|
+
try {
|
|
16
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
17
|
+
const projects = await readdir(projectsDir);
|
|
18
|
+
const daysParam = request.query.days || '30';
|
|
19
|
+
const cutoffDate = new Date();
|
|
20
|
+
if (daysParam !== 'all') {
|
|
21
|
+
const days = parseInt(daysParam, 10);
|
|
22
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
23
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
cutoffDate.setFullYear(2000, 0, 1);
|
|
27
|
+
}
|
|
28
|
+
const dailyMap = new Map();
|
|
29
|
+
const projectMap = new Map();
|
|
30
|
+
const modelMap = new Map();
|
|
31
|
+
let totalMessages = 0;
|
|
32
|
+
let totalSessions = 0;
|
|
33
|
+
let minDate = null;
|
|
34
|
+
let maxDate = null;
|
|
35
|
+
let totalCacheCreation = 0;
|
|
36
|
+
let totalCacheRead = 0;
|
|
37
|
+
let ephemeral5mTokens = 0;
|
|
38
|
+
let ephemeral1hTokens = 0;
|
|
39
|
+
const allUsages = [];
|
|
40
|
+
const allCosts = [];
|
|
41
|
+
const toolUsageMap = new Map();
|
|
42
|
+
let totalAgentSessions = 0;
|
|
43
|
+
const hourlyMap = new Map();
|
|
44
|
+
const weekdayMap = new Map();
|
|
45
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
46
|
+
hourlyMap.set(hour, {
|
|
47
|
+
usage: createEmptyTokenUsage(),
|
|
48
|
+
sessionIds: new Set(),
|
|
49
|
+
messageCount: 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
for (let weekday = 0; weekday < 7; weekday++) {
|
|
53
|
+
weekdayMap.set(weekday, {
|
|
54
|
+
usage: createEmptyTokenUsage(),
|
|
55
|
+
sessionIds: new Set(),
|
|
56
|
+
messageCount: 0
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const project of projects) {
|
|
60
|
+
const projectPath = join(projectsDir, project);
|
|
61
|
+
const projectStat = await stat(projectPath);
|
|
62
|
+
if (!projectStat.isDirectory())
|
|
63
|
+
continue;
|
|
64
|
+
const files = await readdir(projectPath);
|
|
65
|
+
const displayName = getProjectDisplayName(project);
|
|
66
|
+
if (!projectMap.has(project)) {
|
|
67
|
+
projectMap.set(project, {
|
|
68
|
+
usage: createEmptyTokenUsage(),
|
|
69
|
+
sessionIds: new Set(),
|
|
70
|
+
displayName
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
if (!file.endsWith('.jsonl'))
|
|
75
|
+
continue;
|
|
76
|
+
const filePath = join(projectPath, file);
|
|
77
|
+
const fileStat = await stat(filePath);
|
|
78
|
+
if (isEmptyFile(fileStat.size))
|
|
79
|
+
continue;
|
|
80
|
+
const sessionId = file.replace('.jsonl', '');
|
|
81
|
+
if (isAgentSession(sessionId))
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const messages = await parseJsonl(filePath);
|
|
85
|
+
if (shouldSkipSession(messages))
|
|
86
|
+
continue;
|
|
87
|
+
totalSessions++;
|
|
88
|
+
const projectData = projectMap.get(project);
|
|
89
|
+
projectData.sessionIds.add(sessionId);
|
|
90
|
+
const agentDescriptions = collectAgentDescriptions(messages);
|
|
91
|
+
if (agentDescriptions.size > 0) {
|
|
92
|
+
totalAgentSessions++;
|
|
93
|
+
}
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
const tokenData = extractTokenUsage(message);
|
|
96
|
+
if (!tokenData)
|
|
97
|
+
continue;
|
|
98
|
+
const messageDate = new Date(message.timestamp);
|
|
99
|
+
if (messageDate < cutoffDate)
|
|
100
|
+
continue;
|
|
101
|
+
totalMessages++;
|
|
102
|
+
const { usage, model } = tokenData;
|
|
103
|
+
const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
|
|
104
|
+
allUsages.push(usage);
|
|
105
|
+
allCosts.push(calculateCost(usage, pricing));
|
|
106
|
+
const dateKey = messageDate.toISOString().split('T')[0];
|
|
107
|
+
if (!minDate || messageDate < minDate)
|
|
108
|
+
minDate = messageDate;
|
|
109
|
+
if (!maxDate || messageDate > maxDate)
|
|
110
|
+
maxDate = messageDate;
|
|
111
|
+
if (!dailyMap.has(dateKey)) {
|
|
112
|
+
dailyMap.set(dateKey, {
|
|
113
|
+
usage: createEmptyTokenUsage(),
|
|
114
|
+
sessionIds: new Set()
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const dailyData = dailyMap.get(dateKey);
|
|
118
|
+
dailyData.usage.inputTokens += usage.inputTokens;
|
|
119
|
+
dailyData.usage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
120
|
+
dailyData.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
121
|
+
dailyData.usage.outputTokens += usage.outputTokens;
|
|
122
|
+
dailyData.usage.totalTokens += usage.totalTokens;
|
|
123
|
+
dailyData.sessionIds.add(sessionId);
|
|
124
|
+
const hour = messageDate.getHours();
|
|
125
|
+
const hourData = hourlyMap.get(hour);
|
|
126
|
+
hourData.usage.inputTokens += usage.inputTokens;
|
|
127
|
+
hourData.usage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
128
|
+
hourData.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
129
|
+
hourData.usage.outputTokens += usage.outputTokens;
|
|
130
|
+
hourData.usage.totalTokens += usage.totalTokens;
|
|
131
|
+
hourData.sessionIds.add(sessionId);
|
|
132
|
+
hourData.messageCount++;
|
|
133
|
+
const weekday = messageDate.getDay();
|
|
134
|
+
const weekdayData = weekdayMap.get(weekday);
|
|
135
|
+
weekdayData.usage.inputTokens += usage.inputTokens;
|
|
136
|
+
weekdayData.usage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
137
|
+
weekdayData.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
138
|
+
weekdayData.usage.outputTokens += usage.outputTokens;
|
|
139
|
+
weekdayData.usage.totalTokens += usage.totalTokens;
|
|
140
|
+
weekdayData.sessionIds.add(sessionId);
|
|
141
|
+
weekdayData.messageCount++;
|
|
142
|
+
projectData.usage.inputTokens += usage.inputTokens;
|
|
143
|
+
projectData.usage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
144
|
+
projectData.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
145
|
+
projectData.usage.outputTokens += usage.outputTokens;
|
|
146
|
+
projectData.usage.totalTokens += usage.totalTokens;
|
|
147
|
+
if (!modelMap.has(model)) {
|
|
148
|
+
modelMap.set(model, {
|
|
149
|
+
usage: createEmptyTokenUsage(),
|
|
150
|
+
messageCount: 0
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const modelData = modelMap.get(model);
|
|
154
|
+
modelData.usage.inputTokens += usage.inputTokens;
|
|
155
|
+
modelData.usage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
156
|
+
modelData.usage.cacheReadTokens += usage.cacheReadTokens;
|
|
157
|
+
modelData.usage.outputTokens += usage.outputTokens;
|
|
158
|
+
modelData.usage.totalTokens += usage.totalTokens;
|
|
159
|
+
modelData.messageCount++;
|
|
160
|
+
totalCacheCreation += usage.cacheCreationTokens;
|
|
161
|
+
totalCacheRead += usage.cacheReadTokens;
|
|
162
|
+
const cacheCreation = message.message?.usage?.cache_creation;
|
|
163
|
+
if (cacheCreation) {
|
|
164
|
+
ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
|
|
165
|
+
ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const toolUseIds = new Set();
|
|
169
|
+
const successfulToolUseIds = new Set();
|
|
170
|
+
for (const message of messages) {
|
|
171
|
+
const messageDate = new Date(message.timestamp);
|
|
172
|
+
if (messageDate < cutoffDate)
|
|
173
|
+
continue;
|
|
174
|
+
if (message.type === 'assistant' && message.message?.content && Array.isArray(message.message.content)) {
|
|
175
|
+
for (const item of message.message.content) {
|
|
176
|
+
if (item.type === 'tool_use' && item.name && item.id) {
|
|
177
|
+
const toolName = item.name;
|
|
178
|
+
if (!toolUsageMap.has(toolName)) {
|
|
179
|
+
toolUsageMap.set(toolName, { total: 0, successful: 0 });
|
|
180
|
+
}
|
|
181
|
+
if (!toolUseIds.has(item.id)) {
|
|
182
|
+
toolUseIds.add(item.id);
|
|
183
|
+
toolUsageMap.get(toolName).total++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const message of messages) {
|
|
190
|
+
const messageDate = new Date(message.timestamp);
|
|
191
|
+
if (messageDate < cutoffDate)
|
|
192
|
+
continue;
|
|
193
|
+
if (message.message?.content && Array.isArray(message.message.content)) {
|
|
194
|
+
for (const item of message.message.content) {
|
|
195
|
+
if (item.type === 'tool_result' && !item.is_error && item.tool_use_id) {
|
|
196
|
+
if (!successfulToolUseIds.has(item.tool_use_id)) {
|
|
197
|
+
successfulToolUseIds.add(item.tool_use_id);
|
|
198
|
+
let found = false;
|
|
199
|
+
for (const msg of messages) {
|
|
200
|
+
if (found)
|
|
201
|
+
break;
|
|
202
|
+
if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
|
|
203
|
+
for (const toolUseItem of msg.message.content) {
|
|
204
|
+
if (toolUseItem.type === 'tool_use' && toolUseItem.id === item.tool_use_id && toolUseItem.name) {
|
|
205
|
+
const toolStats = toolUsageMap.get(toolUseItem.name);
|
|
206
|
+
if (toolStats) {
|
|
207
|
+
toolStats.successful++;
|
|
208
|
+
found = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error(`Error processing ${file}:`, error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const totalUsage = aggregateTokenUsage(allUsages);
|
|
227
|
+
const totalCost = allCosts.reduce((acc, cost) => ({
|
|
228
|
+
inputCost: acc.inputCost + cost.inputCost,
|
|
229
|
+
outputCost: acc.outputCost + cost.outputCost,
|
|
230
|
+
cacheCreationCost: acc.cacheCreationCost + cost.cacheCreationCost,
|
|
231
|
+
cacheReadCost: acc.cacheReadCost + cost.cacheReadCost,
|
|
232
|
+
totalCost: acc.totalCost + cost.totalCost
|
|
233
|
+
}), { inputCost: 0, outputCost: 0, cacheCreationCost: 0, cacheReadCost: 0, totalCost: 0 });
|
|
234
|
+
const totalPotentialInput = totalUsage.inputTokens + totalCacheCreation + totalCacheRead;
|
|
235
|
+
const cacheHitRate = totalPotentialInput > 0 ? (totalCacheRead / totalPotentialInput) * 100 : 0;
|
|
236
|
+
const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
|
|
237
|
+
const savedCost = (totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
|
|
238
|
+
const daily = Array.from(dailyMap.entries())
|
|
239
|
+
.map(([date, data]) => ({
|
|
240
|
+
date,
|
|
241
|
+
usage: data.usage,
|
|
242
|
+
sessionCount: data.sessionIds.size
|
|
243
|
+
}))
|
|
244
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
245
|
+
const byProject = Array.from(projectMap.entries())
|
|
246
|
+
.map(([project, data]) => ({
|
|
247
|
+
project,
|
|
248
|
+
displayName: data.displayName,
|
|
249
|
+
usage: data.usage,
|
|
250
|
+
sessionCount: data.sessionIds.size
|
|
251
|
+
}))
|
|
252
|
+
.sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
|
|
253
|
+
const byModel = Array.from(modelMap.entries())
|
|
254
|
+
.map(([model, data]) => ({
|
|
255
|
+
model,
|
|
256
|
+
usage: data.usage,
|
|
257
|
+
messageCount: data.messageCount
|
|
258
|
+
}))
|
|
259
|
+
.sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
|
|
260
|
+
const toolUsage = Array.from(toolUsageMap.entries())
|
|
261
|
+
.map(([toolName, stats]) => ({
|
|
262
|
+
toolName,
|
|
263
|
+
totalUses: stats.total,
|
|
264
|
+
successfulUses: stats.successful,
|
|
265
|
+
successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
|
|
266
|
+
}))
|
|
267
|
+
.sort((a, b) => b.totalUses - a.totalUses);
|
|
268
|
+
const totalToolCalls = Array.from(toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
|
|
269
|
+
const agentUsageRate = totalSessions > 0 ? (totalAgentSessions / totalSessions) * 100 : 0;
|
|
270
|
+
const byHour = Array.from(hourlyMap.entries())
|
|
271
|
+
.map(([hour, data]) => ({
|
|
272
|
+
hour,
|
|
273
|
+
sessionCount: data.sessionIds.size,
|
|
274
|
+
messageCount: data.messageCount,
|
|
275
|
+
usage: data.usage
|
|
276
|
+
}))
|
|
277
|
+
.sort((a, b) => a.hour - b.hour);
|
|
278
|
+
const byWeekday = Array.from(weekdayMap.entries())
|
|
279
|
+
.map(([weekday, data]) => ({
|
|
280
|
+
weekday,
|
|
281
|
+
weekdayName: WEEKDAY_NAMES[weekday],
|
|
282
|
+
sessionCount: data.sessionIds.size,
|
|
283
|
+
messageCount: data.messageCount,
|
|
284
|
+
usage: data.usage
|
|
285
|
+
}))
|
|
286
|
+
.sort((a, b) => a.weekday - b.weekday);
|
|
287
|
+
const statistics = {
|
|
288
|
+
overview: {
|
|
289
|
+
total: totalUsage,
|
|
290
|
+
totalSessions,
|
|
291
|
+
totalMessages,
|
|
292
|
+
dateRange: {
|
|
293
|
+
start: minDate?.toISOString() || new Date().toISOString(),
|
|
294
|
+
end: maxDate?.toISOString() || new Date().toISOString()
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
daily,
|
|
298
|
+
byProject,
|
|
299
|
+
byModel,
|
|
300
|
+
cache: {
|
|
301
|
+
totalCacheCreation,
|
|
302
|
+
totalCacheRead,
|
|
303
|
+
ephemeral5mTokens,
|
|
304
|
+
ephemeral1hTokens,
|
|
305
|
+
cacheHitRate,
|
|
306
|
+
estimatedSavings: savedCost
|
|
307
|
+
},
|
|
308
|
+
cost: totalCost,
|
|
309
|
+
productivity: {
|
|
310
|
+
toolUsage,
|
|
311
|
+
totalToolCalls,
|
|
312
|
+
agentSessions: totalAgentSessions,
|
|
313
|
+
totalSessions,
|
|
314
|
+
agentUsageRate
|
|
315
|
+
},
|
|
316
|
+
trends: {
|
|
317
|
+
byHour,
|
|
318
|
+
byWeekday
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
return statistics;
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
console.error('Error calculating token statistics:', error);
|
|
325
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Project-specific constants
|
|
5
|
+
*/
|
|
6
|
+
// Directory paths
|
|
7
|
+
export const CLAUDE_DIR = join(homedir(), '.claude');
|
|
8
|
+
// Server configuration
|
|
9
|
+
export const DEFAULT_PORT = 9090;
|
|
10
|
+
// Session configuration
|
|
11
|
+
export const MAX_TITLE_LENGTH = 100;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Pure JSONL parser
|
|
4
|
+
* No session-specific logic, just file parsing
|
|
5
|
+
*/
|
|
6
|
+
export async function parseJsonl(filePath) {
|
|
7
|
+
const content = await readFile(filePath, 'utf-8');
|
|
8
|
+
return content
|
|
9
|
+
.split('\n')
|
|
10
|
+
.filter(line => line.trim())
|
|
11
|
+
.map(line => JSON.parse(line));
|
|
12
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic text utilities
|
|
3
|
+
* Pure functions with no project-specific logic
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Remove XML-like tags from text
|
|
7
|
+
*/
|
|
8
|
+
export function removeTags(text, tags) {
|
|
9
|
+
let result = text;
|
|
10
|
+
for (const tag of tags) {
|
|
11
|
+
const regex = new RegExp(`<${tag}>[\\s\\S]*?</${tag}>`, 'g');
|
|
12
|
+
result = result.replace(regex, ' ');
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalize whitespace in text
|
|
18
|
+
*/
|
|
19
|
+
export function normalizeWhitespace(text) {
|
|
20
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Clean text by removing common IDE/system tags
|
|
24
|
+
*/
|
|
25
|
+
export function cleanText(text) {
|
|
26
|
+
const cleaned = removeTags(text, ['ide_selection', 'ide_opened_file', 'system-reminder']);
|
|
27
|
+
return normalizeWhitespace(cleaned);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Truncate text to maximum length
|
|
31
|
+
*/
|
|
32
|
+
export function truncate(text, maxLength) {
|
|
33
|
+
return text.length > maxLength ? text.substring(0, maxLength) : text;
|
|
34
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic token statistics utilities
|
|
3
|
+
* Pure calculation functions
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract token usage from a message object
|
|
7
|
+
*/
|
|
8
|
+
export function extractTokenUsage(message) {
|
|
9
|
+
if (message.type !== 'assistant' || !message.message?.usage) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const usage = message.message.usage;
|
|
13
|
+
const model = message.message.model || 'unknown';
|
|
14
|
+
return {
|
|
15
|
+
usage: {
|
|
16
|
+
inputTokens: usage.input_tokens || 0,
|
|
17
|
+
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
18
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
19
|
+
outputTokens: usage.output_tokens || 0,
|
|
20
|
+
totalTokens: (usage.input_tokens || 0) +
|
|
21
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
22
|
+
(usage.cache_read_input_tokens || 0) +
|
|
23
|
+
(usage.output_tokens || 0)
|
|
24
|
+
},
|
|
25
|
+
model
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Aggregate multiple token usages into a single total
|
|
30
|
+
*/
|
|
31
|
+
export function aggregateTokenUsage(usages) {
|
|
32
|
+
return usages.reduce((acc, usage) => ({
|
|
33
|
+
inputTokens: acc.inputTokens + usage.inputTokens,
|
|
34
|
+
cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
|
|
35
|
+
cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
|
|
36
|
+
outputTokens: acc.outputTokens + usage.outputTokens,
|
|
37
|
+
totalTokens: acc.totalTokens + usage.totalTokens
|
|
38
|
+
}), createEmptyTokenUsage());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculate cost for token usage based on pricing
|
|
42
|
+
*/
|
|
43
|
+
export function calculateCost(usage, pricing) {
|
|
44
|
+
return {
|
|
45
|
+
inputCost: (usage.inputTokens / 1000000) * pricing.input,
|
|
46
|
+
outputCost: (usage.outputTokens / 1000000) * pricing.output,
|
|
47
|
+
cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
|
|
48
|
+
cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
|
|
49
|
+
totalCost: (usage.inputTokens / 1000000) * pricing.input +
|
|
50
|
+
(usage.outputTokens / 1000000) * pricing.output +
|
|
51
|
+
(usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
|
|
52
|
+
(usage.cacheReadTokens / 1000000) * pricing.cacheRead
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an empty token usage object
|
|
57
|
+
*/
|
|
58
|
+
export function createEmptyTokenUsage() {
|
|
59
|
+
return {
|
|
60
|
+
inputTokens: 0,
|
|
61
|
+
cacheCreationTokens: 0,
|
|
62
|
+
cacheReadTokens: 0,
|
|
63
|
+
outputTokens: 0,
|
|
64
|
+
totalTokens: 0
|
|
65
|
+
};
|
|
66
|
+
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5\/6{width:83.333333%}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-col-resize{cursor:col-resize}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/20{border-color:#3b82f633}.border-cyan-500\/20{border-color:#06b6d433}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-500\/20{border-color:#22c55e33}.border-orange-500\/20{border-color:#f9731633}.border-pink-500\/20{border-color:#ec489933}.border-purple-500\/20{border-color:#a855f733}.border-transparent{border-color:transparent}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/30{background-color:#1f29374d}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-900\/70{background-color:#111827b3}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-900\/50{background-color:#14532d80}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity, 1))}.bg-pink-500\/10{background-color:#ec48991a}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-700{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.bg-purple-900\/50{background-color:#581c8780}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-500{--tw-gradient-to: #22c55e var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-14{padding-left:3.5rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-tight{line-height:1.25}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-100{--tw-text-opacity: 1;color:rgb(243 232 255 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.\@container{container-type:inline-size}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}#root{width:100%;min-height:100vh}*{scrollbar-width:thin;scrollbar-color:#4B5563 #1F2937}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:#1f2937}*::-webkit-scrollbar-thumb{background-color:#4b5563;border-radius:4px}*::-webkit-scrollbar-thumb:hover{background-color:#6b7280}.last\:border-b-0:last-child{border-bottom-width:0px}.last\:pb-0:last-child{padding-bottom:0}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-600:hover{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}@container (min-width: 300px){.\@\[300px\]\:inline{display:inline}.\@\[300px\]\:hidden{display:none}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
|