@statforge/claudestat 1.2.2 → 1.3.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 +52 -18
- package/dist/doctor.js +27 -9
- package/dist/index.js +58 -2
- package/dist/install.js +39 -0
- package/dist/mcp-server.js +28 -7
- package/dist/service.d.ts +2 -0
- package/dist/service.js +124 -0
- 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
|
```
|
|
@@ -468,7 +501,7 @@ claudestat includes an MCP (Model Context Protocol) server that lets Claude Code
|
|
|
468
501
|
### Register with Claude Code
|
|
469
502
|
|
|
470
503
|
```bash
|
|
471
|
-
claude mcp add
|
|
504
|
+
claude mcp add claudestat -s user -- claudestat-mcp
|
|
472
505
|
```
|
|
473
506
|
|
|
474
507
|
Once registered, ask Claude things like:
|
|
@@ -478,6 +511,8 @@ Once registered, ask Claude things like:
|
|
|
478
511
|
- *"Give me usage insights for the last 14 days"*
|
|
479
512
|
- *"Break down my usage by model"*
|
|
480
513
|
|
|
514
|
+

|
|
515
|
+
|
|
481
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.
|
|
482
517
|
|
|
483
518
|
---
|
|
@@ -673,9 +708,8 @@ Want to appear here? Pick a [good-first-issue](https://github.com/DeibyGS/claude
|
|
|
673
708
|
|
|
674
709
|
## FAQ
|
|
675
710
|
|
|
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.
|
|
711
|
+
**What is claudestat? How is it different from ccusage?**
|
|
712
|
+
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
713
|
|
|
680
714
|
**How do I monitor Claude Code token usage?**
|
|
681
715
|
Install with `npm install -g @statforge/claudestat`, run `claudestat start`, and open `http://localhost:7337` for the live dashboard.
|
package/dist/doctor.js
CHANGED
|
@@ -17,11 +17,9 @@ async function runDoctor() {
|
|
|
17
17
|
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
18
18
|
checks.push({
|
|
19
19
|
label: `Node.js version (${process.versions.node})`,
|
|
20
|
-
ok: nodeMajor >=
|
|
21
|
-
note: nodeMajor >= 22 ? 'node:sqlite supported ✓'
|
|
22
|
-
|
|
23
|
-
: undefined,
|
|
24
|
-
fix: nodeMajor < 18 ? 'Install Node.js 18 or later: https://nodejs.org' : undefined,
|
|
20
|
+
ok: nodeMajor >= 22,
|
|
21
|
+
note: nodeMajor >= 22 ? 'node:sqlite supported ✓' : undefined,
|
|
22
|
+
fix: nodeMajor < 22 ? 'Install Node.js 22 or later: https://nodejs.org' : undefined,
|
|
25
23
|
});
|
|
26
24
|
// 2. Claude Code installed
|
|
27
25
|
const claudeOk = (() => { try {
|
|
@@ -98,7 +96,7 @@ async function runDoctor() {
|
|
|
98
96
|
label: 'Global CLI symlink valid',
|
|
99
97
|
ok: symlinkOk,
|
|
100
98
|
note: symlinkNote,
|
|
101
|
-
fix: symlinkOk ? undefined : 'npm install -g @
|
|
99
|
+
fix: symlinkOk ? undefined : 'npm install -g @statforge/claudestat',
|
|
102
100
|
});
|
|
103
101
|
// 8. No duplicate claudestat binaries in PATH
|
|
104
102
|
let duplicatesOk = true;
|
|
@@ -117,7 +115,7 @@ async function runDoctor() {
|
|
|
117
115
|
ok: duplicatesOk,
|
|
118
116
|
note: duplicatesNote,
|
|
119
117
|
fix: duplicatesOk ? undefined :
|
|
120
|
-
`npm uninstall -g @
|
|
118
|
+
`npm uninstall -g @statforge/claudestat && npm install -g @statforge/claudestat\n Then restart your terminal or run: ${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'}`,
|
|
121
119
|
});
|
|
122
120
|
// 9. Active binary version matches installed package
|
|
123
121
|
let versionOk = true;
|
|
@@ -146,7 +144,7 @@ async function runDoctor() {
|
|
|
146
144
|
ok: versionOk,
|
|
147
145
|
note: versionNote,
|
|
148
146
|
fix: versionOk ? undefined :
|
|
149
|
-
`${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'} (or restart terminal)\n If persists: npm uninstall -g @
|
|
147
|
+
`${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'} (or restart terminal)\n If persists: npm uninstall -g @statforge/claudestat && npm install -g @statforge/claudestat`,
|
|
150
148
|
});
|
|
151
149
|
// 10. NVM prefix sanity (only when NVM is active)
|
|
152
150
|
if ((process.env.NVM_DIR || process.env.NVM_HOME) && activeBinary) {
|
|
@@ -165,9 +163,29 @@ async function runDoctor() {
|
|
|
165
163
|
ok: nvmOk,
|
|
166
164
|
note: nvmNote,
|
|
167
165
|
fix: nvmOk ? undefined :
|
|
168
|
-
`nvm use default && npm install -g @
|
|
166
|
+
`nvm use default && npm install -g @statforge/claudestat\n Then restart terminal`,
|
|
169
167
|
});
|
|
170
168
|
}
|
|
169
|
+
// 11. MCP server registered in Claude Code
|
|
170
|
+
let mcpOk = false;
|
|
171
|
+
let mcpNote;
|
|
172
|
+
const mcpResult = (0, child_process_1.spawnSync)('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 15000 });
|
|
173
|
+
if (mcpResult.error) {
|
|
174
|
+
mcpNote = '"claude" CLI not found — install Claude Code first';
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
const mcpList = (mcpResult.stdout ?? '') + (mcpResult.stderr ?? '');
|
|
178
|
+
const mcpLine = mcpList.split('\n').find(l => l.includes('claudestat'));
|
|
179
|
+
mcpOk = !!mcpLine && !mcpLine.includes('Failed') && !mcpLine.includes('✗');
|
|
180
|
+
if (!mcpOk)
|
|
181
|
+
mcpNote = 'Run "claudestat install" to register it automatically';
|
|
182
|
+
}
|
|
183
|
+
checks.push({
|
|
184
|
+
label: 'MCP server registered in Claude Code',
|
|
185
|
+
ok: mcpOk,
|
|
186
|
+
note: mcpNote,
|
|
187
|
+
fix: mcpOk ? undefined : 'claudestat install',
|
|
188
|
+
});
|
|
171
189
|
// ── Print results ───────────────────────────────────────────
|
|
172
190
|
console.log('\n🩺 claudestat doctor\n' + '─'.repeat(46));
|
|
173
191
|
for (const c of checks) {
|
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");
|
|
@@ -34,6 +35,41 @@ const quota_tracker_1 = require("./quota-tracker");
|
|
|
34
35
|
const program = new commander_1.Command();
|
|
35
36
|
const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
36
37
|
const PID_FILE = (0, paths_1.getPidFile)();
|
|
38
|
+
// ── Update notifier ────────────────────────────────────────────
|
|
39
|
+
const SKIP_UPDATE_NOTICE = new Set(['start', 'stop', 'restart', 'watch']);
|
|
40
|
+
const subcommand = process.argv[2];
|
|
41
|
+
if (!SKIP_UPDATE_NOTICE.has(subcommand)) {
|
|
42
|
+
const UPDATE_CACHE = path_1.default.join((0, paths_1.getClaudestatDir)(), 'update-cache.json');
|
|
43
|
+
let cachedLatest = null;
|
|
44
|
+
const fetchLatestVersion = () => {
|
|
45
|
+
fetch('https://registry.npmjs.org/@statforge/claudestat/latest', { signal: AbortSignal.timeout(3000) })
|
|
46
|
+
.then(r => r.json())
|
|
47
|
+
.then(j => {
|
|
48
|
+
if (j?.version) {
|
|
49
|
+
cachedLatest = j.version;
|
|
50
|
+
fs_1.default.writeFileSync(UPDATE_CACHE, JSON.stringify({ version: j.version, ts: Date.now() }));
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.catch(() => { });
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
const cache = JSON.parse(fs_1.default.readFileSync(UPDATE_CACHE, 'utf8'));
|
|
57
|
+
cachedLatest = cache.version;
|
|
58
|
+
if (Date.now() - cache.ts >= 24 * 60 * 60 * 1000)
|
|
59
|
+
fetchLatestVersion();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
fetchLatestVersion();
|
|
63
|
+
}
|
|
64
|
+
const _exit = process.exit.bind(process);
|
|
65
|
+
process.exit = ((code) => {
|
|
66
|
+
if ((code ?? 0) === 0 && cachedLatest && cachedLatest !== PKG_VERSION) {
|
|
67
|
+
console.log(`\n ✦ Update available: ${PKG_VERSION} → ${cachedLatest}`);
|
|
68
|
+
console.log(` Run: npm install -g @statforge/claudestat\n`);
|
|
69
|
+
}
|
|
70
|
+
_exit(code);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
37
73
|
function spawnDaemon() {
|
|
38
74
|
const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], 'start'], {
|
|
39
75
|
detached: true,
|
|
@@ -151,14 +187,34 @@ program
|
|
|
151
187
|
console.error('\n❌ Error:', err.message);
|
|
152
188
|
process.exit(1);
|
|
153
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
|
+
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');
|
|
208
|
+
process.exit(0);
|
|
209
|
+
});
|
|
154
210
|
program
|
|
155
211
|
.command('install')
|
|
156
212
|
.description('Install hooks into Claude Code settings')
|
|
157
|
-
.action(install_1.runInstall);
|
|
213
|
+
.action(async () => { await (0, install_1.runInstall)(); process.exit(0); });
|
|
158
214
|
program
|
|
159
215
|
.command('uninstall')
|
|
160
216
|
.description('Remove hooks from Claude Code')
|
|
161
|
-
.action(install_1.uninstallHooks);
|
|
217
|
+
.action(() => { (0, install_1.uninstallHooks)(); process.exit(0); });
|
|
162
218
|
program
|
|
163
219
|
.command('export [format]')
|
|
164
220
|
.description('Export session data (json | csv, default: json). Max 500 sessions.')
|
package/dist/install.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.uninstallHooks = uninstallHooks;
|
|
|
20
20
|
const fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
22
22
|
const readline_1 = __importDefault(require("readline"));
|
|
23
|
+
const child_process_1 = require("child_process");
|
|
23
24
|
const paths_1 = require("./paths");
|
|
24
25
|
const config_1 = require("./config");
|
|
25
26
|
const CLAUDESTAT_DIR = (0, paths_1.getClaudestatDir)();
|
|
@@ -98,7 +99,9 @@ async function runInstall() {
|
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
100
101
|
showInstallStatus();
|
|
102
|
+
installMcp();
|
|
101
103
|
}
|
|
104
|
+
process.exit(0);
|
|
102
105
|
}
|
|
103
106
|
async function runWizard() {
|
|
104
107
|
const nonInteractive = !process.stdin.isTTY;
|
|
@@ -150,6 +153,42 @@ async function runWizard() {
|
|
|
150
153
|
}
|
|
151
154
|
// Paso 5: instalar hooks
|
|
152
155
|
installHooks();
|
|
156
|
+
// Paso 6: registrar MCP server en Claude Code
|
|
157
|
+
installMcp();
|
|
158
|
+
}
|
|
159
|
+
function installMcp() {
|
|
160
|
+
const nodeExec = process.execPath;
|
|
161
|
+
const mcpScript = path_1.default.join(__dirname, 'mcp-server.js');
|
|
162
|
+
const manualCmd = `claude mcp add claudestat -s user -- "${nodeExec}" --disable-warning=ExperimentalWarning "${mcpScript}"`;
|
|
163
|
+
try {
|
|
164
|
+
const result = (0, child_process_1.spawnSync)('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 15000 });
|
|
165
|
+
const list = (result.stdout ?? '') + (result.stderr ?? '');
|
|
166
|
+
const mcpLine = list.split('\n').find((l) => l.includes('claudestat'));
|
|
167
|
+
if (mcpLine && !mcpLine.includes('Failed') && !mcpLine.includes('✗')) {
|
|
168
|
+
console.log(' (already registered): MCP server');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (mcpLine) {
|
|
172
|
+
// Registered but failing — remove from both scopes and re-register
|
|
173
|
+
(0, child_process_1.spawnSync)('claude', ['mcp', 'remove', 'claudestat', '-s', 'user'], { encoding: 'utf8' });
|
|
174
|
+
(0, child_process_1.spawnSync)('claude', ['mcp', 'remove', 'claudestat', '-s', 'local'], { encoding: 'utf8' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
console.log('\n⚠ Could not reach "claude" CLI — skipping MCP setup.');
|
|
179
|
+
console.log(' To register manually:');
|
|
180
|
+
console.log(` ${manualCmd}\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
(0, child_process_1.execSync)(manualCmd, { stdio: 'pipe' });
|
|
185
|
+
console.log('✓ MCP server registered (user scope)\n');
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.log('\n⚠ MCP registration failed.');
|
|
189
|
+
console.log(' To register manually:');
|
|
190
|
+
console.log(` ${manualCmd}\n`);
|
|
191
|
+
}
|
|
153
192
|
}
|
|
154
193
|
function showInstallStatus() {
|
|
155
194
|
const cfg = (0, config_1.readConfig)();
|
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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statforge/claudestat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|