@statforge/claudestat 1.6.0 → 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.
Files changed (49) hide show
  1. package/README.md +3 -1
  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/daemon.js +58 -2
  14. package/dist/db.d.ts +76 -2
  15. package/dist/db.js +295 -65
  16. package/dist/doctor.js +1 -1
  17. package/dist/enricher.d.ts +3 -2
  18. package/dist/enricher.js +12 -5
  19. package/dist/index.js +12 -1
  20. package/dist/intelligence.d.ts +55 -0
  21. package/dist/intelligence.js +163 -1
  22. package/dist/paths.d.ts +10 -0
  23. package/dist/paths.js +17 -0
  24. package/dist/pricing.d.ts +2 -0
  25. package/dist/pricing.js +12 -1
  26. package/dist/routes/events.js +136 -5
  27. package/dist/routes/history.js +6 -2
  28. package/dist/routes/intents.d.ts +1 -0
  29. package/dist/routes/intents.js +155 -0
  30. package/dist/routes/misc.js +132 -4
  31. package/dist/routes/opencode-reader.js +42 -8
  32. package/dist/routes/projects.js +10 -1
  33. package/dist/routes/replay.d.ts +1 -0
  34. package/dist/routes/replay.js +29 -0
  35. package/dist/routes/reports.js +7 -0
  36. package/dist/routes/stream.js +1 -1
  37. package/dist/routes/top.js +8 -1
  38. package/dist/watchers/adapter.d.ts +1 -0
  39. package/dist/watchers/claude-code.d.ts +16 -1
  40. package/dist/watchers/claude-code.js +201 -76
  41. package/dist/watchers/opencode.d.ts +1 -0
  42. package/dist/watchers/opencode.js +161 -23
  43. package/package.json +1 -1
  44. package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
  45. package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
  46. package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
  47. package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
  48. package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
  49. package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
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,6 +69,8 @@ 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");
@@ -91,6 +95,8 @@ app.use(misc_1.miscRouter);
91
95
  app.use(reports_1.reportsRouter);
92
96
  app.use(top_1.topRouter);
93
97
  app.use(opencode_reader_1.opencodeReaderRouter);
98
+ app.use(replay_1.replayRouter);
99
+ app.use(intents_1.intentsRouter);
94
100
  // ─── GET /health — necesita acceso al tamaño del pool SSE ─────────────────────
