@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.
- 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 +58 -2
- 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 +12 -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 +10 -0
- package/dist/paths.js +17 -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 +132 -4
- package/dist/routes/opencode-reader.js +42 -8
- 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/stream.js +1 -1
- 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 +161 -23
- 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
|
@@ -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
|
+
});
|
package/dist/routes/misc.js
CHANGED
|
@@ -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() -
|
|
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 ?? '
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
80
|
+
`).all(...sessionIds);
|
|
47
81
|
const events = [];
|
|
48
82
|
const prompts = [];
|
|
49
83
|
let totalParts = 0;
|
package/dist/routes/projects.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
});
|
package/dist/routes/reports.js
CHANGED
|
@@ -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)();
|
package/dist/routes/stream.js
CHANGED
|
@@ -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.
|
|
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
|
package/dist/routes/top.js
CHANGED
|
@@ -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
|
-
|
|
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;
|