@statforge/claudestat 1.2.0 β†’ 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.
package/dist/roast.js CHANGED
@@ -2,54 +2,38 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runRoast = runRoast;
4
4
  const db_js_1 = require("./db.js");
5
- function formatMinutes(totalMinutes) {
6
- if (totalMinutes < 60)
7
- return `${Math.round(totalMinutes)} minutes`;
8
- const hours = Math.floor(totalMinutes / 60);
9
- if (hours < 24)
10
- return `${hours} hours`;
11
- const days = Math.floor(hours / 24);
12
- return `${days} days`;
13
- }
14
5
  function getRoastRating(avgEfficiency) {
15
6
  if (avgEfficiency >= 90)
16
- return "You're a machine. Or maybe you're just not using Claude enough. 😏";
7
+ return "You're a machine. Or maybe you're just not using Claude enough.";
17
8
  if (avgEfficiency >= 70)
18
9
  return "Solid. Not great, not terrible. The AI equivalent of a C+ student.";
19
10
  if (avgEfficiency >= 50)
20
- return "Room for growth, champ. πŸ“ˆ";
21
- return "Oof. That's a lot of money down the drain. Are you okay? πŸ’€";
11
+ return "Room for growth, champ.";
12
+ return "Oof. That's a lot of money down the drain. Are you okay?";
22
13
  }
