@statforge/claudestat 1.6.1 → 1.8.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.
Files changed (63) hide show
  1. package/README.md +36 -3
  2. package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
  3. package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
  4. package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
  5. package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
  6. package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
  7. package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
  8. package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
  9. package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
  10. package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
  11. package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
  12. package/dashboard/dist/index.html +3 -3
  13. package/dist/config.d.ts +7 -0
  14. package/dist/config.js +36 -0
  15. package/dist/daemon.js +113 -9
  16. package/dist/db.d.ts +87 -2
  17. package/dist/db.js +325 -65
  18. package/dist/doctor.js +21 -3
  19. package/dist/enricher.d.ts +3 -2
  20. package/dist/enricher.js +10 -5
  21. package/dist/export.d.ts +2 -1
  22. package/dist/export.js +41 -6
  23. package/dist/index.js +406 -20
  24. package/dist/insights.d.ts +1 -0
  25. package/dist/insights.js +26 -0
  26. package/dist/install.js +28 -1
  27. package/dist/intelligence.d.ts +66 -4
  28. package/dist/intelligence.js +205 -17
  29. package/dist/logger.d.ts +6 -0
  30. package/dist/logger.js +49 -0
  31. package/dist/notifier.d.ts +15 -0
  32. package/dist/notifier.js +26 -0
  33. package/dist/paths.d.ts +23 -0
  34. package/dist/paths.js +42 -0
  35. package/dist/pricing.d.ts +2 -0
  36. package/dist/pricing.js +12 -1
  37. package/dist/routes/events.js +136 -5
  38. package/dist/routes/helpers.d.ts +5 -0
  39. package/dist/routes/helpers.js +21 -1
  40. package/dist/routes/history.js +6 -2
  41. package/dist/routes/intents.d.ts +1 -0
  42. package/dist/routes/intents.js +155 -0
  43. package/dist/routes/misc.js +150 -4
  44. package/dist/routes/opencode-reader.js +39 -3
  45. package/dist/routes/projects.js +19 -1
  46. package/dist/routes/replay.d.ts +1 -0
  47. package/dist/routes/replay.js +29 -0
  48. package/dist/routes/reports.js +7 -0
  49. package/dist/routes/top.js +8 -1
  50. package/dist/service.js +11 -0
  51. package/dist/watchers/adapter.d.ts +1 -0
  52. package/dist/watchers/claude-code.d.ts +16 -1
  53. package/dist/watchers/claude-code.js +201 -76
  54. package/dist/watchers/opencode.d.ts +1 -0
  55. package/dist/watchers/opencode.js +152 -14
  56. package/hooks/event.js +44 -26
  57. package/package.json +1 -1
  58. package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
  59. package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
  60. package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
  61. package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
  62. package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
  63. package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
package/dist/config.js CHANGED
@@ -43,6 +43,13 @@ const DEFAULTS = {
43
43
  reportDay: 1,
44
44
  reportTime: '09:00',
45
45
  alertsEnabled: true,
46
+ killSwitchForce: false,
47
+ logLevel: 'info',
48
+ port: 7337,
49
+ loopThreshold: 8,
50
+ loopWindowSecs: 120,
51
+ projectAliases: {},
52
+ webhookUrl: null,
46
53
  };
47
54
  /** Lee la config del disco. Valores ausentes se rellenan con defaults. */
