@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 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
  ```
@@ -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 --transport stdio claudestat -- claudestat-mcp
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
+ ![claudestat MCP demo](https://raw.githubusercontent.com/DeibyGS/claudestat/main/assets/mcp-demo.gif)
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 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.
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 >= 18,
21
- note: nodeMajor >= 22 ? 'node:sqlite supported ✓'
22
- : nodeMajor >= 18 ? 'Works Node 22+ recommended for native node:sqlite'
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 @deibygs/claudestat',
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 @deibygs/claudestat && npm install -g @deibygs/claudestat\n Then restart your terminal or run: ${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'}`,
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 @deibygs/claudestat && npm install -g @deibygs/claudestat`,
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 @deibygs/claudestat\n Then restart terminal`,
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)();
@@ -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
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.2.2",
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",