@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
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ // ─── /api/intent/* — Multi-tool file coordination ─────────────────────────────
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.intentsRouter = void 0;
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const express_1 = require("express");
11
+ const db_1 = require("../db");
12
+ const stream_1 = require("./stream");
13
+ function computeHash(filePath) {
14
+ try {
15
+ if (!fs_1.default.existsSync(filePath))
16
+ return null;
17
+ const content = fs_1.default.readFileSync(filePath);
18
+ return crypto_1.default.createHash('sha256').update(content).digest('hex');
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ // Middleware para validar required `id` en body
25
+ const validateId = (req, res, next) => {
26
+ const { id } = req.body;
27
+ if (!id) {
28
+ res.status(400).json({ error: 'id is required' });
29
+ return;
30
+ }
31
+ next();
32
+ };
33
+ exports.intentsRouter = (0, express_1.Router)();
34
+ // POST /api/intent/declare
35
+ // Body: { tool, session_id, task_desc?, files: [{ file_path, operation?, line_start?, line_end? }] }
36
+ // Returns: { id, status: 'acquired'|'blocked', blocked_by?, files? }
37
+ exports.intentsRouter.post('/api/intent/declare', (req, res) => {
38
+ const { tool, session_id, task_desc, files } = req.body;
39
+ if (!tool || !session_id || !Array.isArray(files) || files.length === 0) {
40
+ res.status(400).json({ error: 'tool, session_id and files[] are required' });
41
+ return;
42
+ }
43
+ const writeFiles = files.filter(f => !f.operation || f.operation === 'write');
44
+ const writePaths = writeFiles
45
+ .map(f => f.file_path)
46
+ .filter((p) => typeof p === 'string' && p.startsWith('/'));
47
+ if (writePaths.length < writeFiles.length) {
48
+ res.status(400).json({ error: 'file_path must be an absolute path (starting with /)' });
49
+ return;
50
+ }
51
+ // Check conflicts from other tools
52
+ const conflicts = writePaths.length > 0 ? db_1.dbOps.getWriteConflicts(writePaths, tool) : [];
53
+ if (conflicts.length > 0) {
54
+ const blockedBy = conflicts[0].tool;
55
+ (0, stream_1.broadcast)({ type: 'intent_conflict', payload: {
56
+ files: conflicts.map(c => c.file_path),
57
+ locked_by: blockedBy,
58
+ session_id: conflicts[0].session_id,
59
+ task: conflicts[0].task_desc,
60
+ } });
61
+ res.json({
62
+ id: null, status: 'blocked',
63
+ blocked_by: blockedBy,
64
+ files: conflicts.map(c => c.file_path),
65
+ });
66
+ return;
67
+ }
68
+ // Acquire
69
+ const id = db_1.dbOps.insertIntent(tool, session_id, task_desc);
70
+ const filePaths = [];
71
+ for (const f of files) {
72
+ if (f.file_path) {
73
+ db_1.dbOps.insertIntentFile(id, f.file_path, f.operation ?? 'write', f.line_start, f.line_end);
74
+ filePaths.push(f.file_path);
75
+ }
76
+ }
77
+ (0, stream_1.broadcast)({ type: 'intent_declared', payload: { tool, files: filePaths, task: task_desc } });
78
+ res.json({ id, status: 'acquired' });
79
+ });
80
+ // POST /api/intent/done
81
+ // Body: { id }
82
+ exports.intentsRouter.post('/api/intent/done', validateId, (req, res) => {
83
+ const { id } = req.body;
84
+ const intent = db_1.dbOps.getIntent(id);
85
+ const files = db_1.dbOps.getIntentFiles(id);
86
+ db_1.dbOps.releaseIntent(id);
87
+ for (const f of files) {
88
+ (0, stream_1.broadcast)({ type: 'intent_released', payload: { file: f.file_path, operation: f.operation, tool: intent?.tool } });
89
+ }
90
+ res.json({ ok: true });
91
+ });
92
+ // POST /api/intent/heartbeat
93
+ // Body: { id }
94
+ exports.intentsRouter.post('/api/intent/heartbeat', validateId, (req, res) => {
95
+ const { id } = req.body;
96
+ db_1.dbOps.heartbeatIntent(id);
97
+ res.json({ ok: true });
98
+ });
99
+ // GET /api/intent/status?files=path1,path2
100
+ exports.intentsRouter.get('/api/intent/status', (req, res) => {
101
+ const raw = req.query.files;
102
+ const tool = req.query.exclude_tool;
103
+ const paths = raw ? raw.split(',').map(p => p.trim()).filter(Boolean) : [];
104
+ if (paths.length === 0) {
105
+ res.json({ conflicts: [] });
106
+ return;
107
+ }
108
+ const conflicts = db_1.dbOps.getWriteConflicts(paths, tool ?? '');
109
+ res.json({
110
+ conflicts: conflicts.map(c => ({
111
+ file: c.file_path,
112
+ locked_by: c.tool,
113
+ task: c.task_desc,
114
+ since: c.acquired_at,
115
+ })),
116
+ });
117
+ });
118
+ // POST /api/intent/check-hash — verify files haven't changed since last known hash
119
+ // Body: { files: [{ file_path: string, hash: string }] }
120
+ // Returns: { safe: boolean, changed: Array<{ file_path, current_hash }> }
121
+ exports.intentsRouter.post('/api/intent/check-hash', (req, res) => {
122
+ const { files } = req.body;
123
+ if (!Array.isArray(files) || files.length === 0) {
124
+ res.status(400).json({ error: 'files[] is required' });
125
+ return;
126
+ }
127
+ const changed = [];
128
+ for (const f of files) {
129
+ const current = computeHash(f.file_path);
130
+ if (current && f.hash && current !== f.hash) {
131
+ changed.push({ file_path: f.file_path, current_hash: current });
132
+ }
133
+ }
134
+ res.json({ safe: changed.length === 0, changed });
135
+ });
136
+ // GET /api/intent/hashes?files=/abs/path1,/abs/path2
137
+ // Returns current SHA256 hashes for the given file paths
138
+ exports.intentsRouter.get('/api/intent/hashes', (req, res) => {
139
+ const raw = req.query.files;
140
+ const paths = raw ? raw.split(',').map(p => p.trim()).filter((p) => !!p && p.startsWith('/')) : [];
141
+ if (paths.length === 0) {
142
+ res.status(400).json({ error: 'files query param required (comma-separated absolute paths)' });
143
+ return;
144
+ }
145
+ const hashes = {};
146
+ for (const p of paths) {
147
+ hashes[p] = computeHash(p);
148
+ }
149
+ res.json({ hashes });
150
+ });
151
+ // GET /api/intent/active — list all currently active intents
152
+ exports.intentsRouter.get('/api/intent/active', (_req, res) => {
153
+ const intents = db_1.dbOps.getActiveIntents();
154
+ res.json({ intents });
155
+ });
@@ -22,6 +22,7 @@ const projects_1 = require("./projects");
22
22
  const session_state_1 = require("../session-state");
23
23
  const stream_1 = require("./stream");
24
24
  const paths_1 = require("../paths");
25
+ const opencode_1 = require("../watchers/opencode");
25
26
  const cost_projector_1 = require("../cost-projector");
26
27
  exports.miscRouter = (0, express_1.Router)();
27
28
  // ─── GET /git?path=... — git info para un proyecto ────────────────────────────
@@ -134,14 +135,14 @@ exports.miscRouter.get('/claude-stats', (_req, res) => {
134
135
  });
135
136
  // ─── GET /api/active-sessions — fuentes activas en los últimos 5 min ──────────
136
137
  exports.miscRouter.get('/api/active-sessions', (_req, res) => {
137
- const cutoff = Date.now() - 5 * 60 * 1000;
138
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
138
139
  const sessions = db_1.dbOps.getAllSessions();
139
140
  const bySource = new Map();
140
141
  for (const s of sessions) {
141
142
  const lastSeen = s.last_event_at ?? s.started_at;
142
143
  if (lastSeen < cutoff)
143
144
  continue;
144
- const src = s.source ?? 'claude-code';
145
+ const src = s.source ?? 'unknown';
145
146
  const existing = bySource.get(src);
146
147
  if (!existing || lastSeen > existing.last_seen_ms) {
147
148
  bySource.set(src, {
@@ -153,10 +154,16 @@ exports.miscRouter.get('/api/active-sessions', (_req, res) => {
153
154
  output_tokens: s.total_output_tokens ?? 0,
154
155
  cache_read: s.total_cache_read ?? 0,
155
156
  cache_creation: s.total_cache_creation ?? 0,
157
+ project: s.project_path ?? s.cwd ?? null,
156
158
  });
157
159
  }
158
160
  }
159
- const result = Array.from(bySource.entries()).map(([source, v]) => ({ source, ...v }));
161
+ const KNOWN_SOURCES = new Set(['claude-code', 'opencode', 'codex', 'amp', 'droid', 'codebuff']);
162
+ let result = Array.from(bySource.entries())
163
+ .filter(([source]) => KNOWN_SOURCES.has(source))
164
+ .map(([source, v]) => ({ source, ...v }));
165
+ // OpenCode: filter out archived sessions (user closed the tool)
166
+ result = result.filter(s => s.source !== 'opencode' || !(0, opencode_1.isSessionArchived)(s.sessionId));
160
167
  res.json(result);
161
168
  });
162
169
  // ─── GET /system-config — mapa completo del setup de Claude ──────────────────
@@ -262,7 +269,74 @@ exports.miscRouter.get('/system-config', (_req, res) => {
262
269
  const modeDistribution = db_1.dbOps.getModeDistribution(7);
263
270
  // 6. Config de claudestat
264
271
  const claudestatConfig = (0, config_1.readConfig)();
265
- _systemConfigCache = { hooks, agents, workflows, skills, contextFiles, memoryFiles, modeDistribution, claudestatConfig };
272
+ // ─── 7. OpenCode data ────────────────────────────────────────────────────────
273
+ const opencodeDir = (0, paths_1.getOpencodeDir)();
274
+ let opencodeConfig = null;
275
+ try {
276
+ opencodeConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(opencodeDir, 'opencode.json'), 'utf-8'));
277
+ }
278
+ catch { }
279
+ let opencodeAgentsMd = null;
280
+ try {
281
+ const p = path_1.default.join(opencodeDir, 'AGENTS.md');
282
+ const content = fs_1.default.readFileSync(p, 'utf-8');
283
+ opencodeAgentsMd = { lines: content.split('\n').length, sizeKb: Math.round(Buffer.byteLength(content, 'utf-8') / 1024 * 10) / 10 };
284
+ }
285
+ catch { }
286
+ let opencodeSkills = [];
287
+ try {
288
+ const skillsDir = path_1.default.join(opencodeDir, 'skills');
289
+ for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
290
+ if (entry.isDirectory()) {
291
+ const skillMd = path_1.default.join(skillsDir, entry.name, 'SKILL.md');
292
+ try {
293
+ const content = fs_1.default.readFileSync(skillMd, 'utf-8');
294
+ const lines = content.split('\n').length;
295
+ const description = content.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? '';
296
+ opencodeSkills.push({ name: entry.name, description, lines });
297
+ }
298
+ catch { }
299
+ }
300
+ }
301
+ }
302
+ catch { }
303
+ let opencodeAgents = [];
304
+ try {
305
+ const agentsDir = path_1.default.join(opencodeDir, 'agents');
306
+ opencodeAgents = fs_1.default.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
307
+ }
308
+ catch { }
309
+ let opencodeProjects = 0;
310
+ try {
311
+ const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(opencodeDir, 'projects.json'), 'utf-8'));
312
+ opencodeProjects = Object.keys(raw.projects ?? {}).length;
313
+ }
314
+ catch { }
315
+ let opencodeCommands = [];
316
+ try {
317
+ const cmdsDir = path_1.default.join(opencodeDir, 'commands');
318
+ opencodeCommands = fs_1.default.readdirSync(cmdsDir).filter(f => f.endsWith('.md'));
319
+ }
320
+ catch { }
321
+ const opencodePlugins = [];
322
+ try {
323
+ const pluginsDir = path_1.default.join(opencodeDir, 'plugins');
324
+ opencodePlugins.push(...fs_1.default.readdirSync(pluginsDir).filter(f => f.endsWith('.ts') || f.endsWith('.js')));
325
+ }
326
+ catch { }
327
+ _systemConfigCache = {
328
+ hooks, agents, workflows, skills, contextFiles, memoryFiles,
329
+ modeDistribution, claudestatConfig,
330
+ opencode: {
331
+ config: opencodeConfig,
332
+ agentsMd: opencodeAgentsMd,
333
+ skills: opencodeSkills,
334
+ agents: opencodeAgents,
335
+ projects: opencodeProjects,
336
+ commands: opencodeCommands,
337
+ plugins: opencodePlugins,
338
+ },
339
+ };
266
340
  _systemConfigCacheTs = Date.now();
267
341
  res.json(_systemConfigCache);
268
342
  }
