claude-code-templates 1.12.2 → 1.13.1
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 +3 -0
- package/package.json +1 -1
- package/src/analytics/core/AgentAnalyzer.js +341 -0
- package/src/analytics/core/SessionAnalyzer.js +101 -46
- package/src/analytics-web/components/AgentAnalytics.js +710 -0
- package/src/analytics-web/components/DashboardPage.js +810 -39
- package/src/analytics-web/components/SessionTimer.js +14 -11
- package/src/analytics-web/index.html +666 -65
- package/src/analytics.js +112 -10
- package/src/index.js +183 -0
- package/src/analytics-web/components/Dashboard.js.deprecated +0 -589
|
@@ -54,6 +54,9 @@ program
|
|
|
54
54
|
.option('--analytics', 'launch real-time Claude Code analytics dashboard')
|
|
55
55
|
.option('--chats, --agents', 'launch Claude Code chats/agents dashboard (opens directly to conversations)')
|
|
56
56
|
.option('--health-check, --health, --check, --verify', 'run comprehensive health check to verify Claude Code setup')
|
|
57
|
+
.option('--agent <agent>', 'install specific agent component')
|
|
58
|
+
.option('--command <command>', 'install specific command component')
|
|
59
|
+
.option('--mcp <mcp>', 'install specific MCP component')
|
|
57
60
|
.action(async (options) => {
|
|
58
61
|
try {
|
|
59
62
|
await createClaudeConfig(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentAnalyzer - Analyzes Claude Code specialized agent usage patterns
|
|
3
|
+
* Extracts agent invocation data, usage frequency, and workflow patterns
|
|
4
|
+
*/
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class AgentAnalyzer {
|
|
10
|
+
constructor() {
|
|
11
|
+
// Known Claude Code specialized agents
|
|
12
|
+
this.AGENT_TYPES = {
|
|
13
|
+
'general-purpose': {
|
|
14
|
+
name: 'General Purpose',
|
|
15
|
+
description: 'Multi-step tasks and research',
|
|
16
|
+
color: '#3fb950',
|
|
17
|
+
icon: '🔧'
|
|
18
|
+
},
|
|
19
|
+
'claude-code-best-practices': {
|
|
20
|
+
name: 'Claude Code Best Practices',
|
|
21
|
+
description: 'Workflow optimization and setup guidance',
|
|
22
|
+
color: '#f97316',
|
|
23
|
+
icon: '⚡'
|
|
24
|
+
},
|
|
25
|
+
'docusaurus-expert': {
|
|
26
|
+
name: 'Docusaurus Expert',
|
|
27
|
+
description: 'Documentation site management',
|
|
28
|
+
color: '#0969da',
|
|
29
|
+
icon: '📚'
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Analyze agent usage across all conversations
|
|
36
|
+
* @param {Array} conversations - Array of conversation objects with parsed messages
|
|
37
|
+
* @param {Object} dateRange - Optional date range filter
|
|
38
|
+
* @returns {Object} Agent usage analysis
|
|
39
|
+
*/
|
|
40
|
+
async analyzeAgentUsage(conversations, dateRange = null) {
|
|
41
|
+
const agentStats = {};
|
|
42
|
+
const agentTimeline = [];
|
|
43
|
+
const agentWorkflows = {};
|
|
44
|
+
let totalAgentInvocations = 0;
|
|
45
|
+
|
|
46
|
+
for (const conversation of conversations) {
|
|
47
|
+
// Parse messages from JSONL file if not already parsed
|
|
48
|
+
let messages = conversation.parsedMessages;
|
|
49
|
+
if (!messages && conversation.filePath) {
|
|
50
|
+
messages = await this.parseJsonlFile(conversation.filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!messages) continue;
|
|
54
|
+
|
|
55
|
+
messages.forEach(message => {
|
|
56
|
+
// Skip if outside date range
|
|
57
|
+
if (dateRange && !this.isWithinDateRange(message.timestamp, dateRange)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Look for Task tool usage with subagent_type
|
|
62
|
+
// Handle both direct message structure and nested message structure
|
|
63
|
+
const messageContent = message.message ? message.message.content : message.content;
|
|
64
|
+
const messageRole = message.message ? message.message.role : message.role;
|
|
65
|
+
|
|
66
|
+
if (messageRole === 'assistant' &&
|
|
67
|
+
messageContent &&
|
|
68
|
+
Array.isArray(messageContent)) {
|
|
69
|
+
|
|
70
|
+
messageContent.forEach(content => {
|
|
71
|
+
if (content.type === 'tool_use' &&
|
|
72
|
+
content.name === 'Task' &&
|
|
73
|
+
content.input &&
|
|
74
|
+
content.input.subagent_type) {
|
|
75
|
+
|
|
76
|
+
const agentType = content.input.subagent_type;
|
|
77
|
+
const timestamp = new Date(message.timestamp);
|
|
78
|
+
const prompt = content.input.prompt || content.input.description || 'No description';
|
|
79
|
+
|
|
80
|
+
// Initialize agent stats
|
|
81
|
+
if (!agentStats[agentType]) {
|
|
82
|
+
agentStats[agentType] = {
|
|
83
|
+
type: agentType,
|
|
84
|
+
name: this.AGENT_TYPES[agentType]?.name || agentType,
|
|
85
|
+
description: this.AGENT_TYPES[agentType]?.description || 'Custom agent',
|
|
86
|
+
color: this.AGENT_TYPES[agentType]?.color || '#8b5cf6',
|
|
87
|
+
icon: this.AGENT_TYPES[agentType]?.icon || '🤖',
|
|
88
|
+
totalInvocations: 0,
|
|
89
|
+
uniqueConversations: new Set(),
|
|
90
|
+
firstUsed: timestamp,
|
|
91
|
+
lastUsed: timestamp,
|
|
92
|
+
prompts: [],
|
|
93
|
+
hourlyDistribution: new Array(24).fill(0),
|
|
94
|
+
dailyUsage: {}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const stats = agentStats[agentType];
|
|
99
|
+
|
|
100
|
+
// Update stats
|
|
101
|
+
stats.totalInvocations++;
|
|
102
|
+
stats.uniqueConversations.add(conversation.id);
|
|
103
|
+
stats.lastUsed = new Date(Math.max(stats.lastUsed, timestamp));
|
|
104
|
+
stats.firstUsed = new Date(Math.min(stats.firstUsed, timestamp));
|
|
105
|
+
|
|
106
|
+
// Store prompt for analysis
|
|
107
|
+
stats.prompts.push({
|
|
108
|
+
text: prompt,
|
|
109
|
+
timestamp: timestamp,
|
|
110
|
+
conversationId: conversation.id
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Track hourly distribution
|
|
114
|
+
const hour = timestamp.getHours();
|
|
115
|
+
stats.hourlyDistribution[hour]++;
|
|
116
|
+
|
|
117
|
+
// Track daily usage
|
|
118
|
+
const dateKey = timestamp.toISOString().split('T')[0];
|
|
119
|
+
stats.dailyUsage[dateKey] = (stats.dailyUsage[dateKey] || 0) + 1;
|
|
120
|
+
|
|
121
|
+
// Add to timeline
|
|
122
|
+
agentTimeline.push({
|
|
123
|
+
timestamp: timestamp,
|
|
124
|
+
agentType: agentType,
|
|
125
|
+
agentName: stats.name,
|
|
126
|
+
prompt: prompt.substring(0, 100) + (prompt.length > 100 ? '...' : ''),
|
|
127
|
+
conversationId: conversation.id,
|
|
128
|
+
color: stats.color,
|
|
129
|
+
icon: stats.icon
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
totalAgentInvocations++;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Convert Sets to counts and finalize stats
|
|
140
|
+
Object.keys(agentStats).forEach(agentType => {
|
|
141
|
+
const stats = agentStats[agentType];
|
|
142
|
+
stats.uniqueConversations = stats.uniqueConversations.size;
|
|
143
|
+
stats.averageUsagePerConversation = stats.uniqueConversations > 0 ?
|
|
144
|
+
(stats.totalInvocations / stats.uniqueConversations).toFixed(1) : 0;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Sort timeline by timestamp
|
|
148
|
+
agentTimeline.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
149
|
+
|
|
150
|
+
// Calculate agent workflows (sequences of agent usage)
|
|
151
|
+
const workflowPatterns = this.analyzeAgentWorkflows(agentTimeline);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
totalAgentInvocations,
|
|
155
|
+
totalAgentTypes: Object.keys(agentStats).length,
|
|
156
|
+
agentStats: Object.values(agentStats).sort((a, b) => b.totalInvocations - a.totalInvocations),
|
|
157
|
+
agentTimeline,
|
|
158
|
+
workflowPatterns,
|
|
159
|
+
popularHours: this.calculatePopularHours(agentStats),
|
|
160
|
+
usageByDay: this.calculateDailyUsage(agentStats),
|
|
161
|
+
efficiency: this.calculateAgentEfficiency(agentStats)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Analyze agent workflow patterns
|
|
167
|
+
* @param {Array} timeline - Chronological agent invocations
|
|
168
|
+
* @returns {Object} Workflow patterns
|
|
169
|
+
*/
|
|
170
|
+
analyzeAgentWorkflows(timeline) {
|
|
171
|
+
const workflows = {};
|
|
172
|
+
const SESSION_GAP_MINUTES = 30; // Minutes between workflow sessions
|
|
173
|
+
|
|
174
|
+
let currentWorkflow = [];
|
|
175
|
+
let lastTimestamp = null;
|
|
176
|
+
|
|
177
|
+
timeline.forEach(event => {
|
|
178
|
+
const currentTime = new Date(event.timestamp);
|
|
179
|
+
|
|
180
|
+
// Start new workflow if gap is too large or first event
|
|
181
|
+
if (!lastTimestamp ||
|
|
182
|
+
(currentTime - lastTimestamp) > (SESSION_GAP_MINUTES * 60 * 1000)) {
|
|
183
|
+
|
|
184
|
+
// Save previous workflow if it had multiple agents
|
|
185
|
+
if (currentWorkflow.length > 1) {
|
|
186
|
+
const workflowKey = currentWorkflow.map(e => e.agentType).join(' → ');
|
|
187
|
+
workflows[workflowKey] = (workflows[workflowKey] || 0) + 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
currentWorkflow = [event];
|
|
191
|
+
} else {
|
|
192
|
+
currentWorkflow.push(event);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
lastTimestamp = currentTime;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Don't forget the last workflow
|
|
199
|
+
if (currentWorkflow.length > 1) {
|
|
200
|
+
const workflowKey = currentWorkflow.map(e => e.agentType).join(' → ');
|
|
201
|
+
workflows[workflowKey] = (workflows[workflowKey] || 0) + 1;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Convert to sorted array
|
|
205
|
+
return Object.entries(workflows)
|
|
206
|
+
.map(([pattern, count]) => ({ pattern, count }))
|
|
207
|
+
.sort((a, b) => b.count - a.count)
|
|
208
|
+
.slice(0, 10); // Top 10 workflows
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Calculate popular usage hours across all agents
|
|
213
|
+
* @param {Object} agentStats - Agent statistics
|
|
214
|
+
* @returns {Array} Hour usage data
|
|
215
|
+
*/
|
|
216
|
+
calculatePopularHours(agentStats) {
|
|
217
|
+
const hourlyTotals = new Array(24).fill(0);
|
|
218
|
+
|
|
219
|
+
Object.values(agentStats).forEach(stats => {
|
|
220
|
+
stats.hourlyDistribution.forEach((count, hour) => {
|
|
221
|
+
hourlyTotals[hour] += count;
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return hourlyTotals.map((count, hour) => ({
|
|
226
|
+
hour,
|
|
227
|
+
count,
|
|
228
|
+
label: `${hour.toString().padStart(2, '0')}:00`
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Calculate daily usage across all agents
|
|
234
|
+
* @param {Object} agentStats - Agent statistics
|
|
235
|
+
* @returns {Array} Daily usage data
|
|
236
|
+
*/
|
|
237
|
+
calculateDailyUsage(agentStats) {
|
|
238
|
+
const dailyTotals = {};
|
|
239
|
+
|
|
240
|
+
Object.values(agentStats).forEach(stats => {
|
|
241
|
+
Object.entries(stats.dailyUsage).forEach(([date, count]) => {
|
|
242
|
+
dailyTotals[date] = (dailyTotals[date] || 0) + count;
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return Object.entries(dailyTotals)
|
|
247
|
+
.map(([date, count]) => ({
|
|
248
|
+
date,
|
|
249
|
+
count,
|
|
250
|
+
timestamp: new Date(date)
|
|
251
|
+
}))
|
|
252
|
+
.sort((a, b) => a.timestamp - b.timestamp);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Calculate agent efficiency metrics
|
|
257
|
+
* @param {Object} agentStats - Agent statistics
|
|
258
|
+
* @returns {Object} Efficiency metrics
|
|
259
|
+
*/
|
|
260
|
+
calculateAgentEfficiency(agentStats) {
|
|
261
|
+
const agents = Object.values(agentStats);
|
|
262
|
+
if (agents.length === 0) return {};
|
|
263
|
+
|
|
264
|
+
const totalInvocations = agents.reduce((sum, agent) => sum + agent.totalInvocations, 0);
|
|
265
|
+
const totalConversations = agents.reduce((sum, agent) => sum + agent.uniqueConversations, 0);
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
averageInvocationsPerAgent: (totalInvocations / agents.length).toFixed(1),
|
|
269
|
+
averageConversationsPerAgent: (totalConversations / agents.length).toFixed(1),
|
|
270
|
+
mostUsedAgent: agents[0],
|
|
271
|
+
agentDiversity: agents.length,
|
|
272
|
+
adoptionRate: (agents.filter(a => a.totalInvocations > 1).length / agents.length * 100).toFixed(1)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Parse JSONL file to extract messages
|
|
278
|
+
* @param {string} filePath - Path to the JSONL file
|
|
279
|
+
* @returns {Array} Array of parsed messages
|
|
280
|
+
*/
|
|
281
|
+
async parseJsonlFile(filePath) {
|
|
282
|
+
try {
|
|
283
|
+
if (!await fs.pathExists(filePath)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
288
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
289
|
+
|
|
290
|
+
return lines.map(line => {
|
|
291
|
+
try {
|
|
292
|
+
return JSON.parse(line);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.warn(`Error parsing JSONL line in ${filePath}:`, error.message);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}).filter(Boolean);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error(`Error reading JSONL file ${filePath}:`, error);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Check if timestamp is within date range
|
|
306
|
+
* @param {string|Date} timestamp - Message timestamp
|
|
307
|
+
* @param {Object} dateRange - Date range with startDate and endDate
|
|
308
|
+
* @returns {boolean} Whether timestamp is in range
|
|
309
|
+
*/
|
|
310
|
+
isWithinDateRange(timestamp, dateRange) {
|
|
311
|
+
if (!dateRange || (!dateRange.startDate && !dateRange.endDate)) return true;
|
|
312
|
+
|
|
313
|
+
const messageDate = new Date(timestamp);
|
|
314
|
+
const startDate = dateRange.startDate ? new Date(dateRange.startDate) : new Date(0);
|
|
315
|
+
const endDate = dateRange.endDate ? new Date(dateRange.endDate) : new Date();
|
|
316
|
+
|
|
317
|
+
return messageDate >= startDate && messageDate <= endDate;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Generate agent usage summary for display
|
|
322
|
+
* @param {Object} analysisResult - Result from analyzeAgentUsage
|
|
323
|
+
* @returns {Object} Summary data
|
|
324
|
+
*/
|
|
325
|
+
generateSummary(analysisResult) {
|
|
326
|
+
const { totalAgentInvocations, totalAgentTypes, agentStats, efficiency } = analysisResult;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
totalInvocations: totalAgentInvocations,
|
|
330
|
+
totalAgentTypes,
|
|
331
|
+
topAgent: agentStats[0] || null,
|
|
332
|
+
averageUsage: efficiency.averageInvocationsPerAgent,
|
|
333
|
+
adoptionRate: efficiency.adoptionRate,
|
|
334
|
+
summary: totalAgentInvocations > 0 ?
|
|
335
|
+
`${totalAgentInvocations} agent invocations across ${totalAgentTypes} different agents` :
|
|
336
|
+
'No agent usage detected'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = AgentAnalyzer;
|
|
@@ -6,34 +6,40 @@ const chalk = require('chalk');
|
|
|
6
6
|
|
|
7
7
|
class SessionAnalyzer {
|
|
8
8
|
constructor() {
|
|
9
|
-
|
|
9
|
+
// CORRECTED: Sessions don't have fixed duration - they reset at scheduled times
|
|
10
|
+
// Reset hours: 1am, 7am, 1pm, 7pm local time
|
|
11
|
+
this.RESET_HOURS = [1, 7, 13, 19];
|
|
10
12
|
this.MONTHLY_SESSION_LIMIT = 50;
|
|
11
13
|
|
|
12
|
-
// Plan-specific
|
|
14
|
+
// Plan-specific usage information (Claude uses complexity-based limits, not fixed message counts)
|
|
13
15
|
this.PLAN_LIMITS = {
|
|
14
16
|
'free': {
|
|
15
17
|
name: 'Free Plan',
|
|
16
|
-
|
|
18
|
+
estimatedMessagesPerSession: null,
|
|
17
19
|
monthlyPrice: 0,
|
|
18
|
-
hasSessionLimits: false
|
|
20
|
+
hasSessionLimits: false,
|
|
21
|
+
description: 'Daily usage limits apply'
|
|
19
22
|
},
|
|
20
23
|
'standard': {
|
|
21
24
|
name: 'Pro Plan',
|
|
22
|
-
|
|
25
|
+
estimatedMessagesPerSession: 45, // Rough estimate for ~200 sentence messages
|
|
23
26
|
monthlyPrice: 20,
|
|
24
|
-
hasSessionLimits: true
|
|
27
|
+
hasSessionLimits: true,
|
|
28
|
+
description: 'Usage based on message complexity, conversation length, and current capacity. Limits reset every 5 hours.'
|
|
25
29
|
},
|
|
26
30
|
'max': {
|
|
27
31
|
name: 'Max Plan (5x)',
|
|
28
|
-
|
|
32
|
+
estimatedMessagesPerSession: null, // 5x more than Pro, but still complexity-based
|
|
29
33
|
monthlyPrice: 100,
|
|
30
|
-
hasSessionLimits: true
|
|
34
|
+
hasSessionLimits: true,
|
|
35
|
+
description: '5x the usage of Pro plan. Complexity-based limits.'
|
|
31
36
|
},
|
|
32
37
|
'premium': {
|
|
33
|
-
name: 'Max Plan (
|
|
34
|
-
|
|
38
|
+
name: 'Max Plan (20x)',
|
|
39
|
+
estimatedMessagesPerSession: null, // 20x more than Pro
|
|
35
40
|
monthlyPrice: 200,
|
|
36
|
-
hasSessionLimits: true
|
|
41
|
+
hasSessionLimits: true,
|
|
42
|
+
description: '20x the usage of Pro plan. Complexity-based limits.'
|
|
37
43
|
}
|
|
38
44
|
};
|
|
39
45
|
}
|
|
@@ -325,7 +331,7 @@ class SessionAnalyzer {
|
|
|
325
331
|
|
|
326
332
|
// Create current session based on Claude's actual session window
|
|
327
333
|
const sessionStartTime = new Date(claudeSessionInfo.startTime);
|
|
328
|
-
const sessionEndTime = new Date(claudeSessionInfo.
|
|
334
|
+
const sessionEndTime = new Date(claudeSessionInfo.sessionLimit.nextResetTime);
|
|
329
335
|
const now = new Date();
|
|
330
336
|
|
|
331
337
|
// Find the first user message that occurred AT OR AFTER the Claude session started
|
|
@@ -346,8 +352,50 @@ class SessionAnalyzer {
|
|
|
346
352
|
return msgTime >= effectiveSessionStart && msgTime < sessionEndTime;
|
|
347
353
|
});
|
|
348
354
|
|
|
355
|
+
// If no estimated messages found in session window, check for active conversations by lastModified
|
|
349
356
|
if (currentSessionMessages.length === 0) {
|
|
350
|
-
|
|
357
|
+
const RECENT_ACTIVITY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
|
358
|
+
const now = new Date();
|
|
359
|
+
|
|
360
|
+
// Find conversations with recent activity (lastModified within session timeframe)
|
|
361
|
+
const activeConversations = conversations.filter(conversation => {
|
|
362
|
+
if (!conversation.lastModified) return false;
|
|
363
|
+
|
|
364
|
+
const lastModified = new Date(conversation.lastModified);
|
|
365
|
+
const timeSinceModified = now - lastModified;
|
|
366
|
+
|
|
367
|
+
// Consider conversation active if:
|
|
368
|
+
// 1. Modified after session start, AND
|
|
369
|
+
// 2. Recently modified (within threshold)
|
|
370
|
+
return lastModified >= sessionStartTime && timeSinceModified < RECENT_ACTIVITY_THRESHOLD;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (activeConversations.length === 0) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Create messages for active conversations based on their real message count
|
|
378
|
+
activeConversations.forEach(conversation => {
|
|
379
|
+
const lastModified = new Date(conversation.lastModified);
|
|
380
|
+
const messageCount = conversation.messageCount || 0;
|
|
381
|
+
|
|
382
|
+
// Create messages based on real message count, distributing them over the session
|
|
383
|
+
const sessionDuration = now - sessionStartTime;
|
|
384
|
+
const timePerMessage = sessionDuration / messageCount;
|
|
385
|
+
|
|
386
|
+
for (let i = 0; i < messageCount; i++) {
|
|
387
|
+
// Distribute messages over the session timeline, alternating user/assistant
|
|
388
|
+
const messageTime = new Date(sessionStartTime.getTime() + (i * timePerMessage));
|
|
389
|
+
const role = i % 2 === 0 ? 'user' : 'assistant';
|
|
390
|
+
|
|
391
|
+
currentSessionMessages.push({
|
|
392
|
+
timestamp: messageTime,
|
|
393
|
+
role: role,
|
|
394
|
+
conversationId: conversation.id,
|
|
395
|
+
usage: conversation.tokenUsage || null
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
351
399
|
}
|
|
352
400
|
|
|
353
401
|
// Create the current session object
|
|
@@ -409,12 +457,19 @@ class SessionAnalyzer {
|
|
|
409
457
|
getCurrentActiveSessionFromClaudeInfo(sessions, claudeSessionInfo) {
|
|
410
458
|
if (sessions.length === 0) return null;
|
|
411
459
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
const RECENT_ACTIVITY_THRESHOLD = 5 * 60 * 1000; // 5 minutes
|
|
462
|
+
|
|
463
|
+
// Check if there's recent activity - sessions can be renewed at reset time
|
|
464
|
+
const timeSinceLastUpdate = now - claudeSessionInfo.lastUpdate;
|
|
465
|
+
const hasRecentActivity = timeSinceLastUpdate < RECENT_ACTIVITY_THRESHOLD;
|
|
466
|
+
|
|
467
|
+
// Session is active if not expired OR has recent activity (session was renewed)
|
|
468
|
+
if (!claudeSessionInfo.estimatedTimeRemaining.isExpired || hasRecentActivity) {
|
|
469
|
+
return sessions[0];
|
|
415
470
|
}
|
|
416
471
|
|
|
417
|
-
return
|
|
472
|
+
return null;
|
|
418
473
|
}
|
|
419
474
|
|
|
420
475
|
/**
|
|
@@ -516,38 +571,33 @@ class SessionAnalyzer {
|
|
|
516
571
|
const warnings = [];
|
|
517
572
|
const planLimits = this.PLAN_LIMITS[userPlan.planType] || this.PLAN_LIMITS['standard'];
|
|
518
573
|
|
|
519
|
-
// Session-level warnings
|
|
574
|
+
// Session-level warnings - only for time remaining and token usage
|
|
520
575
|
if (currentSession) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (sessionProgress >= 0.9) {
|
|
524
|
-
warnings.push({
|
|
525
|
-
type: 'session_limit_critical',
|
|
526
|
-
level: 'error',
|
|
527
|
-
message: `You're near your session message limit (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
|
|
528
|
-
timeRemaining: currentSession.timeRemaining
|
|
529
|
-
});
|
|
530
|
-
} else if (sessionProgress >= 0.75) {
|
|
576
|
+
// Time remaining warning (30 minutes before reset)
|
|
577
|
+
if (currentSession.timeRemaining < 30 * 60 * 1000) { // 30 minutes
|
|
531
578
|
warnings.push({
|
|
532
|
-
type: '
|
|
533
|
-
level: '
|
|
534
|
-
message: `
|
|
579
|
+
type: 'session_time_warning',
|
|
580
|
+
level: 'info',
|
|
581
|
+
message: `Session resets in ${Math.round(currentSession.timeRemaining / 60000)} minutes`,
|
|
535
582
|
timeRemaining: currentSession.timeRemaining
|
|
536
583
|
});
|
|
537
584
|
}
|
|
538
585
|
|
|
539
|
-
//
|
|
540
|
-
if (currentSession.
|
|
586
|
+
// High token usage warning (if we have token data and it's exceptionally high)
|
|
587
|
+
if (currentSession.tokenUsage && currentSession.tokenUsage.total > 1000000) { // 1M tokens
|
|
541
588
|
warnings.push({
|
|
542
|
-
type: '
|
|
589
|
+
type: 'high_token_usage',
|
|
543
590
|
level: 'info',
|
|
544
|
-
message: `
|
|
545
|
-
|
|
591
|
+
message: `High token usage in this session (${Math.round(currentSession.tokenUsage.total / 1000)}K tokens)`,
|
|
592
|
+
tokenUsage: currentSession.tokenUsage.total
|
|
546
593
|
});
|
|
547
594
|
}
|
|
595
|
+
|
|
596
|
+
// Note: We don't warn about message counts since Claude uses complexity-based limits
|
|
597
|
+
// that can't be accurately predicted from simple message counts
|
|
548
598
|
}
|
|
549
599
|
|
|
550
|
-
// Monthly warnings
|
|
600
|
+
// Monthly warnings (these limits are more predictable)
|
|
551
601
|
const monthlyProgress = monthlyUsage.sessionCount / this.MONTHLY_SESSION_LIMIT;
|
|
552
602
|
|
|
553
603
|
if (monthlyProgress >= 0.9) {
|
|
@@ -605,25 +655,30 @@ class SessionAnalyzer {
|
|
|
605
655
|
// Ensure limits exist, fallback to standard plan
|
|
606
656
|
const planLimits = limits || this.PLAN_LIMITS['standard'];
|
|
607
657
|
|
|
608
|
-
//
|
|
609
|
-
const
|
|
658
|
+
// Calculate only user messages (Claude only counts prompts toward limits)
|
|
659
|
+
const userMessages = currentSession.messages ? currentSession.messages.filter(msg => msg.role === 'user') : [];
|
|
660
|
+
const userMessageCount = userMessages.length;
|
|
610
661
|
|
|
611
662
|
return {
|
|
612
663
|
hasActiveSession: true,
|
|
613
664
|
timeRemaining: currentSession.timeRemaining,
|
|
614
665
|
timeRemainingFormatted: this.formatTimeRemaining(currentSession.timeRemaining),
|
|
615
|
-
messagesUsed:
|
|
616
|
-
|
|
617
|
-
messageWeight: currentSession.messageWeight,
|
|
618
|
-
usageDetails: currentSession.usageDetails,
|
|
666
|
+
messagesUsed: userMessageCount,
|
|
667
|
+
messagesEstimate: planLimits.estimatedMessagesPerSession, // Show as estimate, not limit
|
|
619
668
|
tokensUsed: currentSession.tokenUsage.total,
|
|
620
|
-
sessionProgress: weightedProgress,
|
|
621
|
-
sessionProgressSimple: (currentSession.messageCount / planLimits.messagesPerSession) * 100,
|
|
622
669
|
planName: planLimits.name,
|
|
670
|
+
planDescription: planLimits.description,
|
|
623
671
|
monthlySessionsUsed: monthlyUsage.sessionCount,
|
|
624
672
|
monthlySessionsLimit: this.MONTHLY_SESSION_LIMIT,
|
|
625
673
|
warnings: warnings.filter(w => w.type.includes('session')),
|
|
626
|
-
willResetAt: currentSession.endTime
|
|
674
|
+
willResetAt: currentSession.endTime,
|
|
675
|
+
// Usage insights
|
|
676
|
+
usageInsights: {
|
|
677
|
+
tokensPerMessage: userMessageCount > 0 ? Math.round(currentSession.tokenUsage.total / userMessageCount) : 0,
|
|
678
|
+
averageMessageComplexity: userMessageCount > 0 ? currentSession.messageWeight / userMessageCount : 0,
|
|
679
|
+
conversationLength: currentSession.messages ? currentSession.messages.length : 0,
|
|
680
|
+
sessionDuration: Date.now() - currentSession.startTime
|
|
681
|
+
}
|
|
627
682
|
};
|
|
628
683
|
}
|
|
629
684
|
}
|