@statforge/claudestat 1.1.1 → 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/db.d.ts CHANGED
@@ -126,9 +126,40 @@ export declare const dbOps: {
126
126
  total_duration_ms: number;
127
127
  total_cost_usd: number;
128
128
  }[];
129
+ getWeeklyInsight(days?: number): {
130
+ total_sessions: number;
131
+ total_cost: number;
132
+ input_tokens: number;
133
+ output_tokens: number;
134
+ cache_read: number;
135
+ total_loops: number;
136
+ avg_efficiency: number;
137
+ week_start: number;
138
+ week_end: number;
139
+ };
140
+ setMeta(key: string, value: string): void;
141
+ getMeta(key: string): string | undefined;
129
142
  getCostProjection(days?: number): {
130
143
  total_cost_usd: number;
131
144
  earliest: number;
132
145
  latest: number;
133
146
  };
147
+ getProjectCosts(days?: number): {
148
+ project: string;
149
+ session_count: number;
150
+ total_cost: number;
151
+ }[];
152
+ getHourlyDistribution(days?: number): {
153
+ hour: number;
154
+ session_count: number;
155
+ }[];
156
+ getCacheReadByModel(days: number): {
157
+ model: string;
158
+ cache_read: number;
159
+ }[];
160
+ getModelBreakdown(days: number): {
161
+ model: string;
162
+ total_cost: number;
163
+ session_count: number;
164
+ }[];
134
165
  };
