@statforge/claudestat 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -21
- package/dist/config.d.ts +1 -0
- package/dist/config.js +6 -0
- package/dist/index.js +25 -5
- package/dist/routes/events.js +24 -0
- package/dist/routes/misc.js +1 -1
- package/hooks/event.js +4 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,40 +118,43 @@ Claude Code event
|
|
|
118
118
|
## Installation
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
|
-
npm install -g @statforge/claudestat
|
|
121
|
+
npm install -g @statforge/claudestat && claudestat setup
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
`claudestat setup` installs the Claude Code hooks and registers the daemon as a system service (launchd on macOS, systemd on Linux) — no sudo required. The daemon starts automatically whenever you log in.
|
|
125
|
+
|
|
124
126
|
> **Using NVM?** Make sure you're on your default Node version before installing to avoid stale binary conflicts:
|
|
125
127
|
> ```bash
|
|
126
128
|
> nvm use default && npm install -g @statforge/claudestat
|
|
129
|
+
> claudestat setup
|
|
127
130
|
> ```
|
|
128
131
|
> Works with [nvm](https://github.com/nvm-sh/nvm) (macOS/Linux) and [nvm-windows](https://github.com/coreybutler/nvm-windows).
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
> Restart Claude Code after setup so the hooks take effect.
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
claudestat install
|
|
134
|
-
```
|
|
135
|
+
### Manual setup (alternative)
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
If you prefer to manage the daemon yourself:
|
|
137
138
|
|
|
138
|
-
|
|
139
|
+
```bash
|
|
140
|
+
npm install -g @statforge/claudestat
|
|
141
|
+
claudestat install # installs hooks into Claude Code
|
|
142
|
+
claudestat start # start the daemon manually
|
|
143
|
+
```
|
|
139
144
|
|
|
140
145
|
---
|
|
141
146
|
|
|
142
147
|
## Quick Start
|
|
143
148
|
|
|
144
149
|
```bash
|
|
145
|
-
# 1.
|
|
146
|
-
claudestat
|
|
150
|
+
# 1. Install and set up everything in one command
|
|
151
|
+
npm install -g @statforge/claudestat && claudestat setup
|
|
147
152
|
|
|
148
153
|
# 2. Open the dashboard in your browser
|
|
149
154
|
# macOS:
|
|
150
155
|
open http://localhost:7337
|
|
151
|
-
# Windows:
|
|
152
|
-
start http://localhost:7337
|
|
153
|
-
# Linux:
|
|
154
|
-
xdg-open http://localhost:7337
|
|
156
|
+
# Windows / Linux:
|
|
157
|
+
claudestat start # start daemon manually, then open http://localhost:7337
|
|
155
158
|
|
|
156
159
|
# 3. Or watch a live terminal trace
|
|
157
160
|
claudestat watch
|
|
@@ -165,7 +168,9 @@ That's it. Start a Claude Code session and watch the events flow in.
|
|
|
165
168
|
|
|
166
169
|
| Command | Description |
|
|
167
170
|
|---|---|
|
|
168
|
-
| `claudestat
|
|
171
|
+
| `claudestat setup` | One-command setup: install hooks + register daemon as system service |
|
|
172
|
+
| `claudestat setup --uninstall` | Remove hooks and system service |
|
|
173
|
+
| `claudestat start` | Start the background daemon manually |
|
|
169
174
|
| `claudestat stop` | Stop the daemon |
|
|
170
175
|
| `claudestat restart` | Restart the daemon |
|
|
171
176
|
| `claudestat install` | Install hooks into Claude Code |
|
|
@@ -432,8 +437,9 @@ claudestat doctor
|
|
|
432
437
|
✓ Daemon running (localhost:7337)
|
|
433
438
|
✓ Global CLI symlink valid
|
|
434
439
|
✓ No duplicate claudestat binaries in PATH
|
|
435
|
-
✓ Version match (installed: v1.
|
|
440
|
+
✓ Version match (installed: v1.3.0)
|
|
436
441
|
✓ NVM prefix matches active binary
|
|
442
|
+
✓ MCP server registered in Claude Code
|
|
437
443
|
──────────────────────────────────────────────
|
|
438
444
|
All checks passed — claudestat is healthy!
|
|
439
445
|
```
|
|
@@ -449,11 +455,11 @@ Shows the current version and checks npm for updates.
|
|
|
449
455
|
```bash
|
|
450
456
|
claudestat version
|
|
451
457
|
|
|
452
|
-
1.
|
|
458
|
+
1.3.0
|
|
453
459
|
latest ✓
|
|
454
460
|
```
|
|
455
461
|
|
|
456
|
-
If a newer version is available, it shows: `latest: 1.
|
|
462
|
+
If a newer version is available, it shows: `latest: 1.4.0 — run npm update`.
|
|
457
463
|
|
|
458
464
|
### `claudestat export`
|
|
459
465
|
|
|
@@ -513,7 +519,7 @@ Once registered, ask Claude things like:
|
|
|
513
519
|
|
|
514
520
|

|
|
515
521
|
|
|
516
|
-
Zero extra dependencies — stdio JSON-RPC
|
|
522
|
+
Zero extra dependencies — stdio JSON-RPC. Works without the daemon running (reads SQLite directly), but will warn you to start it if it's not active: `claudestat start`. Uses on-demand API refresh with shared disk cache for accurate quota data.
|
|
517
523
|
|
|
518
524
|
---
|
|
519
525
|
|
|
@@ -648,15 +654,19 @@ Have an idea? [Open an issue](https://github.com/DeibyGS/claudestat/issues) or s
|
|
|
648
654
|
## Uninstall
|
|
649
655
|
|
|
650
656
|
```bash
|
|
651
|
-
|
|
657
|
+
# Full uninstall (hooks + system service + daemon):
|
|
658
|
+
claudestat setup --uninstall
|
|
652
659
|
|
|
660
|
+
# Then remove the data directory:
|
|
653
661
|
# macOS / Linux:
|
|
654
|
-
rm -rf ~/.claudestat
|
|
662
|
+
rm -rf ~/.claudestat
|
|
655
663
|
|
|
656
664
|
# Windows (PowerShell):
|
|
657
665
|
Remove-Item -Recurse -Force "$env:USERPROFILE\.claudestat"
|
|
658
666
|
```
|
|
659
667
|
|
|
668
|
+
> If you installed manually (without `setup`), use `claudestat uninstall` to remove only the hooks.
|
|
669
|
+
|
|
660
670
|
---
|
|
661
671
|
|
|
662
672
|
## Contributing
|
|
@@ -712,7 +722,7 @@ Want to appear here? Pick a [good-first-issue](https://github.com/DeibyGS/claude
|
|
|
712
722
|
claudestat is a real-time monitor for Claude Code — not a log reader. It hooks into every tool call as it fires, tracks token usage and cost live, guards your quota with configurable alerts, and exposes an MCP server so Claude can answer questions about its own usage. ccusage reads JSONL history after sessions end; claudestat runs while you code.
|
|
713
723
|
|
|
714
724
|
**How do I monitor Claude Code token usage?**
|
|
715
|
-
Install with `npm install -g @statforge/claudestat
|
|
725
|
+
Install with `npm install -g @statforge/claudestat && claudestat setup`, then open `http://localhost:7337` for the live dashboard. The daemon starts automatically on login after setup.
|
|
716
726
|
|
|
717
727
|
**How do I track Claude Code costs?**
|
|
718
728
|
claudestat records every session's token usage and estimates API cost per tool call.
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -36,6 +36,7 @@ const DEFAULTS = {
|
|
|
36
36
|
warnThresholds: [70, 85, 95],
|
|
37
37
|
weeklyWarnThresholds: [50, 75, 90],
|
|
38
38
|
resetReminderMins: 10,
|
|
39
|
+
sessionCostLimitUsd: 0,
|
|
39
40
|
plan: null,
|
|
40
41
|
reportsEnabled: false,
|
|
41
42
|
reportFrequency: 'weekly',
|
|
@@ -92,6 +93,11 @@ function validateConfig(raw) {
|
|
|
92
93
|
if (typeof v !== 'number' || isNaN(v) || v < 0 || v > 60)
|
|
93
94
|
return 'resetReminderMins must be a number between 0 and 60';
|
|
94
95
|
}
|
|
96
|
+
if ('sessionCostLimitUsd' in cfg) {
|
|
97
|
+
const v = cfg.sessionCostLimitUsd;
|
|
98
|
+
if (typeof v !== 'number' || isNaN(v) || v < 0)
|
|
99
|
+
return 'sessionCostLimitUsd debe ser un número >= 0 (0 = desactivado)';
|
|
100
|
+
}
|
|
95
101
|
if ('alertsEnabled' in cfg && typeof cfg.alertsEnabled !== 'boolean')
|
|
96
102
|
return 'alertsEnabled debe ser boolean';
|
|
97
103
|
if ('reportsEnabled' in cfg && typeof cfg.reportsEnabled !== 'boolean')
|
package/dist/index.js
CHANGED
|
@@ -200,11 +200,20 @@ program
|
|
|
200
200
|
console.log('✅ claudestat fully removed');
|
|
201
201
|
process.exit(0);
|
|
202
202
|
}
|
|
203
|
-
|
|
204
|
-
(0, install_1.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
// 1. Wizard: Node check + plan + config + hooks + MCP
|
|
204
|
+
await (0, install_1.runWizard)();
|
|
205
|
+
// 2. Start daemon now
|
|
206
|
+
const daemonRunning = await fetch('http://localhost:7337/health', {
|
|
207
|
+
signal: AbortSignal.timeout(2000),
|
|
208
|
+
}).then(r => r.ok).catch(() => false);
|
|
209
|
+
if (!daemonRunning) {
|
|
210
|
+
spawnDaemon();
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log('✅ Daemon already running');
|
|
214
|
+
console.log(' Dashboard → http://localhost:7337');
|
|
215
|
+
}
|
|
216
|
+
console.log('\n Run \x1b[36mclaudestat watch\x1b[0m to see live activity');
|
|
208
217
|
process.exit(0);
|
|
209
218
|
});
|
|
210
219
|
program
|
|
@@ -315,6 +324,7 @@ program
|
|
|
315
324
|
.option('--threshold <number>', 'Quota percentage to trigger the kill switch (default: 95)')
|
|
316
325
|
.option('--plan <plan>', 'Force plan detection: pro|max5|max20|auto')
|
|
317
326
|
.option('--alerts <bool>', 'Enable/disable daemon rate limit alerts: true|false')
|
|
327
|
+
.option('--session-limit <usd>', 'Alert when a session exceeds this cost in USD (0 = disabled)')
|
|
318
328
|
.action((opts) => {
|
|
319
329
|
const cfg = (0, config_1.readConfig)();
|
|
320
330
|
let changed = false;
|
|
@@ -344,6 +354,15 @@ program
|
|
|
344
354
|
cfg.alertsEnabled = opts.alerts === 'true';
|
|
345
355
|
changed = true;
|
|
346
356
|
}
|
|
357
|
+
if (opts.sessionLimit !== undefined) {
|
|
358
|
+
const v = parseFloat(opts.sessionLimit);
|
|
359
|
+
if (!isNaN(v) && v >= 0) {
|
|
360
|
+
cfg.sessionCostLimitUsd = v;
|
|
361
|
+
changed = true;
|
|
362
|
+
}
|
|
363
|
+
else
|
|
364
|
+
console.warn(' ⚠️ session-limit must be a number >= 0 (e.g. 5 for $5)');
|
|
365
|
+
}
|
|
347
366
|
if (changed) {
|
|
348
367
|
(0, config_1.writeConfig)(cfg);
|
|
349
368
|
console.log('✅ Config saved to ~/.claudestat/config.json');
|
|
@@ -373,6 +392,7 @@ program
|
|
|
373
392
|
if (cfg.killSwitchEnabled) {
|
|
374
393
|
lines.push(` ${bar(cfg.killSwitchThreshold)}`);
|
|
375
394
|
}
|
|
395
|
+
lines.push(` Session limit ${cfg.sessionCostLimitUsd > 0 ? `${Y}$${cfg.sessionCostLimitUsd.toFixed(2)}${R}` : `${D}OFF${R}`}`);
|
|
376
396
|
lines.push('');
|
|
377
397
|
lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
|
|
378
398
|
lines.push(` ${D}yellow${R} ${bar(cfg.warnThresholds[0], 8)} ${D}orange${R} ${bar(cfg.warnThresholds[1], 8)} ${D}red${R} ${bar(cfg.warnThresholds[2], 8)}`);
|
package/dist/routes/events.js
CHANGED
|
@@ -17,8 +17,14 @@ const quota_tracker_1 = require("../quota-tracker");
|
|
|
17
17
|
const config_1 = require("../config");
|
|
18
18
|
const rate_limiter_1 = require("../middleware/rate-limiter");
|
|
19
19
|
const stream_1 = require("./stream");
|
|
20
|
+
const notifier_1 = require("../notifier");
|
|
20
21
|
const enricher_1 = require("../enricher");
|
|
21
22
|
Object.defineProperty(exports, "processLatestForSession", { enumerable: true, get: function () { return enricher_1.processLatestForSession; } });
|
|
23
|
+
// ─── Loop alert cooldown (toolName:sessionId → last alert ts) ─────────────────
|
|
24
|
+
const loopAlertCooldown = new Map();
|
|
25
|
+
const LOOP_ALERT_COOLDOWN_MS = 120000; // coincide con LOOP_COOLDOWN_MS en intelligence.ts
|
|
26
|
+
// ─── Session cost alert: sesiones que ya recibieron notificación ───────────────
|
|
27
|
+
const sessionCostAlertFired = new Set();
|
|
22
28
|
exports.eventsRouter = (0, express_1.Router)();
|
|
23
29
|
// Skill activa por sesión — se setea tras Skill Done, se limpia en Stop.
|
|
24
30
|
// Permite taggear los eventos siguientes con skill_parent para agruparlos en la UI.
|
|
@@ -248,6 +254,24 @@ const onCostUpdate = (sessionId, cost) => {
|
|
|
248
254
|
projected_hourly_usd: projectedHourlyUsd,
|
|
249
255
|
}
|
|
250
256
|
});
|
|
257
|
+
// ─── Session cost alert: notificar si la sesión supera el límite configurado ──
|
|
258
|
+
const cfg = (0, config_1.readConfig)();
|
|
259
|
+
if (cfg.alertsEnabled && cfg.sessionCostLimitUsd > 0 && cost.cost_usd >= cfg.sessionCostLimitUsd && !sessionCostAlertFired.has(sessionId)) {
|
|
260
|
+
sessionCostAlertFired.add(sessionId);
|
|
261
|
+
(0, notifier_1.sendDesktopNotification)('claudestat — Session cost limit reached', `Session cost: $${cost.cost_usd.toFixed(2)} — limit $${cfg.sessionCostLimitUsd.toFixed(2)} reached`);
|
|
262
|
+
}
|
|
263
|
+
// ─── Loop alert: notificación de escritorio si hay loops activos ─────────────
|
|
264
|
+
if (cfg.alertsEnabled && report.loops.length > 0) {
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
for (const loop of report.loops) {
|
|
267
|
+
const key = `${loop.toolName}:${sessionId}`;
|
|
268
|
+
const lastSent = loopAlertCooldown.get(key) ?? 0;
|
|
269
|
+
if (now - lastSent >= LOOP_ALERT_COOLDOWN_MS) {
|
|
270
|
+
loopAlertCooldown.set(key, now);
|
|
271
|
+
(0, notifier_1.sendDesktopNotification)('claudestat — Loop detected', `${loop.toolName} called ${loop.count}× in 2min — session may be stuck`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
251
275
|
// Emitir desglose de costo del último bloque (input vs output) para el TracePanel
|
|
252
276
|
if (cost.lastEntry) {
|
|
253
277
|
(0, stream_1.broadcast)({
|
package/dist/routes/misc.js
CHANGED
|
@@ -90,7 +90,7 @@ exports.miscRouter.get('/kill-switch', (_req, res) => {
|
|
|
90
90
|
const data = (0, quota_tracker_1.computeQuota)(cfg.plan ?? undefined);
|
|
91
91
|
const blocked = cfg.killSwitchEnabled && data.cyclePct >= cfg.killSwitchThreshold;
|
|
92
92
|
const reason = blocked
|
|
93
|
-
? `
|
|
93
|
+
? `Quota at ${data.cyclePct}% — kill switch threshold is ${cfg.killSwitchThreshold}%. Resets in ${formatMs(data.cycleResetMs)}.`
|
|
94
94
|
: undefined;
|
|
95
95
|
res.json({ blocked, reason, cyclePct: data.cyclePct });
|
|
96
96
|
}
|
package/hooks/event.js
CHANGED
|
@@ -46,18 +46,13 @@ process.stdin.on('end', () => {
|
|
|
46
46
|
signal: AbortSignal.timeout(1500),
|
|
47
47
|
})
|
|
48
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
|
-
}),
|
|
49
|
+
.catch(() => ({ blocked: false })), // daemon no disponible → fail-open
|
|
55
50
|
])
|
|
56
51
|
.then(([_, ks]) => {
|
|
57
52
|
if (ks && ks.blocked) {
|
|
58
|
-
|
|
59
|
-
process.stderr.write(
|
|
60
|
-
process.stderr.write(`
|
|
53
|
+
process.stderr.write(`\n🚫 claudestat — kill switch active\n`)
|
|
54
|
+
process.stderr.write(` ${ks.reason ?? 'Usage quota exceeded.'}\n`)
|
|
55
|
+
process.stderr.write(` To disable: claudestat config --kill-switch false\n\n`)
|
|
61
56
|
process.exit(2)
|
|
62
57
|
} else {
|
|
63
58
|
process.exit(0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statforge/claudestat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|