@statforge/claudestat 1.2.0 → 1.2.3

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/doctor.js CHANGED
@@ -17,11 +17,9 @@ async function runDoctor() {
17
17
  const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
18
18
  checks.push({
19
19
  label: `Node.js version (${process.versions.node})`,
20
- ok: nodeMajor >= 18,
21
- note: nodeMajor >= 22 ? 'node:sqlite supported ✓'
22
- : nodeMajor >= 18 ? 'Works Node 22+ recommended for native node:sqlite'
23
- : undefined,
24
- fix: nodeMajor < 18 ? 'Install Node.js 18 or later: https://nodejs.org' : undefined,
20
+ ok: nodeMajor >= 22,
21
+ note: nodeMajor >= 22 ? 'node:sqlite supported ✓' : undefined,
22
+ fix: nodeMajor < 22 ? 'Install Node.js 22 or later: https://nodejs.org' : undefined,
25
23
  });
26
24
  // 2. Claude Code installed
27
25
  const claudeOk = (() => { try {
@@ -98,7 +96,7 @@ async function runDoctor() {
98
96
  label: 'Global CLI symlink valid',
99
97
  ok: symlinkOk,
100
98
  note: symlinkNote,
101
- fix: symlinkOk ? undefined : 'npm install -g @deibygs/claudestat',
99
+ fix: symlinkOk ? undefined : 'npm install -g @statforge/claudestat',
102
100
  });
103
101
  // 8. No duplicate claudestat binaries in PATH
104
102
  let duplicatesOk = true;
@@ -117,7 +115,7 @@ async function runDoctor() {
117
115
  ok: duplicatesOk,
118
116
  note: duplicatesNote,
119
117
  fix: duplicatesOk ? undefined :
120
- `npm uninstall -g @deibygs/claudestat && npm install -g @deibygs/claudestat\n Then restart your terminal or run: ${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'}`,
118
+ `npm uninstall -g @statforge/claudestat && npm install -g @statforge/claudestat\n Then restart your terminal or run: ${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'}`,
121
119
  });
122
120
  // 9. Active binary version matches installed package
123
121
  let versionOk = true;
@@ -146,7 +144,7 @@ async function runDoctor() {
146
144
  ok: versionOk,
147
145
  note: versionNote,
148
146
  fix: versionOk ? undefined :
149
- `${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'} (or restart terminal)\n If persists: npm uninstall -g @deibygs/claudestat && npm install -g @deibygs/claudestat`,
147
+ `${paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat'} (or restart terminal)\n If persists: npm uninstall -g @statforge/claudestat && npm install -g @statforge/claudestat`,
150
148
  });
151
149
  // 10. NVM prefix sanity (only when NVM is active)
152
150
  if ((process.env.NVM_DIR || process.env.NVM_HOME) && activeBinary) {
@@ -165,9 +163,29 @@ async function runDoctor() {
165
163
  ok: nvmOk,
166
164
  note: nvmNote,
167
165
  fix: nvmOk ? undefined :
168
- `nvm use default && npm install -g @deibygs/claudestat\n Then restart terminal`,
166
+ `nvm use default && npm install -g @statforge/claudestat\n Then restart terminal`,
169
167
  });
170
168
  }
