@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
package/dist/install.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* install.ts — Instalador de hooks en Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Claude Code permite definir hooks en ~/.claude/settings.json.
|
|
6
|
+
* Este comando modifica ese archivo para agregar nuestros hooks
|
|
7
|
+
* sin pisar los que ya existan.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANTE: Hacemos un backup antes de modificar.
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.installHooks = installHooks;
|
|
16
|
+
exports.runInstall = runInstall;
|
|
17
|
+
exports.runWizard = runWizard;
|
|
18
|
+
exports.showInstallStatus = showInstallStatus;
|
|
19
|
+
exports.uninstallHooks = uninstallHooks;
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const readline_1 = __importDefault(require("readline"));
|
|
23
|
+
const paths_1 = require("./paths");
|
|
24
|
+
const config_1 = require("./config");
|
|
25
|
+
const CLAUDESTAT_DIR = (0, paths_1.getClaudestatDir)();
|
|
26
|
+
const CLAUDE_SETTINGS = path_1.default.join((0, paths_1.getClaudeDir)(), 'settings.json');
|
|
27
|
+
const HOOKS_DIR = path_1.default.join(CLAUDESTAT_DIR, 'hooks');
|
|
28
|
+
const HOOK_SCRIPT = path_1.default.join(HOOKS_DIR, 'event.js');
|
|
29
|
+
function installHookScript() {
|
|
30
|
+
fs_1.default.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
31
|
+
// El script original está en el paquete junto a este archivo
|
|
32
|
+
const source = path_1.default.join(__dirname, '..', 'hooks', 'event.js');
|
|
33
|
+
fs_1.default.copyFileSync(source, HOOK_SCRIPT);
|
|
34
|
+
if (!paths_1.isWindows) {
|
|
35
|
+
fs_1.default.chmodSync(HOOK_SCRIPT, 0o755);
|
|
36
|
+
}
|
|
37
|
+
console.log(`✓ Hook script installed → ${HOOK_SCRIPT}`);
|
|
38
|
+
}
|
|
39
|
+
function hookEntry(eventType, matcher = '.*') {
|
|
40
|
+
return {
|
|
41
|
+
matcher,
|
|
42
|
+
hooks: [{
|
|
43
|
+
type: 'command',
|
|
44
|
+
// Usamos el path absoluto para que funcione desde cualquier directorio
|
|
45
|
+
command: `node "${HOOK_SCRIPT}" ${eventType}`
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function installHooks() {
|
|
50
|
+
installHookScript();
|
|
51
|
+
// Leer settings.json existente
|
|
52
|
+
let settings = {};
|
|
53
|
+
try {
|
|
54
|
+
settings = JSON.parse(fs_1.default.readFileSync(CLAUDE_SETTINGS, 'utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.error(`\n❌ Could not read ${CLAUDE_SETTINGS}`);
|
|
58
|
+
console.error(' Make sure Claude Code is installed.\n');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
// Backup antes de modificar
|
|
62
|
+
const backupPath = CLAUDE_SETTINGS + '.bak';
|
|
63
|
+
fs_1.default.copyFileSync(CLAUDE_SETTINGS, backupPath);
|
|
64
|
+
console.log(`✓ Backup created → ${backupPath}`);
|
|
65
|
+
if (!settings.hooks)
|
|
66
|
+
settings.hooks = {};
|
|
67
|
+
const hookTypes = ['SessionStart', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
68
|
+
let added = 0;
|
|
69
|
+
for (const hookType of hookTypes) {
|
|
70
|
+
if (!settings.hooks[hookType])
|
|
71
|
+
settings.hooks[hookType] = [];
|
|
72
|
+
const exists = settings.hooks[hookType].some(hasClaudestatHook);
|
|
73
|
+
if (!exists) {
|
|
74
|
+
settings.hooks[hookType].push(hookEntry(hookType));
|
|
75
|
+
console.log(`✓ Hook configured: ${hookType}`);
|
|
76
|
+
added++;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log(` (already installed): ${hookType}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
fs_1.default.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
|
|
83
|
+
if (added > 0) {
|
|
84
|
+
console.log(`\n✅ ${added} hooks installed.`);
|
|
85
|
+
console.log(' Restart Claude Code to activate them.\n');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.log('\n✅ All hooks already installed.\n');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const CONFIG_PATH = path_1.default.join(CLAUDESTAT_DIR, 'config.json');
|
|
92
|
+
function hasClaudestatHook(entry) {
|
|
93
|
+
return entry.hooks?.some((h) => typeof h.command === 'string' && h.command.includes('claudestat'));
|
|
94
|
+
}
|
|
95
|
+
async function runInstall() {
|
|
96
|
+
if (!fs_1.default.existsSync(CONFIG_PATH)) {
|
|
97
|
+
await runWizard();
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
showInstallStatus();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function runWizard() {
|
|
104
|
+
const nonInteractive = !process.stdin.isTTY;
|
|
105
|
+
console.log('\n╔═══════════════════════════════════════════╗');
|
|
106
|
+
console.log('║ claudestat — First Install ║');
|
|
107
|
+
console.log('╚═══════════════════════════════════════════╝\n');
|
|
108
|
+
console.log('claudestat hooks into Claude Code to capture:');
|
|
109
|
+
console.log(' • Tool calls (name, duration, input/output)');
|
|
110
|
+
console.log(' • Token usage and costs per session');
|
|
111
|
+
console.log(' • Quota consumption (5h rolling window)\n');
|
|
112
|
+
console.log('It modifies ~/.claude/settings.json to add lifecycle hooks.');
|
|
113
|
+
console.log('A backup is created before any change.\n');
|
|
114
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
115
|
+
try {
|
|
116
|
+
// Paso 1: confirmación
|
|
117
|
+
if (!nonInteractive) {
|
|
118
|
+
const answer = await new Promise(resolve => rl.question('Continue with installation? [Y/n] ', resolve));
|
|
119
|
+
if (answer.trim().toLowerCase() === 'n') {
|
|
120
|
+
console.log('\nInstallation cancelled.\n');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Paso 2: verificar versión de Node
|
|
125
|
+
const nodeMajor = parseInt(process.version.slice(1).split('.')[0], 10);
|
|
126
|
+
if (nodeMajor < 22) {
|
|
127
|
+
console.log(`\n⚠ Node.js ${process.version} detected — claudestat requires Node ≥ 22.`);
|
|
128
|
+
console.log(' Some features may not work correctly.\n');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`✓ Node.js ${process.version}`);
|
|
132
|
+
}
|
|
133
|
+
// Paso 3: selección de plan
|
|
134
|
+
let plan = 'pro';
|
|
135
|
+
if (!nonInteractive) {
|
|
136
|
+
console.log('\nSelect your Claude plan:');
|
|
137
|
+
console.log(' 1) free 2) pro (default) 3) max5 4) max20');
|
|
138
|
+
const input = await new Promise(resolve => rl.question('Plan [1-4, default: 2]: ', resolve));
|
|
139
|
+
const planMap = { '1': 'free', '2': 'pro', '3': 'max5', '4': 'max20' };
|
|
140
|
+
plan = planMap[input.trim()] ?? 'pro';
|
|
141
|
+
}
|
|
142
|
+
console.log(`✓ Plan: ${plan}`);
|
|
143
|
+
// Paso 4: crear config inicial
|
|
144
|
+
const cfg = (0, config_1.readConfig)();
|
|
145
|
+
(0, config_1.writeConfig)({ ...cfg, plan: plan });
|
|
146
|
+
console.log(`✓ Config created → ${CONFIG_PATH}\n`);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
rl.close();
|
|
150
|
+
}
|
|
151
|
+
// Paso 5: instalar hooks
|
|
152
|
+
installHooks();
|
|
153
|
+
}
|
|
154
|
+
function showInstallStatus() {
|
|
155
|
+
const cfg = (0, config_1.readConfig)();
|
|
156
|
+
console.log('\n╔═══════════════════════════════════════════╗');
|
|
157
|
+
console.log('║ claudestat — Status ║');
|
|
158
|
+
console.log('╚═══════════════════════════════════════════╝\n');
|
|
159
|
+
console.log('✅ Already installed. Current config:\n');
|
|
160
|
+
console.log(` plan: ${cfg.plan ?? 'auto-detect'}`);
|
|
161
|
+
console.log(` killSwitchEnabled: ${cfg.killSwitchEnabled}`);
|
|
162
|
+
console.log(` killSwitchThreshold: ${cfg.killSwitchThreshold}%`);
|
|
163
|
+
// Verificar si los hooks están en settings.json
|
|
164
|
+
let hooksOk = false;
|
|
165
|
+
try {
|
|
166
|
+
const settings = JSON.parse(fs_1.default.readFileSync(CLAUDE_SETTINGS, 'utf8'));
|
|
167
|
+
hooksOk = Object.values(settings.hooks ?? {})
|
|
168
|
+
.flat()
|
|
169
|
+
.some(hasClaudestatHook);
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
console.log(`\n hooks in settings.json: ${hooksOk ? '✅ installed' : '❌ not found'}`);
|
|
173
|
+
if (!hooksOk) {
|
|
174
|
+
console.log('\n Run `claudestat install` again to reinstall hooks.\n');
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log('');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function uninstallHooks() {
|
|
181
|
+
let settings = {};
|
|
182
|
+
try {
|
|
183
|
+
settings = JSON.parse(fs_1.default.readFileSync(CLAUDE_SETTINGS, 'utf8'));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
console.error('Could not read settings.json');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (!settings.hooks) {
|
|
190
|
+
console.log('No hooks installed.');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let removed = 0;
|
|
194
|
+
for (const hookType of Object.keys(settings.hooks)) {
|
|
195
|
+
const before = settings.hooks[hookType].length;
|
|
196
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter((entry) => !hasClaudestatHook(entry));
|
|
197
|
+
removed += before - settings.hooks[hookType].length;
|
|
198
|
+
}
|
|
199
|
+
fs_1.default.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
|
|
200
|
+
console.log(`✅ ${removed} claudestat hooks removed.`);
|
|
201
|
+
console.log(' Restart Claude Code for changes to take effect.\n');
|
|
202
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* intelligence.ts — Detección de loops y scoring de eficiencia
|
|
3
|
+
*
|
|
4
|
+
* Módulo puro: solo recibe datos y retorna análisis.
|
|
5
|
+
* Sin efectos secundarios, sin acceso a DB.
|
|
6
|
+
* Esto lo hace fácil de testear y reutilizar.
|
|
7
|
+
*/
|
|
8
|
+
import type { EventRow } from './db';
|
|
9
|
+
export interface LoopAlert {
|
|
10
|
+
toolName: string;
|
|
11
|
+
count: number;
|
|
12
|
+
windowMs: number;
|
|
13
|
+
ts: number;
|
|
14
|
+
}
|
|
15
|
+
export interface IntelligenceReport {
|
|
16
|
+
loops: LoopAlert[];
|
|
17
|
+
efficiencyScore: number;
|
|
18
|
+
summary: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Detecta loops: cuando el mismo tool se llama ≥ LOOP_THRESHOLD veces
|
|
22
|
+
* dentro de LOOP_WINDOW_MS. Evita alertas duplicadas con LOOP_COOLDOWN_MS.
|
|
23
|
+
*
|
|
24
|
+
* Algoritmo: ventana deslizante sobre eventos ordenados por timestamp.
|
|
25
|
+
*/
|
|
26
|
+
export declare function detectLoops(events: EventRow[]): LoopAlert[];
|
|
27
|
+
/**
|
|
28
|
+
* Calcula un score de 0-100 basado en:
|
|
29
|
+
* - Loops detectados → -10 por loop, cap -25
|
|
30
|
+
* - Tool calls excesivos → -5 por cada 50 calls sobre el umbral (150), cap -20
|
|
31
|
+
* - Coste alto → -5 si >$2, -10 si >$10, -20 si >$30
|
|
32
|
+
*
|
|
33
|
+
* Principio: una sesión de coding larga y productiva (88-200 tools) NO debería
|
|
34
|
+
* llegar a 0. El score 0 se reserva para sesiones con loops masivos + coste alto.
|
|
35
|
+
*
|
|
36
|
+
* Ejemplos calibrados:
|
|
37
|
+
* 88 tools, 5 loops, $6.49 → 100 - 25 - 0 - 5 = 70
|
|
38
|
+
* 236 tools, 2 loops, $25.34 → 100 - 20 - 9 - 10 = 61
|
|
39
|
+
* 20 tools, 0 loops, $0.30 → 100 - 0 - 0 - 0 = 100
|
|
40
|
+
*/
|
|
41
|
+
export declare function calcEfficiencyScore(events: EventRow[], loops: LoopAlert[], costUsd: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Genera el reporte completo de inteligencia para una sesión.
|
|
44
|
+
*/
|
|
45
|
+
export declare function analyzeSession(events: EventRow[], costUsd: number): IntelligenceReport;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* intelligence.ts — Detección de loops y scoring de eficiencia
|
|
4
|
+
*
|
|
5
|
+
* Módulo puro: solo recibe datos y retorna análisis.
|
|
6
|
+
* Sin efectos secundarios, sin acceso a DB.
|
|
7
|
+
* Esto lo hace fácil de testear y reutilizar.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.detectLoops = detectLoops;
|
|
11
|
+
exports.calcEfficiencyScore = calcEfficiencyScore;
|
|
12
|
+
exports.analyzeSession = analyzeSession;
|
|
13
|
+
// ─── Detección de loops ───────────────────────────────────────────────────────
|
|
14
|
+
const LOOP_THRESHOLD = 8; // calls para considerar loop
|
|
15
|
+
const LOOP_WINDOW_MS = 120000; // ventana de tiempo: 2 minutos (antes 60s → demasiados falsos positivos en coding)
|
|
16
|
+
const LOOP_COOLDOWN_MS = 120000; // cooldown entre alertas del mismo tool: 2 min (antes 15s → re-alertaba constantemente)
|
|
17
|
+
/**
|
|
18
|
+
* Detecta loops: cuando el mismo tool se llama ≥ LOOP_THRESHOLD veces
|
|
19
|
+
* dentro de LOOP_WINDOW_MS. Evita alertas duplicadas con LOOP_COOLDOWN_MS.
|
|
20
|
+
*
|
|
21
|
+
* Algoritmo: ventana deslizante sobre eventos ordenados por timestamp.
|
|
22
|
+
*/
|
|
23
|
+
function detectLoops(events) {
|
|
24
|
+
const alerts = [];
|
|
25
|
+
const windowsByTool = new Map(); // toolName → timestamps en ventana
|
|
26
|
+
for (const ev of events) {
|
|
27
|
+
// Solo contamos tool calls (PreToolUse o Done — uno de los dos)
|
|
28
|
+
if (ev.type !== 'Done' && ev.type !== 'PreToolUse')
|
|
29
|
+
continue;
|
|
30
|
+
if (!ev.tool_name)
|
|
31
|
+
continue;
|
|
32
|
+
const toolName = ev.tool_name;
|
|
33
|
+
const ts = ev.ts;
|
|
34
|
+
// Mantener solo los timestamps dentro de la ventana deslizante
|
|
35
|
+
const window = (windowsByTool.get(toolName) || []).filter(t => t >= ts - LOOP_WINDOW_MS);
|
|
36
|
+
window.push(ts);
|
|
37
|
+
windowsByTool.set(toolName, window);
|
|
38
|
+
if (window.length >= LOOP_THRESHOLD) {
|
|
39
|
+
// Verificar cooldown: no alertar si ya alertamos recientemente para este tool
|
|
40
|
+
const lastAlert = [...alerts].reverse().find(a => a.toolName === toolName);
|
|
41
|
+
if (!lastAlert || ts - lastAlert.ts >= LOOP_COOLDOWN_MS) {
|
|
42
|
+
alerts.push({ toolName, count: window.length, windowMs: LOOP_WINDOW_MS, ts });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return alerts;
|
|
47
|
+
}
|
|
48
|
+
// ─── Scoring de eficiencia ────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Calcula un score de 0-100 basado en:
|
|
51
|
+
* - Loops detectados → -10 por loop, cap -25
|
|
52
|
+
* - Tool calls excesivos → -5 por cada 50 calls sobre el umbral (150), cap -20
|
|
53
|
+
* - Coste alto → -5 si >$2, -10 si >$10, -20 si >$30
|
|
54
|
+
*
|
|
55
|
+
* Principio: una sesión de coding larga y productiva (88-200 tools) NO debería
|
|
56
|
+
* llegar a 0. El score 0 se reserva para sesiones con loops masivos + coste alto.
|
|
57
|
+
*
|
|
58
|
+
* Ejemplos calibrados:
|
|
59
|
+
* 88 tools, 5 loops, $6.49 → 100 - 25 - 0 - 5 = 70
|
|
60
|
+
* 236 tools, 2 loops, $25.34 → 100 - 20 - 9 - 10 = 61
|
|
61
|
+
* 20 tools, 0 loops, $0.30 → 100 - 0 - 0 - 0 = 100
|
|
62
|
+
*/
|
|
63
|
+
function calcEfficiencyScore(events, loops, costUsd) {
|
|
64
|
+
let score = 100;
|
|
65
|
+
// Loops: señal fuerte de ineficiencia, pero no colapsa el score
|
|
66
|
+
score -= Math.min(loops.length * 10, 25);
|
|
67
|
+
// Tool calls: solo penalizar si supera un umbral alto (sesiones muy largas)
|
|
68
|
+
const toolCallCount = events.filter(e => e.type === 'Done').length;
|
|
69
|
+
if (toolCallCount > 150) {
|
|
70
|
+
score -= Math.min(Math.floor((toolCallCount - 150) / 50) * 5, 20);
|
|
71
|
+
}
|
|
72
|
+
// Coste: escala progresiva, no binaria
|
|
73
|
+
if (costUsd > 30)
|
|
74
|
+
score -= 20;
|
|
75
|
+
else if (costUsd > 10)
|
|
76
|
+
score -= 10;
|
|
77
|
+
else if (costUsd > 2)
|
|
78
|
+
score -= 5;
|
|
79
|
+
return Math.max(0, Math.min(100, score));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Genera el reporte completo de inteligencia para una sesión.
|
|
83
|
+
*/
|
|
84
|
+
function analyzeSession(events, costUsd) {
|
|
85
|
+
const loops = detectLoops(events);
|
|
86
|
+
const efficiencyScore = calcEfficiencyScore(events, loops, costUsd);
|
|
87
|
+
const summary = buildSummary(loops, efficiencyScore, costUsd, events);
|
|
88
|
+
return { loops, efficiencyScore, summary };
|
|
89
|
+
}
|
|
90
|
+
function buildSummary(loops, score, costUsd, events) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (loops.length > 0) {
|
|
93
|
+
const loopDesc = loops.map(l => `${l.toolName} x${l.count}`).join(', ');
|
|
94
|
+
parts.push(`⚠️ Loop detected: ${loopDesc}`);
|
|
95
|
+
}
|
|
96
|
+
if (score >= 90)
|
|
97
|
+
parts.push('✅ Efficient session');
|
|
98
|
+
else if (score >= 70)
|
|
99
|
+
parts.push('⚡ Medium efficiency');
|
|
100
|
+
else
|
|
101
|
+
parts.push('🔴 Inefficient session');
|
|
102
|
+
const toolCalls = events.filter(e => e.type === 'Done').length;
|
|
103
|
+
parts.push(`${toolCalls} tool calls · $${costUsd.toFixed(4)}`);
|
|
104
|
+
return parts.join(' · ');
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* meta-stats.ts — Context overhead KPIs
|
|
3
|
+
*
|
|
4
|
+
* Detects files that Claude Code loads into context automatically
|
|
5
|
+
* (CLAUDE.md, settings, AGENTS.md, etc.) and estimates their token cost.
|
|
6
|
+
* Keeps a rolling history for sparklines (last 30 snapshots).
|
|
7
|
+
*/
|
|
8
|
+
export interface MetaAlert {
|
|
9
|
+
level: 'info' | 'warning' | 'critical';
|
|
10
|
+
message: string;
|
|
11
|
+
metric: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ContextFileInfo {
|
|
14
|
+
label: string;
|
|
15
|
+
tokens: number;
|
|
16
|
+
}
|
|
17
|
+
export interface MetaStats {
|
|
18
|
+
ts: number;
|
|
19
|
+
contextFiles: ContextFileInfo[];
|
|
20
|
+
contextOverheadTokens: number;
|
|
21
|
+
alerts: MetaAlert[];
|
|
22
|
+
}
|
|
23
|
+
export interface MetaSnapshot {
|
|
24
|
+
ts: number;
|
|
25
|
+
contextOverheadTokens: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function computeMetaStats(sessionCwd?: string, contextPct?: number): MetaStats;
|
|
28
|
+
export declare function getMetaHistory(): MetaSnapshot[];
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* meta-stats.ts — Context overhead KPIs
|
|
4
|
+
*
|
|
5
|
+
* Detects files that Claude Code loads into context automatically
|
|
6
|
+
* (CLAUDE.md, settings, AGENTS.md, etc.) and estimates their token cost.
|
|
7
|
+
* Keeps a rolling history for sparklines (last 30 snapshots).
|
|
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.computeMetaStats = computeMetaStats;
|
|
14
|
+
exports.getMetaHistory = getMetaHistory;
|
|
15
|
+
const fs_1 = __importDefault(require("fs"));
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const paths_1 = require("./paths");
|
|
19
|
+
// ─── In-memory history ────────────────────────────────────────────────────────
|
|
20
|
+
const MAX_HISTORY = 30;
|
|
21
|
+
const history = [];
|
|
22
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Decodes the real cwd from Claude Code's internal project path format.
|
|
25
|
+
*
|
|
26
|
+
* Claude Code stores transcript paths like:
|
|
27
|
+
* /Users/db/.claude/projects/-Users-db-Documents-GitHub-myproject
|
|
28
|
+
*
|
|
29
|
+
* Where the suffix is the real cwd with each '/' replaced by '-'.
|
|
30
|
+
*/
|
|
31
|
+
function resolveProjectCwd(storedCwd) {
|
|
32
|
+
const homeDir = os_1.default.homedir();
|
|
33
|
+
const projectsDir = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
|
|
34
|
+
if (!storedCwd.startsWith(projectsDir + path_1.default.sep))
|
|
35
|
+
return storedCwd;
|
|
36
|
+
const encodedPath = storedCwd.slice(projectsDir.length + 1);
|
|
37
|
+
const encodedHome = (0, paths_1.encodeClaudePath)(homeDir);
|
|
38
|
+
if (encodedPath.startsWith(encodedHome)) {
|
|
39
|
+
const rest = encodedPath.slice(encodedHome.length);
|
|
40
|
+
return homeDir + rest.replace(/-/g, path_1.default.sep);
|
|
41
|
+
}
|
|
42
|
+
return path_1.default.sep + encodedPath.replace(/-/g, path_1.default.sep);
|
|
43
|
+
}
|
|
44
|
+
function estimateTokens(text) {
|
|
45
|
+
return Math.ceil(text.length / 4);
|
|
46
|
+
}
|
|
47
|
+
function readFile(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
return fs_1.default.readFileSync(filePath, 'utf8');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function fmtTok(n) {
|
|
56
|
+
if (n >= 1000000)
|
|
57
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
58
|
+
if (n >= 1000)
|
|
59
|
+
return `${Math.round(n / 1000)}K`;
|
|
60
|
+
return String(n);
|
|
61
|
+
}
|
|
62
|
+
// ─── Context file candidates ──────────────────────────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Files that Claude Code automatically loads into context.
|
|
65
|
+
* Each candidate is resolved to a full path; non-existent files are skipped.
|
|
66
|
+
*
|
|
67
|
+
* Universal files (always checked):
|
|
68
|
+
* - ~/.claude/CLAUDE.md — global instructions for all projects
|
|
69
|
+
* - ~/.claude/settings.json — global config (hooks, permissions, model)
|
|
70
|
+
* - ~/.claude/settings.local.json — personal overrides
|
|
71
|
+
*
|
|
72
|
+
* Project-level files (checked if a project cwd is known):
|
|
73
|
+
* - {project}/CLAUDE.md — project-specific instructions
|
|
74
|
+
* - {project}/AGENTS.md — instructions for agent mode
|
|
75
|
+
* - {project}/.claude/CLAUDE.md — alternative project-level location
|
|
76
|
+
*/
|
|
77
|
+
function resolveContextCandidates(homeDir, projectCwd) {
|
|
78
|
+
const claudeDir = (0, paths_1.getClaudeDir)();
|
|
79
|
+
const candidates = [
|
|
80
|
+
{ label: 'CLAUDE.md (global)', filePath: path_1.default.join(claudeDir, 'CLAUDE.md') },
|
|
81
|
+
{ label: 'settings.json', filePath: path_1.default.join(claudeDir, 'settings.json') },
|
|
82
|
+
{ label: 'settings.local.json', filePath: path_1.default.join(claudeDir, 'settings.local.json') },
|
|
83
|
+
];
|
|
84
|
+
if (projectCwd) {
|
|
85
|
+
candidates.push({ label: 'CLAUDE.md (proyecto)', filePath: path_1.default.join(projectCwd, 'CLAUDE.md') }, { label: 'AGENTS.md', filePath: path_1.default.join(projectCwd, 'AGENTS.md') }, { label: '.claude/CLAUDE.md', filePath: path_1.default.join(projectCwd, '.claude', 'CLAUDE.md') });
|
|
86
|
+
}
|
|
87
|
+
return candidates;
|
|
88
|
+
}
|
|
89
|
+
// ─── Compute ──────────────────────────────────────────────────────────────────
|
|
90
|
+
function computeMetaStats(sessionCwd, contextPct) {
|
|
91
|
+
const ts = Date.now();
|
|
92
|
+
const homeDir = os_1.default.homedir();
|
|
93
|
+
const projectCwd = sessionCwd ? resolveProjectCwd(sessionCwd) : undefined;
|
|
94
|
+
// ── Detect context files ───────────────────────────────────────────────────
|
|
95
|
+
const contextFiles = [];
|
|
96
|
+
let contextOverheadTokens = 0;
|
|
97
|
+
for (const { label, filePath } of resolveContextCandidates(homeDir, projectCwd)) {
|
|
98
|
+
const content = readFile(filePath);
|
|
99
|
+
if (!content)
|
|
100
|
+
continue;
|
|
101
|
+
const tokens = estimateTokens(content);
|
|
102
|
+
contextFiles.push({ label, tokens });
|
|
103
|
+
contextOverheadTokens += tokens;
|
|
104
|
+
}
|
|
105
|
+
// ── Alerts ────────────────────────────────────────────────────────────────
|
|
106
|
+
const alerts = [];
|
|
107
|
+
if (contextOverheadTokens > 20000) {
|
|
108
|
+
alerts.push({
|
|
109
|
+
level: 'critical',
|
|
110
|
+
message: `Overhead de contexto muy alto — ${fmtTok(contextOverheadTokens)} tokens en ${contextFiles.length} archivos`,
|
|
111
|
+
metric: 'context_files',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else if (contextOverheadTokens > 10000) {
|
|
115
|
+
alerts.push({
|
|
116
|
+
level: 'warning',
|
|
117
|
+
message: `Overhead de contexto alto — ${fmtTok(contextOverheadTokens)} tokens en ${contextFiles.length} archivos`,
|
|
118
|
+
metric: 'context_files',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (contextPct !== undefined) {
|
|
122
|
+
if (contextPct > 85) {
|
|
123
|
+
alerts.push({ level: 'critical', message: `Auto-compact muy pronto — aprox. ${100 - contextPct}% libre`, metric: 'context' });
|
|
124
|
+
}
|
|
125
|
+
else if (contextPct > 65) {
|
|
126
|
+
alerts.push({ level: 'warning', message: `Contexto aproximado al ${contextPct}%`, metric: 'context' });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ── History ───────────────────────────────────────────────────────────────
|
|
130
|
+
history.push({ ts, contextOverheadTokens });
|
|
131
|
+
if (history.length > MAX_HISTORY)
|
|
132
|
+
history.shift();
|
|
133
|
+
return { ts, contextFiles, contextOverheadTokens, alerts };
|
|
134
|
+
}
|
|
135
|
+
function getMetaHistory() {
|
|
136
|
+
return [...history];
|
|
137
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ─── Rate limiter simple para POST /event ────────────────────────────────────
|
|
3
|
+
// Protege contra flood local. Límite: 120 requests/min por IP.
|
|
4
|
+
// Usa ventana fija de 60s para simplicidad (sin dependencias externas).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isRateLimited = isRateLimited;
|
|
7
|
+
exports.stopRateLimiter = stopRateLimiter;
|
|
8
|
+
const rateLimitMap = new Map();
|
|
9
|
+
const RATE_LIMIT_MAX = 120;
|
|
10
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
11
|
+
function isRateLimited(ip) {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
const entry = rateLimitMap.get(ip);
|
|
14
|
+
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
15
|
+
rateLimitMap.set(ip, { count: 1, windowStart: now });
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
entry.count++;
|
|
19
|
+
return entry.count > RATE_LIMIT_MAX;
|
|
20
|
+
}
|
|
21
|
+
// Limpiar entradas expiradas cada 5 minutos para no acumular IPs inactivas
|
|
22
|
+
const rateLimitCleanupInterval = setInterval(() => {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
rateLimitMap.forEach((v, k) => { if (now - v.windowStart > RATE_LIMIT_WINDOW_MS)
|
|
25
|
+
rateLimitMap.delete(k); });
|
|
26
|
+
}, 5 * 60000);
|
|
27
|
+
function stopRateLimiter() {
|
|
28
|
+
clearInterval(rateLimitCleanupInterval);
|
|
29
|
+
rateLimitMap.clear();
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sendDesktopNotification(title: string, body: string): void;
|
package/dist/notifier.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
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.sendDesktopNotification = sendDesktopNotification;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
function sendDesktopNotification(title, body) {
|
|
10
|
+
try {
|
|
11
|
+
if (os_1.default.platform() === 'darwin') {
|
|
12
|
+
const escaped = body.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
13
|
+
(0, child_process_1.execSync)(`osascript -e 'display notification "${escaped}" with title "${title}"'`, { stdio: 'ignore' });
|
|
14
|
+
}
|
|
15
|
+
else if (os_1.default.platform() === 'linux') {
|
|
16
|
+
(0, child_process_1.execSync)(`notify-send "${title}" "${body}"`, { stdio: 'ignore' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// notification not available — already logged to console
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paths.ts — Cross-platform path resolution for Claude Code data directories
|
|
3
|
+
*
|
|
4
|
+
* Claude Code stores data in the same location on all platforms:
|
|
5
|
+
* All platforms: ~/.claude/
|
|
6
|
+
*
|
|
7
|
+
* ClaudeStat stores its own data in:
|
|
8
|
+
* All platforms: ~/.claudestat/ (or CLAUDESTAT_DATA_DIR env var)
|
|
9
|
+
*
|
|
10
|
+
* Claude Code encodes project paths by replacing path separators AND colons with '-'.
|
|
11
|
+
* This module provides helpers to encode/decode those paths cross-platform.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Returns the Claude Code data directory (~/.claude on all platforms).
|
|
15
|
+
* Empirically verified: Claude Code CLI stores settings at ~/.claude on macOS, Linux, and Windows.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getClaudeDir(): string;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the ClaudeStat data directory.
|
|
20
|
+
* Can be overridden via CLAUDESTAT_DATA_DIR env var.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getClaudestatDir(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Returns the PID file path for the daemon.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getPidFile(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Encodes a real filesystem path into Claude Code's internal format.
|
|
29
|
+
* Claude Code replaces path separators AND colons with '-'.
|
|
30
|
+
*
|
|
31
|
+
* macOS: /Users/db/Documents/GitHub → -Users-db-Documents-GitHub
|
|
32
|
+
* Windows: C:\Users\db\Documents → C--Users-db-Documents
|
|
33
|
+
*/
|
|
34
|
+
export declare function encodeClaudePath(realPath: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Decodes a Claude Code encoded directory name back to a real filesystem path.
|
|
37
|
+
*
|
|
38
|
+
* Since directory names with '-' are ambiguous (is "gmail-ai-agent" one dir or three?),
|
|
39
|
+
* this function requires a reference to the greedy filesystem resolver.
|
|
40
|
+
* Use project-scanner's findRealPath for the actual resolution.
|
|
41
|
+
*
|
|
42
|
+
* Returns the encodedHome + rest with '-' replaced by path.sep,
|
|
43
|
+
* but actual resolution should be done via findRealPath in project-scanner.
|
|
44
|
+
*/
|
|
45
|
+
export declare function decodeClaudePath(encoded: string): string | null;
|
|
46
|
+
/**
|
|
47
|
+
* Creates the regex pattern for the encoded home path.
|
|
48
|
+
* Handles both / and \ separators.
|
|
49
|
+
*/
|
|
50
|
+
export declare function homeSlugRegex(): RegExp;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the engram-compatible slug for the home directory.
|
|
53
|
+
* Used for MEMORY.md path resolution.
|
|
54
|
+
* macOS: /Users/db → -Users-db
|
|
55
|
+
* Windows: C:\Users\db → C--Users-db
|
|
56
|
+
*/
|
|
57
|
+
export declare function getHomeSlug(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the appropriate command to find an executable in PATH.
|
|
60
|
+
* Unix: which <name>
|
|
61
|
+
* Windows: where <name>
|
|
62
|
+
*/
|
|
63
|
+
export declare function whichCmd(name: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Returns the appropriate command to find all instances of an executable in PATH.
|
|
66
|
+
* Unix: which -a <name>
|
|
67
|
+
* Windows: where <name> (where already lists all matches)
|
|
68
|
+
*/
|
|
69
|
+
export declare function whichAllCmd(name: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Returns the appropriate command to check if a port is in use.
|
|
72
|
+
* Unix: lsof -i :<port>
|
|
73
|
+
* Windows: netstat -ano | findstr :<port>
|
|
74
|
+
*/
|
|
75
|
+
export declare function portCheckCmd(port: number): string;
|
|
76
|
+
/**
|
|
77
|
+
* Returns true if running on Windows.
|
|
78
|
+
*/
|
|
79
|
+
export declare const isWindows: boolean;
|