claude-code-templates 1.5.0 → 1.5.2
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/package.json +1 -1
- package/src/analytics.js +132 -20
package/package.json
CHANGED
package/src/analytics.js
CHANGED
|
@@ -70,12 +70,33 @@ class ClaudeAnalytics {
|
|
|
70
70
|
const conversations = [];
|
|
71
71
|
|
|
72
72
|
try {
|
|
73
|
-
|
|
74
|
-
const
|
|
73
|
+
// Search for .jsonl files recursively in all subdirectories
|
|
74
|
+
const findJsonlFiles = async (dir) => {
|
|
75
|
+
const files = [];
|
|
76
|
+
const items = await fs.readdir(dir);
|
|
77
|
+
|
|
78
|
+
for (const item of items) {
|
|
79
|
+
const itemPath = path.join(dir, item);
|
|
80
|
+
const stats = await fs.stat(itemPath);
|
|
81
|
+
|
|
82
|
+
if (stats.isDirectory()) {
|
|
83
|
+
// Recursively search subdirectories
|
|
84
|
+
const subFiles = await findJsonlFiles(itemPath);
|
|
85
|
+
files.push(...subFiles);
|
|
86
|
+
} else if (item.endsWith('.jsonl')) {
|
|
87
|
+
files.push(itemPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return files;
|
|
92
|
+
};
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
const jsonlFiles = await findJsonlFiles(this.claudeDir);
|
|
95
|
+
console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
|
|
96
|
+
|
|
97
|
+
for (const filePath of jsonlFiles) {
|
|
78
98
|
const stats = await fs.stat(filePath);
|
|
99
|
+
const filename = path.basename(filePath);
|
|
79
100
|
|
|
80
101
|
try {
|
|
81
102
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -88,21 +109,25 @@ class ClaudeAnalytics {
|
|
|
88
109
|
}
|
|
89
110
|
}).filter(Boolean);
|
|
90
111
|
|
|
112
|
+
// Extract project name from path
|
|
113
|
+
const projectFromPath = this.extractProjectFromPath(filePath);
|
|
114
|
+
|
|
91
115
|
const conversation = {
|
|
92
|
-
id:
|
|
93
|
-
filename:
|
|
116
|
+
id: filename.replace('.jsonl', ''),
|
|
117
|
+
filename: filename,
|
|
118
|
+
filePath: filePath,
|
|
94
119
|
messageCount: messages.length,
|
|
95
120
|
fileSize: stats.size,
|
|
96
121
|
lastModified: stats.mtime,
|
|
97
122
|
created: stats.birthtime,
|
|
98
123
|
tokens: this.estimateTokens(content),
|
|
99
|
-
project: this.extractProjectFromConversation(messages),
|
|
124
|
+
project: projectFromPath || this.extractProjectFromConversation(messages),
|
|
100
125
|
status: this.determineConversationStatus(messages, stats.mtime)
|
|
101
126
|
};
|
|
102
127
|
|
|
103
128
|
conversations.push(conversation);
|
|
104
129
|
} catch (error) {
|
|
105
|
-
console.warn(chalk.yellow(`Warning: Could not parse ${
|
|
130
|
+
console.warn(chalk.yellow(`Warning: Could not parse ${filename}:`, error.message));
|
|
106
131
|
}
|
|
107
132
|
}
|
|
108
133
|
|
|
@@ -160,6 +185,27 @@ class ClaudeAnalytics {
|
|
|
160
185
|
return Math.ceil(text.length / 4);
|
|
161
186
|
}
|
|
162
187
|
|
|
188
|
+
extractProjectFromPath(filePath) {
|
|
189
|
+
// Extract project name from file path like:
|
|
190
|
+
// /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
|
|
191
|
+
const pathParts = filePath.split('/');
|
|
192
|
+
const projectIndex = pathParts.findIndex(part => part === 'projects');
|
|
193
|
+
|
|
194
|
+
if (projectIndex !== -1 && projectIndex + 1 < pathParts.length) {
|
|
195
|
+
const projectDir = pathParts[projectIndex + 1];
|
|
196
|
+
// Clean up the project directory name
|
|
197
|
+
const cleanName = projectDir
|
|
198
|
+
.replace(/^-/, '')
|
|
199
|
+
.replace(/-/g, '/')
|
|
200
|
+
.split('/')
|
|
201
|
+
.pop() || 'Unknown';
|
|
202
|
+
|
|
203
|
+
return cleanName;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
extractProjectFromConversation(messages) {
|
|
164
210
|
// Try to extract project information from conversation
|
|
165
211
|
for (const message of messages.slice(0, 5)) {
|
|
@@ -233,20 +279,24 @@ class ClaudeAnalytics {
|
|
|
233
279
|
setupFileWatchers() {
|
|
234
280
|
console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
|
|
235
281
|
|
|
236
|
-
// Watch conversation files
|
|
237
|
-
const conversationWatcher = chokidar.watch(
|
|
282
|
+
// Watch conversation files recursively in all subdirectories
|
|
283
|
+
const conversationWatcher = chokidar.watch([
|
|
284
|
+
path.join(this.claudeDir, '**/*.jsonl')
|
|
285
|
+
], {
|
|
238
286
|
persistent: true,
|
|
239
287
|
ignoreInitial: true
|
|
240
288
|
});
|
|
241
289
|
|
|
242
290
|
conversationWatcher.on('change', async () => {
|
|
291
|
+
console.log(chalk.yellow('🔄 Conversation file changed, updating data...'));
|
|
243
292
|
await this.loadInitialData();
|
|
244
|
-
console.log(chalk.green('
|
|
293
|
+
console.log(chalk.green('✅ Data updated'));
|
|
245
294
|
});
|
|
246
295
|
|
|
247
296
|
conversationWatcher.on('add', async () => {
|
|
297
|
+
console.log(chalk.yellow('📝 New conversation file detected...'));
|
|
248
298
|
await this.loadInitialData();
|
|
249
|
-
console.log(chalk.green('
|
|
299
|
+
console.log(chalk.green('✅ Data updated'));
|
|
250
300
|
});
|
|
251
301
|
|
|
252
302
|
this.watchers.push(conversationWatcher);
|
|
@@ -255,15 +305,28 @@ class ClaudeAnalytics {
|
|
|
255
305
|
const projectWatcher = chokidar.watch(this.claudeDir, {
|
|
256
306
|
persistent: true,
|
|
257
307
|
ignoreInitial: true,
|
|
258
|
-
depth:
|
|
308
|
+
depth: 2 // Increased depth to catch subdirectories
|
|
259
309
|
});
|
|
260
310
|
|
|
261
311
|
projectWatcher.on('addDir', async () => {
|
|
312
|
+
console.log(chalk.yellow('📁 New project directory detected...'));
|
|
313
|
+
await this.loadInitialData();
|
|
314
|
+
console.log(chalk.green('✅ Data updated'));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
projectWatcher.on('change', async () => {
|
|
318
|
+
console.log(chalk.yellow('📁 Project directory changed...'));
|
|
262
319
|
await this.loadInitialData();
|
|
263
|
-
console.log(chalk.green('
|
|
320
|
+
console.log(chalk.green('✅ Data updated'));
|
|
264
321
|
});
|
|
265
322
|
|
|
266
323
|
this.watchers.push(projectWatcher);
|
|
324
|
+
|
|
325
|
+
// Also set up periodic refresh to catch any missed changes
|
|
326
|
+
setInterval(async () => {
|
|
327
|
+
console.log(chalk.blue('⏱️ Periodic data refresh...'));
|
|
328
|
+
await this.loadInitialData();
|
|
329
|
+
}, 30000); // Every 30 seconds
|
|
267
330
|
}
|
|
268
331
|
|
|
269
332
|
setupWebServer() {
|
|
@@ -271,12 +334,34 @@ class ClaudeAnalytics {
|
|
|
271
334
|
this.app.use(express.static(path.join(__dirname, 'analytics-web')));
|
|
272
335
|
|
|
273
336
|
// API endpoints
|
|
274
|
-
this.app.get('/api/data', (req, res) => {
|
|
275
|
-
|
|
337
|
+
this.app.get('/api/data', async (req, res) => {
|
|
338
|
+
// Add timestamp to verify data freshness
|
|
339
|
+
const dataWithTimestamp = {
|
|
340
|
+
...this.data,
|
|
341
|
+
timestamp: new Date().toISOString(),
|
|
342
|
+
lastUpdate: new Date().toLocaleString()
|
|
343
|
+
};
|
|
344
|
+
res.json(dataWithTimestamp);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
this.app.get('/api/realtime', async (req, res) => {
|
|
348
|
+
const realtimeWithTimestamp = {
|
|
349
|
+
...this.data.realtimeStats,
|
|
350
|
+
timestamp: new Date().toISOString(),
|
|
351
|
+
lastUpdate: new Date().toLocaleString()
|
|
352
|
+
};
|
|
353
|
+
res.json(realtimeWithTimestamp);
|
|
276
354
|
});
|
|
277
355
|
|
|
278
|
-
|
|
279
|
-
|
|
356
|
+
// Force refresh endpoint
|
|
357
|
+
this.app.get('/api/refresh', async (req, res) => {
|
|
358
|
+
console.log(chalk.blue('🔄 Manual refresh requested...'));
|
|
359
|
+
await this.loadInitialData();
|
|
360
|
+
res.json({
|
|
361
|
+
success: true,
|
|
362
|
+
message: 'Data refreshed',
|
|
363
|
+
timestamp: new Date().toISOString()
|
|
364
|
+
});
|
|
280
365
|
});
|
|
281
366
|
|
|
282
367
|
// Main dashboard route
|
|
@@ -543,6 +628,7 @@ async function createWebDashboard() {
|
|
|
543
628
|
Claude Code Analytics
|
|
544
629
|
</h1>
|
|
545
630
|
<p>Real-time monitoring of your Claude Code usage</p>
|
|
631
|
+
<p id="lastUpdate" style="font-size: 0.75rem; opacity: 0.8;"></p>
|
|
546
632
|
</div>
|
|
547
633
|
|
|
548
634
|
<div id="loading" class="loading">
|
|
@@ -601,9 +687,15 @@ async function createWebDashboard() {
|
|
|
601
687
|
const response = await fetch('/api/data');
|
|
602
688
|
const data = await response.json();
|
|
603
689
|
|
|
690
|
+
console.log('Data loaded:', data.timestamp);
|
|
691
|
+
|
|
604
692
|
document.getElementById('loading').style.display = 'none';
|
|
605
693
|
document.getElementById('dashboard').style.display = 'block';
|
|
606
694
|
|
|
695
|
+
// Update timestamp
|
|
696
|
+
document.getElementById('lastUpdate').textContent =
|
|
697
|
+
\`Last updated: \${data.lastUpdate}\`;
|
|
698
|
+
|
|
607
699
|
updateStats(data.summary);
|
|
608
700
|
updateConversations(data.conversations);
|
|
609
701
|
updateProjects(data.activeProjects);
|
|
@@ -660,11 +752,31 @@ async function createWebDashboard() {
|
|
|
660
752
|
\`).join('');
|
|
661
753
|
}
|
|
662
754
|
|
|
755
|
+
// Manual refresh function
|
|
756
|
+
async function forceRefresh() {
|
|
757
|
+
try {
|
|
758
|
+
const response = await fetch('/api/refresh');
|
|
759
|
+
const result = await response.json();
|
|
760
|
+
console.log('Manual refresh:', result);
|
|
761
|
+
await loadData();
|
|
762
|
+
} catch (error) {
|
|
763
|
+
console.error('Failed to refresh:', error);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
663
767
|
// Load initial data
|
|
664
768
|
loadData();
|
|
665
769
|
|
|
666
|
-
// Refresh data every
|
|
667
|
-
setInterval(loadData,
|
|
770
|
+
// Refresh data every 5 seconds
|
|
771
|
+
setInterval(loadData, 5000);
|
|
772
|
+
|
|
773
|
+
// Add keyboard shortcut for refresh (F5 or Ctrl+R)
|
|
774
|
+
document.addEventListener('keydown', function(e) {
|
|
775
|
+
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
|
|
776
|
+
e.preventDefault();
|
|
777
|
+
forceRefresh();
|
|
778
|
+
}
|
|
779
|
+
});
|
|
668
780
|
</script>
|
|
669
781
|
</body>
|
|
670
782
|
</html>`;
|