@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.
Files changed (80) hide show
  1. package/README.md +437 -0
  2. package/dashboard/dist/assets/AnalyticsView-BApcOGsD.js +8 -0
  3. package/dashboard/dist/assets/HistoryView-B331k5oL.js +1 -0
  4. package/dashboard/dist/assets/ProjectsView-DUleaXsP.js +6 -0
  5. package/dashboard/dist/assets/SystemView-BGe__vl1.js +1 -0
  6. package/dashboard/dist/assets/TopView-CXggyydU.js +1 -0
  7. package/dashboard/dist/assets/index-CB01c5lb.js +84 -0
  8. package/dashboard/dist/assets/vendor-lucide-Cym0q5l_.js +344 -0
  9. package/dashboard/dist/assets/vendor-react-B_Jzs0gY.js +24 -0
  10. package/dashboard/dist/index.html +21 -0
  11. package/dist/cache/projects-cache.d.ts +9 -0
  12. package/dist/cache/projects-cache.js +51 -0
  13. package/dist/claude-auth.d.ts +38 -0
  14. package/dist/claude-auth.js +133 -0
  15. package/dist/claude-stats.d.ts +32 -0
  16. package/dist/claude-stats.js +98 -0
  17. package/dist/config.d.ts +43 -0
  18. package/dist/config.js +110 -0
  19. package/dist/daemon.d.ts +15 -0
  20. package/dist/daemon.js +247 -0
  21. package/dist/db.d.ts +134 -0
  22. package/dist/db.js +546 -0
  23. package/dist/doctor.d.ts +1 -0
  24. package/dist/doctor.js +191 -0
  25. package/dist/enricher.d.ts +34 -0
  26. package/dist/enricher.js +394 -0
  27. package/dist/export.d.ts +8 -0
  28. package/dist/export.js +82 -0
  29. package/dist/git.d.ts +22 -0
  30. package/dist/git.js +57 -0
  31. package/dist/github.d.ts +27 -0
  32. package/dist/github.js +62 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.js +319 -0
  35. package/dist/install.d.ts +14 -0
  36. package/dist/install.js +202 -0
  37. package/dist/intelligence.d.ts +45 -0
  38. package/dist/intelligence.js +105 -0
  39. package/dist/meta-stats.d.ts +28 -0
  40. package/dist/meta-stats.js +137 -0
  41. package/dist/middleware/rate-limiter.d.ts +2 -0
  42. package/dist/middleware/rate-limiter.js +30 -0
  43. package/dist/notifier.d.ts +1 -0
  44. package/dist/notifier.js +22 -0
  45. package/dist/paths.d.ts +79 -0
  46. package/dist/paths.js +134 -0
  47. package/dist/pattern-analyzer.d.ts +35 -0
  48. package/dist/pattern-analyzer.js +123 -0
  49. package/dist/project-scanner.d.ts +71 -0
  50. package/dist/project-scanner.js +619 -0
  51. package/dist/quota-tracker.d.ts +45 -0
  52. package/dist/quota-tracker.js +320 -0
  53. package/dist/render.d.ts +55 -0
  54. package/dist/render.js +229 -0
  55. package/dist/routes/events.d.ts +18 -0
  56. package/dist/routes/events.js +272 -0
  57. package/dist/routes/history.d.ts +1 -0
  58. package/dist/routes/history.js +65 -0
  59. package/dist/routes/misc.d.ts +1 -0
  60. package/dist/routes/misc.js +280 -0
  61. package/dist/routes/projects.d.ts +15 -0
  62. package/dist/routes/projects.js +153 -0
  63. package/dist/routes/reports.d.ts +11 -0
  64. package/dist/routes/reports.js +205 -0
  65. package/dist/routes/stream.d.ts +8 -0
  66. package/dist/routes/stream.js +70 -0
  67. package/dist/routes/top.d.ts +1 -0
  68. package/dist/routes/top.js +30 -0
  69. package/dist/session-state.d.ts +35 -0
  70. package/dist/session-state.js +50 -0
  71. package/dist/summarizer.d.ts +18 -0
  72. package/dist/summarizer.js +137 -0
  73. package/dist/watch.d.ts +8 -0
  74. package/dist/watch.js +157 -0
  75. package/dist/watchdog.d.ts +11 -0
  76. package/dist/watchdog.js +75 -0
  77. package/dist/weekly.d.ts +13 -0
  78. package/dist/weekly.js +39 -0
  79. package/hooks/event.js +80 -0
  80. package/package.json +78 -0
