@statforge/claudestat 1.3.0 → 1.5.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 +140 -547
- package/dist/config.d.ts +1 -0
- package/dist/config.js +6 -0
- package/dist/cost-projector.d.ts +24 -0
- package/dist/cost-projector.js +133 -0
- package/dist/daemon.js +1 -1
- 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 +48 -7
- 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 +44 -0
- package/dist/quota-tracker.js +0 -1
- package/dist/roast.js +0 -2
- package/dist/routes/events.js +29 -5
- package/dist/routes/misc.js +4 -22
- package/dist/routes/stream.d.ts +1 -1
- package/dist/routes/stream.js +3 -3
- package/dist/service.js +11 -7
- 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/hooks/event.js +4 -9
- package/package.json +10 -2
package/dist/paths.d.ts
CHANGED
|
@@ -10,6 +10,23 @@
|
|
|
10
10
|
* Claude Code encodes project paths by replacing path separators AND colons with '-'.
|
|
11
11
|
* This module provides helpers to encode/decode those paths cross-platform.
|
|
12
12
|
*/
|
|
13
|
+
/**
|
|
14
|
+
* Detecta si claudestat está corriendo como binario standalone (Bun compile)
|
|
15
|
+
* vs desde npm (node dist/index.js).
|
|
16
|
+
*/
|
|
17
|
+
export declare function isBinary(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Retorna el directorio base del binario o del proyecto.
|
|
20
|
+
* En binario: directorio donde está el ejecutable.
|
|
21
|
+
* En npm: root del proyecto (unimos dist/..).
|
|
22
|
+
*/
|
|
23
|
+
export declare function getBinaryDir(): string;
|
|
24
|
+
/**
|
|
25
|
+
* Retorna el directorio del dashboard build (dashboard/dist/).
|
|
26
|
+
* En binario: busca relativo al binario o en CLAUDESTAT_DATA_DIR.
|
|
27
|
+
* En npm: usa __dirname para encontrar dist/.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getDashboardDir(): string;
|
|
13
30
|
/**
|
|
14
31
|
* Returns the Claude Code data directory (~/.claude on all platforms).
|
|
15
32
|
* Empirically verified: Claude Code CLI stores settings at ~/.claude on macOS, Linux, and Windows.
|
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,7 +31,48 @@ 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).
|
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.js
CHANGED
|
@@ -17,8 +17,14 @@ const quota_tracker_1 = require("../quota-tracker");
|
|
|
17
17
|
const config_1 = require("../config");
|
|
18
18
|
const rate_limiter_1 = require("../middleware/rate-limiter");
|
|
19
19
|
const stream_1 = require("./stream");
|
|
20
|
+
const notifier_1 = require("../notifier");
|
|
20
21
|
const enricher_1 = require("../enricher");
|
|
21
22
|
Object.defineProperty(exports, "processLatestForSession", { enumerable: true, get: function () { return enricher_1.processLatestForSession; } });
|
|
23
|
+
// ─── Loop alert cooldown (toolName:sessionId → last alert ts) ─────────────────
|
|
24
|
+
const loopAlertCooldown = new Map();
|
|
25
|
+
const LOOP_ALERT_COOLDOWN_MS = 120000; // coincide con LOOP_COOLDOWN_MS en intelligence.ts
|
|
26
|
+
// ─── Session cost alert: sesiones que ya recibieron notificación ───────────────
|
|
27
|
+
const sessionCostAlertFired = new Set();
|
|
22
28
|
exports.eventsRouter = (0, express_1.Router)();
|
|
23
29
|
// Skill activa por sesión — se setea tras Skill Done, se limpia en Stop.
|
|
24
30
|
// Permite taggear los eventos siguientes con skill_parent para agruparlos en la UI.
|
|
@@ -69,14 +75,14 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
69
75
|
res.status(429).json({ error: 'Too many requests — wait 1 minute' });
|
|
70
76
|
return;
|
|
71
77
|
}
|
|
72
|
-
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path } = req.body;
|
|
78
|
+
const { type, session_id, tool_name, tool_input, tool_response, ts, cwd, transcript_path, source } = req.body;
|
|
73
79
|
if (!session_id || !type) {
|
|
74
80
|
res.status(400).json({ error: 'Missing session_id or type' });
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
77
83
|
const resolvedCwd = cwd
|
|
78
84
|
?? (transcript_path ? path_1.default.dirname(transcript_path) || undefined : undefined);
|
|
79
|
-
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts });
|
|
85
|
+
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts, source });
|
|
80
86
|
// Skill grouping: get current parent BEFORE processing this event
|
|
81
87
|
// (the Skill Done event itself is NOT tagged — only its subsequent sub-calls are)
|
|
82
88
|
const skillParent = (tool_name !== 'Skill' && type !== 'Stop')
|
|
@@ -106,7 +112,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
106
112
|
session_id, type,
|
|
107
113
|
tool_name: tool_name ?? undefined,
|
|
108
114
|
tool_input: tool_input ? JSON.stringify(tool_input) : undefined,
|
|
109
|
-
ts, cwd: resolvedCwd, skill_parent: skillParent
|
|
115
|
+
ts, cwd: resolvedCwd, skill_parent: skillParent, source
|
|
110
116
|
});
|
|
111
117
|
(0, stream_1.broadcast)({ type: 'event', payload: { ...req.body, skill_parent: skillParent } });
|
|
112
118
|
// Stop limpia el skill activo para esta sesión
|
|
@@ -202,12 +208,12 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
202
208
|
* 2. Guarda el coste + score en DB
|
|
203
209
|
* 3. Hace broadcast vía SSE para que el watch muestre el coste actualizado
|
|
204
210
|
*/
|
|
205
|
-
const onCostUpdate = (sessionId, cost) => {
|
|
211
|
+
const onCostUpdate = (sessionId, cost, source) => {
|
|
206
212
|
// Ensure session row exists — sub-agent JSONLs arrive from the enricher without a
|
|
207
213
|
// prior hook event (Claude Code does not fire hooks for sub-agent sessions).
|
|
208
214
|
let sessionRow = db_1.dbOps.getSession(sessionId);
|
|
209
215
|
if (!sessionRow) {
|
|
210
|
-
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.firstTs ?? Date.now() });
|
|
216
|
+
db_1.dbOps.upsertSession({ id: sessionId, cwd: undefined, started_at: cost.firstTs ?? Date.now(), last_event_at: cost.firstTs ?? Date.now(), source });
|
|
211
217
|
sessionRow = db_1.dbOps.getSession(sessionId);
|
|
212
218
|
}
|
|
213
219
|
// Sub-agent detection: first time we see a session, check if its firstTs falls after
|
|
@@ -248,6 +254,24 @@ const onCostUpdate = (sessionId, cost) => {
|
|
|
248
254
|
projected_hourly_usd: projectedHourlyUsd,
|
|
249
255
|
}
|
|
250
256
|
});
|
|
257
|
+
// ─── Session cost alert: notificar si la sesión supera el límite configurado ──
|
|
258
|
+
const cfg = (0, config_1.readConfig)();
|
|
259
|
+
if (cfg.alertsEnabled && cfg.sessionCostLimitUsd > 0 && cost.cost_usd >= cfg.sessionCostLimitUsd && !sessionCostAlertFired.has(sessionId)) {
|
|
260
|
+
sessionCostAlertFired.add(sessionId);
|
|
261
|
+
(0, notifier_1.sendDesktopNotification)('claudestat — Session cost limit reached', `Session cost: $${cost.cost_usd.toFixed(2)} — limit $${cfg.sessionCostLimitUsd.toFixed(2)} reached`);
|
|
262
|
+
}
|
|
263
|
+
// ─── Loop alert: notificación de escritorio si hay loops activos ─────────────
|
|
264
|
+
if (cfg.alertsEnabled && report.loops.length > 0) {
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
for (const loop of report.loops) {
|
|
267
|
+
const key = `${loop.toolName}:${sessionId}`;
|
|
268
|
+
const lastSent = loopAlertCooldown.get(key) ?? 0;
|
|
269
|
+
if (now - lastSent >= LOOP_ALERT_COOLDOWN_MS) {
|
|
270
|
+
loopAlertCooldown.set(key, now);
|
|
271
|
+
(0, notifier_1.sendDesktopNotification)('claudestat — Loop detected', `${loop.toolName} called ${loop.count}× in 2min — session may be stuck`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
251
275
|
// Emitir desglose de costo del último bloque (input vs output) para el TracePanel
|
|
252
276
|
if (cost.lastEntry) {
|
|
253
277
|
(0, stream_1.broadcast)({
|
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) => {
|
|
@@ -90,7 +90,7 @@ exports.miscRouter.get('/kill-switch', (_req, res) => {
|
|
|
90
90
|
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
91
91
|
const blocked = cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold;
|
|
92
92
|
const reason = blocked
|
|
93
|
-
? `
|
|
93
|
+
? `Quota at ${data.cyclePct}% — kill switch threshold is ${cfg.killSwitchThreshold}%. Resets in ${formatMs(data.cycleResetMs)}.`
|
|
94
94
|
: undefined;
|
|
95
95
|
res.json({ blocked, reason, cyclePct: data.cyclePct });
|
|
96
96
|
}
|
|
@@ -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
|
});
|
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
|
|
@@ -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[]>;
|