@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,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── POST /event — recibe eventos de los hooks de Claude Code ─────────────────
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.processLatestForSession = exports.onCompactDetected = exports.onCostUpdate = exports.taggedSessionParents = exports.lastAgentByCwd = exports.eventsRouter = void 0;
|
|
8
|
+
exports.findProjectCwdForFile = findProjectCwdForFile;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const express_1 = require("express");
|
|
12
|
+
const db_1 = require("../db");
|
|
13
|
+
const intelligence_1 = require("../intelligence");
|
|
14
|
+
const summarizer_1 = require("../summarizer");
|
|
15
|
+
const session_state_1 = require("../session-state");
|
|
16
|
+
const quota_tracker_1 = require("../quota-tracker");
|
|
17
|
+
const config_1 = require("../config");
|
|
18
|
+
const rate_limiter_1 = require("../middleware/rate-limiter");
|
|
19
|
+
const stream_1 = require("./stream");
|
|
20
|
+
const enricher_1 = require("../enricher");
|
|
21
|
+
Object.defineProperty(exports, "processLatestForSession", { enumerable: true, get: function () { return enricher_1.processLatestForSession; } });
|
|
22
|
+
exports.eventsRouter = (0, express_1.Router)();
|
|
23
|
+
// Skill activa por sesión — se setea tras Skill Done, se limpia en Stop.
|
|
24
|
+
// Permite taggear los eventos siguientes con skill_parent para agruparlos en la UI.
|
|
25
|
+
const activeSkillBySession = new Map();
|
|
26
|
+
// Último Agent PreToolUse por CWD — se usa para detectar sub-sesiones de agentes.
|
|
27
|
+
// Clave: cwd Valor: { pre_ts, session_id }
|
|
28
|
+
exports.lastAgentByCwd = new Map();
|
|
29
|
+
// Sesiones ya evaluadas para taggeo de parent — evita re-evaluar en cada cost update.
|
|
30
|
+
exports.taggedSessionParents = new Set();
|
|
31
|
+
// ─── Quota alerter: moving average de 3 muestras + cooldown 1h por nivel ─────
|
|
32
|
+
// Evita falsas alarmas por spikes puntuales. Solo emite si el promedio de las
|
|
33
|
+
// últimas 3 lecturas supera el threshold Y no se emitió ese nivel en la última hora.
|
|
34
|
+
const quotaSamples = []; // últimas 3 lecturas de cyclePct
|
|
35
|
+
const alertCooldown = new Map(); // nivel → timestamp del último aviso
|
|
36
|
+
const ALERT_COOLDOWN_MS = 60 * 60 * 1000; // 1 hora
|
|
37
|
+
const SAMPLES_NEEDED = 3; // muestras para el moving average
|
|
38
|
+
function shouldFireAlert(level, pct) {
|
|
39
|
+
// Añadir muestra al buffer circular (máx SAMPLES_NEEDED)
|
|
40
|
+
quotaSamples.push(pct);
|
|
41
|
+
if (quotaSamples.length > SAMPLES_NEEDED)
|
|
42
|
+
quotaSamples.shift();
|
|
43
|
+
// Necesitamos SAMPLES_NEEDED lecturas antes de disparar (evita alert en el primer spike)
|
|
44
|
+
if (quotaSamples.length < SAMPLES_NEEDED)
|
|
45
|
+
return false;
|
|
46
|
+
// Cooldown: no repetir el mismo nivel en la última hora
|
|
47
|
+
const lastFired = alertCooldown.get(level) ?? 0;
|
|
48
|
+
if (Date.now() - lastFired < ALERT_COOLDOWN_MS)
|
|
49
|
+
return false;
|
|
50
|
+
alertCooldown.set(level, Date.now());
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
54
|
+
function findProjectCwdForFile(filePath) {
|
|
55
|
+
let dir = path_1.default.dirname(filePath);
|
|
56
|
+
for (let i = 0; i < 6; i++) {
|
|
57
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'HANDOFF.md')))
|
|
58
|
+
return dir;
|
|
59
|
+
const parent = path_1.default.dirname(dir);
|
|
60
|
+
if (parent === dir)
|
|
61
|
+
break;
|
|
62
|
+
dir = parent;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
exports.eventsRouter.post('/event', (req, res) => {
|
|
67
|
+
const ip = req.ip ?? '127.0.0.1';
|
|
68
|
+
if ((0, rate_limiter_1.isRateLimited)(ip)) {
|
|
69
|
+
res.status(429).json({ error: 'Too many requests — wait 1 minute' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path } = req.body;
|
|
73
|
+
if (!session_id || !type) {
|
|
74
|
+
res.status(400).json({ error: 'Missing session_id or type' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const resolvedCwd = cwd
|
|
78
|
+
?? (transcript_path ? transcript_path.split('/').slice(0, -1).join('/') : undefined);
|
|
79
|
+
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts });
|
|
80
|
+
// Skill grouping: get current parent BEFORE processing this event
|
|
81
|
+
// (the Skill Done event itself is NOT tagged — only its subsequent sub-calls are)
|
|
82
|
+
const skillParent = (tool_name !== 'Skill' && type !== 'Stop')
|
|
83
|
+
? activeSkillBySession.get(session_id)
|
|
84
|
+
: undefined;
|
|
85
|
+
if (type === 'PostToolUse' && tool_name) {
|
|
86
|
+
const pairedId = db_1.dbOps.pairPostWithPre(session_id, tool_name, typeof tool_response === 'string' ? tool_response : JSON.stringify(tool_response ?? ''), ts);
|
|
87
|
+
// Truncar tool_response a 4000 chars para no saturar SSE con archivos grandes
|
|
88
|
+
const rawResp = typeof tool_response === 'string' ? tool_response : JSON.stringify(tool_response ?? '');
|
|
89
|
+
const tool_output = rawResp.length > 4000
|
|
90
|
+
? rawResp.slice(0, 4000) + `\n…[truncated: ${rawResp.length} chars]`
|
|
91
|
+
: rawResp;
|
|
92
|
+
(0, stream_1.broadcast)({ type: 'event', payload: { type: 'Done', session_id, tool_name, tool_input: tool_input != null ? JSON.stringify(tool_input) : undefined, tool_output, ts, pairedId, skill_parent: skillParent } });
|
|
93
|
+
// Activar skill parent para los eventos siguientes si este fue un Skill Done
|
|
94
|
+
if (tool_name === 'Skill') {
|
|
95
|
+
try {
|
|
96
|
+
const inp = typeof tool_input === 'object' ? tool_input : JSON.parse(tool_input ?? '{}');
|
|
97
|
+
activeSkillBySession.set(session_id, inp?.skill || inp?.name || 'skill');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
activeSkillBySession.set(session_id, 'skill');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
db_1.dbOps.insertEvent({
|
|
106
|
+
session_id, type,
|
|
107
|
+
tool_name: tool_name ?? undefined,
|
|
108
|
+
tool_input: tool_input ? JSON.stringify(tool_input) : undefined,
|
|
109
|
+
ts, cwd: resolvedCwd, skill_parent: skillParent
|
|
110
|
+
});
|
|
111
|
+
(0, stream_1.broadcast)({ type: 'event', payload: { ...req.body, skill_parent: skillParent } });
|
|
112
|
+
// Stop limpia el skill activo para esta sesión
|
|
113
|
+
if (type === 'Stop') {
|
|
114
|
+
activeSkillBySession.delete(session_id);
|
|
115
|
+
(0, enricher_1.cleanupSession)(session_id);
|
|
116
|
+
}
|
|
117
|
+
// Registrar Agent PreToolUse para detección de sub-sesiones
|
|
118
|
+
if (type === 'PreToolUse' && tool_name === 'Agent' && resolvedCwd) {
|
|
119
|
+
exports.lastAgentByCwd.set(resolvedCwd, { pre_ts: ts, session_id });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Intentar etiquetar la sesión con su proyecto (solo en eventos de herramientas de archivo)
|
|
123
|
+
const FILE_TOOLS = new Set(['Read', 'Write', 'Edit', 'Glob', 'Grep']);
|
|
124
|
+
if (FILE_TOOLS.has(tool_name || '') && tool_input) {
|
|
125
|
+
try {
|
|
126
|
+
const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : tool_input;
|
|
127
|
+
const filePath = (inp.file_path || inp.path);
|
|
128
|
+
if (filePath?.startsWith('/')) {
|
|
129
|
+
const projectCwd = findProjectCwdForFile(filePath);
|
|
130
|
+
if (projectCwd)
|
|
131
|
+
db_1.dbOps.updateSessionProject(session_id, projectCwd);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch { /* ignorar errores de parsing */ }
|
|
135
|
+
}
|
|
136
|
+
// Actualizar estado de sesión en memoria + broadcast state_change via SSE
|
|
137
|
+
stream_1.sessionLastEvent.set(session_id, { type, ts });
|
|
138
|
+
const state = (0, session_state_1.deriveSessionState)(type, ts);
|
|
139
|
+
const stateMeta = session_state_1.STATE_META[state];
|
|
140
|
+
(0, stream_1.broadcast)({ type: 'state_change', payload: { session_id, state, ...stateMeta } });
|
|
141
|
+
// Al terminar un turno (Stop) → invalidar quota cache + emitir warnings
|
|
142
|
+
if (type === 'Stop') {
|
|
143
|
+
(0, quota_tracker_1.invalidateQuotaCache)();
|
|
144
|
+
// Emitir warning SSE si la cuota supera algún threshold (moving avg 3 muestras + cooldown 1h)
|
|
145
|
+
setImmediate(() => {
|
|
146
|
+
try {
|
|
147
|
+
const cfg = (0, config_1.readConfig)();
|
|
148
|
+
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
149
|
+
const level = (0, config_1.getWarnLevel)(data.cyclePct, cfg.warnThresholds);
|
|
150
|
+
// shouldFireAlert acumula siempre la muestra; devuelve true solo si avg estable + cooldown ok
|
|
151
|
+
if (level && shouldFireAlert(level, data.cyclePct)) {
|
|
152
|
+
(0, stream_1.broadcast)({
|
|
153
|
+
type: 'quota_warning',
|
|
154
|
+
payload: {
|
|
155
|
+
level,
|
|
156
|
+
cyclePct: data.cyclePct,
|
|
157
|
+
cycleLimit: data.cycleLimit,
|
|
158
|
+
resetMs: data.cycleResetMs,
|
|
159
|
+
blocked: cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold,
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else if (!level) {
|
|
164
|
+
// Sin nivel activo → alimentar el buffer igualmente para que las próximas muestras sean correctas
|
|
165
|
+
quotaSamples.push(data.cyclePct);
|
|
166
|
+
if (quotaSamples.length > SAMPLES_NEEDED)
|
|
167
|
+
quotaSamples.shift();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { /* ignorar errores al calcular quota */ }
|
|
171
|
+
});
|
|
172
|
+
// Generar resumen IA solo si el usuario lo activa explícitamente
|
|
173
|
+
// Activar: CLAUDESTAT_AI_SUMMARY=true claudestat start
|
|
174
|
+
if (process.env.CLAUDESTAT_AI_SUMMARY === 'true') {
|
|
175
|
+
setImmediate(() => {
|
|
176
|
+
(async () => {
|
|
177
|
+
try {
|
|
178
|
+
const session = db_1.dbOps.getSession(session_id);
|
|
179
|
+
if (!session)
|
|
180
|
+
return;
|
|
181
|
+
const events = db_1.dbOps.getSessionEvents(session_id);
|
|
182
|
+
const projectName = session.project_path ? path_1.default.basename(session.project_path) : undefined;
|
|
183
|
+
const summary = await (0, summarizer_1.summarizeSession)(events, session.total_cost_usd ?? 0, projectName);
|
|
184
|
+
if (summary) {
|
|
185
|
+
db_1.dbOps.updateSessionSummary(session_id, summary);
|
|
186
|
+
(0, stream_1.broadcast)({ type: 'summary_ready', payload: { session_id, summary } });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error('[events] Summary error:', err);
|
|
191
|
+
}
|
|
192
|
+
})();
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
res.json({ ok: true });
|
|
197
|
+
});
|
|
198
|
+
// ─── Callback del enricher ────────────────────────────────────────────────────
|
|
199
|
+
/**
|
|
200
|
+
* Cuando el enricher detecta nuevos tokens en un JSONL:
|
|
201
|
+
* 1. Corre el análisis de inteligencia
|
|
202
|
+
* 2. Guarda el coste + score en DB
|
|
203
|
+
* 3. Hace broadcast vía SSE para que el watch muestre el coste actualizado
|
|
204
|
+
*/
|
|
205
|
+
const onCostUpdate = (sessionId, cost) => {
|
|
206
|
+
// Ensure session row exists — sub-agent JSONLs arrive from the enricher without a
|
|
207
|
+
// prior hook event (Claude Code does not fire hooks for sub-agent sessions).
|
|
208
|
+
let sessionRow = db_1.dbOps.getSession(sessionId);
|
|
209
|
+
if (!sessionRow) {
|
|
210
|
+
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.firstTs ?? Date.now() });
|
|
211
|
+
sessionRow = db_1.dbOps.getSession(sessionId);
|
|
212
|
+
}
|
|
213
|
+
// Sub-agent detection: first time we see a session, check if its firstTs falls after
|
|
214
|
+
// a recent Agent PreToolUse from another session in the same CWD → tag as child.
|
|
215
|
+
if (!exports.taggedSessionParents.has(sessionId) && cost.firstTs) {
|
|
216
|
+
exports.taggedSessionParents.add(sessionId);
|
|
217
|
+
const cwd = sessionRow?.cwd;
|
|
218
|
+
if (cwd) {
|
|
219
|
+
const agentInfo = exports.lastAgentByCwd.get(cwd);
|
|
220
|
+
if (agentInfo && agentInfo.session_id !== sessionId && agentInfo.pre_ts < cost.firstTs) {
|
|
221
|
+
db_1.dbOps.updateSessionParent(sessionId, agentInfo.session_id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const events = db_1.dbOps.getSessionEvents(sessionId);
|
|
226
|
+
const report = (0, intelligence_1.analyzeSession)(events, cost.cost_usd);
|
|
227
|
+
db_1.dbOps.updateSessionCost(sessionId, cost, report.efficiencyScore, report.loops.length);
|
|
228
|
+
const startedAt = sessionRow?.started_at ?? Date.now();
|
|
229
|
+
const sessionDurationMinutes = (Date.now() - startedAt) / 60000;
|
|
230
|
+
const projectedHourlyUsd = sessionDurationMinutes > 0.5
|
|
231
|
+
? cost.cost_usd / sessionDurationMinutes * 60
|
|
232
|
+
: 0;
|
|
233
|
+
(0, stream_1.broadcast)({
|
|
234
|
+
type: 'cost_update',
|
|
235
|
+
payload: {
|
|
236
|
+
session_id: sessionId,
|
|
237
|
+
cost_usd: cost.cost_usd,
|
|
238
|
+
input_tokens: cost.input_tokens,
|
|
239
|
+
output_tokens: cost.output_tokens,
|
|
240
|
+
cache_read: cost.cache_read,
|
|
241
|
+
cache_creation: cost.cache_creation,
|
|
242
|
+
context_used: cost.context_used,
|
|
243
|
+
context_window: cost.context_window,
|
|
244
|
+
efficiency_score: report.efficiencyScore,
|
|
245
|
+
loops: report.loops,
|
|
246
|
+
summary: report.summary,
|
|
247
|
+
model: cost.lastModel,
|
|
248
|
+
projected_hourly_usd: projectedHourlyUsd,
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// Emitir desglose de costo del último bloque (input vs output) para el TracePanel
|
|
252
|
+
if (cost.lastEntry) {
|
|
253
|
+
(0, stream_1.broadcast)({
|
|
254
|
+
type: 'block_cost',
|
|
255
|
+
payload: {
|
|
256
|
+
session_id: sessionId,
|
|
257
|
+
inputUsd: cost.lastEntry.inputUsd,
|
|
258
|
+
outputUsd: cost.lastEntry.outputUsd,
|
|
259
|
+
totalUsd: cost.lastEntry.totalUsd,
|
|
260
|
+
inputTokens: cost.lastEntry.inputTokens,
|
|
261
|
+
outputTokens: cost.lastEntry.outputTokens,
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
exports.onCostUpdate = onCostUpdate;
|
|
267
|
+
// ─── Callback de auto-compact ────────────────────────────────────────────────
|
|
268
|
+
const onCompactDetected = (sessionId) => {
|
|
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)}`);
|
|
271
|
+
};
|
|
272
|
+
exports.onCompactDetected = onCompactDetected;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const historyRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── GET /history — sesiones agrupadas por día ────────────────────────────────
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.historyRouter = void 0;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const express_1 = require("express");
|
|
10
|
+
const db_1 = require("../db");
|
|
11
|
+
const projects_cache_1 = require("../cache/projects-cache");
|
|
12
|
+
exports.historyRouter = (0, express_1.Router)();
|
|
13
|
+
exports.historyRouter.get('/history', (_req, res) => {
|
|
14
|
+
const sessions = db_1.dbOps.getRecentSessions(30);
|
|
15
|
+
// Agrupar por fecha local (YYYY-MM-DD)
|
|
16
|
+
const byDate = new Map();
|
|
17
|
+
for (const s of sessions) {
|
|
18
|
+
// toLocaleDateString('en-CA') produce YYYY-MM-DD en la zona horaria local del usuario
|
|
19
|
+
const date = new Date(s.started_at).toLocaleDateString('en-CA');
|
|
20
|
+
if (!byDate.has(date))
|
|
21
|
+
byDate.set(date, []);
|
|
22
|
+
// Detectar modo desde los contadores precalculados en la query
|
|
23
|
+
const hasAgent = (s.agent_count ?? 0) > 0;
|
|
24
|
+
const hasSkill = (s.skill_count ?? 0) > 0;
|
|
25
|
+
const mode = hasAgent && hasSkill ? 'agentes+skills'
|
|
26
|
+
: hasAgent ? 'agentes' : hasSkill ? 'skills' : 'directo';
|
|
27
|
+
// Git info cacheada para este proyecto
|
|
28
|
+
const gitInfo = s.project_path ? (0, projects_cache_1.getCachedGitInfo)(s.project_path) : null;
|
|
29
|
+
byDate.get(date).push({
|
|
30
|
+
id: s.id,
|
|
31
|
+
project_path: s.project_path ?? null,
|
|
32
|
+
project_name: s.project_path ? path_1.default.basename(s.project_path) : null,
|
|
33
|
+
started_at: s.started_at,
|
|
34
|
+
last_event_at: s.last_event_at ?? s.started_at,
|
|
35
|
+
duration_ms: (s.last_event_at ?? s.started_at) - s.started_at,
|
|
36
|
+
total_cost_usd: s.total_cost_usd ?? 0,
|
|
37
|
+
total_tokens: (s.total_input_tokens ?? 0) + (s.total_output_tokens ?? 0) + (s.total_cache_read ?? 0),
|
|
38
|
+
efficiency_score: s.efficiency_score ?? 100,
|
|
39
|
+
loops_detected: s.loops_detected ?? 0,
|
|
40
|
+
done_count: s.done_count ?? 0,
|
|
41
|
+
top_tools: s.top_tools_csv ? (() => { try {
|
|
42
|
+
return JSON.parse(s.top_tools_csv);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
} })() : [],
|
|
47
|
+
mode,
|
|
48
|
+
ai_summary: s.ai_summary ?? null,
|
|
49
|
+
git_branch: gitInfo?.branch ?? null,
|
|
50
|
+
git_dirty: gitInfo?.dirty ?? false,
|
|
51
|
+
git_ahead: gitInfo?.ahead ?? 0,
|
|
52
|
+
git_behind: gitInfo?.behind ?? 0,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const days = [...byDate.entries()]
|
|
56
|
+
.map(([date, sessions]) => ({
|
|
57
|
+
date,
|
|
58
|
+
sessions,
|
|
59
|
+
total_cost: sessions.reduce((s, x) => s + x.total_cost_usd, 0),
|
|
60
|
+
total_tokens: sessions.reduce((s, x) => s + x.total_tokens, 0),
|
|
61
|
+
total_duration_ms: sessions.reduce((s, x) => s + x.duration_ms, 0),
|
|
62
|
+
}))
|
|
63
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
64
|
+
res.json({ days });
|
|
65
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const miscRouter: import("express-serve-static-core").Router;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Rutas misceláneas: /git, /pr, /meta-stats, /intelligence, /quota,
|
|
3
|
+
// /kill-switch, /sessions, /prompts, /hidden-cost, /claude-stats,
|
|
4
|
+
// /system-config, /config ─────────────────────────────────────────────────
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.miscRouter = void 0;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const express_1 = require("express");
|
|
14
|
+
const db_1 = require("../db");
|
|
15
|
+
const intelligence_1 = require("../intelligence");
|
|
16
|
+
const meta_stats_1 = require("../meta-stats");
|
|
17
|
+
const quota_tracker_1 = require("../quota-tracker");
|
|
18
|
+
const config_1 = require("../config");
|
|
19
|
+
const enricher_1 = require("../enricher");
|
|
20
|
+
const claude_stats_1 = require("../claude-stats");
|
|
21
|
+
const projects_cache_1 = require("../cache/projects-cache");
|
|
22
|
+
const projects_1 = require("./projects");
|
|
23
|
+
const session_state_1 = require("../session-state");
|
|
24
|
+
const stream_1 = require("./stream");
|
|
25
|
+
const paths_1 = require("../paths");
|
|
26
|
+
exports.miscRouter = (0, express_1.Router)();
|
|
27
|
+
// ─── GET /git?path=... — git info para un proyecto ────────────────────────────
|
|
28
|
+
exports.miscRouter.get('/git', (req, res) => {
|
|
29
|
+
const projectPath = req.query.path;
|
|
30
|
+
if (!projectPath) {
|
|
31
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
res.json((0, projects_cache_1.getCachedGitInfo)(projectPath) ?? null);
|
|
35
|
+
});
|
|
36
|
+
// ─── GET /pr?path=... — estado del PR para un proyecto ────────────────────────
|
|
37
|
+
exports.miscRouter.get('/pr', (req, res) => {
|
|
38
|
+
const projectPath = req.query.path;
|
|
39
|
+
if (!projectPath) {
|
|
40
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
res.json((0, projects_cache_1.getCachedPRStatus)(projectPath) ?? null);
|
|
44
|
+
});
|
|
45
|
+
// ─── GET /meta-stats — KPIs de contexto ──────────────────────────────────────
|
|
46
|
+
exports.miscRouter.get('/meta-stats', (_req, res) => {
|
|
47
|
+
const latestSession = db_1.dbOps.getLatestSession();
|
|
48
|
+
const events = latestSession ? db_1.dbOps.getSessionEvents(latestSession.id) : [];
|
|
49
|
+
// Inferir el directorio del proyecto desde los eventos (más fiable que el cwd del daemon)
|
|
50
|
+
const projectCwd = (0, projects_1.inferProjectCwd)(events) ?? latestSession?.cwd ?? undefined;
|
|
51
|
+
const current = (0, meta_stats_1.computeMetaStats)(projectCwd);
|
|
52
|
+
const history = (0, meta_stats_1.getMetaHistory)();
|
|
53
|
+
res.json({ current, history });
|
|
54
|
+
});
|
|
55
|
+
// ─── GET /intelligence/:sessionId — reporte de inteligencia ──────────────────
|
|
56
|
+
exports.miscRouter.get('/intelligence/:sessionId', (req, res) => {
|
|
57
|
+
const { sessionId } = req.params;
|
|
58
|
+
const session = db_1.dbOps.getSession(sessionId);
|
|
59
|
+
if (!session) {
|
|
60
|
+
res.status(404).json({ error: 'Session not found' });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const events = db_1.dbOps.getSessionEvents(sessionId);
|
|
64
|
+
const report = (0, intelligence_1.analyzeSession)(events, session.total_cost_usd ?? 0);
|
|
65
|
+
res.json({ sessionId, ...report });
|
|
66
|
+
});
|
|
67
|
+
// ─── GET /quota — datos de cuota y burn rate ──────────────────────────────────
|
|
68
|
+
exports.miscRouter.get('/quota', (_req, res) => {
|
|
69
|
+
try {
|
|
70
|
+
const cfg = (0, config_1.readConfig)();
|
|
71
|
+
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
72
|
+
res.json(data);
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
res.status(500).json({ error: 'Error computing quota' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
/** Formatea ms a "Xh Ym" legible */
|
|
79
|
+
function formatMs(ms) {
|
|
80
|
+
const totalMin = Math.ceil(ms / 60000);
|
|
81
|
+
const h = Math.floor(totalMin / 60);
|
|
82
|
+
const m = totalMin % 60;
|
|
83
|
+
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
|
84
|
+
}
|
|
85
|
+
// ─── GET /kill-switch — consultado por el hook PreToolUse ─────────────────────
|
|
86
|
+
// Si está bloqueado, el hook hace exit(2) y Claude Code cancela la acción.
|
|
87
|
+
exports.miscRouter.get('/kill-switch', (_req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const cfg = (0, config_1.readConfig)();
|
|
90
|
+
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
91
|
+
const blocked = cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold;
|
|
92
|
+
const reason = blocked
|
|
93
|
+
? `5h quota at ${data.cyclePct}% (limit: ${cfg.killSwitchThreshold}%). Resets in ${formatMs(data.cycleResetMs)}.`
|
|
94
|
+
: undefined;
|
|
95
|
+
res.json({ blocked, reason, cyclePct: data.cyclePct });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
res.json({ blocked: false }); // si hay error, no bloquear
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// ─── GET /sessions — listado para dashboard futuro ────────────────────────────
|
|
102
|
+
exports.miscRouter.get('/sessions', (_req, res) => {
|
|
103
|
+
const sessions = db_1.dbOps.getAllSessions();
|
|
104
|
+
// Enriquecer cada sesión con el estado derivado en tiempo real
|
|
105
|
+
const enriched = sessions.map(s => {
|
|
106
|
+
const lastEvt = stream_1.sessionLastEvent.get(s.id);
|
|
107
|
+
const ts = lastEvt?.ts ?? s.last_event_at ?? s.started_at;
|
|
108
|
+
const state = (0, session_state_1.deriveSessionState)(lastEvt?.type, ts);
|
|
109
|
+
return { ...s, state };
|
|
110
|
+
});
|
|
111
|
+
res.json(enriched);
|
|
112
|
+
});
|
|
113
|
+
// ─── GET /prompts — mensajes del usuario para una sesión ─────────────────────
|
|
114
|
+
exports.miscRouter.get('/prompts', async (req, res) => {
|
|
115
|
+
const sessionId = req.query.session_id;
|
|
116
|
+
if (!sessionId)
|
|
117
|
+
return res.status(400).json({ error: 'session_id required' });
|
|
118
|
+
res.json({ prompts: await (0, enricher_1.getSessionPrompts)(sessionId) });
|
|
119
|
+
});
|
|
120
|
+
// ─── GET /hidden-cost — coste oculto en loops (últimos 7 días) ───────────────
|
|
121
|
+
exports.miscRouter.get('/hidden-cost', (_req, res) => {
|
|
122
|
+
res.json(db_1.dbOps.getHiddenCostStats(7));
|
|
123
|
+
});
|
|
124
|
+
// ─── GET /claude-stats — actividad de ~/.claude/stats-cache.json ─────────────
|
|
125
|
+
exports.miscRouter.get('/claude-stats', (_req, res) => {
|
|
126
|
+
res.json((0, claude_stats_1.readClaudeStats)());
|
|
127
|
+
});
|
|
128
|
+
// ─── GET /system-config — mapa completo del setup de Claude ──────────────────
|
|
129
|
+
let _systemConfigCache = null;
|
|
130
|
+
let _systemConfigCacheTs = 0;
|
|
131
|
+
const SYSTEM_CONFIG_TTL = 30000;
|
|
132
|
+
exports.miscRouter.get('/system-config', (_req, res) => {
|
|
133
|
+
if (_systemConfigCache && Date.now() - _systemConfigCacheTs < SYSTEM_CONFIG_TTL) {
|
|
134
|
+
res.json(_systemConfigCache);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const home = os_1.default.homedir();
|
|
139
|
+
const claudeDir = (0, paths_1.getClaudeDir)();
|
|
140
|
+
let hooks = {};
|
|
141
|
+
try {
|
|
142
|
+
const raw = fs_1.default.readFileSync(path_1.default.join(claudeDir, 'settings.json'), 'utf-8');
|
|
143
|
+
const rawHooks = JSON.parse(raw).hooks ?? {};
|
|
144
|
+
for (const [event, entries] of Object.entries(rawHooks)) {
|
|
145
|
+
hooks[event] = entries.flatMap(e => (e.hooks ?? []).map(h => ({ matcher: e.matcher, command: h.command })));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
// Helper para extraer descripción del frontmatter
|
|
150
|
+
const getDescription = (content) => content.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? '';
|
|
151
|
+
// Helper compartido — escanea archivos .md directos o anidados en subdirectorios
|
|
152
|
+
const scanMarkdownDir = (dir, excludes = [], nested) => {
|
|
153
|
+
const items = [];
|
|
154
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
155
|
+
let filePath;
|
|
156
|
+
let itemName;
|
|
157
|
+
if (nested) {
|
|
158
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
159
|
+
continue;
|
|
160
|
+
filePath = path_1.default.join(dir, entry.name, nested);
|
|
161
|
+
itemName = entry.name;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
if (!entry.isFile() || !entry.name.endsWith('.md') || excludes.includes(entry.name))
|
|
165
|
+
continue;
|
|
166
|
+
filePath = path_1.default.join(dir, entry.name);
|
|
167
|
+
itemName = entry.name.replace('.md', '');
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
171
|
+
const lines = content.split('\n').length;
|
|
172
|
+
items.push({ name: itemName, description: getDescription(content), lines });
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Ignorar archivos no encontrados o no legibles
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return items;
|
|
179
|
+
};
|
|
180
|
+
// 2. Agentes desde Claude Code agents/
|
|
181
|
+
let agents = [];
|
|
182
|
+
try {
|
|
183
|
+
agents = scanMarkdownDir(path_1.default.join(claudeDir, 'agents'), ['CLAUDE.md', 'ORCHESTRATOR.md', 'AGENTS.md']);
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
// 2b. Workflows desde Claude Code agents/workflows/
|
|
187
|
+
let workflows = [];
|
|
188
|
+
try {
|
|
189
|
+
workflows = scanMarkdownDir(path_1.default.join(claudeDir, 'agents', 'workflows'));
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
// 3. Archivos de contexto relevantes
|
|
193
|
+
const engramSlugCtx = (0, paths_1.getHomeSlug)();
|
|
194
|
+
const contextPaths = [
|
|
195
|
+
{ key: 'CLAUDE.md global', filePath: path_1.default.join(claudeDir, 'CLAUDE.md') },
|
|
196
|
+
{ key: 'MEMORY.md', filePath: path_1.default.join(claudeDir, 'projects', engramSlugCtx, 'memory', 'MEMORY.md') },
|
|
197
|
+
{ key: 'settings.json', filePath: path_1.default.join(claudeDir, 'settings.json') },
|
|
198
|
+
{ key: 'config claudestat', filePath: path_1.default.join((0, paths_1.getClaudestatDir)(), 'config.json') },
|
|
199
|
+
];
|
|
200
|
+
const contextFiles = contextPaths.map(({ key, filePath }) => {
|
|
201
|
+
try {
|
|
202
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
203
|
+
const lines = content.split('\n').length;
|
|
204
|
+
const sizeKb = Math.round(Buffer.byteLength(content, 'utf-8') / 1024 * 10) / 10;
|
|
205
|
+
return { key, exists: true, sizeKb, lines };
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return { key, exists: false, sizeKb: 0, lines: 0 };
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// 3b. Skills: commands/ (skills nativos de Claude Code) + skills/ (skills.sh)
|
|
212
|
+
let skills = [];
|
|
213
|
+
try {
|
|
214
|
+
skills = scanMarkdownDir(path_1.default.join(claudeDir, 'commands'));
|
|
215
|
+
}
|
|
216
|
+
catch { }
|
|
217
|
+
try {
|
|
218
|
+
skills = [...skills, ...scanMarkdownDir(path_1.default.join(claudeDir, 'skills'), [], 'SKILL.md')];
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
// 4. Archivos de memoria Engram
|
|
222
|
+
let memoryFiles = [];
|
|
223
|
+
try {
|
|
224
|
+
const memDir = path_1.default.join(claudeDir, 'projects', engramSlugCtx, 'memory');
|
|
225
|
+
memoryFiles = fs_1.default.readdirSync(memDir).filter(f => f.endsWith('.md')).sort();
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
// 5. Distribución de modos (últimos 7 días)
|
|
229
|
+
const modeDistribution = db_1.dbOps.getModeDistribution(7);
|
|
230
|
+
// 6. Config de claudestat
|
|
231
|
+
const claudestatConfig = (0, config_1.readConfig)();
|
|
232
|
+
_systemConfigCache = { hooks, agents, workflows, skills, contextFiles, memoryFiles, modeDistribution, claudestatConfig };
|
|
233
|
+
_systemConfigCacheTs = Date.now();
|
|
234
|
+
res.json(_systemConfigCache);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
res.status(500).json({ error: 'Error leyendo config del sistema' });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// ─── GET /config — leer configuración ────────────────────────────────────────
|
|
241
|
+
exports.miscRouter.get('/config', (_req, res) => {
|
|
242
|
+
res.json((0, config_1.readConfig)());
|
|
243
|
+
});
|
|
244
|
+
// ─── PUT /config — guardar configuración ─────────────────────────────────────
|
|
245
|
+
exports.miscRouter.put('/config', (req, res) => {
|
|
246
|
+
const validationError = (0, config_1.validateConfig)(req.body);
|
|
247
|
+
if (validationError) {
|
|
248
|
+
res.status(400).json({ error: validationError });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const current = (0, config_1.readConfig)();
|
|
253
|
+
(0, config_1.writeConfig)({ ...current, ...req.body });
|
|
254
|
+
res.json({ ok: true });
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
res.status(500).json({ error: String(e) });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// ─── GET /cost-projection — weekly/monthly cost projection ──────────────────
|
|
261
|
+
exports.miscRouter.get('/cost-projection', (_req, res) => {
|
|
262
|
+
const week = db_1.dbOps.getCostProjection(7);
|
|
263
|
+
const month = db_1.dbOps.getCostProjection(30);
|
|
264
|
+
const weekSpan = week.latest && week.earliest ? (week.latest - week.earliest) / 86400000 : 0;
|
|
265
|
+
const monthSpan = month.latest && month.earliest ? (month.latest - month.earliest) / 86400000 : 0;
|
|
266
|
+
const weeklyProjected = weekSpan > 0.5 ? (week.total_cost_usd ?? 0) / weekSpan * 7 : 0;
|
|
267
|
+
const monthlyProjected = monthSpan > 1 ? (month.total_cost_usd ?? 0) / monthSpan * 30 : 0;
|
|
268
|
+
res.json({
|
|
269
|
+
weekly: {
|
|
270
|
+
daysWithData: Math.round(weekSpan * 10) / 10,
|
|
271
|
+
costSoFar: week.total_cost_usd ?? 0,
|
|
272
|
+
projected: Math.round(weeklyProjected * 100) / 100,
|
|
273
|
+
},
|
|
274
|
+
monthly: {
|
|
275
|
+
daysWithData: Math.round(monthSpan * 10) / 10,
|
|
276
|
+
costSoFar: month.total_cost_usd ?? 0,
|
|
277
|
+
projected: Math.round(monthlyProjected * 100) / 100,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type EventRow } from '../db';
|
|
2
|
+
export declare const projectsRouter: import("express-serve-static-core").Router;
|
|
3
|
+
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
4
|
+
export declare function findProjectCwdForFile(filePath: string): string | undefined;
|
|
5
|
+
/** Infiere el proyecto activo mirando los eventos de archivo de una sesión */
|
|
6
|
+
export declare function inferProjectCwd(events: EventRow[]): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Determina el proyecto activo por mayoría de operaciones de archivo
|
|
9
|
+
* en una ventana de tiempo reciente. Evita que un único archivo tocado
|
|
10
|
+
* de otro proyecto cambie el badge.
|
|
11
|
+
*
|
|
12
|
+
* - Mínimo 2 hits en la ventana para declarar un proyecto como activo.
|
|
13
|
+
* - Si hay empate, gana el que tuvo actividad más reciente.
|
|
14
|
+
*/
|
|
15
|
+
export declare function inferActiveProjectByMajority(events: EventRow[], windowMs: number): string | undefined;
|