@statforge/claudestat 1.6.1 → 1.7.0
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 +3 -1
- package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
- package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
- package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
- package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
- package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
- package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
- package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
- package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
- package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
- package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/dist/daemon.js +57 -1
- package/dist/db.d.ts +76 -2
- package/dist/db.js +295 -65
- package/dist/doctor.js +1 -1
- package/dist/enricher.d.ts +3 -2
- package/dist/enricher.js +10 -5
- package/dist/index.js +12 -1
- package/dist/intelligence.d.ts +55 -0
- package/dist/intelligence.js +163 -1
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +8 -0
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +12 -1
- package/dist/routes/events.js +136 -5
- package/dist/routes/history.js +6 -2
- package/dist/routes/intents.d.ts +1 -0
- package/dist/routes/intents.js +155 -0
- package/dist/routes/misc.js +131 -3
- package/dist/routes/opencode-reader.js +39 -3
- package/dist/routes/projects.js +10 -1
- package/dist/routes/replay.d.ts +1 -0
- package/dist/routes/replay.js +29 -0
- package/dist/routes/reports.js +7 -0
- package/dist/routes/top.js +8 -1
- package/dist/watchers/adapter.d.ts +1 -0
- package/dist/watchers/claude-code.d.ts +16 -1
- package/dist/watchers/claude-code.js +201 -76
- package/dist/watchers/opencode.d.ts +1 -0
- package/dist/watchers/opencode.js +152 -14
- package/package.json +1 -1
- package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
- package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
- package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
- package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
- package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
- package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
package/dist/db.js
CHANGED
|
@@ -13,7 +13,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
13
13
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
14
|
};
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.dbOps = exports.CLAUDESTAT_DIR = void 0;
|
|
16
|
+
exports.dbOps = exports.TOOL_RESPONSE_MAX_BYTES = exports.CLAUDESTAT_DIR = void 0;
|
|
17
|
+
exports.capToolResponse = capToolResponse;
|
|
17
18
|
const node_sqlite_1 = require("node:sqlite");
|
|
18
19
|
const path_1 = __importDefault(require("path"));
|
|
19
20
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -22,35 +23,22 @@ exports.CLAUDESTAT_DIR = (0, paths_1.getClaudestatDir)();
|
|
|
22
23
|
const DB_PATH = process.env.CLAUDESTAT_DB_PATH ?? path_1.default.join(exports.CLAUDESTAT_DIR, 'events.db');
|
|
23
24
|
fs_1.default.mkdirSync(exports.CLAUDESTAT_DIR, { recursive: true });
|
|
24
25
|
const db = new node_sqlite_1.DatabaseSync(DB_PATH);
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
// ─── Tool response size cap ────────────────────────────────────────────────────
|
|
27
|
+
exports.TOOL_RESPONSE_MAX_BYTES = 8192;
|
|
28
|
+
function capToolResponse(raw) {
|
|
29
|
+
if (Buffer.byteLength(raw, 'utf8') <= exports.TOOL_RESPONSE_MAX_BYTES)
|
|
30
|
+
return raw;
|
|
31
|
+
const buf = Buffer.from(raw, 'utf8').subarray(0, exports.TOOL_RESPONSE_MAX_BYTES);
|
|
32
|
+
return buf.toString('utf8') + `…[truncated: ${raw.length} chars]`;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
try {
|
|
46
|
-
db.exec(`
|
|
34
|
+
// ─── Base tables (idempotent — safe on every start) ───────────────────────────
|
|
35
|
+
// meta must exist before runMigrations reads schema_version
|
|
36
|
+
db.exec(`
|
|
47
37
|
CREATE TABLE IF NOT EXISTS meta (
|
|
48
38
|
key TEXT PRIMARY KEY,
|
|
49
39
|
value TEXT NOT NULL
|
|
50
40
|
)
|
|
51
41
|
`);
|
|
52
|
-
}
|
|
53
|
-
catch { /* ya existe */ }
|
|
54
42
|
db.exec(`
|
|
55
43
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
56
44
|
id TEXT PRIMARY KEY,
|
|
@@ -82,7 +70,6 @@ db.exec(`
|
|
|
82
70
|
);
|
|
83
71
|
`);
|
|
84
72
|
// Índices para acelerar las subqueries de getRecentSessions (N+3 pattern)
|
|
85
|
-
// Wrapped en try-catch para no romper instalaciones que ya los tienen
|
|
86
73
|
try {
|
|
87
74
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_events_session_type ON events(session_id, type)`);
|
|
88
75
|
}
|
|
@@ -99,32 +86,96 @@ try {
|
|
|
99
86
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`);
|
|
100
87
|
}
|
|
101
88
|
catch { }
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
89
|
+
// ─── Schema migrations ─────────────────────────────────────────────────────────
|
|
90
|
+
// Each migration runs exactly once, tracked by meta.schema_version.
|
|
91
|
+
// try/catch per migration handles existing installs where columns already exist.
|
|
92
|
+
const MIGRATIONS = [
|
|
93
|
+
{ version: 1, sql: `ALTER TABLE sessions ADD COLUMN project_path TEXT` },
|
|
94
|
+
{ version: 2, sql: `ALTER TABLE sessions ADD COLUMN ai_summary TEXT` },
|
|
95
|
+
{ version: 3, sql: `CREATE TABLE IF NOT EXISTS weekly_reports (
|
|
96
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
97
|
+
date TEXT NOT NULL UNIQUE,
|
|
98
|
+
report_markdown TEXT NOT NULL,
|
|
99
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
100
|
+
)` },
|
|
101
|
+
{ version: 4, sql: `ALTER TABLE sessions ADD COLUMN dominant_model TEXT` },
|
|
102
|
+
{ version: 5, sql: `ALTER TABLE events ADD COLUMN skill_parent TEXT` },
|
|
103
|
+
{ version: 6, sql: `ALTER TABLE sessions ADD COLUMN parent_session_id TEXT` },
|
|
104
|
+
{ version: 7, sql: `ALTER TABLE sessions ADD COLUMN source TEXT DEFAULT 'claude-code'` },
|
|
105
|
+
{ version: 8, sql: `ALTER TABLE events ADD COLUMN source TEXT DEFAULT 'claude-code'` },
|
|
106
|
+
{ version: 9, sql: `ALTER TABLE sessions ADD COLUMN exact_retries INTEGER DEFAULT 0` },
|
|
107
|
+
{ version: 10, sql: `ALTER TABLE sessions ADD COLUMN error_rate REAL DEFAULT 0` },
|
|
108
|
+
{ version: 11, sql: `ALTER TABLE sessions ADD COLUMN file_churn_score REAL DEFAULT 0` },
|
|
109
|
+
{ version: 12, sql: `ALTER TABLE sessions ADD COLUMN seq_cycle_count INTEGER DEFAULT 0` },
|
|
110
|
+
{ version: 13, sql: `ALTER TABLE sessions ADD COLUMN avg_output_chars INTEGER DEFAULT 0` },
|
|
111
|
+
{ version: 14, sql: `ALTER TABLE sessions ADD COLUMN error_block_count INTEGER DEFAULT 0` },
|
|
112
|
+
{ version: 15, sql: `CREATE TABLE IF NOT EXISTS assistant_turns (
|
|
113
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
114
|
+
session_id TEXT NOT NULL,
|
|
115
|
+
turn_index INTEGER NOT NULL,
|
|
116
|
+
ts INTEGER,
|
|
117
|
+
text_preview TEXT,
|
|
118
|
+
tool_calls TEXT,
|
|
119
|
+
error_count INTEGER DEFAULT 0,
|
|
120
|
+
output_chars INTEGER DEFAULT 0,
|
|
121
|
+
UNIQUE(session_id, turn_index) ON CONFLICT REPLACE
|
|
122
|
+
)` },
|
|
123
|
+
{ version: 16, sql: `ALTER TABLE sessions ADD COLUMN semantic_loop_count INTEGER DEFAULT 0` },
|
|
124
|
+
{ version: 17, sql: `ALTER TABLE assistant_turns ADD COLUMN context_used INTEGER DEFAULT 0` },
|
|
125
|
+
{ version: 18, sql: `
|
|
126
|
+
CREATE TABLE IF NOT EXISTS file_intents (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
tool TEXT NOT NULL,
|
|
129
|
+
session_id TEXT NOT NULL,
|
|
130
|
+
task_desc TEXT,
|
|
131
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
132
|
+
acquired_at INTEGER NOT NULL,
|
|
133
|
+
released_at INTEGER,
|
|
134
|
+
last_heartbeat INTEGER
|
|
135
|
+
);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_intents_status ON file_intents(status);
|
|
137
|
+
` },
|
|
138
|
+
{ version: 19, sql: `
|
|
139
|
+
CREATE TABLE IF NOT EXISTS file_intent_files (
|
|
140
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
+
intent_id INTEGER NOT NULL REFERENCES file_intents(id) ON DELETE CASCADE,
|
|
142
|
+
file_path TEXT NOT NULL,
|
|
143
|
+
operation TEXT NOT NULL DEFAULT 'write',
|
|
144
|
+
line_start INTEGER,
|
|
145
|
+
line_end INTEGER
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_intent_files_path ON file_intent_files(file_path);
|
|
148
|
+
` },
|
|
149
|
+
{ version: 20, sql: `ALTER TABLE events ADD COLUMN external_id TEXT` },
|
|
150
|
+
{ version: 21, sql: `CREATE UNIQUE INDEX IF NOT EXISTS idx_events_ext ON events(session_id, external_id) WHERE external_id IS NOT NULL` },
|
|
151
|
+
];
|
|
152
|
+
function runMigrations() {
|
|
153
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
154
|
+
const currentVersion = row ? parseInt(row.value, 10) : 0;
|
|
155
|
+
const upsertVersion = db.prepare(`INSERT INTO meta (key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
|
|
156
|
+
for (const m of MIGRATIONS) {
|
|
157
|
+
if (m.version <= currentVersion)
|
|
158
|
+
continue;
|
|
159
|
+
try {
|
|
160
|
+
db.exec(m.sql);
|
|
161
|
+
}
|
|
162
|
+
catch { /* column/table already exists on prior installs */ }
|
|
163
|
+
upsertVersion.run(String(m.version));
|
|
164
|
+
}
|
|
120
165
|
}
|
|
121
|
-
|
|
166
|
+
runMigrations(); // must run before stmts — some queries reference migrated columns
|
|
122
167
|
// ─── Prepared statements (se compilan una vez al iniciar) ─────────────────────
|
|
123
168
|
const stmts = {
|
|
124
169
|
upsertSession: db.prepare(`
|
|
125
170
|
INSERT INTO sessions (id, cwd, started_at, last_event_at, source)
|
|
126
171
|
VALUES (?, ?, ?, ?, COALESCE(?, 'claude-code'))
|
|
127
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
172
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
173
|
+
last_event_at = excluded.last_event_at,
|
|
174
|
+
source = COALESCE(excluded.source, source)
|
|
175
|
+
`),
|
|
176
|
+
insertSessionIfAbsent: db.prepare(`
|
|
177
|
+
INSERT OR IGNORE INTO sessions (id, cwd, started_at, last_event_at, source)
|
|
178
|
+
VALUES (?, ?, ?, ?, COALESCE(?, 'claude-code'))
|
|
128
179
|
`),
|
|
129
180
|
updateSessionCost: db.prepare(`
|
|
130
181
|
UPDATE sessions SET
|
|
@@ -135,12 +186,20 @@ const stmts = {
|
|
|
135
186
|
total_cache_creation = ?,
|
|
136
187
|
efficiency_score = ?,
|
|
137
188
|
loops_detected = ?,
|
|
138
|
-
dominant_model =
|
|
189
|
+
dominant_model = ?,
|
|
190
|
+
exact_retries = ?,
|
|
191
|
+
error_rate = ?,
|
|
192
|
+
file_churn_score = ?,
|
|
193
|
+
seq_cycle_count = ?
|
|
139
194
|
WHERE id = ?
|
|
140
195
|
`),
|
|
141
196
|
insertEvent: db.prepare(`
|
|
142
197
|
INSERT INTO events (session_id, type, tool_name, tool_input, ts, cwd, skill_parent, source)
|
|
143
198
|
VALUES (?, ?, ?, ?, ?, ?, ?, COALESCE(?, 'claude-code'))
|
|
199
|
+
`),
|
|
200
|
+
insertOcEvent: db.prepare(`
|
|
201
|
+
INSERT OR IGNORE INTO events (session_id, type, tool_name, ts, duration_ms, external_id, source)
|
|
202
|
+
VALUES (?, 'Done', ?, ?, 0, ?, 'opencode')
|
|
144
203
|
`),
|
|
145
204
|
pairPost: db.prepare(`
|
|
146
205
|
UPDATE events SET type = 'Done', tool_response = ?, duration_ms = ?
|
|
@@ -171,7 +230,7 @@ const stmts = {
|
|
|
171
230
|
SELECT * FROM sessions WHERE id = ?
|
|
172
231
|
`),
|
|
173
232
|
updateSessionProject: db.prepare(`
|
|
174
|
-
UPDATE sessions SET project_path = ? WHERE id = ?
|
|
233
|
+
UPDATE sessions SET project_path = ? WHERE id = ?
|
|
175
234
|
`),
|
|
176
235
|
getRecentSessions: db.prepare(`
|
|
177
236
|
SELECT s.*, s.ai_summary,
|
|
@@ -219,6 +278,13 @@ const stmts = {
|
|
|
219
278
|
WHERE s.project_path = ? AND e.type = 'Done' AND e.tool_name IS NOT NULL
|
|
220
279
|
GROUP BY e.tool_name
|
|
221
280
|
ORDER BY count DESC
|
|
281
|
+
`),
|
|
282
|
+
getProjectCliHours: db.prepare(`
|
|
283
|
+
SELECT project_path, COALESCE(source, 'claude-code') as source,
|
|
284
|
+
SUM(last_event_at - started_at) as total_ms
|
|
285
|
+
FROM sessions
|
|
286
|
+
WHERE project_path IS NOT NULL AND last_event_at IS NOT NULL
|
|
287
|
+
GROUP BY project_path, COALESCE(source, 'claude-code')
|
|
222
288
|
`),
|
|
223
289
|
// Session-level aggregates for pattern analysis (cache, loops, cost, efficiency)
|
|
224
290
|
getProjectSessionStats: db.prepare(`
|
|
@@ -284,6 +350,7 @@ const stmts = {
|
|
|
284
350
|
analyticsDaily: db.prepare(`
|
|
285
351
|
SELECT
|
|
286
352
|
date(started_at / 1000, 'unixepoch', 'localtime') AS date,
|
|
353
|
+
COALESCE(source, 'claude-code') AS source,
|
|
287
354
|
COUNT(*) AS sessions,
|
|
288
355
|
COALESCE(SUM(total_cost_usd), 0) AS cost,
|
|
289
356
|
COALESCE(SUM(total_input_tokens), 0) AS input_tokens,
|
|
@@ -293,31 +360,34 @@ const stmts = {
|
|
|
293
360
|
COALESCE(AVG(CASE WHEN efficiency_score > 0 THEN efficiency_score END), 100) AS avg_efficiency
|
|
294
361
|
FROM sessions
|
|
295
362
|
WHERE started_at >= ?
|
|
296
|
-
GROUP BY date
|
|
363
|
+
GROUP BY date, COALESCE(source, 'claude-code')
|
|
297
364
|
ORDER BY date ASC
|
|
298
365
|
`),
|
|
299
366
|
analyticsByModel: db.prepare(`
|
|
300
367
|
SELECT
|
|
301
368
|
date(started_at / 1000, 'unixepoch', 'localtime') AS date,
|
|
302
369
|
COALESCE(dominant_model, 'claude-sonnet-4-6') AS model,
|
|
370
|
+
COALESCE(source, 'claude-code') AS source,
|
|
303
371
|
COALESCE(SUM(total_input_tokens + total_output_tokens + total_cache_read), 0) AS tokens,
|
|
304
372
|
COALESCE(SUM(total_cost_usd), 0) AS cost
|
|
305
373
|
FROM sessions
|
|
306
374
|
WHERE started_at >= ?
|
|
307
|
-
|
|
375
|
+
AND (dominant_model IS NULL OR dominant_model NOT LIKE '<%')
|
|
376
|
+
GROUP BY date, model, COALESCE(source, 'claude-code')
|
|
308
377
|
ORDER BY date ASC
|
|
309
378
|
`),
|
|
310
379
|
analyticsProjectHours: db.prepare(`
|
|
311
380
|
SELECT
|
|
312
381
|
COALESCE(project_path, 'No project') AS project,
|
|
382
|
+
COALESCE(source, 'claude-code') AS source,
|
|
313
383
|
COUNT(*) AS sessions,
|
|
314
384
|
COALESCE(SUM(last_event_at - started_at), 0) / 3600000.0 AS hours,
|
|
315
385
|
COALESCE(SUM(total_cost_usd), 0) AS cost
|
|
316
386
|
FROM sessions
|
|
317
387
|
WHERE started_at >= ?
|
|
318
|
-
GROUP BY project
|
|
388
|
+
GROUP BY project, COALESCE(source, 'claude-code')
|
|
319
389
|
ORDER BY hours DESC
|
|
320
|
-
LIMIT
|
|
390
|
+
LIMIT 16
|
|
321
391
|
`),
|
|
322
392
|
getTopToolsByCost: db.prepare(`
|
|
323
393
|
WITH session_totals AS (
|
|
@@ -334,6 +404,7 @@ const stmts = {
|
|
|
334
404
|
)
|
|
335
405
|
SELECT
|
|
336
406
|
tps.tool_name,
|
|
407
|
+
COALESCE(s.source, 'claude-code') AS source,
|
|
337
408
|
SUM(tps.cnt) AS count,
|
|
338
409
|
SUM(tps.tool_dur) AS total_duration_ms,
|
|
339
410
|
SUM(CASE WHEN st.total_done > 0
|
|
@@ -342,8 +413,8 @@ const stmts = {
|
|
|
342
413
|
FROM tool_per_session tps
|
|
343
414
|
JOIN session_totals st ON tps.session_id = st.session_id
|
|
344
415
|
JOIN sessions s ON tps.session_id = s.id
|
|
345
|
-
WHERE s.
|
|
346
|
-
GROUP BY tps.tool_name
|
|
416
|
+
WHERE (? = 'all' OR COALESCE(s.source, 'claude-code') = ?)
|
|
417
|
+
GROUP BY tps.tool_name, COALESCE(s.source, 'claude-code')
|
|
347
418
|
ORDER BY total_cost_usd DESC
|
|
348
419
|
LIMIT ?
|
|
349
420
|
`),
|
|
@@ -362,6 +433,7 @@ const stmts = {
|
|
|
362
433
|
)
|
|
363
434
|
SELECT
|
|
364
435
|
tps.tool_name,
|
|
436
|
+
COALESCE(s.source, 'claude-code') AS source,
|
|
365
437
|
SUM(tps.cnt) AS count,
|
|
366
438
|
SUM(tps.tool_dur) AS total_duration_ms,
|
|
367
439
|
SUM(CASE WHEN st.total_done > 0
|
|
@@ -370,8 +442,8 @@ const stmts = {
|
|
|
370
442
|
FROM tool_per_session tps
|
|
371
443
|
JOIN session_totals st ON tps.session_id = st.session_id
|
|
372
444
|
JOIN sessions s ON tps.session_id = s.id
|
|
373
|
-
WHERE s.
|
|
374
|
-
GROUP BY tps.tool_name
|
|
445
|
+
WHERE (? = 'all' OR COALESCE(s.source, 'claude-code') = ?)
|
|
446
|
+
GROUP BY tps.tool_name, COALESCE(s.source, 'claude-code')
|
|
375
447
|
ORDER BY count DESC
|
|
376
448
|
LIMIT ?
|
|
377
449
|
`),
|
|
@@ -390,6 +462,7 @@ const stmts = {
|
|
|
390
462
|
)
|
|
391
463
|
SELECT
|
|
392
464
|
tps.tool_name,
|
|
465
|
+
COALESCE(s.source, 'claude-code') AS source,
|
|
393
466
|
SUM(tps.cnt) AS count,
|
|
394
467
|
SUM(tps.tool_dur) AS total_duration_ms,
|
|
395
468
|
SUM(CASE WHEN st.total_done > 0
|
|
@@ -398,8 +471,8 @@ const stmts = {
|
|
|
398
471
|
FROM tool_per_session tps
|
|
399
472
|
JOIN session_totals st ON tps.session_id = st.session_id
|
|
400
473
|
JOIN sessions s ON tps.session_id = s.id
|
|
401
|
-
WHERE s.
|
|
402
|
-
GROUP BY tps.tool_name
|
|
474
|
+
WHERE (? = 'all' OR COALESCE(s.source, 'claude-code') = ?)
|
|
475
|
+
GROUP BY tps.tool_name, COALESCE(s.source, 'claude-code')
|
|
403
476
|
ORDER BY total_duration_ms DESC
|
|
404
477
|
LIMIT ?
|
|
405
478
|
`),
|
|
@@ -440,6 +513,13 @@ const stmts = {
|
|
|
440
513
|
GROUP BY project_path
|
|
441
514
|
ORDER BY total_cost DESC
|
|
442
515
|
LIMIT 5
|
|
516
|
+
`),
|
|
517
|
+
updateSessionSemantic: db.prepare(`
|
|
518
|
+
UPDATE sessions SET avg_output_chars = ?, error_block_count = ?, semantic_loop_count = ? WHERE id = ?
|
|
519
|
+
`),
|
|
520
|
+
getAssistantTurns: db.prepare(`
|
|
521
|
+
SELECT turn_index, ts, text_preview, tool_calls, error_count, output_chars, context_used
|
|
522
|
+
FROM assistant_turns WHERE session_id = ? ORDER BY turn_index ASC
|
|
443
523
|
`),
|
|
444
524
|
getHourlyDistribution: db.prepare(`
|
|
445
525
|
SELECT
|
|
@@ -490,6 +570,12 @@ exports.dbOps = {
|
|
|
490
570
|
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, e.source ?? null);
|
|
491
571
|
return Number(res.lastInsertRowid);
|
|
492
572
|
},
|
|
573
|
+
insertOcEvent(sessionId, toolName, ts, externalId) {
|
|
574
|
+
stmts.insertOcEvent.run(sessionId, toolName, ts, externalId);
|
|
575
|
+
},
|
|
576
|
+
insertSessionIfAbsent(s) {
|
|
577
|
+
stmts.insertSessionIfAbsent.run(s.id, s.cwd ?? null, s.started_at, s.last_event_at ?? s.started_at, s.source ?? null);
|
|
578
|
+
},
|
|
493
579
|
/**
|
|
494
580
|
* Al llegar PostToolUse, actualizamos el PreToolUse pendiente más reciente
|
|
495
581
|
* del mismo tool para esta sesión. Esto convierte el par Pre+Post en
|
|
@@ -503,13 +589,13 @@ exports.dbOps = {
|
|
|
503
589
|
ORDER BY ts DESC LIMIT 1
|
|
504
590
|
`).get(sessionId, toolName);
|
|
505
591
|
if (pending) {
|
|
506
|
-
stmts.pairPost.run(response, postTs - pending.ts, sessionId, toolName);
|
|
592
|
+
stmts.pairPost.run(capToolResponse(response), postTs - pending.ts, sessionId, toolName);
|
|
507
593
|
return pending.id;
|
|
508
594
|
}
|
|
509
595
|
return null;
|
|
510
596
|
},
|
|
511
|
-
updateSessionCost(sessionId, cost, efficiencyScore, loopsDetected) {
|
|
512
|
-
stmts.updateSessionCost.run(cost.cost_usd, cost.input_tokens, cost.output_tokens, cost.cache_read, cost.cache_creation, efficiencyScore, loopsDetected, cost.lastModel ?? null, sessionId);
|
|
597
|
+
updateSessionCost(sessionId, cost, efficiencyScore, loopsDetected, exactRetries = 0, errorRate = 0, fileChurnScore = 1, seqCycleCount = 0) {
|
|
598
|
+
stmts.updateSessionCost.run(cost.cost_usd, cost.input_tokens, cost.output_tokens, cost.cache_read, cost.cache_creation, efficiencyScore, loopsDetected, cost.lastModel ?? null, exactRetries, errorRate, fileChurnScore, seqCycleCount, sessionId);
|
|
513
599
|
},
|
|
514
600
|
getSessionEvents(sessionId) {
|
|
515
601
|
return stmts.getSessionEvents.all(sessionId);
|
|
@@ -530,6 +616,13 @@ exports.dbOps = {
|
|
|
530
616
|
return stmts.getSessionEventsRecent.all(sessionId, limit);
|
|
531
617
|
},
|
|
532
618
|
updateSessionProject(sessionId, projectPath) {
|
|
619
|
+
const existing = this.getSession(sessionId);
|
|
620
|
+
if (existing?.project_path) {
|
|
621
|
+
const cur = existing.project_path.endsWith('/')
|
|
622
|
+
? existing.project_path : existing.project_path + '/';
|
|
623
|
+
if (!projectPath.startsWith(cur))
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
533
626
|
stmts.updateSessionProject.run(projectPath, sessionId);
|
|
534
627
|
},
|
|
535
628
|
getRecentSessions(days) {
|
|
@@ -545,6 +638,9 @@ exports.dbOps = {
|
|
|
545
638
|
getProjectSessionStats(projectPath) {
|
|
546
639
|
return stmts.getProjectSessionStats.get(projectPath);
|
|
547
640
|
},
|
|
641
|
+
getProjectCliHours() {
|
|
642
|
+
return stmts.getProjectCliHours.all();
|
|
643
|
+
},
|
|
548
644
|
updateSessionSummary(sessionId, summary) {
|
|
549
645
|
stmts.updateSessionSummary.run(summary, sessionId);
|
|
550
646
|
},
|
|
@@ -591,15 +687,31 @@ exports.dbOps = {
|
|
|
591
687
|
getAnalyticsDaily(since) { return stmts.analyticsDaily.all(since); },
|
|
592
688
|
getAnalyticsByModel(since) { return stmts.analyticsByModel.all(since); },
|
|
593
689
|
getProjectHours(since) { return stmts.analyticsProjectHours.all(since); },
|
|
594
|
-
|
|
690
|
+
getCoachEvents(sessionIds) {
|
|
691
|
+
if (sessionIds.length === 0)
|
|
692
|
+
return [];
|
|
693
|
+
const placeholders = sessionIds.map(() => '?').join(',');
|
|
694
|
+
const cutoff = Date.now() - 2 * 60 * 60000;
|
|
695
|
+
return db.prepare(`
|
|
696
|
+
SELECT session_id, type, tool_name, tool_input, ts
|
|
697
|
+
FROM events
|
|
698
|
+
WHERE session_id IN (${placeholders})
|
|
699
|
+
AND type IN ('PreToolUse', 'Done', 'Stop')
|
|
700
|
+
AND ts > ?
|
|
701
|
+
ORDER BY ts ASC
|
|
702
|
+
`).all(...sessionIds, cutoff);
|
|
703
|
+
},
|
|
704
|
+
getTopTools(days = 30, by = 'cost', limit = 10, source = 'all') {
|
|
595
705
|
const since = Date.now() - days * 86400000;
|
|
596
706
|
const stmt = by === 'count' ? stmts.getTopToolsByCount
|
|
597
707
|
: by === 'duration' ? stmts.getTopToolsByDuration
|
|
598
708
|
: stmts.getTopToolsByCost;
|
|
599
|
-
const tools = stmt.all(since, since, limit);
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
709
|
+
const tools = stmt.all(since, since, source, source, limit);
|
|
710
|
+
if (source === 'all') {
|
|
711
|
+
const other = stmts.getUnattributedCost.get(since, since, since);
|
|
712
|
+
if (other.other_cost_usd > 0) {
|
|
713
|
+
tools.push({ tool_name: 'Other', source: 'claude-code', count: 0, total_duration_ms: 0, total_cost_usd: other.other_cost_usd });
|
|
714
|
+
}
|
|
603
715
|
}
|
|
604
716
|
return tools;
|
|
605
717
|
},
|
|
@@ -626,6 +738,30 @@ exports.dbOps = {
|
|
|
626
738
|
const since = Date.now() - days * 86400000;
|
|
627
739
|
return stmts.getHourlyDistribution.all(since);
|
|
628
740
|
},
|
|
741
|
+
updateSessionSemantic(sessionId, avgOutputChars, errorBlockCount, semanticLoopCount = 0) {
|
|
742
|
+
stmts.updateSessionSemantic.run(avgOutputChars, errorBlockCount, semanticLoopCount, sessionId);
|
|
743
|
+
},
|
|
744
|
+
upsertAssistantTurns(sessionId, turns) {
|
|
745
|
+
db.prepare(`DELETE FROM assistant_turns WHERE session_id = ?`).run(sessionId);
|
|
746
|
+
const insert = db.prepare(`
|
|
747
|
+
INSERT INTO assistant_turns (session_id, turn_index, ts, text_preview, tool_calls, error_count, output_chars, context_used)
|
|
748
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
749
|
+
`);
|
|
750
|
+
for (const t of turns) {
|
|
751
|
+
insert.run(sessionId, t.turn_index, t.ts ?? null, t.text_preview ?? null, JSON.stringify(t.tool_calls ?? []), t.error_count, t.output_chars, t.context_used);
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
getAssistantTurns(sessionId) {
|
|
755
|
+
return stmts.getAssistantTurns.all(sessionId).map(r => ({
|
|
756
|
+
turn_index: r.turn_index,
|
|
757
|
+
ts: r.ts ?? undefined,
|
|
758
|
+
text_preview: r.text_preview ?? undefined,
|
|
759
|
+
tool_calls: r.tool_calls ? JSON.parse(r.tool_calls) : [],
|
|
760
|
+
error_count: r.error_count,
|
|
761
|
+
output_chars: r.output_chars,
|
|
762
|
+
context_used: r.context_used ?? 0,
|
|
763
|
+
}));
|
|
764
|
+
},
|
|
629
765
|
getCacheReadByModel(days) {
|
|
630
766
|
const since = Date.now() - days * 86400000;
|
|
631
767
|
return db.prepare(`
|
|
@@ -648,4 +784,98 @@ exports.dbOps = {
|
|
|
648
784
|
ORDER BY total_cost DESC
|
|
649
785
|
`).all(since);
|
|
650
786
|
},
|
|
787
|
+
// ─── File intent coordination ──────────────────────────────────────────────
|
|
788
|
+
insertIntent(tool, sessionId, taskDesc) {
|
|
789
|
+
const res = db.prepare(`
|
|
790
|
+
INSERT INTO file_intents (tool, session_id, task_desc, status, acquired_at, last_heartbeat)
|
|
791
|
+
VALUES (?, ?, ?, 'active', ?, ?)
|
|
792
|
+
`).run(tool, sessionId, taskDesc ?? null, Date.now(), Date.now());
|
|
793
|
+
return Number(res.lastInsertRowid);
|
|
794
|
+
},
|
|
795
|
+
insertIntentFile(intentId, filePath, operation, lineStart, lineEnd) {
|
|
796
|
+
db.prepare(`
|
|
797
|
+
INSERT INTO file_intent_files (intent_id, file_path, operation, line_start, line_end)
|
|
798
|
+
VALUES (?, ?, ?, ?, ?)
|
|
799
|
+
`).run(intentId, filePath, operation, lineStart ?? null, lineEnd ?? null);
|
|
800
|
+
},
|
|
801
|
+
getWriteConflicts(filePaths, excludeTool) {
|
|
802
|
+
if (filePaths.length === 0)
|
|
803
|
+
return [];
|
|
804
|
+
const placeholders = filePaths.map(() => '?').join(',');
|
|
805
|
+
return db.prepare(`
|
|
806
|
+
SELECT f.file_path, i.tool, i.session_id, i.task_desc, i.acquired_at
|
|
807
|
+
FROM file_intent_files f
|
|
808
|
+
JOIN file_intents i ON i.id = f.intent_id
|
|
809
|
+
WHERE f.file_path IN (${placeholders})
|
|
810
|
+
AND f.operation = 'write'
|
|
811
|
+
AND i.status = 'active'
|
|
812
|
+
AND i.tool != ?
|
|
813
|
+
`).all(...filePaths, excludeTool);
|
|
814
|
+
},
|
|
815
|
+
getIntent(id) {
|
|
816
|
+
return db.prepare(`SELECT tool, session_id, task_desc FROM file_intents WHERE id = ?`).get(id);
|
|
817
|
+
},
|
|
818
|
+
releaseIntent(id) {
|
|
819
|
+
db.prepare(`
|
|
820
|
+
UPDATE file_intents SET status = 'done', released_at = ? WHERE id = ?
|
|
821
|
+
`).run(Date.now(), id);
|
|
822
|
+
},
|
|
823
|
+
heartbeatIntent(id) {
|
|
824
|
+
db.prepare(`
|
|
825
|
+
UPDATE file_intents SET last_heartbeat = ? WHERE id = ? AND status = 'active'
|
|
826
|
+
`).run(Date.now(), id);
|
|
827
|
+
},
|
|
828
|
+
getIntentFiles(id) {
|
|
829
|
+
return db.prepare(`
|
|
830
|
+
SELECT file_path, operation FROM file_intent_files WHERE intent_id = ?
|
|
831
|
+
`).all(id);
|
|
832
|
+
},
|
|
833
|
+
getActiveToolsInProject(projectPath, excludeTool) {
|
|
834
|
+
const since = Date.now() - 10 * 60000;
|
|
835
|
+
return db.prepare(`
|
|
836
|
+
SELECT COALESCE(source, 'claude-code') AS source, MAX(last_event_at) AS last_event_at
|
|
837
|
+
FROM sessions
|
|
838
|
+
WHERE project_path = ?
|
|
839
|
+
AND COALESCE(source, 'claude-code') != ?
|
|
840
|
+
AND last_event_at >= ?
|
|
841
|
+
GROUP BY COALESCE(source, 'claude-code')
|
|
842
|
+
`).all(projectPath, excludeTool, since);
|
|
843
|
+
},
|
|
844
|
+
releaseOrphanedIntents() {
|
|
845
|
+
db.prepare(`UPDATE file_intents SET status = 'stale' WHERE status = 'active'`).run();
|
|
846
|
+
db.prepare(`DELETE FROM file_intents WHERE status IN ('done','stale')`).run();
|
|
847
|
+
},
|
|
848
|
+
markStaleIntents() {
|
|
849
|
+
const cutoff = Date.now() - 10 * 60000;
|
|
850
|
+
const stale = db.prepare(`
|
|
851
|
+
SELECT id, tool, task_desc FROM file_intents
|
|
852
|
+
WHERE status = 'active' AND (last_heartbeat IS NULL OR last_heartbeat < ?)
|
|
853
|
+
`).all(cutoff);
|
|
854
|
+
if (stale.length > 0) {
|
|
855
|
+
const ids = stale.map(s => s.id);
|
|
856
|
+
db.prepare(`UPDATE file_intents SET status = 'stale' WHERE id IN (${ids.map(() => '?').join(',')})`).run(...ids);
|
|
857
|
+
}
|
|
858
|
+
return stale;
|
|
859
|
+
},
|
|
860
|
+
deleteOldStaleIntents() {
|
|
861
|
+
const cutoff = Date.now() - 60 * 60000;
|
|
862
|
+
db.prepare(`DELETE FROM file_intents WHERE status IN ('done','stale') AND COALESCE(released_at, acquired_at) < ?`).run(cutoff);
|
|
863
|
+
},
|
|
864
|
+
hasActiveIntent(tool, filePath) {
|
|
865
|
+
const row = db.prepare(`
|
|
866
|
+
SELECT 1 FROM file_intents i
|
|
867
|
+
JOIN file_intent_files f ON f.intent_id = i.id
|
|
868
|
+
WHERE i.tool = ? AND f.file_path = ? AND i.status = 'active'
|
|
869
|
+
LIMIT 1
|
|
870
|
+
`).get(tool, filePath);
|
|
871
|
+
return !!row;
|
|
872
|
+
},
|
|
873
|
+
getActiveIntents() {
|
|
874
|
+
return db.prepare(`
|
|
875
|
+
SELECT i.id, i.tool, f.file_path
|
|
876
|
+
FROM file_intents i
|
|
877
|
+
JOIN file_intent_files f ON f.intent_id = i.id
|
|
878
|
+
WHERE i.status = 'active'
|
|
879
|
+
`).all();
|
|
880
|
+
},
|
|
651
881
|
};
|
package/dist/doctor.js
CHANGED
|
@@ -131,7 +131,7 @@ async function runDoctor() {
|
|
|
131
131
|
if (activeBinary) {
|
|
132
132
|
try {
|
|
133
133
|
const runningVersion = (0, child_process_1.execSync)(`${activeBinary} --version`, { stdio: 'pipe' })
|
|
134
|
-
.toString().trim().replace(/^v?/, '');
|
|
134
|
+
.toString().split('\n')[0].trim().replace(/^v?/, '');
|
|
135
135
|
versionOk = runningVersion === installedVersion;
|
|
136
136
|
if (!versionOk) {
|
|
137
137
|
versionNote = `Active binary reports v${runningVersion}, installed package is v${installedVersion}`;
|
package/dist/enricher.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ import './watchers/opencode';
|
|
|
16
16
|
import './watchers/amp';
|
|
17
17
|
import './watchers/droid';
|
|
18
18
|
import './watchers/codebuff';
|
|
19
|
-
import type
|
|
20
|
-
export { getAllBlockCostsForSession,
|
|
19
|
+
import { type CostUpdate } from './db';
|
|
20
|
+
export { getAllBlockCostsForSession, getSessionPrompts } from './watchers/claude-code';
|
|
21
|
+
export { getContextWindow } from './pricing';
|
|
21
22
|
export type CostUpdateCallback = (sessionId: string, cost: CostUpdate, source?: string) => void;
|
|
22
23
|
export type CompactDetectedCallback = (sessionId: string) => void;
|
|
23
24
|
export declare function startEnricher(onUpdate: CostUpdateCallback, onCompact?: CompactDetectedCallback): void;
|
package/dist/enricher.js
CHANGED
|
@@ -15,7 +15,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
15
15
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.
|
|
18
|
+
exports.getContextWindow = exports.getSessionPrompts = exports.getAllBlockCostsForSession = void 0;
|
|
19
19
|
exports.startEnricher = startEnricher;
|
|
20
20
|
exports.stopEnricher = stopEnricher;
|
|
21
21
|
exports.cleanupSession = cleanupSession;
|
|
@@ -30,11 +30,13 @@ require("./watchers/opencode");
|
|
|
30
30
|
require("./watchers/amp");
|
|
31
31
|
require("./watchers/droid");
|
|
32
32
|
require("./watchers/codebuff");
|
|
33
|
+
const db_1 = require("./db");
|
|
33
34
|
// Re-export Claude Code-specific utilities for routes/stream and routes/misc
|
|
34
35
|
var claude_code_1 = require("./watchers/claude-code");
|
|
35
36
|
Object.defineProperty(exports, "getAllBlockCostsForSession", { enumerable: true, get: function () { return claude_code_1.getAllBlockCostsForSession; } });
|
|
36
|
-
Object.defineProperty(exports, "getContextWindow", { enumerable: true, get: function () { return claude_code_1.getContextWindow; } });
|
|
37
37
|
Object.defineProperty(exports, "getSessionPrompts", { enumerable: true, get: function () { return claude_code_1.getSessionPrompts; } });
|
|
38
|
+
var pricing_1 = require("./pricing");
|
|
39
|
+
Object.defineProperty(exports, "getContextWindow", { enumerable: true, get: function () { return pricing_1.getContextWindow; } });
|
|
38
40
|
const prevContextBySession = new Map();
|
|
39
41
|
let watcher = null;
|
|
40
42
|
const pendingFiles = new Map();
|
|
@@ -90,7 +92,7 @@ function startEnricher(onUpdate, onCompact) {
|
|
|
90
92
|
console.log(`[enricher] Watching ${adapters.map(a => a.label).join(', ')}`);
|
|
91
93
|
watcher = chokidar_1.default.watch(watchPaths, {
|
|
92
94
|
persistent: true,
|
|
93
|
-
ignoreInitial:
|
|
95
|
+
ignoreInitial: false,
|
|
94
96
|
awaitWriteFinish: {
|
|
95
97
|
stabilityThreshold: 200,
|
|
96
98
|
pollInterval: 100,
|
|
@@ -119,16 +121,19 @@ function startEnricher(onUpdate, onCompact) {
|
|
|
119
121
|
watcher.on('add', handleFile);
|
|
120
122
|
// ─── Poll-based adapters (e.g. OpenCode SQLite) ─────────────────────────────
|
|
121
123
|
const POLL_INTERVAL_MS = 10000;
|
|
124
|
+
const POLL_LOOKBACK_MS = 7 * 24 * 60 * 60000; // backfill 7 days on first start
|
|
122
125
|
for (const adapter of adapters) {
|
|
123
126
|
if (!(0, adapter_1.isPollable)(adapter))
|
|
124
127
|
continue;
|
|
125
|
-
let lastPoll = Date.now();
|
|
128
|
+
let lastPoll = Date.now() - POLL_LOOKBACK_MS;
|
|
126
129
|
const interval = setInterval(async () => {
|
|
127
130
|
const since = lastPoll;
|
|
128
131
|
lastPoll = Date.now();
|
|
129
132
|
const sessions = await adapter.pollSessions(since);
|
|
130
|
-
for (const { sessionId, cost } of sessions) {
|
|
133
|
+
for (const { sessionId, cost, cwd } of sessions) {
|
|
131
134
|
onUpdate(sessionId, cost, adapter.name);
|
|
135
|
+
if (cwd)
|
|
136
|
+
db_1.dbOps.updateSessionProject(sessionId, cwd);
|
|
132
137
|
}
|
|
133
138
|
}, POLL_INTERVAL_MS);
|
|
134
139
|
interval.unref();
|
package/dist/index.js
CHANGED
|
@@ -62,9 +62,20 @@ if (!SKIP_UPDATE_NOTICE.has(subcommand)) {
|
|
|
62
62
|
catch {
|
|
63
63
|
fetchLatestVersion();
|
|
64
64
|
}
|
|
65
|
+
const semverGt = (a, b) => {
|
|
66
|
+
const [pa, pb] = [a.split('.').map(Number), b.split('.').map(Number)];
|
|
67
|
+
for (let i = 0; i < 3; i++) {
|
|
68
|
+
const aVal = pa[i] ?? 0, bVal = pb[i] ?? 0;
|
|
69
|
+
if (aVal > bVal)
|
|
70
|
+
return true;
|
|
71
|
+
if (aVal < bVal)
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
65
76
|
const _exit = process.exit.bind(process);
|
|
66
77
|
process.exit = ((code) => {
|
|
67
|
-
if ((code ?? 0) === 0 && cachedLatest && cachedLatest
|
|
78
|
+
if ((code ?? 0) === 0 && cachedLatest && semverGt(cachedLatest, PKG_VERSION)) {
|
|
68
79
|
console.log(`\n ✦ Update available: ${PKG_VERSION} → ${cachedLatest}`);
|
|
69
80
|
console.log(` Run: npm install -g @statforge/claudestat\n`);
|
|
70
81
|
}
|