@statforge/claudestat 1.6.1 → 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 +36 -3
- package/dashboard/dist/assets/AnalyticsView-DDGLDoCN.js +7 -0
- package/dashboard/dist/assets/HistoryView-DkPfrNrv.js +1 -0
- package/dashboard/dist/assets/LineChart-BOWYkkEW.js +2 -0
- package/dashboard/dist/assets/ProjectsView-VRoRiEL4.js +6 -0
- package/dashboard/dist/assets/SystemView-B2zbIxhY.js +1 -0
- package/dashboard/dist/assets/TopView-C2qdsy0Y.js +1 -0
- package/dashboard/dist/assets/index-CMhe3KaT.js +84 -0
- package/dashboard/dist/assets/shared-BbBtsdh1.js +1 -0
- package/dashboard/dist/assets/{vendor-lucide-Cym0q5l_.js → vendor-lucide-ClCW-axQ.js} +79 -64
- package/dashboard/dist/assets/{vendor-react-B_Jzs0gY.js → vendor-react-gHSHIE2L.js} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/dist/config.d.ts +7 -0
- package/dist/config.js +36 -0
- package/dist/daemon.js +113 -9
- package/dist/db.d.ts +87 -2
- package/dist/db.js +325 -65
- package/dist/doctor.js +21 -3
- package/dist/enricher.d.ts +3 -2
- package/dist/enricher.js +10 -5
- package/dist/export.d.ts +2 -1
- package/dist/export.js +41 -6
- package/dist/index.js +406 -20
- package/dist/insights.d.ts +1 -0
- package/dist/insights.js +26 -0
- package/dist/install.js +28 -1
- package/dist/intelligence.d.ts +66 -4
- package/dist/intelligence.js +205 -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 +23 -0
- package/dist/paths.js +42 -0
- package/dist/pricing.d.ts +2 -0
- package/dist/pricing.js +12 -1
- package/dist/routes/events.js +136 -5
- package/dist/routes/helpers.d.ts +5 -0
- package/dist/routes/helpers.js +21 -1
- package/dist/routes/history.js +6 -2
- package/dist/routes/intents.d.ts +1 -0
- package/dist/routes/intents.js +155 -0
- package/dist/routes/misc.js +150 -4
- package/dist/routes/opencode-reader.js +39 -3
- package/dist/routes/projects.js +19 -1
- package/dist/routes/replay.d.ts +1 -0
- package/dist/routes/replay.js +29 -0
- package/dist/routes/reports.js +7 -0
- package/dist/routes/top.js +8 -1
- package/dist/service.js +11 -0
- package/dist/watchers/adapter.d.ts +1 -0
- package/dist/watchers/claude-code.d.ts +16 -1
- package/dist/watchers/claude-code.js +201 -76
- package/dist/watchers/opencode.d.ts +1 -0
- package/dist/watchers/opencode.js +152 -14
- package/hooks/event.js +44 -26
- package/package.json +1 -1
- package/dashboard/dist/assets/AnalyticsView-5bUM3UHp.js +0 -8
- package/dashboard/dist/assets/HistoryView-C-AsEqos.js +0 -1
- package/dashboard/dist/assets/ProjectsView-D9bZBdY2.js +0 -6
- package/dashboard/dist/assets/SystemView-DIYDCCF3.js +0 -1
- package/dashboard/dist/assets/TopView-DhdLpsiA.js +0 -1
- package/dashboard/dist/assets/index-DgbWvj42.js +0 -84
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];
|
|
@@ -64,7 +107,7 @@ if (!SKIP_UPDATE_NOTICE.has(subcommand)) {
|
|
|
64
107
|
}
|
|
65
108
|
const _exit = process.exit.bind(process);
|
|
66
109
|
process.exit = ((code) => {
|
|
67
|
-
if ((code ?? 0) === 0 && cachedLatest && cachedLatest
|
|
110
|
+
if ((code ?? 0) === 0 && cachedLatest && semverGt(cachedLatest, PKG_VERSION)) {
|
|
68
111
|
console.log(`\n ✦ Update available: ${PKG_VERSION} → ${cachedLatest}`);
|
|
69
112
|
console.log(` Run: npm install -g @statforge/claudestat\n`);
|
|
70
113
|
}
|
|
@@ -79,7 +122,7 @@ function spawnDaemon() {
|
|
|
79
122
|
});
|
|
80
123
|
child.unref();
|
|
81
124
|
console.log(`✅ claudestat daemon started (pid ${child.pid})`);
|
|
82
|
-
console.log(` Dashboard → http://localhost
|
|
125
|
+
console.log(` Dashboard → http://localhost:${PORT}`);
|
|
83
126
|
}
|
|
84
127
|
function removePidFile() {
|
|
85
128
|
try {
|
|
@@ -89,7 +132,7 @@ function removePidFile() {
|
|
|
89
132
|
}
|
|
90
133
|
async function stopDaemon() {
|
|
91
134
|
try {
|
|
92
|
-
const res = await fetch(
|
|
135
|
+
const res = await fetch(`http://localhost:${PORT}/shutdown`, {
|
|
93
136
|
method: 'POST',
|
|
94
137
|
signal: AbortSignal.timeout(2000),
|
|
95
138
|
});
|
|
@@ -166,18 +209,96 @@ program
|
|
|
166
209
|
}
|
|
167
210
|
process.exit(0);
|
|
168
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
|
+
});
|
|
169
272
|
program
|
|
170
273
|
.command('start')
|
|
171
274
|
.description('Start the background daemon (receives Claude Code hook events)')
|
|
172
275
|
.option('--watchdog', 'Auto-restart daemon if it crashes')
|
|
173
|
-
.
|
|
276
|
+
.option('--wait', 'Wait until daemon responds on /health before returning (max 10s)')
|
|
277
|
+
.action(async (opts) => {
|
|
174
278
|
if (process.env.CLAUDESTAT_DAEMON) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
279
|
+
const { startDaemon } = require('./daemon');
|
|
280
|
+
startDaemon();
|
|
281
|
+
if (opts.watchdog) {
|
|
282
|
+
const { startWatchdog } = require('./watchdog');
|
|
283
|
+
startWatchdog();
|
|
284
|
+
}
|
|
178
285
|
}
|
|
179
286
|
else {
|
|
180
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
|
+
}
|
|
181
302
|
process.exit(0);
|
|
182
303
|
}
|
|
183
304
|
});
|
|
@@ -192,7 +313,21 @@ program
|
|
|
192
313
|
.command('setup')
|
|
193
314
|
.description('One-command setup: install hooks + register daemon as system service (auto-starts on login)')
|
|
194
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)')
|
|
195
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
|
+
}
|
|
196
331
|
if (opts.uninstall) {
|
|
197
332
|
console.log('Uninstalling claudestat...');
|
|
198
333
|
(0, service_1.uninstallService)();
|
|
@@ -201,10 +336,23 @@ program
|
|
|
201
336
|
console.log('✅ claudestat fully removed');
|
|
202
337
|
process.exit(0);
|
|
203
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
|
+
}
|
|
204
352
|
// 1. Wizard: Node check + plan + config + hooks + MCP
|
|
205
353
|
await (0, install_1.runWizard)();
|
|
206
354
|
// 2. Start daemon now
|
|
207
|
-
const daemonRunning = await fetch(
|
|
355
|
+
const daemonRunning = await fetch(`http://localhost:${PORT}/health`, {
|
|
208
356
|
signal: AbortSignal.timeout(2000),
|
|
209
357
|
}).then(r => r.ok).catch(() => false);
|
|
210
358
|
if (!daemonRunning) {
|
|
@@ -212,7 +360,7 @@ program
|
|
|
212
360
|
}
|
|
213
361
|
else {
|
|
214
362
|
console.log('✅ Daemon already running');
|
|
215
|
-
console.log(
|
|
363
|
+
console.log(` Dashboard → http://localhost:${PORT}`);
|
|
216
364
|
}
|
|
217
365
|
console.log('\n Run \x1b[36mclaudestat watch\x1b[0m to see live activity');
|
|
218
366
|
process.exit(0);
|
|
@@ -227,15 +375,16 @@ program
|
|
|
227
375
|
.action(() => { (0, install_1.uninstallHooks)(); process.exit(0); });
|
|
228
376
|
program
|
|
229
377
|
.command('export [format]')
|
|
230
|
-
.description('Export session data (json | csv, default: json). Max 500 sessions.')
|
|
378
|
+
.description('Export session data (json | csv | markdown, default: json). Max 500 sessions.')
|
|
231
379
|
.option('--from <date>', 'Start date YYYY-MM-DD (inclusive)')
|
|
232
380
|
.option('--to <date>', 'End date YYYY-MM-DD (inclusive)')
|
|
381
|
+
.option('--since <period>', 'Shorthand: 7d, 30d, 90d (overrides --from)')
|
|
233
382
|
.option('--project <name>', 'Filter by project path (case-insensitive substring)')
|
|
234
|
-
.option('--output <path>', 'Write to file
|
|
383
|
+
.option('--output <path>', 'Write to file (default: stdout)')
|
|
235
384
|
.action((format, opts) => {
|
|
236
385
|
const fmt = (format ?? 'json').toLowerCase();
|
|
237
|
-
if (
|
|
238
|
-
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"');
|
|
239
388
|
process.exit(1);
|
|
240
389
|
}
|
|
241
390
|
(0, export_1.runExport)({ format: fmt, ...opts });
|
|
@@ -249,8 +398,8 @@ program
|
|
|
249
398
|
try {
|
|
250
399
|
await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
|
|
251
400
|
const [quotaRes] = await Promise.all([
|
|
252
|
-
fetch(
|
|
253
|
-
fetch(
|
|
401
|
+
fetch(`http://localhost:${PORT}/quota`),
|
|
402
|
+
fetch(`http://localhost:${PORT}/health`),
|
|
254
403
|
]);
|
|
255
404
|
if (!quotaRes.ok)
|
|
256
405
|
throw new Error('Daemon unavailable');
|
|
@@ -325,6 +474,13 @@ program
|
|
|
325
474
|
.option('--plan <plan>', 'Force plan detection: pro|max5|max20|auto')
|
|
326
475
|
.option('--alerts <bool>', 'Enable/disable daemon rate limit alerts: true|false')
|
|
327
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.')
|
|
328
484
|
.action((opts) => {
|
|
329
485
|
const cfg = (0, config_1.readConfig)();
|
|
330
486
|
let changed = false;
|
|
@@ -363,6 +519,63 @@ program
|
|
|
363
519
|
else
|
|
364
520
|
console.warn(' ⚠️ session-limit must be a number >= 0 (e.g. 5 for $5)');
|
|
365
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
|
+
}
|
|
366
579
|
if (changed) {
|
|
367
580
|
(0, config_1.writeConfig)(cfg);
|
|
368
581
|
console.log('✅ Config saved to ~/.claudestat/config.json');
|
|
@@ -386,6 +599,7 @@ program
|
|
|
386
599
|
lines.push('━'.repeat(42));
|
|
387
600
|
lines.push('');
|
|
388
601
|
lines.push(` Plan ${planColor}${planLabel.toUpperCase()}${R}`);
|
|
602
|
+
lines.push(` Port ${C}${cfg.port}${R}`);
|
|
389
603
|
lines.push(` Alerts ${alertsIcon}`);
|
|
390
604
|
lines.push('');
|
|
391
605
|
lines.push(` Kill switch ${cfg.killSwitchEnabled ? `${Y}ON${R} at ${cfg.killSwitchThreshold}%` : `${D}OFF${R}`}`);
|
|
@@ -393,6 +607,17 @@ program
|
|
|
393
607
|
lines.push(` ${bar(cfg.killSwitchThreshold)}`);
|
|
394
608
|
}
|
|
395
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
|
+
}
|
|
396
621
|
lines.push('');
|
|
397
622
|
lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
|
|
398
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)}`);
|
|
@@ -409,6 +634,20 @@ program
|
|
|
409
634
|
await stopDaemon().catch((e) => { console.error(`❌ ${e.message}`); process.exit(1); });
|
|
410
635
|
process.exit(0);
|
|
411
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
|
+
});
|
|
412
651
|
program
|
|
413
652
|
.command('restart')
|
|
414
653
|
.description('Restart the claudestat daemon')
|
|
@@ -425,16 +664,21 @@ program
|
|
|
425
664
|
.option('--by <metric>', 'Sort by: cost, count, duration (default: cost)')
|
|
426
665
|
.option('--limit <number>', 'Number of tools to show (default: 10)')
|
|
427
666
|
.option('--days <number>', 'Look back N days (default: 30)')
|
|
667
|
+
.option('--json', 'Output as JSON')
|
|
428
668
|
.action(async (opts) => {
|
|
429
669
|
try {
|
|
430
670
|
const by = opts.by ?? 'cost';
|
|
431
671
|
const limit = opts.limit ?? 10;
|
|
432
672
|
const days = opts.days ?? 30;
|
|
433
|
-
const url = `http://localhost
|
|
673
|
+
const url = `http://localhost:${PORT}/api/top?by=${by}&limit=${limit}&days=${days}`;
|
|
434
674
|
const res = await fetch(url);
|
|
435
675
|
if (!res.ok)
|
|
436
676
|
throw new Error('Daemon unavailable');
|
|
437
677
|
const data = await res.json();
|
|
678
|
+
if (opts.json) {
|
|
679
|
+
console.log(JSON.stringify(data, null, 2));
|
|
680
|
+
process.exit(0);
|
|
681
|
+
}
|
|
438
682
|
const R = '\x1b[0m';
|
|
439
683
|
const B = '\x1b[1m';
|
|
440
684
|
const D = '\x1b[2m';
|
|
@@ -465,13 +709,16 @@ program
|
|
|
465
709
|
const dur = isOther ? '—' : fmtDur(t.totalDurationMs);
|
|
466
710
|
const cost = isOther ? fmtCost(t.estimatedCostUsd) : fmtCost(t.estimatedCostUsd);
|
|
467
711
|
const countStr = isOther ? '—' : String(t.count);
|
|
712
|
+
const avgPerCall = t.count > 0 && !isOther
|
|
713
|
+
? `$${(t.estimatedCostUsd / t.count).toFixed(4)}`
|
|
714
|
+
: '—';
|
|
468
715
|
const toolName = (t.tool.length > 18 ? t.tool.slice(0, 16) + '…' : t.tool).padEnd(18);
|
|
469
716
|
if (isOther) {
|
|
470
717
|
lines.push(` ${D}Other${R} ${'—'.padStart(20)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
471
718
|
}
|
|
472
719
|
else {
|
|
473
720
|
lines.push(` ${B}${(i + 1).toString().padStart(2)}${R} ${toolName} ${bar(val, maxVal)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
474
|
-
lines.push(` ${D}${countStr} calls · ${dur}${R}`);
|
|
721
|
+
lines.push(` ${D}${countStr} calls · ${dur} · avg/call ${avgPerCall}${R}`);
|
|
475
722
|
}
|
|
476
723
|
}
|
|
477
724
|
lines.push('');
|
|
@@ -485,6 +732,63 @@ program
|
|
|
485
732
|
process.exit(1);
|
|
486
733
|
}
|
|
487
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
|
+
});
|
|
488
792
|
program
|
|
489
793
|
.command('doctor')
|
|
490
794
|
.description('Check installation health and diagnose common issues')
|
|
@@ -492,6 +796,70 @@ program
|
|
|
492
796
|
console.error('\n❌ Error:', err.message);
|
|
493
797
|
process.exit(1);
|
|
494
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
|
+
});
|
|
495
863
|
program
|
|
496
864
|
.command('roast')
|
|
497
865
|
.description('Roast your Claude Code usage habits')
|
|
@@ -519,11 +887,29 @@ program
|
|
|
519
887
|
console.log('\n📊 No usage data yet — start using Claude Code and claudestat will track it.\n');
|
|
520
888
|
process.exit(0);
|
|
521
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;
|
|
522
897
|
if (opts.json) {
|
|
523
|
-
console.log(JSON.stringify(data, null, 2));
|
|
898
|
+
console.log(JSON.stringify({ current: data, prev, deltaCostPct: deltaCost, deltaSessionsPct: deltaSessions }, null, 2));
|
|
524
899
|
process.exit(0);
|
|
525
900
|
}
|
|
526
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
|
+
}
|
|
527
913
|
process.exit(0);
|
|
528
914
|
}
|
|
529
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
|
+
}
|