claude-session-viewer 0.3.5 → 0.3.7
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/bin/dev.js +20 -1
- package/dist/client/assets/index-BKZOwcdy.css +1 -0
- package/dist/client/assets/index-Bo1qXVSw.js +70 -0
- package/dist/client/index.html +2 -2
- package/dist/server/claude/projects/repository.js +1 -1
- package/dist/server/claude/projects/service.js +4 -4
- package/dist/server/claude/sessions/service.js +9 -5
- package/dist/server/routes/sessionWindows.js +127 -0
- package/dist/server/routes/sessions.js +5 -4
- package/dist/server/routes/statistics.js +34 -310
- package/dist/server/statistics/aggregator.js +290 -0
- package/dist/server/statistics/service.js +226 -0
- package/dist/server/statistics/tokenStats.js +66 -0
- package/dist/server/statistics/tokenUsage.js +66 -0
- package/dist/server/statistics/utils.js +33 -0
- package/dist/server/utils/sessionWindows.js +159 -0
- package/package.json +2 -2
- package/dist/client/assets/index-BREABlIc.js +0 -69
- package/dist/client/assets/index-nqJgfpl7.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Claude Session Viewer</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Bo1qXVSw.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BKZOwcdy.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
|
@@ -30,7 +30,7 @@ export function getProjectPath(projectsDir, projectName) {
|
|
|
30
30
|
* Remove user's home directory prefix from project directory name
|
|
31
31
|
* e.g., "-Users-hanyeol-Projects-foo" → "Projects-foo"
|
|
32
32
|
*/
|
|
33
|
-
export function
|
|
33
|
+
export function getProjectName(projectDirName) {
|
|
34
34
|
const userHomePath = homedir().split('/').filter(Boolean).join('-');
|
|
35
35
|
const prefix = `-${userHomePath}-`;
|
|
36
36
|
if (projectDirName.startsWith(prefix)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stat } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { listProjects,
|
|
3
|
+
import { listProjects, getProjectName } from './repository.js';
|
|
4
4
|
import { getProjectSessions, sortSessionsByTimestamp } from '../sessions/service.js';
|
|
5
5
|
/**
|
|
6
6
|
* Project service
|
|
@@ -25,10 +25,10 @@ export async function getAllProjectsWithSessions(projectsDir) {
|
|
|
25
25
|
const sessions = await getProjectSessions(projectPath);
|
|
26
26
|
if (sessions.length > 0) {
|
|
27
27
|
sortSessionsByTimestamp(sessions);
|
|
28
|
-
const
|
|
28
|
+
const name = getProjectName(project);
|
|
29
29
|
projectGroups.push({
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
id: project,
|
|
31
|
+
name,
|
|
32
32
|
sessionCount: sessions.length,
|
|
33
33
|
lastActivity: sessions[0].timestamp,
|
|
34
34
|
sessions
|
|
@@ -3,7 +3,7 @@ import { listSessionFiles, readSessionFile } from './repository.js';
|
|
|
3
3
|
import { shouldSkipSession, isAgentSession, isEmptyFile } from './filters.js';
|
|
4
4
|
import { extractSessionTitle } from './title.js';
|
|
5
5
|
import { collectAgentDescriptions, attachAgentSessionsToParent } from './agents.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProjectName } from '../projects/repository.js';
|
|
7
7
|
/**
|
|
8
8
|
* Session service
|
|
9
9
|
* Orchestrates session loading, filtering, and organization
|
|
@@ -21,7 +21,8 @@ export async function getProjectSessions(projectPath) {
|
|
|
21
21
|
const sessionFiles = await listSessionFiles(projectPath);
|
|
22
22
|
const allSessions = [];
|
|
23
23
|
const agentSessionsMap = new Map();
|
|
24
|
-
const
|
|
24
|
+
const projectId = projectPath.split('/').pop() || 'unknown';
|
|
25
|
+
const projectName = getProjectName(projectId);
|
|
25
26
|
// First pass: load all sessions
|
|
26
27
|
for (const { filename, size, path } of sessionFiles) {
|
|
27
28
|
if (isEmptyFile(size))
|
|
@@ -33,7 +34,8 @@ export async function getProjectSessions(projectPath) {
|
|
|
33
34
|
continue;
|
|
34
35
|
const session = {
|
|
35
36
|
id: sessionId,
|
|
36
|
-
|
|
37
|
+
projectId,
|
|
38
|
+
projectName,
|
|
37
39
|
timestamp,
|
|
38
40
|
messages,
|
|
39
41
|
messageCount: messages.length,
|
|
@@ -61,15 +63,17 @@ export async function getProjectSessions(projectPath) {
|
|
|
61
63
|
/**
|
|
62
64
|
* Load agent sessions from files
|
|
63
65
|
*/
|
|
64
|
-
export async function loadAgentSessionsFromFiles(projectPath,
|
|
66
|
+
export async function loadAgentSessionsFromFiles(projectPath, projectId, agentDescriptions) {
|
|
65
67
|
const agentSessions = [];
|
|
68
|
+
const projectName = getProjectName(projectId);
|
|
66
69
|
for (const [agentSessionId, description] of agentDescriptions) {
|
|
67
70
|
const agentFile = join(projectPath, `${agentSessionId}.jsonl`);
|
|
68
71
|
try {
|
|
69
72
|
const { messages, timestamp } = await readSessionFile(agentFile);
|
|
70
73
|
agentSessions.push({
|
|
71
74
|
id: agentSessionId,
|
|
72
|
-
|
|
75
|
+
projectId,
|
|
76
|
+
projectName,
|
|
73
77
|
timestamp,
|
|
74
78
|
messages,
|
|
75
79
|
messageCount: messages.length,
|
|
@@ -0,0 +1,127 @@
|
|
|
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 { isAgentSession, isEmptyFile, shouldSkipSession } from '../claude/sessions/filters.js';
|
|
6
|
+
import { calculateSessionWindows, getActiveWindow, loadPlanConfig } from '../utils/sessionWindows.js';
|
|
7
|
+
import { PLAN_LIMITS } from '../claude/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Session window routes
|
|
10
|
+
*/
|
|
11
|
+
export async function registerSessionWindowRoutes(server) {
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/session-windows
|
|
14
|
+
* Returns all 5-hour session windows with token usage
|
|
15
|
+
*/
|
|
16
|
+
server.get('/api/session-windows', async (request, reply) => {
|
|
17
|
+
try {
|
|
18
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
19
|
+
const projects = await readdir(projectsDir);
|
|
20
|
+
// Parse days parameter
|
|
21
|
+
const daysParam = request.query.days || '8'; // Default 8 days for P90 calculation
|
|
22
|
+
const cutoffDate = new Date();
|
|
23
|
+
if (daysParam !== 'all') {
|
|
24
|
+
const days = parseInt(daysParam, 10);
|
|
25
|
+
cutoffDate.setDate(cutoffDate.getDate() - days);
|
|
26
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
cutoffDate.setFullYear(2000, 0, 1);
|
|
30
|
+
}
|
|
31
|
+
// Collect all session data
|
|
32
|
+
const sessionsData = [];
|
|
33
|
+
for (const project of projects) {
|
|
34
|
+
const projectPath = join(projectsDir, project);
|
|
35
|
+
const projectStat = await stat(projectPath);
|
|
36
|
+
if (!projectStat.isDirectory())
|
|
37
|
+
continue;
|
|
38
|
+
const files = await readdir(projectPath);
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (!file.endsWith('.jsonl'))
|
|
41
|
+
continue;
|
|
42
|
+
const filePath = join(projectPath, file);
|
|
43
|
+
const fileStat = await stat(filePath);
|
|
44
|
+
if (isEmptyFile(fileStat.size))
|
|
45
|
+
continue;
|
|
46
|
+
const sessionId = file.replace('.jsonl', '');
|
|
47
|
+
if (isAgentSession(sessionId))
|
|
48
|
+
continue;
|
|
49
|
+
try {
|
|
50
|
+
const messages = await parseJsonl(filePath);
|
|
51
|
+
if (shouldSkipSession(messages))
|
|
52
|
+
continue;
|
|
53
|
+
// Filter messages by date
|
|
54
|
+
const filteredMessages = messages.filter((msg) => {
|
|
55
|
+
if (!msg.timestamp)
|
|
56
|
+
return false;
|
|
57
|
+
const msgDate = new Date(msg.timestamp);
|
|
58
|
+
return msgDate >= cutoffDate;
|
|
59
|
+
});
|
|
60
|
+
if (filteredMessages.length > 0) {
|
|
61
|
+
sessionsData.push({
|
|
62
|
+
sessionId,
|
|
63
|
+
messages: filteredMessages
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error(`Error processing ${file}:`, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Calculate initial windows with default plan for detection
|
|
73
|
+
const initialWindows = calculateSessionWindows(sessionsData, {
|
|
74
|
+
name: 'Custom',
|
|
75
|
+
limits: PLAN_LIMITS.Custom,
|
|
76
|
+
autoDetected: false
|
|
77
|
+
});
|
|
78
|
+
// Auto-detect plan based on windows
|
|
79
|
+
const planConfig = loadPlanConfig(initialWindows);
|
|
80
|
+
// Recalculate windows with detected plan
|
|
81
|
+
const windows = calculateSessionWindows(sessionsData, planConfig);
|
|
82
|
+
// Get active window
|
|
83
|
+
const activeWindow = getActiveWindow(windows);
|
|
84
|
+
const response = {
|
|
85
|
+
windows,
|
|
86
|
+
activeWindow,
|
|
87
|
+
planConfig
|
|
88
|
+
};
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
console.error('Error calculating session windows:', error);
|
|
93
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* GET /api/session-windows/active
|
|
98
|
+
* Returns only the currently active session window
|
|
99
|
+
*/
|
|
100
|
+
server.get('/api/session-windows/active', async (request, reply) => {
|
|
101
|
+
try {
|
|
102
|
+
// Reuse the main endpoint logic but return only active window
|
|
103
|
+
const fullResponse = await server.inject({
|
|
104
|
+
method: 'GET',
|
|
105
|
+
url: '/api/session-windows?days=1' // Only check last day for performance
|
|
106
|
+
});
|
|
107
|
+
if (fullResponse.statusCode !== 200) {
|
|
108
|
+
return reply.code(fullResponse.statusCode).send(fullResponse.json());
|
|
109
|
+
}
|
|
110
|
+
const data = fullResponse.json();
|
|
111
|
+
if (!data.activeWindow) {
|
|
112
|
+
return {
|
|
113
|
+
activeWindow: null,
|
|
114
|
+
planConfig: data.planConfig
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
activeWindow: data.activeWindow,
|
|
119
|
+
planConfig: data.planConfig
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Error getting active session window:', error);
|
|
124
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { CLAUDE_DIR } from '../constants.js';
|
|
4
4
|
import { parseJsonl } from '../utils/jsonl.js';
|
|
5
5
|
import { getAllProjectsWithSessions } from '../claude/projects/service.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProjectName } from '../claude/projects/repository.js';
|
|
7
7
|
import { loadAgentSessionsFromFiles } from '../claude/sessions/service.js';
|
|
8
8
|
import { collectAgentDescriptions, injectAgentIdsIntoMessages, findAgentTitleFromParentMessages } from '../claude/sessions/agents.js';
|
|
9
9
|
import { isAgentSession } from '../claude/sessions/filters.js';
|
|
@@ -37,7 +37,7 @@ export async function registerSessionRoutes(server) {
|
|
|
37
37
|
try {
|
|
38
38
|
const messages = await parseJsonl(sessionFile);
|
|
39
39
|
const fileStat = await stat(sessionFile);
|
|
40
|
-
const projectName =
|
|
40
|
+
const projectName = project;
|
|
41
41
|
let title = extractSessionTitle(messages);
|
|
42
42
|
// For agent sessions, find description from parent
|
|
43
43
|
if (isAgent) {
|
|
@@ -64,14 +64,15 @@ export async function registerSessionRoutes(server) {
|
|
|
64
64
|
if (!isAgent) {
|
|
65
65
|
const agentDescriptions = collectAgentDescriptions(messages);
|
|
66
66
|
if (agentDescriptions.size > 0) {
|
|
67
|
-
agentSessions = await loadAgentSessionsFromFiles(projectPath,
|
|
67
|
+
agentSessions = await loadAgentSessionsFromFiles(projectPath, project, agentDescriptions);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
const messagesWithAgentIds = injectAgentIdsIntoMessages(messages);
|
|
71
71
|
return {
|
|
72
72
|
session: {
|
|
73
73
|
id,
|
|
74
|
-
|
|
74
|
+
projectId: projectName,
|
|
75
|
+
projectName: getProjectName(projectName),
|
|
75
76
|
timestamp: fileStat.mtime.toISOString(),
|
|
76
77
|
messages: messagesWithAgentIds,
|
|
77
78
|
messageCount: messages.length,
|
|
@@ -1,327 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
-
import {
|
|
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';
|
|
4
|
+
import { getOverallStatistics, getProjectStatistics } from '../statistics/service.js';
|
|
10
5
|
/**
|
|
11
6
|
* Statistics routes
|
|
12
7
|
*/
|
|
13
8
|
export async function registerStatisticsRoutes(server) {
|
|
14
|
-
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/statistics/overall
|
|
11
|
+
* Get overall usage statistics across all projects
|
|
12
|
+
*/
|
|
13
|
+
server.get('/api/statistics/overall', async (request, reply) => {
|
|
15
14
|
try {
|
|
15
|
+
const days = request.query.days || '7';
|
|
16
|
+
const statistics = await getOverallStatistics(days);
|
|
17
|
+
return statistics;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Error calculating overall statistics:', error);
|
|
21
|
+
return reply.code(500).send({ error: 'Internal server error' });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* GET /api/statistics/projects/:projectId
|
|
26
|
+
* Get usage statistics for a specific project
|
|
27
|
+
*/
|
|
28
|
+
server.get('/api/statistics/projects/:projectId', async (request, reply) => {
|
|
29
|
+
try {
|
|
30
|
+
const { projectId } = request.params;
|
|
16
31
|
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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);
|
|
32
|
+
const projectPath = join(projectsDir, projectId);
|
|
33
|
+
// Check if project exists
|
|
34
|
+
try {
|
|
61
35
|
const projectStat = await stat(projectPath);
|
|
62
|
-
if (!projectStat.isDirectory())
|
|
63
|
-
|
|
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
|
-
}
|
|
36
|
+
if (!projectStat.isDirectory()) {
|
|
37
|
+
return reply.code(404).send({ error: 'Project not found' });
|
|
224
38
|
}
|
|
225
39
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
};
|
|
40
|
+
catch (error) {
|
|
41
|
+
return reply.code(404).send({ error: 'Project not found' });
|
|
42
|
+
}
|
|
43
|
+
const days = request.query.days || '7';
|
|
44
|
+
const statistics = await getProjectStatistics(projectId, days);
|
|
321
45
|
return statistics;
|
|
322
46
|
}
|
|
323
47
|
catch (error) {
|
|
324
|
-
console.error('Error calculating
|
|
48
|
+
console.error('Error calculating project statistics:', error);
|
|
325
49
|
return reply.code(500).send({ error: 'Internal server error' });
|
|
326
50
|
}
|
|
327
51
|
});
|