23
- function getRoastMessage(data) {
24
- const lines = [];
25
- lines.push('πŸ”₯ Your Claude Code Roast');
26
- lines.push('');
14
+ function getRoastCards(data) {
15
+ const cards = [];
27
16
  if (data.totalBashCalls > 0) {
28
17
  const minutesPerCall = (data.days * 24 * 60) / data.totalBashCalls;
29
18
  if (minutesPerCall < 60) {
30
- lines.push(` You called Bash ${data.totalBashCalls} times in ${data.days} days.`);
31
- lines.push(` That's once every ${minutesPerCall.toFixed(1)} minutes.`);
32
- lines.push(' Are you okay?');
33
- lines.push('');
19
+ cards.push(` πŸ–₯️ BASH OVERLOAD\n` +
20
+ ` ${data.totalBashCalls} calls in ${data.days}d β€” once every ${minutesPerCall.toFixed(1)} min\n` +
21
+ ` Are you okay?`);
34
22
  }
35
23
  }
36
24
  if (data.contextHits > 0) {
37
- lines.push(` You hit 90%+ context in ${data.contextHits} sessions.`);
38
- lines.push(' Claude was writing with amnesia half the time.');
39
- lines.push('');
25
+ cards.push(` 🧠 CONTEXT AMNESIA\n` +
26
+ ` ${data.contextHits} sessions at 90%+ context\n` +
27
+ ` Claude was writing with amnesia half the time.`);
40
28
  }
41
29
  if (data.totalLoops > 0) {
42
30
  const loopCost = data.totalCost * 0.15;
43
- lines.push(` You spent $${loopCost.toFixed(2)} on loops you never noticed.`);
44
31
  const coffees = Math.floor(loopCost / 0.3);
45
- if (coffees > 0) {
46
- lines.push(` That's ${coffees} coffees. Just saying.`);
47
- lines.push('');
48
- }
32
+ cards.push(` πŸ”„ LOOP MONEY PIT\n` +
33
+ ` $${loopCost.toFixed(2)} wasted on loops${coffees > 0 ? ` β€” that's ${coffees} coffees` : ''}\n` +
34
+ ` Just saying.`);
49
35
  }
50
- lines.push(` Efficiency score: ${Math.round(data.avgEfficiency)}/100`);
51
- lines.push(` ${getRoastRating(data.avgEfficiency)}`);
52
- return lines.join('\n');
36
+ return cards;
53
37
  }
54
38
  async function runRoast(opts) {
55
39
  const days = opts.months ?? 30;
@@ -58,8 +42,10 @@ async function runRoast(opts) {
58
42
  let totalBashCalls = 0;
59
43
  let totalLoops = 0;
60
44
  let contextHits = 0;
45
+ let totalTokens = 0;
61
46
  for (const session of sessions) {
62
47
  totalLoops += session.loops_detected || 0;
48
+ totalTokens += (session.total_input_tokens || 0) + (session.total_output_tokens || 0) + (session.total_cache_read || 0);
63
49
  if ((session.total_input_tokens || 0) + (session.total_output_tokens || 0) > 150000) {
64
50
  contextHits++;
65
51
  }
@@ -71,6 +57,10 @@ async function runRoast(opts) {
71
57
  const avgEfficiency = sessions.length > 0
72
58
  ? sessions.reduce((a, s) => a + (s.efficiency_score || 0), 0) / sessions.length
73
59
  : 100;
60
+ const avgCostPerSession = sessions.length > 0 ? totalCost / sessions.length : 0;
61
+ const topTools = db_js_1.dbOps.getTopTools(days, 'cost', 1);
62
+ const topTool = topTools[0]?.tool_name ?? 'Unknown';
63
+ const topToolPct = totalCost > 0 ? Math.round((topTools[0]?.total_cost_usd ?? 0) / totalCost * 100) : 0;
74
64
  const data = {
75
65
  totalCost,
76
66
  totalSessions: sessions.length,
@@ -79,17 +69,116 @@ async function runRoast(opts) {
79
69
  avgEfficiency,
80
70
  contextHits,
81
71
  days,
72
+ totalTokens,
73
+ avgCostPerSession,
74
+ topTool,
75
+ topToolPct,
76
+ };
77
+ const R = '\x1b[0m';
78
+ const B = '\x1b[1m';
79
+ const D = '\x1b[2m';
80
+ const G = '\x1b[32m';
81
+ const Y = '\x1b[33m';
82
+ const C = '\x1b[36m';
83
+ const M = '\x1b[35m';
84
+ const bar = (pct, width = 20) => {
85
+ const filled = Math.round(Math.min(pct, 100) / 100 * width);
86
+ const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
87
+ return `${color}${'β–ˆ'.repeat(filled)}${R}${D}${'β–‘'.repeat(width - filled)}${R}`;
88
+ };
89
+ const fmtTok = (n) => {
90
+ if (n >= 1000000)
91
+ return `${(n / 1000000).toFixed(1)}M`;
92
+ if (n >= 1000)
93
+ return `${Math.round(n / 1000)}K`;
94
+ return n.toString();
82
95
  };
83
96
  if (opts.stats) {
84
- console.log(`=== Claude Code Stats (${days} days) ===`);
85
- console.log(`Sessions: ${sessions.length}`);
86
- console.log(`Total cost: $${totalCost.toFixed(2)}`);
87
- console.log(`Bash calls: ${totalBashCalls}`);
88
- console.log(`Loops: ${totalLoops}`);
89
- console.log(`Effficiency: ${Math.round(avgEfficiency)}/100`);
97
+ const effPct = Math.round(avgEfficiency);
98
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
99
+ const padVisual = (s, target) => {
100
+ const visible = stripAnsi(s).length;
101
+ return s + ' '.repeat(Math.max(0, target - visible));
102
+ };
103
+ const rows = [
104
+ { label: 'Sessions', value: `${B}${sessions.length}${R}`, barPct: Math.min(sessions.length / 50 * 100, 100) },
105
+ { label: 'Total cost', value: `${B}$${totalCost.toFixed(2)}${R}`, barPct: Math.min(totalCost / 200 * 100, 100) },
106
+ { label: 'Bash calls', value: `${B}${totalBashCalls}${R}`, barPct: Math.min(totalBashCalls / 500 * 100, 100) },
107
+ { label: 'Loops', value: `${B}${totalLoops}${R}`, barPct: Math.min(totalLoops / 200 * 100, 100) },
108
+ { label: 'Efficiency', value: `${B}${effPct}/100${R}`, barPct: effPct },
109
+ ];
110
+ const lines = [];
111
+ lines.push(`\n${B}πŸ“Š Claude Code Stats${R} ${D}(${days} days)${R}`);
112
+ lines.push('━'.repeat(42));
113
+ lines.push('');
114
+ for (let i = 0; i < rows.length; i++) {
115
+ const r = rows[i];
116
+ const labelCol = ` ${r.label}`.padEnd(14);
117
+ const valueCol = padVisual(r.value, 12);
118
+ lines.push(`${labelCol}${valueCol}${bar(r.barPct)}`);
119
+ if (i < rows.length - 1)
120
+ lines.push('');
121
+ }
122
+ lines.push('');
123
+ console.log(lines.join('\n'));
90
124
  return;
91
125
  }
92
- console.log(getRoastMessage(data));
93
- console.log('');
94
- console.log(' github.com/DeibyGS/claudestat');
126
+ const rating = Math.round(avgEfficiency);
127
+ const stars = rating >= 90 ? 'β˜…β˜…β˜…β˜…β˜…' : rating >= 70 ? 'β˜…β˜…β˜…β˜…β˜†' : rating >= 50 ? 'β˜…β˜…β˜…β˜†β˜†' : 'β˜…β˜…β˜†β˜†β˜†';
128
+ const lines = [];
129
+ lines.push(`\n${B}πŸ”₯ Your Claude Code Roast${R} ${D}(${days} days)${R}`);
130
+ lines.push('━'.repeat(44));
131
+ lines.push('');
132
+ lines.push(` ${B}Score${R} ${bar(rating)} ${B}${rating}/100${R} ${Y}${stars}${R}`);
133
+ lines.push('');
134
+ lines.push(` ${B}Scorecard${R}`);
135
+ lines.push(` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”`);
136
+ lines.push(` β”‚ ${D}Metric${R} β”‚ ${D}Value${R} β”‚ ${D}Rating${R} β”‚`);
137
+ lines.push(` β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€`);
138
+ const sessionLabel = `${data.totalSessions}`;
139
+ const sessionRating = data.totalSessions > 30 ? `${Y}prolific${R}` : `${G}normal${R}`;
140
+ lines.push(` β”‚ Sessions β”‚ ${sessionLabel.padEnd(12)} β”‚ ${sessionRating}`.padEnd(46) + 'β”‚');
141
+ const costLabel = `$${totalCost.toFixed(2)}`;
142
+ const costRating = totalCost > 100 ? `${Y}πŸ’Έ burning${R}` : totalCost > 20 ? `${G}reasonable${R}` : `${G}frugal${R}`;
143
+ lines.push(` β”‚ Total cost β”‚ ${costLabel.padEnd(12)} β”‚ ${costRating}`.padEnd(46) + 'β”‚');
144
+ const avgLabel = `$${avgCostPerSession.toFixed(2)}/session`;
145
+ const avgRating = avgCostPerSession > 10 ? `${Y}expensive${R}` : `${G}efficient${R}`;
146
+ lines.push(` β”‚ Avg/session β”‚ ${avgLabel.padEnd(12)} β”‚ ${avgRating}`.padEnd(46) + 'β”‚');
147
+ const bashLabel = `${totalBashCalls}`;
148
+ const bashRating = totalBashCalls > 200 ? `${Y}πŸ”¨ overload${R}` : `${G}normal${R}`;
149
+ lines.push(` β”‚ Bash calls β”‚ ${bashLabel.padEnd(12)} β”‚ ${bashRating}`.padEnd(46) + 'β”‚');
150
+ const loopLabel = `${totalLoops}`;
151
+ const loopRating = totalLoops > 50 ? `${Y}πŸ”„ looping${R}` : `${G}clean${R}`;
152
+ lines.push(` β”‚ Loops β”‚ ${loopLabel.padEnd(12)} β”‚ ${loopRating}`.padEnd(46) + 'β”‚');
153
+ const effLabel = `${Math.round(avgEfficiency)}/100`;
154
+ const effRating = avgEfficiency >= 90 ? `${G}πŸ† elite${R}` : avgEfficiency >= 70 ? `${Y}decent${R}` : `${Y}needs work${R}`;
155
+ lines.push(` β”‚ Efficiency β”‚ ${effLabel.padEnd(12)} β”‚ ${effRating}`.padEnd(46) + 'β”‚');
156
+ const tokLabel = fmtTok(totalTokens);
157
+ lines.push(` β”‚ Tokens β”‚ ${tokLabel.padEnd(12)} β”‚ ${D}β€”${R}`.padEnd(46) + 'β”‚');
158
+ const toolLabel = `${topTool} ${topToolPct}%`;
159
+ lines.push(` β”‚ Top tool β”‚ ${toolLabel.padEnd(12)} β”‚ ${D}β€”${R}`.padEnd(46) + 'β”‚');
160
+ lines.push(` β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜`);
161
+ lines.push('');
162
+ const cards = getRoastCards(data);
163
+ if (cards.length > 0) {
164
+ lines.push(` ${B}Roast Cards${R}`);
165
+ lines.push('');
166
+ for (const card of cards) {
167
+ const cardLines = card.split('\n');
168
+ const maxLen = Math.max(...cardLines.map(l => l.length));
169
+ lines.push(` β”Œ${'─'.repeat(maxLen + 2)}┐`);
170
+ for (const cl of cardLines) {
171
+ lines.push(` β”‚ ${cl.padEnd(maxLen)} β”‚`);
172
+ }
173
+ lines.push(` β””${'─'.repeat(maxLen + 2)}β”˜`);
174
+ lines.push('');
175
+ }
176
+ }
177
+ lines.push(` ${B}Verdict${R}`);
178
+ lines.push(` ${getRoastRating(avgEfficiency)}`);
179
+ lines.push('');
180
+ lines.push('━'.repeat(44));
181
+ lines.push(` ${D}github.com/DeibyGS/claudestat${R}`);
182
+ lines.push('');
183
+ console.log(lines.join('\n'));
95
184
  }
@@ -267,6 +267,6 @@ exports.onCostUpdate = onCostUpdate;
267
267
  // ─── Callback de auto-compact ────────────────────────────────────────────────
268
268
  const onCompactDetected = (sessionId) => {
269
269
  (0, stream_1.broadcast)({ type: 'compact_detected', payload: { session_id: sessionId, ts: Date.now() } });
270
- console.log(`[daemon] Auto-compact detectado para sesiΓ³n ${sessionId.slice(0, 8)}`);
270
+ console.log(`[daemon] Auto-compact detected for session ${sessionId.slice(0, 8)}`);
271
271
  };
272
272
  exports.onCompactDetected = onCompactDetected;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Observability layer for Claude Code β€” live token tracking, cost analytics, quota guard, loop detection, and usage dashboard. The htop for Claude Code.",
5
5
  "keywords": [
6
6
  "claude-code",