@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/README.md +199 -87
- 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 +28 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +199 -76
- package/dist/insights.d.ts +26 -0
- package/dist/insights.js +172 -20
- package/dist/install.js +39 -0
- 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/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 >=
|
|
21
|
-
note: nodeMajor >= 22 ? 'node:sqlite supported ✓'
|
|
22
|
-
|
|
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 @
|
|
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 @
|
|
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 @
|
|
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 @
|
|
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
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 @
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
|
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}%`;
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
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;
|