@statforge/claudestat 1.4.0 → 1.5.1
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/LICENSE +21 -0
- package/README.md +130 -547
- package/dist/cost-projector.d.ts +24 -0
- package/dist/cost-projector.js +133 -0
- package/dist/daemon.js +2 -2
- package/dist/db.d.ts +4 -0
- package/dist/db.js +14 -6
- package/dist/enricher.d.ts +18 -26
- package/dist/enricher.js +113 -333
- package/dist/index.js +23 -2
- package/dist/insights.js +0 -2
- package/dist/meta-stats.js +1 -1
- package/dist/middleware/rate-limiter.js +1 -1
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +45 -1
- package/dist/quota-tracker.js +0 -1
- package/dist/roast.js +0 -2
- package/dist/routes/events.d.ts +0 -2
- package/dist/routes/events.js +7 -21
- package/dist/routes/helpers.d.ts +2 -0
- package/dist/routes/helpers.js +21 -0
- package/dist/routes/misc.js +3 -21
- package/dist/routes/projects.d.ts +0 -2
- package/dist/routes/projects.js +3 -17
- package/dist/routes/stream.d.ts +1 -1
- package/dist/routes/stream.js +3 -3
- package/dist/service.js +11 -7
- package/dist/watch.js +0 -1
- package/dist/watchdog.d.ts +5 -0
- package/dist/watchdog.js +6 -1
- package/dist/watchers/adapter.d.ts +37 -0
- package/dist/watchers/adapter.js +31 -0
- package/dist/watchers/amp.d.ts +8 -0
- package/dist/watchers/amp.js +42 -0
- package/dist/watchers/claude-code.d.ts +17 -0
- package/dist/watchers/claude-code.js +300 -0
- package/dist/watchers/codebuff.d.ts +8 -0
- package/dist/watchers/codebuff.js +42 -0
- package/dist/watchers/codex.d.ts +9 -0
- package/dist/watchers/codex.js +43 -0
- package/dist/watchers/droid.d.ts +8 -0
- package/dist/watchers/droid.js +42 -0
- package/dist/watchers/opencode.d.ts +9 -0
- package/dist/watchers/opencode.js +43 -0
- package/package.json +12 -3
package/dist/paths.js
CHANGED
|
@@ -16,6 +16,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
16
16
|
};
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
18
|
exports.isWindows = void 0;
|
|
19
|
+
exports.isBinary = isBinary;
|
|
20
|
+
exports.getBinaryDir = getBinaryDir;
|
|
21
|
+
exports.getDashboardDir = getDashboardDir;
|
|
19
22
|
exports.getClaudeDir = getClaudeDir;
|
|
20
23
|
exports.getClaudestatDir = getClaudestatDir;
|
|
21
24
|
exports.getPidFile = getPidFile;
|
|
@@ -28,14 +31,55 @@ exports.whichAllCmd = whichAllCmd;
|
|
|
28
31
|
exports.portCheckCmd = portCheckCmd;
|
|
29
32
|
const os_1 = __importDefault(require("os"));
|
|
30
33
|
const path_1 = __importDefault(require("path"));
|
|
34
|
+
const fs_1 = __importDefault(require("fs"));
|
|
31
35
|
const isWin = process.platform === 'win32';
|
|
36
|
+
// ─── Binary detection ──────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Detecta si claudestat está corriendo como binario standalone (Bun compile)
|
|
39
|
+
* vs desde npm (node dist/index.js).
|
|
40
|
+
*/
|
|
41
|
+
function isBinary() {
|
|
42
|
+
const arg1 = process.argv[1] ?? '';
|
|
43
|
+
return !arg1.includes('node_modules') && !arg1.includes('dist/index.js')
|
|
44
|
+
&& !arg1.includes('tsx') && arg1.includes('claudestat');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Retorna el directorio base del binario o del proyecto.
|
|
48
|
+
* En binario: directorio donde está el ejecutable.
|
|
49
|
+
* En npm: root del proyecto (unimos dist/..).
|
|
50
|
+
*/
|
|
51
|
+
function getBinaryDir() {
|
|
52
|
+
if (isBinary()) {
|
|
53
|
+
return path_1.default.dirname(process.argv[1]);
|
|
54
|
+
}
|
|
55
|
+
return path_1.default.join(__dirname, '..');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Retorna el directorio del dashboard build (dashboard/dist/).
|
|
59
|
+
* En binario: busca relativo al binario o en CLAUDESTAT_DATA_DIR.
|
|
60
|
+
* En npm: usa __dirname para encontrar dist/.
|
|
61
|
+
*/
|
|
62
|
+
function getDashboardDir() {
|
|
63
|
+
if (isBinary()) {
|
|
64
|
+
const candidates = [
|
|
65
|
+
path_1.default.join(getBinaryDir(), 'dashboard'),
|
|
66
|
+
path_1.default.join(getClaudestatDir(), 'dashboard'),
|
|
67
|
+
path_1.default.join(getBinaryDir(), '..', 'dashboard', 'dist'),
|
|
68
|
+
];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (fs_1.default.existsSync(path_1.default.join(c, 'index.html')))
|
|
71
|
+
return c;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return path_1.default.join(__dirname, '..', 'dashboard', 'dist');
|
|
75
|
+
}
|
|
32
76
|
// ─── Claude Code data directory ────────────────────────────────────────────────
|
|
33
77
|
/**
|
|
34
78
|
* Returns the Claude Code data directory (~/.claude on all platforms).
|
|
35
79
|
* Empirically verified: Claude Code CLI stores settings at ~/.claude on macOS, Linux, and Windows.
|
|
36
80
|
*/
|
|
37
81
|
function getClaudeDir() {
|
|
38
|
-
return path_1.default.join(os_1.default.homedir(), '.claude');
|
|
82
|
+
return process.env.CLAUDE_DIR ?? path_1.default.join(os_1.default.homedir(), '.claude');
|
|
39
83
|
}
|
|
40
84
|
// ─── ClaudeStat data directory ─────────────────────────────────────────────────
|
|
41
85
|
/**
|
package/dist/quota-tracker.js
CHANGED
|
@@ -298,7 +298,6 @@ function computeQuota(forcePlan) {
|
|
|
298
298
|
const cycleEntries = entries.filter(e => e.ts >= fiveHAgo);
|
|
299
299
|
const cycleTokens = cycleEntries.reduce((sum, e) => sum + (e.inputTokens ?? 0) + (e.outputTokens ?? 0), 0);
|
|
300
300
|
const cyclePct = Math.min(100, Math.round(cycleTokens / limits.tokens5h * 100));
|
|
301
|
-
const cycleResetMs = Math.max(0, cycleResetAt - now);
|
|
302
301
|
// ─ Semanal por modelo: ventanas de 5 min con actividad ─
|
|
303
302
|
// Contamos ventanas de 5 min distintas con al menos 1 respuesta por modelo
|
|
304
303
|
const sonnetWindows = new Set();
|
package/dist/roast.js
CHANGED
|
@@ -79,8 +79,6 @@ async function runRoast(opts) {
|
|
|
79
79
|
const D = '\x1b[2m';
|
|
80
80
|
const G = '\x1b[32m';
|
|
81
81
|
const Y = '\x1b[33m';
|
|
82
|
-
const C = '\x1b[36m';
|
|
83
|
-
const M = '\x1b[35m';
|
|
84
82
|
const bar = (pct, width = 20) => {
|
|
85
83
|
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
86
84
|
const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
|
package/dist/routes/events.d.ts
CHANGED
|
@@ -5,8 +5,6 @@ export declare const lastAgentByCwd: Map<string, {
|
|
|
5
5
|
session_id: string;
|
|
6
6
|
}>;
|
|
7
7
|
export declare const taggedSessionParents: Set<string>;
|
|
8
|
-
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
9
|
-
export declare function findProjectCwdForFile(filePath: string): string | undefined;
|
|
10
8
|
/**
|
|
11
9
|
* Cuando el enricher detecta nuevos tokens en un JSONL:
|
|
12
10
|
* 1. Corre el análisis de inteligencia
|
package/dist/routes/events.js
CHANGED
|
@@ -5,9 +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
|
-
exports.findProjectCwdForFile = findProjectCwdForFile;
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
10
|
-
const fs_1 = __importDefault(require("fs"));
|
|
11
9
|
const express_1 = require("express");
|
|
12
10
|
const db_1 = require("../db");
|
|
13
11
|
const intelligence_1 = require("../intelligence");
|
|
@@ -18,6 +16,7 @@ const config_1 = require("../config");
|
|
|
18
16
|
const rate_limiter_1 = require("../middleware/rate-limiter");
|
|
19
17
|
const stream_1 = require("./stream");
|
|
20
18
|
const notifier_1 = require("../notifier");
|
|
19
|
+
const helpers_1 = require("./helpers");
|
|
21
20
|
const enricher_1 = require("../enricher");
|
|
22
21
|
Object.defineProperty(exports, "processLatestForSession", { enumerable: true, get: function () { return enricher_1.processLatestForSession; } });
|
|
23
22
|
// ─── Loop alert cooldown (toolName:sessionId → last alert ts) ─────────────────
|
|
@@ -56,33 +55,20 @@ function shouldFireAlert(level, pct) {
|
|
|
56
55
|
alertCooldown.set(level, Date.now());
|
|
57
56
|
return true;
|
|
58
57
|
}
|
|
59
|
-
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
60
|
-
function findProjectCwdForFile(filePath) {
|
|
61
|
-
let dir = path_1.default.dirname(filePath);
|
|
62
|
-
for (let i = 0; i < 6; i++) {
|
|
63
|
-
if (fs_1.default.existsSync(path_1.default.join(dir, 'HANDOFF.md')))
|
|
64
|
-
return dir;
|
|
65
|
-
const parent = path_1.default.dirname(dir);
|
|
66
|
-
if (parent === dir)
|
|
67
|
-
break;
|
|
68
|
-
dir = parent;
|
|
69
|
-
}
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
58
|
exports.eventsRouter.post('/event', (req, res) => {
|
|
73
59
|
const ip = req.ip ?? '127.0.0.1';
|
|
74
60
|
if ((0, rate_limiter_1.isRateLimited)(ip)) {
|
|
75
61
|
res.status(429).json({ error: 'Too many requests — wait 1 minute' });
|
|
76
62
|
return;
|
|
77
63
|
}
|
|
78
|
-
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path } = req.body;
|
|
64
|
+
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path, source } = req.body;
|
|
79
65
|
if (!session_id || !type) {
|
|
80
66
|
res.status(400).json({ error: 'Missing session_id or type' });
|
|
81
67
|
return;
|
|
82
68
|
}
|
|
83
69
|
const resolvedCwd = cwd
|
|
84
70
|
?? (transcript_path ? path_1.default.dirname(transcript_path) || undefined : undefined);
|
|
85
|
-
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts });
|
|
71
|
+
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts, source });
|
|
86
72
|
// Skill grouping: get current parent BEFORE processing this event
|
|
87
73
|
// (the Skill Done event itself is NOT tagged — only its subsequent sub-calls are)
|
|
88
74
|
const skillParent = (tool_name !== 'Skill' && type !== 'Stop')
|
|
@@ -112,7 +98,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
112
98
|
session_id, type,
|
|
113
99
|
tool_name: tool_name ?? undefined,
|
|
114
100
|
tool_input: tool_input ? JSON.stringify(tool_input) : undefined,
|
|
115
|
-
ts, cwd: resolvedCwd, skill_parent: skillParent
|
|
101
|
+
ts, cwd: resolvedCwd, skill_parent: skillParent, source
|
|
116
102
|
});
|
|
117
103
|
(0, stream_1.broadcast)({ type: 'event', payload: { ...req.body, skill_parent: skillParent } });
|
|
118
104
|
// Stop limpia el skill activo para esta sesión
|
|
@@ -132,7 +118,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
132
118
|
const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : (tool_input ?? {});
|
|
133
119
|
const filePath = inp?.file_path ?? inp?.path;
|
|
134
120
|
if (typeof filePath === 'string' && path_1.default.isAbsolute(filePath)) {
|
|
135
|
-
const projectCwd = findProjectCwdForFile(filePath);
|
|
121
|
+
const projectCwd = (0, helpers_1.findProjectCwdForFile)(filePath);
|
|
136
122
|
if (projectCwd)
|
|
137
123
|
db_1.dbOps.updateSessionProject(session_id, projectCwd);
|
|
138
124
|
}
|
|
@@ -208,12 +194,12 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
208
194
|
* 2. Guarda el coste + score en DB
|
|
209
195
|
* 3. Hace broadcast vía SSE para que el watch muestre el coste actualizado
|
|
210
196
|
*/
|
|
211
|
-
const onCostUpdate = (sessionId, cost) => {
|
|
197
|
+
const onCostUpdate = (sessionId, cost, source) => {
|
|
212
198
|
// Ensure session row exists — sub-agent JSONLs arrive from the enricher without a
|
|
213
199
|
// prior hook event (Claude Code does not fire hooks for sub-agent sessions).
|
|
214
200
|
let sessionRow = db_1.dbOps.getSession(sessionId);
|
|
215
201
|
if (!sessionRow) {
|
|
216
|
-
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.firstTs ?? Date.now() });
|
|
202
|
+
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.firstTs ?? Date.now(), source });
|
|
217
203
|
sessionRow = db_1.dbOps.getSession(sessionId);
|
|
218
204
|
}
|
|
219
205
|
// Sub-agent detection: first time we see a session, check if its firstTs falls after
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.findProjectCwdForFile = findProjectCwdForFile;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
10
|
+
function findProjectCwdForFile(filePath) {
|
|
11
|
+
let dir = path_1.default.dirname(filePath);
|
|
12
|
+
for (let i = 0; i < 6; i++) {
|
|
13
|
+
if (fs_1.default.existsSync(path_1.default.join(dir, 'HANDOFF.md')))
|
|
14
|
+
return dir;
|
|
15
|
+
const parent = path_1.default.dirname(dir);
|
|
16
|
+
if (parent === dir)
|
|
17
|
+
break;
|
|
18
|
+
dir = parent;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
package/dist/routes/misc.js
CHANGED
|
@@ -9,7 +9,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.miscRouter = void 0;
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
|
-
const os_1 = __importDefault(require("os"));
|
|
13
12
|
const express_1 = require("express");
|
|
14
13
|
const db_1 = require("../db");
|
|
15
14
|
const intelligence_1 = require("../intelligence");
|
|
@@ -23,6 +22,7 @@ const projects_1 = require("./projects");
|
|
|
23
22
|
const session_state_1 = require("../session-state");
|
|
24
23
|
const stream_1 = require("./stream");
|
|
25
24
|
const paths_1 = require("../paths");
|
|
25
|
+
const cost_projector_1 = require("../cost-projector");
|
|
26
26
|
exports.miscRouter = (0, express_1.Router)();
|
|
27
27
|
// ─── GET /git?path=... — git info para un proyecto ────────────────────────────
|
|
28
28
|
exports.miscRouter.get('/git', (req, res) => {
|
|
@@ -135,7 +135,6 @@ exports.miscRouter.get('/system-config', (_req, res) => {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
try {
|
|
138
|
-
const home = os_1.default.homedir();
|
|
139
138
|
const claudeDir = (0, paths_1.getClaudeDir)();
|
|
140
139
|
let hooks = {};
|
|
141
140
|
try {
|
|
@@ -257,24 +256,7 @@ exports.miscRouter.put('/config', (req, res) => {
|
|
|
257
256
|
res.status(500).json({ error: String(e) });
|
|
258
257
|
}
|
|
259
258
|
});
|
|
260
|
-
// ─── GET /cost-projection —
|
|
259
|
+
// ─── GET /cost-projection — linear regression cost projection ───────────────
|
|
261
260
|
exports.miscRouter.get('/cost-projection', (_req, res) => {
|
|
262
|
-
|
|
263
|
-
const month = db_1.dbOps.getCostProjection(30);
|
|
264
|
-
const weekSpan = week.latest && week.earliest ? (week.latest - week.earliest) / 86400000 : 0;
|
|
265
|
-
const monthSpan = month.latest && month.earliest ? (month.latest - month.earliest) / 86400000 : 0;
|
|
266
|
-
const weeklyProjected = weekSpan > 0.5 ? (week.total_cost_usd ?? 0) / weekSpan * 7 : 0;
|
|
267
|
-
const monthlyProjected = monthSpan > 1 ? (month.total_cost_usd ?? 0) / monthSpan * 30 : 0;
|
|
268
|
-
res.json({
|
|
269
|
-
weekly: {
|
|
270
|
-
daysWithData: Math.round(weekSpan * 10) / 10,
|
|
271
|
-
costSoFar: week.total_cost_usd ?? 0,
|
|
272
|
-
projected: Math.round(weeklyProjected * 100) / 100,
|
|
273
|
-
},
|
|
274
|
-
monthly: {
|
|
275
|
-
daysWithData: Math.round(monthSpan * 10) / 10,
|
|
276
|
-
costSoFar: month.total_cost_usd ?? 0,
|
|
277
|
-
projected: Math.round(monthlyProjected * 100) / 100,
|
|
278
|
-
},
|
|
279
|
-
});
|
|
261
|
+
res.json((0, cost_projector_1.computeProjection)(90));
|
|
280
262
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { type EventRow } from '../db';
|
|
2
2
|
export declare const projectsRouter: import("express-serve-static-core").Router;
|
|
3
|
-
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
4
|
-
export declare function findProjectCwdForFile(filePath: string): string | undefined;
|
|
5
3
|
/** Infiere el proyecto activo mirando los eventos de archivo de una sesión */
|
|
6
4
|
export declare function inferProjectCwd(events: EventRow[]): string | undefined;
|
|
7
5
|
/**
|
package/dist/routes/projects.js
CHANGED
|
@@ -5,29 +5,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.projectsRouter = void 0;
|
|
8
|
-
exports.findProjectCwdForFile = findProjectCwdForFile;
|
|
9
8
|
exports.inferProjectCwd = inferProjectCwd;
|
|
10
9
|
exports.inferActiveProjectByMajority = inferActiveProjectByMajority;
|
|
11
10
|
const path_1 = __importDefault(require("path"));
|
|
12
|
-
const fs_1 = __importDefault(require("fs"));
|
|
13
11
|
const express_1 = require("express");
|
|
14
12
|
const db_1 = require("../db");
|
|
15
13
|
const projects_cache_1 = require("../cache/projects-cache");
|
|
16
14
|
const pattern_analyzer_1 = require("../pattern-analyzer");
|
|
15
|
+
const helpers_1 = require("./helpers");
|
|
17
16
|
exports.projectsRouter = (0, express_1.Router)();
|
|
18
|
-
/** Sube el árbol desde un file_path hasta encontrar HANDOFF.md → directorio del proyecto */
|
|
19
|
-
function findProjectCwdForFile(filePath) {
|
|
20
|
-
let dir = path_1.default.dirname(filePath);
|
|
21
|
-
for (let i = 0; i < 6; i++) {
|
|
22
|
-
if (fs_1.default.existsSync(path_1.default.join(dir, 'HANDOFF.md')))
|
|
23
|
-
return dir;
|
|
24
|
-
const parent = path_1.default.dirname(dir);
|
|
25
|
-
if (parent === dir)
|
|
26
|
-
break;
|
|
27
|
-
dir = parent;
|
|
28
|
-
}
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
17
|
/** Infiere el proyecto activo mirando los eventos de archivo de una sesión */
|
|
32
18
|
function inferProjectCwd(events) {
|
|
33
19
|
const FILE_TOOLS = new Set(['Read', 'Write', 'Edit', 'Glob', 'Grep']);
|
|
@@ -41,7 +27,7 @@ function inferProjectCwd(events) {
|
|
|
41
27
|
const filePath = (inp.file_path || inp.path);
|
|
42
28
|
if (!filePath || !path_1.default.isAbsolute(filePath))
|
|
43
29
|
continue;
|
|
44
|
-
const cwd = findProjectCwdForFile(filePath);
|
|
30
|
+
const cwd = (0, helpers_1.findProjectCwdForFile)(filePath);
|
|
45
31
|
if (cwd)
|
|
46
32
|
return cwd;
|
|
47
33
|
}
|
|
@@ -73,7 +59,7 @@ function inferActiveProjectByMajority(events, windowMs) {
|
|
|
73
59
|
const filePath = (inp.file_path || inp.path);
|
|
74
60
|
if (!filePath || !path_1.default.isAbsolute(filePath))
|
|
75
61
|
continue;
|
|
76
|
-
const project = findProjectCwdForFile(filePath);
|
|
62
|
+
const project = (0, helpers_1.findProjectCwdForFile)(filePath);
|
|
77
63
|
if (!project)
|
|
78
64
|
continue;
|
|
79
65
|
const entry = hits.get(project) ?? { count: 0, lastTs: 0 };
|
package/dist/routes/stream.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export declare const sessionLastEvent: Map<string, {
|
|
|
5
5
|
}>;
|
|
6
6
|
export declare function broadcast(msg: object): void;
|
|
7
7
|
export declare function getSseClientsSize(): number;
|
|
8
|
-
export declare function setOnCostUpdateRef(cb: (sessionId: string, cost: any) => void): void;
|
|
8
|
+
export declare function setOnCostUpdateRef(cb: (sessionId: string, cost: any, source?: string) => void): void;
|
package/dist/routes/stream.js
CHANGED
|
@@ -54,13 +54,13 @@ exports.streamRouter.get('/stream', (req, res) => {
|
|
|
54
54
|
: allEvents;
|
|
55
55
|
const lastEvt = exports.sessionLastEvent.get(latestSession.id);
|
|
56
56
|
const state = (0, session_state_1.deriveSessionState)(lastEvt?.type, lastEvt?.ts ?? latestSession.last_event_at ?? latestSession.started_at);
|
|
57
|
-
(0, enricher_1.getAllBlockCostsForSession)(latestSession.id).then(blockCosts => {
|
|
57
|
+
(0, enricher_1.getAllBlockCostsForSession)(latestSession.id).then((blockCosts) => {
|
|
58
58
|
const subAgentSessions = db_1.dbOps.getChildSessions(latestSession.id);
|
|
59
59
|
res.write(`data: ${JSON.stringify({ type: 'init', session: { ...latestSession, state }, events, blockCosts, subAgentSessions })}\n\n`);
|
|
60
60
|
if (_onCostUpdateRef) {
|
|
61
|
-
(0, enricher_1.processLatestForSession)(latestSession.id, _onCostUpdateRef).catch(err => console.error('[stream] Error processing latest session:', err));
|
|
61
|
+
(0, enricher_1.processLatestForSession)(latestSession.id, _onCostUpdateRef).catch((err) => console.error('[stream] Error processing latest session:', err));
|
|
62
62
|
}
|
|
63
|
-
}).catch(err => {
|
|
63
|
+
}).catch((err) => {
|
|
64
64
|
console.error('[stream] Error loading block costs:', err);
|
|
65
65
|
const subAgentSessions = db_1.dbOps.getChildSessions(latestSession.id);
|
|
66
66
|
res.write(`data: ${JSON.stringify({ type: 'init', session: { ...latestSession, state }, events, blockCosts: [], subAgentSessions })}\n\n`);
|
package/dist/service.js
CHANGED
|
@@ -12,9 +12,16 @@ const PLIST_LABEL = 'com.statforge.claudestat';
|
|
|
12
12
|
const PLIST_PATH = path_1.default.join(process.env.HOME ?? '~', 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
|
|
13
13
|
const SYSTEMD_DIR = path_1.default.join(process.env.HOME ?? '~', '.config', 'systemd', 'user');
|
|
14
14
|
const SYSTEMD_PATH = path_1.default.join(SYSTEMD_DIR, 'claudestat.service');
|
|
15
|
+
function isBinary() {
|
|
16
|
+
return process.argv[1]?.includes('claudestat') && !process.argv[1]?.includes('node_modules')
|
|
17
|
+
&& !process.argv[1]?.includes('dist/index.js');
|
|
18
|
+
}
|
|
19
|
+
function serviceCommand() {
|
|
20
|
+
if (isBinary())
|
|
21
|
+
return process.argv[1];
|
|
22
|
+
return `${process.execPath} ${process.argv[1]}`;
|
|
23
|
+
}
|
|
15
24
|
function makePlist() {
|
|
16
|
-
const node = process.execPath;
|
|
17
|
-
const script = process.argv[1];
|
|
18
25
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
19
26
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
20
27
|
<plist version="1.0">
|
|
@@ -23,8 +30,7 @@ function makePlist() {
|
|
|
23
30
|
<string>${PLIST_LABEL}</string>
|
|
24
31
|
<key>ProgramArguments</key>
|
|
25
32
|
<array>
|
|
26
|
-
<string>${
|
|
27
|
-
<string>${script}</string>
|
|
33
|
+
<string>${serviceCommand()}</string>
|
|
28
34
|
<string>start</string>
|
|
29
35
|
</array>
|
|
30
36
|
<key>EnvironmentVariables</key>
|
|
@@ -40,15 +46,13 @@ function makePlist() {
|
|
|
40
46
|
</plist>`;
|
|
41
47
|
}
|
|
42
48
|
function makeUnit() {
|
|
43
|
-
const node = process.execPath;
|
|
44
|
-
const script = process.argv[1];
|
|
45
49
|
return `[Unit]
|
|
46
50
|
Description=ClaudeStat daemon — real-time Claude Code monitor
|
|
47
51
|
After=default.target
|
|
48
52
|
|
|
49
53
|
[Service]
|
|
50
54
|
Type=simple
|
|
51
|
-
ExecStart=${
|
|
55
|
+
ExecStart=${serviceCommand()} start
|
|
52
56
|
Restart=on-failure
|
|
53
57
|
RestartSec=5
|
|
54
58
|
Environment=CLAUDESTAT_DAEMON=1
|
package/dist/watch.js
CHANGED
|
@@ -80,7 +80,6 @@ async function startWatch() {
|
|
|
80
80
|
}
|
|
81
81
|
fetchQuota().then(pct => { state.cyclePct = pct; });
|
|
82
82
|
setInterval(async () => { state.cyclePct = await fetchQuota(); }, 30000);
|
|
83
|
-
// Refrescar stats semanales cada 5 minutos
|
|
84
83
|
setInterval(() => { state.weekly = (0, weekly_1.readWeeklyStats)(); }, 5 * 60 * 1000);
|
|
85
84
|
function draw() {
|
|
86
85
|
if (state.sessionId) {
|
package/dist/watchdog.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* watchdog.ts — Daemon auto-restart mechanism
|
|
3
3
|
*
|
|
4
|
+
* NOTE: The watchdog currently runs in the same process as the daemon.
|
|
5
|
+
* This means it cannot restart the daemon if the process crashes.
|
|
6
|
+
* For true resilience, the watchdog should be spawned as a separate
|
|
7
|
+
* process via `child_process.spawn()` with `detached: true`.
|
|
8
|
+
*
|
|
4
9
|
* If the daemon process crashes or is killed unexpectedly, the watchdog
|
|
5
10
|
* detects the stale PID file and relaunches the daemon automatically.
|
|
6
11
|
*
|
package/dist/watchdog.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* watchdog.ts — Daemon auto-restart mechanism
|
|
4
4
|
*
|
|
5
|
+
* NOTE: The watchdog currently runs in the same process as the daemon.
|
|
6
|
+
* This means it cannot restart the daemon if the process crashes.
|
|
7
|
+
* For true resilience, the watchdog should be spawned as a separate
|
|
8
|
+
* process via `child_process.spawn()` with `detached: true`.
|
|
9
|
+
*
|
|
5
10
|
* If the daemon process crashes or is killed unexpectedly, the watchdog
|
|
6
11
|
* detects the stale PID file and relaunches the daemon automatically.
|
|
7
12
|
*
|
|
@@ -69,7 +74,7 @@ function startWatchdog() {
|
|
|
69
74
|
catch { }
|
|
70
75
|
restartDaemon();
|
|
71
76
|
}
|
|
72
|
-
}, CHECK_INTERVAL_MS);
|
|
77
|
+
}, CHECK_INTERVAL_MS).unref();
|
|
73
78
|
process.on('SIGTERM', () => { clearInterval(interval); process.exit(0); });
|
|
74
79
|
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
75
80
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapter.ts — WatcherAdapter interface + factory para Multi-CLI support
|
|
3
|
+
*
|
|
4
|
+
* Cada CLI coding (Claude Code, Codex, OpenCode, etc.) tiene su propio
|
|
5
|
+
* formato de trazas y ubicación de archivos. Este adapter pattern permite
|
|
6
|
+
* que claudestat las soporte todas con una interfaz común.
|
|
7
|
+
*/
|
|
8
|
+
import type { CostUpdate } from '../db';
|
|
9
|
+
export interface ParsedEvent {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
type: string;
|
|
12
|
+
toolName?: string;
|
|
13
|
+
toolInput?: string;
|
|
14
|
+
ts: number;
|
|
15
|
+
cwd?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WatcherAdapter {
|
|
18
|
+
/** Nombre único del CLI (claude-code, codex, opencode, etc.) */
|
|
19
|
+
readonly name: string;
|
|
20
|
+
/** Label legible mostrado en UI */
|
|
21
|
+
readonly label: string;
|
|
22
|
+
/** Detecta si este CLI está instalado y tiene trazas disponibles */
|
|
23
|
+
detect(): boolean;
|
|
24
|
+
/** Directorios/glob a observar con chokidar */
|
|
25
|
+
getWatchPaths(): string[];
|
|
26
|
+
/** Parsea una línea de traza en un ParsedEvent */
|
|
27
|
+
parseEvent(raw: string, filePath: string): ParsedEvent | null;
|
|
28
|
+
/** Lee y calcula costos acumulados de un archivo de sesión */
|
|
29
|
+
getSessionCost(filePath: string): Promise<CostUpdate | null>;
|
|
30
|
+
/** Nombre amigable para el badge de la CLI (ej: "CC", "Codex") */
|
|
31
|
+
get shortName(): string;
|
|
32
|
+
}
|
|
33
|
+
export declare function registerAdapter(adapter: WatcherAdapter): void;
|
|
34
|
+
export declare function getAdapter(name: string): WatcherAdapter | undefined;
|
|
35
|
+
export declare function getAllAdapters(): WatcherAdapter[];
|
|
36
|
+
export declare function getActiveAdapters(): WatcherAdapter[];
|
|
37
|
+
export declare function getAdapterNames(): string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* adapter.ts — WatcherAdapter interface + factory para Multi-CLI support
|
|
4
|
+
*
|
|
5
|
+
* Cada CLI coding (Claude Code, Codex, OpenCode, etc.) tiene su propio
|
|
6
|
+
* formato de trazas y ubicación de archivos. Este adapter pattern permite
|
|
7
|
+
* que claudestat las soporte todas con una interfaz común.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.registerAdapter = registerAdapter;
|
|
11
|
+
exports.getAdapter = getAdapter;
|
|
12
|
+
exports.getAllAdapters = getAllAdapters;
|
|
13
|
+
exports.getActiveAdapters = getActiveAdapters;
|
|
14
|
+
exports.getAdapterNames = getAdapterNames;
|
|
15
|
+
// ─── Factory ───────────────────────────────────────────────────────────────────
|
|
16
|
+
const registry = new Map();
|
|
17
|
+
function registerAdapter(adapter) {
|
|
18
|
+
registry.set(adapter.name, adapter);
|
|
19
|
+
}
|
|
20
|
+
function getAdapter(name) {
|
|
21
|
+
return registry.get(name);
|
|
22
|
+
}
|
|
23
|
+
function getAllAdapters() {
|
|
24
|
+
return Array.from(registry.values());
|
|
25
|
+
}
|
|
26
|
+
function getActiveAdapters() {
|
|
27
|
+
return getAllAdapters().filter(a => a.detect());
|
|
28
|
+
}
|
|
29
|
+
function getAdapterNames() {
|
|
30
|
+
return Array.from(registry.keys());
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* amp.ts — WatcherAdapter para Amp (coding CLI agent)
|
|
3
|
+
*
|
|
4
|
+
* Amp stores traces in ~/.amp/logs/ as JSONL files.
|
|
5
|
+
* This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
|
|
6
|
+
*/
|
|
7
|
+
import { type WatcherAdapter } from './adapter';
|
|
8
|
+
export declare const ampAdapter: WatcherAdapter;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* amp.ts — WatcherAdapter para Amp (coding CLI agent)
|
|
4
|
+
*
|
|
5
|
+
* Amp stores traces in ~/.amp/logs/ as JSONL files.
|
|
6
|
+
* This is a scaffold — fill in parseEvent/getSessionCost with sample traces.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.ampAdapter = void 0;
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const adapter_1 = require("./adapter");
|
|
17
|
+
const AMP_DIR = path_1.default.join(os_1.default.homedir(), '.amp', 'logs');
|
|
18
|
+
exports.ampAdapter = {
|
|
19
|
+
name: 'amp',
|
|
20
|
+
label: 'Amp',
|
|
21
|
+
get shortName() { return 'Amp'; },
|
|
22
|
+
detect() {
|
|
23
|
+
try {
|
|
24
|
+
return fs_1.default.existsSync(AMP_DIR);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
getWatchPaths() {
|
|
31
|
+
return [`${AMP_DIR}/**/*.jsonl`];
|
|
32
|
+
},
|
|
33
|
+
parseEvent(_raw, _filePath) {
|
|
34
|
+
// TODO: implement when Amp trace format is known
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
async getSessionCost(_filePath) {
|
|
38
|
+
// TODO: implement when Amp trace format is known
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
(0, adapter_1.registerAdapter)(exports.ampAdapter);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* claude-code.ts — WatcherAdapter para Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Claude Code escribe trazas JSONL en ~/.claude/projects/{hash}/{session-id}.jsonl
|
|
5
|
+
* Cada línea "assistant" contiene usage tokens y modelo.
|
|
6
|
+
*/
|
|
7
|
+
import { type WatcherAdapter } from './adapter';
|
|
8
|
+
import type { BlockCostEntry } from '../db';
|
|
9
|
+
export declare function getContextWindow(model: string): number;
|
|
10
|
+
export declare const claudeCodeAdapter: WatcherAdapter;
|
|
11
|
+
export declare function getAllBlockCostsForSession(sessionId: string): Promise<BlockCostEntry[]>;
|
|
12
|
+
export interface SessionPrompt {
|
|
13
|
+
index: number;
|
|
14
|
+
ts: number;
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function getSessionPrompts(sessionId: string): Promise<SessionPrompt[]>;
|