@statforge/claudestat 1.2.0 → 1.2.3
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 +199 -87
- package/dist/claude-auth.d.ts +7 -0
- package/dist/claude-auth.js +12 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -0
- package/dist/daemon.js +32 -15
- package/dist/db.d.ts +18 -0
- package/dist/db.js +50 -0
- package/dist/doctor.js +28 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +199 -76
- package/dist/insights.d.ts +26 -0
- package/dist/insights.js +172 -20
- package/dist/install.js +39 -0
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +148 -21
- package/dist/notifier.js +24 -6
- package/dist/pattern-analyzer.d.ts +1 -0
- package/dist/pattern-analyzer.js +13 -0
- package/dist/quota-tracker.d.ts +6 -0
- package/dist/quota-tracker.js +79 -32
- package/dist/roast.js +129 -40
- package/dist/routes/events.js +1 -1
- package/package.json +1 -1
package/dist/quota-tracker.js
CHANGED
|
@@ -19,6 +19,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
19
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.refreshFromApi = refreshFromApi;
|
|
22
23
|
exports.computeQuota = computeQuota;
|
|
23
24
|
exports.invalidateQuotaCache = invalidateQuotaCache;
|
|
24
25
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -35,33 +36,6 @@ const PLAN_LIMITS = {
|
|
|
35
36
|
const CYCLE_MS = 5 * 60 * 60 * 1000; // 5 horas en ms
|
|
36
37
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
37
38
|
const WINDOW_5MIN = 5 * 60 * 1000; // ventana de 5 min para agrupar actividad por modelo
|
|
38
|
-
/**
|
|
39
|
-
* Calcula el timestamp de reset usando ventana rolling real.
|
|
40
|
-
*
|
|
41
|
-
* Claude Code NO usa floor(now/5h) desde epoch UTC — usa una ventana rolling
|
|
42
|
-
* que empieza desde el primer mensaje del ciclo actual.
|
|
43
|
-
*
|
|
44
|
-
* Enfoque: buscar el primer mensaje humano en los últimos 5h de actividad.
|
|
45
|
-
* resetAt = primerMensaje.ts + 5h
|
|
46
|
-
*
|
|
47
|
-
* Si no hay mensajes en las últimas 5h → el ciclo ya reseteó, el próximo
|
|
48
|
-
* reset es en 5h desde el primer mensaje futuro (mostramos ~5h).
|
|
49
|
-
*/
|
|
50
|
-
function computeResetAt(entries, now) {
|
|
51
|
-
const fiveHoursAgo = now - CYCLE_MS;
|
|
52
|
-
const recentHuman = entries
|
|
53
|
-
.filter(e => e.type === 'human' && e.ts >= fiveHoursAgo)
|
|
54
|
-
.sort((a, b) => a.ts - b.ts);
|
|
55
|
-
if (recentHuman.length > 0) {
|
|
56
|
-
// Usamos el PRIMER mensaje humano (más antiguo) + 5h
|
|
57
|
-
// = momento en que el primer prompt de la ventana actual expira → cuota empieza a liberarse
|
|
58
|
-
// Los mensajes tool_result de sub-agentes ya están filtrados en el caller
|
|
59
|
-
return recentHuman[0].ts + CYCLE_MS;
|
|
60
|
-
}
|
|
61
|
-
// Sin actividad en las últimas 5h → cuota ya libre, no hay reset pendiente
|
|
62
|
-
// Retornar `now` hace que cycleResetMs = 0, la UI puede mostrar "Disponible"
|
|
63
|
-
return now;
|
|
64
|
-
}
|
|
65
39
|
function getWeekStart(now) {
|
|
66
40
|
// Lunes 00:00 hora local
|
|
67
41
|
const d = new Date(now);
|
|
@@ -210,6 +184,63 @@ function readAllEntries(sinceTs) {
|
|
|
210
184
|
catch { /* PROJECTS_DIR inaccesible */ }
|
|
211
185
|
return all;
|
|
212
186
|
}
|
|
187
|
+
// ─── API de uso de Anthropic (datos exactos = claude.ai/settings/usage) ──────
|
|
188
|
+
const USAGE_API_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
189
|
+
const USAGE_API_BETA = 'oauth-2025-04-20';
|
|
190
|
+
const API_CACHE_TTL = 5 * 60000; // 5 minutes
|
|
191
|
+
const API_DISK_CACHE = path_1.default.join((0, paths_1.getClaudestatDir)(), 'api-cache.json');
|
|
192
|
+
let apiCache = null;
|
|
193
|
+
/**
|
|
194
|
+
* Llama a la API de Anthropic para obtener los % de quota exactos que muestra claude.ai.
|
|
195
|
+
* Actualiza apiCache si la llamada tiene éxito; de lo contrario, no hace nada (silent fallback).
|
|
196
|
+
* Debe llamarse periódicamente desde el daemon y al inicio del MCP server.
|
|
197
|
+
*/
|
|
198
|
+
async function refreshFromApi() {
|
|
199
|
+
// Shared disk cache: all processes (daemon + MCP) read/write the same file.
|
|
200
|
+
// This ensures at most 1 API call per API_CACHE_TTL across all processes.
|
|
201
|
+
try {
|
|
202
|
+
const raw = fs_1.default.readFileSync(API_DISK_CACHE, 'utf8');
|
|
203
|
+
const disk = JSON.parse(raw);
|
|
204
|
+
if (disk && Date.now() - disk.ts < API_CACHE_TTL) {
|
|
205
|
+
apiCache = disk;
|
|
206
|
+
invalidateQuotaCache();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* no disk cache yet — proceed to API */ }
|
|
211
|
+
const token = (0, claude_auth_1.getOAuthAccessToken)();
|
|
212
|
+
if (!token)
|
|
213
|
+
return;
|
|
214
|
+
try {
|
|
215
|
+
const ctrl = new AbortController();
|
|
216
|
+
const timer = setTimeout(() => ctrl.abort(), 3000);
|
|
217
|
+
const res = await fetch(USAGE_API_URL, {
|
|
218
|
+
headers: { Authorization: `Bearer ${token}`, 'anthropic-beta': USAGE_API_BETA },
|
|
219
|
+
signal: ctrl.signal,
|
|
220
|
+
});
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
if (!res.ok) {
|
|
223
|
+
process.stderr.write(`[claudestat] API quota fetch failed: HTTP ${res.status}\n`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const data = await res.json();
|
|
227
|
+
const fh = data.five_hour;
|
|
228
|
+
if (fh == null)
|
|
229
|
+
return;
|
|
230
|
+
apiCache = {
|
|
231
|
+
cyclePct: Math.round(fh.utilization),
|
|
232
|
+
weeklyPctAll: data.seven_day != null ? Math.round(data.seven_day.utilization) : 0,
|
|
233
|
+
cycleResetAt: fh.resets_at ? new Date(fh.resets_at).getTime() : 0,
|
|
234
|
+
ts: Date.now(),
|
|
235
|
+
};
|
|
236
|
+
try {
|
|
237
|
+
fs_1.default.writeFileSync(API_DISK_CACHE, JSON.stringify(apiCache), 'utf8');
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
invalidateQuotaCache();
|
|
241
|
+
}
|
|
242
|
+
catch { /* no network or keychain — silent fallback to JSONL */ }
|
|
243
|
+
}
|
|
213
244
|
// ─── Caché de 30 segundos ─────────────────────────────────────────────────────
|
|
214
245
|
let cache = null;
|
|
215
246
|
const CACHE_TTL = 30000; // 30 segundos
|
|
@@ -225,6 +256,16 @@ function computeQuota(forcePlan) {
|
|
|
225
256
|
if (!forcePlan && cache && now - cache.ts < CACHE_TTL) {
|
|
226
257
|
return cache.data;
|
|
227
258
|
}
|
|
259
|
+
// Load API data from disk cache if in-memory is stale (shared across processes)
|
|
260
|
+
if (!apiCache || now - apiCache.ts >= API_CACHE_TTL) {
|
|
261
|
+
try {
|
|
262
|
+
const raw = fs_1.default.readFileSync(API_DISK_CACHE, 'utf8');
|
|
263
|
+
const disk = JSON.parse(raw);
|
|
264
|
+
if (disk && now - disk.ts < API_CACHE_TTL)
|
|
265
|
+
apiCache = disk;
|
|
266
|
+
}
|
|
267
|
+
catch { }
|
|
268
|
+
}
|
|
228
269
|
const weekStart = getWeekStart(now);
|
|
229
270
|
const thirtyMinAgo = now - 30 * 60 * 1000;
|
|
230
271
|
// Leer entradas relevantes (última semana + un poco más para detección de plan)
|
|
@@ -251,7 +292,7 @@ function computeQuota(forcePlan) {
|
|
|
251
292
|
const limits = PLAN_LIMITS[plan];
|
|
252
293
|
// ─ Ciclo 5h: ventana deslizante [now-5h, now] ─
|
|
253
294
|
const fiveHAgo = now - CYCLE_MS;
|
|
254
|
-
const cycleResetAt =
|
|
295
|
+
const cycleResetAt = (Math.floor(now / CYCLE_MS) + 1) * CYCLE_MS; // epoch-aligned, matches claude.ai
|
|
255
296
|
const cycleStart = fiveHAgo; // inicio real de la ventana de conteo
|
|
256
297
|
// Basado en tokens (como claude.ai/settings/usage)
|
|
257
298
|
const cycleEntries = entries.filter(e => e.ts >= fiveHAgo);
|
|
@@ -297,14 +338,20 @@ function computeQuota(forcePlan) {
|
|
|
297
338
|
const weeklyPctAll = limits.weeklyHoursSonnet + limits.weeklyHoursOpus > 0
|
|
298
339
|
? Math.min(100, Math.round((weeklyHoursSonnet + weeklyHoursOpus) / (limits.weeklyHoursSonnet + limits.weeklyHoursOpus) * 100))
|
|
299
340
|
: 0;
|
|
341
|
+
// Override with API data if fresh — matches claude.ai exactly
|
|
342
|
+
const apiFresh = apiCache != null && Date.now() - apiCache.ts < API_CACHE_TTL;
|
|
343
|
+
const resolvedCyclePct = apiFresh ? apiCache.cyclePct : cyclePct;
|
|
344
|
+
const resolvedWeeklyPctAll = apiFresh ? apiCache.weeklyPctAll : weeklyPctAll;
|
|
345
|
+
const resolvedCycleResetAt = (apiFresh && apiCache.cycleResetAt > 0) ? apiCache.cycleResetAt : cycleResetAt;
|
|
346
|
+
const resolvedCycleResetMs = Math.max(0, resolvedCycleResetAt - now);
|
|
300
347
|
const data = {
|
|
301
348
|
cyclePrompts: cycleEntries.filter(e => e.type === 'human').length,
|
|
302
349
|
cycleLimit: limits.prompts5h,
|
|
303
|
-
cyclePct,
|
|
350
|
+
cyclePct: resolvedCyclePct,
|
|
304
351
|
cycleTokens,
|
|
305
352
|
cycleLimitTokens: limits.tokens5h,
|
|
306
|
-
cycleResetMs,
|
|
307
|
-
cycleResetAt,
|
|
353
|
+
cycleResetMs: resolvedCycleResetMs,
|
|
354
|
+
cycleResetAt: resolvedCycleResetAt,
|
|
308
355
|
cycleStartTs: cycleStart,
|
|
309
356
|
weeklyHoursSonnet,
|
|
310
357
|
weeklyHoursOpus,
|
|
@@ -314,7 +361,7 @@ function computeQuota(forcePlan) {
|
|
|
314
361
|
weeklyTokensHaiku,
|
|
315
362
|
weeklyLimitSonnet: limits.weeklyHoursSonnet,
|
|
316
363
|
weeklyLimitOpus: limits.weeklyHoursOpus,
|
|
317
|
-
weeklyPctAll,
|
|
364
|
+
weeklyPctAll: resolvedWeeklyPctAll,
|
|
318
365
|
burnRateTokensPerMin,
|
|
319
366
|
detectedPlan: plan,
|
|
320
367
|
planSource,
|
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
|
|
24
|
-
const
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
}
|
package/dist/routes/events.js
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "1.2.3",
|
|
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",
|