48
55
  function readConfig() {
@@ -100,6 +107,30 @@ function validateConfig(raw) {
100
107
  }
101
108
  if ('alertsEnabled' in cfg && typeof cfg.alertsEnabled !== 'boolean')
102
109
  return 'alertsEnabled debe ser boolean';
110
+ if ('killSwitchForce' in cfg && typeof cfg.killSwitchForce !== 'boolean')
111
+ return 'killSwitchForce must be boolean';
112
+ if ('logLevel' in cfg && !['debug', 'info', 'warn', 'error'].includes(cfg.logLevel))
113
+ return 'logLevel must be: debug, info, warn, error';
114
+ if ('loopThreshold' in cfg) {
115
+ const v = cfg.loopThreshold;
116
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 2 || v > 50)
117
+ return 'loopThreshold must be an integer between 2 and 50';
118
+ }
119
+ if ('loopWindowSecs' in cfg) {
120
+ const v = cfg.loopWindowSecs;
121
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 10 || v > 600)
122
+ return 'loopWindowSecs must be an integer between 10 and 600';
123
+ }
124
+ if ('projectAliases' in cfg) {
125
+ const v = cfg.projectAliases;
126
+ if (typeof v !== 'object' || v === null || Array.isArray(v))
127
+ return 'projectAliases must be an object { "/path": "Alias" }';
128
+ }
129
+ if ('webhookUrl' in cfg) {
130
+ const v = cfg.webhookUrl;
131
+ if (v !== null && (typeof v !== 'string' || (!v.startsWith('http://') && !v.startsWith('https://'))))
132
+ return 'webhookUrl must be null or a valid http/https URL';
133
+ }
103
134
  if ('reportsEnabled' in cfg && typeof cfg.reportsEnabled !== 'boolean')
104
135
  return 'reportsEnabled debe ser boolean';
105
136
  if ('reportFrequency' in cfg && !['weekly', 'biweekly', 'monthly'].includes(cfg.reportFrequency))
@@ -113,6 +144,11 @@ function validateConfig(raw) {
113
144
  if (typeof cfg.reportTime !== 'string' || !/^\d{2}:\d{2}$/.test(cfg.reportTime))
114
145
  return 'reportTime debe tener formato HH:MM';
115
146
  }
147
+ if ('port' in cfg) {
148
+ const v = cfg.port;
149
+ if (typeof v !== 'number' || !Number.isInteger(v) || v < 1024 || v > 65535)
150
+ return 'port must be an integer between 1024 and 65535';
151
+ }
116
152
  return null;
117
153
  }
118
154
  /** Devuelve el nivel de warning para un % dado, o null si no alcanza ningún threshold. */
package/dist/daemon.js CHANGED
@@ -51,9 +51,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  };
52
52
  Object.defineProperty(exports, "__esModule", { value: true });
53
53
  exports.startDaemon = startDaemon;
54
+ const crypto_1 = __importDefault(require("crypto"));
54
55
  const express_1 = __importDefault(require("express"));
55
56
  const path_1 = __importDefault(require("path"));
56
57
  const fs_1 = __importDefault(require("fs"));
58
+ const os_1 = __importDefault(require("os"));
57
59
  const db_1 = require("./db");
58
60
  const enricher_1 = require("./enricher");
59
61
  const config_1 = require("./config");
@@ -67,11 +69,14 @@ const misc_1 = require("./routes/misc");
67
69
  const reports_1 = require("./routes/reports");
68
70
  const top_1 = require("./routes/top");
69
71
  const opencode_reader_1 = require("./routes/opencode-reader");
72
+ const replay_1 = require("./routes/replay");
73
+ const intents_1 = require("./routes/intents");
70
74
  const projects_cache_1 = require("./cache/projects-cache");
71
75
  const rate_limiter_1 = require("./middleware/rate-limiter");
72
76
  const summarizer_1 = require("./summarizer");
73
77
  const paths_1 = require("./paths");
74
- const PORT = 7337;
78
+ const logger_1 = require("./logger");
79
+ const PORT = (0, config_1.readConfig)().port;
75
80
  const app = (0, express_1.default)();
76
81
  app.use(express_1.default.json());
77
82
  // ─── Shutdown graceful (cross-platform, no depende de SIGTERM) ────────────────
@@ -91,6 +96,8 @@ app.use(misc_1.miscRouter);
91
96
  app.use(reports_1.reportsRouter);
92
97
  app.use(top_1.topRouter);
93
98
  app.use(opencode_reader_1.opencodeReaderRouter);
99
+ app.use(replay_1.replayRouter);
100
+ app.use(intents_1.intentsRouter);
94
101
  // ─── GET /health — necesita acceso al tamaño del pool SSE ─────────────────────
