claude-code-templates 1.5.3 → 1.5.5
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 +389 -1
package/package.json
CHANGED
package/src/analytics.js
CHANGED
|
@@ -364,6 +364,69 @@ class ClaudeAnalytics {
|
|
|
364
364
|
});
|
|
365
365
|
});
|
|
366
366
|
|
|
367
|
+
// Session detail endpoint
|
|
368
|
+
this.app.get('/api/session/:sessionId', async (req, res) => {
|
|
369
|
+
const sessionId = req.params.sessionId;
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const session = this.data.conversations.find(conv => conv.id === sessionId);
|
|
373
|
+
if (!session) {
|
|
374
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Read the actual conversation file
|
|
378
|
+
const content = await fs.readFile(session.filePath, 'utf8');
|
|
379
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
380
|
+
const rawMessages = lines.map(line => {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(line);
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}).filter(Boolean);
|
|
387
|
+
|
|
388
|
+
// Extract actual messages from Claude Code format
|
|
389
|
+
const messages = rawMessages.map(item => {
|
|
390
|
+
if (item.message && item.message.role) {
|
|
391
|
+
let content = '';
|
|
392
|
+
|
|
393
|
+
if (typeof item.message.content === 'string') {
|
|
394
|
+
content = item.message.content;
|
|
395
|
+
} else if (Array.isArray(item.message.content)) {
|
|
396
|
+
content = item.message.content
|
|
397
|
+
.map(block => {
|
|
398
|
+
if (block.type === 'text') return block.text;
|
|
399
|
+
if (block.type === 'tool_use') return `[Tool: ${block.name}]`;
|
|
400
|
+
if (block.type === 'tool_result') return '[Tool Result]';
|
|
401
|
+
return block.content || '';
|
|
402
|
+
})
|
|
403
|
+
.join('\n');
|
|
404
|
+
} else if (item.message.content && item.message.content.length) {
|
|
405
|
+
content = item.message.content[0].text || '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
role: item.message.role,
|
|
410
|
+
content: content || 'No content',
|
|
411
|
+
timestamp: item.timestamp,
|
|
412
|
+
type: item.type
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}).filter(Boolean);
|
|
417
|
+
|
|
418
|
+
res.json({
|
|
419
|
+
session: session,
|
|
420
|
+
messages: messages,
|
|
421
|
+
timestamp: new Date().toISOString()
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error(chalk.red('Error loading session details:'), error.message);
|
|
426
|
+
res.status(500).json({ error: 'Failed to load session details' });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
367
430
|
// Main dashboard route
|
|
368
431
|
this.app.get('/', (req, res) => {
|
|
369
432
|
res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
|
|
@@ -643,6 +706,140 @@ async function createWebDashboard() {
|
|
|
643
706
|
font-style: italic;
|
|
644
707
|
}
|
|
645
708
|
|
|
709
|
+
.session-detail {
|
|
710
|
+
display: none;
|
|
711
|
+
margin-top: 20px;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.session-detail.active {
|
|
715
|
+
display: block;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.detail-header {
|
|
719
|
+
display: flex;
|
|
720
|
+
justify-content: space-between;
|
|
721
|
+
align-items: center;
|
|
722
|
+
padding: 16px 0;
|
|
723
|
+
border-bottom: 1px solid #30363d;
|
|
724
|
+
margin-bottom: 20px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.detail-title {
|
|
728
|
+
color: #58a6ff;
|
|
729
|
+
font-size: 1.1rem;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.detail-actions {
|
|
733
|
+
display: flex;
|
|
734
|
+
gap: 12px;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.btn {
|
|
738
|
+
background: none;
|
|
739
|
+
border: 1px solid #30363d;
|
|
740
|
+
color: #7d8590;
|
|
741
|
+
padding: 6px 12px;
|
|
742
|
+
border-radius: 4px;
|
|
743
|
+
cursor: pointer;
|
|
744
|
+
font-family: inherit;
|
|
745
|
+
font-size: 0.875rem;
|
|
746
|
+
transition: all 0.2s ease;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.btn:hover {
|
|
750
|
+
border-color: #58a6ff;
|
|
751
|
+
color: #58a6ff;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.btn-primary {
|
|
755
|
+
background: #58a6ff;
|
|
756
|
+
border-color: #58a6ff;
|
|
757
|
+
color: #0d1117;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.btn-primary:hover {
|
|
761
|
+
background: #79c0ff;
|
|
762
|
+
border-color: #79c0ff;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
.session-info {
|
|
766
|
+
display: grid;
|
|
767
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
768
|
+
gap: 20px;
|
|
769
|
+
margin-bottom: 30px;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.info-item {
|
|
773
|
+
display: flex;
|
|
774
|
+
flex-direction: column;
|
|
775
|
+
gap: 4px;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.info-label {
|
|
779
|
+
color: #7d8590;
|
|
780
|
+
font-size: 0.75rem;
|
|
781
|
+
text-transform: uppercase;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.info-value {
|
|
785
|
+
color: #c9d1d9;
|
|
786
|
+
font-size: 0.875rem;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.conversation-history {
|
|
790
|
+
border: 1px solid #30363d;
|
|
791
|
+
border-radius: 6px;
|
|
792
|
+
max-height: 600px;
|
|
793
|
+
overflow-y: auto;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
.message {
|
|
797
|
+
padding: 16px;
|
|
798
|
+
border-bottom: 1px solid #21262d;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.message:last-child {
|
|
802
|
+
border-bottom: none;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
.message-header {
|
|
806
|
+
display: flex;
|
|
807
|
+
justify-content: space-between;
|
|
808
|
+
align-items: center;
|
|
809
|
+
margin-bottom: 8px;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
.message-role {
|
|
813
|
+
color: #58a6ff;
|
|
814
|
+
font-size: 0.875rem;
|
|
815
|
+
font-weight: bold;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.message-role.user {
|
|
819
|
+
color: #3fb950;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.message-role.assistant {
|
|
823
|
+
color: #58a6ff;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.message-time {
|
|
827
|
+
color: #7d8590;
|
|
828
|
+
font-size: 0.75rem;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
.message-content {
|
|
832
|
+
color: #c9d1d9;
|
|
833
|
+
font-size: 0.875rem;
|
|
834
|
+
line-height: 1.5;
|
|
835
|
+
white-space: pre-wrap;
|
|
836
|
+
word-wrap: break-word;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.back-btn {
|
|
840
|
+
margin-bottom: 20px;
|
|
841
|
+
}
|
|
842
|
+
|
|
646
843
|
@media (max-width: 768px) {
|
|
647
844
|
.stats-bar {
|
|
648
845
|
gap: 20px;
|
|
@@ -733,12 +930,36 @@ async function createWebDashboard() {
|
|
|
733
930
|
<div id="noSessions" class="no-sessions" style="display: none;">
|
|
734
931
|
no sessions found for current filter
|
|
735
932
|
</div>
|
|
933
|
+
|
|
934
|
+
<div id="sessionDetail" class="session-detail">
|
|
935
|
+
<button class="btn back-btn" onclick="showSessionsList()">← back to sessions</button>
|
|
936
|
+
|
|
937
|
+
<div class="detail-header">
|
|
938
|
+
<div class="detail-title" id="detailTitle">session details</div>
|
|
939
|
+
<div class="detail-actions">
|
|
940
|
+
<button class="btn" onclick="exportSessionCSV()">export csv</button>
|
|
941
|
+
<button class="btn btn-primary" onclick="refreshSessionDetail()">refresh</button>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<div class="session-info" id="sessionInfo">
|
|
946
|
+
<!-- Session info will be loaded here -->
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
<div>
|
|
950
|
+
<h3 style="color: #7d8590; margin-bottom: 16px; font-size: 0.875rem; text-transform: uppercase;">conversation history</h3>
|
|
951
|
+
<div class="conversation-history" id="conversationHistory">
|
|
952
|
+
<!-- Conversation history will be loaded here -->
|
|
953
|
+
</div>
|
|
954
|
+
</div>
|
|
955
|
+
</div>
|
|
736
956
|
</div>
|
|
737
957
|
</div>
|
|
738
958
|
|
|
739
959
|
<script>
|
|
740
960
|
let allConversations = [];
|
|
741
961
|
let currentFilter = 'active';
|
|
962
|
+
let currentSession = null;
|
|
742
963
|
|
|
743
964
|
async function loadData() {
|
|
744
965
|
try {
|
|
@@ -791,7 +1012,7 @@ async function createWebDashboard() {
|
|
|
791
1012
|
noSessionsDiv.style.display = 'none';
|
|
792
1013
|
|
|
793
1014
|
tableBody.innerHTML = filteredConversations.map(conv => \`
|
|
794
|
-
<tr>
|
|
1015
|
+
<tr onclick="showSessionDetail('\${conv.id}')" style="cursor: pointer;">
|
|
795
1016
|
<td class="session-id">\${conv.id.substring(0, 8)}...</td>
|
|
796
1017
|
<td class="session-project">\${conv.project}</td>
|
|
797
1018
|
<td class="session-messages">\${conv.messageCount}</td>
|
|
@@ -815,6 +1036,16 @@ async function createWebDashboard() {
|
|
|
815
1036
|
return \`\${days}d ago\`;
|
|
816
1037
|
}
|
|
817
1038
|
|
|
1039
|
+
function formatMessageTime(timestamp) {
|
|
1040
|
+
const date = new Date(timestamp);
|
|
1041
|
+
return date.toLocaleTimeString('en-US', {
|
|
1042
|
+
hour12: false,
|
|
1043
|
+
hour: '2-digit',
|
|
1044
|
+
minute: '2-digit',
|
|
1045
|
+
second: '2-digit'
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
818
1049
|
// Filter button handlers
|
|
819
1050
|
document.addEventListener('DOMContentLoaded', function() {
|
|
820
1051
|
const filterButtons = document.querySelectorAll('.filter-btn');
|
|
@@ -836,6 +1067,163 @@ async function createWebDashboard() {
|
|
|
836
1067
|
});
|
|
837
1068
|
});
|
|
838
1069
|
|
|
1070
|
+
// Session detail functions
|
|
1071
|
+
async function showSessionDetail(sessionId) {
|
|
1072
|
+
currentSession = allConversations.find(conv => conv.id === sessionId);
|
|
1073
|
+
if (!currentSession) return;
|
|
1074
|
+
|
|
1075
|
+
// Hide sessions list and show detail
|
|
1076
|
+
document.querySelector('.filter-bar').style.display = 'none';
|
|
1077
|
+
document.querySelector('.sessions-table').style.display = 'none';
|
|
1078
|
+
document.getElementById('noSessions').style.display = 'none';
|
|
1079
|
+
document.getElementById('sessionDetail').classList.add('active');
|
|
1080
|
+
|
|
1081
|
+
// Update title
|
|
1082
|
+
document.getElementById('detailTitle').textContent = \`session: \${sessionId.substring(0, 8)}...\`;
|
|
1083
|
+
|
|
1084
|
+
// Load session info
|
|
1085
|
+
updateSessionInfo(currentSession);
|
|
1086
|
+
|
|
1087
|
+
// Load conversation history
|
|
1088
|
+
await loadConversationHistory(currentSession);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function showSessionsList() {
|
|
1092
|
+
document.getElementById('sessionDetail').classList.remove('active');
|
|
1093
|
+
document.querySelector('.filter-bar').style.display = 'flex';
|
|
1094
|
+
document.querySelector('.sessions-table').style.display = 'table';
|
|
1095
|
+
updateSessionsTable();
|
|
1096
|
+
currentSession = null;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function updateSessionInfo(session) {
|
|
1100
|
+
const container = document.getElementById('sessionInfo');
|
|
1101
|
+
|
|
1102
|
+
container.innerHTML = \`
|
|
1103
|
+
<div class="info-item">
|
|
1104
|
+
<div class="info-label">session id</div>
|
|
1105
|
+
<div class="info-value">\${session.id}</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
<div class="info-item">
|
|
1108
|
+
<div class="info-label">project</div>
|
|
1109
|
+
<div class="info-value">\${session.project}</div>
|
|
1110
|
+
</div>
|
|
1111
|
+
<div class="info-item">
|
|
1112
|
+
<div class="info-label">messages</div>
|
|
1113
|
+
<div class="info-value">\${session.messageCount}</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
<div class="info-item">
|
|
1116
|
+
<div class="info-label">tokens (estimated)</div>
|
|
1117
|
+
<div class="info-value">\${session.tokens.toLocaleString()}</div>
|
|
1118
|
+
</div>
|
|
1119
|
+
<div class="info-item">
|
|
1120
|
+
<div class="info-label">file size</div>
|
|
1121
|
+
<div class="info-value">\${formatBytes(session.fileSize)}</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
<div class="info-item">
|
|
1124
|
+
<div class="info-label">created</div>
|
|
1125
|
+
<div class="info-value">\${new Date(session.created).toLocaleString()}</div>
|
|
1126
|
+
</div>
|
|
1127
|
+
<div class="info-item">
|
|
1128
|
+
<div class="info-label">last modified</div>
|
|
1129
|
+
<div class="info-value">\${new Date(session.lastModified).toLocaleString()}</div>
|
|
1130
|
+
</div>
|
|
1131
|
+
<div class="info-item">
|
|
1132
|
+
<div class="info-label">status</div>
|
|
1133
|
+
<div class="info-value status-\${session.status}">\${session.status}</div>
|
|
1134
|
+
</div>
|
|
1135
|
+
\`;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
async function loadConversationHistory(session) {
|
|
1139
|
+
try {
|
|
1140
|
+
const response = await fetch(\`/api/session/\${session.id}\`);
|
|
1141
|
+
const sessionData = await response.json();
|
|
1142
|
+
|
|
1143
|
+
const container = document.getElementById('conversationHistory');
|
|
1144
|
+
|
|
1145
|
+
if (!sessionData.messages || sessionData.messages.length === 0) {
|
|
1146
|
+
container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">no messages found</div>';
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
container.innerHTML = sessionData.messages.map((message, index) => \`
|
|
1151
|
+
<div class="message">
|
|
1152
|
+
<div class="message-header">
|
|
1153
|
+
<div class="message-role \${message.role}">\${message.role}</div>
|
|
1154
|
+
<div class="message-time">
|
|
1155
|
+
#\${index + 1} • \${message.timestamp ? formatMessageTime(message.timestamp) : 'unknown time'}
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="message-content">\${truncateContent(message.content || 'no content')}</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
\`).join('');
|
|
1161
|
+
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
document.getElementById('conversationHistory').innerHTML =
|
|
1164
|
+
'<div style="padding: 20px; text-align: center; color: #f85149;">error loading conversation history</div>';
|
|
1165
|
+
console.error('Failed to load conversation history:', error);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function truncateContent(content, maxLength = 1000) {
|
|
1170
|
+
if (typeof content !== 'string') return 'no content';
|
|
1171
|
+
if (!content.trim()) return 'empty message';
|
|
1172
|
+
if (content.length <= maxLength) return content;
|
|
1173
|
+
return content.substring(0, maxLength) + '\\n\\n[... message truncated ...]';
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function formatBytes(bytes) {
|
|
1177
|
+
if (bytes === 0) return '0 B';
|
|
1178
|
+
const k = 1024;
|
|
1179
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1180
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1181
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function exportSessionCSV() {
|
|
1185
|
+
if (!currentSession) return;
|
|
1186
|
+
|
|
1187
|
+
// Create CSV content
|
|
1188
|
+
let csvContent = 'Session ID,Project,Message Count,Tokens,File Size,Created,Last Modified,Status\\n';
|
|
1189
|
+
csvContent += \`"\${currentSession.id}","\${currentSession.project}",\${currentSession.messageCount},\${currentSession.tokens},\${currentSession.fileSize},"\${new Date(currentSession.created).toISOString()}","\${new Date(currentSession.lastModified).toISOString()}","\${currentSession.status}"\\n\\n\`;
|
|
1190
|
+
|
|
1191
|
+
csvContent += 'Message #,Role,Content\\n';
|
|
1192
|
+
|
|
1193
|
+
// Add conversation history if loaded
|
|
1194
|
+
fetch(\`/api/session/\${currentSession.id}\`)
|
|
1195
|
+
.then(response => response.json())
|
|
1196
|
+
.then(sessionData => {
|
|
1197
|
+
if (sessionData.messages) {
|
|
1198
|
+
sessionData.messages.forEach((message, index) => {
|
|
1199
|
+
const content = (message.content || 'no content').replace(/"/g, '""');
|
|
1200
|
+
csvContent += \`\${index + 1},"\${message.role}","\${content}"\\n\`;
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Download CSV
|
|
1205
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
1206
|
+
const link = document.createElement('a');
|
|
1207
|
+
const url = URL.createObjectURL(blob);
|
|
1208
|
+
link.setAttribute('href', url);
|
|
1209
|
+
link.setAttribute('download', \`claude-session-\${currentSession.id.substring(0, 8)}.csv\`);
|
|
1210
|
+
link.style.visibility = 'hidden';
|
|
1211
|
+
document.body.appendChild(link);
|
|
1212
|
+
link.click();
|
|
1213
|
+
document.body.removeChild(link);
|
|
1214
|
+
})
|
|
1215
|
+
.catch(error => {
|
|
1216
|
+
console.error('Failed to export CSV:', error);
|
|
1217
|
+
alert('Failed to export CSV. Please try again.');
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function refreshSessionDetail() {
|
|
1222
|
+
if (currentSession) {
|
|
1223
|
+
loadConversationHistory(currentSession);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
839
1227
|
// Manual refresh function
|
|
840
1228
|
async function forceRefresh() {
|
|
841
1229
|
try {
|