package/dist/db.d.ts ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * db.ts — Capa de acceso a SQLite (node:sqlite)
3
+ *
4
+ * Por qué node:sqlite sobre better-sqlite3:
5
+ * - Integrado en Node 22+, sin compilación nativa
6
+ * - Cross-platform sin configuración extra
7
+ * - API síncrona igual de rápida para uso local
8
+ *
9
+ * El warning "ExperimentalWarning" se suprime en index.ts.
10
+ */
11
+ export declare const CLAUDESTAT_DIR: string;
12
+ export interface SessionRow {
13
+ id: string;
14
+ cwd?: string;
15
+ project_path?: string;
16
+ started_at: number;
17
+ last_event_at?: number;
18
+ total_cost_usd?: number;
19
+ total_input_tokens?: number;
20
+ total_output_tokens?: number;
21
+ total_cache_read?: number;
22
+ total_cache_creation?: number;
23
+ efficiency_score?: number;
24
+ loops_detected?: number;
25
+ ai_summary?: string;
26
+ }
27
+ export interface EventRow {
28
+ id?: number;
29
+ session_id: string;
30
+ type: string;
31
+ tool_name?: string;
32
+ tool_input?: string;
33
+ tool_response?: string;
34
+ ts: number;
35
+ cwd?: string;
36
+ duration_ms?: number;
37
+ skill_parent?: string;
38
+ }
39
+ export interface BlockCostEntry {
40
+ inputUsd: number;
41
+ outputUsd: number;
42
+ totalUsd: number;
43
+ inputTokens: number;
44
+ outputTokens: number;
45
+ }
46
+ export interface CostUpdate {
47
+ input_tokens: number;
48
+ output_tokens: number;
49
+ cache_read: number;
50
+ cache_creation: number;
51
+ cost_usd: number;
52
+ context_used: number;
53
+ context_window: number;
54
+ lastEntry?: BlockCostEntry;
55
+ lastModel?: string;
56
+ firstTs?: number;
57
+ }
58
+ export declare const dbOps: {
59
+ upsertSession(s: SessionRow): void;
60
+ insertEvent(e: EventRow): number;
61
+ /**
62
+ * Al llegar PostToolUse, actualizamos el PreToolUse pendiente más reciente
63
+ * del mismo tool para esta sesión. Esto convierte el par Pre+Post en
64
+ * un único registro de tipo 'Done' con duration_ms calculado.
65
+ */
66
+ pairPostWithPre(sessionId: string, toolName: string, response: string, postTs: number): number | null;
67
+ updateSessionCost(sessionId: string, cost: CostUpdate, efficiencyScore: number, loopsDetected: number): void;
68
+ getSessionEvents(sessionId: string): EventRow[];
69
+ getSession(sessionId: string): SessionRow | undefined;
70
+ getLatestSession(): SessionRow | undefined;
71
+ getAllSessions(limit?: number): SessionRow[];
72
+ getSessionEventsRecent(sessionId: string, limit?: number): EventRow[];
73
+ updateSessionProject(sessionId: string, projectPath: string): void;
74
+ getRecentSessions(days: number): any[];
75
+ getProjectAggregates(): any[];
76
+ getProjectToolCounts(projectPath: string): {
77
+ tool_name: string;
78
+ count: number;
79
+ }[];
80
+ getProjectSessionStats(projectPath: string): any;
81
+ updateSessionSummary(sessionId: string, summary: string): void;
82
+ updateSessionParent(sessionId: string, parentId: string): void;
83
+ getChildSessions(parentSessionId: string): {
84
+ id: string;
85
+ dominant_model?: string;
86
+ total_cost_usd?: number;
87
+ started_at: number;
88
+ }[];
89
+ getHiddenCostStats(days: number): {
90
+ loop_waste_usd: number;
91
+ total_cost_usd: number;
92
+ loop_sessions: number;
93
+ total_loops: number;
94
+ total_sessions: number;
95
+ };
96
+ insertWeeklyReport(date: string, markdown: string): void;
97
+ deleteWeeklyReport(date: string): void;
98
+ listWeeklyReports(): {
99
+ id: number;
100
+ date: string;
101
+ preview: string;
102
+ created_at: string;
103
+ }[];
104
+ getWeeklyReportByDate(date: string): {
105
+ id: number;
106
+ date: string;
107
+ report_markdown: string;
108
+ created_at: string;
109
+ } | undefined;
110
+ getQuotaStats(since: number): Array<{
111
+ total_tokens: number;
112
+ total_cost_usd: number;
113
+ }>;
114
+ getModeDistribution(days: number): {
115
+ direct: number;
116
+ mini: number;
117
+ pipeline: number;
118
+ total: number;
119
+ };
120
+ getAnalyticsDaily(since: number): any[];
121
+ getAnalyticsByModel(since: number): any[];
122
+ getProjectHours(since: number): any[];
123
+ getTopTools(days?: number, by?: "cost" | "count" | "duration", limit?: number): {
124
+ tool_name: string;
125
+ count: number;
126
+ total_duration_ms: number;
127
+ total_cost_usd: number;
128
+ }[];
129
+ getCostProjection(days?: number): {
130
+ total_cost_usd: number;
131
+ earliest: number;
132
+ latest: number;
133
+ };
134
+ };
package/dist/db.js ADDED
@@ -0,0 +1,546 @@
1
+ "use strict";
2
+ /**
3
+ * db.ts — Capa de acceso a SQLite (node:sqlite)
4
+ *
5
+ * Por qué node:sqlite sobre better-sqlite3:
6
+ * - Integrado en Node 22+, sin compilación nativa
7
+ * - Cross-platform sin configuración extra
8
+ * - API síncrona igual de rápida para uso local
9
+ *
10
+ * El warning "ExperimentalWarning" se suprime en index.ts.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.dbOps = exports.CLAUDESTAT_DIR = void 0;
17
+ const node_sqlite_1 = require("node:sqlite");
18
+ const path_1 = __importDefault(require("path"));
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const paths_1 = require("./paths");
21
+ exports.CLAUDESTAT_DIR = (0, paths_1.getClaudestatDir)();
22
+ const DB_PATH = process.env.CLAUDESTAT_DB_PATH ?? path_1.default.join(exports.CLAUDESTAT_DIR, 'events.db');
23
+ fs_1.default.mkdirSync(exports.CLAUDESTAT_DIR, { recursive: true });
24
+ const db = new node_sqlite_1.DatabaseSync(DB_PATH);
25
+ // Migraciones: añadir columnas nuevas sin romper instalaciones previas
26
+ try {
27
+ db.exec(`ALTER TABLE sessions ADD COLUMN project_path TEXT`);
28
+ }
29
+ catch { /* ya existe */ }
30
+ try {
31
+ db.exec(`ALTER TABLE sessions ADD COLUMN ai_summary TEXT`);
32
+ }
33
+ catch { /* ya existe */ }
34
+ try {
35
+ db.exec(`
36
+ CREATE TABLE IF NOT EXISTS weekly_reports (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ date TEXT NOT NULL UNIQUE,
39
+ report_markdown TEXT NOT NULL,
40
+ created_at TEXT DEFAULT (datetime('now'))
41
+ )
42
+ `);
43
+ }
44
+ catch { /* ya existe */ }
45
+ db.exec(`
46
+ CREATE TABLE IF NOT EXISTS sessions (
47
+ id TEXT PRIMARY KEY,
48
+ cwd TEXT,
49
+ project_path TEXT,
50
+ started_at INTEGER NOT NULL,
51
+ last_event_at INTEGER,
52
+ total_cost_usd REAL DEFAULT 0,
53
+ total_input_tokens INTEGER DEFAULT 0,
54
+ total_output_tokens INTEGER DEFAULT 0,
55
+ total_cache_read INTEGER DEFAULT 0,
56
+ total_cache_creation INTEGER DEFAULT 0,
57
+ efficiency_score INTEGER DEFAULT 100,
58
+ loops_detected INTEGER DEFAULT 0,
59
+ ai_summary TEXT
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS events (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
64
+ session_id TEXT NOT NULL,
65
+ type TEXT NOT NULL,
66
+ tool_name TEXT,
67
+ tool_input TEXT,
68
+ tool_response TEXT,
69
+ ts INTEGER NOT NULL,
70
+ cwd TEXT,
71
+ duration_ms INTEGER,
72
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
73
+ );
74
+ `);
75
+ // Índices para acelerar las subqueries de getRecentSessions (N+3 pattern)
76
+ // Wrapped en try-catch para no romper instalaciones que ya los tienen
77
+ try {
78
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_session_type ON events(session_id, type)`);
79
+ }
80
+ catch { }
81
+ try {
82
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_tool ON events(session_id, tool_name)`);
83
+ }
84
+ catch { }
85
+ try {
86
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at DESC)`);
87
+ }
88
+ catch { }
89
+ try {
90
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`);
91
+ }
92
+ catch { }
93
+ try {
94
+ db.exec(`ALTER TABLE sessions ADD COLUMN dominant_model TEXT`);
95
+ }
96
+ catch { }
97
+ try {
98
+ db.exec(`ALTER TABLE events ADD COLUMN skill_parent TEXT`);
99
+ }
100
+ catch { }
101
+ try {
102
+ db.exec(`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT`);
103
+ }
104
+ catch { }
105
+ // ─── Prepared statements (se compilan una vez al iniciar) ─────────────────────
106
+ const stmts = {
107
+ upsertSession: db.prepare(`
108
+ INSERT INTO sessions (id, cwd, started_at, last_event_at)
109
+ VALUES (?, ?, ?, ?)
110
+ ON CONFLICT(id) DO UPDATE SET last_event_at = excluded.last_event_at
111
+ `),
112
+ updateSessionCost: db.prepare(`
113
+ UPDATE sessions SET
114
+ total_cost_usd = ?,
115
+ total_input_tokens = ?,
116
+ total_output_tokens = ?,
117
+ total_cache_read = ?,
118
+ total_cache_creation = ?,
119
+ efficiency_score = ?,
120
+ loops_detected = ?,
121
+ dominant_model = ?
122
+ WHERE id = ?
123
+ `),
124
+ insertEvent: db.prepare(`
125
+ INSERT INTO events (session_id, type, tool_name, tool_input, ts, cwd, skill_parent)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?)
127
+ `),
128
+ pairPost: db.prepare(`
129
+ UPDATE events SET type = 'Done', tool_response = ?, duration_ms = ?
130
+ WHERE id = (
131
+ SELECT id FROM events
132
+ WHERE session_id = ? AND type = 'PreToolUse' AND tool_name = ? AND tool_response IS NULL
133
+ ORDER BY ts DESC LIMIT 1
134
+ )
135
+ `),
136
+ getSessionEvents: db.prepare(`
137
+ SELECT * FROM events WHERE session_id = ? ORDER BY ts ASC
138
+ `),
139
+ getLatestSession: db.prepare(`
140
+ SELECT * FROM sessions ORDER BY last_event_at DESC LIMIT 1
141
+ `),
142
+ getAllSessions: db.prepare(`
143
+ SELECT * FROM sessions ORDER BY started_at DESC LIMIT 500
144
+ `),
145
+ getSessionEventsRecent: db.prepare(`
146
+ SELECT * FROM events WHERE session_id = ? ORDER BY ts DESC LIMIT ?
147
+ `),
148
+ getSession: db.prepare(`
149
+ SELECT * FROM sessions WHERE id = ?
150
+ `),
151
+ updateSessionProject: db.prepare(`
152
+ UPDATE sessions SET project_path = ? WHERE id = ? AND project_path IS NULL
153
+ `),
154
+ getRecentSessions: db.prepare(`
155
+ SELECT s.*, s.ai_summary,
156
+ (SELECT COUNT(*) FROM events e WHERE e.session_id = s.id AND e.type = 'Done') as done_count,
157
+ (SELECT json_group_array(tool_name) FROM (
158
+ SELECT tool_name FROM events WHERE session_id = s.id AND type = 'Done' AND tool_name IS NOT NULL
159
+ GROUP BY tool_name ORDER BY COUNT(*) DESC LIMIT 3
160
+ )) as top_tools_csv,
161
+ (SELECT COUNT(*) FROM events WHERE session_id = s.id AND tool_name = 'Agent') as agent_count,
162
+ (SELECT COUNT(*) FROM events WHERE session_id = s.id AND tool_name = 'Skill') as skill_count
163
+ FROM sessions s
164
+ WHERE s.started_at >= ?
165
+ ORDER BY s.started_at DESC
166
+ `),
167
+ updateSessionSummary: db.prepare(`
168
+ UPDATE sessions SET ai_summary = ? WHERE id = ?
169
+ `),
170
+ updateSessionParent: db.prepare(`
171
+ UPDATE sessions SET parent_session_id = ? WHERE id = ? AND parent_session_id IS NULL
172
+ `),
173
+ getChildSessions: db.prepare(`
174
+ SELECT id, dominant_model, total_cost_usd, started_at
175
+ FROM sessions WHERE parent_session_id = ? ORDER BY started_at ASC
176
+ `),
177
+ getProjectAggregates: db.prepare(`
178
+ SELECT
179
+ project_path,
180
+ COUNT(*) as session_count,
181
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
182
+ COALESCE(SUM(total_input_tokens),0) as total_input_tokens,
183
+ COALESCE(SUM(total_output_tokens),0) as total_output_tokens,
184
+ COALESCE(SUM(total_cache_read),0) as total_cache_read,
185
+ MAX(last_event_at) as last_active,
186
+ AVG(CASE WHEN efficiency_score > 0 THEN efficiency_score END) as avg_efficiency
187
+ FROM sessions
188
+ WHERE project_path IS NOT NULL
189
+ GROUP BY project_path
190
+ ORDER BY last_active DESC
191
+ `),
192
+ // Tool usage counts for a specific project — used by pattern analyzer
193
+ getProjectToolCounts: db.prepare(`
194
+ SELECT e.tool_name, COUNT(*) as count
195
+ FROM events e
196
+ JOIN sessions s ON e.session_id = s.id
197
+ WHERE s.project_path = ? AND e.type = 'Done' AND e.tool_name IS NOT NULL
198
+ GROUP BY e.tool_name
199
+ ORDER BY count DESC
200
+ `),
201
+ // Session-level aggregates for pattern analysis (cache, loops, cost, efficiency)
202
+ getProjectSessionStats: db.prepare(`
203
+ SELECT
204
+ COUNT(*) as session_count,
205
+ AVG(total_cache_read) as avg_cache_read,
206
+ AVG(total_input_tokens + total_cache_read) as avg_total_input,
207
+ AVG(loops_detected) as avg_loops,
208
+ AVG(total_cost_usd) as avg_cost_usd,
209
+ AVG(CASE WHEN efficiency_score > 0 THEN efficiency_score END) as avg_efficiency
210
+ FROM sessions
211
+ WHERE project_path = ?
212
+ `),
213
+ insertWeeklyReport: db.prepare(`
214
+ INSERT INTO weekly_reports (date, report_markdown)
215
+ VALUES (?, ?)
216
+ ON CONFLICT(date) DO UPDATE SET report_markdown = excluded.report_markdown, created_at = datetime('now')
217
+ `),
218
+ listWeeklyReports: db.prepare(`
219
+ SELECT id, date, substr(report_markdown, 1, 200) as preview, created_at
220
+ FROM weekly_reports
221
+ ORDER BY date DESC
222
+ LIMIT 52
223
+ `),
224
+ getWeeklyReportByDate: db.prepare(`
225
+ SELECT id, date, report_markdown, created_at FROM weekly_reports WHERE date = ?
226
+ `),
227
+ deleteWeeklyReport: db.prepare(`
228
+ DELETE FROM weekly_reports WHERE date = ?
229
+ `),
230
+ // Coste oculto: dinero estimado perdido en loops en los últimos N días
231
+ // Fórmula: cost × (loops / done_count) — fracción de tool calls que fueron desperdicio
232
+ // Ejemplo: 5 loops / 88 tools × $6.49 = $0.37 (mucho más realista que usar efficiency_score)
233
+ getHiddenCostStats: db.prepare(`
234
+ SELECT
235
+ COALESCE(SUM(CASE
236
+ WHEN s.loops_detected > 0
237
+ THEN s.total_cost_usd * CAST(s.loops_detected AS REAL) / MAX(1.0,
238
+ (SELECT CAST(COUNT(*) AS REAL) FROM events e WHERE e.session_id = s.id AND e.type = 'Done')
239
+ )
240
+ ELSE 0.0
241
+ END), 0) AS loop_waste_usd,
242
+ COALESCE(SUM(s.total_cost_usd), 0) AS total_cost_usd,
243
+ COUNT(CASE WHEN s.loops_detected > 0 THEN 1 END) AS loop_sessions,
244
+ COALESCE(SUM(s.loops_detected), 0) AS total_loops,
245
+ COUNT(*) AS total_sessions
246
+ FROM sessions s
247
+ WHERE s.started_at >= ?
248
+ `),
249
+ getModeDistribution: db.prepare(`
250
+ SELECT s.id, COUNT(e.id) as agent_count
251
+ FROM sessions s
252
+ LEFT JOIN events e ON e.session_id = s.id AND e.tool_name = 'Agent'
253
+ WHERE s.started_at > ?
254
+ GROUP BY s.id
255
+ `),
256
+ getQuotaStats: db.prepare(`
257
+ SELECT (total_input_tokens + total_output_tokens + total_cache_read) as total_tokens, total_cost_usd
258
+ FROM sessions
259
+ WHERE started_at > ? AND total_cost_usd > 0
260
+ ORDER BY total_tokens ASC
261
+ `),
262
+ analyticsDaily: db.prepare(`
263
+ SELECT
264
+ date(started_at / 1000, 'unixepoch', 'localtime') AS date,
265
+ COUNT(*) AS sessions,
266
+ COALESCE(SUM(total_cost_usd), 0) AS cost,
267
+ COALESCE(SUM(total_input_tokens), 0) AS input_tokens,
268
+ COALESCE(SUM(total_output_tokens), 0) AS output_tokens,
269
+ COALESCE(SUM(total_cache_read), 0) AS cache_read,
270
+ COALESCE(SUM(loops_detected), 0) AS loops,
271
+ COALESCE(AVG(CASE WHEN efficiency_score > 0 THEN efficiency_score END), 100) AS avg_efficiency
272
+ FROM sessions
273
+ WHERE started_at >= ?
274
+ GROUP BY date
275
+ ORDER BY date ASC
276
+ `),
277
+ analyticsByModel: db.prepare(`
278
+ SELECT
279
+ date(started_at / 1000, 'unixepoch', 'localtime') AS date,
280
+ COALESCE(dominant_model, 'claude-sonnet-4-6') AS model,
281
+ COALESCE(SUM(total_input_tokens + total_output_tokens + total_cache_read), 0) AS tokens,
282
+ COALESCE(SUM(total_cost_usd), 0) AS cost
283
+ FROM sessions
284
+ WHERE started_at >= ?
285
+ GROUP BY date, model
286
+ ORDER BY date ASC
287
+ `),
288
+ analyticsProjectHours: db.prepare(`
289
+ SELECT
290
+ COALESCE(project_path, 'No project') AS project,
291
+ COUNT(*) AS sessions,
292
+ COALESCE(SUM(last_event_at - started_at), 0) / 3600000.0 AS hours,
293
+ COALESCE(SUM(total_cost_usd), 0) AS cost
294
+ FROM sessions
295
+ WHERE started_at >= ?
296
+ GROUP BY project
297
+ ORDER BY hours DESC
298
+ LIMIT 8
299
+ `),
300
+ getTopToolsByCost: db.prepare(`
301
+ WITH session_totals AS (
302
+ SELECT session_id, COUNT(*) AS total_done
303
+ FROM events WHERE type = 'Done' AND ts >= ? GROUP BY session_id
304
+ ),
305
+ tool_per_session AS (
306
+ SELECT e.session_id, e.tool_name,
307
+ COUNT(*) AS cnt,
308
+ COALESCE(SUM(e.duration_ms), 0) AS tool_dur
309
+ FROM events e
310
+ WHERE e.type = 'Done' AND e.tool_name IS NOT NULL AND e.ts >= ?
311
+ GROUP BY e.session_id, e.tool_name
312
+ )
313
+ SELECT
314
+ tps.tool_name,
315
+ SUM(tps.cnt) AS count,
316
+ SUM(tps.tool_dur) AS total_duration_ms,
317
+ SUM(CASE WHEN st.total_done > 0
318
+ THEN (tps.cnt * 1.0 / st.total_done) * s.total_cost_usd
319
+ ELSE 0 END) AS total_cost_usd
320
+ FROM tool_per_session tps
321
+ JOIN session_totals st ON tps.session_id = st.session_id
322
+ JOIN sessions s ON tps.session_id = s.id
323
+ WHERE s.total_cost_usd > 0
324
+ GROUP BY tps.tool_name
325
+ ORDER BY total_cost_usd DESC
326
+ LIMIT ?
327
+ `),
328
+ getTopToolsByCount: db.prepare(`
329
+ WITH session_totals AS (
330
+ SELECT session_id, COUNT(*) AS total_done
331
+ FROM events WHERE type = 'Done' AND ts >= ? GROUP BY session_id
332
+ ),
333
+ tool_per_session AS (
334
+ SELECT e.session_id, e.tool_name,
335
+ COUNT(*) AS cnt,
336
+ COALESCE(SUM(e.duration_ms), 0) AS tool_dur
337
+ FROM events e
338
+ WHERE e.type = 'Done' AND e.tool_name IS NOT NULL AND e.ts >= ?
339
+ GROUP BY e.session_id, e.tool_name
340
+ )
341
+ SELECT
342
+ tps.tool_name,
343
+ SUM(tps.cnt) AS count,
344
+ SUM(tps.tool_dur) AS total_duration_ms,
345
+ SUM(CASE WHEN st.total_done > 0
346
+ THEN (tps.cnt * 1.0 / st.total_done) * s.total_cost_usd
347
+ ELSE 0 END) AS total_cost_usd
348
+ FROM tool_per_session tps
349
+ JOIN session_totals st ON tps.session_id = st.session_id
350
+ JOIN sessions s ON tps.session_id = s.id
351
+ WHERE s.total_cost_usd > 0
352
+ GROUP BY tps.tool_name
353
+ ORDER BY count DESC
354
+ LIMIT ?
355
+ `),
356
+ getTopToolsByDuration: db.prepare(`
357
+ WITH session_totals AS (
358
+ SELECT session_id, COUNT(*) AS total_done
359
+ FROM events WHERE type = 'Done' AND ts >= ? GROUP BY session_id
360
+ ),
361
+ tool_per_session AS (
362
+ SELECT e.session_id, e.tool_name,
363
+ COUNT(*) AS cnt,
364
+ COALESCE(SUM(e.duration_ms), 0) AS tool_dur
365
+ FROM events e
366
+ WHERE e.type = 'Done' AND e.tool_name IS NOT NULL AND e.ts >= ?
367
+ GROUP BY e.session_id, e.tool_name
368
+ )
369
+ SELECT
370
+ tps.tool_name,
371
+ SUM(tps.cnt) AS count,
372
+ SUM(tps.tool_dur) AS total_duration_ms,
373
+ SUM(CASE WHEN st.total_done > 0
374
+ THEN (tps.cnt * 1.0 / st.total_done) * s.total_cost_usd
375
+ ELSE 0 END) AS total_cost_usd
376
+ FROM tool_per_session tps
377
+ JOIN session_totals st ON tps.session_id = st.session_id
378
+ JOIN sessions s ON tps.session_id = s.id
379
+ WHERE s.total_cost_usd > 0
380
+ GROUP BY tps.tool_name
381
+ ORDER BY total_duration_ms DESC
382
+ LIMIT ?
383
+ `),
384
+ getCostProjection: db.prepare(`
385
+ SELECT
386
+ SUM(total_cost_usd) AS total_cost_usd,
387
+ MIN(started_at) AS earliest,
388
+ MAX(last_event_at) AS latest
389
+ FROM sessions
390
+ WHERE started_at >= ?
391
+ `),
392
+ getUnattributedCost: db.prepare(`
393
+ WITH period_cost AS (
394
+ SELECT COALESCE(SUM(total_cost_usd), 0) AS total_cost
395
+ FROM sessions WHERE started_at >= ?
396
+ ),
397
+ attributed_cost AS (
398
+ WITH session_totals AS (
399
+ SELECT session_id, COUNT(*) AS total_done
400
+ FROM events WHERE type = 'Done' AND ts >= ? GROUP BY session_id
401
+ ),
402
+ tool_per_session AS (
403
+ SELECT e.session_id,
404
+ COUNT(*) AS cnt
405
+ FROM events e
406
+ WHERE e.type = 'Done' AND e.tool_name IS NOT NULL AND e.ts >= ?
407
+ GROUP BY e.session_id, e.tool_name
408
+ )
409
+ SELECT COALESCE(SUM(
410
+ CASE WHEN st.total_done > 0
411
+ THEN (tps.cnt * 1.0 / st.total_done) * s.total_cost_usd
412
+ ELSE 0 END
413
+ ), 0) AS attributed
414
+ FROM tool_per_session tps
415
+ JOIN session_totals st ON tps.session_id = st.session_id
416
+ JOIN sessions s ON tps.session_id = s.id
417
+ WHERE s.total_cost_usd > 0
418
+ )
419
+ SELECT (pc.total_cost - ac.attributed) AS other_cost_usd
420
+ FROM period_cost pc, attributed_cost ac
421
+ `),
422
+ };
423
+ // ─── Operaciones públicas ─────────────────────────────────────────────────────
424
+ exports.dbOps = {
425
+ upsertSession(s) {
426
+ stmts.upsertSession.run(s.id, s.cwd ?? null, s.started_at, s.last_event_at ?? s.started_at);
427
+ },
428
+ insertEvent(e) {
429
+ const res = stmts.insertEvent.run(e.session_id, e.type, e.tool_name ?? null, e.tool_input ?? null, e.ts, e.cwd ?? null, e.skill_parent ?? null);
430
+ return Number(res.lastInsertRowid);
431
+ },
432
+ /**
433
+ * Al llegar PostToolUse, actualizamos el PreToolUse pendiente más reciente
434
+ * del mismo tool para esta sesión. Esto convierte el par Pre+Post en
435
+ * un único registro de tipo 'Done' con duration_ms calculado.
436
+ */
437
+ pairPostWithPre(sessionId, toolName, response, postTs) {
438
+ // Primero obtenemos el ID del PreToolUse pendiente
439
+ const pending = db.prepare(`
440
+ SELECT id, ts FROM events
441
+ WHERE session_id = ? AND type = 'PreToolUse' AND tool_name = ? AND tool_response IS NULL
442
+ ORDER BY ts DESC LIMIT 1
443
+ `).get(sessionId, toolName);
444
+ if (pending) {
445
+ stmts.pairPost.run(response, postTs - pending.ts, sessionId, toolName);
446
+ return pending.id;
447
+ }
448
+ return null;
449
+ },
450
+ updateSessionCost(sessionId, cost, efficiencyScore, loopsDetected) {
451
+ stmts.updateSessionCost.run(cost.cost_usd, cost.input_tokens, cost.output_tokens, cost.cache_read, cost.cache_creation, efficiencyScore, loopsDetected, cost.lastModel ?? null, sessionId);
452
+ },
453
+ getSessionEvents(sessionId) {
454
+ return stmts.getSessionEvents.all(sessionId);
455
+ },
456
+ getSession(sessionId) {
457
+ return stmts.getSession.get(sessionId);
458
+ },
459
+ getLatestSession() {
460
+ return stmts.getLatestSession.get();
461
+ },
462
+ getAllSessions(limit = 500) {
463
+ return db.prepare(`SELECT * FROM sessions ORDER BY started_at DESC LIMIT ?`).all(limit);
464
+ },
465
+ getSessionEventsRecent(sessionId, limit = 200) {
466
+ return stmts.getSessionEventsRecent.all(sessionId, limit);
467
+ },
468
+ updateSessionProject(sessionId, projectPath) {
469
+ stmts.updateSessionProject.run(projectPath, sessionId);
470
+ },
471
+ getRecentSessions(days) {
472
+ const since = Date.now() - days * 24 * 60 * 60 * 1000;
473
+ return stmts.getRecentSessions.all(since);
474
+ },
475
+ getProjectAggregates() {
476
+ return stmts.getProjectAggregates.all();
477
+ },
478
+ getProjectToolCounts(projectPath) {
479
+ return stmts.getProjectToolCounts.all(projectPath);
480
+ },
481
+ getProjectSessionStats(projectPath) {
482
+ return stmts.getProjectSessionStats.get(projectPath);
483
+ },
484
+ updateSessionSummary(sessionId, summary) {
485
+ stmts.updateSessionSummary.run(summary, sessionId);
486
+ },
487
+ updateSessionParent(sessionId, parentId) {
488
+ stmts.updateSessionParent.run(parentId, sessionId);
489
+ },
490
+ getChildSessions(parentSessionId) {
491
+ return stmts.getChildSessions.all(parentSessionId);
492
+ },
493
+ getHiddenCostStats(days) {
494
+ const since = Date.now() - days * 24 * 60 * 60 * 1000;
495
+ return stmts.getHiddenCostStats.get(since);
496
+ },
497
+ insertWeeklyReport(date, markdown) {
498
+ stmts.insertWeeklyReport.run(date, markdown);
499
+ },
500
+ deleteWeeklyReport(date) {
501
+ stmts.deleteWeeklyReport.run(date);
502
+ },
503
+ listWeeklyReports() {
504
+ return stmts.listWeeklyReports.all();
505
+ },
506
+ getWeeklyReportByDate(date) {
507
+ return stmts.getWeeklyReportByDate.get(date);
508
+ },
509
+ getQuotaStats(since) {
510
+ return stmts.getQuotaStats.all(since);
511
+ },
512
+ // Cuenta sesiones por modo: directo (0 agentes), mini (1-3), pipeline (4+)
513
+ getModeDistribution(days) {
514
+ const cutoff = Date.now() - days * 86400000;
515
+ const rows = stmts.getModeDistribution.all(cutoff);
516
+ let direct = 0, mini = 0, pipeline = 0;
517
+ for (const r of rows) {
518
+ if (r.agent_count === 0)
519
+ direct++;
520
+ else if (r.agent_count <= 6)
521
+ mini++;
522
+ else
523
+ pipeline++;
524
+ }
525
+ return { direct, mini, pipeline, total: rows.length };
526
+ },
527
+ getAnalyticsDaily(since) { return stmts.analyticsDaily.all(since); },
528
+ getAnalyticsByModel(since) { return stmts.analyticsByModel.all(since); },
529
+ getProjectHours(since) { return stmts.analyticsProjectHours.all(since); },
530
+ getTopTools(days = 30, by = 'cost', limit = 10) {
531
+ const since = Date.now() - days * 86400000;
532
+ const stmt = by === 'count' ? stmts.getTopToolsByCount
533
+ : by === 'duration' ? stmts.getTopToolsByDuration
534
+ : stmts.getTopToolsByCost;
535
+ const tools = stmt.all(since, since, limit);
536
+ const other = stmts.getUnattributedCost.get(since, since, since);
537
+ if (other.other_cost_usd > 0) {
538
+ tools.push({ tool_name: 'Other', count: 0, total_duration_ms: 0, total_cost_usd: other.other_cost_usd });
539
+ }
540
+ return tools;
541
+ },
542
+ getCostProjection(days = 7) {
543
+ const since = Date.now() - days * 86400000;
544
+ return stmts.getCostProjection.get(since);
545
+ },
546
+ };
@@ -0,0 +1 @@
1
+ export declare function runDoctor(): Promise<void>;