@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.
@@ -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 = computeResetAt(entries, now);
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);
@@ -293,14 +334,24 @@ function computeQuota(forcePlan) {
293
334
  const burnRateTokensPerMin = recentAssistant.length > 0
294
335
  ? Math.round(totalRecentTok / 30)
295
336
  : 0;
337
+ // ─ % semanal combinado (Sonnet + Opus, coincide con "Todos los modelos" de claude.ai) ─
338
+ const weeklyPctAll = limits.weeklyHoursSonnet + limits.weeklyHoursOpus > 0
339
+ ? Math.min(100, Math.round((weeklyHoursSonnet + weeklyHoursOpus) / (limits.weeklyHoursSonnet + limits.weeklyHoursOpus) * 100))
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);
296
347
  const data = {
297
348
  cyclePrompts: cycleEntries.filter(e => e.type === 'human').length,
298
349
  cycleLimit: limits.prompts5h,
299
- cyclePct,
350
+ cyclePct: resolvedCyclePct,
300
351
  cycleTokens,
301
352
  cycleLimitTokens: limits.tokens5h,
302
- cycleResetMs,
303
- cycleResetAt,
353
+ cycleResetMs: resolvedCycleResetMs,
354
+ cycleResetAt: resolvedCycleResetAt,
304
355
  cycleStartTs: cycleStart,
305
356
  weeklyHoursSonnet,
306
357
  weeklyHoursOpus,
@@ -310,6 +361,7 @@ function computeQuota(forcePlan) {
310
361
  weeklyTokensHaiku,
311
362
  weeklyLimitSonnet: limits.weeklyHoursSonnet,
312
363
  weeklyLimitOpus: limits.weeklyHoursOpus,
364
+ weeklyPctAll: resolvedWeeklyPctAll,
313
365
  burnRateTokensPerMin,
314
366
  detectedPlan: plan,
315
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 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
  }
@@ -75,7 +75,7 @@ exports.eventsRouter.post('/event', (req, res) => {
75
75
  return;
76
76
  }
77
77
  const resolvedCwd = cwd
78
- ?? (transcript_path ? transcript_path.split('/').slice(0, -1).join('/') : undefined);
78
+ ?? (transcript_path ? path_1.default.dirname(transcript_path) || undefined : undefined);
79
79
  db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts });
80
80
  // Skill grouping: get current parent BEFORE processing this event
81
81
  // (the Skill Done event itself is NOT tagged — only its subsequent sub-calls are)
@@ -125,7 +125,7 @@ exports.eventsRouter.post('/event', (req, res) => {
125
125
  try {
126
126
  const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : (tool_input ?? {});
127
127
  const filePath = inp?.file_path ?? inp?.path;
128
- if (typeof filePath === 'string' && filePath.startsWith('/')) {
128
+ if (typeof filePath === 'string' && path_1.default.isAbsolute(filePath)) {
129
129
  const projectCwd = findProjectCwdForFile(filePath);
130
130
  if (projectCwd)
131
131
  db_1.dbOps.updateSessionProject(session_id, projectCwd);
@@ -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;
@@ -39,7 +39,7 @@ function inferProjectCwd(events) {
39
39
  try {
40
40
  const inp = JSON.parse(ev.tool_input);
41
41
  const filePath = (inp.file_path || inp.path);
42
- if (!filePath?.startsWith('/'))
42
+ if (!filePath || !path_1.default.isAbsolute(filePath))
43
43
  continue;
44
44
  const cwd = findProjectCwdForFile(filePath);
45
45
  if (cwd)
@@ -71,7 +71,7 @@ function inferActiveProjectByMajority(events, windowMs) {
71
71
  try {
72
72
  const inp = JSON.parse(ev.tool_input);
73
73
  const filePath = (inp.file_path || inp.path);
74
- if (!filePath?.startsWith('/'))
74
+ if (!filePath || !path_1.default.isAbsolute(filePath))
75
75
  continue;
76
76
  const project = findProjectCwdForFile(filePath);
77
77
  if (!project)
package/dist/share.js CHANGED
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.runShare = runShare;
4
7
  const db_js_1 = require("./db.js");
5
8
  const child_process_1 = require("child_process");
9
+ const path_1 = __importDefault(require("path"));
6
10
  function formatDuration(ms) {
7
11
  const seconds = Math.floor(ms / 1000);
8
12
  const minutes = Math.floor(seconds / 60);
@@ -45,7 +49,7 @@ async function getSessionData(sessionId) {
45
49
  topToolPct = Math.round((sorted[0][1] / toolCalls.length) * 100);
46
50
  }
47
51
  const durationMs = (session.last_event_at || session.started_at) - session.started_at;
48
- const project = session.project_path?.split('/').pop() || 'unknown';
52
+ const project = path_1.default.basename(session.project_path ?? '') || 'unknown';
49
53
  return {
50
54
  id: session.id,
51
55
  project: project.length > 18 ? project.slice(0, 15) + '...' : project,
@@ -92,7 +92,7 @@ function buildContext(events, costUsd, projectName) {
92
92
  try {
93
93
  const inp = JSON.parse(e.tool_input);
94
94
  const fp = inp.file_path || inp.path;
95
- if (typeof fp === 'string' && fp.startsWith('/')) {
95
+ if (typeof fp === 'string' && path_1.default.isAbsolute(fp)) {
96
96
  filesSet.add(path_1.default.basename(fp));
97
97
  }
98
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.1.1",
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",
@@ -48,7 +48,8 @@
48
48
  "dashboard/dist/"
49
49
  ],
50
50
  "bin": {
51
- "claudestat": "dist/index.js"
51
+ "claudestat": "dist/index.js",
52
+ "claudestat-mcp": "dist/mcp-server.js"
52
53
  },
53
54
  "scripts": {
54
55
  "build": "tsc && npm run build:dashboard",
@@ -58,7 +59,7 @@
58
59
  "dev": "tsx src/index.ts",
59
60
  "dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
60
61
  "start": "node dist/index.js",
61
- "test": "bash run-tests.sh"
62
+ "test": "node --require tsx/cjs tests/index.ts"
62
63
  },
63
64
  "dependencies": {
64
65
  "@anthropic-ai/sdk": "^0.88.0",