@@ -294,3 +368,57 @@ exports.miscRouter.put('/config', (req, res) => {
294
368
  exports.miscRouter.get('/cost-projection', (_req, res) => {
295
369
  res.json((0, cost_projector_1.computeProjection)(90));
296
370
  });
371
+ // ─── GET /coordination/status — detección automática de herramienta activa ───
372
+ exports.miscRouter.get('/coordination/status', (req, res) => {
373
+ const project = req.query.project;
374
+ const tool = req.query.tool ?? 'unknown';
375
+ if (!project) {
376
+ res.status(400).json({ error: 'project is required' });
377
+ return;
378
+ }
379
+ const active = db_1.dbOps.getActiveToolsInProject(project, tool);
380
+ res.json({
381
+ other_tool_active: active.length > 0,
382
+ tools: active.map(r => r.source),
383
+ since: active.length > 0 ? Math.min(...active.map(r => r.last_event_at)) : null,
384
+ });
385
+ });
386
+ // ─── GET /tool-status — estado de cada tool (claude-code, opencode) ──────────
387
+ const AI_COLLAB_STATUS = path_1.default.join(process.env.HOME ?? '/tmp', '.ai-collab', 'STATUS.json');
388
+ exports.miscRouter.get('/tool-status', (_req, res) => {
389
+ try {
390
+ const raw = fs_1.default.readFileSync(AI_COLLAB_STATUS, 'utf-8');
391
+ res.json(JSON.parse(raw));
392
+ }
393
+ catch {
394
+ res.json({
395
+ 'claude-code': { status: 'unknown', last_task: null, finished_at: null, session_id: null, waiting_for: null },
396
+ 'opencode': { status: 'unknown', last_task: null, finished_at: null, session_id: null, waiting_for: null },
397
+ });
398
+ }
399
+ });
400
+ // ─── POST /tool-status — actualizar estado de un tool ────────────────────────
401
+ exports.miscRouter.post('/tool-status', (req, res) => {
402
+ const { tool, status, last_task, session_id, waiting_for } = req.body;
403
+ if (!tool || !status) {
404
+ res.status(400).json({ error: 'tool and status are required' });
405
+ return;
406
+ }
407
+ let current = {};
408
+ try {
409
+ current = JSON.parse(fs_1.default.readFileSync(AI_COLLAB_STATUS, 'utf-8'));
410
+ }
411
+ catch { }
412
+ const finished_at = status === 'idle' ? Date.now() : null;
413
+ current[tool] = { status, last_task: last_task ?? null, finished_at, session_id: session_id ?? null, waiting_for: waiting_for ?? null };
414
+ try {
415
+ fs_1.default.mkdirSync(path_1.default.dirname(AI_COLLAB_STATUS), { recursive: true });
416
+ fs_1.default.writeFileSync(AI_COLLAB_STATUS, JSON.stringify(current, null, 2), 'utf-8');
417
+ }
418
+ catch (e) {
419
+ res.status(500).json({ error: String(e) });
420
+ return;
421
+ }
422
+ (0, stream_1.broadcast)({ type: 'tool_status_changed', payload: { tool, status, last_task: last_task ?? null, finished_at, session_id: session_id ?? null, waiting_for: waiting_for ?? null } });
423
+ res.json({ ok: true });
424
+ });
@@ -10,12 +10,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.opencodeReaderRouter = void 0;
13
- const os_1 = __importDefault(require("os"));
14
- const path_1 = __importDefault(require("path"));
15
13
  const fs_1 = __importDefault(require("fs"));
16
14
  const express_1 = require("express");
15
+ const paths_1 = require("../paths");
17
16
  exports.opencodeReaderRouter = (0, express_1.Router)();
18
- const OPENCODE_DB = path_1.default.join(os_1.default.homedir(), '.local', 'share', 'opencode', 'opencode.db');
19
17
  // OpenCode tool names → claudestat canonical names
20
18
  const TOOL_NAME_MAP = {
21
19
  glob: 'Glob', read: 'Read', write: 'Write', edit: 'Edit', bash: 'Bash',
@@ -27,23 +25,59 @@ function mapToolName(raw) {
27
25
  }
28
26
  function openDb() {
29
27
  const { DatabaseSync } = require('node:sqlite');
30
- return new DatabaseSync(OPENCODE_DB, { open: true });
28
+ return new DatabaseSync((0, paths_1.getOpencodeDb)(), { open: true });
31
29
  }
32
30
  exports.opencodeReaderRouter.get('/api/opencode/session/:id', (req, res) => {
33
- if (!fs_1.default.existsSync(OPENCODE_DB)) {
31
+ if (!fs_1.default.existsSync((0, paths_1.getOpencodeDb)())) {
34
32
  res.status(404).json({ error: 'OpenCode DB not found' });
35
33
  return;
36
34
  }
37
35
  try {
38
36
  const { id: sessionId } = req.params;
39
37
  const db = openDb();
40
- // Get all messages for this session ordered by time
38
+ // ── Find all related session IDs (OpenCode creates one session per prompt) ──
39
+ // Look up directory for this session
40
+ const sessionRow = db.prepare(`
41
+ SELECT directory, time_created FROM session WHERE id = ?
42
+ `).get(sessionId);
43
+ let sessionIds = [sessionId];
44
+ if (sessionRow?.directory) {
45
+ // Find sessions with same directory, ordered by creation time
46
+ const sameDir = db.prepare(`
47
+ SELECT id, time_created FROM session
48
+ WHERE directory = ? AND time_archived IS NULL
49
+ ORDER BY time_created ASC
50
+ `).all(sessionRow.directory);
51
+ if (sameDir.length > 1) {
52
+ // Locate our session in the sorted list
53
+ let idx = sameDir.findIndex(s => s.id === sessionId);
54
+ if (idx !== -1) {
55
+ // Expand forward: consecutive sessions with gap < 60s
56
+ let end = idx;
57
+ for (let i = idx + 1; i < sameDir.length; i++) {
58
+ if (sameDir[i].time_created - sameDir[i - 1].time_created > 300000)
59
+ break;
60
+ end = i;
61
+ }
62
+ // Expand backward
63
+ let begin = idx;
64
+ for (let i = idx - 1; i >= 0; i--) {
65
+ if (sameDir[begin].time_created - sameDir[i].time_created > 300000)
66
+ break;
67
+ begin = i;
68
+ }
69
+ sessionIds = sameDir.slice(begin, end + 1).map(s => s.id);
70
+ }
71
+ }
72
+ }
73
+ // ── Query messages from ALL sessions in the group ──
74
+ const placeholders = sessionIds.map(() => '?').join(',');
41
75
  const messages = db.prepare(`
42
76
  SELECT id, time_created, time_updated, data
43
77
  FROM message
44
- WHERE session_id = ?
78
+ WHERE session_id IN (${placeholders})
45
79
  ORDER BY time_created ASC
46
- `).all(sessionId);
80
+ `).all(...sessionIds);
47
81
  const events = [];
48
82
  const prompts = [];
49
83
  let totalParts = 0;
@@ -125,6 +125,14 @@ exports.projectsRouter.get('/projects', (_req, res) => {
125
125
  jsonl_source: useJSONL,
126
126
  });
127
127
  }
128
+ // CLI hours per project (source breakdown from DB)
129
+ const cliHoursRows = db_1.dbOps.getProjectCliHours();
130
+ const cliHoursMap = new Map();
131
+ for (const row of cliHoursRows) {
132
+ if (!cliHoursMap.has(row.project_path))
133
+ cliHoursMap.set(row.project_path, {});
134
+ cliHoursMap.get(row.project_path)[row.source] = row.total_ms / 3600000;
135
+ }
128
136
  // Attach pattern insights per project (only if DB has enough data)
129
137
  const projects = [...projectMap.values()].map(p => {
130
138
  const toolCounts = db_1.dbOps.getProjectToolCounts(p.path);
@@ -132,7 +140,8 @@ exports.projectsRouter.get('/projects', (_req, res) => {
132
140
  const insights = (sessionStats && sessionStats.session_count >= 2)
133
141
  ? (0, pattern_analyzer_1.analyzePatterns)(toolCounts, sessionStats)
134
142
  : [];
135
- return { ...p, insights };
143
+ const cli_hours = cliHoursMap.get(p.path);
144
+ return { ...p, insights, ...(cli_hours ? { cli_hours } : {}) };
136
145
  })
137
146
  .sort((a, b) => (b.last_active ?? 0) - (a.last_active ?? 0));
138
147
  res.json({ projects, active_project: activeProject });
@@ -0,0 +1 @@
1
+ export declare const replayRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.replayRouter = void 0;
4
+ const express_1 = require("express");
5
+ const db_1 = require("../db");
6
+ exports.replayRouter = (0, express_1.Router)();
7
+ const CONTEXT_WINDOW = 200000;
8
+ exports.replayRouter.get('/session/:id/replay', (req, res) => {
9
+ const { id } = req.params;
10
+ const turns = db_1.dbOps.getAssistantTurns(id);
11
+ res.json({ turns, context_window: CONTEXT_WINDOW });
12
+ });
13
+ function buildTree(sessionId, depth = 0) {
14
+ if (depth > 5)
15
+ return { id: sessionId, started_at: 0, children: [] };
16
+ const session = db_1.dbOps.getSession(sessionId);
17
+ const children = db_1.dbOps.getChildSessions(sessionId);
18
+ return {
19
+ id: sessionId,
20
+ dominant_model: session?.dominant_model ?? undefined,
21
+ total_cost_usd: session?.total_cost_usd ?? undefined,
22
+ started_at: session?.started_at ?? 0,
23
+ children: children.map(c => buildTree(c.id, depth + 1)),
24
+ };
25
+ }
26
+ exports.replayRouter.get('/session/:id/agent-tree', (req, res) => {
27
+ const { id } = req.params;
28
+ res.json(buildTree(id));
29
+ });
@@ -61,6 +61,13 @@ exports.reportsRouter.get('/api/analytics', (req, res) => {
61
61
  },
62
62
  });
63
63
  });
64
+ // ─── GET /api/analytics/coach — eventos combinados para el coach multi-source ─
65
+ exports.reportsRouter.get('/api/analytics/coach', (req, res) => {
66
+ const raw = req.query.session_ids;
67
+ const sessionIds = raw ? raw.split(',').map(s => s.trim()).filter(Boolean) : [];
68
+ const events = db_1.dbOps.getCoachEvents(sessionIds);
69
+ res.json({ events });
70
+ });
64
71
  // ─── POST /api/weekly-reports/generate-now — generar informe inmediatamente ───
65
72
  exports.reportsRouter.post('/api/weekly-reports/generate-now', (_req, res) => {
66
73
  const cfg = (0, config_1.readConfig)();
@@ -46,7 +46,7 @@ exports.streamRouter.get('/stream', (req, res) => {
46
46
  res.flushHeaders();
47
47
  const clientId = Math.random().toString(36).slice(2);
48
48
  sseClients.set(clientId, res);
49
- const latestSession = db_1.dbOps.getLatestClaudeSession();
49
+ const latestSession = db_1.dbOps.getLatestSession();
50
50
  if (latestSession) {
51
51
  const allEvents = db_1.dbOps.getSessionEvents(latestSession.id);
52
52
  const events = allEvents.length > SSE_INIT_EVENT_LIMIT
@@ -8,18 +8,25 @@ exports.topRouter.get('/api/top', (req, res) => {
8
8
  const by = req.query.by ?? 'cost';
9
9
  const limit = Math.min(parseInt(req.query.limit, 10) || 10, 50);
10
10
  const days = Math.min(parseInt(req.query.days, 10) || 30, 365);
11
+ const source = req.query.source ?? 'all';
11
12
  if (!['cost', 'count', 'duration'].includes(by)) {
12
13
  res.status(400).json({ error: 'Invalid "by" parameter. Use: cost, count, duration' });
13
14
  return;
14
15
  }
15
- const tools = db_1.dbOps.getTopTools(days, by, limit);
16
+ if (!['all', 'claude-code', 'opencode'].includes(source)) {
17
+ res.status(400).json({ error: 'Invalid "source" parameter. Use: all, claude-code, opencode' });
18
+ return;
19
+ }
20
+ const tools = db_1.dbOps.getTopTools(days, by, limit, source);
16
21
  const totalCost = tools.reduce((s, t) => s + t.total_cost_usd, 0);
17
22
  const totalCount = tools.filter(t => t.tool_name !== 'Other').reduce((s, t) => s + t.count, 0);
18
23
  res.json({
19
24
  by,
20
25
  days,
26
+ source,
21
27
  tools: tools.map(t => ({
22
28
  tool: t.tool_name,
29
+ source: t.source,
23
30
  count: t.count,
24
31
  totalDurationMs: t.total_duration_ms,
25
32
  estimatedCostUsd: t.total_cost_usd,
@@ -33,6 +33,7 @@ export interface WatcherAdapter {
33
33
  export interface PollSession {
34
34
  sessionId: string;
35
35
  cost: CostUpdate;
36
+ cwd?: string;
36
37
  }
37
38
  /** Adapters que no usan archivos JSONL sino una DB o API propia */
38
39
  export interface PollableAdapter extends WatcherAdapter {
@@ -6,8 +6,23 @@
6
6
  */
7
7
  import { type WatcherAdapter } from './adapter';
8
8
  import type { BlockCostEntry } from '../db';
9
- export declare function getContextWindow(model: string): number;
10
9
  export declare const claudeCodeAdapter: WatcherAdapter;
10
+ export interface AssistantTurn {
11
+ turn_index: number;
12
+ ts?: number;
13
+ text_preview: string;
14
+ tool_calls: string[];
15
+ error_count: number;
16
+ output_chars: number;
17
+ context_used: number;
18
+ }
19
+ export interface SemanticData {
20
+ turns: AssistantTurn[];
21
+ avg_output_chars: number;
22
+ error_block_count: number;
23
+ }
24
+ export declare function findJSONLForSession(sessionId: string): Promise<string | null>;
25
+ export declare function extractSemanticData(filePath: string): Promise<SemanticData | null>;
11
26
  export declare function getAllBlockCostsForSession(sessionId: string): Promise<BlockCostEntry[]>;
12
27
  export interface SessionPrompt {
13
28
  index: number;