@statforge/claudestat 1.7.0 → 1.8.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 +33 -2
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/daemon.js +56 -8
- package/dist/db.d.ts +11 -0
- package/dist/db.js +30 -0
- package/dist/doctor.js +20 -2
- package/dist/export.d.ts +2 -1
- package/dist/export.js +41 -6
- package/dist/index.js +405 -30
- package/dist/insights.d.ts +1 -0
- package/dist/insights.js +26 -0
- package/dist/install.js +28 -1
- package/dist/intelligence.d.ts +11 -4
- package/dist/intelligence.js +43 -17
- package/dist/logger.d.ts +6 -0
- package/dist/logger.js +49 -0
- package/dist/notifier.d.ts +15 -0
- package/dist/notifier.js +26 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +34 -0
- package/dist/routes/helpers.d.ts +5 -0
- package/dist/routes/helpers.js +21 -1
- package/dist/routes/misc.js +19 -1
- package/dist/routes/projects.js +10 -1
- package/dist/service.js +11 -0
- package/hooks/event.js +44 -26
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,6 +6,39 @@
|
|
|
6
6
|
* Suprimimos el ExperimentalWarning de node:sqlite antes de importar nada.
|
|
7
7
|
* El módulo funciona perfectamente — el warning es solo informativo.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
10
43
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
11
44
|
};
|
|
@@ -20,8 +53,6 @@ const commander_1 = require("commander");
|
|
|
20
53
|
const fs_1 = __importDefault(require("fs"));
|
|
21
54
|
const path_1 = __importDefault(require("path"));
|
|
22
55
|
const child_process_1 = require("child_process");
|
|
23
|
-
const daemon_1 = require("./daemon");
|
|
24
|
-
const watchdog_1 = require("./watchdog");
|
|
25
56
|
const watch_1 = require("./watch");
|
|
26
57
|
const install_1 = require("./install");
|
|
27
58
|
const service_1 = require("./service");
|
|
@@ -36,6 +67,18 @@ const quota_tracker_1 = require("./quota-tracker");
|
|
|
36
67
|
const program = new commander_1.Command();
|
|
37
68
|
const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
38
69
|
const PID_FILE = (0, paths_1.getPidFile)();
|
|
70
|
+
const PORT = (0, config_1.readConfig)().port;
|
|
71
|
+
function semverGt(a, b) {
|
|
72
|
+
const [pa, pb] = [a.split('.').map(Number), b.split('.').map(Number)];
|
|
73
|
+
for (let i = 0; i < 3; i++) {
|
|
74
|
+
const aVal = pa[i] ?? 0, bVal = pb[i] ?? 0;
|
|
75
|
+
if (aVal > bVal)
|
|
76
|
+
return true;
|
|
77
|
+
if (aVal < bVal)
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
39
82
|
// ── Update notifier ────────────────────────────────────────────
|
|
40
83
|
const SKIP_UPDATE_NOTICE = new Set(['start', 'stop', 'restart', 'watch']);
|
|
41
84
|
const subcommand = process.argv[2];
|
|
@@ -62,17 +105,6 @@ if (!SKIP_UPDATE_NOTICE.has(subcommand)) {
|
|
|
62
105
|
catch {
|
|
63
106
|
fetchLatestVersion();
|
|
64
107
|
}
|
|
65
|
-
const semverGt = (a, b) => {
|
|
66
|
-
const [pa, pb] = [a.split('.').map(Number), b.split('.').map(Number)];
|
|
67
|
-
for (let i = 0; i < 3; i++) {
|
|
68
|
-
const aVal = pa[i] ?? 0, bVal = pb[i] ?? 0;
|
|
69
|
-
if (aVal > bVal)
|
|
70
|
-
return true;
|
|
71
|
-
if (aVal < bVal)
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
};
|
|
76
108
|
const _exit = process.exit.bind(process);
|
|
77
109
|
process.exit = ((code) => {
|
|
78
110
|
if ((code ?? 0) === 0 && cachedLatest && semverGt(cachedLatest, PKG_VERSION)) {
|
|
@@ -90,7 +122,7 @@ function spawnDaemon() {
|
|
|
90
122
|
});
|
|
91
123
|
child.unref();
|
|
92
124
|
console.log(`✅ claudestat daemon started (pid ${child.pid})`);
|
|
93
|
-
console.log(` Dashboard → http://localhost
|
|
125
|
+
console.log(` Dashboard → http://localhost:${PORT}`);
|
|
94
126
|
}
|
|
95
127
|
function removePidFile() {
|
|
96
128
|
try {
|
|
@@ -100,7 +132,7 @@ function removePidFile() {
|
|
|
100
132
|
}
|
|
101
133
|
async function stopDaemon() {
|
|
102
134
|
try {
|
|
103
|
-
const res = await fetch(
|
|
135
|
+
const res = await fetch(`http://localhost:${PORT}/shutdown`, {
|
|
104
136
|
method: 'POST',
|
|
105
137
|
signal: AbortSignal.timeout(2000),
|
|
106
138
|
});
|
|
@@ -177,18 +209,96 @@ program
|
|
|
177
209
|
}
|
|
178
210
|
process.exit(0);
|
|
179
211
|
});
|
|
212
|
+
program
|
|
213
|
+
.command('update')
|
|
214
|
+
.description('Check for updates and install the latest version from npm')
|
|
215
|
+
.option('--dry-run', 'Only check for updates, do not install')
|
|
216
|
+
.action(async (opts) => {
|
|
217
|
+
console.log('\n🔍 Checking for updates...');
|
|
218
|
+
const latest = await checkLatestVersion();
|
|
219
|
+
if (!latest) {
|
|
220
|
+
console.log(' ❌ Could not reach npm registry. Check your internet connection.\n');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
if (!semverGt(latest, PKG_VERSION)) {
|
|
224
|
+
console.log(` ✅ Already on latest version (${PKG_VERSION})\n`);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
console.log(` ✦ Update available: ${PKG_VERSION} → ${latest}`);
|
|
228
|
+
try {
|
|
229
|
+
const changelogPath = path_1.default.join(__dirname, '..', 'CHANGELOG.md');
|
|
230
|
+
const changelog = fs_1.default.readFileSync(changelogPath, 'utf8');
|
|
231
|
+
const sections = changelog.split(/^## /m).filter(Boolean);
|
|
232
|
+
const relevant = sections.filter(s => s.startsWith(`[${latest}]`) ||
|
|
233
|
+
(semverGt(s.split(']')[0].replace('[', ''), PKG_VERSION) &&
|
|
234
|
+
semverGt(latest, s.split(']')[0].replace('[', ''))));
|
|
235
|
+
if (relevant.length > 0) {
|
|
236
|
+
console.log('\n 📋 What\'s new:');
|
|
237
|
+
relevant.slice(0, 3).forEach(s => {
|
|
238
|
+
const lines = s.split('\n').slice(0, 6);
|
|
239
|
+
lines.forEach(l => console.log(` ${l}`));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch { }
|
|
244
|
+
if (opts.dryRun) {
|
|
245
|
+
console.log(`\n Run \x1b[36mclaudestat update\x1b[0m to install.\n`);
|
|
246
|
+
process.exit(0);
|
|
247
|
+
}
|
|
248
|
+
console.log(`\n📦 Installing @statforge/claudestat@${latest}...`);
|
|
249
|
+
try {
|
|
250
|
+
(0, child_process_1.execSync)('npm install -g @statforge/claudestat', { stdio: 'inherit' });
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
console.error('\n❌ Installation failed. Try manually: npm install -g @statforge/claudestat\n');
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
console.log('\n🔗 Re-registering hooks...');
|
|
257
|
+
try {
|
|
258
|
+
const { installHooks } = await Promise.resolve().then(() => __importStar(require('./install')));
|
|
259
|
+
installHooks();
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
262
|
+
console.log('\n🔄 Restarting daemon...');
|
|
263
|
+
try {
|
|
264
|
+
await stopDaemon().catch(() => { });
|
|
265
|
+
await new Promise(r => setTimeout(r, 500));
|
|
266
|
+
spawnDaemon();
|
|
267
|
+
}
|
|
268
|
+
catch { }
|
|
269
|
+
console.log(`\n✅ Updated to ${latest}. Run \x1b[36mclaudestat doctor\x1b[0m to verify.\n`);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
});
|
|
180
272
|
program
|
|
181
273
|
.command('start')
|
|
182
274
|
.description('Start the background daemon (receives Claude Code hook events)')
|
|
183
275
|
.option('--watchdog', 'Auto-restart daemon if it crashes')
|
|
184
|
-
.
|
|
276
|
+
.option('--wait', 'Wait until daemon responds on /health before returning (max 10s)')
|
|
277
|
+
.action(async (opts) => {
|
|
185
278
|
if (process.env.CLAUDESTAT_DAEMON) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
279
|
+
const { startDaemon } = require('./daemon');
|
|
280
|
+
startDaemon();
|
|
281
|
+
if (opts.watchdog) {
|
|
282
|
+
const { startWatchdog } = require('./watchdog');
|
|
283
|
+
startWatchdog();
|
|
284
|
+
}
|
|
189
285
|
}
|
|
190
286
|
else {
|
|
191
287
|
spawnDaemon();
|
|
288
|
+
if (opts.wait) {
|
|
289
|
+
const deadline = Date.now() + 10000;
|
|
290
|
+
while (Date.now() < deadline) {
|
|
291
|
+
await new Promise(r => setTimeout(r, 200));
|
|
292
|
+
try {
|
|
293
|
+
const res = await fetch(`http://localhost:${PORT}/health`, { signal: AbortSignal.timeout(500) });
|
|
294
|
+
if (res.ok) {
|
|
295
|
+
console.log('✅ Daemon is ready');
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
192
302
|
process.exit(0);
|
|
193
303
|
}
|
|
194
304
|
});
|
|
@@ -203,7 +313,21 @@ program
|
|
|
203
313
|
.command('setup')
|
|
204
314
|
.description('One-command setup: install hooks + register daemon as system service (auto-starts on login)')
|
|
205
315
|
.option('--uninstall', 'Remove hooks and system service')
|
|
316
|
+
.option('--port <number>', 'Custom daemon port (default: 7337)')
|
|
317
|
+
.option('--reset', 'Reinstall from scratch (keeps SQLite history)')
|
|
206
318
|
.action(async (opts) => {
|
|
319
|
+
if (opts.port) {
|
|
320
|
+
const p = parseInt(opts.port, 10);
|
|
321
|
+
if (!isNaN(p) && p >= 1024 && p <= 65535) {
|
|
322
|
+
const cfg = (0, config_1.readConfig)();
|
|
323
|
+
(0, config_1.writeConfig)({ ...cfg, port: p });
|
|
324
|
+
console.log(`✓ Port set to ${p}`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
console.error('❌ Invalid port. Must be between 1024 and 65535.');
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
207
331
|
if (opts.uninstall) {
|
|
208
332
|
console.log('Uninstalling claudestat...');
|
|
209
333
|
(0, service_1.uninstallService)();
|
|
@@ -212,10 +336,23 @@ program
|
|
|
212
336
|
console.log('✅ claudestat fully removed');
|
|
213
337
|
process.exit(0);
|
|
214
338
|
}
|
|
339
|
+
if (opts.reset) {
|
|
340
|
+
console.log('\n🔄 Resetting claudestat installation...');
|
|
341
|
+
(0, service_1.uninstallService)();
|
|
342
|
+
(0, install_1.uninstallHooks)();
|
|
343
|
+
await stopDaemon().catch(() => { });
|
|
344
|
+
const cfgPath = path_1.default.join((0, paths_1.getClaudestatDir)(), 'config.json');
|
|
345
|
+
try {
|
|
346
|
+
fs_1.default.unlinkSync(cfgPath);
|
|
347
|
+
}
|
|
348
|
+
catch { }
|
|
349
|
+
console.log(' ✅ Hooks, service, and config removed (history preserved)');
|
|
350
|
+
console.log(' 🔁 Starting fresh install...\n');
|
|
351
|
+
}
|
|
215
352
|
// 1. Wizard: Node check + plan + config + hooks + MCP
|
|
216
353
|
await (0, install_1.runWizard)();
|
|
217
354
|
// 2. Start daemon now
|
|
218
|
-
const daemonRunning = await fetch(
|
|
355
|
+
const daemonRunning = await fetch(`http://localhost:${PORT}/health`, {
|
|
219
356
|
signal: AbortSignal.timeout(2000),
|
|
220
357
|
}).then(r => r.ok).catch(() => false);
|
|
221
358
|
if (!daemonRunning) {
|
|
@@ -223,7 +360,7 @@ program
|
|
|
223
360
|
}
|
|
224
361
|
else {
|
|
225
362
|
console.log('✅ Daemon already running');
|
|
226
|
-
console.log(
|
|
363
|
+
console.log(` Dashboard → http://localhost:${PORT}`);
|
|
227
364
|
}
|
|
228
365
|
console.log('\n Run \x1b[36mclaudestat watch\x1b[0m to see live activity');
|
|
229
366
|
process.exit(0);
|
|
@@ -238,15 +375,16 @@ program
|
|
|
238
375
|
.action(() => { (0, install_1.uninstallHooks)(); process.exit(0); });
|
|
239
376
|
program
|
|
240
377
|
.command('export [format]')
|
|
241
|
-
.description('Export session data (json | csv, default: json). Max 500 sessions.')
|
|
378
|
+
.description('Export session data (json | csv | markdown, default: json). Max 500 sessions.')
|
|
242
379
|
.option('--from <date>', 'Start date YYYY-MM-DD (inclusive)')
|
|
243
380
|
.option('--to <date>', 'End date YYYY-MM-DD (inclusive)')
|
|
381
|
+
.option('--since <period>', 'Shorthand: 7d, 30d, 90d (overrides --from)')
|
|
244
382
|
.option('--project <name>', 'Filter by project path (case-insensitive substring)')
|
|
245
|
-
.option('--output <path>', 'Write to file
|
|
383
|
+
.option('--output <path>', 'Write to file (default: stdout)')
|
|
246
384
|
.action((format, opts) => {
|
|
247
385
|
const fmt = (format ?? 'json').toLowerCase();
|
|
248
|
-
if (
|
|
249
|
-
console.error('Error: format must be "json" or "
|
|
386
|
+
if (!['json', 'csv', 'markdown'].includes(fmt)) {
|
|
387
|
+
console.error('Error: format must be "json", "csv", or "markdown"');
|
|
250
388
|
process.exit(1);
|
|
251
389
|
}
|
|
252
390
|
(0, export_1.runExport)({ format: fmt, ...opts });
|
|
@@ -260,8 +398,8 @@ program
|
|
|
260
398
|
try {
|
|
261
399
|
await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
|
|
262
400
|
const [quotaRes] = await Promise.all([
|
|
263
|
-
fetch(
|
|
264
|
-
fetch(
|
|
401
|
+
fetch(`http://localhost:${PORT}/quota`),
|
|
402
|
+
fetch(`http://localhost:${PORT}/health`),
|
|
265
403
|
]);
|
|
266
404
|
if (!quotaRes.ok)
|
|
267
405
|
throw new Error('Daemon unavailable');
|
|
@@ -336,6 +474,13 @@ program
|
|
|
336
474
|
.option('--plan <plan>', 'Force plan detection: pro|max5|max20|auto')
|
|
337
475
|
.option('--alerts <bool>', 'Enable/disable daemon rate limit alerts: true|false')
|
|
338
476
|
.option('--session-limit <usd>', 'Alert when a session exceeds this cost in USD (0 = disabled)')
|
|
477
|
+
.option('--kill-switch-force <bool>', 'Hard-block on kill switch instead of warning: true|false')
|
|
478
|
+
.option('--log-level <level>', 'Set log level: debug|info|warn|error (default: info)')
|
|
479
|
+
.option('--loop-threshold <number>', 'Tool calls in window to trigger loop detection (default: 8)')
|
|
480
|
+
.option('--loop-window <seconds>', 'Detection window in seconds (default: 120)')
|
|
481
|
+
.option('--alias <path=name>', 'Set a project alias: --alias "/path/to/repo=MyApp"')
|
|
482
|
+
.option('--remove-alias <path>', 'Remove a project alias')
|
|
483
|
+
.option('--webhook <url>', 'Set webhook URL for external alerts (Slack, Discord). Use "off" to disable.')
|
|
339
484
|
.action((opts) => {
|
|
340
485
|
const cfg = (0, config_1.readConfig)();
|
|
341
486
|
let changed = false;
|
|
@@ -374,6 +519,63 @@ program
|
|
|
374
519
|
else
|
|
375
520
|
console.warn(' ⚠️ session-limit must be a number >= 0 (e.g. 5 for $5)');
|
|
376
521
|
}
|
|
522
|
+
if (opts.killSwitchForce !== undefined) {
|
|
523
|
+
cfg.killSwitchForce = opts.killSwitchForce === 'true';
|
|
524
|
+
changed = true;
|
|
525
|
+
}
|
|
526
|
+
if (opts.logLevel !== undefined) {
|
|
527
|
+
if (['debug', 'info', 'warn', 'error'].includes(opts.logLevel)) {
|
|
528
|
+
cfg.logLevel = opts.logLevel;
|
|
529
|
+
changed = true;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
console.warn(' ⚠️ log-level must be: debug | info | warn | error');
|
|
533
|
+
}
|
|
534
|
+
if (opts.loopThreshold !== undefined) {
|
|
535
|
+
const v = parseInt(opts.loopThreshold, 10);
|
|
536
|
+
if (!isNaN(v) && v >= 2 && v <= 50) {
|
|
537
|
+
cfg.loopThreshold = v;
|
|
538
|
+
changed = true;
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
console.warn(' ⚠️ loopThreshold must be between 2 and 50');
|
|
542
|
+
}
|
|
543
|
+
if (opts.loopWindow !== undefined) {
|
|
544
|
+
const v = parseInt(opts.loopWindow, 10);
|
|
545
|
+
if (!isNaN(v) && v >= 10 && v <= 600) {
|
|
546
|
+
cfg.loopWindowSecs = v;
|
|
547
|
+
changed = true;
|
|
548
|
+
}
|
|
549
|
+
else
|
|
550
|
+
console.warn(' ⚠️ loopWindow must be between 10 and 600 seconds');
|
|
551
|
+
}
|
|
552
|
+
if (opts.alias !== undefined) {
|
|
553
|
+
const eqIdx = opts.alias.indexOf('=');
|
|
554
|
+
if (eqIdx < 1) {
|
|
555
|
+
console.warn(' ⚠️ format: --alias "/absolute/path=Alias Name"');
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
const projectPath = opts.alias.slice(0, eqIdx).trim();
|
|
559
|
+
const aliasName = opts.alias.slice(eqIdx + 1).trim();
|
|
560
|
+
if (!projectPath.startsWith('/') && !projectPath.match(/^[A-Z]:\\/)) {
|
|
561
|
+
console.warn(' ⚠️ project path must be absolute');
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
cfg.projectAliases = { ...cfg.projectAliases, [projectPath]: aliasName };
|
|
565
|
+
changed = true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (opts.removeAlias !== undefined) {
|
|
570
|
+
const { [opts.removeAlias]: _, ...rest } = cfg.projectAliases;
|
|
571
|
+
cfg.projectAliases = rest;
|
|
572
|
+
changed = true;
|
|
573
|
+
}
|
|
574
|
+
if (opts.webhook !== undefined) {
|
|
575
|
+
cfg.webhookUrl = opts.webhook === 'off' ? null : opts.webhook;
|
|
576
|
+
changed = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
377
579
|
if (changed) {
|
|
378
580
|
(0, config_1.writeConfig)(cfg);
|
|
379
581
|
console.log('✅ Config saved to ~/.claudestat/config.json');
|
|
@@ -397,6 +599,7 @@ program
|
|
|
397
599
|
lines.push('━'.repeat(42));
|
|
398
600
|
lines.push('');
|
|
399
601
|
lines.push(` Plan ${planColor}${planLabel.toUpperCase()}${R}`);
|
|
602
|
+
lines.push(` Port ${C}${cfg.port}${R}`);
|
|
400
603
|
lines.push(` Alerts ${alertsIcon}`);
|
|
401
604
|
lines.push('');
|
|
402
605
|
lines.push(` Kill switch ${cfg.killSwitchEnabled ? `${Y}ON${R} at ${cfg.killSwitchThreshold}%` : `${D}OFF${R}`}`);
|
|
@@ -404,6 +607,17 @@ program
|
|
|
404
607
|
lines.push(` ${bar(cfg.killSwitchThreshold)}`);
|
|
405
608
|
}
|
|
406
609
|
lines.push(` Session limit ${cfg.sessionCostLimitUsd > 0 ? `${Y}$${cfg.sessionCostLimitUsd.toFixed(2)}${R}` : `${D}OFF${R}`}`);
|
|
610
|
+
lines.push(` Kill switch mode ${cfg.killSwitchForce ? `${R}force-block${R}` : `${G}warn-only${R} ${D}(use --kill-switch-force true to hard-block)${R}`}`);
|
|
611
|
+
const logColors = { debug: D, info: '', warn: Y, error: '\x1b[31m' };
|
|
612
|
+
lines.push(` Log level ${logColors[cfg.logLevel] ?? ''}${cfg.logLevel.toUpperCase()}${R}`);
|
|
613
|
+
lines.push(` Loop detection ${C}${cfg.loopThreshold} calls${R} in ${C}${cfg.loopWindowSecs}s${R}`);
|
|
614
|
+
lines.push(` Webhook ${cfg.webhookUrl ? `${C}${cfg.webhookUrl}${R}` : `${D}off${R}`}`);
|
|
615
|
+
if (Object.keys(cfg.projectAliases).length > 0) {
|
|
616
|
+
lines.push(` Project aliases`);
|
|
617
|
+
for (const [p, a] of Object.entries(cfg.projectAliases)) {
|
|
618
|
+
lines.push(` ${D}${p}${R} → ${C}${a}${R}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
407
621
|
lines.push('');
|
|
408
622
|
lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
|
|
409
623
|
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)}`);
|
|
@@ -420,6 +634,20 @@ program
|
|
|
420
634
|
await stopDaemon().catch((e) => { console.error(`❌ ${e.message}`); process.exit(1); });
|
|
421
635
|
process.exit(0);
|
|
422
636
|
});
|
|
637
|
+
program
|
|
638
|
+
.command('resume')
|
|
639
|
+
.description('Remove the pause signal — allows Claude Code to continue after a quota warning')
|
|
640
|
+
.action(() => {
|
|
641
|
+
const signalFile = (0, paths_1.getPauseSignalFile)();
|
|
642
|
+
try {
|
|
643
|
+
fs_1.default.unlinkSync(signalFile);
|
|
644
|
+
console.log('✅ Pause signal removed — claudestat will no longer warn on tool calls');
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
console.log(' No pause signal active.');
|
|
648
|
+
}
|
|
649
|
+
process.exit(0);
|
|
650
|
+
});
|
|
423
651
|
program
|
|
424
652
|
.command('restart')
|
|
425
653
|
.description('Restart the claudestat daemon')
|
|
@@ -436,16 +664,21 @@ program
|
|
|
436
664
|
.option('--by <metric>', 'Sort by: cost, count, duration (default: cost)')
|
|
437
665
|
.option('--limit <number>', 'Number of tools to show (default: 10)')
|
|
438
666
|
.option('--days <number>', 'Look back N days (default: 30)')
|
|
667
|
+
.option('--json', 'Output as JSON')
|
|
439
668
|
.action(async (opts) => {
|
|
440
669
|
try {
|
|
441
670
|
const by = opts.by ?? 'cost';
|
|
442
671
|
const limit = opts.limit ?? 10;
|
|
443
672
|
const days = opts.days ?? 30;
|
|
444
|
-
const url = `http://localhost
|
|
673
|
+
const url = `http://localhost:${PORT}/api/top?by=${by}&limit=${limit}&days=${days}`;
|
|
445
674
|
const res = await fetch(url);
|
|
446
675
|
if (!res.ok)
|
|
447
676
|
throw new Error('Daemon unavailable');
|
|
448
677
|
const data = await res.json();
|
|
678
|
+
if (opts.json) {
|
|
679
|
+
console.log(JSON.stringify(data, null, 2));
|
|
680
|
+
process.exit(0);
|
|
681
|
+
}
|
|
449
682
|
const R = '\x1b[0m';
|
|
450
683
|
const B = '\x1b[1m';
|
|
451
684
|
const D = '\x1b[2m';
|
|
@@ -476,13 +709,16 @@ program
|
|
|
476
709
|
const dur = isOther ? '—' : fmtDur(t.totalDurationMs);
|
|
477
710
|
const cost = isOther ? fmtCost(t.estimatedCostUsd) : fmtCost(t.estimatedCostUsd);
|
|
478
711
|
const countStr = isOther ? '—' : String(t.count);
|
|
712
|
+
const avgPerCall = t.count > 0 && !isOther
|
|
713
|
+
? `$${(t.estimatedCostUsd / t.count).toFixed(4)}`
|
|
714
|
+
: '—';
|
|
479
715
|
const toolName = (t.tool.length > 18 ? t.tool.slice(0, 16) + '…' : t.tool).padEnd(18);
|
|
480
716
|
if (isOther) {
|
|
481
717
|
lines.push(` ${D}Other${R} ${'—'.padStart(20)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
482
718
|
}
|
|
483
719
|
else {
|
|
484
720
|
lines.push(` ${B}${(i + 1).toString().padStart(2)}${R} ${toolName} ${bar(val, maxVal)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
485
|
-
lines.push(` ${D}${countStr} calls · ${dur}${R}`);
|
|
721
|
+
lines.push(` ${D}${countStr} calls · ${dur} · avg/call ${avgPerCall}${R}`);
|
|
486
722
|
}
|
|
487
723
|
}
|
|
488
724
|
lines.push('');
|
|
@@ -496,6 +732,63 @@ program
|
|
|
496
732
|
process.exit(1);
|
|
497
733
|
}
|
|
498
734
|
});
|
|
735
|
+
program
|
|
736
|
+
.command('loops')
|
|
737
|
+
.description('List sessions with detected loops')
|
|
738
|
+
.option('--days <number>', 'Look back N days (default: 30)', '30')
|
|
739
|
+
.option('--limit <number>', 'Max sessions to show (default: 10)', '10')
|
|
740
|
+
.option('--json', 'Output raw JSON')
|
|
741
|
+
.action(async (opts) => {
|
|
742
|
+
try {
|
|
743
|
+
const days = Math.min(parseInt(opts.days, 10) || 30, 365);
|
|
744
|
+
const limit = Math.min(parseInt(opts.limit, 10) || 10, 50);
|
|
745
|
+
const since = Date.now() - days * 86400000;
|
|
746
|
+
const res = await fetch(`http://localhost:${PORT}/sessions`);
|
|
747
|
+
if (!res.ok)
|
|
748
|
+
throw new Error('Daemon unavailable');
|
|
749
|
+
const sessions = await res.json();
|
|
750
|
+
const withLoops = sessions
|
|
751
|
+
.filter(s => (s.loops_detected ?? 0) > 0 && s.started_at >= since)
|
|
752
|
+
.slice(0, limit);
|
|
753
|
+
if (opts.json) {
|
|
754
|
+
console.log(JSON.stringify(withLoops, null, 2));
|
|
755
|
+
process.exit(0);
|
|
756
|
+
}
|
|
757
|
+
const R = '\x1b[0m', B = '\x1b[1m', D = '\x1b[2m', Y = '\x1b[33m';
|
|
758
|
+
if (withLoops.length === 0) {
|
|
759
|
+
console.log(`\n No loops detected in the last ${days} days.\n`);
|
|
760
|
+
process.exit(0);
|
|
761
|
+
}
|
|
762
|
+
console.log(`\n${B}🔁 claudestat loops${R} ${D}last ${days} days${R}`);
|
|
763
|
+
console.log('━'.repeat(52));
|
|
764
|
+
for (const s of withLoops) {
|
|
765
|
+
const date = new Date(s.started_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
766
|
+
const project = s.project_path ? path_1.default.basename(s.project_path) : 'unknown';
|
|
767
|
+
const cost = s.total_cost_usd ? `$${s.total_cost_usd.toFixed(3)}` : '$0.000';
|
|
768
|
+
console.log(`\n ${Y}${s.loops_detected} loop(s)${R} ${B}${project}${R} ${D}${date} ${cost}${R}`);
|
|
769
|
+
try {
|
|
770
|
+
const iRes = await fetch(`http://localhost:${PORT}/intelligence/${s.id}`);
|
|
771
|
+
const intel = await iRes.json();
|
|
772
|
+
for (const loop of (intel.loops ?? []).slice(0, 3)) {
|
|
773
|
+
console.log(` ${D}↳ ${loop.toolName} ×${loop.count} in ${loop.windowMs / 1000}s${R}`);
|
|
774
|
+
if (loop.context?.repeatedFiles?.length > 0) {
|
|
775
|
+
loop.context.repeatedFiles.slice(0, 3).forEach((f) => console.log(` ${D}file: ${path_1.default.basename(f)}${R}`));
|
|
776
|
+
}
|
|
777
|
+
if (loop.context?.repeatedCommands?.length > 0) {
|
|
778
|
+
loop.context.repeatedCommands.slice(0, 2).forEach((c) => console.log(` ${D}cmd: ${c.slice(0, 60)}${R}`));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
catch { }
|
|
783
|
+
}
|
|
784
|
+
console.log('\n' + '━'.repeat(52) + '\n');
|
|
785
|
+
process.exit(0);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
console.error('\n❌ Daemon is not running. Start it with: claudestat start\n');
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
499
792
|
program
|
|
500
793
|
.command('doctor')
|
|
501
794
|
.description('Check installation health and diagnose common issues')
|
|
@@ -503,6 +796,70 @@ program
|
|
|
503
796
|
console.error('\n❌ Error:', err.message);
|
|
504
797
|
process.exit(1);
|
|
505
798
|
}));
|
|
799
|
+
program
|
|
800
|
+
.command('logs')
|
|
801
|
+
.description('Show daemon log (~/.claudestat/daemon.log)')
|
|
802
|
+
.option('-n <number>', 'Number of lines to show (default: 50)', '50')
|
|
803
|
+
.option('--follow', 'Tail the log in real time')
|
|
804
|
+
.option('--level <level>', 'Filter by minimum level: debug|info|warn|error')
|
|
805
|
+
.action((opts) => {
|
|
806
|
+
const logFile = (0, paths_1.getDaemonLogFile)();
|
|
807
|
+
if (!fs_1.default.existsSync(logFile)) {
|
|
808
|
+
console.log('\n No daemon log found. Start the daemon first: claudestat start\n');
|
|
809
|
+
process.exit(0);
|
|
810
|
+
}
|
|
811
|
+
const minRank = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
812
|
+
const levelFilter = opts.level ? (minRank[opts.level] ?? 0) : 0;
|
|
813
|
+
function filterLine(line) {
|
|
814
|
+
if (!opts.level)
|
|
815
|
+
return true;
|
|
816
|
+
const match = line.match(/\[(DEBUG|INFO|WARN|ERROR)\]/);
|
|
817
|
+
if (!match)
|
|
818
|
+
return true;
|
|
819
|
+
return (minRank[match[1].toLowerCase()] ?? 0) >= levelFilter;
|
|
820
|
+
}
|
|
821
|
+
const levelColor = {
|
|
822
|
+
DEBUG: '\x1b[2m', INFO: '\x1b[36m', WARN: '\x1b[33m', ERROR: '\x1b[31m'
|
|
823
|
+
};
|
|
824
|
+
function colorize(line) {
|
|
825
|
+
return line.replace(/\[(DEBUG|INFO|WARN|ERROR)\]/, (_, l) => `${levelColor[l] ?? ''}[${l}]\x1b[0m`);
|
|
826
|
+
}
|
|
827
|
+
if (opts.follow) {
|
|
828
|
+
const content = fs_1.default.readFileSync(logFile, 'utf8');
|
|
829
|
+
content.split('\n').filter(Boolean).filter(filterLine).slice(-20)
|
|
830
|
+
.forEach(l => console.log(colorize(l)));
|
|
831
|
+
let size = fs_1.default.statSync(logFile).size;
|
|
832
|
+
setInterval(() => {
|
|
833
|
+
try {
|
|
834
|
+
const newSize = fs_1.default.statSync(logFile).size;
|
|
835
|
+
if (newSize <= size)
|
|
836
|
+
return;
|
|
837
|
+
const buf = Buffer.alloc(newSize - size);
|
|
838
|
+
const fd = fs_1.default.openSync(logFile, 'r');
|
|
839
|
+
fs_1.default.readSync(fd, buf, 0, buf.length, size);
|
|
840
|
+
fs_1.default.closeSync(fd);
|
|
841
|
+
size = newSize;
|
|
842
|
+
buf.toString('utf8').split('\n').filter(Boolean).filter(filterLine)
|
|
843
|
+
.forEach(l => console.log(colorize(l)));
|
|
844
|
+
}
|
|
845
|
+
catch { }
|
|
846
|
+
}, 300);
|
|
847
|
+
}
|
|
848
|
+
else {
|
|
849
|
+
const n = Math.min(parseInt(opts.n, 10) || 50, 500);
|
|
850
|
+
const content = fs_1.default.readFileSync(logFile, 'utf8');
|
|
851
|
+
const lines = content.split('\n').filter(Boolean).filter(filterLine).slice(-n);
|
|
852
|
+
if (lines.length === 0) {
|
|
853
|
+
console.log(`\n No log entries${opts.level ? ` at level ≥ ${opts.level}` : ''}.\n`);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
console.log('');
|
|
857
|
+
lines.forEach(l => console.log(colorize(l)));
|
|
858
|
+
console.log('');
|
|
859
|
+
}
|
|
860
|
+
process.exit(0);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
506
863
|
program
|
|
507
864
|
.command('roast')
|
|
508
865
|
.description('Roast your Claude Code usage habits')
|
|
@@ -530,11 +887,29 @@ program
|
|
|
530
887
|
console.log('\n📊 No usage data yet — start using Claude Code and claudestat will track it.\n');
|
|
531
888
|
process.exit(0);
|
|
532
889
|
}
|
|
890
|
+
const prev = (0, insights_1.getPrevWeekInsightData)();
|
|
891
|
+
const deltaCost = prev.total_cost > 0
|
|
892
|
+
? Math.round((data.total_cost - prev.total_cost) / prev.total_cost * 100)
|
|
893
|
+
: null;
|
|
894
|
+
const deltaSessions = prev.total_sessions > 0
|
|
895
|
+
? Math.round((data.total_sessions - prev.total_sessions) / prev.total_sessions * 100)
|
|
896
|
+
: null;
|
|
533
897
|
if (opts.json) {
|
|
534
|
-
console.log(JSON.stringify(data, null, 2));
|
|
898
|
+
console.log(JSON.stringify({ current: data, prev, deltaCostPct: deltaCost, deltaSessionsPct: deltaSessions }, null, 2));
|
|
535
899
|
process.exit(0);
|
|
536
900
|
}
|
|
537
901
|
console.log((0, insights_1.renderWeeklyInsight)(data));
|
|
902
|
+
if (deltaCost !== null || deltaSessions !== null) {
|
|
903
|
+
const R = '\x1b[0m', D = '\x1b[2m', G = '\x1b[32m', Y = '\x1b[33m';
|
|
904
|
+
const sign = (n) => n >= 0 ? `+${n}` : `${n}`;
|
|
905
|
+
const color = (n) => n > 0 ? Y : G;
|
|
906
|
+
console.log(` ${D}vs last week:${R}`);
|
|
907
|
+
if (deltaCost !== null)
|
|
908
|
+
console.log(` cost ${color(deltaCost)}${sign(deltaCost)}%${R}`);
|
|
909
|
+
if (deltaSessions !== null)
|
|
910
|
+
console.log(` sessions ${sign(deltaSessions)}%`);
|
|
911
|
+
console.log('');
|
|
912
|
+
}
|
|
538
913
|
process.exit(0);
|
|
539
914
|
}
|
|
540
915
|
catch (err) {
|
package/dist/insights.d.ts
CHANGED
|
@@ -43,3 +43,4 @@ export declare function renderInsights(d: UsageInsightsData): string;
|
|
|
43
43
|
export declare function shouldShowInsight(): boolean;
|
|
44
44
|
export declare function markInsightShown(): void;
|
|
45
45
|
export declare function renderWeeklyInsight(d: WeeklyInsightData): string;
|
|
46
|
+
export declare function getPrevWeekInsightData(): WeeklyInsightData;
|
package/dist/insights.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.renderInsights = renderInsights;
|
|
|
10
10
|
exports.shouldShowInsight = shouldShowInsight;
|
|
11
11
|
exports.markInsightShown = markInsightShown;
|
|
12
12
|
exports.renderWeeklyInsight = renderWeeklyInsight;
|
|
13
|
+
exports.getPrevWeekInsightData = getPrevWeekInsightData;
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
const db_1 = require("./db");
|
|
15
16
|
const WEEK_MS = 7 * 86400000;
|
|
@@ -253,3 +254,28 @@ function renderWeeklyInsight(d) {
|
|
|
253
254
|
lines.push('');
|
|
254
255
|
return lines.join('\n');
|
|
255
256
|
}
|
|
257
|
+
function getPrevWeekInsightData() {
|
|
258
|
+
const agg = db_1.dbOps.getPrevWeekInsight();
|
|
259
|
+
const topTools = db_1.dbOps.getTopTools(14, 'cost', 1);
|
|
260
|
+
const topTool = topTools[0];
|
|
261
|
+
const totalInputWithCache = agg.input_tokens + agg.cache_read;
|
|
262
|
+
const cacheHitPct = totalInputWithCache > 0
|
|
263
|
+
? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
|
|
264
|
+
: 0;
|
|
265
|
+
return {
|
|
266
|
+
total_sessions: agg.total_sessions,
|
|
267
|
+
total_cost: agg.total_cost,
|
|
268
|
+
input_tokens: agg.input_tokens,
|
|
269
|
+
output_tokens: agg.output_tokens,
|
|
270
|
+
cache_read: agg.cache_read,
|
|
271
|
+
cache_hit_pct: cacheHitPct,
|
|
272
|
+
total_loops: agg.total_loops,
|
|
273
|
+
avg_efficiency: Math.round(agg.avg_efficiency),
|
|
274
|
+
top_tool: topTool?.tool_name ?? 'Unknown',
|
|
275
|
+
top_tool_cost_pct: agg.total_cost > 0
|
|
276
|
+
? Math.round((topTool?.total_cost_usd ?? 0) / agg.total_cost * 100)
|
|
277
|
+
: 0,
|
|
278
|
+
week_start: agg.week_start,
|
|
279
|
+
week_end: agg.week_end ?? agg.week_start,
|
|
280
|
+
};
|
|
281
|
+
}
|