@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 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
- Then wire up the hooks into Claude Code:
133
+ > Restart Claude Code after setup so the hooks take effect.
131
134
 
132
- ```bash
133
- claudestat install
134
- ```
135
+ ### Manual setup (alternative)
135
136
 
136
- This modifies `~/.claude/settings.json` to add `SessionStart`, `PreToolUse`, `PostToolUse`, and `Stop` hooks. A backup is created before any change.
137
+ If you prefer to manage the daemon yourself:
137
138
 
138
- > Restart Claude Code after installing so the hooks take effect.
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. Start the background daemon
146
- claudestat start
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 start` | Start the background daemon |
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.2.2)
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.2.2
458
+ 1.3.0
453
459
  latest ✓
454
460
  ```
455
461
 
456
- If a newer version is available, it shows: `latest: 1.3.0 — run npm update`.
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
  ![claudestat MCP demo](https://raw.githubusercontent.com/DeibyGS/claudestat/main/assets/mcp-demo.gif)
515
521
 
516
- Zero extra dependencies — stdio JSON-RPC, works without the daemon running. Uses on-demand API refresh with shared disk cache for accurate quota data.
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
- claudestat uninstall # removes hooks from Claude Code settings
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 # removes DB, config, and PID file
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`, run `claudestat start`, and open `http://localhost:7337` for the live dashboard.
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
@@ -25,6 +25,7 @@ export interface ClaudestatConfig {
25
25
  warnThresholds: number[];
26
26
  weeklyWarnThresholds: number[];
27
27
  resetReminderMins: number;
28
+ sessionCostLimitUsd: number;
28
29
  plan: ClaudePlan | null;
29
30
  reportsEnabled: boolean;
30
31
  reportFrequency: ReportFrequency;
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
- console.log('Setting up claudestat...');
204
- (0, install_1.installHooks)();
205
- (0, service_1.installService)();
206
- console.log('✅ claudestat is running and will start automatically on login');
207
- console.log(' Dashboard → http://localhost:7337');
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)}`);
@@ -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)({
@@ -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
- ? `5h quota at ${data.cyclePct}% (limit: ${cfg.killSwitchThreshold}%). Resets in ${formatMs(data.cycleResetMs)}.`
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
- // 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`)
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.0",
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",