@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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  /**
3
3
  * index.ts — Entry point del CLI
4
4
  *
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 @deibygs/claudestat\x1b[0m\n` +
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 pctColor = q.cyclePct >= 95 ? '\x1b[31m'
191
- : q.cyclePct >= 85 ? '\x1b[33m'
192
- : q.cyclePct >= 70 ? '\x1b[33m'
193
- : '\x1b[32m';
194
- const resetMin = Math.ceil(q.cycleResetMs / 60000);
195
- const resetLabel = resetMin >= 60
196
- ? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
197
- : `${resetMin}m`;
198
- const burnRow = q.burnRateTokensPerMin > 0
199
- ? ` Burn rate ${q.burnRateTokensPerMin.toLocaleString()} tok/min\n`
200
- : '';
201
- const weeklyTotalHours = q.weeklyHoursSonnet + q.weeklyHoursOpus;
202
- const weeklyLimitTotal = q.weeklyLimitSonnet + q.weeklyLimitOpus;
203
- const weeklyPctColor = q.weeklyPctAll >= 95 ? '\x1b[31m'
204
- : q.weeklyPctAll >= 70 ? '\x1b[33m'
205
- : '\x1b[32m';
206
- console.log(`\n📊 claudestat status\n` +
207
- `──────────────────────────────────────────\n` +
208
- ` Quota 5h ${pctColor}${q.cyclePrompts}/${q.cycleLimit} prompts (${q.cyclePct}%)${R} │ resets in ${resetLabel}\n` +
209
- ` Plan ${q.detectedPlan.toUpperCase()}\n` +
210
- ` Weekly ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${weeklyPctColor}${q.weeklyPctAll}%${R}) this week\n` +
211
- (q.weeklyLimitOpus > 0
212
- ? ` ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h\n` +
213
- ` └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h\n`
214
- : '') +
215
- `${burnRow}` +
216
- `──────────────────────────────────────────\n`);
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
- // Always show current config
265
- console.log('\n📋 Current config:');
266
- console.log(` killSwitchEnabled: ${cfg.killSwitchEnabled}`);
267
- console.log(` killSwitchThreshold: ${cfg.killSwitchThreshold}%`);
268
- console.log(` warnThresholds: ${cfg.warnThresholds.join('%, ')}%`);
269
- console.log(` alertsEnabled: ${cfg.alertsEnabled}`);
270
- console.log(` plan: ${cfg.plan ?? 'auto-detect'}\n`);
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
- console.log(`\n🏆 claudestat top by ${label} (last ${days} days)\n`);
308
- console.log(' # Tool Calls Duration Est. Cost %');
309
- console.log(' ── ───────────────── ──────── ───────────── ───────── ────');
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 dur = isOther ? ''
314
- : t.totalDurationMs >= 60000
315
- ? `${(t.totalDurationMs / 60000).toFixed(1)}m`
316
- : `${(t.totalDurationMs / 1000).toFixed(0)}s`;
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
- console.log(` ${(i + 1).toString().padStart(2)} ${t.tool.padEnd(18)} ${countStr.padStart(8)} ${dur.padStart(13)} ${cost.padStart(9)} ${pct.padStart(4)}`);
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
- console.log();
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();
@@ -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 insight${R} ${D}(${fmtDate(d.week_start)} – ${fmtDate(d.week_end)})${R}`);
89
- lines.push(`${''.repeat(60)}`);
90
- lines.push(` Sessions: ${d.total_sessions} · Cost: ${fmtCost(d.total_cost)} · Loops: ${d.total_loops}`);
91
- lines.push(` Top tool: ${d.top_tool} (${d.top_tool_cost_pct}% of cost) · Efficiency: ${d.avg_efficiency}/100`);
92
- const cacheLabel = d.total_sessions > 0
93
- ? ` · Cache hit: ${d.cache_hit_pct}%`
94
- : '';
95
- const tokLabel = d.input_tokens + d.output_tokens > 0
96
- ? ` · Tokens: ${fmtTok(d.input_tokens)}+${fmtTok(d.output_tokens)}`
97
- : '';
98
- if (cacheLabel || tokLabel) {
99
- lines.push(` ${D}${tokLabel}${cacheLabel}${R}`);
100
- }
101
- lines.push(`${'─'.repeat(60)}`);
102
- lines.push(` ${C}⚡${R} Tip: ${generateTip(d)}`);
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
  }
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
2
  /**
3
3
  * mcp-server.ts — MCP (Model Context Protocol) server for claudestat
4
4
  *