@statforge/claudestat 1.0.1
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 +437 -0
- package/dashboard/dist/assets/AnalyticsView-BApcOGsD.js +8 -0
- package/dashboard/dist/assets/HistoryView-B331k5oL.js +1 -0
- package/dashboard/dist/assets/ProjectsView-DUleaXsP.js +6 -0
- package/dashboard/dist/assets/SystemView-BGe__vl1.js +1 -0
- package/dashboard/dist/assets/TopView-CXggyydU.js +1 -0
- package/dashboard/dist/assets/index-CB01c5lb.js +84 -0
- package/dashboard/dist/assets/vendor-lucide-Cym0q5l_.js +344 -0
- package/dashboard/dist/assets/vendor-react-B_Jzs0gY.js +24 -0
- package/dashboard/dist/index.html +21 -0
- package/dist/cache/projects-cache.d.ts +9 -0
- package/dist/cache/projects-cache.js +51 -0
- package/dist/claude-auth.d.ts +38 -0
- package/dist/claude-auth.js +133 -0
- package/dist/claude-stats.d.ts +32 -0
- package/dist/claude-stats.js +98 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +110 -0
- package/dist/daemon.d.ts +15 -0
- package/dist/daemon.js +247 -0
- package/dist/db.d.ts +134 -0
- package/dist/db.js +546 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +191 -0
- package/dist/enricher.d.ts +34 -0
- package/dist/enricher.js +394 -0
- package/dist/export.d.ts +8 -0
- package/dist/export.js +82 -0
- package/dist/git.d.ts +22 -0
- package/dist/git.js +57 -0
- package/dist/github.d.ts +27 -0
- package/dist/github.js +62 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +319 -0
- package/dist/install.d.ts +14 -0
- package/dist/install.js +202 -0
- package/dist/intelligence.d.ts +45 -0
- package/dist/intelligence.js +105 -0
- package/dist/meta-stats.d.ts +28 -0
- package/dist/meta-stats.js +137 -0
- package/dist/middleware/rate-limiter.d.ts +2 -0
- package/dist/middleware/rate-limiter.js +30 -0
- package/dist/notifier.d.ts +1 -0
- package/dist/notifier.js +22 -0
- package/dist/paths.d.ts +79 -0
- package/dist/paths.js +134 -0
- package/dist/pattern-analyzer.d.ts +35 -0
- package/dist/pattern-analyzer.js +123 -0
- package/dist/project-scanner.d.ts +71 -0
- package/dist/project-scanner.js +619 -0
- package/dist/quota-tracker.d.ts +45 -0
- package/dist/quota-tracker.js +320 -0
- package/dist/render.d.ts +55 -0
- package/dist/render.js +229 -0
- package/dist/routes/events.d.ts +18 -0
- package/dist/routes/events.js +272 -0
- package/dist/routes/history.d.ts +1 -0
- package/dist/routes/history.js +65 -0
- package/dist/routes/misc.d.ts +1 -0
- package/dist/routes/misc.js +280 -0
- package/dist/routes/projects.d.ts +15 -0
- package/dist/routes/projects.js +153 -0
- package/dist/routes/reports.d.ts +11 -0
- package/dist/routes/reports.js +205 -0
- package/dist/routes/stream.d.ts +8 -0
- package/dist/routes/stream.js +70 -0
- package/dist/routes/top.d.ts +1 -0
- package/dist/routes/top.js +30 -0
- package/dist/session-state.d.ts +35 -0
- package/dist/session-state.js +50 -0
- package/dist/summarizer.d.ts +18 -0
- package/dist/summarizer.js +137 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +157 -0
- package/dist/watchdog.d.ts +11 -0
- package/dist/watchdog.js +75 -0
- package/dist/weekly.d.ts +13 -0
- package/dist/weekly.js +39 -0
- package/hooks/event.js +80 -0
- package/package.json +78 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* quota-tracker.ts — Seguimiento de cuota de uso de Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Calcula en base a los JSONL de ~/.claude/projects/:
|
|
6
|
+
* - Prompts reales en el ciclo actual de 5 horas (ventana deslizante desde epoch UTC)
|
|
7
|
+
* - Tiempo hasta el reset del ciclo
|
|
8
|
+
* - Horas de sesión semanales por modelo (Sonnet vs Opus)
|
|
9
|
+
* - Burn rate: tokens/min promedio de los últimos 30 minutos
|
|
10
|
+
* - Plan detectado automáticamente desde el máximo histórico
|
|
11
|
+
*
|
|
12
|
+
* Por qué 5h desde epoch UTC (no desde una hora fija):
|
|
13
|
+
* Claude Code usa ventanas deslizantes de exactamente 5 horas desde el epoch Unix.
|
|
14
|
+
* Ciclo actual = floor(now / 5h) * 5h. El reset ocurre cuando cruza ese límite.
|
|
15
|
+
*
|
|
16
|
+
* Caché de 30 segundos para no re-leer todos los JSONL en cada request del dashboard.
|
|
17
|
+
*/
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.computeQuota = computeQuota;
|
|
23
|
+
exports.invalidateQuotaCache = invalidateQuotaCache;
|
|
24
|
+
const fs_1 = __importDefault(require("fs"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const claude_auth_1 = require("./claude-auth");
|
|
27
|
+
const paths_1 = require("./paths");
|
|
28
|
+
const PLAN_LIMITS = {
|
|
29
|
+
free: { prompts5h: 10, weeklyHoursSonnet: 40, weeklyHoursOpus: 0 },
|
|
30
|
+
pro: { prompts5h: 45, weeklyHoursSonnet: 80, weeklyHoursOpus: 0 },
|
|
31
|
+
max5: { prompts5h: 225, weeklyHoursSonnet: 280, weeklyHoursOpus: 35 },
|
|
32
|
+
max20: { prompts5h: 900, weeklyHoursSonnet: 480, weeklyHoursOpus: 40 },
|
|
33
|
+
};
|
|
34
|
+
// ─── Helpers de ventanas temporales ──────────────────────────────────────────
|
|
35
|
+
const CYCLE_MS = 5 * 60 * 60 * 1000; // 5 horas en ms
|
|
36
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
37
|
+
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
|
+
function getWeekStart(now) {
|
|
66
|
+
// Lunes 00:00 hora local
|
|
67
|
+
const d = new Date(now);
|
|
68
|
+
const day = d.getDay(); // 0=domingo … 6=sábado
|
|
69
|
+
const diff = day === 0 ? -6 : 1 - day; // días hasta el lunes anterior
|
|
70
|
+
d.setDate(d.getDate() + diff);
|
|
71
|
+
d.setHours(0, 0, 0, 0);
|
|
72
|
+
return d.getTime();
|
|
73
|
+
}
|
|
74
|
+
// ─── Parser de un archivo JSONL ───────────────────────────────────────────────
|
|
75
|
+
function parseJSONLFile(filePath) {
|
|
76
|
+
const entries = [];
|
|
77
|
+
let content;
|
|
78
|
+
try {
|
|
79
|
+
content = fs_1.default.readFileSync(filePath, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return entries;
|
|
83
|
+
}
|
|
84
|
+
for (const raw of content.split('\n')) {
|
|
85
|
+
const line = raw.trim();
|
|
86
|
+
if (!line)
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const obj = JSON.parse(line);
|
|
90
|
+
// Timestamp: ISO string en la raíz del objeto
|
|
91
|
+
const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
|
|
92
|
+
if (!ts || isNaN(ts))
|
|
93
|
+
continue;
|
|
94
|
+
// Mensajes de usuario: tipo 'human' (algunas versiones usan 'user')
|
|
95
|
+
if (obj.type === 'human' || obj.type === 'user') {
|
|
96
|
+
// Filtrar comandos locales — no son prompts reales del usuario
|
|
97
|
+
const content = obj.message?.content;
|
|
98
|
+
// Saltar mensajes internos de sub-agentes: su content comienza con tool_result
|
|
99
|
+
// (son respuestas de herramientas que el sub-agente recibe, no prompts del usuario)
|
|
100
|
+
if (Array.isArray(content) && content[0]?.type === 'tool_result')
|
|
101
|
+
continue;
|
|
102
|
+
const text = typeof content === 'string' ? content
|
|
103
|
+
: Array.isArray(content)
|
|
104
|
+
? content.find(c => c?.type === 'text')?.text ?? ''
|
|
105
|
+
: '';
|
|
106
|
+
if (text.includes('<command-name>') ||
|
|
107
|
+
text.includes('<local-command-stdout>') ||
|
|
108
|
+
text.includes('<system-reminder>'))
|
|
109
|
+
continue;
|
|
110
|
+
entries.push({ ts, type: 'human' });
|
|
111
|
+
}
|
|
112
|
+
// Respuestas del asistente: tienen datos de uso de tokens
|
|
113
|
+
if (obj.type === 'assistant') {
|
|
114
|
+
const usage = obj.message?.usage;
|
|
115
|
+
const model = obj.message?.model ?? 'claude-sonnet-4-6';
|
|
116
|
+
entries.push({
|
|
117
|
+
ts, type: 'assistant',
|
|
118
|
+
model,
|
|
119
|
+
inputTokens: usage?.input_tokens ?? 0,
|
|
120
|
+
outputTokens: usage?.output_tokens ?? 0,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Línea malformada — ignorar y continuar
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return entries;
|
|
129
|
+
}
|
|
130
|
+
// ─── Detección automática de plan ────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Infiere el plan mirando el máximo de prompts humanos en cualquier ciclo de 5h.
|
|
133
|
+
* Si en algún ciclo hubo >200 prompts → Max20. Si >40 → Max5. Si ≤40 → Pro.
|
|
134
|
+
*
|
|
135
|
+
* Es conservador: si nunca se ha llegado al límite, asume Pro.
|
|
136
|
+
*/
|
|
137
|
+
function detectPlan(entries) {
|
|
138
|
+
const countsByCycle = new Map();
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
if (e.type !== 'human')
|
|
141
|
+
continue;
|
|
142
|
+
const cycle = Math.floor(e.ts / CYCLE_MS) * CYCLE_MS;
|
|
143
|
+
countsByCycle.set(cycle, (countsByCycle.get(cycle) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
const maxSeen = countsByCycle.size > 0 ? Math.max(...countsByCycle.values()) : 0;
|
|
146
|
+
if (maxSeen > 200)
|
|
147
|
+
return 'max20';
|
|
148
|
+
if (maxSeen > 40)
|
|
149
|
+
return 'max5';
|
|
150
|
+
return 'pro';
|
|
151
|
+
}
|
|
152
|
+
// ─── Lector de todos los JSONL ────────────────────────────────────────────────
|
|
153
|
+
const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
|
|
154
|
+
function readAllEntries(sinceTs) {
|
|
155
|
+
const all = [];
|
|
156
|
+
try {
|
|
157
|
+
if (!fs_1.default.existsSync(PROJECTS_DIR))
|
|
158
|
+
return all;
|
|
159
|
+
for (const dir of fs_1.default.readdirSync(PROJECTS_DIR)) {
|
|
160
|
+
const dirPath = path_1.default.join(PROJECTS_DIR, dir);
|
|
161
|
+
try {
|
|
162
|
+
if (!fs_1.default.statSync(dirPath).isDirectory())
|
|
163
|
+
continue;
|
|
164
|
+
// Recopilar todos los subdirectorios a leer: el propio dir + cualquier <uuid>/subagents/
|
|
165
|
+
const subdirs = [dirPath];
|
|
166
|
+
try {
|
|
167
|
+
for (const entry of fs_1.default.readdirSync(dirPath)) {
|
|
168
|
+
const entryPath = path_1.default.join(dirPath, entry);
|
|
169
|
+
if (fs_1.default.statSync(entryPath).isDirectory()) {
|
|
170
|
+
const subagentsPath = path_1.default.join(entryPath, 'subagents');
|
|
171
|
+
if (fs_1.default.existsSync(subagentsPath))
|
|
172
|
+
subdirs.push(subagentsPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* ignorar */ }
|
|
177
|
+
for (const subdir of subdirs) {
|
|
178
|
+
try {
|
|
179
|
+
if (!fs_1.default.existsSync(subdir))
|
|
180
|
+
continue;
|
|
181
|
+
// Recopilar archivos con su mtime para ordenar por recencia y limitar
|
|
182
|
+
const candidates = [];
|
|
183
|
+
for (const file of fs_1.default.readdirSync(subdir)) {
|
|
184
|
+
if (!file.endsWith('.jsonl'))
|
|
185
|
+
continue;
|
|
186
|
+
if (!file.includes('-') || file.length < 15)
|
|
187
|
+
continue;
|
|
188
|
+
try {
|
|
189
|
+
const mtime = fs_1.default.statSync(path_1.default.join(subdir, file)).mtimeMs;
|
|
190
|
+
if (mtime < sinceTs - WEEK_MS)
|
|
191
|
+
continue;
|
|
192
|
+
candidates.push({ path: path_1.default.join(subdir, file), mtime });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Procesar solo los 300 más recientes — evita bloquear el event loop
|
|
199
|
+
candidates
|
|
200
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
201
|
+
.slice(0, 300)
|
|
202
|
+
.forEach(c => all.push(...parseJSONLFile(c.path)));
|
|
203
|
+
}
|
|
204
|
+
catch { /* subdir inaccesible */ }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { /* directorio inaccesible */ }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* PROJECTS_DIR inaccesible */ }
|
|
211
|
+
return all;
|
|
212
|
+
}
|
|
213
|
+
// ─── Caché de 30 segundos ─────────────────────────────────────────────────────
|
|
214
|
+
let cache = null;
|
|
215
|
+
const CACHE_TTL = 30000; // 30 segundos
|
|
216
|
+
// ─── Función principal ────────────────────────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Calcula y retorna QuotaData.
|
|
219
|
+
* Usa caché de 30s para no re-leer todos los JSONL en cada request del dashboard.
|
|
220
|
+
* Pasar `forcePlan` para fijar el plan manualmente (por config del usuario).
|
|
221
|
+
*/
|
|
222
|
+
function computeQuota(forcePlan) {
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
// Devolver caché si está fresco y no se fuerza plan
|
|
225
|
+
if (!forcePlan && cache && now - cache.ts < CACHE_TTL) {
|
|
226
|
+
return cache.data;
|
|
227
|
+
}
|
|
228
|
+
const weekStart = getWeekStart(now);
|
|
229
|
+
const thirtyMinAgo = now - 30 * 60 * 1000;
|
|
230
|
+
// Leer entradas relevantes (última semana + un poco más para detección de plan)
|
|
231
|
+
const entries = readAllEntries(weekStart - CYCLE_MS);
|
|
232
|
+
// ─ Plan (prioridad: config manual → keychain → inferencia JSONL) ─
|
|
233
|
+
let plan;
|
|
234
|
+
let planSource;
|
|
235
|
+
if (forcePlan) {
|
|
236
|
+
plan = forcePlan;
|
|
237
|
+
planSource = 'config';
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Intentar leer plan desde las credenciales del keychain (más fiable que inferir)
|
|
241
|
+
const auth = (0, claude_auth_1.readClaudeAuth)();
|
|
242
|
+
if (auth.source !== 'unknown' && auth.subscriptionType !== 'unknown') {
|
|
243
|
+
plan = (0, claude_auth_1.subscriptionTypeToPlan)(auth.subscriptionType, auth.rateLimitTier);
|
|
244
|
+
planSource = 'keychain';
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
plan = detectPlan(entries);
|
|
248
|
+
planSource = 'inferred';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const limits = PLAN_LIMITS[plan];
|
|
252
|
+
// ─ Ciclo 5h: ventana deslizante [now-5h, now] ─
|
|
253
|
+
const fiveHAgo = now - CYCLE_MS;
|
|
254
|
+
const cycleResetAt = computeResetAt(entries, now);
|
|
255
|
+
const cycleStart = fiveHAgo; // inicio real de la ventana de conteo
|
|
256
|
+
const cyclePrompts = entries.filter(e => e.type === 'human' && e.ts >= fiveHAgo).length;
|
|
257
|
+
const cyclePct = Math.min(100, Math.round(cyclePrompts / limits.prompts5h * 100));
|
|
258
|
+
const cycleResetMs = Math.max(0, cycleResetAt - now);
|
|
259
|
+
// ─ Semanal por modelo: ventanas de 5 min con actividad ─
|
|
260
|
+
// Contamos ventanas de 5 min distintas con al menos 1 respuesta por modelo
|
|
261
|
+
const sonnetWindows = new Set();
|
|
262
|
+
const opusWindows = new Set();
|
|
263
|
+
const haikuWindows = new Set();
|
|
264
|
+
let weeklyTokensSonnet = 0;
|
|
265
|
+
let weeklyTokensOpus = 0;
|
|
266
|
+
let weeklyTokensHaiku = 0;
|
|
267
|
+
for (const e of entries) {
|
|
268
|
+
if (e.type !== 'assistant' || e.ts < weekStart)
|
|
269
|
+
continue;
|
|
270
|
+
const win = Math.floor(e.ts / WINDOW_5MIN) * WINDOW_5MIN;
|
|
271
|
+
const tokens = (e.inputTokens ?? 0) + (e.outputTokens ?? 0);
|
|
272
|
+
if (e.model?.includes('opus')) {
|
|
273
|
+
opusWindows.add(win);
|
|
274
|
+
weeklyTokensOpus += tokens;
|
|
275
|
+
}
|
|
276
|
+
else if (e.model?.includes('haiku')) {
|
|
277
|
+
haikuWindows.add(win);
|
|
278
|
+
weeklyTokensHaiku += tokens;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
sonnetWindows.add(win);
|
|
282
|
+
weeklyTokensSonnet += tokens;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const weeklyHoursSonnet = parseFloat((sonnetWindows.size * 5 / 60).toFixed(1));
|
|
286
|
+
const weeklyHoursOpus = parseFloat((opusWindows.size * 5 / 60).toFixed(1));
|
|
287
|
+
const weeklyHoursHaiku = parseFloat((haikuWindows.size * 5 / 60).toFixed(1));
|
|
288
|
+
// ─ Burn rate: tokens/min en los últimos 30 min ─
|
|
289
|
+
const recentAssistant = entries.filter(e => e.type === 'assistant' && e.ts >= thirtyMinAgo);
|
|
290
|
+
const totalRecentTok = recentAssistant.reduce((sum, e) => sum + (e.inputTokens ?? 0) + (e.outputTokens ?? 0), 0);
|
|
291
|
+
const burnRateTokensPerMin = recentAssistant.length > 0
|
|
292
|
+
? Math.round(totalRecentTok / 30)
|
|
293
|
+
: 0;
|
|
294
|
+
const data = {
|
|
295
|
+
cyclePrompts,
|
|
296
|
+
cycleLimit: limits.prompts5h,
|
|
297
|
+
cyclePct,
|
|
298
|
+
cycleResetMs,
|
|
299
|
+
cycleResetAt,
|
|
300
|
+
cycleStartTs: cycleStart,
|
|
301
|
+
weeklyHoursSonnet,
|
|
302
|
+
weeklyHoursOpus,
|
|
303
|
+
weeklyHoursHaiku,
|
|
304
|
+
weeklyTokensSonnet,
|
|
305
|
+
weeklyTokensOpus,
|
|
306
|
+
weeklyTokensHaiku,
|
|
307
|
+
weeklyLimitSonnet: limits.weeklyHoursSonnet,
|
|
308
|
+
weeklyLimitOpus: limits.weeklyHoursOpus,
|
|
309
|
+
burnRateTokensPerMin,
|
|
310
|
+
detectedPlan: plan,
|
|
311
|
+
planSource,
|
|
312
|
+
computedAt: now,
|
|
313
|
+
};
|
|
314
|
+
cache = { data, ts: now };
|
|
315
|
+
return data;
|
|
316
|
+
}
|
|
317
|
+
/** Invalida la caché (llamar cuando llega un nuevo evento para que el siguiente /quota sea fresco) */
|
|
318
|
+
function invalidateQuotaCache() {
|
|
319
|
+
cache = null;
|
|
320
|
+
}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render.ts — Renderizado del trace tree en terminal (Phase 2 visual upgrade)
|
|
3
|
+
*
|
|
4
|
+
* Mejoras sobre Phase 1:
|
|
5
|
+
* - Agrupa tool calls por bloque de respuesta (entre Stop events)
|
|
6
|
+
* - Detecta modo por bloque: Claude directo / Con agentes / Con skills
|
|
7
|
+
* - Barra visual de contexto (cuánto % del contexto está en uso)
|
|
8
|
+
* - Barra visual de eficiencia
|
|
9
|
+
* - Tokens en formato legible (K / M)
|
|
10
|
+
* - Badge de loop por línea de tool repetido
|
|
11
|
+
* - Coste y stats por bloque individual
|
|
12
|
+
*/
|
|
13
|
+
export interface TraceEvent {
|
|
14
|
+
type: string;
|
|
15
|
+
tool_name?: string;
|
|
16
|
+
tool_input?: string;
|
|
17
|
+
ts: number;
|
|
18
|
+
duration_ms?: number;
|
|
19
|
+
session_id?: string;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LoopAlert {
|
|
23
|
+
toolName: string;
|
|
24
|
+
count: number;
|
|
25
|
+
ts: number;
|
|
26
|
+
}
|
|
27
|
+
export interface CostInfo {
|
|
28
|
+
cost_usd: number;
|
|
29
|
+
input_tokens: number;
|
|
30
|
+
output_tokens: number;
|
|
31
|
+
cache_read: number;
|
|
32
|
+
cache_creation: number;
|
|
33
|
+
efficiency_score: number;
|
|
34
|
+
loops: LoopAlert[];
|
|
35
|
+
summary?: string;
|
|
36
|
+
context_used?: number;
|
|
37
|
+
context_window?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface WeeklyStats {
|
|
40
|
+
totalTokens: number;
|
|
41
|
+
byDay: {
|
|
42
|
+
date: string;
|
|
43
|
+
tokens: number;
|
|
44
|
+
}[];
|
|
45
|
+
lastUpdated: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface RenderState {
|
|
48
|
+
sessionId: string;
|
|
49
|
+
cwd: string;
|
|
50
|
+
startedAt: number;
|
|
51
|
+
events: TraceEvent[];
|
|
52
|
+
cost?: CostInfo;
|
|
53
|
+
weekly?: WeeklyStats;
|
|
54
|
+
}
|
|
55
|
+
export declare function renderTrace(state: RenderState): string;
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* render.ts — Renderizado del trace tree en terminal (Phase 2 visual upgrade)
|
|
4
|
+
*
|
|
5
|
+
* Mejoras sobre Phase 1:
|
|
6
|
+
* - Agrupa tool calls por bloque de respuesta (entre Stop events)
|
|
7
|
+
* - Detecta modo por bloque: Claude directo / Con agentes / Con skills
|
|
8
|
+
* - Barra visual de contexto (cuánto % del contexto está en uso)
|
|
9
|
+
* - Barra visual de eficiencia
|
|
10
|
+
* - Tokens en formato legible (K / M)
|
|
11
|
+
* - Badge de loop por línea de tool repetido
|
|
12
|
+
* - Coste y stats por bloque individual
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.renderTrace = renderTrace;
|
|
16
|
+
const TOOL_ICONS = {
|
|
17
|
+
Read: '📖', Write: '✏️', Edit: '✏️', Bash: '🖥️',
|
|
18
|
+
Glob: '🔍', Grep: '🔎', WebSearch: '🌐', WebFetch: '🌐',
|
|
19
|
+
Agent: '🤖', Skill: '⚡', TodoWrite: '📝', TodoRead: '📝',
|
|
20
|
+
Task: '📋', default: '🔧'
|
|
21
|
+
};
|
|
22
|
+
const C = {
|
|
23
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
24
|
+
green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m',
|
|
25
|
+
cyan: '\x1b[36m', red: '\x1b[31m', gray: '\x1b[90m',
|
|
26
|
+
bgRed: '\x1b[41m',
|
|
27
|
+
};
|
|
28
|
+
// ─── Helpers de formato ───────────────────────────────────────────────────────
|
|
29
|
+
function relTs(base, ts) {
|
|
30
|
+
const diff = ts - base;
|
|
31
|
+
const s = Math.floor(diff / 1000);
|
|
32
|
+
const ms = diff % 1000;
|
|
33
|
+
return `${String(s).padStart(2, '0')}:${String(ms).padStart(3, '0')}`;
|
|
34
|
+
}
|
|
35
|
+
function fmtMs(ms) {
|
|
36
|
+
if (!ms)
|
|
37
|
+
return '';
|
|
38
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
39
|
+
}
|
|
40
|
+
// Formatea tokens en K o M para legibilidad
|
|
41
|
+
function fmtTok(n) {
|
|
42
|
+
if (n >= 1000000)
|
|
43
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
44
|
+
if (n >= 1000)
|
|
45
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
46
|
+
return String(n);
|
|
47
|
+
}
|
|
48
|
+
function trunc(s, n = 48) {
|
|
49
|
+
return s.length > n ? s.slice(0, n - 3) + '...' : s;
|
|
50
|
+
}
|
|
51
|
+
function detail(toolName, rawInput) {
|
|
52
|
+
if (!toolName || !rawInput)
|
|
53
|
+
return '';
|
|
54
|
+
try {
|
|
55
|
+
const inp = JSON.parse(rawInput);
|
|
56
|
+
if (['Read', 'Write', 'Edit'].includes(toolName))
|
|
57
|
+
return trunc(inp.file_path || inp.path || '');
|
|
58
|
+
if (toolName === 'Bash')
|
|
59
|
+
return trunc(inp.command || '');
|
|
60
|
+
if (['Glob', 'Grep'].includes(toolName))
|
|
61
|
+
return trunc(inp.pattern || inp.query || '');
|
|
62
|
+
if (['WebSearch', 'WebFetch'].includes(toolName))
|
|
63
|
+
return trunc(inp.query || inp.url || '');
|
|
64
|
+
if (toolName === 'Agent')
|
|
65
|
+
return trunc((inp.prompt || '').slice(0, 45));
|
|
66
|
+
if (toolName === 'Skill')
|
|
67
|
+
return trunc(inp.skill || inp.name || '');
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
// Barra de progreso visual: ████████░░░░ 63%
|
|
73
|
+
function progressBar(pct, width = 18, color = C.cyan) {
|
|
74
|
+
const clamped = Math.max(0, Math.min(100, pct));
|
|
75
|
+
const filled = Math.round(clamped / 100 * width);
|
|
76
|
+
const empty = width - filled;
|
|
77
|
+
return `${color}${'█'.repeat(filled)}${C.dim}${'░'.repeat(empty)}${C.reset}`;
|
|
78
|
+
}
|
|
79
|
+
function detectMode(tools) {
|
|
80
|
+
const hasAgent = tools.some(e => e.tool_name === 'Agent');
|
|
81
|
+
const hasSkill = tools.some(e => e.tool_name === 'Skill');
|
|
82
|
+
if (hasAgent && hasSkill)
|
|
83
|
+
return 'agentes+skills';
|
|
84
|
+
if (hasAgent)
|
|
85
|
+
return 'agentes';
|
|
86
|
+
if (hasSkill)
|
|
87
|
+
return 'skills';
|
|
88
|
+
return 'directo';
|
|
89
|
+
}
|
|
90
|
+
function modeLabel(mode) {
|
|
91
|
+
const labels = {
|
|
92
|
+
'directo': `${C.dim}Claude directo${C.reset}`,
|
|
93
|
+
'agentes': `${C.yellow}🤖 Con agentes${C.reset}`,
|
|
94
|
+
'skills': `${C.cyan}⚡ Con skills${C.reset}`,
|
|
95
|
+
'agentes+skills': `${C.yellow}🤖⚡ Agentes + skills${C.reset}`,
|
|
96
|
+
};
|
|
97
|
+
return labels[mode];
|
|
98
|
+
}
|
|
99
|
+
function groupByResponse(events) {
|
|
100
|
+
const blocks = [];
|
|
101
|
+
let current = { index: 1, tools: [], mode: 'directo' };
|
|
102
|
+
for (const ev of events) {
|
|
103
|
+
if (ev.type === 'SessionStart')
|
|
104
|
+
continue;
|
|
105
|
+
if (ev.type === 'Stop') {
|
|
106
|
+
current.stop = ev;
|
|
107
|
+
current.mode = detectMode(current.tools);
|
|
108
|
+
blocks.push(current);
|
|
109
|
+
current = { index: blocks.length + 1, tools: [], mode: 'directo' };
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
current.tools.push(ev);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Bloque en curso (sin Stop todavía — Claude está respondiendo)
|
|
116
|
+
if (current.tools.length > 0) {
|
|
117
|
+
current.mode = detectMode(current.tools);
|
|
118
|
+
blocks.push(current);
|
|
119
|
+
}
|
|
120
|
+
return blocks;
|
|
121
|
+
}
|
|
122
|
+
// ─── Render principal ─────────────────────────────────────────────────────────
|
|
123
|
+
function renderTrace(state) {
|
|
124
|
+
const { sessionId, cwd, startedAt, events, cost } = state;
|
|
125
|
+
const lines = [];
|
|
126
|
+
// ── Header de sesión ──────────────────────────────────────────────────────
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`${C.bold}● claudestat${C.reset} ` +
|
|
129
|
+
`${C.dim}session:${C.reset} ${C.cyan}${sessionId.slice(0, 8)}${C.reset} ` +
|
|
130
|
+
`${C.dim}dir:${C.reset} ${C.blue}${cwd || '—'}${C.reset}`);
|
|
131
|
+
// ── Barra de contexto ─────────────────────────────────────────────────────
|
|
132
|
+
if (cost?.context_used && cost.context_window) {
|
|
133
|
+
const pct = Math.round(cost.context_used / cost.context_window * 100);
|
|
134
|
+
const barColor = pct > 80 ? C.red : pct > 60 ? C.yellow : C.green;
|
|
135
|
+
const bar = progressBar(pct, 24, barColor);
|
|
136
|
+
const remaining = 100 - pct;
|
|
137
|
+
lines.push(` ${C.dim}auto-compact en:${C.reset} ${bar} ` +
|
|
138
|
+
`${barColor}${remaining}% restante${C.reset} ` +
|
|
139
|
+
`${C.dim}${fmtTok(cost.context_used)} / ${fmtTok(cost.context_window)} tokens usados${C.reset}`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
lines.push(` ${C.dim}contexto: calculando...${C.reset}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(C.dim + '─'.repeat(72) + C.reset);
|
|
145
|
+
// ── Bloques de respuesta ──────────────────────────────────────────────────
|
|
146
|
+
const blocks = groupByResponse(events);
|
|
147
|
+
for (const block of blocks) {
|
|
148
|
+
const isLast = block.index === blocks.length;
|
|
149
|
+
const inProgress = !block.stop && isLast;
|
|
150
|
+
// Contar repeticiones de cada tool en este bloque (para badge de loop)
|
|
151
|
+
const toolCount = new Map();
|
|
152
|
+
for (const t of block.tools) {
|
|
153
|
+
if (t.tool_name)
|
|
154
|
+
toolCount.set(t.tool_name, (toolCount.get(t.tool_name) || 0) + 1);
|
|
155
|
+
}
|
|
156
|
+
const isLooping = (name) => (toolCount.get(name) || 0) >= 3;
|
|
157
|
+
// Cabecera del bloque
|
|
158
|
+
const blockHeader = inProgress
|
|
159
|
+
? `${C.yellow}⟳ Respuesta #${block.index}${C.reset}`
|
|
160
|
+
: `${C.dim}Respuesta #${block.index}${C.reset}`;
|
|
161
|
+
lines.push(` ${blockHeader} ${modeLabel(block.mode)}`);
|
|
162
|
+
// Tool calls del bloque
|
|
163
|
+
for (const ev of block.tools) {
|
|
164
|
+
const ts = `${C.gray}[${relTs(startedAt, ev.ts)}]${C.reset}`;
|
|
165
|
+
const det = detail(ev.tool_name, ev.tool_input);
|
|
166
|
+
const ico = TOOL_ICONS[ev.tool_name || ''] || TOOL_ICONS.default;
|
|
167
|
+
const loopBadge = ev.tool_name && isLooping(ev.tool_name)
|
|
168
|
+
? ` ${C.red}⚠ loop${C.reset}` : '';
|
|
169
|
+
if (ev.type === 'PreToolUse') {
|
|
170
|
+
lines.push(` ${ts} ${ico} ${C.yellow}${ev.tool_name}${C.reset}` +
|
|
171
|
+
(det ? ` ${C.dim}${det}${C.reset}` : '') +
|
|
172
|
+
` ${C.dim}⟳${C.reset}${loopBadge}`);
|
|
173
|
+
}
|
|
174
|
+
else if (ev.type === 'Done') {
|
|
175
|
+
lines.push(` ${ts} ${ico} ${C.green}${ev.tool_name}${C.reset}` +
|
|
176
|
+
(det ? ` ${C.dim}${det}${C.reset}` : '') +
|
|
177
|
+
(ev.duration_ms ? ` ${C.dim}(${fmtMs(ev.duration_ms)})${C.reset}` : '') +
|
|
178
|
+
loopBadge);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Pie del bloque (solo si terminó)
|
|
182
|
+
if (block.stop) {
|
|
183
|
+
const toolsDone = block.tools.filter(e => e.type === 'Done').length;
|
|
184
|
+
const elapsed = fmtMs(block.stop.ts - (block.tools[0]?.ts ?? block.stop.ts));
|
|
185
|
+
lines.push(` ${C.dim}└─ ✅ ${toolsDone} tools · ${elapsed}${C.reset}`);
|
|
186
|
+
}
|
|
187
|
+
lines.push('');
|
|
188
|
+
}
|
|
189
|
+
// ── Footer global ─────────────────────────────────────────────────────────
|
|
190
|
+
lines.push(C.dim + '─'.repeat(72) + C.reset);
|
|
191
|
+
if (cost && cost.cost_usd > 0) {
|
|
192
|
+
// Alertas de loops
|
|
193
|
+
if (cost.loops?.length) {
|
|
194
|
+
for (const loop of cost.loops) {
|
|
195
|
+
lines.push(` ${C.red}⚠ Loop: ${loop.toolName} x${loop.count} en 60s${C.reset}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Barra de eficiencia — si score es 0 y cost es muy bajo, aún no calculó
|
|
199
|
+
const score = (cost.efficiency_score === 0 && cost.cost_usd < 0.001) ? 100 : cost.efficiency_score;
|
|
200
|
+
const scoreColor = score >= 90 ? C.green : score >= 70 ? C.yellow : C.red;
|
|
201
|
+
const scoreBar = progressBar(score, 14, scoreColor);
|
|
202
|
+
// Tokens
|
|
203
|
+
const tokenLine = `${C.dim}↑${C.reset}${fmtTok(cost.input_tokens)} ` +
|
|
204
|
+
`${C.dim}↓${C.reset}${fmtTok(cost.output_tokens)} ` +
|
|
205
|
+
`${C.dim}🗄${C.reset}${fmtTok(cost.cache_read)}`;
|
|
206
|
+
lines.push(` ${C.bold}💰 $${cost.cost_usd.toFixed(4)}${C.reset} ` +
|
|
207
|
+
`${tokenLine} ` +
|
|
208
|
+
`eficiencia: ${scoreBar} ${scoreColor}${score}/100${C.reset}`);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const totalDone = events.filter(e => e.type === 'Done').length;
|
|
212
|
+
const elapsed = fmtMs((events.at(-1)?.ts ?? startedAt) - startedAt);
|
|
213
|
+
lines.push(` ${C.dim}⏱ ${elapsed} ✅ ${totalDone} tools 💰 calculando...${C.reset}`);
|
|
214
|
+
}
|
|
215
|
+
// ── Barra semanal (stats-cache.json) ──────────────────────────────────────
|
|
216
|
+
if (state.weekly && state.weekly.totalTokens > 0) {
|
|
217
|
+
const { totalTokens, byDay, lastUpdated } = state.weekly;
|
|
218
|
+
// Mini sparkline: un char por día de la semana
|
|
219
|
+
const maxDay = Math.max(...byDay.map(d => d.tokens), 1);
|
|
220
|
+
const BARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
221
|
+
const spark = byDay.map(d => BARS[Math.min(7, Math.floor(d.tokens / maxDay * 7))]).join('');
|
|
222
|
+
const padded = spark.padStart(7, '▁'); // garantizar 7 chars (1 por día)
|
|
223
|
+
lines.push(` ${C.dim}semanal:${C.reset} ${C.cyan}${padded}${C.reset} ` +
|
|
224
|
+
`${C.bold}${fmtTok(totalTokens)} tokens${C.reset} ` +
|
|
225
|
+
`${C.dim}(últimos 7 días${lastUpdated ? ' · datos al ' + lastUpdated : ''})${C.reset}`);
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
return lines.join('\n');
|
|
229
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { processLatestForSession, type CostUpdateCallback, type CompactDetectedCallback } from '../enricher';
|
|
2
|
+
export declare const eventsRouter: import("express-serve-static-core").Router;
|
|
3
|
+
export declare const lastAgentByCwd: Map<string, {
|
|
4
|
+
pre_ts: number;
|
|
5
|
+
session_id: string;
|
|
6
|
+
}>;
|
|
7
|
+
export declare const taggedSessionParents: Set<string>;
|
|
8
|
+
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
9
|
+
export declare function findProjectCwdForFile(filePath: string): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Cuando el enricher detecta nuevos tokens en un JSONL:
|
|
12
|
+
* 1. Corre el análisis de inteligencia
|
|
13
|
+
* 2. Guarda el coste + score en DB
|
|
14
|
+
* 3. Hace broadcast vía SSE para que el watch muestre el coste actualizado
|
|
15
|
+
*/
|
|
16
|
+
export declare const onCostUpdate: CostUpdateCallback;
|
|
17
|
+
export declare const onCompactDetected: CompactDetectedCallback;
|
|
18
|
+
export { processLatestForSession };
|