95
101
  app.get('/health', (_req, res) => {
96
102
  res.json({ status: 'ok', port: PORT, clients: (0, stream_1.getSseClientsSize)() });
@@ -112,9 +118,10 @@ app.get('*', (_req, res) => {
112
118
  // ─── Migración de arranque: etiquetar sesiones históricas ────────────────────
113
119
  function migrateSessionProjects() {
114
120
  const sessions = db_1.dbOps.getAllSessions();
121
+ const HOME = os_1.default.homedir();
115
122
  let tagged = 0;
116
123
  for (const session of sessions) {
117
- if (session?.project_path)
124
+ if (session?.project_path && session.project_path !== HOME)
118
125
  continue;
119
126
  const events = db_1.dbOps.getSessionEvents(session.id);
120
127
  const projectCwd = (0, projects_1.inferProjectCwd)(events);
@@ -149,10 +156,14 @@ async function migrateSessionSummaries(limit = 5) {
149
156
  }
150
157
  }
151
158
  }
159
+ // ─── Previous hashes map for external change detection ──────────────────────────
160
+ let _prevHashes = new Map();
152
161
  // ─── Interval refs for cleanup ────────────────────────────────────────────────
153
162
  let projectCacheInterval = null;
154
163
  let reportInterval = null;
155
164
  let alertInterval = null;
165
+ let intentCleanupInterval = null;
166
+ let intentWatchInterval = null;
156
167
  function shutdown(server) {
157
168
  (0, enricher_1.stopEnricher)();
158
169
  (0, rate_limiter_1.stopRateLimiter)();
@@ -168,6 +179,15 @@ function shutdown(server) {
168
179
  clearInterval(alertInterval);
169
180
  alertInterval = null;
170
181
  }
182
+ if (intentCleanupInterval) {
183
+ clearInterval(intentCleanupInterval);
184
+ intentCleanupInterval = null;
185
+ }
186
+ if (intentWatchInterval) {
187
+ clearInterval(intentWatchInterval);
188
+ intentWatchInterval = null;
189
+ }
190
+ _prevHashes.clear();
171
191
  cleanPid();
172
192
  server.close(() => { });
173
193
  }
@@ -248,7 +268,7 @@ function startDaemon() {
248
268
  process.on('SIGINT', () => { if (_server)
249
269
  shutdown(_server); process.exit(0); });
250
270
  console.log(`\n● claudestat daemon → http://localhost:${PORT}`);
251
- console.log(` Waiting for Claude Code events...\n`);
271
+ console.log(` Watching for events...\n`);
252
272
  console.log(` In another terminal: \x1b[36mclaudestat watch\x1b[0m\n`);
253
273
  // Weekly insight — se muestra una vez por semana al iniciar el daemon
254
274
  Promise.resolve().then(() => __importStar(require('./insights'))).then(({ getWeeklyInsightData, shouldShowInsight, markInsightShown, renderWeeklyInsight }) => {
@@ -293,6 +313,42 @@ function startDaemon() {
293
313
  db_1.dbOps.insertWeeklyReport(dateLabel, markdown);
294
314
  console.log(`[daemon] Report auto-generated: ${dateLabel}`);
295
315
  }, 60000);
316
+ // Al arrancar: liberar intents activos de sesiones anteriores (ya no hay nadie que los use)
317
+ db_1.dbOps.releaseOrphanedIntents();
318
+ // Cleanup de intents — cada 2min marca stale los sin heartbeat > 10min, borra viejos > 1h
319
+ intentCleanupInterval = setInterval(() => {
320
+ db_1.dbOps.markStaleIntents();
321
+ db_1.dbOps.deleteOldStaleIntents();
322
+ }, 2 * 60000);
323
+ // Watcher de cambios externos — cada 10s compara hashes de archivos en intents activos
324
+ intentWatchInterval = setInterval(() => {
325
+ const active = db_1.dbOps.getActiveIntents();
326
+ if (active.length === 0)
327
+ return;
328
+ const fileSet = new Set(active.map(a => a.file_path));
329
+ for (const filePath of fileSet) {
330
+ try {
331
+ if (!fs_1.default.existsSync(filePath))
332
+ continue;
333
+ const content = fs_1.default.readFileSync(filePath);
334
+ const hash = crypto_1.default.createHash('sha256').update(content).digest('hex');
335
+ const prev = _prevHashes.get(filePath);
336
+ if (prev && prev !== hash) {
337
+ // Check if any active intent covers this file
338
+ const coveringIntent = active.find(a => a.file_path === filePath);
339
+ const tool = coveringIntent?.tool ?? 'unknown';
340
+ (0, stream_1.broadcast)({ type: 'external_change', payload: { file: filePath, tool, hash_prev: prev, hash_now: hash } });
341
+ }
342
+ _prevHashes.set(filePath, hash);
343
+ }
344
+ catch { }
345
+ }
346
+ // Cleanup stale entries from prevHashes
347
+ for (const [fp] of _prevHashes) {
348
+ if (!fileSet.has(fp))
349
+ _prevHashes.delete(fp);
350
+ }
351
+ }, 10000);
296
352
  // Summaries IA solo si opt-in explícito (CLAUDESTAT_AI_SUMMARY=true)
297
353
  if (process.env.CLAUDESTAT_AI_SUMMARY === 'true') {
298
354
  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;
@@ -158,6 +192,9 @@ export declare const dbOps: {
158
192
  hour: number;
159
193
  session_count: number;
160
194
  }[];
195
+ updateSessionSemantic(sessionId: string, avgOutputChars: number, errorBlockCount: number, semanticLoopCount?: number): void;
196
+ upsertAssistantTurns(sessionId: string, turns: AssistantTurnRow[]): void;
197
+ getAssistantTurns(sessionId: string): AssistantTurnRow[];
161
198
  getCacheReadByModel(days: number): {
162
199
  model: string;
163
200
  cache_read: number;
@@ -167,4 +204,41 @@ export declare const dbOps: {
167
204
  total_cost: number;
168
205
  session_count: number;
169
206
  }[];
207
+ insertIntent(tool: string, sessionId: string, taskDesc: string | undefined): number;
208
+ insertIntentFile(intentId: number, filePath: string, operation: string, lineStart?: number, lineEnd?: number): void;
209
+ getWriteConflicts(filePaths: string[], excludeTool: string): {
210
+ file_path: string;
211
+ tool: string;
212
+ session_id: string;
213
+ task_desc: string | null;
214
+ acquired_at: number;
215
+ }[];
216
+ getIntent(id: number): {
217
+ tool: string;
218
+ session_id: string;
219
+ task_desc: string | null;
220
+ } | undefined;
221
+ releaseIntent(id: number): void;
222
+ heartbeatIntent(id: number): void;
223
+ getIntentFiles(id: number): {
224
+ file_path: string;
225
+ operation: string;
226
+ }[];
227
+ getActiveToolsInProject(projectPath: string, excludeTool: string): {
228
+ source: string;
229
+ last_event_at: number;
230
+ }[];
231
+ releaseOrphanedIntents(): void;
232
+ markStaleIntents(): {
233
+ id: number;
234
+ tool: string;
235
+ task_desc: string | null;
236
+ }[];
237
+ deleteOldStaleIntents(): void;
238
+ hasActiveIntent(tool: string, filePath: string): boolean;
239
+ getActiveIntents(): {
240
+ id: number;
241
+ tool: string;
242
+ file_path: string;
243
+ }[];
170
244
  };