claude-session-insights 0.3.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/README.md +153 -0
- package/bin/cli.js +49 -0
- package/package.json +30 -0
- package/public/index.html +1560 -0
- package/src/ai-analyze.js +277 -0
- package/src/export.js +79 -0
- package/src/parser.js +273 -0
- package/src/scorer.js +464 -0
- package/src/server.js +229 -0
- package/src/summarizer.js +253 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// Rule-based summary generation for overall and per-session insights.
|
|
2
|
+
|
|
3
|
+
function pct(n) {
|
|
4
|
+
return Math.round(n * 100);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function plural(n, word) {
|
|
8
|
+
return `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// --- Overall Summary ---
|
|
12
|
+
|
|
13
|
+
export function generateOverallSummary(scoredData) {
|
|
14
|
+
const { sessions, overallScore, badges, tips } = scoredData;
|
|
15
|
+
if (sessions.length === 0) {
|
|
16
|
+
return { paragraphs: ["No sessions found yet. Start using Claude Code and come back!"], patterns: [], recommendations: [] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const patterns = [];
|
|
20
|
+
const recommendations = [];
|
|
21
|
+
|
|
22
|
+
// 1. Session length habits
|
|
23
|
+
const sessionsWithInflection = sessions.filter(s => {
|
|
24
|
+
const tip = s.tips?.find(t => t.type === "cost-inflection");
|
|
25
|
+
return !!tip;
|
|
26
|
+
});
|
|
27
|
+
const inflectionRate = sessionsWithInflection.length / sessions.length;
|
|
28
|
+
|
|
29
|
+
const avgTurns = sessions.reduce((sum, s) => sum + s.totals.userMessages, 0) / sessions.length;
|
|
30
|
+
const longSessions = sessions.filter(s => s.totals.userMessages > 20);
|
|
31
|
+
const longRate = longSessions.length / sessions.length;
|
|
32
|
+
|
|
33
|
+
if (inflectionRate > 0.3) {
|
|
34
|
+
patterns.push({
|
|
35
|
+
type: "session-length",
|
|
36
|
+
sentiment: "negative",
|
|
37
|
+
text: `${pct(inflectionRate)}% of your sessions hit a cost inflection point where per-turn cost doubles — typically from context growth.`,
|
|
38
|
+
});
|
|
39
|
+
const clearUsers = sessions.filter(s => s.clearPoints.length > 0).length;
|
|
40
|
+
if (clearUsers / sessions.length < 0.3) {
|
|
41
|
+
recommendations.push("Use /clear when switching subtasks within a long session. This resets context and cuts per-turn cost back down.");
|
|
42
|
+
} else {
|
|
43
|
+
recommendations.push("You already use /clear sometimes — try to use it right when you notice you're switching to a different subtask.");
|
|
44
|
+
}
|
|
45
|
+
} else if (longRate > 0.3) {
|
|
46
|
+
patterns.push({
|
|
47
|
+
type: "session-length",
|
|
48
|
+
sentiment: "neutral",
|
|
49
|
+
text: `${pct(longRate)}% of your sessions run 20+ turns, but cost stays controlled. You manage long sessions well.`,
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
patterns.push({
|
|
53
|
+
type: "session-length",
|
|
54
|
+
sentiment: "positive",
|
|
55
|
+
text: `Your sessions are focused — averaging ${Math.round(avgTurns)} turns. Short, targeted sessions are cost-efficient.`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. Prompt style
|
|
60
|
+
const avgToolRatio = sessions.reduce((sum, s) => {
|
|
61
|
+
if (s.totals.userMessages === 0) return sum;
|
|
62
|
+
return sum + s.totals.toolCalls / s.totals.userMessages;
|
|
63
|
+
}, 0) / sessions.filter(s => s.totals.userMessages > 0).length;
|
|
64
|
+
|
|
65
|
+
const highToolSessions = sessions.filter(s =>
|
|
66
|
+
s.totals.userMessages > 0 && s.totals.toolCalls / s.totals.userMessages > 5
|
|
67
|
+
);
|
|
68
|
+
const highToolRate = highToolSessions.length / sessions.length;
|
|
69
|
+
|
|
70
|
+
if (highToolRate > 0.3) {
|
|
71
|
+
patterns.push({
|
|
72
|
+
type: "prompt-style",
|
|
73
|
+
sentiment: "negative",
|
|
74
|
+
text: `${pct(highToolRate)}% of sessions have a high tool-call ratio (>5x). This usually means prompts lack specificity, causing Claude to search/read extensively.`,
|
|
75
|
+
});
|
|
76
|
+
recommendations.push("Include file paths, function names, or line numbers in your prompts. Instead of \"fix the bug\", try \"fix the null check in src/parser.js:45\".");
|
|
77
|
+
} else if (avgToolRatio < 2) {
|
|
78
|
+
patterns.push({
|
|
79
|
+
type: "prompt-style",
|
|
80
|
+
sentiment: "positive",
|
|
81
|
+
text: `Your prompts are surgical — averaging ${avgToolRatio.toFixed(1)} tool calls per message. You give Claude enough context to act without excessive searching.`,
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
patterns.push({
|
|
85
|
+
type: "prompt-style",
|
|
86
|
+
sentiment: "neutral",
|
|
87
|
+
text: `Your tool-call ratio averages ${avgToolRatio.toFixed(1)}x per message — reasonable, with room to be more specific in some sessions.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Cache efficiency
|
|
92
|
+
const avgCache = sessions.reduce((sum, s) => sum + s.totals.cacheHitRate, 0) / sessions.length;
|
|
93
|
+
if (avgCache > 0.75) {
|
|
94
|
+
patterns.push({
|
|
95
|
+
type: "cache-usage",
|
|
96
|
+
sentiment: "positive",
|
|
97
|
+
text: `Strong cache hit rate at ${pct(avgCache)}%. You're reusing context effectively, which keeps input costs low.`,
|
|
98
|
+
});
|
|
99
|
+
} else if (avgCache > 0.5) {
|
|
100
|
+
patterns.push({
|
|
101
|
+
type: "cache-usage",
|
|
102
|
+
sentiment: "neutral",
|
|
103
|
+
text: `Cache hit rate is ${pct(avgCache)}% — decent but could improve. Grouping related tasks in one session helps warm the cache.`,
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
patterns.push({
|
|
107
|
+
type: "cache-usage",
|
|
108
|
+
sentiment: "negative",
|
|
109
|
+
text: `Low cache hit rate at ${pct(avgCache)}%. Many tokens are being re-sent without cache reuse.`,
|
|
110
|
+
});
|
|
111
|
+
recommendations.push("Group related tasks in the same session so the cache stays warm. Avoid starting new sessions for quick follow-ups.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 4. Model usage
|
|
115
|
+
const opusSessions = sessions.filter(s => (s.model || "").includes("opus"));
|
|
116
|
+
// Use suggestedModel (same as the table column) rather than the model-mismatch tip,
|
|
117
|
+
// which only fires for short/small sessions and undercounts real mismatches.
|
|
118
|
+
const opusMismatches = sessions.filter(s => s.suggestedModel);
|
|
119
|
+
if (opusMismatches.length > 2) {
|
|
120
|
+
patterns.push({
|
|
121
|
+
type: "model-usage",
|
|
122
|
+
sentiment: "negative",
|
|
123
|
+
text: `${plural(opusMismatches.length, "session")} used Opus for simple tasks that Sonnet could handle at 5x lower cost.`,
|
|
124
|
+
});
|
|
125
|
+
recommendations.push("Use Sonnet for quick edits, lookups, and simple bug fixes. Reserve Opus for complex refactors, architecture decisions, and multi-file changes.");
|
|
126
|
+
} else if (opusSessions.length > 0) {
|
|
127
|
+
patterns.push({
|
|
128
|
+
type: "model-usage",
|
|
129
|
+
sentiment: "positive",
|
|
130
|
+
text: `You use Opus selectively (${plural(opusSessions.length, "session")}) — good model-cost awareness.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 5. Cost distribution
|
|
135
|
+
const totalCost = sessions.reduce((sum, s) => sum + s.totals.estimatedCost, 0);
|
|
136
|
+
const topSession = [...sessions].sort((a, b) => b.totals.estimatedCost - a.totals.estimatedCost)[0];
|
|
137
|
+
if (topSession && totalCost > 0) {
|
|
138
|
+
const topPct = (topSession.totals.estimatedCost / totalCost) * 100;
|
|
139
|
+
if (topPct > 30) {
|
|
140
|
+
patterns.push({
|
|
141
|
+
type: "cost-distribution",
|
|
142
|
+
sentiment: "negative",
|
|
143
|
+
text: `Your most expensive session accounts for ${Math.round(topPct)}% of total cost ($${topSession.totals.estimatedCost.toFixed(2)}). A few heavy sessions dominate your spend.`,
|
|
144
|
+
});
|
|
145
|
+
recommendations.push("For your most expensive sessions, check if you could have broken the task into smaller, focused sessions or used /clear mid-session.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Build paragraphs
|
|
150
|
+
const paragraphs = [];
|
|
151
|
+
|
|
152
|
+
// Opening line based on score
|
|
153
|
+
if (overallScore >= 85) {
|
|
154
|
+
paragraphs.push(`You're using Claude Code efficiently (score: ${overallScore}/100). Here's what stands out across your ${plural(sessions.length, "session")}:`);
|
|
155
|
+
} else if (overallScore >= 65) {
|
|
156
|
+
paragraphs.push(`Your efficiency score is ${overallScore}/100 across ${plural(sessions.length, "session")}. There are some clear opportunities to get more value from Claude Code:`);
|
|
157
|
+
} else {
|
|
158
|
+
paragraphs.push(`Your efficiency score is ${overallScore}/100 across ${plural(sessions.length, "session")}. A few habit changes could significantly reduce your costs and improve results:`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Pattern sentences grouped by sentiment
|
|
162
|
+
const negativePatterns = patterns.filter(p => p.sentiment === "negative");
|
|
163
|
+
const positivePatterns = patterns.filter(p => p.sentiment === "positive");
|
|
164
|
+
const neutralPatterns = patterns.filter(p => p.sentiment === "neutral");
|
|
165
|
+
|
|
166
|
+
if (positivePatterns.length > 0) {
|
|
167
|
+
paragraphs.push(positivePatterns.map(p => p.text).join(" "));
|
|
168
|
+
}
|
|
169
|
+
if (negativePatterns.length > 0 || neutralPatterns.length > 0) {
|
|
170
|
+
paragraphs.push([...negativePatterns, ...neutralPatterns].map(p => p.text).join(" "));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { paragraphs, patterns, recommendations };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Per-Session Summary ---
|
|
177
|
+
|
|
178
|
+
export function generateSessionSummary(session) {
|
|
179
|
+
const { totals, turns, clearPoints, tips, score, dimensions, model } = session;
|
|
180
|
+
const sentences = [];
|
|
181
|
+
|
|
182
|
+
// Session type classification
|
|
183
|
+
const duration = session.startTime && session.endTime
|
|
184
|
+
? (new Date(session.endTime) - new Date(session.startTime)) / 60000
|
|
185
|
+
: 0;
|
|
186
|
+
|
|
187
|
+
if (totals.userMessages <= 3) {
|
|
188
|
+
sentences.push(`A quick ${plural(totals.userMessages, "message")} session.`);
|
|
189
|
+
} else if (totals.userMessages <= 10) {
|
|
190
|
+
sentences.push(`A focused ${plural(totals.userMessages, "message")} session${duration > 0 ? ` over ${Math.round(duration)} minutes` : ""}.`);
|
|
191
|
+
} else {
|
|
192
|
+
sentences.push(`A longer ${plural(totals.userMessages, "message")} session${duration > 0 ? ` spanning ${Math.round(duration)} minutes` : ""}.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Cost analysis
|
|
196
|
+
const costInflection = tips?.find(t => t.type === "cost-inflection");
|
|
197
|
+
if (costInflection) {
|
|
198
|
+
const match = costInflection.message.match(/turn (\d+)/);
|
|
199
|
+
const turnNum = match ? match[1] : "?";
|
|
200
|
+
sentences.push(`Cost per turn doubled around turn ${turnNum} — likely from growing context without clearing.`);
|
|
201
|
+
} else if (totals.userMessages > 5) {
|
|
202
|
+
sentences.push("Cost stayed stable throughout — no runaway context growth.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Tool efficiency
|
|
206
|
+
if (totals.userMessages > 0) {
|
|
207
|
+
const ratio = totals.toolCalls / totals.userMessages;
|
|
208
|
+
if (ratio > 5) {
|
|
209
|
+
// Find the most expensive user prompt that triggered lots of tools
|
|
210
|
+
const userTurns = turns.filter(t => t.role === "user");
|
|
211
|
+
const expensivePrompts = userTurns.filter(t => {
|
|
212
|
+
const idx = turns.indexOf(t);
|
|
213
|
+
const nextAssistant = turns.slice(idx + 1).find(a => a.role === "assistant");
|
|
214
|
+
return nextAssistant && nextAssistant.toolCalls && nextAssistant.toolCalls.length > 5;
|
|
215
|
+
});
|
|
216
|
+
if (expensivePrompts.length > 0) {
|
|
217
|
+
sentences.push(`${plural(expensivePrompts.length, "prompt")} triggered heavy tool usage (5+ calls each) — more specificity would help.`);
|
|
218
|
+
} else {
|
|
219
|
+
sentences.push(`High tool-call ratio (${ratio.toFixed(1)}x) — prompts could be more targeted.`);
|
|
220
|
+
}
|
|
221
|
+
} else if (ratio < 2) {
|
|
222
|
+
sentences.push("Prompts were well-targeted with minimal tool searching.");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Cache
|
|
227
|
+
if (totals.totalTokens > 50000) {
|
|
228
|
+
if (totals.cacheHitRate > 0.75) {
|
|
229
|
+
sentences.push(`Excellent cache reuse at ${pct(totals.cacheHitRate)}%.`);
|
|
230
|
+
} else if (totals.cacheHitRate < 0.2) {
|
|
231
|
+
sentences.push(`Low cache hit rate (${pct(totals.cacheHitRate)}%) — most tokens were sent fresh.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Model note
|
|
236
|
+
const modelMismatch = tips?.find(t => t.type === "model-mismatch");
|
|
237
|
+
if (modelMismatch) {
|
|
238
|
+
sentences.push(`Opus was used for a simple task — Sonnet would have been 5x cheaper here.`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// What went well
|
|
242
|
+
const strengths = [];
|
|
243
|
+
if (dimensions.toolRatio >= 80) strengths.push("specific prompts");
|
|
244
|
+
if (dimensions.cacheHitRate >= 80) strengths.push("good cache reuse");
|
|
245
|
+
if (dimensions.contextManagement >= 80) strengths.push("clean context management");
|
|
246
|
+
if (dimensions.promptSpecificity >= 80) strengths.push("detailed prompts");
|
|
247
|
+
|
|
248
|
+
if (strengths.length > 0) {
|
|
249
|
+
sentences.push(`Strengths: ${strengths.join(", ")}.`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return sentences.join(" ");
|
|
253
|
+
}
|