@statforge/claudestat 1.1.1 → 1.2.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.
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getWeeklyInsightData = getWeeklyInsightData;
7
+ exports.generateTip = generateTip;
8
+ exports.getUsageInsights = getUsageInsights;
9
+ exports.renderInsights = renderInsights;
10
+ exports.shouldShowInsight = shouldShowInsight;
11
+ exports.markInsightShown = markInsightShown;
12
+ exports.renderWeeklyInsight = renderWeeklyInsight;
13
+ const path_1 = __importDefault(require("path"));
14
+ const db_1 = require("./db");
15
+ const WEEK_MS = 7 * 86400000;
16
+ const META_KEY = 'last_insight_at';
17
+ function getWeeklyInsightData(days = 7) {
18
+ const agg = db_1.dbOps.getWeeklyInsight(days);
19
+ const topTools = db_1.dbOps.getTopTools(days, 'cost', 1);
20
+ const topTool = topTools[0];
21
+ const topToolName = topTool?.tool_name ?? 'Unknown';
22
+ const topToolPct = agg.total_cost > 0
23
+ ? Math.round((topTool?.total_cost_usd ?? 0) / agg.total_cost * 100)
24
+ : 0;
25
+ const totalInputWithCache = agg.input_tokens + agg.cache_read;
26
+ const cacheHitPct = totalInputWithCache > 0
27
+ ? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
28
+ : 0;
29
+ return {
30
+ total_sessions: agg.total_sessions,
31
+ total_cost: agg.total_cost,
32
+ input_tokens: agg.input_tokens,
33
+ output_tokens: agg.output_tokens,
34
+ cache_read: agg.cache_read,
35
+ cache_hit_pct: cacheHitPct,
36
+ total_loops: agg.total_loops,
37
+ avg_efficiency: Math.round(agg.avg_efficiency),
38
+ top_tool: topToolName,
39
+ top_tool_cost_pct: topToolPct,
40
+ week_start: agg.week_start,
41
+ week_end: agg.week_end ?? agg.week_start,
42
+ };
43
+ }
44
+ function generateTip(d) {
45
+ const costPct = d.top_tool_cost_pct;
46
+ const tool = d.top_tool;
47
+ if (tool === 'Bash' && costPct >= 40) {
48
+ return 'Group bash commands to reduce tool calls — each call costs context';
49
+ }
50
+ if (d.total_loops >= 3) {
51
+ return `${d.total_loops} loops detected — consider using /compact earlier to prevent context thrashing`;
52
+ }
53
+ if (d.avg_efficiency < 60) {
54
+ return 'Low efficiency score — try smaller, focused tasks instead of long sessions';
55
+ }
56
+ if (d.total_sessions > 30) {
57
+ return `${d.total_sessions} sessions this week — consider batching related work into fewer sessions`;
58
+ }
59
+ if (d.cache_hit_pct < 10 && d.total_sessions > 5) {
60
+ return 'Low cache hit rate — repetitive context is costing you; use CLAUDE.md for common instructions';
61
+ }
62
+ if (d.total_cost > 20) {
63
+ return `$${d.total_cost.toFixed(0)} spent this week — enable quota alerts with "claudestat config --alerts true" to stay in control`;
64
+ }
65
+ return 'Enable quota alerts with "claudestat config --alerts true" to avoid surprise limits';
66
+ }
67
+ function getUsageInsights(days = 7) {
68
+ const agg = db_1.dbOps.getWeeklyInsight(days);
69
+ const aggTotal = db_1.dbOps.getWeeklyInsight(days * 2);
70
+ const prevSessions = aggTotal.total_sessions - agg.total_sessions;
71
+ const effDelta = prevSessions > 2
72
+ ? Math.round(agg.avg_efficiency -
73
+ (aggTotal.avg_efficiency * aggTotal.total_sessions - agg.avg_efficiency * agg.total_sessions) / prevSessions)
74
+ : -999;
75
+ const totalInputWithCache = agg.input_tokens + agg.cache_read;
76
+ const cacheHitPct = totalInputWithCache > 0
77
+ ? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
78
+ : 0;
79
+ const outputInputRatio = agg.input_tokens > 0
80
+ ? parseFloat((agg.output_tokens / agg.input_tokens).toFixed(1))
81
+ : 0;
82
+ const ratioLabel = outputInputRatio > 10 ? 'cache-heavy workload'
83
+ : outputInputRatio > 5 ? 'generation-heavy'
84
+ : outputInputRatio > 2 ? 'balanced'
85
+ : 'reading-heavy';
86
+ const CACHE_READ_PRICE = {
87
+ 'claude-haiku-4-5-20251001': 0.30 / 1000000,
88
+ 'claude-sonnet-4-6': 2.70 / 1000000,
89
+ 'claude-opus-4-6': 3.00 / 1000000,
90
+ };
91
+ const DEFAULT_CACHE_PRICE = 2.70 / 1000000;
92
+ const cacheByModel = db_1.dbOps.getCacheReadByModel(days);
93
+ const cacheSavings = cacheByModel.reduce((total, row) => {
94
+ const price = CACHE_READ_PRICE[row.model] ?? DEFAULT_CACHE_PRICE;
95
+ return total + row.cache_read * price;
96
+ }, 0);
97
+ return {
98
+ days,
99
+ total_sessions: agg.total_sessions,
100
+ total_cost: agg.total_cost,
101
+ avg_cost_per_session: agg.total_sessions > 0 ? agg.total_cost / agg.total_sessions : 0,
102
+ cache_savings_usd: cacheSavings,
103
+ cache_hit_pct: cacheHitPct,
104
+ output_input_ratio: outputInputRatio,
105
+ ratio_label: ratioLabel,
106
+ avg_efficiency: Math.round(agg.avg_efficiency),
107
+ efficiency_delta: effDelta,
108
+ total_loops: agg.total_loops,
109
+ project_costs: db_1.dbOps.getProjectCosts(days),
110
+ hour_ranges: (() => {
111
+ const hours = db_1.dbOps.getHourlyDistribution(days);
112
+ const dawn = hours.filter(h => h.hour >= 0 && h.hour <= 5).reduce((s, h) => s + h.session_count, 0);
113
+ const morn = hours.filter(h => h.hour >= 6 && h.hour <= 11).reduce((s, h) => s + h.session_count, 0);
114
+ const after = hours.filter(h => h.hour >= 12 && h.hour <= 17).reduce((s, h) => s + h.session_count, 0);
115
+ const night = hours.filter(h => h.hour >= 18 && h.hour <= 23).reduce((s, h) => s + h.session_count, 0);
116
+ return [
117
+ { emoji: '🌙', from: '00:00', to: '05:59', count: dawn },
118
+ { emoji: '🌅', from: '06:00', to: '11:59', count: morn },
119
+ { emoji: '☀️', from: '12:00', to: '17:59', count: after },
120
+ { emoji: '🌆', from: '18:00', to: '23:59', count: night },
121
+ ].filter(r => r.count > 0);
122
+ })(),
123
+ };
124
+ }
125
+ function renderInsights(d) {
126
+ const R = '\x1b[0m';
127
+ const B = '\x1b[1m';
128
+ const D = '\x1b[2m';
129
+ const G = '\x1b[32m';
130
+ const Y = '\x1b[33m';
131
+ const C = '\x1b[36m';
132
+ const M = '\x1b[35m';
133
+ const bar = (pct, width = 20) => '█'.repeat(Math.round(pct / 100 * width)) + '░'.repeat(width - Math.round(pct / 100 * width));
134
+ const fmtDollar = (n) => n < 0.01 ? '< $0.01' : `$${n.toFixed(2)}`;
135
+ const lines = [];
136
+ lines.push(`\n${B}💡 claudestat insights${R} ${D}last ${d.days} days${R}`);
137
+ lines.push('━'.repeat(44));
138
+ // Cost summary
139
+ lines.push(`\n 💰 ${B}${fmtDollar(d.avg_cost_per_session)}/session${R} · ${d.total_sessions} sessions · ${fmtDollar(d.total_cost)} total`);
140
+ // Top projects
141
+ if (d.project_costs.length > 0) {
142
+ lines.push(`\n 🗂 Top projects`);
143
+ const topTotal = d.project_costs.reduce((s, p) => s + p.total_cost, 0);
144
+ const shown = d.project_costs.slice(0, 4);
145
+ const otherCost = d.total_cost - shown.reduce((s, p) => s + p.total_cost, 0);
146
+ for (let i = 0; i < shown.length; i++) {
147
+ const p = shown[i];
148
+ const pct = topTotal > 0 ? Math.round(p.total_cost / topTotal * 100) : 0;
149
+ const name = path_1.default.basename(p.project).slice(0, 14).padEnd(14);
150
+ lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(p.total_cost)} ${D}${pct}%${R}`);
151
+ if (i < shown.length - 1 || (otherCost > 0.01 && d.project_costs.length > 4))
152
+ lines.push('');
153
+ }
154
+ if (otherCost > 0.01 && d.project_costs.length > 4) {
155
+ const pct = topTotal > 0 ? Math.round(otherCost / topTotal * 100) : 0;
156
+ lines.push(` ${'other'.padEnd(14)} ${bar(pct)} ${fmtDollar(otherCost)} ${D}${pct}%${R}`);
157
+ }
158
+ }
159
+ // Cache savings
160
+ const savingsLabel = d.cache_savings_usd >= 0.01
161
+ ? `${G}~${fmtDollar(d.cache_savings_usd)}${R} saved`
162
+ : 'no savings yet';
163
+ lines.push(`\n ⚡ Cache ${savingsLabel} · ${d.cache_hit_pct}% hit rate`);
164
+ // Output/input ratio
165
+ lines.push(`\n 📊 ${B}${d.output_input_ratio}×${R} output/input · ${D}${d.ratio_label}${R}`);
166
+ // Efficiency trend
167
+ let effTrend = '';
168
+ if (d.efficiency_delta !== -999) {
169
+ const arrow = d.efficiency_delta > 0
170
+ ? `${G}↑ +${d.efficiency_delta}${R}`
171
+ : d.efficiency_delta < 0 ? `${Y}↓ ${d.efficiency_delta}${R}` : '→ same';
172
+ effTrend = ` ${arrow} vs prev period`;
173
+ }
174
+ const loopLabel = d.total_loops > 0 ? ` · ${Y}${d.total_loops} loops${R}` : '';
175
+ lines.push(`\n 📈 Efficiency ${B}${d.avg_efficiency}/100${R}${effTrend}${loopLabel}`);
176
+ // Activity by time range
177
+ if (d.hour_ranges.length > 0) {
178
+ lines.push(`\n ⏰ Activity by time of day`);
179
+ const maxCount = Math.max(...d.hour_ranges.map(r => r.count));
180
+ for (let i = 0; i < d.hour_ranges.length; i++) {
181
+ const r = d.hour_ranges[i];
182
+ const pct = maxCount > 0 ? Math.round(r.count / maxCount * 100) : 0;
183
+ lines.push(` ${r.emoji} ${D}${r.from}–${r.to}${R} ${M}${bar(pct)}${R} ${D}${r.count} sessions${R}`);
184
+ if (i < d.hour_ranges.length - 1)
185
+ lines.push('');
186
+ }
187
+ }
188
+ // Model breakdown
189
+ const models = db_1.dbOps.getModelBreakdown(d.days);
190
+ const modelsWithCost = models.filter(m => m.total_cost > 0);
191
+ if (modelsWithCost.length >= 2) {
192
+ lines.push(`\n 🤖 Models`);
193
+ for (let i = 0; i < modelsWithCost.length; i++) {
194
+ const m = modelsWithCost[i];
195
+ const pct = d.total_cost > 0 ? Math.round(m.total_cost / d.total_cost * 100) : 0;
196
+ const rawName = (m.model ?? 'unknown').replace(/^<|>$/g, '');
197
+ const name = rawName.slice(0, 28).padEnd(28);
198
+ lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(m.total_cost)} ${D}${pct}% · ${m.session_count} sessions${R}`);
199
+ if (i < modelsWithCost.length - 1)
200
+ lines.push('');
201
+ }
202
+ }
203
+ lines.push('\n' + '━'.repeat(44) + '\n');
204
+ return lines.join('\n');
205
+ }
206
+ function shouldShowInsight() {
207
+ const last = db_1.dbOps.getMeta(META_KEY);
208
+ if (!last)
209
+ return true;
210
+ return Date.now() - parseInt(last, 10) >= WEEK_MS;
211
+ }
212
+ function markInsightShown() {
213
+ db_1.dbOps.setMeta(META_KEY, Date.now().toString());
214
+ }
215
+ function renderWeeklyInsight(d) {
216
+ const fmtTok = (n) => {
217
+ if (n >= 1000000)
218
+ return `${(n / 1000000).toFixed(1)}M`;
219
+ if (n >= 1000)
220
+ return `${Math.round(n / 1000)}K`;
221
+ return n.toString();
222
+ };
223
+ const R = '\x1b[0m';
224
+ const B = '\x1b[1m';
225
+ const D = '\x1b[2m';
226
+ const G = '\x1b[32m';
227
+ const Y = '\x1b[33m';
228
+ const C = '\x1b[36m';
229
+ const bar = (pct, width = 20) => {
230
+ const filled = Math.round(Math.min(pct, 100) / 100 * width);
231
+ const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
232
+ return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
233
+ };
234
+ const fmtDate = (ts) => {
235
+ const dt = new Date(ts);
236
+ return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
237
+ };
238
+ const lines = [];
239
+ lines.push(`\n${B}📊 claudestat weekly${R} ${D}${fmtDate(d.week_start)} – ${fmtDate(d.week_end)}${R}`);
240
+ lines.push('━'.repeat(44));
241
+ lines.push('');
242
+ lines.push(` 💰 ${B}$${d.total_cost.toFixed(2)}${R} total · ${B}${d.total_sessions}${R} sessions · ${B}${d.total_loops}${R} loops`);
243
+ lines.push('');
244
+ lines.push(` 🔧 Top tool ${B}${d.top_tool}${R} ${D}${d.top_tool_cost_pct}% of cost${R}`);
245
+ lines.push('');
246
+ lines.push(` 📈 Efficiency ${bar(d.avg_efficiency)} ${B}${d.avg_efficiency}/100${R}`);
247
+ lines.push('');
248
+ lines.push(` 💾 Cache hit ${bar(d.cache_hit_pct)} ${B}${d.cache_hit_pct}%${R}`);
249
+ lines.push('');
250
+ lines.push(` 📦 Tokens ${D}${fmtTok(d.input_tokens)} in + ${fmtTok(d.output_tokens)} out${R}`);
251
+ lines.push('');
252
+ lines.push(` ${C}⚡ Tip:${R} ${generateTip(d)}`);
253
+ lines.push('');
254
+ lines.push('━'.repeat(44));
255
+ lines.push('');
256
+ return lines.join('\n');
257
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
+ /**
3
+ * mcp-server.ts — MCP (Model Context Protocol) server for claudestat
4
+ *
5
+ * Exposes Claude Code usage metrics as tools that Claude can query.
6
+ * Zero extra dependencies — stdio JSON-RPC 2.0, readline only.
7
+ * Works without the daemon — reads SQLite + JSONL directly.
8
+ */
9
+ export {};