package/dist/db.js CHANGED
@@ -42,6 +42,15 @@ try {
42
42
  `);
43
43
  }
44
44
  catch { /* ya existe */ }
45
+ try {
46
+ db.exec(`
47
+ CREATE TABLE IF NOT EXISTS meta (
48
+ key TEXT PRIMARY KEY,
49
+ value TEXT NOT NULL
50
+ )
51
+ `);
52
+ }
53
+ catch { /* ya existe */ }
45
54
  db.exec(`
46
55
  CREATE TABLE IF NOT EXISTS sessions (
47
56
  id TEXT PRIMARY KEY,
@@ -381,6 +390,25 @@ const stmts = {
381
390
  ORDER BY total_duration_ms DESC
382
391
  LIMIT ?
383
392
  `),
393
+ getWeeklyInsight: db.prepare(`
394
+ SELECT
395
+ COUNT(*) AS total_sessions,
396
+ COALESCE(SUM(total_cost_usd), 0) AS total_cost,
397
+ COALESCE(SUM(total_input_tokens), 0) AS input_tokens,
398
+ COALESCE(SUM(total_output_tokens), 0) AS output_tokens,
399
+ COALESCE(SUM(total_cache_read), 0) AS cache_read,
400
+ COALESCE(SUM(loops_detected), 0) AS total_loops,
401
+ COALESCE(AVG(CASE WHEN efficiency_score > 0 THEN efficiency_score END), 100) AS avg_efficiency,
402
+ MIN(started_at) AS week_start,
403
+ MAX(last_event_at) AS week_end
404
+ FROM sessions
405
+ WHERE started_at >= ?
406
+ `),
407
+ upsertMeta: db.prepare(`
408
+ INSERT INTO meta (key, value) VALUES (?, ?)
409
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
410
+ `),
411
+ getMeta: db.prepare(`SELECT value FROM meta WHERE key = ?`),
384
412
  getCostProjection: db.prepare(`
385
413
  SELECT
386
414
  SUM(total_cost_usd) AS total_cost_usd,
@@ -388,6 +416,26 @@ const stmts = {
388
416
  MAX(last_event_at) AS latest
389
417
  FROM sessions
390
418
  WHERE started_at >= ?
419
+ `),
420
+ getProjectCosts: db.prepare(`
421
+ SELECT
422
+ COALESCE(project_path, 'no project') AS project,
423
+ COUNT(*) AS session_count,
424
+ COALESCE(SUM(total_cost_usd), 0) AS total_cost
425
+ FROM sessions
426
+ WHERE started_at >= ? AND total_cost_usd > 0
427
+ GROUP BY project_path
428
+ ORDER BY total_cost DESC
429
+ LIMIT 5
430
+ `),
431
+ getHourlyDistribution: db.prepare(`
432
+ SELECT
433
+ CAST(strftime('%H', datetime(started_at/1000, 'unixepoch', 'localtime')) AS INTEGER) AS hour,
434
+ COUNT(*) AS session_count
435
+ FROM sessions
436
+ WHERE started_at >= ?
437
+ GROUP BY hour
438
+ ORDER BY hour ASC
391
439
  `),
392
440
  getUnattributedCost: db.prepare(`
393
441
  WITH period_cost AS (
@@ -539,8 +587,49 @@ exports.dbOps = {
539
587
  }
540
588
  return tools;
541
589
  },
590
+ getWeeklyInsight(days = 7) {
591
+ const since = Date.now() - days * 86400000;
592
+ return stmts.getWeeklyInsight.get(since);
593
+ },
594
+ setMeta(key, value) {
595
+ stmts.upsertMeta.run(key, value);
596
+ },
597
+ getMeta(key) {
598
+ const row = stmts.getMeta.get(key);
599
+ return row?.value;
600
+ },
542
601
  getCostProjection(days = 7) {
543
602
  const since = Date.now() - days * 86400000;
544
603
  return stmts.getCostProjection.get(since);
545
604
  },
605
+ getProjectCosts(days = 7) {
606
+ const since = Date.now() - days * 86400000;
607
+ return stmts.getProjectCosts.all(since);
608
+ },
609
+ getHourlyDistribution(days = 7) {
610
+ const since = Date.now() - days * 86400000;
611
+ return stmts.getHourlyDistribution.all(since);
612
+ },
613
+ getCacheReadByModel(days) {
614
+ const since = Date.now() - days * 86400000;
615
+ return db.prepare(`
616
+ SELECT COALESCE(dominant_model, 'unknown') as model, SUM(total_cache_read) as cache_read
617
+ FROM sessions
618
+ WHERE started_at >= ?
619
+ GROUP BY dominant_model
620
+ `).all(since);
621
+ },
622
+ getModelBreakdown(days) {
623
+ const since = Date.now() - days * 86400000;
624
+ return db.prepare(`
625
+ SELECT
626
+ COALESCE(dominant_model, 'unknown') as model,
627
+ SUM(total_cost_usd) as total_cost,
628
+ COUNT(*) as session_count
629
+ FROM sessions
630
+ WHERE started_at >= ?
631
+ GROUP BY dominant_model
632
+ ORDER BY total_cost DESC
633
+ `).all(since);
634
+ },
546
635
  };
package/dist/doctor.js CHANGED
@@ -183,6 +183,7 @@ async function runDoctor() {
183
183
  const failed = checks.filter(c => !c.ok).length;
184
184
  if (failed === 0) {
185
185
  console.log(' \x1b[32mAll checks passed — claudestat is healthy!\x1b[0m\n');
186
+ process.exit(0);
186
187
  }
187
188
  else {
188
189
  console.log(` \x1b[31m${failed} check(s) failed — see fixes above\x1b[0m\n`);
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,9 +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");
31
+ const insights_1 = require("./insights");
32
32
  const paths_1 = require("./paths");
33
+ const quota_tracker_1 = require("./quota-tracker");
33
34
  const program = new commander_1.Command();
34
35
  const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
35
36
  const PID_FILE = (0, paths_1.getPidFile)();
@@ -64,7 +65,12 @@ async function stopDaemon() {
64
65
  catch { }
65
66
  try {
66
67
  const pid = parseInt(fs_1.default.readFileSync(PID_FILE, 'utf8').trim(), 10);
67
- process.kill(pid, 'SIGTERM');
68
+ if (process.platform === 'win32') {
69
+ process.kill(pid);
70
+ }
71
+ else {
72
+ process.kill(pid, 'SIGTERM');
73
+ }
68
74
  console.log(`✅ claudestat daemon stopped (pid ${pid})`);
69
75
  removePidFile();
70
76
  }
@@ -77,6 +83,20 @@ async function stopDaemon() {
77
83
  throw new Error(`Error stopping daemon: ${e.message}`);
78
84
  }
79
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
+ }
80
100
  // Warn if the active binary is outside the current npm global prefix (NVM conflict)
81
101
  if (process.env.NVM_DIR || process.env.NVM_HOME) {
82
102
  try {
@@ -86,7 +106,7 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
86
106
  const refreshCmd = paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat';
87
107
  process.stderr.write(`\x1b[33m⚠️ claudestat is running from ${runningFrom}\x1b[0m\n` +
88
108
  ` This binary may not match the active Node version (${process.version}).\n` +
89
- ` 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` +
90
110
  ` Then restart your terminal or run: \x1b[36m${refreshCmd}\x1b[0m\n\n`);
91
111
  }
92
112
  }
@@ -94,8 +114,21 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
94
114
  }
95
115
  program
96
116
  .name('claudestat')
97
- .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')
98
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
+ });
99
132
  program
100
133
  .command('start')
101
134
  .description('Start the background daemon (receives Claude Code hook events)')
@@ -146,9 +179,9 @@ program
146
179
  .command('status')
147
180
  .description('Show current quota, cost and burn rate')
148
181
  .option('--json', 'Output raw JSON instead of formatted text')
149
- .option('--compact', 'One-line output for tmux')
150
182
  .action(async (opts) => {
151
183
  try {
184
+ await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
152
185
  const [quotaRes, healthRes] = await Promise.all([
153
186
  fetch('http://localhost:7337/quota'),
154
187
  fetch('http://localhost:7337/health'),
@@ -157,12 +190,6 @@ program
157
190
  throw new Error('Daemon unavailable');
158
191
  const q = await quotaRes.json();
159
192
  const _h = await healthRes.json().catch(() => ({}));
160
- if (opts.compact) {
161
- const pctCycle = q.cyclePct;
162
- const cycleEmoji = pctCycle >= 95 ? '🔴' : pctCycle >= 70 ? '🟡' : '🟢';
163
- console.log(`Current ${pctCycle}%${cycleEmoji} ${q.detectedPlan}`);
164
- process.exit(0);
165
- }
166
193
  if (opts.json) {
167
194
  console.log(JSON.stringify({
168
195
  cyclePrompts: q.cyclePrompts,
@@ -174,35 +201,50 @@ program
174
201
  weeklyLimitSonnet: q.weeklyLimitSonnet,
175
202
  weeklyHoursOpus: q.weeklyHoursOpus,
176
203
  weeklyLimitOpus: q.weeklyLimitOpus,
204
+ weeklyPctAll: q.weeklyPctAll,
177
205
  burnRateTokensPerMin: q.burnRateTokensPerMin,
178
206
  }));
179
207
  process.exit(0);
180
208
  }
181
209
  const R = '\x1b[0m';
182
- const pctColor = q.cyclePct >= 95 ? '\x1b[31m'
183
- : q.cyclePct >= 85 ? '\x1b[33m'
184
- : q.cyclePct >= 70 ? '\x1b[33m'
185
- : '\x1b[32m';
186
- const resetMin = Math.ceil(q.cycleResetMs / 60000);
187
- const resetLabel = resetMin >= 60
188
- ? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
189
- : `${resetMin}m`;
190
- const burnLabel = q.burnRateTokensPerMin > 0
191
- ? ` 🔥 ${q.burnRateTokensPerMin.toLocaleString()} tok/min`
192
- : '';
193
- const burnRow = q.burnRateTokensPerMin > 0
194
- ? ` Burn rate ${q.burnRateTokensPerMin.toLocaleString()} tok/min\n`
195
- : '';
196
- console.log(`\n📊 claudestat status\n` +
197
- `──────────────────────────────────────────\n` +
198
- ` Quota 5h ${pctColor}${q.cyclePrompts}/${q.cycleLimit} prompts (${q.cyclePct}%)${R} │ resets in ${resetLabel}\n` +
199
- ` Plan ${q.detectedPlan.toUpperCase()}\n` +
200
- ` Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h this week\n` +
201
- (q.weeklyLimitOpus > 0
202
- ? ` Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h this week\n`
203
- : '') +
204
- `${burnRow}` +
205
- `──────────────────────────────────────────\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'));
206
248
  process.exit(0);
207
249
  }
208
250
  catch {
@@ -250,13 +292,38 @@ program
250
292
  (0, config_1.writeConfig)(cfg);
251
293
  console.log('✅ Config saved to ~/.claudestat/config.json');
252
294
  }
253
- // Always show current config
254
- console.log('\n📋 Current config:');
255
- console.log(` killSwitchEnabled: ${cfg.killSwitchEnabled}`);
256
- console.log(` killSwitchThreshold: ${cfg.killSwitchThreshold}%`);
257
- console.log(` warnThresholds: ${cfg.warnThresholds.join('%, ')}%`);
258
- console.log(` alertsEnabled: ${cfg.alertsEnabled}`);
259
- 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'));
260
327
  process.exit(0);
261
328
  });
262
329
  program
@@ -292,25 +359,49 @@ program
292
359
  if (!res.ok)
293
360
  throw new Error('Daemon unavailable');
294
361
  const data = await res.json();
362
+ const R = '\x1b[0m';
363
+ const B = '\x1b[1m';
364
+ const D = '\x1b[2m';
295
365
  const label = by === 'count' ? 'calls' : by === 'duration' ? 'duration' : 'est. cost';
296
- console.log(`\n🏆 claudestat top by ${label} (last ${days} days)\n`);
297
- console.log(' # Tool Calls Duration Est. Cost %');
298
- 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('');
299
384
  for (let i = 0; i < data.tools.length; i++) {
300
385
  const t = data.tools[i];
301
386
  const isOther = t.tool === 'Other';
302
- const dur = isOther ? ''
303
- : t.totalDurationMs >= 60000
304
- ? `${(t.totalDurationMs / 60000).toFixed(1)}m`
305
- : `${(t.totalDurationMs / 1000).toFixed(0)}s`;
306
- const cost = t.estimatedCostUsd < 0.01
307
- ? `$${t.estimatedCostUsd.toFixed(4)}`
308
- : `$${t.estimatedCostUsd.toFixed(2)}`;
309
- 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);
310
391
  const countStr = isOther ? '—' : String(t.count);
311
- 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
+ }
312
400
  }
313
- console.log();
401
+ lines.push('');
402
+ lines.push('━'.repeat(52));
403
+ lines.push('');
404
+ console.log(lines.join('\n'));
314
405
  process.exit(0);
315
406
  }
316
407
  catch {
@@ -326,15 +417,14 @@ program
326
417
  process.exit(1);
327
418
  }));
328
419
  program
329
- .command('share [session-id]')
330
- .description('Generate a shareable session card (ASCII or JSON)')
331
- .option('--format <type>', 'Output format: ascii, json (default: ascii)')
332
- .option('--copy', 'Copy to clipboard (macOS only)')
333
- .action(async (sessionId, opts) => {
420
+ .command('roast')
421
+ .description('Roast your Claude Code usage habits')
422
+ .option('--stats', 'Show raw stats only, no roast')
423
+ .option('--months <n>', 'Look back N months (default: 1)', String, '1')
424
+ .action(async (opts) => {
334
425
  try {
335
- const format = (opts.format ?? 'ascii');
336
- const copy = !!opts.copy;
337
- await (0, share_1.runShare)({ sessionId, format, copy });
426
+ const months = parseInt(opts.months || '1', 10);
427
+ await (0, roast_1.runRoast)({ stats: !!opts.stats, months });
338
428
  process.exit(0);
339
429
  }
340
430
  catch (err) {
@@ -343,14 +433,46 @@ program
343
433
  }
344
434
  });
345
435
  program
346
- .command('roast')
347
- .description('Roast your Claude Code usage habits')
348
- .option('--stats', 'Show raw stats only, no roast')
349
- .option('--months <n>', 'Look back N months (default: 1)', String, '1')
436
+ .command('weekly')
437
+ .description('Show weekly usage summary')
438
+ .option('--json', 'Output as JSON')
350
439
  .action(async (opts) => {
351
440
  try {
352
- const months = parseInt(opts.months || '1', 10);
353
- await (0, roast_1.runRoast)({ stats: !!opts.stats, months });
441
+ const data = (0, insights_1.getWeeklyInsightData)();
442
+ if (data.total_sessions === 0) {
443
+ console.log('\n📊 No usage data yet — start using Claude Code and claudestat will track it.\n');
444
+ process.exit(0);
445
+ }
446
+ if (opts.json) {
447
+ console.log(JSON.stringify(data, null, 2));
448
+ process.exit(0);
449
+ }
450
+ console.log((0, insights_1.renderWeeklyInsight)(data));
451
+ process.exit(0);
452
+ }
453
+ catch (err) {
454
+ console.error('\n❌ Error:', err.message);
455
+ process.exit(1);
456
+ }
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));
354
476
  process.exit(0);
355
477
  }
356
478
  catch (err) {
@@ -0,0 +1,45 @@
1
+ export interface WeeklyInsightData {
2
+ total_sessions: number;
3
+ total_cost: number;
4
+ input_tokens: number;
5
+ output_tokens: number;
6
+ cache_read: number;
7
+ cache_hit_pct: number;
8
+ total_loops: number;
9
+ avg_efficiency: number;
10
+ top_tool: string;
11
+ top_tool_cost_pct: number;
12
+ week_start: number;
13
+ week_end: number;
14
+ }
15
+ export declare function getWeeklyInsightData(days?: number): WeeklyInsightData;
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;
43
+ export declare function shouldShowInsight(): boolean;
44
+ export declare function markInsightShown(): void;
45
+ export declare function renderWeeklyInsight(d: WeeklyInsightData): string;