95
102
  app.get('/health', (_req, res) => {
96
103
  res.json({ status: 'ok', port: PORT, clients: (0, stream_1.getSseClientsSize)() });
@@ -112,9 +119,10 @@ app.get('*', (_req, res) => {
112
119
  // ─── Migración de arranque: etiquetar sesiones históricas ────────────────────
113
120
  function migrateSessionProjects() {
114
121
  const sessions = db_1.dbOps.getAllSessions();
122
+ const HOME = os_1.default.homedir();
115
123
  let tagged = 0;
116
124
  for (const session of sessions) {
117
- if (session?.project_path)
125
+ if (session?.project_path && session.project_path !== HOME)
118
126
  continue;
119
127
  const events = db_1.dbOps.getSessionEvents(session.id);
120
128
  const projectCwd = (0, projects_1.inferProjectCwd)(events);
@@ -124,7 +132,7 @@ function migrateSessionProjects() {
124
132
  }
125
133
  }
126
134
  if (tagged > 0)
127
- console.log(`[daemon] ${tagged} sessions tagged with project`);
135
+ logger_1.logger.info(`${tagged} sessions tagged with project`);
128
136
  }
129
137
  /**
130
138
  * Genera summaries IA para las últimas N sesiones que no tienen uno.
@@ -141,18 +149,22 @@ async function migrateSessionSummaries(limit = 5) {
141
149
  const summary = await (0, summarizer_1.summarizeSession)(events, s.total_cost_usd ?? 0, projectName);
142
150
  if (summary) {
143
151
  db_1.dbOps.updateSessionSummary(s.id, summary);
144
- console.log(`[daemon] Summary generated for session ${s.id.slice(0, 8)}: "${summary}"`);
152
+ logger_1.logger.info(`Summary generated for session ${s.id.slice(0, 8)}: "${summary}"`);
145
153
  }
146
154
  }
147
155
  catch (err) {
148
- console.error('[daemon] Error generating summary:', err);
156
+ logger_1.logger.error(`Error generating summary: ${err}`);
149
157
  }
150
158
  }
151
159
  }
160
+ // ─── Previous hashes map for external change detection ──────────────────────────
161
+ let _prevHashes = new Map();
152
162
  // ─── Interval refs for cleanup ────────────────────────────────────────────────
153
163
  let projectCacheInterval = null;
154
164
  let reportInterval = null;
155
165
  let alertInterval = null;
166
+ let intentCleanupInterval = null;
167
+ let intentWatchInterval = null;
156
168
  function shutdown(server) {
157
169
  (0, enricher_1.stopEnricher)();
158
170
  (0, rate_limiter_1.stopRateLimiter)();
@@ -168,6 +180,15 @@ function shutdown(server) {
168
180
  clearInterval(alertInterval);
169
181
  alertInterval = null;
170
182
  }
183
+ if (intentCleanupInterval) {
184
+ clearInterval(intentCleanupInterval);
185
+ intentCleanupInterval = null;
186
+ }
187
+ if (intentWatchInterval) {
188
+ clearInterval(intentWatchInterval);
189
+ intentWatchInterval = null;
190
+ }
191
+ _prevHashes.clear();
171
192
  cleanPid();
172
193
  server.close(() => { });
173
194
  }
@@ -186,6 +207,7 @@ function checkAlertLevel(level, lastLevel, logMsg, notifTitle, notifBody) {
186
207
  const prevRank = lastLevel ? LEVEL_RANK[lastLevel] ?? 0 : 0;
187
208
  const currRank = LEVEL_RANK[level];
188
209
  if (currRank > prevRank) {
210
+ logger_1.logger.warn(logMsg);
189
211
  process.stderr.write(`${LEVEL_COLOR[level]}${logMsg}\x1b[0m\n`);
190
212
  (0, notifier_1.sendDesktopNotification)(notifTitle, notifBody);
191
213
  }
@@ -200,9 +222,37 @@ function startAlertPolling() {
200
222
  const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
201
223
  const resetMins = Math.ceil(data.cycleResetMs / 60000);
202
224
  // ── Cycle 5h alerts ──────────────────────────────────────────────────────
203
- _lastCycleAlertLevel = checkAlertLevel((0, config_1.getWarnLevel)(data.cyclePct, cfg.warnThresholds), _lastCycleAlertLevel, `[claudestat] ⚠️ 5h cycle at ${data.cyclePct}% (${data.cyclePrompts}/${data.cycleLimit} prompts)`, 'claudestat — 5h cycle alert', `${data.cyclePct}% of cycle used · resets in ${resetMins}m`);
225
+ const cycleLevel = (0, config_1.getWarnLevel)(data.cyclePct, cfg.warnThresholds);
226
+ if (cycleLevel && cycleLevel !== _lastCycleAlertLevel) {
227
+ const webhookUrl = (0, config_1.readConfig)().webhookUrl;
228
+ if (webhookUrl) {
229
+ (0, notifier_1.sendWebhookAlert)(webhookUrl, {
230
+ title: 'claudestat — 5h cycle alert',
231
+ body: `${data.cyclePct}% of cycle used · resets in ${resetMins}m`,
232
+ level: cycleLevel,
233
+ cyclePct: data.cyclePct,
234
+ resetInMins: resetMins,
235
+ burnRate: data.burnRateTokensPerMin,
236
+ });
237
+ }
238
+ }
239
+ _lastCycleAlertLevel = checkAlertLevel(cycleLevel, _lastCycleAlertLevel, `[claudestat] ⚠️ 5h cycle at ${data.cyclePct}% (${data.cyclePrompts}/${data.cycleLimit} prompts)`, 'claudestat — 5h cycle alert', `${data.cyclePct}% of cycle used · resets in ${resetMins}m`);
204
240
  // ── Weekly alerts ────────────────────────────────────────────────────────
205
- _lastWeeklyAlertLevel = checkAlertLevel((0, config_1.getWarnLevel)(data.weeklyPctAll, cfg.weeklyWarnThresholds), _lastWeeklyAlertLevel, `[claudestat] ⚠️ Weekly usage at ${data.weeklyPctAll}%`, 'claudestat — Weekly usage alert', `${data.weeklyPctAll}% of weekly quota used`);
241
+ const weeklyLevel = (0, config_1.getWarnLevel)(data.weeklyPctAll, cfg.weeklyWarnThresholds);
242
+ if (weeklyLevel && weeklyLevel !== _lastWeeklyAlertLevel) {
243
+ const webhookUrl = (0, config_1.readConfig)().webhookUrl;
244
+ if (webhookUrl) {
245
+ (0, notifier_1.sendWebhookAlert)(webhookUrl, {
246
+ title: 'claudestat — Weekly usage alert',
247
+ body: `${data.weeklyPctAll}% of weekly quota used`,
248
+ level: weeklyLevel,
249
+ cyclePct: data.cyclePct,
250
+ weeklyPct: data.weeklyPctAll,
251
+ resetInMins: resetMins,
252
+ });
253
+ }
254
+ }
255
+ _lastWeeklyAlertLevel = checkAlertLevel(weeklyLevel, _lastWeeklyAlertLevel, `[claudestat] ⚠️ Weekly usage at ${data.weeklyPctAll}%`, 'claudestat — Weekly usage alert', `${data.weeklyPctAll}% of weekly quota used`);
206
256
  // ── Reset reminder ───────────────────────────────────────────────────────
207
257
  const reminderMs = (cfg.resetReminderMins ?? 10) * 60000;
208
258
  if (reminderMs > 0) {
@@ -211,11 +261,28 @@ function startAlertPolling() {
211
261
  }
212
262
  else if (data.cycleResetMs <= reminderMs && data.cycleResetMs > 0 && !_resetReminderFired) {
213
263
  const mins = Math.ceil(data.cycleResetMs / 60000);
264
+ logger_1.logger.info(`Quota resets in ${mins}m — good time to wrap up`);
214
265
  process.stderr.write(`\x1b[36m[claudestat] ⏰ Quota resets in ${mins}m — good time to wrap up\x1b[0m\n`);
215
266
  (0, notifier_1.sendDesktopNotification)('claudestat — Quota reset soon', `Your 5h cycle resets in ${mins} min — good time to start a new task`);
216
267
  _resetReminderFired = true;
217
268
  }
218
269
  }
270
+ // Write or remove pause signal file based on kill switch state
271
+ const signalFile = (0, paths_1.getPauseSignalFile)();
272
+ if (cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold) {
273
+ const msg = `Quota at ${data.cyclePct}% — threshold is ${cfg.killSwitchThreshold}%. Resets in ${Math.ceil(data.cycleResetMs / 60000)}m.`;
274
+ try {
275
+ fs_1.default.writeFileSync(signalFile, msg);
276
+ }
277
+ catch { }
278
+ (0, stream_1.broadcast)({ type: 'kill_switch', payload: { blocked: true, reason: msg } });
279
+ }
280
+ else {
281
+ try {
282
+ fs_1.default.unlinkSync(signalFile);
283
+ }
284
+ catch { }
285
+ }
219
286
  }
220
287
  catch {
221
288
  // quota read failed — ignore
@@ -242,6 +309,7 @@ function cleanPid() {
242
309
  function startDaemon() {
243
310
  _server = app.listen(PORT, '127.0.0.1', () => {
244
311
  writePid();
312
+ (0, paths_1.writePortFile)(PORT);
245
313
  process.on('exit', cleanPid);
246
314
  process.on('SIGTERM', () => { if (_server)
247
315
  shutdown(_server); process.exit(0); });
@@ -269,7 +337,7 @@ function startDaemon() {
269
337
  // Se ejecuta en background para no retrasar el inicio del servidor
270
338
  setImmediate(() => {
271
339
  const projects = (0, projects_cache_1.getProjectsCached)();
272
- console.log(`[daemon] ${projects?.length ?? 0} projects scanned`);
340
+ logger_1.logger.info(`${projects?.length ?? 0} projects scanned`);
273
341
  });
274
342
  // Refresh automático del cache de proyectos cada 2 minutos
275
343
  // Recoge cambios en HANDOFF.md aunque el daemon lleve horas corriendo
@@ -291,8 +359,44 @@ function startDaemon() {
291
359
  return; // ya existe
292
360
  const markdown = (0, reports_1.generateReport)(dateLabel, cfg);
293
361
  db_1.dbOps.insertWeeklyReport(dateLabel, markdown);
294
- console.log(`[daemon] Report auto-generated: ${dateLabel}`);
362
+ logger_1.logger.info(`Report auto-generated: ${dateLabel}`);
295
363
  }, 60000);
364
+ // Al arrancar: liberar intents activos de sesiones anteriores (ya no hay nadie que los use)
365
+ db_1.dbOps.releaseOrphanedIntents();
366
+ // Cleanup de intents — cada 2min marca stale los sin heartbeat > 10min, borra viejos > 1h
367
+ intentCleanupInterval = setInterval(() => {
368
+ db_1.dbOps.markStaleIntents();
369
+ db_1.dbOps.deleteOldStaleIntents();
370
+ }, 2 * 60000);
371
+ // Watcher de cambios externos — cada 10s compara hashes de archivos en intents activos
372
+ intentWatchInterval = setInterval(() => {
373
+ const active = db_1.dbOps.getActiveIntents();
374
+ if (active.length === 0)
375
+ return;
376
+ const fileSet = new Set(active.map(a => a.file_path));
377
+ for (const filePath of fileSet) {
378
+ try {
379
+ if (!fs_1.default.existsSync(filePath))
380
+ continue;
381
+ const content = fs_1.default.readFileSync(filePath);
382
+ const hash = crypto_1.default.createHash('sha256').update(content).digest('hex');
383
+ const prev = _prevHashes.get(filePath);
384
+ if (prev && prev !== hash) {
385
+ // Check if any active intent covers this file
386
+ const coveringIntent = active.find(a => a.file_path === filePath);
387
+ const tool = coveringIntent?.tool ?? 'unknown';
388
+ (0, stream_1.broadcast)({ type: 'external_change', payload: { file: filePath, tool, hash_prev: prev, hash_now: hash } });
389
+ }
390
+ _prevHashes.set(filePath, hash);
391
+ }
392
+ catch { }
393
+ }
394
+ // Cleanup stale entries from prevHashes
395
+ for (const [fp] of _prevHashes) {
396
+ if (!fileSet.has(fp))
397
+ _prevHashes.delete(fp);
398
+ }
399
+ }, 10000);
296
400
  // Summaries IA solo si opt-in explícito (CLAUDESTAT_AI_SUMMARY=true)
297
401
  if (process.env.CLAUDESTAT_AI_SUMMARY === 'true') {
298
402
  migrateSessionSummaries(5).catch(() => { });
package/dist/db.d.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  * El warning "ExperimentalWarning" se suprime en index.ts.
10
10
  */
11
11
  export declare const CLAUDESTAT_DIR: string;
12
+ export declare const TOOL_RESPONSE_MAX_BYTES = 8192;
13
+ export declare function capToolResponse(raw: string): string;
12
14
  export interface SessionRow {
13
15
  id: string;
14
16
  cwd?: string;
@@ -26,6 +28,22 @@ export interface SessionRow {
26
28
  dominant_model?: string;
27
29
  parent_session_id?: string;
28
30
  source?: string;
31
+ exact_retries?: number;
32
+ error_rate?: number;
33
+ file_churn_score?: number;
34
+ seq_cycle_count?: number;
35
+ avg_output_chars?: number;
36
+ error_block_count?: number;
37
+ semantic_loop_count?: number;
38
+ }
39
+ export interface AssistantTurnRow {
40
+ turn_index: number;
41
+ ts?: number;
42
+ text_preview?: string;
43
+ tool_calls?: string[];
44
+ error_count: number;
45
+ output_chars: number;
46
+ context_used: number;
29
47
  }
30
48
  export interface EventRow {
31
49
  id?: number;
@@ -58,17 +76,20 @@ export interface CostUpdate {
58
76
  lastEntry?: BlockCostEntry;
59
77
  lastModel?: string;
60
78
  firstTs?: number;
79
+ lastTs?: number;
61
80
  }
62
81
  export declare const dbOps: {
63
82
  upsertSession(s: SessionRow): void;
64
83
  insertEvent(e: EventRow): number;
84
+ insertOcEvent(sessionId: string, toolName: string, ts: number, externalId: string): void;
85
+ insertSessionIfAbsent(s: SessionRow): void;
65
86
  /**
66
87
  * Al llegar PostToolUse, actualizamos el PreToolUse pendiente más reciente
67
88
  * del mismo tool para esta sesión. Esto convierte el par Pre+Post en
68
89
  * un único registro de tipo 'Done' con duration_ms calculado.
69
90
  */
70
91
  pairPostWithPre(sessionId: string, toolName: string, response: string, postTs: number): number | null;
71
- updateSessionCost(sessionId: string, cost: CostUpdate, efficiencyScore: number, loopsDetected: number): void;
92
+ updateSessionCost(sessionId: string, cost: CostUpdate, efficiencyScore: number, loopsDetected: number, exactRetries?: number, errorRate?: number, fileChurnScore?: number, seqCycleCount?: number): void;
72
93
  getSessionEvents(sessionId: string): EventRow[];
73
94
  getSession(sessionId: string): SessionRow | undefined;
74
95
  getLatestSession(): SessionRow | undefined;
@@ -83,6 +104,11 @@ export declare const dbOps: {
83
104
  count: number;
84
105
  }[];
85
106
  getProjectSessionStats(projectPath: string): any;
107
+ getProjectCliHours(): {
108
+ project_path: string;
109
+ source: string;
110
+ total_ms: number;
111
+ }[];
86
112
  updateSessionSummary(sessionId: string, summary: string): void;
87
113
  updateSessionParent(sessionId: string, parentId: string): void;
88
114
  getChildSessions(parentSessionId: string): {
@@ -125,8 +151,16 @@ export declare const dbOps: {
125
151
  getAnalyticsDaily(since: number): any[];
126
152
  getAnalyticsByModel(since: number): any[];
127
153
  getProjectHours(since: number): any[];
128
- getTopTools(days?: number, by?: "cost" | "count" | "duration", limit?: number): {
154
+ getCoachEvents(sessionIds: string[]): {
155
+ session_id: string;
156
+ type: string;
157
+ tool_name: string | null;
158
+ tool_input: string | null;
159
+ ts: number;
160
+ }[];
161
+ getTopTools(days?: number, by?: "cost" | "count" | "duration", limit?: number, source?: "all" | "claude-code" | "opencode"): {
129
162
  tool_name: string;
163
+ source: string;
130
164
  count: number;
131
165
  total_duration_ms: number;
132
166
  total_cost_usd: number;
@@ -142,6 +176,17 @@ export declare const dbOps: {
142
176
  week_start: number;
143
177
  week_end: number;
144
178
  };
179
+ getPrevWeekInsight(): {
180
+ total_sessions: any;
181
+ total_cost: any;
182
+ input_tokens: any;
183
+ output_tokens: any;
184
+ cache_read: any;
185
+ total_loops: any;
186
+ avg_efficiency: any;
187
+ week_start: any;
188
+ week_end: any;
189
+ };
145
190
  setMeta(key: string, value: string): void;
146
191
  getMeta(key: string): string | undefined;
147
192
  getCostProjection(days?: number): {
@@ -158,6 +203,9 @@ export declare const dbOps: {
158
203
  hour: number;
159
204
  session_count: number;
160
205
  }[];
206
+ updateSessionSemantic(sessionId: string, avgOutputChars: number, errorBlockCount: number, semanticLoopCount?: number): void;
207
+ upsertAssistantTurns(sessionId: string, turns: AssistantTurnRow[]): void;
208
+ getAssistantTurns(sessionId: string): AssistantTurnRow[];
161
209
  getCacheReadByModel(days: number): {
162
210
  model: string;
163
211
  cache_read: number;
@@ -167,4 +215,41 @@ export declare const dbOps: {
167
215
  total_cost: number;
168
216
  session_count: number;
169
217
  }[];
218
+ insertIntent(tool: string, sessionId: string, taskDesc: string | undefined): number;
219
+ insertIntentFile(intentId: number, filePath: string, operation: string, lineStart?: number, lineEnd?: number): void;
220
+ getWriteConflicts(filePaths: string[], excludeTool: string): {
221
+ file_path: string;
222
+ tool: string;
223
+ session_id: string;
224
+ task_desc: string | null;
225
+ acquired_at: number;
226
+ }[];
227
+ getIntent(id: number): {
228
+ tool: string;
229
+ session_id: string;
230
+ task_desc: string | null;
231
+ } | undefined;
232
+ releaseIntent(id: number): void;
233
+ heartbeatIntent(id: number): void;
234
+ getIntentFiles(id: number): {
235
+ file_path: string;
236
+ operation: string;
237
+ }[];
238
+ getActiveToolsInProject(projectPath: string, excludeTool: string): {
239
+ source: string;
240
+ last_event_at: number;
241
+ }[];
242
+ releaseOrphanedIntents(): void;
243
+ markStaleIntents(): {
244
+ id: number;
245
+ tool: string;
246
+ task_desc: string | null;
247
+ }[];
248
+ deleteOldStaleIntents(): void;
249
+ hasActiveIntent(tool: string, filePath: string): boolean;
250
+ getActiveIntents(): {
251
+ id: number;
252
+ tool: string;
253
+ file_path: string;
254
+ }[];
170
255
  };