@statforge/claudestat 1.2.3 → 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
@@ -2,9 +2,11 @@
2
2
 
3
3
  # claudestat
4
4
 
5
- **Real-time execution trace and cost intelligence for Claude Code**
5
+ **Live Claude Code monitor — real-time trace, quota guard, and MCP server**
6
+
7
+ Most tools read your logs after a session ends. claudestat hooks into every event as it fires.
8
+ See what Claude is spending right now, get alerted before you hit your quota, and ask Claude about its own usage — from inside the terminal.
6
9
 
7
- Hook into every tool call, token, and dollar — as it happens.
8
10
  Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js. Runs on macOS, Linux, and Windows.
9
11
 
10
12
  [![npm version](https://img.shields.io/npm/v/@statforge/claudestat?color=blue)](https://www.npmjs.com/package/@statforge/claudestat)
@@ -17,7 +19,7 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
17
19
 
18
20
  [Installation](#installation) • [Quick Start](#quick-start) • [Commands](#commands) • [Dashboard](#dashboard) • [Contributing](#contributing)
19
21
 
20
- ![ClaudeStat dashboard](https://res.cloudinary.com/dgscloudinary/image/upload/v1778225605/My%20portfolio%7D/ClaudeStat_vnfmup.png)
22
+ ![ClaudeStat banner](https://raw.githubusercontent.com/DeibyGS/claudestat/main/assets/banner.png)
21
23
 
22
24
  ---
23
25
 
@@ -31,26 +33,57 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
31
33
 
32
34
  ---
33
35
 
34
- ## Why?
36
+ ## Why claudestat?
37
+
38
+ Tools like ccusage are great for reviewing history. claudestat is for while you're coding.
35
39
 
36
- You're burning tokens right now and you have no idea how many, on what, or whether Claude is stuck in a loop.
40
+ It taps into Claude Code's hook system to capture every event the moment it fires, stores everything locally in SQLite, and gives you a live dashboard, quota alerts, and an MCP server not just a report.
37
41
 
38
- Claude Code is powerful but it's a black box while it runs. You can't see what it's spending, how deep the context is, whether it's looping, or if you're about to hit your quota limit.
42
+ | | claudestat | ccusage |
43
+ |---|:---:|:---:|
44
+ | Real-time event stream | ✅ | ❌ |
45
+ | Live terminal trace (`watch`) | ✅ | ❌ |
46
+ | Web dashboard | ✅ | ❌ |
47
+ | Quota alerts + kill switch | ✅ | ❌ |
48
+ | Loop detector | ✅ | ❌ |
49
+ | MCP server (ask Claude about itself) | ✅ | ❌ |
50
+ | Historical usage analysis | ✅ | ✅ |
39
51
 
40
- **claudestat fixes that.** It taps into Claude Code's hook system to capture every event, stores it locally in SQLite, and shows you everything in a live dashboard or terminal trace.
52
+ **What you get:**
41
53
 
42
- - Live tool trace with duration and token cost per call
43
- - Quota guard with configurable kill switch (block new sessions at X%)
44
- - Pattern analyzer: detects loops, Bash overuse, low cache reuse, and more
45
- - Per-session cost breakdown + cache savings + burn rate
46
- - Weekly usage insights with actionable tips
47
- - AI-generated weekly usage reports
48
- - MCP server: query quota, sessions, and tools from within Claude Code
54
+ - Live tool trace — every call with duration and token cost as it runs
55
+ - Quota guard alerts at 70%, 85%, 95%; optional kill switch blocks new sessions at X%
56
+ - Loop detector flags context thrashing with estimated waste cost
57
+ - Top tools know which tools eat most of your budget
58
+ - Web dashboard session history, analytics, model breakdown, charts
59
+ - MCP server — 7 tools so Claude can answer questions about its own usage
60
+ - Weekly insights pattern analysis with actionable tips
49
61
 
50
62
  > If claudestat is useful, give it a ⭐ — it helps other developers find it.
51
63
 
52
64
  ---
53
65
 
66
+ ## Ask Claude about itself
67
+
68
+ claudestat ships an MCP server. Once registered, you can ask Claude Code questions about its own usage — without leaving the terminal.
69
+
70
+ ```bash
71
+ claude mcp add claudestat -s user -- claudestat-mcp
72
+ ```
73
+
74
+ Then just ask:
75
+
76
+ ```
77
+ > What's my current quota status?
78
+ > How much did I spend this week?
79
+ > What are my top 5 tools by cost?
80
+ > Break down my usage by model
81
+ ```
82
+
83
+ Claude reads your local SQLite data through the MCP server and answers in real time. No cloud, no API key, no extra setup. [Full MCP reference →](#mcp-server)
84
+
85
+ ---
86
+
54
87
  ## How it works
55
88
 
56
89
  ```
@@ -85,40 +118,43 @@ Claude Code event
85
118
  ## Installation
86
119
 
87
120
  ```bash
88
- npm install -g @statforge/claudestat
121
+ npm install -g @statforge/claudestat && claudestat setup
89
122
  ```
90
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
+
91
126
  > **Using NVM?** Make sure you're on your default Node version before installing to avoid stale binary conflicts:
92
127
  > ```bash
93
128
  > nvm use default && npm install -g @statforge/claudestat
129
+ > claudestat setup
94
130
  > ```
95
131
  > Works with [nvm](https://github.com/nvm-sh/nvm) (macOS/Linux) and [nvm-windows](https://github.com/coreybutler/nvm-windows).
96
132
 
97
- Then wire up the hooks into Claude Code:
133
+ > Restart Claude Code after setup so the hooks take effect.
98
134
 
99
- ```bash
100
- claudestat install
101
- ```
135
+ ### Manual setup (alternative)
102
136
 
103
- 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:
104
138
 
105
- > 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
+ ```
106
144
 
107
145
  ---
108
146
 
109
147
  ## Quick Start
110
148
 
111
149
  ```bash
112
- # 1. Start the background daemon
113
- claudestat start
150
+ # 1. Install and set up everything in one command
151
+ npm install -g @statforge/claudestat && claudestat setup
114
152
 
115
153
  # 2. Open the dashboard in your browser
116
154
  # macOS:
117
155
  open http://localhost:7337
118
- # Windows:
119
- start http://localhost:7337
120
- # Linux:
121
- xdg-open http://localhost:7337
156
+ # Windows / Linux:
157
+ claudestat start # start daemon manually, then open http://localhost:7337
122
158
 
123
159
  # 3. Or watch a live terminal trace
124
160
  claudestat watch
@@ -132,7 +168,9 @@ That's it. Start a Claude Code session and watch the events flow in.
132
168
 
133
169
  | Command | Description |
134
170
  |---|---|
135
- | `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 |
136
174
  | `claudestat stop` | Stop the daemon |
137
175
  | `claudestat restart` | Restart the daemon |
138
176
  | `claudestat install` | Install hooks into Claude Code |
@@ -399,8 +437,9 @@ claudestat doctor
399
437
  ✓ Daemon running (localhost:7337)
400
438
  ✓ Global CLI symlink valid
401
439
  ✓ No duplicate claudestat binaries in PATH
402
- ✓ Version match (installed: v1.2.2)
440
+ ✓ Version match (installed: v1.3.0)
403
441
  ✓ NVM prefix matches active binary
442
+ ✓ MCP server registered in Claude Code
404
443
  ──────────────────────────────────────────────
405
444
  All checks passed — claudestat is healthy!
406
445
  ```
@@ -416,11 +455,11 @@ Shows the current version and checks npm for updates.
416
455
  ```bash
417
456
  claudestat version
418
457
 
419
- 1.2.2
458
+ 1.3.0
420
459
  latest ✓
421
460
  ```
422
461
 
423
- 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`.
424
463
 
425
464
  ### `claudestat export`
426
465
 
@@ -478,7 +517,9 @@ Once registered, ask Claude things like:
478
517
  - *"Give me usage insights for the last 14 days"*
479
518
  - *"Break down my usage by model"*
480
519
 
481
- Zero extra dependencies — stdio JSON-RPC, works without the daemon running. Uses on-demand API refresh with shared disk cache for accurate quota data.
520
+ ![claudestat MCP demo](https://raw.githubusercontent.com/DeibyGS/claudestat/main/assets/mcp-demo.gif)
521
+
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.
482
523
 
483
524
  ---
484
525
 
@@ -613,15 +654,19 @@ Have an idea? [Open an issue](https://github.com/DeibyGS/claudestat/issues) or s
613
654
  ## Uninstall
614
655
 
615
656
  ```bash
616
- claudestat uninstall # removes hooks from Claude Code settings
657
+ # Full uninstall (hooks + system service + daemon):
658
+ claudestat setup --uninstall
617
659
 
660
+ # Then remove the data directory:
618
661
  # macOS / Linux:
619
- rm -rf ~/.claudestat # removes DB, config, and PID file
662
+ rm -rf ~/.claudestat
620
663
 
621
664
  # Windows (PowerShell):
622
665
  Remove-Item -Recurse -Force "$env:USERPROFILE\.claudestat"
623
666
  ```
624
667
 
668
+ > If you installed manually (without `setup`), use `claudestat uninstall` to remove only the hooks.
669
+
625
670
  ---
626
671
 
627
672
  ## Contributing
@@ -673,12 +718,11 @@ Want to appear here? Pick a [good-first-issue](https://github.com/DeibyGS/claude
673
718
 
674
719
  ## FAQ
675
720
 
676
- **What is claudestat?**
677
- claudestat is a real-time token monitoring and cost analytics tool for Claude Code.
678
- It captures every tool call, token usage, and API cost as it happens — locally, with zero cloud dependencies.
721
+ **What is claudestat? How is it different from ccusage?**
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.
679
723
 
680
724
  **How do I monitor Claude Code token usage?**
681
- 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.
682
726
 
683
727
  **How do I track Claude Code costs?**
684
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
@@ -24,6 +24,7 @@ const daemon_1 = require("./daemon");
24
24
  const watchdog_1 = require("./watchdog");
25
25
  const watch_1 = require("./watch");
26
26
  const install_1 = require("./install");
27
+ const service_1 = require("./service");
27
28
  const export_1 = require("./export");
28
29
  const config_1 = require("./config");
29
30
  const doctor_1 = require("./doctor");
@@ -186,14 +187,43 @@ program
186
187
  console.error('\n❌ Error:', err.message);
187
188
  process.exit(1);
188
189
  }));
190
+ program
191
+ .command('setup')
192
+ .description('One-command setup: install hooks + register daemon as system service (auto-starts on login)')
193
+ .option('--uninstall', 'Remove hooks and system service')
194
+ .action(async (opts) => {
195
+ if (opts.uninstall) {
196
+ console.log('Uninstalling claudestat...');
197
+ (0, service_1.uninstallService)();
198
+ (0, install_1.uninstallHooks)();
199
+ await stopDaemon().catch(() => { });
200
+ console.log('✅ claudestat fully removed');
201
+ process.exit(0);
202
+ }
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');
217
+ process.exit(0);
218
+ });
189
219
  program
190
220
  .command('install')
191
221
  .description('Install hooks into Claude Code settings')
192
- .action(install_1.runInstall);
222
+ .action(async () => { await (0, install_1.runInstall)(); process.exit(0); });
193
223
  program
194
224
  .command('uninstall')
195
225
  .description('Remove hooks from Claude Code')
196
- .action(install_1.uninstallHooks);
226
+ .action(() => { (0, install_1.uninstallHooks)(); process.exit(0); });
197
227
  program
198
228
  .command('export [format]')
199
229
  .description('Export session data (json | csv, default: json). Max 500 sessions.')
@@ -294,6 +324,7 @@ program
294
324
  .option('--threshold <number>', 'Quota percentage to trigger the kill switch (default: 95)')
295
325
  .option('--plan <plan>', 'Force plan detection: pro|max5|max20|auto')
296
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)')
297
328
  .action((opts) => {
298
329
  const cfg = (0, config_1.readConfig)();
299
330
  let changed = false;
@@ -323,6 +354,15 @@ program
323
354
  cfg.alertsEnabled = opts.alerts === 'true';
324
355
  changed = true;
325
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
+ }
326
366
  if (changed) {
327
367
  (0, config_1.writeConfig)(cfg);
328
368
  console.log('✅ Config saved to ~/.claudestat/config.json');
@@ -352,6 +392,7 @@ program
352
392
  if (cfg.killSwitchEnabled) {
353
393
  lines.push(` ${bar(cfg.killSwitchThreshold)}`);
354
394
  }
395
+ lines.push(` Session limit ${cfg.sessionCostLimitUsd > 0 ? `${Y}$${cfg.sessionCostLimitUsd.toFixed(2)}${R}` : `${D}OFF${R}`}`);
355
396
  lines.push('');
356
397
  lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
357
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)}`);
@@ -40,6 +40,9 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  return result;
41
41
  };
42
42
  })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
43
46
  Object.defineProperty(exports, "__esModule", { value: true });
44
47
  process.on('warning', (w) => {
45
48
  if (w.name === 'ExperimentalWarning' && w.message.includes('SQLite'))
@@ -47,10 +50,27 @@ process.on('warning', (w) => {
47
50
  process.stderr.write(`${w.name}: ${w.message}\n`);
48
51
  });
49
52
  const readline = __importStar(require("readline"));
53
+ const fs_1 = __importDefault(require("fs"));
50
54
  const db_1 = require("./db");
51
55
  const quota_tracker_1 = require("./quota-tracker");
52
56
  const insights_1 = require("./insights");
53
57
  const config_1 = require("./config");
58
+ const paths_1 = require("./paths");
59
+ function isDaemonRunning() {
60
+ try {
61
+ const pid = parseInt(fs_1.default.readFileSync((0, paths_1.getPidFile)(), 'utf8').trim(), 10);
62
+ process.kill(pid, 0);
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ const DAEMON_WARNING = `⚠️ claudestat daemon is not running — real-time monitoring is disabled.
70
+ Start it with: claudestat start
71
+
72
+ Data shown below is from the last recorded session.
73
+ ---`;
54
74
  const SERVER_NAME = 'claudestat';
55
75
  const SERVER_VERSION = '1.2.2';
56
76
  const PROTOCOL_VERSION = '2025-03-26';
@@ -364,17 +384,18 @@ function toolGetWeeklyInsight(days) {
364
384
  ].join('\n');
365
385
  }
366
386
  async function handleToolCall(name, args) {
387
+ const warning = isDaemonRunning() ? '' : DAEMON_WARNING + '\n';
367
388
  const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'cost';
368
389
  switch (name) {
369
390
  case 'get_quota_status':
370
391
  await (0, quota_tracker_1.refreshFromApi)();
371
- return toolGetQuotaStatus();
372
- case 'get_current_session': return toolGetCurrentSession();
373
- case 'get_session_stats': return toolGetSessionStats(typeof args.days === 'number' ? args.days : 7);
374
- case 'get_top_tools': return toolGetTopTools(typeof args.days === 'number' ? args.days : 30, sortBy);
375
- case 'get_usage_insights': return toolGetUsageInsights(typeof args.days === 'number' ? args.days : 7);
376
- case 'get_model_breakdown': return toolGetModelBreakdown(typeof args.days === 'number' ? args.days : 7);
377
- case 'get_weekly_insight': return toolGetWeeklyInsight(typeof args.days === 'number' ? args.days : 7);
392
+ return warning + toolGetQuotaStatus();
393
+ case 'get_current_session': return warning + toolGetCurrentSession();
394
+ case 'get_session_stats': return warning + toolGetSessionStats(typeof args.days === 'number' ? args.days : 7);
395
+ case 'get_top_tools': return warning + toolGetTopTools(typeof args.days === 'number' ? args.days : 30, sortBy);
396
+ case 'get_usage_insights': return warning + toolGetUsageInsights(typeof args.days === 'number' ? args.days : 7);
397
+ case 'get_model_breakdown': return warning + toolGetModelBreakdown(typeof args.days === 'number' ? args.days : 7);
398
+ case 'get_weekly_insight': return warning + toolGetWeeklyInsight(typeof args.days === 'number' ? args.days : 7);
378
399
  default: return `Unknown tool: ${name}`;
379
400
  }
380
401
  }
@@ -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
  }
@@ -0,0 +1,2 @@
1
+ export declare function installService(): void;
2
+ export declare function uninstallService(): void;
@@ -0,0 +1,124 @@
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.installService = installService;
7
+ exports.uninstallService = uninstallService;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const child_process_1 = require("child_process");
11
+ const PLIST_LABEL = 'com.statforge.claudestat';
12
+ const PLIST_PATH = path_1.default.join(process.env.HOME ?? '~', 'Library', 'LaunchAgents', `${PLIST_LABEL}.plist`);
13
+ const SYSTEMD_DIR = path_1.default.join(process.env.HOME ?? '~', '.config', 'systemd', 'user');
14
+ const SYSTEMD_PATH = path_1.default.join(SYSTEMD_DIR, 'claudestat.service');
15
+ function makePlist() {
16
+ const node = process.execPath;
17
+ const script = process.argv[1];
18
+ return `<?xml version="1.0" encoding="UTF-8"?>
19
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
20
+ <plist version="1.0">
21
+ <dict>
22
+ <key>Label</key>
23
+ <string>${PLIST_LABEL}</string>
24
+ <key>ProgramArguments</key>
25
+ <array>
26
+ <string>${node}</string>
27
+ <string>${script}</string>
28
+ <string>start</string>
29
+ </array>
30
+ <key>EnvironmentVariables</key>
31
+ <dict>
32
+ <key>CLAUDESTAT_DAEMON</key>
33
+ <string>1</string>
34
+ </dict>
35
+ <key>StandardOutPath</key>
36
+ <string>/tmp/claudestat-daemon.log</string>
37
+ <key>StandardErrorPath</key>
38
+ <string>/tmp/claudestat-daemon.err</string>
39
+ </dict>
40
+ </plist>`;
41
+ }
42
+ function makeUnit() {
43
+ const node = process.execPath;
44
+ const script = process.argv[1];
45
+ return `[Unit]
46
+ Description=ClaudeStat daemon — real-time Claude Code monitor
47
+ After=default.target
48
+
49
+ [Service]
50
+ Type=simple
51
+ ExecStart=${node} ${script} start
52
+ Restart=on-failure
53
+ RestartSec=5
54
+ Environment=CLAUDESTAT_DAEMON=1
55
+
56
+ [Install]
57
+ WantedBy=default.target`;
58
+ }
59
+ function installService() {
60
+ if (process.platform === 'darwin') {
61
+ fs_1.default.mkdirSync(path_1.default.dirname(PLIST_PATH), { recursive: true });
62
+ try {
63
+ (0, child_process_1.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
64
+ }
65
+ catch { }
66
+ fs_1.default.writeFileSync(PLIST_PATH, makePlist());
67
+ (0, child_process_1.execSync)(`launchctl load "${PLIST_PATH}"`);
68
+ console.log(` service → ${PLIST_PATH}`);
69
+ console.log(` node → ${process.execPath}`);
70
+ }
71
+ else if (process.platform === 'linux') {
72
+ const hasSystemd = (() => {
73
+ try {
74
+ (0, child_process_1.execSync)('which systemctl', { stdio: 'pipe' });
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ })();
81
+ if (!hasSystemd) {
82
+ console.log(' systemd not found — run `claudestat start` manually to start the daemon');
83
+ return;
84
+ }
85
+ fs_1.default.mkdirSync(SYSTEMD_DIR, { recursive: true });
86
+ fs_1.default.writeFileSync(SYSTEMD_PATH, makeUnit());
87
+ (0, child_process_1.execSync)('systemctl --user daemon-reload');
88
+ (0, child_process_1.execSync)('systemctl --user enable --now claudestat');
89
+ console.log(` service → ${SYSTEMD_PATH}`);
90
+ console.log(` node → ${process.execPath}`);
91
+ }
92
+ else {
93
+ console.log(' Auto-start on Windows coming soon. Run `claudestat start` manually.');
94
+ }
95
+ }
96
+ function uninstallService() {
97
+ if (process.platform === 'darwin') {
98
+ try {
99
+ (0, child_process_1.execSync)(`launchctl unload "${PLIST_PATH}" 2>/dev/null`, { stdio: 'ignore' });
100
+ }
101
+ catch { }
102
+ try {
103
+ fs_1.default.unlinkSync(PLIST_PATH);
104
+ console.log(` removed → ${PLIST_PATH}`);
105
+ }
106
+ catch {
107
+ console.log(' service file not found (already removed)');
108
+ }
109
+ }
110
+ else if (process.platform === 'linux') {
111
+ try {
112
+ (0, child_process_1.execSync)('systemctl --user disable --now claudestat 2>/dev/null', { stdio: 'ignore' });
113
+ }
114
+ catch { }
115
+ try {
116
+ fs_1.default.unlinkSync(SYSTEMD_PATH);
117
+ (0, child_process_1.execSync)('systemctl --user daemon-reload');
118
+ console.log(` removed → ${SYSTEMD_PATH}`);
119
+ }
120
+ catch {
121
+ console.log(' service file not found (already removed)');
122
+ }
123
+ }
124
+ }
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.2.3",
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",