@statforge/claudestat 1.2.0 → 1.2.2
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 +198 -86
- package/dist/claude-auth.d.ts +7 -0
- package/dist/claude-auth.js +12 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -0
- package/dist/daemon.js +32 -15
- package/dist/db.d.ts +18 -0
- package/dist/db.js +50 -0
- package/dist/doctor.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +164 -76
- package/dist/insights.d.ts +26 -0
- package/dist/insights.js +172 -20
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +148 -21
- package/dist/notifier.js +24 -6
- package/dist/pattern-analyzer.d.ts +1 -0
- package/dist/pattern-analyzer.js +13 -0
- package/dist/quota-tracker.d.ts +6 -0
- package/dist/quota-tracker.js +79 -32
- package/dist/roast.js +129 -40
- package/dist/routes/events.js +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
4
|
* index.ts — Entry point del CLI
|
|
@@ -27,10 +27,10 @@ const install_1 = require("./install");
|
|
|
27
27
|
const export_1 = require("./export");
|
|
28
28
|
const config_1 = require("./config");
|
|
29
29
|
const doctor_1 = require("./doctor");
|
|
30
|
-
const share_1 = require("./share");
|
|
31
30
|
const roast_1 = require("./roast");
|
|
32
31
|
const insights_1 = require("./insights");
|
|
33
32
|
const paths_1 = require("./paths");
|
|
33
|
+
const quota_tracker_1 = require("./quota-tracker");
|
|
34
34
|
const program = new commander_1.Command();
|
|
35
35
|
const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
36
36
|
const PID_FILE = (0, paths_1.getPidFile)();
|
|
@@ -83,6 +83,20 @@ async function stopDaemon() {
|
|
|
83
83
|
throw new Error(`Error stopping daemon: ${e.message}`);
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
async function checkLatestVersion() {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch('https://registry.npmjs.org/@statforge/claudestat/latest', {
|
|
89
|
+
signal: AbortSignal.timeout(2000),
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
return null;
|
|
93
|
+
const json = await res.json();
|
|
94
|
+
return json.version;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
86
100
|
// Warn if the active binary is outside the current npm global prefix (NVM conflict)
|
|
87
101
|
if (process.env.NVM_DIR || process.env.NVM_HOME) {
|
|
88
102
|
try {
|
|
@@ -92,7 +106,7 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
|
|
|
92
106
|
const refreshCmd = paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat';
|
|
93
107
|
process.stderr.write(`\x1b[33m⚠️ claudestat is running from ${runningFrom}\x1b[0m\n` +
|
|
94
108
|
` This binary may not match the active Node version (${process.version}).\n` +
|
|
95
|
-
` Fix: \x1b[36mnvm use default && npm install -g @
|
|
109
|
+
` Fix: \x1b[36mnvm use default && npm install -g @statforge/claudestat\x1b[0m\n` +
|
|
96
110
|
` Then restart your terminal or run: \x1b[36m${refreshCmd}\x1b[0m\n\n`);
|
|
97
111
|
}
|
|
98
112
|
}
|
|
@@ -100,8 +114,21 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
|
|
|
100
114
|
}
|
|
101
115
|
program
|
|
102
116
|
.name('claudestat')
|
|
103
|
-
.description('Real-time execution trace and cost intelligence for Claude Code')
|
|
117
|
+
.description('Real-time execution trace and cost intelligence for Claude Code · github.com/DeibyGS/claudestat')
|
|
104
118
|
.version(PKG_VERSION);
|
|
119
|
+
program
|
|
120
|
+
.command('version')
|
|
121
|
+
.description('Show version and check for updates')
|
|
122
|
+
.action(async () => {
|
|
123
|
+
console.log(PKG_VERSION);
|
|
124
|
+
const latest = await checkLatestVersion();
|
|
125
|
+
if (latest) {
|
|
126
|
+
const isLatest = latest === PKG_VERSION;
|
|
127
|
+
const tag = isLatest ? `\x1b[32mlatest ✓\x1b[0m` : `\x1b[33mlatest: ${latest} — run npm update\x1b[0m`;
|
|
128
|
+
console.log(` ${tag}`);
|
|
129
|
+
}
|
|
130
|
+
process.exit(0);
|
|
131
|
+
});
|
|
105
132
|
program
|
|
106
133
|
.command('start')
|
|
107
134
|
.description('Start the background daemon (receives Claude Code hook events)')
|
|
@@ -152,9 +179,9 @@ program
|
|
|
152
179
|
.command('status')
|
|
153
180
|
.description('Show current quota, cost and burn rate')
|
|
154
181
|
.option('--json', 'Output raw JSON instead of formatted text')
|
|
155
|
-
.option('--compact', 'One-line output for tmux')
|
|
156
182
|
.action(async (opts) => {
|
|
157
183
|
try {
|
|
184
|
+
await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
|
|
158
185
|
const [quotaRes, healthRes] = await Promise.all([
|
|
159
186
|
fetch('http://localhost:7337/quota'),
|
|
160
187
|
fetch('http://localhost:7337/health'),
|
|
@@ -163,13 +190,6 @@ program
|
|
|
163
190
|
throw new Error('Daemon unavailable');
|
|
164
191
|
const q = await quotaRes.json();
|
|
165
192
|
const _h = await healthRes.json().catch(() => ({}));
|
|
166
|
-
if (opts.compact) {
|
|
167
|
-
const pctCycle = q.cyclePct;
|
|
168
|
-
const cycleEmoji = pctCycle >= 95 ? '🔴' : pctCycle >= 70 ? '🟡' : '🟢';
|
|
169
|
-
const wEmoji = q.weeklyPctAll >= 95 ? '🔴' : q.weeklyPctAll >= 70 ? '🟡' : '🟢';
|
|
170
|
-
console.log(`C:${pctCycle}%${cycleEmoji} W:${q.weeklyPctAll}%${wEmoji} ${q.detectedPlan}`);
|
|
171
|
-
process.exit(0);
|
|
172
|
-
}
|
|
173
193
|
if (opts.json) {
|
|
174
194
|
console.log(JSON.stringify({
|
|
175
195
|
cyclePrompts: q.cyclePrompts,
|
|
@@ -187,33 +207,44 @@ program
|
|
|
187
207
|
process.exit(0);
|
|
188
208
|
}
|
|
189
209
|
const R = '\x1b[0m';
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
210
|
+
const B = '\x1b[1m';
|
|
211
|
+
const D = '\x1b[2m';
|
|
212
|
+
const pctBar = (pct, width = 20) => {
|
|
213
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
214
|
+
const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
|
|
215
|
+
return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
|
|
216
|
+
};
|
|
217
|
+
const resetTime = q.cycleResetAt
|
|
218
|
+
? new Date(q.cycleResetAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
|
219
|
+
: (() => {
|
|
220
|
+
const m = Math.ceil(q.cycleResetMs / 60000);
|
|
221
|
+
return m >= 60 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${m}m`;
|
|
222
|
+
})();
|
|
223
|
+
const now = new Date();
|
|
224
|
+
const daysToMonday = ((8 - now.getDay()) % 7) || 7;
|
|
225
|
+
const nextMonday = new Date(now);
|
|
226
|
+
nextMonday.setDate(now.getDate() + daysToMonday);
|
|
227
|
+
const weekReset = nextMonday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
228
|
+
const lines = [];
|
|
229
|
+
lines.push(`\n${B}📊 claudestat${R} ${D}${q.detectedPlan.toUpperCase()} plan${R}`);
|
|
230
|
+
lines.push('━'.repeat(42));
|
|
231
|
+
lines.push('');
|
|
232
|
+
lines.push(` 5h ${pctBar(q.cyclePct)} ${B}${q.cyclePct}%${R} ${D}resets ${resetTime}${R}`);
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push(` Week ${pctBar(q.weeklyPctAll)} ${B}${q.weeklyPctAll}%${R} ${D}resets ${weekReset}${R}`);
|
|
235
|
+
if (q.weeklyLimitOpus > 0) {
|
|
236
|
+
lines.push('');
|
|
237
|
+
lines.push(` ${D} ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h${R}`);
|
|
238
|
+
lines.push(` ${D} └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h${R}`);
|
|
239
|
+
}
|
|
240
|
+
if (q.burnRateTokensPerMin > 0) {
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(` 🔥 ${B}${q.burnRateTokensPerMin.toLocaleString()}${R} tok/min ${D}· ${q.cyclePrompts} prompts used${R}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
lines.push('━'.repeat(42));
|
|
246
|
+
lines.push('');
|
|
247
|
+
console.log(lines.join('\n'));
|
|
217
248
|
process.exit(0);
|
|
218
249
|
}
|
|
219
250
|
catch {
|
|
@@ -261,13 +292,38 @@ program
|
|
|
261
292
|
(0, config_1.writeConfig)(cfg);
|
|
262
293
|
console.log('✅ Config saved to ~/.claudestat/config.json');
|
|
263
294
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
295
|
+
const R = '\x1b[0m';
|
|
296
|
+
const B = '\x1b[1m';
|
|
297
|
+
const D = '\x1b[2m';
|
|
298
|
+
const G = '\x1b[32m';
|
|
299
|
+
const Y = '\x1b[33m';
|
|
300
|
+
const C = '\x1b[36m';
|
|
301
|
+
const bar = (pct, width = 20) => {
|
|
302
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
303
|
+
const color = pct >= 95 ? '\x1b[31m' : pct >= 85 ? '\x1b[33m' : '\x1b[32m';
|
|
304
|
+
return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
|
|
305
|
+
};
|
|
306
|
+
const planColor = cfg.plan === 'pro' ? G : cfg.plan === 'max5' ? C : cfg.plan === 'max20' ? '\x1b[35m' : Y;
|
|
307
|
+
const planLabel = cfg.plan ?? 'auto-detect';
|
|
308
|
+
const alertsIcon = cfg.alertsEnabled ? `${G}enabled${R}` : `${Y}disabled${R}`;
|
|
309
|
+
const lines = [];
|
|
310
|
+
lines.push(`\n${B}⚙️ claudestat config${R}`);
|
|
311
|
+
lines.push('━'.repeat(42));
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push(` Plan ${planColor}${planLabel.toUpperCase()}${R}`);
|
|
314
|
+
lines.push(` Alerts ${alertsIcon}`);
|
|
315
|
+
lines.push('');
|
|
316
|
+
lines.push(` Kill switch ${cfg.killSwitchEnabled ? `${Y}ON${R} at ${cfg.killSwitchThreshold}%` : `${D}OFF${R}`}`);
|
|
317
|
+
if (cfg.killSwitchEnabled) {
|
|
318
|
+
lines.push(` ${bar(cfg.killSwitchThreshold)}`);
|
|
319
|
+
}
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
|
|
322
|
+
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)}`);
|
|
323
|
+
lines.push('');
|
|
324
|
+
lines.push('━'.repeat(42));
|
|
325
|
+
lines.push('');
|
|
326
|
+
console.log(lines.join('\n'));
|
|
271
327
|
process.exit(0);
|
|
272
328
|
});
|
|
273
329
|
program
|
|
@@ -303,25 +359,49 @@ program
|
|
|
303
359
|
if (!res.ok)
|
|
304
360
|
throw new Error('Daemon unavailable');
|
|
305
361
|
const data = await res.json();
|
|
362
|
+
const R = '\x1b[0m';
|
|
363
|
+
const B = '\x1b[1m';
|
|
364
|
+
const D = '\x1b[2m';
|
|
306
365
|
const label = by === 'count' ? 'calls' : by === 'duration' ? 'duration' : 'est. cost';
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
366
|
+
const maxVal = Math.max(...data.tools.map((t) => by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs));
|
|
367
|
+
const bar = (val, max, width = 20) => {
|
|
368
|
+
const pct = max > 0 ? val / max * 100 : 0;
|
|
369
|
+
const filled = Math.round(pct / 100 * width);
|
|
370
|
+
const rank = data.tools.findIndex((t) => {
|
|
371
|
+
const tv = by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs;
|
|
372
|
+
return tv === val;
|
|
373
|
+
});
|
|
374
|
+
const color = rank === 0 ? '\x1b[31m' : rank <= 2 ? '\x1b[33m' : '\x1b[32m';
|
|
375
|
+
return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
|
|
376
|
+
};
|
|
377
|
+
const fmtCost = (n) => n < 0.01 ? `< $0.01` : `$${n.toFixed(2)}`;
|
|
378
|
+
const fmtDur = (ms) => ms >= 60000 ? `${(ms / 60000).toFixed(1)}m` : `${(ms / 1000).toFixed(0)}s`;
|
|
379
|
+
const fmtPct = (n) => `${Math.round(n)}%`;
|
|
380
|
+
const lines = [];
|
|
381
|
+
lines.push(`\n${B}🏆 claudestat top${R} ${D}by ${label} (last ${days} days)${R}`);
|
|
382
|
+
lines.push('━'.repeat(52));
|
|
383
|
+
lines.push('');
|
|
310
384
|
for (let i = 0; i < data.tools.length; i++) {
|
|
311
385
|
const t = data.tools[i];
|
|
312
386
|
const isOther = t.tool === 'Other';
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const cost = t.estimatedCostUsd < 0.01
|
|
318
|
-
? `$${t.estimatedCostUsd.toFixed(4)}`
|
|
319
|
-
: `$${t.estimatedCostUsd.toFixed(2)}`;
|
|
320
|
-
const pct = by === 'cost' ? `${t.pctCost}%` : isOther ? '' : `${t.pctCount}%`;
|
|
387
|
+
const val = isOther ? 0 : (by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs);
|
|
388
|
+
const pct = by === 'cost' ? t.pctCost : (isOther ? 0 : t.pctCount);
|
|
389
|
+
const dur = isOther ? '—' : fmtDur(t.totalDurationMs);
|
|
390
|
+
const cost = isOther ? fmtCost(t.estimatedCostUsd) : fmtCost(t.estimatedCostUsd);
|
|
321
391
|
const countStr = isOther ? '—' : String(t.count);
|
|
322
|
-
|
|
392
|
+
const toolName = (t.tool.length > 18 ? t.tool.slice(0, 16) + '…' : t.tool).padEnd(18);
|
|
393
|
+
if (isOther) {
|
|
394
|
+
lines.push(` ${D}Other${R} ${'—'.padStart(20)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
lines.push(` ${B}${(i + 1).toString().padStart(2)}${R} ${toolName} ${bar(val, maxVal)} ${cost.padStart(10)} ${fmtPct(pct)}`);
|
|
398
|
+
lines.push(` ${D}${countStr} calls · ${dur}${R}`);
|
|
399
|
+
}
|
|
323
400
|
}
|
|
324
|
-
|
|
401
|
+
lines.push('');
|
|
402
|
+
lines.push('━'.repeat(52));
|
|
403
|
+
lines.push('');
|
|
404
|
+
console.log(lines.join('\n'));
|
|
325
405
|
process.exit(0);
|
|
326
406
|
}
|
|
327
407
|
catch {
|
|
@@ -336,23 +416,6 @@ program
|
|
|
336
416
|
console.error('\n❌ Error:', err.message);
|
|
337
417
|
process.exit(1);
|
|
338
418
|
}));
|
|
339
|
-
program
|
|
340
|
-
.command('share [session-id]')
|
|
341
|
-
.description('Generate a shareable session card (ASCII or JSON)')
|
|
342
|
-
.option('--format <type>', 'Output format: ascii, json (default: ascii)')
|
|
343
|
-
.option('--copy', 'Copy to clipboard (macOS only)')
|
|
344
|
-
.action(async (sessionId, opts) => {
|
|
345
|
-
try {
|
|
346
|
-
const format = (opts.format ?? 'ascii');
|
|
347
|
-
const copy = !!opts.copy;
|
|
348
|
-
await (0, share_1.runShare)({ sessionId, format, copy });
|
|
349
|
-
process.exit(0);
|
|
350
|
-
}
|
|
351
|
-
catch (err) {
|
|
352
|
-
console.error('\n❌ Error:', err.message);
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
419
|
program
|
|
357
420
|
.command('roast')
|
|
358
421
|
.description('Roast your Claude Code usage habits')
|
|
@@ -392,4 +455,29 @@ program
|
|
|
392
455
|
process.exit(1);
|
|
393
456
|
}
|
|
394
457
|
});
|
|
458
|
+
program
|
|
459
|
+
.command('insights')
|
|
460
|
+
.description('Show usage insights: cost breakdown, cache savings, efficiency trend, peak hours')
|
|
461
|
+
.option('--days <number>', 'Look back N days (default 7)')
|
|
462
|
+
.option('--json', 'Output raw JSON')
|
|
463
|
+
.action((opts) => {
|
|
464
|
+
try {
|
|
465
|
+
const days = Math.max(1, Math.min(90, parseInt(opts.days ?? '7', 10) || 7));
|
|
466
|
+
const data = (0, insights_1.getUsageInsights)(days);
|
|
467
|
+
if (data.total_sessions === 0) {
|
|
468
|
+
console.log(`\n💡 No data for the last ${days} days.\n`);
|
|
469
|
+
process.exit(0);
|
|
470
|
+
}
|
|
471
|
+
if (opts.json) {
|
|
472
|
+
console.log(JSON.stringify(data, null, 2));
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
console.log((0, insights_1.renderInsights)(data));
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
console.error('\n❌ Error:', err.message);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
395
483
|
program.parse();
|
package/dist/insights.d.ts
CHANGED
|
@@ -14,6 +14,32 @@ export interface WeeklyInsightData {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function getWeeklyInsightData(days?: number): WeeklyInsightData;
|
|
16
16
|
export declare function generateTip(d: WeeklyInsightData): string;
|
|
17
|
+
export interface UsageInsightsData {
|
|
18
|
+
days: number;
|
|
19
|
+
total_sessions: number;
|
|
20
|
+
total_cost: number;
|
|
21
|
+
avg_cost_per_session: number;
|
|
22
|
+
cache_savings_usd: number;
|
|
23
|
+
cache_hit_pct: number;
|
|
24
|
+
output_input_ratio: number;
|
|
25
|
+
ratio_label: string;
|
|
26
|
+
avg_efficiency: number;
|
|
27
|
+
efficiency_delta: number;
|
|
28
|
+
total_loops: number;
|
|
29
|
+
project_costs: {
|
|
30
|
+
project: string;
|
|
31
|
+
session_count: number;
|
|
32
|
+
total_cost: number;
|
|
33
|
+
}[];
|
|
34
|
+
hour_ranges: {
|
|
35
|
+
emoji: string;
|
|
36
|
+
from: string;
|
|
37
|
+
to: string;
|
|
38
|
+
count: number;
|
|
39
|
+
}[];
|
|
40
|
+
}
|
|
41
|
+
export declare function getUsageInsights(days?: number): UsageInsightsData;
|
|
42
|
+
export declare function renderInsights(d: UsageInsightsData): string;
|
|
17
43
|
export declare function shouldShowInsight(): boolean;
|
|
18
44
|
export declare function markInsightShown(): void;
|
|
19
45
|
export declare function renderWeeklyInsight(d: WeeklyInsightData): string;
|
package/dist/insights.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.getWeeklyInsightData = getWeeklyInsightData;
|
|
4
7
|
exports.generateTip = generateTip;
|
|
8
|
+
exports.getUsageInsights = getUsageInsights;
|
|
9
|
+
exports.renderInsights = renderInsights;
|
|
5
10
|
exports.shouldShowInsight = shouldShowInsight;
|
|
6
11
|
exports.markInsightShown = markInsightShown;
|
|
7
12
|
exports.renderWeeklyInsight = renderWeeklyInsight;
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
8
14
|
const db_1 = require("./db");
|
|
9
15
|
const WEEK_MS = 7 * 86400000;
|
|
10
16
|
const META_KEY = 'last_insight_at';
|
|
@@ -58,6 +64,145 @@ function generateTip(d) {
|
|
|
58
64
|
}
|
|
59
65
|
return 'Enable quota alerts with "claudestat config --alerts true" to avoid surprise limits';
|
|
60
66
|
}
|
|
67
|
+
function getUsageInsights(days = 7) {
|
|
68
|
+
const agg = db_1.dbOps.getWeeklyInsight(days);
|
|
69
|
+
const aggTotal = db_1.dbOps.getWeeklyInsight(days * 2);
|
|
70
|
+
const prevSessions = aggTotal.total_sessions - agg.total_sessions;
|
|
71
|
+
const effDelta = prevSessions > 2
|
|
72
|
+
? Math.round(agg.avg_efficiency -
|
|
73
|
+
(aggTotal.avg_efficiency * aggTotal.total_sessions - agg.avg_efficiency * agg.total_sessions) / prevSessions)
|
|
74
|
+
: -999;
|
|
75
|
+
const totalInputWithCache = agg.input_tokens + agg.cache_read;
|
|
76
|
+
const cacheHitPct = totalInputWithCache > 0
|
|
77
|
+
? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
|
|
78
|
+
: 0;
|
|
79
|
+
const outputInputRatio = agg.input_tokens > 0
|
|
80
|
+
? parseFloat((agg.output_tokens / agg.input_tokens).toFixed(1))
|
|
81
|
+
: 0;
|
|
82
|
+
const ratioLabel = outputInputRatio > 10 ? 'cache-heavy workload'
|
|
83
|
+
: outputInputRatio > 5 ? 'generation-heavy'
|
|
84
|
+
: outputInputRatio > 2 ? 'balanced'
|
|
85
|
+
: 'reading-heavy';
|
|
86
|
+
const CACHE_READ_PRICE = {
|
|
87
|
+
'claude-haiku-4-5-20251001': 0.30 / 1000000,
|
|
88
|
+
'claude-sonnet-4-6': 2.70 / 1000000,
|
|
89
|
+
'claude-opus-4-6': 3.00 / 1000000,
|
|
90
|
+
};
|
|
91
|
+
const DEFAULT_CACHE_PRICE = 2.70 / 1000000;
|
|
92
|
+
const cacheByModel = db_1.dbOps.getCacheReadByModel(days);
|
|
93
|
+
const cacheSavings = cacheByModel.reduce((total, row) => {
|
|
94
|
+
const price = CACHE_READ_PRICE[row.model] ?? DEFAULT_CACHE_PRICE;
|
|
95
|
+
return total + row.cache_read * price;
|
|
96
|
+
}, 0);
|
|
97
|
+
return {
|
|
98
|
+
days,
|
|
99
|
+
total_sessions: agg.total_sessions,
|
|
100
|
+
total_cost: agg.total_cost,
|
|
101
|
+
avg_cost_per_session: agg.total_sessions > 0 ? agg.total_cost / agg.total_sessions : 0,
|
|
102
|
+
cache_savings_usd: cacheSavings,
|
|
103
|
+
cache_hit_pct: cacheHitPct,
|
|
104
|
+
output_input_ratio: outputInputRatio,
|
|
105
|
+
ratio_label: ratioLabel,
|
|
106
|
+
avg_efficiency: Math.round(agg.avg_efficiency),
|
|
107
|
+
efficiency_delta: effDelta,
|
|
108
|
+
total_loops: agg.total_loops,
|
|
109
|
+
project_costs: db_1.dbOps.getProjectCosts(days),
|
|
110
|
+
hour_ranges: (() => {
|
|
111
|
+
const hours = db_1.dbOps.getHourlyDistribution(days);
|
|
112
|
+
const dawn = hours.filter(h => h.hour >= 0 && h.hour <= 5).reduce((s, h) => s + h.session_count, 0);
|
|
113
|
+
const morn = hours.filter(h => h.hour >= 6 && h.hour <= 11).reduce((s, h) => s + h.session_count, 0);
|
|
114
|
+
const after = hours.filter(h => h.hour >= 12 && h.hour <= 17).reduce((s, h) => s + h.session_count, 0);
|
|
115
|
+
const night = hours.filter(h => h.hour >= 18 && h.hour <= 23).reduce((s, h) => s + h.session_count, 0);
|
|
116
|
+
return [
|
|
117
|
+
{ emoji: '🌙', from: '00:00', to: '05:59', count: dawn },
|
|
118
|
+
{ emoji: '🌅', from: '06:00', to: '11:59', count: morn },
|
|
119
|
+
{ emoji: '☀️', from: '12:00', to: '17:59', count: after },
|
|
120
|
+
{ emoji: '🌆', from: '18:00', to: '23:59', count: night },
|
|
121
|
+
].filter(r => r.count > 0);
|
|
122
|
+
})(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function renderInsights(d) {
|
|
126
|
+
const R = '\x1b[0m';
|
|
127
|
+
const B = '\x1b[1m';
|
|
128
|
+
const D = '\x1b[2m';
|
|
129
|
+
const G = '\x1b[32m';
|
|
130
|
+
const Y = '\x1b[33m';
|
|
131
|
+
const C = '\x1b[36m';
|
|
132
|
+
const M = '\x1b[35m';
|
|
133
|
+
const bar = (pct, width = 20) => '█'.repeat(Math.round(pct / 100 * width)) + '░'.repeat(width - Math.round(pct / 100 * width));
|
|
134
|
+
const fmtDollar = (n) => n < 0.01 ? '< $0.01' : `$${n.toFixed(2)}`;
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`\n${B}💡 claudestat insights${R} ${D}last ${d.days} days${R}`);
|
|
137
|
+
lines.push('━'.repeat(44));
|
|
138
|
+
// Cost summary
|
|
139
|
+
lines.push(`\n 💰 ${B}${fmtDollar(d.avg_cost_per_session)}/session${R} · ${d.total_sessions} sessions · ${fmtDollar(d.total_cost)} total`);
|
|
140
|
+
// Top projects
|
|
141
|
+
if (d.project_costs.length > 0) {
|
|
142
|
+
lines.push(`\n 🗂 Top projects`);
|
|
143
|
+
const topTotal = d.project_costs.reduce((s, p) => s + p.total_cost, 0);
|
|
144
|
+
const shown = d.project_costs.slice(0, 4);
|
|
145
|
+
const otherCost = d.total_cost - shown.reduce((s, p) => s + p.total_cost, 0);
|
|
146
|
+
for (let i = 0; i < shown.length; i++) {
|
|
147
|
+
const p = shown[i];
|
|
148
|
+
const pct = topTotal > 0 ? Math.round(p.total_cost / topTotal * 100) : 0;
|
|
149
|
+
const name = path_1.default.basename(p.project).slice(0, 14).padEnd(14);
|
|
150
|
+
lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(p.total_cost)} ${D}${pct}%${R}`);
|
|
151
|
+
if (i < shown.length - 1 || (otherCost > 0.01 && d.project_costs.length > 4))
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
if (otherCost > 0.01 && d.project_costs.length > 4) {
|
|
155
|
+
const pct = topTotal > 0 ? Math.round(otherCost / topTotal * 100) : 0;
|
|
156
|
+
lines.push(` ${'other'.padEnd(14)} ${bar(pct)} ${fmtDollar(otherCost)} ${D}${pct}%${R}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Cache savings
|
|
160
|
+
const savingsLabel = d.cache_savings_usd >= 0.01
|
|
161
|
+
? `${G}~${fmtDollar(d.cache_savings_usd)}${R} saved`
|
|
162
|
+
: 'no savings yet';
|
|
163
|
+
lines.push(`\n ⚡ Cache ${savingsLabel} · ${d.cache_hit_pct}% hit rate`);
|
|
164
|
+
// Output/input ratio
|
|
165
|
+
lines.push(`\n 📊 ${B}${d.output_input_ratio}×${R} output/input · ${D}${d.ratio_label}${R}`);
|
|
166
|
+
// Efficiency trend
|
|
167
|
+
let effTrend = '';
|
|
168
|
+
if (d.efficiency_delta !== -999) {
|
|
169
|
+
const arrow = d.efficiency_delta > 0
|
|
170
|
+
? `${G}↑ +${d.efficiency_delta}${R}`
|
|
171
|
+
: d.efficiency_delta < 0 ? `${Y}↓ ${d.efficiency_delta}${R}` : '→ same';
|
|
172
|
+
effTrend = ` ${arrow} vs prev period`;
|
|
173
|
+
}
|
|
174
|
+
const loopLabel = d.total_loops > 0 ? ` · ${Y}${d.total_loops} loops${R}` : '';
|
|
175
|
+
lines.push(`\n 📈 Efficiency ${B}${d.avg_efficiency}/100${R}${effTrend}${loopLabel}`);
|
|
176
|
+
// Activity by time range
|
|
177
|
+
if (d.hour_ranges.length > 0) {
|
|
178
|
+
lines.push(`\n ⏰ Activity by time of day`);
|
|
179
|
+
const maxCount = Math.max(...d.hour_ranges.map(r => r.count));
|
|
180
|
+
for (let i = 0; i < d.hour_ranges.length; i++) {
|
|
181
|
+
const r = d.hour_ranges[i];
|
|
182
|
+
const pct = maxCount > 0 ? Math.round(r.count / maxCount * 100) : 0;
|
|
183
|
+
lines.push(` ${r.emoji} ${D}${r.from}–${r.to}${R} ${M}${bar(pct)}${R} ${D}${r.count} sessions${R}`);
|
|
184
|
+
if (i < d.hour_ranges.length - 1)
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Model breakdown
|
|
189
|
+
const models = db_1.dbOps.getModelBreakdown(d.days);
|
|
190
|
+
const modelsWithCost = models.filter(m => m.total_cost > 0);
|
|
191
|
+
if (modelsWithCost.length >= 2) {
|
|
192
|
+
lines.push(`\n 🤖 Models`);
|
|
193
|
+
for (let i = 0; i < modelsWithCost.length; i++) {
|
|
194
|
+
const m = modelsWithCost[i];
|
|
195
|
+
const pct = d.total_cost > 0 ? Math.round(m.total_cost / d.total_cost * 100) : 0;
|
|
196
|
+
const rawName = (m.model ?? 'unknown').replace(/^<|>$/g, '');
|
|
197
|
+
const name = rawName.slice(0, 28).padEnd(28);
|
|
198
|
+
lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(m.total_cost)} ${D}${pct}% · ${m.session_count} sessions${R}`);
|
|
199
|
+
if (i < modelsWithCost.length - 1)
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
lines.push('\n' + '━'.repeat(44) + '\n');
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
61
206
|
function shouldShowInsight() {
|
|
62
207
|
const last = db_1.dbOps.getMeta(META_KEY);
|
|
63
208
|
if (!last)
|
|
@@ -75,31 +220,38 @@ function renderWeeklyInsight(d) {
|
|
|
75
220
|
return `${Math.round(n / 1000)}K`;
|
|
76
221
|
return n.toString();
|
|
77
222
|
};
|
|
78
|
-
const fmtCost = (n) => `$${n.toFixed(2)}`;
|
|
79
|
-
const fmtDate = (ts) => {
|
|
80
|
-
const d = new Date(ts);
|
|
81
|
-
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
82
|
-
};
|
|
83
223
|
const R = '\x1b[0m';
|
|
84
224
|
const B = '\x1b[1m';
|
|
85
225
|
const D = '\x1b[2m';
|
|
226
|
+
const G = '\x1b[32m';
|
|
227
|
+
const Y = '\x1b[33m';
|
|
86
228
|
const C = '\x1b[36m';
|
|
229
|
+
const bar = (pct, width = 20) => {
|
|
230
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
231
|
+
const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
|
|
232
|
+
return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
|
|
233
|
+
};
|
|
234
|
+
const fmtDate = (ts) => {
|
|
235
|
+
const dt = new Date(ts);
|
|
236
|
+
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
237
|
+
};
|
|
87
238
|
const lines = [];
|
|
88
|
-
lines.push(`\n${B}📊 claudestat weekly
|
|
89
|
-
lines.push(
|
|
90
|
-
lines.push(
|
|
91
|
-
lines.push(`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
lines.push(
|
|
102
|
-
lines.push(
|
|
239
|
+
lines.push(`\n${B}📊 claudestat weekly${R} ${D}${fmtDate(d.week_start)} – ${fmtDate(d.week_end)}${R}`);
|
|
240
|
+
lines.push('━'.repeat(44));
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(` 💰 ${B}$${d.total_cost.toFixed(2)}${R} total · ${B}${d.total_sessions}${R} sessions · ${B}${d.total_loops}${R} loops`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push(` 🔧 Top tool ${B}${d.top_tool}${R} ${D}${d.top_tool_cost_pct}% of cost${R}`);
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(` 📈 Efficiency ${bar(d.avg_efficiency)} ${B}${d.avg_efficiency}/100${R}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push(` 💾 Cache hit ${bar(d.cache_hit_pct)} ${B}${d.cache_hit_pct}%${R}`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(` 📦 Tokens ${D}${fmtTok(d.input_tokens)} in + ${fmtTok(d.output_tokens)} out${R}`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push(` ${C}⚡ Tip:${R} ${generateTip(d)}`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push('━'.repeat(44));
|
|
103
255
|
lines.push('');
|
|
104
256
|
return lines.join('\n');
|
|
105
257
|
}
|
package/dist/mcp-server.d.ts
CHANGED