@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 +82 -38
- package/dist/config.d.ts +1 -0
- package/dist/config.js +6 -0
- package/dist/index.js +43 -2
- package/dist/mcp-server.js +28 -7
- package/dist/routes/events.js +24 -0
- package/dist/routes/misc.js +1 -1
- package/dist/service.d.ts +2 -0
- package/dist/service.js +124 -0
- package/hooks/event.js +4 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# claudestat
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
[](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
|
-

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
52
|
+
**What you get:**
|
|
41
53
|
|
|
42
|
-
- Live tool trace with duration and token cost
|
|
43
|
-
- Quota guard
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
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
|
-
|
|
133
|
+
> Restart Claude Code after setup so the hooks take effect.
|
|
98
134
|
|
|
99
|
-
|
|
100
|
-
claudestat install
|
|
101
|
-
```
|
|
135
|
+
### Manual setup (alternative)
|
|
102
136
|
|
|
103
|
-
|
|
137
|
+
If you prefer to manage the daemon yourself:
|
|
104
138
|
|
|
105
|
-
|
|
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.
|
|
113
|
-
claudestat
|
|
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
|
|
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.
|
|
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.
|
|
458
|
+
1.3.0
|
|
420
459
|
latest ✓
|
|
421
460
|
```
|
|
422
461
|
|
|
423
|
-
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`.
|
|
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
|
-
|
|
520
|
+

|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
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)}`);
|
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
}
|
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/dist/service.js
ADDED
|
@@ -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
|
-
|
|
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",
|