@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
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
|
+
};
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDoctor(): Promise<void>;
|