@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/export.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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.runExport = runExport;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const db_1 = require("./db");
|
|
11
|
+
function parseDate(str, endOfDay = false) {
|
|
12
|
+
const ms = Date.parse(str);
|
|
13
|
+
if (isNaN(ms))
|
|
14
|
+
throw new Error(`Invalid date: "${str}" — expected YYYY-MM-DD`);
|
|
15
|
+
return endOfDay ? ms + 86399999 : ms;
|
|
16
|
+
}
|
|
17
|
+
function csvField(value) {
|
|
18
|
+
const s = value == null ? '' : String(value);
|
|
19
|
+
return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
20
|
+
}
|
|
21
|
+
const CSV_HEADERS = [
|
|
22
|
+
'id', 'started_at', 'cwd', 'project_path',
|
|
23
|
+
'total_cost_usd', 'total_input_tokens', 'total_output_tokens',
|
|
24
|
+
'efficiency_score', 'loops_detected',
|
|
25
|
+
];
|
|
26
|
+
function toRow(s) {
|
|
27
|
+
return {
|
|
28
|
+
id: s.id,
|
|
29
|
+
started_at: new Date(s.started_at).toISOString(),
|
|
30
|
+
cwd: s.cwd ?? '',
|
|
31
|
+
project_path: s.project_path ?? '',
|
|
32
|
+
total_cost_usd: s.total_cost_usd ?? 0,
|
|
33
|
+
total_input_tokens: s.total_input_tokens ?? 0,
|
|
34
|
+
total_output_tokens: s.total_output_tokens ?? 0,
|
|
35
|
+
efficiency_score: s.efficiency_score ?? 100,
|
|
36
|
+
loops_detected: s.loops_detected ?? 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function runExport(opts) {
|
|
40
|
+
let fromMs;
|
|
41
|
+
let toMs;
|
|
42
|
+
try {
|
|
43
|
+
if (opts.from)
|
|
44
|
+
fromMs = parseDate(opts.from);
|
|
45
|
+
if (opts.to)
|
|
46
|
+
toMs = parseDate(opts.to, true);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(`Error: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const sessions = db_1.dbOps.getAllSessions().filter(s => {
|
|
53
|
+
if (fromMs !== undefined && s.started_at < fromMs)
|
|
54
|
+
return false;
|
|
55
|
+
if (toMs !== undefined && s.started_at > toMs)
|
|
56
|
+
return false;
|
|
57
|
+
if (opts.project) {
|
|
58
|
+
if (!s.project_path)
|
|
59
|
+
return false;
|
|
60
|
+
if (!s.project_path.toLowerCase().includes(opts.project.toLowerCase()))
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
});
|
|
65
|
+
const rows = sessions.map(toRow);
|
|
66
|
+
let output;
|
|
67
|
+
if (opts.format === 'csv') {
|
|
68
|
+
const lines = [
|
|
69
|
+
CSV_HEADERS.join(','),
|
|
70
|
+
...rows.map(r => CSV_HEADERS.map(h => csvField(r[h])).join(',')),
|
|
71
|
+
];
|
|
72
|
+
output = lines.join('\n') + '\n';
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
output = JSON.stringify(rows, null, 2) + '\n';
|
|
76
|
+
}
|
|
77
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
78
|
+
const dest = path_1.default.resolve(opts.output ?? path_1.default.join(os_1.default.homedir(), 'Downloads', `claudestat-export-${date}.${opts.format}`));
|
|
79
|
+
fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
|
|
80
|
+
fs_1.default.writeFileSync(dest, output);
|
|
81
|
+
console.log(`✓ Exported ${rows.length} session(s) → ${dest}`);
|
|
82
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git.ts — Información de git por proyecto (branch, dirty, ahead/behind)
|
|
3
|
+
*
|
|
4
|
+
* Diseño: módulo puro, síncrono, sin efectos secundarios.
|
|
5
|
+
* Usa child_process.execSync con timeout corto (3s) y stderr silenciado.
|
|
6
|
+
* Retorna null si el directorio no es un repo git o git no está disponible.
|
|
7
|
+
*
|
|
8
|
+
* Por qué síncrono: las llamadas son rápidas (<50ms en repos locales) y
|
|
9
|
+
* se invoca solo bajo demanda, no en el hot path de SSE.
|
|
10
|
+
*/
|
|
11
|
+
export interface GitInfo {
|
|
12
|
+
branch: string;
|
|
13
|
+
dirty: boolean;
|
|
14
|
+
ahead: number;
|
|
15
|
+
behind: number;
|
|
16
|
+
hasRemote: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Obtiene información de git para un directorio de proyecto.
|
|
20
|
+
* Retorna null si no es un repo git, git no está en PATH, o hay error.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getGitInfo(projectPath: string): GitInfo | null;
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* git.ts — Información de git por proyecto (branch, dirty, ahead/behind)
|
|
4
|
+
*
|
|
5
|
+
* Diseño: módulo puro, síncrono, sin efectos secundarios.
|
|
6
|
+
* Usa child_process.execSync con timeout corto (3s) y stderr silenciado.
|
|
7
|
+
* Retorna null si el directorio no es un repo git o git no está disponible.
|
|
8
|
+
*
|
|
9
|
+
* Por qué síncrono: las llamadas son rápidas (<50ms en repos locales) y
|
|
10
|
+
* se invoca solo bajo demanda, no en el hot path de SSE.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.getGitInfo = getGitInfo;
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const EXEC_OPTS = (cwd) => ({
|
|
16
|
+
cwd,
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 3000,
|
|
19
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Obtiene información de git para un directorio de proyecto.
|
|
23
|
+
* Retorna null si no es un repo git, git no está en PATH, o hay error.
|
|
24
|
+
*/
|
|
25
|
+
function getGitInfo(projectPath) {
|
|
26
|
+
try {
|
|
27
|
+
// ─ Branch ─
|
|
28
|
+
const branchRaw = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', EXEC_OPTS(projectPath)).trim();
|
|
29
|
+
let branch;
|
|
30
|
+
if (!branchRaw || branchRaw === 'HEAD') {
|
|
31
|
+
// Detached HEAD — mostrar SHA corto
|
|
32
|
+
const sha = (0, child_process_1.execSync)('git rev-parse --short HEAD', EXEC_OPTS(projectPath)).trim();
|
|
33
|
+
return { branch: `(${sha})`, dirty: false, ahead: 0, behind: 0, hasRemote: false };
|
|
34
|
+
}
|
|
35
|
+
branch = branchRaw;
|
|
36
|
+
// ─ Dirty status ─
|
|
37
|
+
const statusOut = (0, child_process_1.execSync)('git status --porcelain', EXEC_OPTS(projectPath)).trim();
|
|
38
|
+
const dirty = statusOut.length > 0;
|
|
39
|
+
// ─ Ahead / behind respecto al upstream ─
|
|
40
|
+
let ahead = 0, behind = 0, hasRemote = false;
|
|
41
|
+
try {
|
|
42
|
+
// --left-right: <upstream>...HEAD → "behind\tahead"
|
|
43
|
+
const ab = (0, child_process_1.execSync)('git rev-list --left-right --count @{upstream}...HEAD', EXEC_OPTS(projectPath)).trim();
|
|
44
|
+
const parts = ab.split(/\s+/);
|
|
45
|
+
behind = parseInt(parts[0] ?? '0', 10) || 0;
|
|
46
|
+
ahead = parseInt(parts[1] ?? '0', 10) || 0;
|
|
47
|
+
hasRemote = true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// No hay upstream configurado — común en branches nuevos
|
|
51
|
+
}
|
|
52
|
+
return { branch, dirty, ahead, behind, hasRemote };
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null; // no es un repo git o git no disponible
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* github.ts — Estado de PR y CI vía GitHub CLI (`gh`)
|
|
3
|
+
*
|
|
4
|
+
* Usa `gh pr view` para obtener PR asociado al branch actual.
|
|
5
|
+
* Retorna null si no hay PR abierto, gh no está disponible, o no es GitHub.
|
|
6
|
+
*
|
|
7
|
+
* Por qué gh CLI y no la API directamente:
|
|
8
|
+
* - gh maneja autenticación automáticamente
|
|
9
|
+
* - Sin necesidad de tokens adicionales
|
|
10
|
+
* - Ya disponible en la mayoría de entornos de desarrollo
|
|
11
|
+
*
|
|
12
|
+
* Nota: `gh pr view` hace una llamada de red → puede ser lento (~500ms-2s).
|
|
13
|
+
* El daemon cachea el resultado 5 minutos por proyecto.
|
|
14
|
+
*/
|
|
15
|
+
export interface PRStatus {
|
|
16
|
+
number: number;
|
|
17
|
+
title: string;
|
|
18
|
+
state: 'OPEN' | 'CLOSED' | 'MERGED';
|
|
19
|
+
url: string;
|
|
20
|
+
branch: string;
|
|
21
|
+
ciState: 'SUCCESS' | 'FAILURE' | 'PENDING' | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Obtiene el estado del PR del branch actual en el repo de GitHub.
|
|
25
|
+
* Retorna null si no hay PR, gh no está disponible, o no es un repo GitHub.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getPRStatus(projectPath: string): PRStatus | null;
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* github.ts — Estado de PR y CI vía GitHub CLI (`gh`)
|
|
4
|
+
*
|
|
5
|
+
* Usa `gh pr view` para obtener PR asociado al branch actual.
|
|
6
|
+
* Retorna null si no hay PR abierto, gh no está disponible, o no es GitHub.
|
|
7
|
+
*
|
|
8
|
+
* Por qué gh CLI y no la API directamente:
|
|
9
|
+
* - gh maneja autenticación automáticamente
|
|
10
|
+
* - Sin necesidad de tokens adicionales
|
|
11
|
+
* - Ya disponible en la mayoría de entornos de desarrollo
|
|
12
|
+
*
|
|
13
|
+
* Nota: `gh pr view` hace una llamada de red → puede ser lento (~500ms-2s).
|
|
14
|
+
* El daemon cachea el resultado 5 minutos por proyecto.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.getPRStatus = getPRStatus;
|
|
18
|
+
const child_process_1 = require("child_process");
|
|
19
|
+
const EXEC_OPTS = (cwd) => ({
|
|
20
|
+
cwd,
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
timeout: 8000,
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Obtiene el estado del PR del branch actual en el repo de GitHub.
|
|
27
|
+
* Retorna null si no hay PR, gh no está disponible, o no es un repo GitHub.
|
|
28
|
+
*/
|
|
29
|
+
function getPRStatus(projectPath) {
|
|
30
|
+
try {
|
|
31
|
+
const out = (0, child_process_1.execSync)('gh pr view --json number,title,state,url,headRefName,statusCheckRollup', EXEC_OPTS(projectPath));
|
|
32
|
+
const pr = JSON.parse(out);
|
|
33
|
+
// ─ Determinar estado de CI desde statusCheckRollup ─
|
|
34
|
+
// Cada check puede tener `state` (PENDING/SUCCESS/FAILURE/ERROR) o
|
|
35
|
+
// `conclusion` (success/failure/cancelled/skipped) según el tipo de check.
|
|
36
|
+
const checks = pr.statusCheckRollup ?? [];
|
|
37
|
+
let ciState = checks.length > 0 ? 'PENDING' : null;
|
|
38
|
+
if (checks.length > 0) {
|
|
39
|
+
const statuses = checks.map((c) => (c.state ?? c.conclusion ?? '').toUpperCase());
|
|
40
|
+
if (statuses.some(s => s === 'FAILURE' || s === 'ERROR' || s === 'CANCELLED')) {
|
|
41
|
+
ciState = 'FAILURE';
|
|
42
|
+
}
|
|
43
|
+
else if (statuses.every(s => s === 'SUCCESS' || s === 'SKIPPED')) {
|
|
44
|
+
ciState = 'SUCCESS';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
ciState = 'PENDING';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
number: pr.number,
|
|
52
|
+
title: pr.title,
|
|
53
|
+
state: pr.state,
|
|
54
|
+
url: pr.url,
|
|
55
|
+
branch: pr.headRefName,
|
|
56
|
+
ciState,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null; // no hay PR, gh no disponible, o no es GitHub
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* index.ts — Entry point del CLI
|
|
5
|
+
*
|
|
6
|
+
* Suprimimos el ExperimentalWarning de node:sqlite antes de importar nada.
|
|
7
|
+
* El módulo funciona perfectamente — el warning es solo informativo.
|
|
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
|
+
// Filtrar solo el warning de SQLite, dejar pasar el resto
|
|
14
|
+
process.on('warning', (w) => {
|
|
15
|
+
if (w.name === 'ExperimentalWarning' && w.message.includes('SQLite'))
|
|
16
|
+
return;
|
|
17
|
+
process.stderr.write(`${w.name}: ${w.message}\n`);
|
|
18
|
+
});
|
|
19
|
+
const commander_1 = require("commander");
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
const child_process_1 = require("child_process");
|
|
23
|
+
const daemon_1 = require("./daemon");
|
|
24
|
+
const watchdog_1 = require("./watchdog");
|
|
25
|
+
const watch_1 = require("./watch");
|
|
26
|
+
const install_1 = require("./install");
|
|
27
|
+
const export_1 = require("./export");
|
|
28
|
+
const config_1 = require("./config");
|
|
29
|
+
const doctor_1 = require("./doctor");
|
|
30
|
+
const paths_1 = require("./paths");
|
|
31
|
+
const program = new commander_1.Command();
|
|
32
|
+
const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
33
|
+
const PID_FILE = (0, paths_1.getPidFile)();
|
|
34
|
+
function spawnDaemon() {
|
|
35
|
+
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], 'start'], {
|
|
36
|
+
detached: true,
|
|
37
|
+
stdio: 'ignore',
|
|
38
|
+
env: { ...process.env, CLAUDESTAT_DAEMON: '1' },
|
|
39
|
+
});
|
|
40
|
+
child.unref();
|
|
41
|
+
console.log(`✅ claudestat daemon started (pid ${child.pid})`);
|
|
42
|
+
console.log(` Dashboard → http://localhost:7337`);
|
|
43
|
+
}
|
|
44
|
+
function removePidFile() {
|
|
45
|
+
try {
|
|
46
|
+
fs_1.default.unlinkSync(PID_FILE);
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
}
|
|
50
|
+
async function stopDaemon() {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch('http://localhost:7337/shutdown', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
signal: AbortSignal.timeout(2000),
|
|
55
|
+
});
|
|
56
|
+
if (res.ok) {
|
|
57
|
+
console.log('✅ claudestat daemon stopped');
|
|
58
|
+
removePidFile();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
try {
|
|
64
|
+
const pid = parseInt(fs_1.default.readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
65
|
+
process.kill(pid, 'SIGTERM');
|
|
66
|
+
console.log(`✅ claudestat daemon stopped (pid ${pid})`);
|
|
67
|
+
removePidFile();
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
removePidFile();
|
|
71
|
+
if (e.code === 'ENOENT')
|
|
72
|
+
throw new Error('Daemon is not running (no PID file found)');
|
|
73
|
+
if (e.code === 'ESRCH')
|
|
74
|
+
throw new Error('Daemon process not found — stale PID file removed');
|
|
75
|
+
throw new Error(`Error stopping daemon: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Warn if the active binary is outside the current npm global prefix (NVM conflict)
|
|
79
|
+
if (process.env.NVM_DIR || process.env.NVM_HOME) {
|
|
80
|
+
try {
|
|
81
|
+
const npmPrefix = (0, child_process_1.execSync)('npm prefix -g', { stdio: 'pipe' }).toString().trim();
|
|
82
|
+
const runningFrom = process.argv[1];
|
|
83
|
+
if (runningFrom && !runningFrom.startsWith(npmPrefix)) {
|
|
84
|
+
const refreshCmd = paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat';
|
|
85
|
+
process.stderr.write(`\x1b[33m⚠️ claudestat is running from ${runningFrom}\x1b[0m\n` +
|
|
86
|
+
` This binary may not match the active Node version (${process.version}).\n` +
|
|
87
|
+
` Fix: \x1b[36mnvm use default && npm install -g @deibygs/claudestat\x1b[0m\n` +
|
|
88
|
+
` Then restart your terminal or run: \x1b[36m${refreshCmd}\x1b[0m\n\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
}
|
|
93
|
+
program
|
|
94
|
+
.name('claudestat')
|
|
95
|
+
.description('Real-time execution trace and cost intelligence for Claude Code')
|
|
96
|
+
.version(PKG_VERSION);
|
|
97
|
+
program
|
|
98
|
+
.command('start')
|
|
99
|
+
.description('Start the background daemon (receives Claude Code hook events)')
|
|
100
|
+
.option('--watchdog', 'Auto-restart daemon if it crashes')
|
|
101
|
+
.action((opts) => {
|
|
102
|
+
if (process.env.CLAUDESTAT_DAEMON) {
|
|
103
|
+
(0, daemon_1.startDaemon)();
|
|
104
|
+
if (opts.watchdog)
|
|
105
|
+
(0, watchdog_1.startWatchdog)();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
spawnDaemon();
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
program
|
|
113
|
+
.command('watch')
|
|
114
|
+
.description('Live terminal trace view')
|
|
115
|
+
.action(() => (0, watch_1.startWatch)().catch(err => {
|
|
116
|
+
console.error('\n❌ Error:', err.message);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}));
|
|
119
|
+
program
|
|
120
|
+
.command('install')
|
|
121
|
+
.description('Install hooks into Claude Code settings')
|
|
122
|
+
.action(install_1.runInstall);
|
|
123
|
+
program
|
|
124
|
+
.command('uninstall')
|
|
125
|
+
.description('Remove hooks from Claude Code')
|
|
126
|
+
.action(install_1.uninstallHooks);
|
|
127
|
+
program
|
|
128
|
+
.command('export [format]')
|
|
129
|
+
.description('Export session data (json | csv, default: json). Max 500 sessions.')
|
|
130
|
+
.option('--from <date>', 'Start date YYYY-MM-DD (inclusive)')
|
|
131
|
+
.option('--to <date>', 'End date YYYY-MM-DD (inclusive)')
|
|
132
|
+
.option('--project <name>', 'Filter by project path (case-insensitive substring)')
|
|
133
|
+
.option('--output <path>', 'Write to file instead of stdout')
|
|
134
|
+
.action((format, opts) => {
|
|
135
|
+
const fmt = (format ?? 'json').toLowerCase();
|
|
136
|
+
if (fmt !== 'json' && fmt !== 'csv') {
|
|
137
|
+
console.error('Error: format must be "json" or "csv"');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
(0, export_1.runExport)({ format: fmt, ...opts });
|
|
141
|
+
process.exit(0);
|
|
142
|
+
});
|
|
143
|
+
program
|
|
144
|
+
.command('status')
|
|
145
|
+
.description('Show current quota, cost and burn rate')
|
|
146
|
+
.option('--json', 'Output raw JSON instead of formatted text')
|
|
147
|
+
.action(async (opts) => {
|
|
148
|
+
try {
|
|
149
|
+
const [quotaRes, healthRes] = await Promise.all([
|
|
150
|
+
fetch('http://localhost:7337/quota'),
|
|
151
|
+
fetch('http://localhost:7337/health'),
|
|
152
|
+
]);
|
|
153
|
+
if (!quotaRes.ok)
|
|
154
|
+
throw new Error('Daemon unavailable');
|
|
155
|
+
const q = await quotaRes.json();
|
|
156
|
+
const _h = await healthRes.json().catch(() => ({}));
|
|
157
|
+
if (opts.json) {
|
|
158
|
+
console.log(JSON.stringify({
|
|
159
|
+
cyclePrompts: q.cyclePrompts,
|
|
160
|
+
cycleLimit: q.cycleLimit,
|
|
161
|
+
cyclePct: q.cyclePct,
|
|
162
|
+
cycleResetMs: q.cycleResetMs,
|
|
163
|
+
plan: q.detectedPlan,
|
|
164
|
+
weeklyHoursSonnet: q.weeklyHoursSonnet,
|
|
165
|
+
weeklyLimitSonnet: q.weeklyLimitSonnet,
|
|
166
|
+
weeklyHoursOpus: q.weeklyHoursOpus,
|
|
167
|
+
weeklyLimitOpus: q.weeklyLimitOpus,
|
|
168
|
+
burnRateTokensPerMin: q.burnRateTokensPerMin,
|
|
169
|
+
}));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const R = '\x1b[0m';
|
|
173
|
+
const pctColor = q.cyclePct >= 95 ? '\x1b[31m'
|
|
174
|
+
: q.cyclePct >= 85 ? '\x1b[33m'
|
|
175
|
+
: q.cyclePct >= 70 ? '\x1b[33m'
|
|
176
|
+
: '\x1b[32m';
|
|
177
|
+
const resetMin = Math.ceil(q.cycleResetMs / 60000);
|
|
178
|
+
const resetLabel = resetMin >= 60
|
|
179
|
+
? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
|
|
180
|
+
: `${resetMin}m`;
|
|
181
|
+
const burnLabel = q.burnRateTokensPerMin > 0
|
|
182
|
+
? ` │ 🔥 ${q.burnRateTokensPerMin.toLocaleString()} tok/min`
|
|
183
|
+
: '';
|
|
184
|
+
const burnRow = q.burnRateTokensPerMin > 0
|
|
185
|
+
? ` Burn rate ${q.burnRateTokensPerMin.toLocaleString()} tok/min\n`
|
|
186
|
+
: '';
|
|
187
|
+
console.log(`\n📊 claudestat status\n` +
|
|
188
|
+
`──────────────────────────────────────────\n` +
|
|
189
|
+
` Quota 5h ${pctColor}${q.cyclePrompts}/${q.cycleLimit} prompts (${q.cyclePct}%)${R} │ resets in ${resetLabel}\n` +
|
|
190
|
+
` Plan ${q.detectedPlan.toUpperCase()}\n` +
|
|
191
|
+
` Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h this week\n` +
|
|
192
|
+
(q.weeklyLimitOpus > 0
|
|
193
|
+
? ` Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h this week\n`
|
|
194
|
+
: '') +
|
|
195
|
+
`${burnRow}` +
|
|
196
|
+
`──────────────────────────────────────────\n`);
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
console.error('\n❌ Daemon is not running. Start it with: claudestat start\n');
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
program
|
|
205
|
+
.command('config')
|
|
206
|
+
.description('View or edit configuration (~/.claudestat/config.json)')
|
|
207
|
+
.option('--kill-switch <bool>', 'Enable/disable kill switch: true|false')
|
|
208
|
+
.option('--threshold <number>', 'Quota percentage to trigger the kill switch (default: 95)')
|
|
209
|
+
.option('--plan <plan>', 'Force plan detection: pro|max5|max20|auto')
|
|
210
|
+
.option('--alerts <bool>', 'Enable/disable daemon rate limit alerts: true|false')
|
|
211
|
+
.action((opts) => {
|
|
212
|
+
const cfg = (0, config_1.readConfig)();
|
|
213
|
+
let changed = false;
|
|
214
|
+
if (opts.killSwitch !== undefined) {
|
|
215
|
+
cfg.killSwitchEnabled = opts.killSwitch === 'true';
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
if (opts.threshold !== undefined) {
|
|
219
|
+
const t = parseInt(opts.threshold, 10);
|
|
220
|
+
if (!isNaN(t) && t > 0 && t <= 100) {
|
|
221
|
+
cfg.killSwitchThreshold = t;
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
else
|
|
225
|
+
console.warn(' ⚠️ threshold must be a number between 1 and 100');
|
|
226
|
+
}
|
|
227
|
+
if (opts.plan !== undefined) {
|
|
228
|
+
if (['pro', 'max5', 'max20', 'auto'].includes(opts.plan)) {
|
|
229
|
+
cfg.plan = opts.plan === 'auto' ? null : opts.plan;
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.warn(' ⚠️ plan must be: pro | max5 | max20 | auto');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (opts.alerts !== undefined) {
|
|
237
|
+
cfg.alertsEnabled = opts.alerts === 'true';
|
|
238
|
+
changed = true;
|
|
239
|
+
}
|
|
240
|
+
if (changed) {
|
|
241
|
+
(0, config_1.writeConfig)(cfg);
|
|
242
|
+
console.log('✅ Config saved to ~/.claudestat/config.json');
|
|
243
|
+
}
|
|
244
|
+
// Always show current config
|
|
245
|
+
console.log('\n📋 Current config:');
|
|
246
|
+
console.log(` killSwitchEnabled: ${cfg.killSwitchEnabled}`);
|
|
247
|
+
console.log(` killSwitchThreshold: ${cfg.killSwitchThreshold}%`);
|
|
248
|
+
console.log(` warnThresholds: ${cfg.warnThresholds.join('%, ')}%`);
|
|
249
|
+
console.log(` alertsEnabled: ${cfg.alertsEnabled}`);
|
|
250
|
+
console.log(` plan: ${cfg.plan ?? 'auto-detect'}\n`);
|
|
251
|
+
process.exit(0);
|
|
252
|
+
});
|
|
253
|
+
program
|
|
254
|
+
.command('stop')
|
|
255
|
+
.description('Stop the claudestat daemon')
|
|
256
|
+
.action(async () => {
|
|
257
|
+
await stopDaemon().catch((e) => { console.error(`❌ ${e.message}`); process.exit(1); });
|
|
258
|
+
process.exit(0);
|
|
259
|
+
});
|
|
260
|
+
program
|
|
261
|
+
.command('restart')
|
|
262
|
+
.description('Restart the claudestat daemon')
|
|
263
|
+
.action(async () => {
|
|
264
|
+
setTimeout(() => process.exit(0), 5000).unref();
|
|
265
|
+
await stopDaemon().catch(() => { console.log(' Daemon was not running, starting fresh…'); });
|
|
266
|
+
await new Promise(r => setTimeout(r, 500));
|
|
267
|
+
spawnDaemon();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
program
|
|
271
|
+
.command('top')
|
|
272
|
+
.description('Rank tools by cost, frequency, or duration')
|
|
273
|
+
.option('--by <metric>', 'Sort by: cost, count, duration (default: cost)')
|
|
274
|
+
.option('--limit <number>', 'Number of tools to show (default: 10)')
|
|
275
|
+
.option('--days <number>', 'Look back N days (default: 30)')
|
|
276
|
+
.action(async (opts) => {
|
|
277
|
+
try {
|
|
278
|
+
const by = opts.by ?? 'cost';
|
|
279
|
+
const limit = opts.limit ?? 10;
|
|
280
|
+
const days = opts.days ?? 30;
|
|
281
|
+
const url = `http://localhost:7337/api/top?by=${by}&limit=${limit}&days=${days}`;
|
|
282
|
+
const res = await fetch(url);
|
|
283
|
+
if (!res.ok)
|
|
284
|
+
throw new Error('Daemon unavailable');
|
|
285
|
+
const data = await res.json();
|
|
286
|
+
const label = by === 'count' ? 'calls' : by === 'duration' ? 'duration' : 'est. cost';
|
|
287
|
+
console.log(`\n🏆 claudestat top — by ${label} (last ${days} days)\n`);
|
|
288
|
+
console.log(' # Tool Calls Duration Est. Cost %');
|
|
289
|
+
console.log(' ── ───────────────── ──────── ───────────── ───────── ────');
|
|
290
|
+
for (let i = 0; i < data.tools.length; i++) {
|
|
291
|
+
const t = data.tools[i];
|
|
292
|
+
const isOther = t.tool === 'Other';
|
|
293
|
+
const dur = isOther ? '—'
|
|
294
|
+
: t.totalDurationMs >= 60000
|
|
295
|
+
? `${(t.totalDurationMs / 60000).toFixed(1)}m`
|
|
296
|
+
: `${(t.totalDurationMs / 1000).toFixed(0)}s`;
|
|
297
|
+
const cost = t.estimatedCostUsd < 0.01
|
|
298
|
+
? `$${t.estimatedCostUsd.toFixed(4)}`
|
|
299
|
+
: `$${t.estimatedCostUsd.toFixed(2)}`;
|
|
300
|
+
const pct = by === 'cost' ? `${t.pctCost}%` : isOther ? '' : `${t.pctCount}%`;
|
|
301
|
+
const countStr = isOther ? '—' : String(t.count);
|
|
302
|
+
console.log(` ${(i + 1).toString().padStart(2)} ${t.tool.padEnd(18)} ${countStr.padStart(8)} ${dur.padStart(13)} ${cost.padStart(9)} ${pct.padStart(4)}`);
|
|
303
|
+
}
|
|
304
|
+
console.log();
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
console.error('\n❌ Daemon is not running. Start it with: claudestat start\n');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
program
|
|
313
|
+
.command('doctor')
|
|
314
|
+
.description('Check installation health and diagnose common issues')
|
|
315
|
+
.action(() => (0, doctor_1.runDoctor)().catch(err => {
|
|
316
|
+
console.error('\n❌ Error:', err.message);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}));
|
|
319
|
+
program.parse();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install.ts — Instalador de hooks en Claude Code
|
|
3
|
+
*
|
|
4
|
+
* Claude Code permite definir hooks en ~/.claude/settings.json.
|
|
5
|
+
* Este comando modifica ese archivo para agregar nuestros hooks
|
|
6
|
+
* sin pisar los que ya existan.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANTE: Hacemos un backup antes de modificar.
|
|
9
|
+
*/
|
|
10
|
+
export declare function installHooks(): void;
|
|
11
|
+
export declare function runInstall(): Promise<void>;
|
|
12
|
+
export declare function runWizard(): Promise<void>;
|
|
13
|
+
export declare function showInstallStatus(): void;
|
|
14
|
+
export declare function uninstallHooks(): void;
|