@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.
Files changed (80) hide show
  1. package/README.md +437 -0
  2. package/dashboard/dist/assets/AnalyticsView-BApcOGsD.js +8 -0
  3. package/dashboard/dist/assets/HistoryView-B331k5oL.js +1 -0
  4. package/dashboard/dist/assets/ProjectsView-DUleaXsP.js +6 -0
  5. package/dashboard/dist/assets/SystemView-BGe__vl1.js +1 -0
  6. package/dashboard/dist/assets/TopView-CXggyydU.js +1 -0
  7. package/dashboard/dist/assets/index-CB01c5lb.js +84 -0
  8. package/dashboard/dist/assets/vendor-lucide-Cym0q5l_.js +344 -0
  9. package/dashboard/dist/assets/vendor-react-B_Jzs0gY.js +24 -0
  10. package/dashboard/dist/index.html +21 -0
  11. package/dist/cache/projects-cache.d.ts +9 -0
  12. package/dist/cache/projects-cache.js +51 -0
  13. package/dist/claude-auth.d.ts +38 -0
  14. package/dist/claude-auth.js +133 -0
  15. package/dist/claude-stats.d.ts +32 -0
  16. package/dist/claude-stats.js +98 -0
  17. package/dist/config.d.ts +43 -0
  18. package/dist/config.js +110 -0
  19. package/dist/daemon.d.ts +15 -0
  20. package/dist/daemon.js +247 -0
  21. package/dist/db.d.ts +134 -0
  22. package/dist/db.js +546 -0
  23. package/dist/doctor.d.ts +1 -0
  24. package/dist/doctor.js +191 -0
  25. package/dist/enricher.d.ts +34 -0
  26. package/dist/enricher.js +394 -0
  27. package/dist/export.d.ts +8 -0
  28. package/dist/export.js +82 -0
  29. package/dist/git.d.ts +22 -0
  30. package/dist/git.js +57 -0
  31. package/dist/github.d.ts +27 -0
  32. package/dist/github.js +62 -0
  33. package/dist/index.d.ts +8 -0
  34. package/dist/index.js +319 -0
  35. package/dist/install.d.ts +14 -0
  36. package/dist/install.js +202 -0
  37. package/dist/intelligence.d.ts +45 -0
  38. package/dist/intelligence.js +105 -0
  39. package/dist/meta-stats.d.ts +28 -0
  40. package/dist/meta-stats.js +137 -0
  41. package/dist/middleware/rate-limiter.d.ts +2 -0
  42. package/dist/middleware/rate-limiter.js +30 -0
  43. package/dist/notifier.d.ts +1 -0
  44. package/dist/notifier.js +22 -0
  45. package/dist/paths.d.ts +79 -0
  46. package/dist/paths.js +134 -0
  47. package/dist/pattern-analyzer.d.ts +35 -0
  48. package/dist/pattern-analyzer.js +123 -0
  49. package/dist/project-scanner.d.ts +71 -0
  50. package/dist/project-scanner.js +619 -0
  51. package/dist/quota-tracker.d.ts +45 -0
  52. package/dist/quota-tracker.js +320 -0
  53. package/dist/render.d.ts +55 -0
  54. package/dist/render.js +229 -0
  55. package/dist/routes/events.d.ts +18 -0
  56. package/dist/routes/events.js +272 -0
  57. package/dist/routes/history.d.ts +1 -0
  58. package/dist/routes/history.js +65 -0
  59. package/dist/routes/misc.d.ts +1 -0
  60. package/dist/routes/misc.js +280 -0
  61. package/dist/routes/projects.d.ts +15 -0
  62. package/dist/routes/projects.js +153 -0
  63. package/dist/routes/reports.d.ts +11 -0
  64. package/dist/routes/reports.js +205 -0
  65. package/dist/routes/stream.d.ts +8 -0
  66. package/dist/routes/stream.js +70 -0
  67. package/dist/routes/top.d.ts +1 -0
  68. package/dist/routes/top.js +30 -0
  69. package/dist/session-state.d.ts +35 -0
  70. package/dist/session-state.js +50 -0
  71. package/dist/summarizer.d.ts +18 -0
  72. package/dist/summarizer.js +137 -0
  73. package/dist/watch.d.ts +8 -0
  74. package/dist/watch.js +157 -0
  75. package/dist/watchdog.d.ts +11 -0
  76. package/dist/watchdog.js +75 -0
  77. package/dist/weekly.d.ts +13 -0
  78. package/dist/weekly.js +39 -0
  79. package/hooks/event.js +80 -0
  80. 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
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * watch.ts — Cliente SSE + renderizador (Phase 2)
3
+ *
4
+ * Novedades:
5
+ * - Maneja el evento 'cost_update' que llega del enricher
6
+ * - Actualiza cost en estado y redibuja con datos reales
7
+ */
8
+ export declare function startWatch(): Promise<void>;
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;
@@ -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
+ }
@@ -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
+ }