@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.
- package/README.md +36 -3
- 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/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/daemon.js +113 -9
- package/dist/db.d.ts +87 -2
- package/dist/db.js +325 -65
- package/dist/doctor.js +21 -3
- package/dist/enricher.d.ts +3 -2
- package/dist/enricher.js +10 -5
- package/dist/export.d.ts +2 -1
- package/dist/export.js +41 -6
- package/dist/index.js +406 -20
- package/dist/insights.d.ts +1 -0
- package/dist/insights.js +26 -0
- package/dist/install.js +28 -1
- package/dist/intelligence.d.ts +66 -4
- package/dist/intelligence.js +205 -17
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +49 -0
- package/dist/notifier.d.ts +15 -0
- package/dist/notifier.js +26 -0
- package/dist/paths.d.ts +23 -0
- package/dist/paths.js +42 -0
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +12 -1
- package/dist/routes/events.js +136 -5
- package/dist/routes/helpers.d.ts +5 -0
- package/dist/routes/helpers.js +21 -1
- 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 +150 -4
- package/dist/routes/opencode-reader.js +39 -3
- package/dist/routes/projects.js +19 -1
- package/dist/routes/replay.d.ts +1 -0
- package/dist/routes/replay.js +29 -0
- package/dist/routes/reports.js +7 -0
- package/dist/routes/top.js +8 -1
- package/dist/service.js +11 -0
- package/dist/watchers/adapter.d.ts +1 -0
- package/dist/watchers/claude-code.d.ts +16 -1
- package/dist/watchers/claude-code.js +201 -76
- package/dist/watchers/opencode.d.ts +1 -0
- package/dist/watchers/opencode.js +152 -14
- package/hooks/event.js +44 -26
- package/package.json +1 -1
- package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
- package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
- package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
- package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
- package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
- package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
package/dist/routes/events.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.processLatestForSession = exports.onCompactDetected = exports.onCostUpdate = exports.taggedSessionParents = exports.lastAgentByCwd = exports.eventsRouter = void 0;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const express_1 = require("express");
|
|
10
11
|
const db_1 = require("../db");
|
|
@@ -19,11 +20,23 @@ const notifier_1 = require("../notifier");
|
|
|
19
20
|
const helpers_1 = require("./helpers");
|
|
20
21
|
const enricher_1 = require("../enricher");
|
|
21
22
|
Object.defineProperty(exports, "processLatestForSession", { enumerable: true, get: function () { return enricher_1.processLatestForSession; } });
|
|
23
|
+
const claude_code_1 = require("../watchers/claude-code");
|
|
24
|
+
// ─── Semantic extraction debounce: 3s after last new assistant turn ───────────
|
|
25
|
+
const semanticDebounce = new Map();
|
|
26
|
+
// ─── Predictive saturation: context samples + alert cooldown per session ──────
|
|
27
|
+
const contextSamples = new Map();
|
|
28
|
+
const saturationCooldown = new Map();
|
|
29
|
+
const SATURATION_WARN_MINUTES = 30;
|
|
30
|
+
const SATURATION_COOLDOWN_MS = 15 * 60000;
|
|
31
|
+
const CONTEXT_SAMPLES_MAX = 20;
|
|
22
32
|
// ─── Loop alert cooldown (toolName:sessionId → last alert ts) ─────────────────
|
|
23
33
|
const loopAlertCooldown = new Map();
|
|
24
34
|
const LOOP_ALERT_COOLDOWN_MS = 120000; // coincide con LOOP_COOLDOWN_MS en intelligence.ts
|
|
25
35
|
// ─── Session cost alert: sesiones que ya recibieron notificación ───────────────
|
|
26
36
|
const sessionCostAlertFired = new Set();
|
|
37
|
+
// ─── Memory leak bounds ────────────────────────────────────────────────────────
|
|
38
|
+
const AGENT_CWD_TTL_MS = 30 * 60000; // entries older than 30min can't be valid parents
|
|
39
|
+
const TAGGED_PARENTS_MAX = 500; // prevent unbounded growth over long daemon runs
|
|
27
40
|
exports.eventsRouter = (0, express_1.Router)();
|
|
28
41
|
// Skill activa por sesión — se setea tras Skill Done, se limpia en Stop.
|
|
29
42
|
// Permite taggear los eventos siguientes con skill_parent para agruparlos en la UI.
|
|
@@ -55,17 +68,52 @@ function shouldFireAlert(level, pct) {
|
|
|
55
68
|
alertCooldown.set(level, Date.now());
|
|
56
69
|
return true;
|
|
57
70
|
}
|
|
71
|
+
function extractTextPreview(transcriptPath) {
|
|
72
|
+
try {
|
|
73
|
+
const size = fs_1.default.statSync(transcriptPath).size;
|
|
74
|
+
const readSize = Math.min(size, 8192);
|
|
75
|
+
const buf = Buffer.alloc(readSize);
|
|
76
|
+
const fd = fs_1.default.openSync(transcriptPath, 'r');
|
|
77
|
+
fs_1.default.readSync(fd, buf, 0, readSize, Math.max(0, size - readSize));
|
|
78
|
+
fs_1.default.closeSync(fd);
|
|
79
|
+
const lines = buf.toString('utf8').split('\n').filter(l => l.trim());
|
|
80
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
81
|
+
try {
|
|
82
|
+
const obj = JSON.parse(lines[i]);
|
|
83
|
+
const data = obj.message ?? obj;
|
|
84
|
+
if (data.role !== 'assistant')
|
|
85
|
+
continue;
|
|
86
|
+
let text = '';
|
|
87
|
+
if (typeof data.content === 'string') {
|
|
88
|
+
text = data.content;
|
|
89
|
+
}
|
|
90
|
+
else if (Array.isArray(data.content)) {
|
|
91
|
+
text = data.content.find((c) => c.type === 'text')?.text ?? '';
|
|
92
|
+
}
|
|
93
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
94
|
+
if (!text)
|
|
95
|
+
continue;
|
|
96
|
+
return text.length > 120 ? text.slice(0, 120) + '…' : text;
|
|
97
|
+
}
|
|
98
|
+
catch { }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch { }
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
58
104
|
exports.eventsRouter.post('/event', (req, res) => {
|
|
59
105
|
const ip = req.ip ?? '127.0.0.1';
|
|
60
106
|
if ((0, rate_limiter_1.isRateLimited)(ip)) {
|
|
61
107
|
res.status(429).json({ error: 'Too many requests — wait 1 minute' });
|
|
62
108
|
return;
|
|
63
109
|
}
|
|
64
|
-
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path, source } = req.body;
|
|
110
|
+
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path, source: rawSource } = req.body;
|
|
65
111
|
if (!session_id || !type) {
|
|
66
112
|
res.status(400).json({ error: 'Missing session_id or type' });
|
|
67
113
|
return;
|
|
68
114
|
}
|
|
115
|
+
const KNOWN_SOURCES = new Set(['claude-code', 'opencode', 'codex', 'amp', 'droid', 'codebuff']);
|
|
116
|
+
const source = rawSource && KNOWN_SOURCES.has(rawSource) ? rawSource : 'claude-code';
|
|
69
117
|
const resolvedCwd = cwd
|
|
70
118
|
?? (transcript_path ? path_1.default.dirname(transcript_path) || undefined : undefined);
|
|
71
119
|
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts, source });
|
|
@@ -94,13 +142,23 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
94
142
|
}
|
|
95
143
|
}
|
|
96
144
|
else {
|
|
145
|
+
const stopPreview = type === 'Stop'
|
|
146
|
+
? (() => {
|
|
147
|
+
const lam = req.body.last_assistant_message;
|
|
148
|
+
if (lam && typeof lam === 'string') {
|
|
149
|
+
const t = lam.replace(/\s+/g, ' ').trim();
|
|
150
|
+
return t.length > 120 ? t.slice(0, 120) + '…' : t;
|
|
151
|
+
}
|
|
152
|
+
return transcript_path ? extractTextPreview(transcript_path) : undefined;
|
|
153
|
+
})()
|
|
154
|
+
: undefined;
|
|
97
155
|
db_1.dbOps.insertEvent({
|
|
98
156
|
session_id, type,
|
|
99
157
|
tool_name: tool_name ?? undefined,
|
|
100
|
-
tool_input: tool_input ? JSON.stringify(tool_input) : undefined,
|
|
158
|
+
tool_input: tool_input ? JSON.stringify(tool_input) : (stopPreview ?? undefined),
|
|
101
159
|
ts, cwd: resolvedCwd, skill_parent: skillParent, source
|
|
102
160
|
});
|
|
103
|
-
(0, stream_1.broadcast)({ type: 'event', payload: { ...req.body, skill_parent: skillParent } });
|
|
161
|
+
(0, stream_1.broadcast)({ type: 'event', payload: { ...req.body, skill_parent: skillParent, ...(stopPreview ? { text_preview: stopPreview } : {}) } });
|
|
104
162
|
// Stop limpia el skill activo para esta sesión
|
|
105
163
|
if (type === 'Stop') {
|
|
106
164
|
activeSkillBySession.delete(session_id);
|
|
@@ -108,6 +166,11 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
108
166
|
}
|
|
109
167
|
// Registrar Agent PreToolUse para detección de sub-sesiones
|
|
110
168
|
if (type === 'PreToolUse' && tool_name === 'Agent' && resolvedCwd) {
|
|
169
|
+
const cwdCutoff = Date.now() - AGENT_CWD_TTL_MS;
|
|
170
|
+
for (const [cwd, info] of exports.lastAgentByCwd) {
|
|
171
|
+
if (info.pre_ts < cwdCutoff)
|
|
172
|
+
exports.lastAgentByCwd.delete(cwd);
|
|
173
|
+
}
|
|
111
174
|
exports.lastAgentByCwd.set(resolvedCwd, { pre_ts: ts, session_id });
|
|
112
175
|
}
|
|
113
176
|
}
|
|
@@ -121,6 +184,17 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
121
184
|
const projectCwd = (0, helpers_1.findProjectCwdForFile)(filePath);
|
|
122
185
|
if (projectCwd)
|
|
123
186
|
db_1.dbOps.updateSessionProject(session_id, projectCwd);
|
|
187
|
+
// Auto-declare intent for write tools when no active intent exists yet
|
|
188
|
+
if (type === 'PreToolUse' && ['Write', 'Edit'].includes(tool_name || '')) {
|
|
189
|
+
if (!db_1.dbOps.hasActiveIntent(source, filePath)) {
|
|
190
|
+
const conflicts = db_1.dbOps.getWriteConflicts([filePath], source);
|
|
191
|
+
if (conflicts.length === 0) {
|
|
192
|
+
const aid = db_1.dbOps.insertIntent(source, session_id, 'auto: ' + tool_name);
|
|
193
|
+
db_1.dbOps.insertIntentFile(aid, filePath, 'write');
|
|
194
|
+
(0, stream_1.broadcast)({ type: 'intent_declared', payload: { tool: source, files: [filePath], task: 'auto: ' + tool_name } });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
124
198
|
}
|
|
125
199
|
}
|
|
126
200
|
catch { /* ignorar errores de parsing */ }
|
|
@@ -197,12 +271,17 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
197
271
|
const onCostUpdate = (sessionId, cost, source) => {
|
|
198
272
|
// Ensure session row exists — sub-agent JSONLs arrive from the enricher without a
|
|
199
273
|
// prior hook event (Claude Code does not fire hooks for sub-agent sessions).
|
|
200
|
-
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: Date.now(), source });
|
|
274
|
+
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.lastTs ?? cost.firstTs ?? Date.now(), source });
|
|
201
275
|
let sessionRow = db_1.dbOps.getSession(sessionId);
|
|
202
276
|
// Sub-agent detection: first time we see a session, check if its firstTs falls after
|
|
203
277
|
// a recent Agent PreToolUse from another session in the same CWD → tag as child.
|
|
204
278
|
if (!exports.taggedSessionParents.has(sessionId) && cost.firstTs) {
|
|
205
279
|
exports.taggedSessionParents.add(sessionId);
|
|
280
|
+
if (exports.taggedSessionParents.size > TAGGED_PARENTS_MAX) {
|
|
281
|
+
const arr = Array.from(exports.taggedSessionParents);
|
|
282
|
+
for (const id of arr.slice(0, arr.length >> 1))
|
|
283
|
+
exports.taggedSessionParents.delete(id);
|
|
284
|
+
}
|
|
206
285
|
const cwd = sessionRow?.cwd;
|
|
207
286
|
if (cwd) {
|
|
208
287
|
const agentInfo = exports.lastAgentByCwd.get(cwd);
|
|
@@ -213,16 +292,44 @@ const onCostUpdate = (sessionId, cost, source) => {
|
|
|
213
292
|
}
|
|
214
293
|
const events = db_1.dbOps.getSessionEvents(sessionId);
|
|
215
294
|
const report = (0, intelligence_1.analyzeSession)(events, cost.cost_usd);
|
|
216
|
-
db_1.dbOps.updateSessionCost(sessionId, cost, report.efficiencyScore, report.loops.length);
|
|
295
|
+
db_1.dbOps.updateSessionCost(sessionId, cost, report.efficiencyScore, report.loops.length, report.exactRetries, report.errorRate, report.fileChurnScore, report.seqCycleCount);
|
|
296
|
+
// ─── Predictive saturation ────────────────────────────────────────────────────
|
|
297
|
+
if (cost.context_used > 0 && cost.context_window > 0) {
|
|
298
|
+
const samples = contextSamples.get(sessionId) ?? [];
|
|
299
|
+
samples.push({ ts: Date.now(), context_used: cost.context_used });
|
|
300
|
+
if (samples.length > CONTEXT_SAMPLES_MAX)
|
|
301
|
+
samples.shift();
|
|
302
|
+
contextSamples.set(sessionId, samples);
|
|
303
|
+
const prediction = (0, intelligence_1.predictSaturation)(samples, cost.context_window);
|
|
304
|
+
if (prediction && prediction.minutesLeft <= SATURATION_WARN_MINUTES) {
|
|
305
|
+
const lastFired = saturationCooldown.get(sessionId) ?? 0;
|
|
306
|
+
if (Date.now() - lastFired >= SATURATION_COOLDOWN_MS) {
|
|
307
|
+
saturationCooldown.set(sessionId, Date.now());
|
|
308
|
+
(0, stream_1.broadcast)({
|
|
309
|
+
type: 'saturation_warning',
|
|
310
|
+
payload: {
|
|
311
|
+
session_id: sessionId,
|
|
312
|
+
minutes_left: prediction.minutesLeft,
|
|
313
|
+
pct_per_min: prediction.pctPerMin,
|
|
314
|
+
context_used: cost.context_used,
|
|
315
|
+
context_window: cost.context_window,
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
217
321
|
const startedAt = sessionRow?.started_at ?? Date.now();
|
|
218
322
|
const sessionDurationMinutes = (Date.now() - startedAt) / 60000;
|
|
219
323
|
const projectedHourlyUsd = sessionDurationMinutes > 0.5
|
|
220
324
|
? cost.cost_usd / sessionDurationMinutes * 60
|
|
221
325
|
: 0;
|
|
326
|
+
const sessionSource = sessionRow?.source ?? 'claude-code';
|
|
222
327
|
(0, stream_1.broadcast)({
|
|
223
328
|
type: 'cost_update',
|
|
224
329
|
payload: {
|
|
225
330
|
session_id: sessionId,
|
|
331
|
+
source: sessionSource,
|
|
332
|
+
started_at: startedAt,
|
|
226
333
|
cost_usd: cost.cost_usd,
|
|
227
334
|
input_tokens: cost.input_tokens,
|
|
228
335
|
output_tokens: cost.output_tokens,
|
|
@@ -268,6 +375,30 @@ const onCostUpdate = (sessionId, cost, source) => {
|
|
|
268
375
|
outputTokens: cost.lastEntry.outputTokens,
|
|
269
376
|
}
|
|
270
377
|
});
|
|
378
|
+
// Semantic extraction: debounced 3s so rapid updates don't cause duplicate parses
|
|
379
|
+
const existing = semanticDebounce.get(sessionId);
|
|
380
|
+
if (existing)
|
|
381
|
+
clearTimeout(existing);
|
|
382
|
+
semanticDebounce.set(sessionId, setTimeout(async () => {
|
|
383
|
+
semanticDebounce.delete(sessionId);
|
|
384
|
+
try {
|
|
385
|
+
const filePath = await (0, claude_code_1.findJSONLForSession)(sessionId);
|
|
386
|
+
if (!filePath)
|
|
387
|
+
return;
|
|
388
|
+
const semantic = await (0, claude_code_1.extractSemanticData)(filePath);
|
|
389
|
+
if (!semantic)
|
|
390
|
+
return;
|
|
391
|
+
db_1.dbOps.upsertAssistantTurns(sessionId, semantic.turns);
|
|
392
|
+
const semLoops = (0, intelligence_1.analyzeSemanticLoops)(semantic.turns);
|
|
393
|
+
db_1.dbOps.updateSessionSemantic(sessionId, semantic.avg_output_chars, semantic.error_block_count, semLoops.length);
|
|
394
|
+
if (semLoops.length > 0) {
|
|
395
|
+
(0, stream_1.broadcast)({ type: 'semantic_loop', payload: { session_id: sessionId, loops: semLoops } });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
console.warn('[events] Semantic extraction error:', err);
|
|
400
|
+
}
|
|
401
|
+
}, 3000));
|
|
271
402
|
}
|
|
272
403
|
};
|
|
273
404
|
exports.onCostUpdate = onCostUpdate;
|
package/dist/routes/helpers.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
2
2
|
export declare function findProjectCwdForFile(filePath: string): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Returns the git root directory for the given directory, or undefined.
|
|
5
|
+
* Uses 'git rev-parse --show-toplevel' — fast and reliable.
|
|
6
|
+
*/
|
|
7
|
+
export declare function findGitRoot(fromDir: string): string | undefined;
|
package/dist/routes/helpers.js
CHANGED
|
@@ -4,8 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.findProjectCwdForFile = findProjectCwdForFile;
|
|
7
|
+
exports.findGitRoot = findGitRoot;
|
|
7
8
|
const path_1 = __importDefault(require("path"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
9
11
|
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
10
12
|
function findProjectCwdForFile(filePath) {
|
|
11
13
|
let dir = path_1.default.dirname(filePath);
|
|
@@ -17,5 +19,23 @@ function findProjectCwdForFile(filePath) {
|
|
|
17
19
|
break;
|
|
18
20
|
dir = parent;
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
// Fallback: git root
|
|
23
|
+
return findGitRoot(path_1.default.dirname(filePath));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Returns the git root directory for the given directory, or undefined.
|
|
27
|
+
* Uses 'git rev-parse --show-toplevel' — fast and reliable.
|
|
28
|
+
*/
|
|
29
|
+
function findGitRoot(fromDir) {
|
|
30
|
+
try {
|
|
31
|
+
const root = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
|
|
32
|
+
cwd: fromDir,
|
|
33
|
+
stdio: 'pipe',
|
|
34
|
+
timeout: 2000,
|
|
35
|
+
}).toString().trim();
|
|
36
|
+
return root || undefined;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
21
41
|
}
|
package/dist/routes/history.js
CHANGED
|
@@ -22,8 +22,10 @@ exports.historyRouter.get('/history', (_req, res) => {
|
|
|
22
22
|
// Detectar modo desde los contadores precalculados en la query
|
|
23
23
|
const hasAgent = (s.agent_count ?? 0) > 0;
|
|
24
24
|
const hasSkill = (s.skill_count ?? 0) > 0;
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const isSubAgent = s.parent_session_id != null;
|
|
26
|
+
const mode = isSubAgent ? 'sub-agente'
|
|
27
|
+
: hasAgent && hasSkill ? 'agentes+skills'
|
|
28
|
+
: hasAgent ? 'agentes' : hasSkill ? 'skills' : 'directo';
|
|
27
29
|
// Git info cacheada para este proyecto
|
|
28
30
|
const gitInfo = s.project_path ? (0, projects_cache_1.getCachedGitInfo)(s.project_path) : null;
|
|
29
31
|
byDate.get(date).push({
|
|
@@ -45,7 +47,9 @@ exports.historyRouter.get('/history', (_req, res) => {
|
|
|
45
47
|
return [];
|
|
46
48
|
} })() : [],
|
|
47
49
|
mode,
|
|
50
|
+
source: s.source ?? 'claude-code',
|
|
48
51
|
ai_summary: s.ai_summary ?? null,
|
|
52
|
+
is_sub_agent: isSubAgent,
|
|
49
53
|
git_branch: gitInfo?.branch ?? null,
|
|
50
54
|
git_dirty: gitInfo?.dirty ?? false,
|
|
51
55
|
git_ahead: gitInfo?.ahead ?? 0,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const intentsRouter: import("express-serve-static-core").Router;
|
|
@@ -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 ────────────────────────────
|
|
@@ -61,7 +62,8 @@ exports.miscRouter.get('/intelligence/:sessionId', (req, res) => {
|
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
const events = db_1.dbOps.getSessionEvents(sessionId);
|
|
64
|
-
const
|
|
65
|
+
const cfg = (0, config_1.readConfig)();
|
|
66
|
+
const report = (0, intelligence_1.analyzeSession)(events, session.total_cost_usd ?? 0, cfg.loopThreshold, cfg.loopWindowSecs * 1000);
|
|
65
67
|
res.json({ sessionId, ...report });
|
|
66
68
|
});
|
|
67
69
|
// ─── GET /quota — datos de cuota y burn rate ──────────────────────────────────
|
|
@@ -134,7 +136,7 @@ exports.miscRouter.get('/claude-stats', (_req, res) => {
|
|
|
134
136
|
});
|
|
135
137
|
// ─── GET /api/active-sessions — fuentes activas en los últimos 5 min ──────────
|
|
136
138
|
exports.miscRouter.get('/api/active-sessions', (_req, res) => {
|
|
137
|
-
const cutoff = Date.now() -
|
|
139
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
138
140
|
const sessions = db_1.dbOps.getAllSessions();
|
|
139
141
|
const bySource = new Map();
|
|
140
142
|
for (const s of sessions) {
|
|
@@ -153,10 +155,16 @@ exports.miscRouter.get('/api/active-sessions', (_req, res) => {
|
|
|
153
155
|
output_tokens: s.total_output_tokens ?? 0,
|
|
154
156
|
cache_read: s.total_cache_read ?? 0,
|
|
155
157
|
cache_creation: s.total_cache_creation ?? 0,
|
|
158
|
+
project: s.project_path ?? s.cwd ?? null,
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
|
-
const
|
|
162
|
+
const KNOWN_SOURCES = new Set(['claude-code', 'opencode', 'codex', 'amp', 'droid', 'codebuff']);
|
|
163
|
+
let result = Array.from(bySource.entries())
|
|
164
|
+
.filter(([source]) => KNOWN_SOURCES.has(source))
|
|
165
|
+
.map(([source, v]) => ({ source, ...v }));
|
|
166
|
+
// OpenCode: filter out archived sessions (user closed the tool)
|
|
167
|
+
result = result.filter(s => s.source !== 'opencode' || !(0, opencode_1.isSessionArchived)(s.sessionId));
|
|
160
168
|
res.json(result);
|
|
161
169
|
});
|
|
162
170
|
// ─── GET /system-config — mapa completo del setup de Claude ──────────────────
|
|
@@ -262,7 +270,74 @@ exports.miscRouter.get('/system-config', (_req, res) => {
|
|
|
262
270
|
const modeDistribution = db_1.dbOps.getModeDistribution(7);
|
|
263
271
|
// 6. Config de claudestat
|
|
264
272
|
const claudestatConfig = (0, config_1.readConfig)();
|
|
265
|
-
|
|
273
|
+
// ─── 7. OpenCode data ────────────────────────────────────────────────────────
|
|
274
|
+
const opencodeDir = (0, paths_1.getOpencodeDir)();
|
|
275
|
+
let opencodeConfig = null;
|
|
276
|
+
try {
|
|
277
|
+
opencodeConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(opencodeDir, 'opencode.json'), 'utf-8'));
|
|
278
|
+
}
|
|
279
|
+
catch { }
|
|
280
|
+
let opencodeAgentsMd = null;
|
|
281
|
+
try {
|
|
282
|
+
const p = path_1.default.join(opencodeDir, 'AGENTS.md');
|
|
283
|
+
const content = fs_1.default.readFileSync(p, 'utf-8');
|
|
284
|
+
opencodeAgentsMd = { lines: content.split('\n').length, sizeKb: Math.round(Buffer.byteLength(content, 'utf-8') / 1024 * 10) / 10 };
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
let opencodeSkills = [];
|
|
288
|
+
try {
|
|
289
|
+
const skillsDir = path_1.default.join(opencodeDir, 'skills');
|
|
290
|
+
for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
291
|
+
if (entry.isDirectory()) {
|
|
292
|
+
const skillMd = path_1.default.join(skillsDir, entry.name, 'SKILL.md');
|
|
293
|
+
try {
|
|
294
|
+
const content = fs_1.default.readFileSync(skillMd, 'utf-8');
|
|
295
|
+
const lines = content.split('\n').length;
|
|
296
|
+
const description = content.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? '';
|
|
297
|
+
opencodeSkills.push({ name: entry.name, description, lines });
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch { }
|
|
304
|
+
let opencodeAgents = [];
|
|
305
|
+
try {
|
|
306
|
+
const agentsDir = path_1.default.join(opencodeDir, 'agents');
|
|
307
|
+
opencodeAgents = fs_1.default.readdirSync(agentsDir).filter(f => f.endsWith('.md'));
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
let opencodeProjects = 0;
|
|
311
|
+
try {
|
|
312
|
+
const raw = JSON.parse(fs_1.default.readFileSync(path_1.default.join(opencodeDir, 'projects.json'), 'utf-8'));
|
|
313
|
+
opencodeProjects = Object.keys(raw.projects ?? {}).length;
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
let opencodeCommands = [];
|
|
317
|
+
try {
|
|
318
|
+
const cmdsDir = path_1.default.join(opencodeDir, 'commands');
|
|
319
|
+
opencodeCommands = fs_1.default.readdirSync(cmdsDir).filter(f => f.endsWith('.md'));
|
|
320
|
+
}
|
|
321
|
+
catch { }
|
|
322
|
+
const opencodePlugins = [];
|
|
323
|
+
try {
|
|
324
|
+
const pluginsDir = path_1.default.join(opencodeDir, 'plugins');
|
|
325
|
+
opencodePlugins.push(...fs_1.default.readdirSync(pluginsDir).filter(f => f.endsWith('.ts') || f.endsWith('.js')));
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
_systemConfigCache = {
|
|
329
|
+
hooks, agents, workflows, skills, contextFiles, memoryFiles,
|
|
330
|
+
modeDistribution, claudestatConfig,
|
|
331
|
+
opencode: {
|
|
332
|
+
config: opencodeConfig,
|
|
333
|
+
agentsMd: opencodeAgentsMd,
|
|
334
|
+
skills: opencodeSkills,
|
|
335
|
+
agents: opencodeAgents,
|
|
336
|
+
projects: opencodeProjects,
|
|
337
|
+
commands: opencodeCommands,
|
|
338
|
+
plugins: opencodePlugins,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
266
341
|
_systemConfigCacheTs = Date.now();
|
|
267
342
|
res.json(_systemConfigCache);
|
|
268
343
|
}
|
|
@@ -294,3 +369,74 @@ exports.miscRouter.put('/config', (req, res) => {
|
|
|
294
369
|
exports.miscRouter.get('/cost-projection', (_req, res) => {
|
|
295
370
|
res.json((0, cost_projector_1.computeProjection)(90));
|
|
296
371
|
});
|
|
372
|
+
// ─── GET /coordination/status — detección automática de herramienta activa ───
|
|
373
|
+
exports.miscRouter.get('/coordination/status', (req, res) => {
|
|
374
|
+
const project = req.query.project;
|
|
375
|
+
const tool = req.query.tool ?? 'unknown';
|
|
376
|
+
if (!project) {
|
|
377
|
+
res.status(400).json({ error: 'project is required' });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const active = db_1.dbOps.getActiveToolsInProject(project, tool);
|
|
381
|
+
res.json({
|
|
382
|
+
other_tool_active: active.length > 0,
|
|
383
|
+
tools: active.map(r => r.source),
|
|
384
|
+
since: active.length > 0 ? Math.min(...active.map(r => r.last_event_at)) : null,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// ─── GET /tool-status — estado de cada tool (claude-code, opencode) ──────────
|
|
388
|
+
const AI_COLLAB_STATUS = path_1.default.join(process.env.HOME ?? '/tmp', '.ai-collab', 'STATUS.json');
|
|
389
|
+
exports.miscRouter.get('/tool-status', (_req, res) => {
|
|
390
|
+
try {
|
|
391
|
+
const raw = fs_1.default.readFileSync(AI_COLLAB_STATUS, 'utf-8');
|
|
392
|
+
res.json(JSON.parse(raw));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
res.json({
|
|
396
|
+
'claude-code': { status: 'unknown', last_task: null, finished_at: null, session_id: null, waiting_for: null },
|
|
397
|
+
'opencode': { status: 'unknown', last_task: null, finished_at: null, session_id: null, waiting_for: null },
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
// ─── POST /tool-status — actualizar estado de un tool ────────────────────────
|
|
402
|
+
exports.miscRouter.post('/tool-status', (req, res) => {
|
|
403
|
+
const { tool, status, last_task, session_id, waiting_for } = req.body;
|
|
404
|
+
if (!tool || !status) {
|
|
405
|
+
res.status(400).json({ error: 'tool and status are required' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
let current = {};
|
|
409
|
+
try {
|
|
410
|
+
current = JSON.parse(fs_1.default.readFileSync(AI_COLLAB_STATUS, 'utf-8'));
|
|
411
|
+
}
|
|
412
|
+
catch { }
|
|
413
|
+
const finished_at = status === 'idle' ? Date.now() : null;
|
|
414
|
+
current[tool] = { status, last_task: last_task ?? null, finished_at, session_id: session_id ?? null, waiting_for: waiting_for ?? null };
|
|
415
|
+
try {
|
|
416
|
+
fs_1.default.mkdirSync(path_1.default.dirname(AI_COLLAB_STATUS), { recursive: true });
|
|
417
|
+
fs_1.default.writeFileSync(AI_COLLAB_STATUS, JSON.stringify(current, null, 2), 'utf-8');
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
res.status(500).json({ error: String(e) });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
(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 } });
|
|
424
|
+
res.json({ ok: true });
|
|
425
|
+
});
|
|
426
|
+
// ─── GET /api/logs — últimos N líneas del daemon log ──────────────────────────
|
|
427
|
+
exports.miscRouter.get('/api/logs', (req, res) => {
|
|
428
|
+
const n = Math.min(parseInt(req.query.n, 10) || 50, 500);
|
|
429
|
+
const logFile = (0, paths_1.getDaemonLogFile)();
|
|
430
|
+
try {
|
|
431
|
+
if (!fs_1.default.existsSync(logFile)) {
|
|
432
|
+
res.json({ lines: [] });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const content = fs_1.default.readFileSync(logFile, 'utf8');
|
|
436
|
+
const lines = content.split('\n').filter(Boolean);
|
|
437
|
+
res.json({ lines: lines.slice(-n) });
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
res.json({ lines: [] });
|
|
441
|
+
}
|
|
442
|
+
});
|