claude-code-templates 1.4.2 → 1.5.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.
- package/bin/create-claude-config.js +4 -1
- package/package.json +10 -3
- package/src/analytics.js +675 -0
- package/src/command-stats.js +38 -17
- package/src/hook-stats.js +258 -0
- package/src/index.js +23 -2
- package/src/mcp-stats.js +321 -0
- package/README.md +0 -436
package/src/analytics.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const express = require('express');
|
|
5
|
+
const chokidar = require('chokidar');
|
|
6
|
+
const open = require('open');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
class ClaudeAnalytics {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.app = express();
|
|
12
|
+
this.port = 3333;
|
|
13
|
+
this.data = {
|
|
14
|
+
conversations: [],
|
|
15
|
+
summary: {},
|
|
16
|
+
activeProjects: [],
|
|
17
|
+
realtimeStats: {
|
|
18
|
+
totalSessions: 0,
|
|
19
|
+
totalTokens: 0,
|
|
20
|
+
activeProjects: 0,
|
|
21
|
+
lastActivity: null
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
this.watchers = [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async initialize() {
|
|
28
|
+
const homeDir = os.homedir();
|
|
29
|
+
this.claudeDir = path.join(homeDir, '.claude');
|
|
30
|
+
this.claudeDesktopDir = path.join(homeDir, 'Library', 'Application Support', 'Claude');
|
|
31
|
+
|
|
32
|
+
// Check if Claude directories exist
|
|
33
|
+
if (!await fs.pathExists(this.claudeDir)) {
|
|
34
|
+
throw new Error(`Claude Code directory not found at ${this.claudeDir}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await this.loadInitialData();
|
|
38
|
+
this.setupFileWatchers();
|
|
39
|
+
this.setupWebServer();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async loadInitialData() {
|
|
43
|
+
console.log(chalk.yellow('📊 Analyzing Claude Code data...'));
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Load conversation files
|
|
47
|
+
const conversations = await this.loadConversations();
|
|
48
|
+
this.data.conversations = conversations;
|
|
49
|
+
|
|
50
|
+
// Load active projects
|
|
51
|
+
const projects = await this.loadActiveProjects();
|
|
52
|
+
this.data.activeProjects = projects;
|
|
53
|
+
|
|
54
|
+
// Calculate summary statistics
|
|
55
|
+
this.data.summary = this.calculateSummary(conversations, projects);
|
|
56
|
+
|
|
57
|
+
// Update realtime stats
|
|
58
|
+
this.updateRealtimeStats();
|
|
59
|
+
|
|
60
|
+
console.log(chalk.green('✅ Data analysis complete'));
|
|
61
|
+
console.log(chalk.gray(`Found ${conversations.length} conversations across ${projects.length} projects`));
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error(chalk.red('Error loading Claude data:'), error.message);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async loadConversations() {
|
|
70
|
+
const conversations = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const files = await fs.readdir(this.claudeDir);
|
|
74
|
+
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
|
75
|
+
|
|
76
|
+
for (const file of jsonlFiles) {
|
|
77
|
+
const filePath = path.join(this.claudeDir, file);
|
|
78
|
+
const stats = await fs.stat(filePath);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
82
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
83
|
+
const messages = lines.map(line => {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(line);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}).filter(Boolean);
|
|
90
|
+
|
|
91
|
+
const conversation = {
|
|
92
|
+
id: file.replace('.jsonl', ''),
|
|
93
|
+
filename: file,
|
|
94
|
+
messageCount: messages.length,
|
|
95
|
+
fileSize: stats.size,
|
|
96
|
+
lastModified: stats.mtime,
|
|
97
|
+
created: stats.birthtime,
|
|
98
|
+
tokens: this.estimateTokens(content),
|
|
99
|
+
project: this.extractProjectFromConversation(messages),
|
|
100
|
+
status: this.determineConversationStatus(messages, stats.mtime)
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
conversations.push(conversation);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn(chalk.yellow(`Warning: Could not parse ${file}:`, error.message));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return conversations.sort((a, b) => b.lastModified - a.lastModified);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(chalk.red('Error loading conversations:'), error.message);
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async loadActiveProjects() {
|
|
117
|
+
const projects = [];
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const files = await fs.readdir(this.claudeDir);
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const filePath = path.join(this.claudeDir, file);
|
|
124
|
+
const stats = await fs.stat(filePath);
|
|
125
|
+
|
|
126
|
+
if (stats.isDirectory() && !file.startsWith('.')) {
|
|
127
|
+
const projectPath = filePath;
|
|
128
|
+
const todoFiles = await this.findTodoFiles(projectPath);
|
|
129
|
+
|
|
130
|
+
const project = {
|
|
131
|
+
name: file,
|
|
132
|
+
path: projectPath,
|
|
133
|
+
lastActivity: stats.mtime,
|
|
134
|
+
todoFiles: todoFiles.length,
|
|
135
|
+
status: this.determineProjectStatus(stats.mtime)
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
projects.push(project);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return projects.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error(chalk.red('Error loading projects:'), error.message);
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async findTodoFiles(projectPath) {
|
|
150
|
+
try {
|
|
151
|
+
const files = await fs.readdir(projectPath);
|
|
152
|
+
return files.filter(file => file.includes('todo') || file.includes('TODO'));
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
estimateTokens(text) {
|
|
159
|
+
// Simple token estimation (roughly 4 characters per token)
|
|
160
|
+
return Math.ceil(text.length / 4);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
extractProjectFromConversation(messages) {
|
|
164
|
+
// Try to extract project information from conversation
|
|
165
|
+
for (const message of messages.slice(0, 5)) {
|
|
166
|
+
if (message.content && typeof message.content === 'string') {
|
|
167
|
+
const pathMatch = message.content.match(/\/([^\/\s]+)$/);
|
|
168
|
+
if (pathMatch) {
|
|
169
|
+
return pathMatch[1];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return 'Unknown';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
determineConversationStatus(messages, lastModified) {
|
|
177
|
+
const now = new Date();
|
|
178
|
+
const timeDiff = now - lastModified;
|
|
179
|
+
const minutesAgo = timeDiff / (1000 * 60);
|
|
180
|
+
|
|
181
|
+
if (minutesAgo < 5) return 'active';
|
|
182
|
+
if (minutesAgo < 60) return 'recent';
|
|
183
|
+
return 'inactive';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
determineProjectStatus(lastActivity) {
|
|
187
|
+
const now = new Date();
|
|
188
|
+
const timeDiff = now - lastActivity;
|
|
189
|
+
const hoursAgo = timeDiff / (1000 * 60 * 60);
|
|
190
|
+
|
|
191
|
+
if (hoursAgo < 1) return 'active';
|
|
192
|
+
if (hoursAgo < 24) return 'recent';
|
|
193
|
+
return 'inactive';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
calculateSummary(conversations, projects) {
|
|
197
|
+
const totalTokens = conversations.reduce((sum, conv) => sum + conv.tokens, 0);
|
|
198
|
+
const totalSessions = conversations.length;
|
|
199
|
+
const activeConversations = conversations.filter(c => c.status === 'active').length;
|
|
200
|
+
const activeProjects = projects.filter(p => p.status === 'active').length;
|
|
201
|
+
|
|
202
|
+
const avgTokensPerSession = totalSessions > 0 ? Math.round(totalTokens / totalSessions) : 0;
|
|
203
|
+
const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
totalSessions,
|
|
207
|
+
totalTokens,
|
|
208
|
+
activeConversations,
|
|
209
|
+
activeProjects,
|
|
210
|
+
avgTokensPerSession,
|
|
211
|
+
totalFileSize: this.formatBytes(totalFileSize),
|
|
212
|
+
lastActivity: conversations.length > 0 ? conversations[0].lastModified : null
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
updateRealtimeStats() {
|
|
217
|
+
this.data.realtimeStats = {
|
|
218
|
+
totalSessions: this.data.conversations.length,
|
|
219
|
+
totalTokens: this.data.conversations.reduce((sum, conv) => sum + conv.tokens, 0),
|
|
220
|
+
activeProjects: this.data.activeProjects.filter(p => p.status === 'active').length,
|
|
221
|
+
lastActivity: this.data.summary.lastActivity
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
formatBytes(bytes) {
|
|
226
|
+
if (bytes === 0) return '0 Bytes';
|
|
227
|
+
const k = 1024;
|
|
228
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
229
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
230
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
setupFileWatchers() {
|
|
234
|
+
console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
|
|
235
|
+
|
|
236
|
+
// Watch conversation files
|
|
237
|
+
const conversationWatcher = chokidar.watch(path.join(this.claudeDir, '*.jsonl'), {
|
|
238
|
+
persistent: true,
|
|
239
|
+
ignoreInitial: true
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
conversationWatcher.on('change', async () => {
|
|
243
|
+
await this.loadInitialData();
|
|
244
|
+
console.log(chalk.green('🔄 Conversation data updated'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
conversationWatcher.on('add', async () => {
|
|
248
|
+
await this.loadInitialData();
|
|
249
|
+
console.log(chalk.green('📝 New conversation detected'));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.watchers.push(conversationWatcher);
|
|
253
|
+
|
|
254
|
+
// Watch project directories
|
|
255
|
+
const projectWatcher = chokidar.watch(this.claudeDir, {
|
|
256
|
+
persistent: true,
|
|
257
|
+
ignoreInitial: true,
|
|
258
|
+
depth: 1
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
projectWatcher.on('addDir', async () => {
|
|
262
|
+
await this.loadInitialData();
|
|
263
|
+
console.log(chalk.green('📁 New project detected'));
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
this.watchers.push(projectWatcher);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
setupWebServer() {
|
|
270
|
+
// Serve static files (we'll create the dashboard HTML)
|
|
271
|
+
this.app.use(express.static(path.join(__dirname, 'analytics-web')));
|
|
272
|
+
|
|
273
|
+
// API endpoints
|
|
274
|
+
this.app.get('/api/data', (req, res) => {
|
|
275
|
+
res.json(this.data);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
this.app.get('/api/realtime', (req, res) => {
|
|
279
|
+
res.json(this.data.realtimeStats);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Main dashboard route
|
|
283
|
+
this.app.get('/', (req, res) => {
|
|
284
|
+
res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async startServer() {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
this.server = this.app.listen(this.port, () => {
|
|
291
|
+
console.log(chalk.green(`🚀 Analytics dashboard started at http://localhost:${this.port}`));
|
|
292
|
+
resolve();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async openBrowser() {
|
|
298
|
+
try {
|
|
299
|
+
await open(`http://localhost:${this.port}`);
|
|
300
|
+
console.log(chalk.blue('🌐 Opening browser...'));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
|
|
303
|
+
console.log(chalk.cyan(`http://localhost:${this.port}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
stop() {
|
|
308
|
+
// Clean up watchers
|
|
309
|
+
this.watchers.forEach(watcher => watcher.close());
|
|
310
|
+
|
|
311
|
+
// Stop server
|
|
312
|
+
if (this.server) {
|
|
313
|
+
this.server.close();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log(chalk.yellow('Analytics dashboard stopped'));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runAnalytics(options = {}) {
|
|
321
|
+
console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
|
|
322
|
+
|
|
323
|
+
const analytics = new ClaudeAnalytics();
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await analytics.initialize();
|
|
327
|
+
|
|
328
|
+
// Create web dashboard files
|
|
329
|
+
await createWebDashboard();
|
|
330
|
+
|
|
331
|
+
await analytics.startServer();
|
|
332
|
+
await analytics.openBrowser();
|
|
333
|
+
|
|
334
|
+
console.log(chalk.green('✅ Analytics dashboard is running!'));
|
|
335
|
+
console.log(chalk.gray('Press Ctrl+C to stop the server'));
|
|
336
|
+
|
|
337
|
+
// Handle graceful shutdown
|
|
338
|
+
process.on('SIGINT', () => {
|
|
339
|
+
console.log(chalk.yellow('\n🛑 Shutting down analytics dashboard...'));
|
|
340
|
+
analytics.stop();
|
|
341
|
+
process.exit(0);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Keep the process running
|
|
345
|
+
await new Promise(() => {});
|
|
346
|
+
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(chalk.red('❌ Failed to start analytics dashboard:'), error.message);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function createWebDashboard() {
|
|
354
|
+
const webDir = path.join(__dirname, 'analytics-web');
|
|
355
|
+
await fs.ensureDir(webDir);
|
|
356
|
+
|
|
357
|
+
// Create the HTML dashboard
|
|
358
|
+
const htmlContent = `<!DOCTYPE html>
|
|
359
|
+
<html lang="en">
|
|
360
|
+
<head>
|
|
361
|
+
<meta charset="UTF-8">
|
|
362
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
363
|
+
<title>Claude Code Analytics Dashboard</title>
|
|
364
|
+
<style>
|
|
365
|
+
* {
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: 0;
|
|
368
|
+
box-sizing: border-box;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
body {
|
|
372
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
373
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
374
|
+
color: #333;
|
|
375
|
+
min-height: 100vh;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.container {
|
|
379
|
+
max-width: 1200px;
|
|
380
|
+
margin: 0 auto;
|
|
381
|
+
padding: 20px;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.header {
|
|
385
|
+
text-align: center;
|
|
386
|
+
color: white;
|
|
387
|
+
margin-bottom: 30px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.header h1 {
|
|
391
|
+
font-size: 2.5rem;
|
|
392
|
+
margin-bottom: 10px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.status-indicator {
|
|
396
|
+
display: inline-block;
|
|
397
|
+
width: 12px;
|
|
398
|
+
height: 12px;
|
|
399
|
+
border-radius: 50%;
|
|
400
|
+
background: #4ade80;
|
|
401
|
+
animation: pulse 2s infinite;
|
|
402
|
+
margin-right: 8px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@keyframes pulse {
|
|
406
|
+
0%, 100% { opacity: 1; }
|
|
407
|
+
50% { opacity: 0.5; }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.stats-grid {
|
|
411
|
+
display: grid;
|
|
412
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
413
|
+
gap: 20px;
|
|
414
|
+
margin-bottom: 30px;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.stat-card {
|
|
418
|
+
background: white;
|
|
419
|
+
border-radius: 12px;
|
|
420
|
+
padding: 24px;
|
|
421
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
422
|
+
transition: transform 0.2s ease;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.stat-card:hover {
|
|
426
|
+
transform: translateY(-2px);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.stat-card h3 {
|
|
430
|
+
color: #6b7280;
|
|
431
|
+
font-size: 0.875rem;
|
|
432
|
+
text-transform: uppercase;
|
|
433
|
+
letter-spacing: 0.05em;
|
|
434
|
+
margin-bottom: 8px;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.stat-card .value {
|
|
438
|
+
font-size: 2rem;
|
|
439
|
+
font-weight: bold;
|
|
440
|
+
color: #1f2937;
|
|
441
|
+
margin-bottom: 4px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.stat-card .label {
|
|
445
|
+
color: #9ca3af;
|
|
446
|
+
font-size: 0.875rem;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.content-grid {
|
|
450
|
+
display: grid;
|
|
451
|
+
grid-template-columns: 1fr 1fr;
|
|
452
|
+
gap: 20px;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.panel {
|
|
456
|
+
background: white;
|
|
457
|
+
border-radius: 12px;
|
|
458
|
+
padding: 24px;
|
|
459
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.panel h2 {
|
|
463
|
+
color: #1f2937;
|
|
464
|
+
margin-bottom: 20px;
|
|
465
|
+
font-size: 1.25rem;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.conversation-item, .project-item {
|
|
469
|
+
display: flex;
|
|
470
|
+
justify-content: space-between;
|
|
471
|
+
align-items: center;
|
|
472
|
+
padding: 12px 0;
|
|
473
|
+
border-bottom: 1px solid #f3f4f6;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.conversation-item:last-child, .project-item:last-child {
|
|
477
|
+
border-bottom: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.item-info h4 {
|
|
481
|
+
color: #1f2937;
|
|
482
|
+
font-size: 0.875rem;
|
|
483
|
+
margin-bottom: 4px;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.item-info p {
|
|
487
|
+
color: #6b7280;
|
|
488
|
+
font-size: 0.75rem;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.status-badge {
|
|
492
|
+
padding: 4px 8px;
|
|
493
|
+
border-radius: 12px;
|
|
494
|
+
font-size: 0.75rem;
|
|
495
|
+
font-weight: 500;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.status-active {
|
|
499
|
+
background: #d1fae5;
|
|
500
|
+
color: #065f46;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.status-recent {
|
|
504
|
+
background: #fef3c7;
|
|
505
|
+
color: #92400e;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.status-inactive {
|
|
509
|
+
background: #f3f4f6;
|
|
510
|
+
color: #6b7280;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.loading {
|
|
514
|
+
text-align: center;
|
|
515
|
+
color: white;
|
|
516
|
+
padding: 40px;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.error {
|
|
520
|
+
background: #fef2f2;
|
|
521
|
+
color: #dc2626;
|
|
522
|
+
padding: 16px;
|
|
523
|
+
border-radius: 8px;
|
|
524
|
+
margin: 20px 0;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@media (max-width: 768px) {
|
|
528
|
+
.content-grid {
|
|
529
|
+
grid-template-columns: 1fr;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.header h1 {
|
|
533
|
+
font-size: 2rem;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
</style>
|
|
537
|
+
</head>
|
|
538
|
+
<body>
|
|
539
|
+
<div class="container">
|
|
540
|
+
<div class="header">
|
|
541
|
+
<h1>
|
|
542
|
+
<span class="status-indicator"></span>
|
|
543
|
+
Claude Code Analytics
|
|
544
|
+
</h1>
|
|
545
|
+
<p>Real-time monitoring of your Claude Code usage</p>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
<div id="loading" class="loading">
|
|
549
|
+
<p>Loading analytics data...</p>
|
|
550
|
+
</div>
|
|
551
|
+
|
|
552
|
+
<div id="error" class="error" style="display: none;">
|
|
553
|
+
<p>Failed to load analytics data. Please check if Claude Code is installed.</p>
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<div id="dashboard" style="display: none;">
|
|
557
|
+
<div class="stats-grid">
|
|
558
|
+
<div class="stat-card">
|
|
559
|
+
<h3>Total Sessions</h3>
|
|
560
|
+
<div class="value" id="totalSessions">0</div>
|
|
561
|
+
<div class="label">Conversations</div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="stat-card">
|
|
564
|
+
<h3>Total Tokens</h3>
|
|
565
|
+
<div class="value" id="totalTokens">0</div>
|
|
566
|
+
<div class="label">Estimated</div>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="stat-card">
|
|
569
|
+
<h3>Active Projects</h3>
|
|
570
|
+
<div class="value" id="activeProjects">0</div>
|
|
571
|
+
<div class="label">Currently</div>
|
|
572
|
+
</div>
|
|
573
|
+
<div class="stat-card">
|
|
574
|
+
<h3>Data Size</h3>
|
|
575
|
+
<div class="value" id="dataSize">0</div>
|
|
576
|
+
<div class="label">Total</div>
|
|
577
|
+
</div>
|
|
578
|
+
</div>
|
|
579
|
+
|
|
580
|
+
<div class="content-grid">
|
|
581
|
+
<div class="panel">
|
|
582
|
+
<h2>Recent Conversations</h2>
|
|
583
|
+
<div id="conversations">
|
|
584
|
+
<!-- Conversations will be loaded here -->
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
|
|
588
|
+
<div class="panel">
|
|
589
|
+
<h2>Active Projects</h2>
|
|
590
|
+
<div id="projects">
|
|
591
|
+
<!-- Projects will be loaded here -->
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
|
|
598
|
+
<script>
|
|
599
|
+
async function loadData() {
|
|
600
|
+
try {
|
|
601
|
+
const response = await fetch('/api/data');
|
|
602
|
+
const data = await response.json();
|
|
603
|
+
|
|
604
|
+
document.getElementById('loading').style.display = 'none';
|
|
605
|
+
document.getElementById('dashboard').style.display = 'block';
|
|
606
|
+
|
|
607
|
+
updateStats(data.summary);
|
|
608
|
+
updateConversations(data.conversations);
|
|
609
|
+
updateProjects(data.activeProjects);
|
|
610
|
+
|
|
611
|
+
} catch (error) {
|
|
612
|
+
document.getElementById('loading').style.display = 'none';
|
|
613
|
+
document.getElementById('error').style.display = 'block';
|
|
614
|
+
console.error('Failed to load data:', error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function updateStats(summary) {
|
|
619
|
+
document.getElementById('totalSessions').textContent = summary.totalSessions.toLocaleString();
|
|
620
|
+
document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
|
|
621
|
+
document.getElementById('activeProjects').textContent = summary.activeProjects;
|
|
622
|
+
document.getElementById('dataSize').textContent = summary.totalFileSize;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function updateConversations(conversations) {
|
|
626
|
+
const container = document.getElementById('conversations');
|
|
627
|
+
|
|
628
|
+
if (conversations.length === 0) {
|
|
629
|
+
container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No conversations found</p>';
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
container.innerHTML = conversations.slice(0, 10).map(conv => \`
|
|
634
|
+
<div class="conversation-item">
|
|
635
|
+
<div class="item-info">
|
|
636
|
+
<h4>\${conv.project}</h4>
|
|
637
|
+
<p>\${conv.messageCount} messages • \${conv.tokens.toLocaleString()} tokens</p>
|
|
638
|
+
</div>
|
|
639
|
+
<span class="status-badge status-\${conv.status}">\${conv.status}</span>
|
|
640
|
+
</div>
|
|
641
|
+
\`).join('');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function updateProjects(projects) {
|
|
645
|
+
const container = document.getElementById('projects');
|
|
646
|
+
|
|
647
|
+
if (projects.length === 0) {
|
|
648
|
+
container.innerHTML = '<p style="color: #6b7280; text-align: center; padding: 20px;">No projects found</p>';
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
container.innerHTML = projects.slice(0, 10).map(project => \`
|
|
653
|
+
<div class="project-item">
|
|
654
|
+
<div class="item-info">
|
|
655
|
+
<h4>\${project.name}</h4>
|
|
656
|
+
<p>\${project.todoFiles} todo files</p>
|
|
657
|
+
</div>
|
|
658
|
+
<span class="status-badge status-\${project.status}">\${project.status}</span>
|
|
659
|
+
</div>
|
|
660
|
+
\`).join('');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Load initial data
|
|
664
|
+
loadData();
|
|
665
|
+
|
|
666
|
+
// Refresh data every 10 seconds
|
|
667
|
+
setInterval(loadData, 10000);
|
|
668
|
+
</script>
|
|
669
|
+
</body>
|
|
670
|
+
</html>`;
|
|
671
|
+
|
|
672
|
+
await fs.writeFile(path.join(webDir, 'index.html'), htmlContent);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
module.exports = { runAnalytics };
|