169
+ // 11. MCP server registered in Claude Code
170
+ let mcpOk = false;
171
+ let mcpNote;
172
+ const mcpResult = (0, child_process_1.spawnSync)('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 15000 });
173
+ if (mcpResult.error) {
174
+ mcpNote = '"claude" CLI not found — install Claude Code first';
175
+ }
176
+ else {
177
+ const mcpList = (mcpResult.stdout ?? '') + (mcpResult.stderr ?? '');
178
+ const mcpLine = mcpList.split('\n').find(l => l.includes('claudestat'));
179
+ mcpOk = !!mcpLine && !mcpLine.includes('Failed') && !mcpLine.includes('✗');
180
+ if (!mcpOk)
181
+ mcpNote = 'Run "claudestat install" to register it automatically';
182
+ }
183
+ checks.push({
184
+ label: 'MCP server registered in Claude Code',
185
+ ok: mcpOk,
186
+ note: mcpNote,
187
+ fix: mcpOk ? undefined : 'claudestat install',
188
+ });
171
189
  // ── Print results ───────────────────────────────────────────
172
190
  console.log('\n🩺 claudestat doctor\n' + '─'.repeat(46));
173
191
  for (const c of checks) {
@@ -183,6 +201,7 @@ async function runDoctor() {
183
201
  const failed = checks.filter(c => !c.ok).length;
184
202
  if (failed === 0) {
185
203
  console.log(' \x1b[32mAll checks passed — claudestat is healthy!\x1b[0m\n');
204
+ process.exit(0);
186
205
  }
187
206
  else {
188
207
  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,13 +27,48 @@ 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)();
37
+ // ── Update notifier ────────────────────────────────────────────
38
+ const SKIP_UPDATE_NOTICE = new Set(['start', 'stop', 'restart', 'watch']);
39
+ const subcommand = process.argv[2];
40
+ if (!SKIP_UPDATE_NOTICE.has(subcommand)) {
41
+ const UPDATE_CACHE = path_1.default.join((0, paths_1.getClaudestatDir)(), 'update-cache.json');
42
+ let cachedLatest = null;
43
+ const fetchLatestVersion = () => {
44
+ fetch('https://registry.npmjs.org/@statforge/claudestat/latest', { signal: AbortSignal.timeout(3000) })
45
+ .then(r => r.json())
46
+ .then(j => {
47
+ if (j?.version) {
48
+ cachedLatest = j.version;
49
+ fs_1.default.writeFileSync(UPDATE_CACHE, JSON.stringify({ version: j.version, ts: Date.now() }));
50
+ }
51
+ })
52
+ .catch(() => { });
53
+ };
54
+ try {
55
+ const cache = JSON.parse(fs_1.default.readFileSync(UPDATE_CACHE, 'utf8'));
56
+ cachedLatest = cache.version;
57
+ if (Date.now() - cache.ts >= 24 * 60 * 60 * 1000)
58
+ fetchLatestVersion();
59
+ }
60
+ catch {
61
+ fetchLatestVersion();
62
+ }
63
+ const _exit = process.exit.bind(process);
64
+ process.exit = ((code) => {
65
+ if ((code ?? 0) === 0 && cachedLatest && cachedLatest !== PKG_VERSION) {
66
+ console.log(`\n ✦ Update available: ${PKG_VERSION} → ${cachedLatest}`);
67
+ console.log(` Run: npm install -g @statforge/claudestat\n`);
68
+ }
69
+ _exit(code);
70
+ });
71
+ }
37
72
  function spawnDaemon() {
38
73
  const child = (0, child_process_1.spawn)(process.execPath, [process.argv[1], 'start'], {
39
74
  detached: true,
@@ -83,6 +118,20 @@ async function stopDaemon() {
83
118
  throw new Error(`Error stopping daemon: ${e.message}`);
84
119
  }
85
120
  }
121
+ async function checkLatestVersion() {
122
+ try {
123
+ const res = await fetch('https://registry.npmjs.org/@statforge/claudestat/latest', {
124
+ signal: AbortSignal.timeout(2000),
125
+ });
126
+ if (!res.ok)
127
+ return null;
128
+ const json = await res.json();
129
+ return json.version;
130
+ }
131
+ catch {
132
+ return null;
133
+ }
134
+ }
86
135
  // Warn if the active binary is outside the current npm global prefix (NVM conflict)
87
136
  if (process.env.NVM_DIR || process.env.NVM_HOME) {
88
137
  try {
@@ -92,7 +141,7 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
92
141
  const refreshCmd = paths_1.isWindows ? 'refreshenv' : 'hash -r claudestat';
93
142
  process.stderr.write(`\x1b[33m⚠️ claudestat is running from ${runningFrom}\x1b[0m\n` +
94
143
  ` 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` +
144
+ ` Fix: \x1b[36mnvm use default && npm install -g @statforge/claudestat\x1b[0m\n` +
96
145
  ` Then restart your terminal or run: \x1b[36m${refreshCmd}\x1b[0m\n\n`);
97
146
  }
98
147
  }
@@ -100,8 +149,21 @@ if (process.env.NVM_DIR || process.env.NVM_HOME) {
100
149
  }
101
150
  program
102
151
  .name('claudestat')
103
- .description('Real-time execution trace and cost intelligence for Claude Code')
152
+ .description('Real-time execution trace and cost intelligence for Claude Code · github.com/DeibyGS/claudestat')
104
153
  .version(PKG_VERSION);
154
+ program
155
+ .command('version')
156
+ .description('Show version and check for updates')
157
+ .action(async () => {
158
+ console.log(PKG_VERSION);
159
+ const latest = await checkLatestVersion();
160
+ if (latest) {
161
+ const isLatest = latest === PKG_VERSION;
162
+ const tag = isLatest ? `\x1b[32mlatest ✓\x1b[0m` : `\x1b[33mlatest: ${latest} — run npm update\x1b[0m`;
163
+ console.log(` ${tag}`);
164
+ }
165
+ process.exit(0);
166
+ });
105
167
  program
106
168
  .command('start')
107
169
  .description('Start the background daemon (receives Claude Code hook events)')
@@ -152,9 +214,9 @@ program
152
214
  .command('status')
153
215
  .description('Show current quota, cost and burn rate')
154
216
  .option('--json', 'Output raw JSON instead of formatted text')
155
- .option('--compact', 'One-line output for tmux')
156
217
  .action(async (opts) => {
157
218
  try {
219
+ await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
158
220
  const [quotaRes, healthRes] = await Promise.all([
159
221
  fetch('http://localhost:7337/quota'),
160
222
  fetch('http://localhost:7337/health'),
@@ -163,13 +225,6 @@ program
163
225
  throw new Error('Daemon unavailable');
164
226
  const q = await quotaRes.json();
165
227
  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
228
  if (opts.json) {
174
229
  console.log(JSON.stringify({
175
230
  cyclePrompts: q.cyclePrompts,
@@ -187,33 +242,44 @@ program
187
242
  process.exit(0);
188
243
  }
189
244
  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`);
245
+ const B = '\x1b[1m';
246
+ const D = '\x1b[2m';
247
+ const pctBar = (pct, width = 20) => {
248
+ const filled = Math.round(Math.min(pct, 100) / 100 * width);
249
+ const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
250
+ return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
251
+ };
252
+ const resetTime = q.cycleResetAt
253
+ ? new Date(q.cycleResetAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
254
+ : (() => {
255
+ const m = Math.ceil(q.cycleResetMs / 60000);
256
+ return m >= 60 ? `${Math.floor(m / 60)}h ${m % 60}m` : `${m}m`;
257
+ })();
258
+ const now = new Date();
259
+ const daysToMonday = ((8 - now.getDay()) % 7) || 7;
260
+ const nextMonday = new Date(now);
261
+ nextMonday.setDate(now.getDate() + daysToMonday);
262
+ const weekReset = nextMonday.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
263
+ const lines = [];
264
+ lines.push(`\n${B}📊 claudestat${R} ${D}${q.detectedPlan.toUpperCase()} plan${R}`);
265
+ lines.push('━'.repeat(42));
266
+ lines.push('');
267
+ lines.push(` 5h ${pctBar(q.cyclePct)} ${B}${q.cyclePct}%${R} ${D}resets ${resetTime}${R}`);
268
+ lines.push('');
269
+ lines.push(` Week ${pctBar(q.weeklyPctAll)} ${B}${q.weeklyPctAll}%${R} ${D}resets ${weekReset}${R}`);
270
+ if (q.weeklyLimitOpus > 0) {
271
+ lines.push('');
272
+ lines.push(` ${D} ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h${R}`);
273
+ lines.push(` ${D} └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h${R}`);
274
+ }
275
+ if (q.burnRateTokensPerMin > 0) {
276
+ lines.push('');
277
+ lines.push(` 🔥 ${B}${q.burnRateTokensPerMin.toLocaleString()}${R} tok/min ${D}· ${q.cyclePrompts} prompts used${R}`);
278
+ }
279
+ lines.push('');
280
+ lines.push('━'.repeat(42));
281
+ lines.push('');
282
+ console.log(lines.join('\n'));
217
283
  process.exit(0);
218
284
  }
219
285
  catch {
@@ -261,13 +327,38 @@ program
261
327
  (0, config_1.writeConfig)(cfg);
262
328
  console.log('✅ Config saved to ~/.claudestat/config.json');
263
329
  }
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`);
330
+ const R = '\x1b[0m';
331
+ const B = '\x1b[1m';
332
+ const D = '\x1b[2m';
333
+ const G = '\x1b[32m';
334
+ const Y = '\x1b[33m';
335
+ const C = '\x1b[36m';
336
+ const bar = (pct, width = 20) => {
337
+ const filled = Math.round(Math.min(pct, 100) / 100 * width);
338
+ const color = pct >= 95 ? '\x1b[31m' : pct >= 85 ? '\x1b[33m' : '\x1b[32m';
339
+ return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
340
+ };
341
+ const planColor = cfg.plan === 'pro' ? G : cfg.plan === 'max5' ? C : cfg.plan === 'max20' ? '\x1b[35m' : Y;
342
+ const planLabel = cfg.plan ?? 'auto-detect';
343
+ const alertsIcon = cfg.alertsEnabled ? `${G}enabled${R}` : `${Y}disabled${R}`;
344
+ const lines = [];
345
+ lines.push(`\n${B}⚙️ claudestat config${R}`);
346
+ lines.push('━'.repeat(42));
347
+ lines.push('');
348
+ lines.push(` Plan ${planColor}${planLabel.toUpperCase()}${R}`);
349
+ lines.push(` Alerts ${alertsIcon}`);
350
+ lines.push('');
351
+ lines.push(` Kill switch ${cfg.killSwitchEnabled ? `${Y}ON${R} at ${cfg.killSwitchThreshold}%` : `${D}OFF${R}`}`);
352
+ if (cfg.killSwitchEnabled) {
353
+ lines.push(` ${bar(cfg.killSwitchThreshold)}`);
354
+ }
355
+ lines.push('');
356
+ lines.push(` Cycle thresholds ${cfg.warnThresholds.join('%, ')}%`);
357
+ 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)}`);
358
+ lines.push('');
359
+ lines.push('━'.repeat(42));
360
+ lines.push('');
361
+ console.log(lines.join('\n'));
271
362
  process.exit(0);
272
363
  });
273
364
  program
@@ -303,25 +394,49 @@ program
303
394
  if (!res.ok)
304
395
  throw new Error('Daemon unavailable');
305
396
  const data = await res.json();
397
+ const R = '\x1b[0m';
398
+ const B = '\x1b[1m';
399
+ const D = '\x1b[2m';
306
400
  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(' ── ───────────────── ──────── ───────────── ───────── ────');
401
+ const maxVal = Math.max(...data.tools.map((t) => by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs));
402
+ const bar = (val, max, width = 20) => {
403
+ const pct = max > 0 ? val / max * 100 : 0;
404
+ const filled = Math.round(pct / 100 * width);
405
+ const rank = data.tools.findIndex((t) => {
406
+ const tv = by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs;
407
+ return tv === val;
408
+ });
409
+ const color = rank === 0 ? '\x1b[31m' : rank <= 2 ? '\x1b[33m' : '\x1b[32m';
410
+ return `${color}${'█'.repeat(filled)}${R}${D}${'░'.repeat(width - filled)}${R}`;
411
+ };
412
+ const fmtCost = (n) => n < 0.01 ? `< $0.01` : `$${n.toFixed(2)}`;
413
+ const fmtDur = (ms) => ms >= 60000 ? `${(ms / 60000).toFixed(1)}m` : `${(ms / 1000).toFixed(0)}s`;
414
+ const fmtPct = (n) => `${Math.round(n)}%`;
415
+ const lines = [];
416
+ lines.push(`\n${B}🏆 claudestat top${R} ${D}by ${label} (last ${days} days)${R}`);
417
+ lines.push('━'.repeat(52));
418
+ lines.push('');
310
419
  for (let i = 0; i < data.tools.length; i++) {
311
420
  const t = data.tools[i];
312
421
  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}%`;
422
+ const val = isOther ? 0 : (by === 'cost' ? t.estimatedCostUsd : by === 'count' ? t.count : t.totalDurationMs);
423
+ const pct = by === 'cost' ? t.pctCost : (isOther ? 0 : t.pctCount);
424
+ const dur = isOther ? '—' : fmtDur(t.totalDurationMs);
425
+ const cost = isOther ? fmtCost(t.estimatedCostUsd) : fmtCost(t.estimatedCostUsd);
321
426
  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)}`);
427
+ const toolName = (t.tool.length > 18 ? t.tool.slice(0, 16) + '…' : t.tool).padEnd(18);
428
+ if (isOther) {
429
+ lines.push(` ${D}Other${R} ${'—'.padStart(20)} ${cost.padStart(10)} ${fmtPct(pct)}`);
430
+ }
431
+ else {
432
+ lines.push(` ${B}${(i + 1).toString().padStart(2)}${R} ${toolName} ${bar(val, maxVal)} ${cost.padStart(10)} ${fmtPct(pct)}`);
433
+ lines.push(` ${D}${countStr} calls · ${dur}${R}`);
434
+ }
323
435
  }
324
- console.log();
436
+ lines.push('');
437
+ lines.push('━'.repeat(52));
438
+ lines.push('');
439
+ console.log(lines.join('\n'));
325
440
  process.exit(0);
326
441
  }
327
442
  catch {
@@ -336,23 +451,6 @@ program
336
451
  console.error('\n❌ Error:', err.message);
337
452
  process.exit(1);
338
453
  }));
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
454
  program
357
455
  .command('roast')
358
456
  .description('Roast your Claude Code usage habits')
@@ -392,4 +490,29 @@ program
392
490
  process.exit(1);
393
491
  }
394
492
  });
493
+ program
494
+ .command('insights')
495
+ .description('Show usage insights: cost breakdown, cache savings, efficiency trend, peak hours')
496
+ .option('--days <number>', 'Look back N days (default 7)')
497
+ .option('--json', 'Output raw JSON')
498
+ .action((opts) => {
499
+ try {
500
+ const days = Math.max(1, Math.min(90, parseInt(opts.days ?? '7', 10) || 7));
501
+ const data = (0, insights_1.getUsageInsights)(days);
502
+ if (data.total_sessions === 0) {
503
+ console.log(`\n💡 No data for the last ${days} days.\n`);
504
+ process.exit(0);
505
+ }
506
+ if (opts.json) {
507
+ console.log(JSON.stringify(data, null, 2));
508
+ process.exit(0);
509
+ }
510
+ console.log((0, insights_1.renderInsights)(data));
511
+ process.exit(0);
512
+ }
513
+ catch (err) {
514
+ console.error('\n❌ Error:', err.message);
515
+ process.exit(1);
516
+ }
517
+ });
395
518
  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;