@statforge/claudestat 1.0.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/README.md +437 -0
- package/dashboard/dist/assets/AnalyticsView-BApcOGsD.js +8 -0
- package/dashboard/dist/assets/HistoryView-B331k5oL.js +1 -0
- package/dashboard/dist/assets/ProjectsView-DUleaXsP.js +6 -0
- package/dashboard/dist/assets/SystemView-BGe__vl1.js +1 -0
- package/dashboard/dist/assets/TopView-CXggyydU.js +1 -0
- package/dashboard/dist/assets/index-CB01c5lb.js +84 -0
- package/dashboard/dist/assets/vendor-lucide-Cym0q5l_.js +344 -0
- package/dashboard/dist/assets/vendor-react-B_Jzs0gY.js +24 -0
- package/dashboard/dist/index.html +21 -0
- package/dist/cache/projects-cache.d.ts +9 -0
- package/dist/cache/projects-cache.js +51 -0
- package/dist/claude-auth.d.ts +38 -0
- package/dist/claude-auth.js +133 -0
- package/dist/claude-stats.d.ts +32 -0
- package/dist/claude-stats.js +98 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +110 -0
- package/dist/daemon.d.ts +15 -0
- package/dist/daemon.js +247 -0
- package/dist/db.d.ts +134 -0
- package/dist/db.js +546 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +191 -0
- package/dist/enricher.d.ts +34 -0
- package/dist/enricher.js +394 -0
- package/dist/export.d.ts +8 -0
- package/dist/export.js +82 -0
- package/dist/git.d.ts +22 -0
- package/dist/git.js +57 -0
- package/dist/github.d.ts +27 -0
- package/dist/github.js +62 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +319 -0
- package/dist/install.d.ts +14 -0
- package/dist/install.js +202 -0
- package/dist/intelligence.d.ts +45 -0
- package/dist/intelligence.js +105 -0
- package/dist/meta-stats.d.ts +28 -0
- package/dist/meta-stats.js +137 -0
- package/dist/middleware/rate-limiter.d.ts +2 -0
- package/dist/middleware/rate-limiter.js +30 -0
- package/dist/notifier.d.ts +1 -0
- package/dist/notifier.js +22 -0
- package/dist/paths.d.ts +79 -0
- package/dist/paths.js +134 -0
- package/dist/pattern-analyzer.d.ts +35 -0
- package/dist/pattern-analyzer.js +123 -0
- package/dist/project-scanner.d.ts +71 -0
- package/dist/project-scanner.js +619 -0
- package/dist/quota-tracker.d.ts +45 -0
- package/dist/quota-tracker.js +320 -0
- package/dist/render.d.ts +55 -0
- package/dist/render.js +229 -0
- package/dist/routes/events.d.ts +18 -0
- package/dist/routes/events.js +272 -0
- package/dist/routes/history.d.ts +1 -0
- package/dist/routes/history.js +65 -0
- package/dist/routes/misc.d.ts +1 -0
- package/dist/routes/misc.js +280 -0
- package/dist/routes/projects.d.ts +15 -0
- package/dist/routes/projects.js +153 -0
- package/dist/routes/reports.d.ts +11 -0
- package/dist/routes/reports.js +205 -0
- package/dist/routes/stream.d.ts +8 -0
- package/dist/routes/stream.js +70 -0
- package/dist/routes/top.d.ts +1 -0
- package/dist/routes/top.js +30 -0
- package/dist/session-state.d.ts +35 -0
- package/dist/session-state.js +50 -0
- package/dist/summarizer.d.ts +18 -0
- package/dist/summarizer.js +137 -0
- package/dist/watch.d.ts +8 -0
- package/dist/watch.js +157 -0
- package/dist/watchdog.d.ts +11 -0
- package/dist/watchdog.js +75 -0
- package/dist/weekly.d.ts +13 -0
- package/dist/weekly.js +39 -0
- package/hooks/event.js +80 -0
- package/package.json +78 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* summarizer.ts — Resumen de sesión con IA (opcional)
|
|
4
|
+
*
|
|
5
|
+
* Solo se activa si ANTHROPIC_API_KEY está disponible en el entorno.
|
|
6
|
+
* Si no hay key → retorna null silenciosamente, sin errores ni warnings.
|
|
7
|
+
*
|
|
8
|
+
* Usa claude-haiku-4-5 (el modelo más económico) para minimizar coste.
|
|
9
|
+
* Un resumen de sesión consume ~200 tokens → ~$0.0002 por resumen.
|
|
10
|
+
*
|
|
11
|
+
* El cliente de Anthropic se carga con dynamic import para que el daemon
|
|
12
|
+
* no falle si @anthropic-ai/sdk no está instalado o la key no existe.
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.summarizeSession = summarizeSession;
|
|
52
|
+
const path_1 = __importDefault(require("path"));
|
|
53
|
+
// Lazy-loaded — se inicializa solo en el primer uso con API key disponible
|
|
54
|
+
let client = null;
|
|
55
|
+
async function getClient() {
|
|
56
|
+
if (!process.env.ANTHROPIC_API_KEY)
|
|
57
|
+
return null;
|
|
58
|
+
if (client)
|
|
59
|
+
return client;
|
|
60
|
+
try {
|
|
61
|
+
const { Anthropic } = await Promise.resolve().then(() => __importStar(require('@anthropic-ai/sdk')));
|
|
62
|
+
client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
63
|
+
return client;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null; // SDK no instalado — falla silenciosamente
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ─── Contexto de sesión para el prompt ───────────────────────────────────────
|
|
70
|
+
function buildContext(events, costUsd, projectName) {
|
|
71
|
+
// Herramientas únicas usadas, ordenadas por frecuencia
|
|
72
|
+
const toolCounts = new Map();
|
|
73
|
+
for (const e of events) {
|
|
74
|
+
if (e.tool_name && e.type === 'Done') {
|
|
75
|
+
toolCounts.set(e.tool_name, (toolCounts.get(e.tool_name) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const topTools = [...toolCounts.entries()]
|
|
79
|
+
.sort((a, b) => b[1] - a[1])
|
|
80
|
+
.slice(0, 6)
|
|
81
|
+
.map(([t, n]) => `${t}(${n})`)
|
|
82
|
+
.join(', ');
|
|
83
|
+
const toolCount = events.filter(e => e.type === 'Done').length;
|
|
84
|
+
const durationMin = events.length > 1
|
|
85
|
+
? Math.round((events[events.length - 1].ts - events[0].ts) / 60000)
|
|
86
|
+
: 0;
|
|
87
|
+
// Intentar inferir archivos tocados desde tool_input
|
|
88
|
+
const filesSet = new Set();
|
|
89
|
+
for (const e of events) {
|
|
90
|
+
if (!e.tool_input)
|
|
91
|
+
continue;
|
|
92
|
+
try {
|
|
93
|
+
const inp = JSON.parse(e.tool_input);
|
|
94
|
+
const fp = inp.file_path || inp.path;
|
|
95
|
+
if (typeof fp === 'string' && fp.startsWith('/')) {
|
|
96
|
+
filesSet.add(path_1.default.basename(fp));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch { /* ignorar */ }
|
|
100
|
+
}
|
|
101
|
+
const files = [...filesSet].slice(0, 5).join(', ');
|
|
102
|
+
return [
|
|
103
|
+
projectName ? `Proyecto: ${projectName}` : '',
|
|
104
|
+
`Duración: ${durationMin}min · ${toolCount} operaciones · $${costUsd.toFixed(4)}`,
|
|
105
|
+
topTools ? `Herramientas: ${topTools}` : '',
|
|
106
|
+
files ? `Archivos: ${files}` : '',
|
|
107
|
+
].filter(Boolean).join('\n');
|
|
108
|
+
}
|
|
109
|
+
// ─── Función principal ────────────────────────────────────────────────────────
|
|
110
|
+
/**
|
|
111
|
+
* Genera un resumen de 10-15 palabras de lo que hizo Claude en la sesión.
|
|
112
|
+
* Retorna null si no hay API key o falla la llamada.
|
|
113
|
+
*/
|
|
114
|
+
async function summarizeSession(events, costUsd, projectName) {
|
|
115
|
+
const c = await getClient();
|
|
116
|
+
if (!c)
|
|
117
|
+
return null;
|
|
118
|
+
// No resumir sesiones demasiado cortas (< 3 tool calls)
|
|
119
|
+
if (events.filter(e => e.type === 'Done').length < 3)
|
|
120
|
+
return null;
|
|
121
|
+
const context = buildContext(events, costUsd, projectName);
|
|
122
|
+
try {
|
|
123
|
+
const msg = await c.messages.create({
|
|
124
|
+
model: 'claude-haiku-4-5-20251001',
|
|
125
|
+
max_tokens: 60,
|
|
126
|
+
messages: [{
|
|
127
|
+
role: 'user',
|
|
128
|
+
content: `Resume en máximo 12 palabras en español qué hizo Claude en esta sesión:\n${context}\n\nResponde solo el resumen, sin comillas ni explicaciones.`,
|
|
129
|
+
}],
|
|
130
|
+
});
|
|
131
|
+
const text = msg.content?.[0]?.type === 'text' ? msg.content[0].text.trim() : null;
|
|
132
|
+
return text || null;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null; // error de API — falla silenciosamente
|
|
136
|
+
}
|
|
137
|
+
}
|
package/dist/watch.d.ts
ADDED
package/dist/watch.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* watch.ts — Cliente SSE + renderizador (Phase 2)
|
|
4
|
+
*
|
|
5
|
+
* Novedades:
|
|
6
|
+
* - Maneja el evento 'cost_update' que llega del enricher
|
|
7
|
+
* - Actualiza cost en estado y redibuja con datos reales
|
|
8
|
+
*/
|
|
9
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.startWatch = startWatch;
|
|
14
|
+
const http_1 = __importDefault(require("http"));
|
|
15
|
+
const render_1 = require("./render");
|
|
16
|
+
const weekly_1 = require("./weekly");
|
|
17
|
+
const DAEMON_HOST = 'localhost';
|
|
18
|
+
const DAEMON_PORT = 7337;
|
|
19
|
+
function clearScreen() { process.stdout.write('\x1b[2J\x1b[H'); }
|
|
20
|
+
async function checkDaemon() {
|
|
21
|
+
return new Promise(resolve => {
|
|
22
|
+
const req = http_1.default.get(`http://${DAEMON_HOST}:${DAEMON_PORT}/health`, res => {
|
|
23
|
+
resolve(res.statusCode === 200);
|
|
24
|
+
});
|
|
25
|
+
req.on('error', () => resolve(false));
|
|
26
|
+
req.setTimeout(1500, () => { req.destroy(); resolve(false); });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function connectSSE(onMessage) {
|
|
30
|
+
return new Promise((_, reject) => {
|
|
31
|
+
const req = http_1.default.request({
|
|
32
|
+
hostname: DAEMON_HOST, port: DAEMON_PORT, path: '/stream', method: 'GET',
|
|
33
|
+
headers: { Accept: 'text/event-stream', 'Cache-Control': 'no-cache' }
|
|
34
|
+
}, res => {
|
|
35
|
+
let buffer = '';
|
|
36
|
+
res.on('data', (chunk) => {
|
|
37
|
+
buffer += chunk.toString();
|
|
38
|
+
const parts = buffer.split('\n\n');
|
|
39
|
+
buffer = parts.pop() || '';
|
|
40
|
+
for (const part of parts) {
|
|
41
|
+
for (const line of part.split('\n')) {
|
|
42
|
+
if (line.startsWith('data: ')) {
|
|
43
|
+
try {
|
|
44
|
+
onMessage(JSON.parse(line.slice(6)));
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
res.on('end', () => reject(new Error('Stream closed')));
|
|
52
|
+
});
|
|
53
|
+
req.on('error', reject);
|
|
54
|
+
req.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function startWatch() {
|
|
58
|
+
const alive = await checkDaemon();
|
|
59
|
+
if (!alive) {
|
|
60
|
+
console.error('\n❌ Daemon is not running.');
|
|
61
|
+
console.error(' Run: \x1b[36mclaudestat start\x1b[0m\n');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
let state = {
|
|
65
|
+
sessionId: '', cwd: '', startedAt: Date.now(), events: [],
|
|
66
|
+
weekly: (0, weekly_1.readWeeklyStats)()
|
|
67
|
+
};
|
|
68
|
+
// Refrescar stats semanales cada 5 minutos
|
|
69
|
+
setInterval(() => { state.weekly = (0, weekly_1.readWeeklyStats)(); }, 5 * 60 * 1000);
|
|
70
|
+
function draw() {
|
|
71
|
+
if (state.sessionId) {
|
|
72
|
+
clearScreen();
|
|
73
|
+
process.stdout.write((0, render_1.renderTrace)(state));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function handleMessage(msg) {
|
|
77
|
+
if (msg.type === 'init') {
|
|
78
|
+
if (msg.session) {
|
|
79
|
+
state = {
|
|
80
|
+
sessionId: msg.session.id,
|
|
81
|
+
cwd: msg.session.cwd || '',
|
|
82
|
+
startedAt: msg.session.started_at,
|
|
83
|
+
events: (msg.events || []),
|
|
84
|
+
cost: buildCostFromSession(msg.session)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (msg.type === 'event') {
|
|
89
|
+
const evt = msg.payload;
|
|
90
|
+
// Nueva sesión → resetear estado
|
|
91
|
+
if (evt.session_id && evt.session_id !== state.sessionId && state.sessionId !== '') {
|
|
92
|
+
state = { sessionId: evt.session_id, cwd: evt.cwd || '', startedAt: evt.ts, events: [] };
|
|
93
|
+
}
|
|
94
|
+
else if (!state.sessionId && evt.session_id) {
|
|
95
|
+
state.sessionId = evt.session_id;
|
|
96
|
+
state.cwd = evt.cwd || '';
|
|
97
|
+
state.startedAt = evt.ts;
|
|
98
|
+
}
|
|
99
|
+
if (evt.type === 'Done' && evt.tool_name) {
|
|
100
|
+
// Actualizar el PreToolUse pendiente a Done
|
|
101
|
+
const pending = [...state.events].reverse()
|
|
102
|
+
.find(e => e.type === 'PreToolUse' && e.tool_name === evt.tool_name);
|
|
103
|
+
if (pending) {
|
|
104
|
+
pending.type = 'Done';
|
|
105
|
+
pending.duration_ms = evt.ts - pending.ts;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
state.events.push(evt);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else if (msg.type === 'cost_update') {
|
|
113
|
+
// El enricher calculó el coste real desde el JSONL — actualizar estado
|
|
114
|
+
const p = msg.payload;
|
|
115
|
+
if (p.session_id === state.sessionId) {
|
|
116
|
+
state.cost = {
|
|
117
|
+
cost_usd: p.cost_usd,
|
|
118
|
+
input_tokens: p.input_tokens,
|
|
119
|
+
output_tokens: p.output_tokens,
|
|
120
|
+
cache_read: p.cache_read,
|
|
121
|
+
cache_creation: p.cache_creation,
|
|
122
|
+
efficiency_score: p.efficiency_score,
|
|
123
|
+
context_used: p.context_used,
|
|
124
|
+
context_window: p.context_window,
|
|
125
|
+
loops: p.loops || [],
|
|
126
|
+
summary: p.summary
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
draw();
|
|
131
|
+
}
|
|
132
|
+
clearScreen();
|
|
133
|
+
process.stdout.write('\x1b[36m● claudestat watch\x1b[0m — connecting...\n');
|
|
134
|
+
while (true) {
|
|
135
|
+
try {
|
|
136
|
+
await connectSSE(handleMessage);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
clearScreen();
|
|
140
|
+
console.log('\x1b[33m⚠ Connection lost. Reconnecting in 2s...\x1b[0m');
|
|
141
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function buildCostFromSession(session) {
|
|
146
|
+
if (!session?.total_cost_usd)
|
|
147
|
+
return undefined;
|
|
148
|
+
return {
|
|
149
|
+
cost_usd: session.total_cost_usd ?? 0,
|
|
150
|
+
input_tokens: session.total_input_tokens ?? 0,
|
|
151
|
+
output_tokens: session.total_output_tokens ?? 0,
|
|
152
|
+
cache_read: session.total_cache_read ?? 0,
|
|
153
|
+
cache_creation: session.total_cache_creation ?? 0,
|
|
154
|
+
efficiency_score: session.efficiency_score ?? 100,
|
|
155
|
+
loops: []
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* watchdog.ts — Daemon auto-restart mechanism
|
|
3
|
+
*
|
|
4
|
+
* If the daemon process crashes or is killed unexpectedly, the watchdog
|
|
5
|
+
* detects the stale PID file and relaunches the daemon automatically.
|
|
6
|
+
*
|
|
7
|
+
* Usage: `claudestat start --watchdog`
|
|
8
|
+
* The watchdog runs as a separate lightweight process that periodically
|
|
9
|
+
* checks if the daemon PID is still alive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startWatchdog(): void;
|
package/dist/watchdog.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* watchdog.ts — Daemon auto-restart mechanism
|
|
4
|
+
*
|
|
5
|
+
* If the daemon process crashes or is killed unexpectedly, the watchdog
|
|
6
|
+
* detects the stale PID file and relaunches the daemon automatically.
|
|
7
|
+
*
|
|
8
|
+
* Usage: `claudestat start --watchdog`
|
|
9
|
+
* The watchdog runs as a separate lightweight process that periodically
|
|
10
|
+
* checks if the daemon PID is still alive.
|
|
11
|
+
*/
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.startWatchdog = startWatchdog;
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const paths_1 = require("./paths");
|
|
20
|
+
const PID_FILE = (0, paths_1.getPidFile)();
|
|
21
|
+
const CHECK_INTERVAL_MS = 10000;
|
|
22
|
+
const RESTART_COOLDOWN_MS = 30000;
|
|
23
|
+
let lastRestart = 0;
|
|
24
|
+
function isProcessAlive(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function readPid() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs_1.default.readFileSync(PID_FILE, 'utf8').trim();
|
|
36
|
+
const pid = parseInt(raw, 10);
|
|
37
|
+
return isNaN(pid) ? null : pid;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function restartDaemon() {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
if (now - lastRestart < RESTART_COOLDOWN_MS)
|
|
46
|
+
return;
|
|
47
|
+
lastRestart = now;
|
|
48
|
+
console.log(`[watchdog] Daemon not running — restarting...`);
|
|
49
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1] ?? 'claudestat', 'start'], {
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: 'ignore',
|
|
52
|
+
});
|
|
53
|
+
child.unref();
|
|
54
|
+
console.log(`[watchdog] Daemon restarted (pid ${child.pid})`);
|
|
55
|
+
}
|
|
56
|
+
function startWatchdog() {
|
|
57
|
+
console.log(`[watchdog] Starting — monitoring daemon every ${CHECK_INTERVAL_MS / 1000}s`);
|
|
58
|
+
const interval = setInterval(() => {
|
|
59
|
+
const pid = readPid();
|
|
60
|
+
if (pid === null) {
|
|
61
|
+
restartDaemon();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!isProcessAlive(pid)) {
|
|
65
|
+
console.log(`[watchdog] Daemon pid ${pid} is dead`);
|
|
66
|
+
try {
|
|
67
|
+
fs_1.default.unlinkSync(PID_FILE);
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
restartDaemon();
|
|
71
|
+
}
|
|
72
|
+
}, CHECK_INTERVAL_MS);
|
|
73
|
+
process.on('SIGTERM', () => { clearInterval(interval); process.exit(0); });
|
|
74
|
+
process.on('SIGINT', () => { clearInterval(interval); process.exit(0); });
|
|
75
|
+
}
|
package/dist/weekly.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weekly.ts — Tokens semanales desde stats-cache.json de Claude Code
|
|
3
|
+
* No necesita daemon ni API — lee el archivo directamente.
|
|
4
|
+
*/
|
|
5
|
+
export interface WeeklyStats {
|
|
6
|
+
totalTokens: number;
|
|
7
|
+
byDay: {
|
|
8
|
+
date: string;
|
|
9
|
+
tokens: number;
|
|
10
|
+
}[];
|
|
11
|
+
lastUpdated: string | null;
|
|
12
|
+
}
|
|
13
|
+
export declare function readWeeklyStats(): WeeklyStats;
|
package/dist/weekly.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* weekly.ts — Tokens semanales desde stats-cache.json de Claude Code
|
|
4
|
+
* No necesita daemon ni API — lee el archivo directamente.
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.readWeeklyStats = readWeeklyStats;
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const paths_1 = require("./paths");
|
|
14
|
+
const STATS_PATH = path_1.default.join((0, paths_1.getClaudeDir)(), 'stats-cache.json');
|
|
15
|
+
function readWeeklyStats() {
|
|
16
|
+
try {
|
|
17
|
+
const raw = fs_1.default.readFileSync(STATS_PATH, 'utf8');
|
|
18
|
+
const data = JSON.parse(raw);
|
|
19
|
+
const byDay = [];
|
|
20
|
+
// Tomar los últimos 7 días disponibles (sin filtrar por fecha actual,
|
|
21
|
+
// porque stats-cache.json puede estar desactualizado por días/semanas)
|
|
22
|
+
const allEntries = (data.dailyModelTokens || [])
|
|
23
|
+
.map((entry) => ({
|
|
24
|
+
date: entry.date,
|
|
25
|
+
tokens: Object.values(entry.tokensByModel)
|
|
26
|
+
.reduce((sum, n) => sum + n, 0)
|
|
27
|
+
}))
|
|
28
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
29
|
+
byDay.push(...allEntries.slice(-7));
|
|
30
|
+
return {
|
|
31
|
+
totalTokens: byDay.reduce((s, d) => s + d.tokens, 0),
|
|
32
|
+
byDay,
|
|
33
|
+
lastUpdated: data.lastComputedDate ?? null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { totalTokens: 0, byDay: [], lastUpdated: null };
|
|
38
|
+
}
|
|
39
|
+
}
|
package/hooks/event.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Hook universal de claudetrace.
|
|
4
|
+
* Claude Code lo ejecuta en cada evento del ciclo de vida.
|
|
5
|
+
* Recibe el JSON del evento por stdin y lo reenvía al daemon.
|
|
6
|
+
*
|
|
7
|
+
* Uso: node event.js <TipoEvento>
|
|
8
|
+
* Ejemplo: node event.js PreToolUse
|
|
9
|
+
*
|
|
10
|
+
* Kill switch (Phase 6):
|
|
11
|
+
* Para PreToolUse, después de enviar el evento al daemon, consulta GET /kill-switch.
|
|
12
|
+
* Si el daemon responde { blocked: true }, este hook termina con exit(2),
|
|
13
|
+
* lo que hace que Claude Code cancele la acción del tool antes de ejecutarla.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const eventType = process.argv[2] || 'Unknown'
|
|
17
|
+
const DAEMON_URL = 'http://localhost:7337/event'
|
|
18
|
+
const KILL_SWITCH_URL = 'http://localhost:7337/kill-switch'
|
|
19
|
+
|
|
20
|
+
let rawData = ''
|
|
21
|
+
process.stdin.on('data', chunk => { rawData += chunk })
|
|
22
|
+
process.stdin.on('end', () => {
|
|
23
|
+
let hookData = {}
|
|
24
|
+
try { hookData = JSON.parse(rawData) } catch (_) {}
|
|
25
|
+
|
|
26
|
+
const payload = {
|
|
27
|
+
type: eventType,
|
|
28
|
+
ts: Date.now(),
|
|
29
|
+
...hookData
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Para PreToolUse: enviamos el evento Y consultamos el kill-switch en paralelo.
|
|
33
|
+
// Si el daemon bloquea, salimos con exit(2) para cancelar la acción.
|
|
34
|
+
if (eventType === 'PreToolUse') {
|
|
35
|
+
Promise.all([
|
|
36
|
+
// 1. Registrar el evento (fire-and-forget, no nos importa el resultado)
|
|
37
|
+
fetch(DAEMON_URL, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(payload),
|
|
41
|
+
signal: AbortSignal.timeout(1500),
|
|
42
|
+
}).catch(() => null),
|
|
43
|
+
|
|
44
|
+
// 2. Consultar el kill-switch con timeout corto para no retrasar a Claude
|
|
45
|
+
fetch(KILL_SWITCH_URL, {
|
|
46
|
+
signal: AbortSignal.timeout(1500),
|
|
47
|
+
})
|
|
48
|
+
.then(r => r.json())
|
|
49
|
+
.catch(() => {
|
|
50
|
+
// Si el daemon no responde, loggeamos en stderr (visible en logs de Claude)
|
|
51
|
+
// pero NO bloqueamos — un fallo del daemon no debe interrumpir el trabajo
|
|
52
|
+
process.stderr.write(`[claudetrace] daemon no disponible — kill-switch desactivado\n`)
|
|
53
|
+
return { blocked: false }
|
|
54
|
+
}),
|
|
55
|
+
])
|
|
56
|
+
.then(([_, ks]) => {
|
|
57
|
+
if (ks && ks.blocked) {
|
|
58
|
+
// Claude Code muestra este stderr al usuario antes de cancelar la acción
|
|
59
|
+
process.stderr.write(`\n🚫 claudetrace kill switch activado\n`)
|
|
60
|
+
process.stderr.write(` ${ks.reason ?? 'Cuota de uso superada.'}\n\n`)
|
|
61
|
+
process.exit(2)
|
|
62
|
+
} else {
|
|
63
|
+
process.exit(0)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch(() => process.exit(0)) // cualquier error → no bloquear
|
|
67
|
+
|
|
68
|
+
} else {
|
|
69
|
+
// Para todos los demás tipos (SessionStart, PostToolUse, Stop):
|
|
70
|
+
// enviar el evento y siempre salir con 0 — NUNCA bloquear a Claude.
|
|
71
|
+
fetch(DAEMON_URL, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify(payload),
|
|
75
|
+
signal: AbortSignal.timeout(2000),
|
|
76
|
+
})
|
|
77
|
+
.catch(() => {}) // error silencioso
|
|
78
|
+
.finally(() => process.exit(0))
|
|
79
|
+
}
|
|
80
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statforge/claudestat",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Observability layer for Claude Code — live token tracking, cost analytics, quota guard, loop detection, and usage dashboard. The htop for Claude Code.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude",
|
|
8
|
+
"anthropic",
|
|
9
|
+
"claude-monitoring",
|
|
10
|
+
"token-tracker",
|
|
11
|
+
"cost-tracker",
|
|
12
|
+
"ai-observability",
|
|
13
|
+
"claude-dashboard",
|
|
14
|
+
"quota-guard",
|
|
15
|
+
"rate-limit",
|
|
16
|
+
"ai-agent",
|
|
17
|
+
"llm-cost",
|
|
18
|
+
"token-usage",
|
|
19
|
+
"claude-hooks",
|
|
20
|
+
"ai-productivity",
|
|
21
|
+
"terminal-dashboard",
|
|
22
|
+
"developer-tools",
|
|
23
|
+
"cli",
|
|
24
|
+
"monitoring",
|
|
25
|
+
"analytics",
|
|
26
|
+
"claude-pro",
|
|
27
|
+
"claude-max",
|
|
28
|
+
"ai-coding",
|
|
29
|
+
"agentic-workflow",
|
|
30
|
+
"observability"
|
|
31
|
+
],
|
|
32
|
+
"homepage": "https://github.com/DeibyGS/claudestat#readme",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/DeibyGS/claudestat.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/DeibyGS/claudestat/issues"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"funding": {
|
|
42
|
+
"type": "github",
|
|
43
|
+
"url": "https://github.com/sponsors/DeibyGS"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist/",
|
|
47
|
+
"hooks/",
|
|
48
|
+
"dashboard/dist/"
|
|
49
|
+
],
|
|
50
|
+
"bin": {
|
|
51
|
+
"claudestat": "dist/index.js"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc && npm run build:dashboard",
|
|
55
|
+
"build:dashboard": "cd dashboard && npm install && npm run build",
|
|
56
|
+
"prepublishOnly": "npm run build",
|
|
57
|
+
"prepack": "npm run build",
|
|
58
|
+
"dev": "tsx src/index.ts",
|
|
59
|
+
"dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
|
|
60
|
+
"start": "node dist/index.js",
|
|
61
|
+
"test": "node --require tsx/cjs tests/index.ts"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@anthropic-ai/sdk": "^0.88.0",
|
|
65
|
+
"chokidar": "^3.6.0",
|
|
66
|
+
"commander": "^12.1.0",
|
|
67
|
+
"express": "^4.19.2"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@types/express": "^4.17.21",
|
|
71
|
+
"@types/node": "^20.12.0",
|
|
72
|
+
"tsx": "^4.7.2",
|
|
73
|
+
"typescript": "^5.4.5"
|
|
74
|
+
},
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=22.0.0"
|
|
77
|
+
}
|
|
78
|
+
}
|