@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/README.md +240 -70
- 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 +78 -15
- package/dist/db.d.ts +31 -0
- package/dist/db.js +89 -0
- package/dist/doctor.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +192 -70
- package/dist/insights.d.ts +45 -0
- package/dist/insights.js +257 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +442 -0
- 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 +7 -0
- package/dist/quota-tracker.js +83 -31
- package/dist/roast.js +129 -40
- package/dist/routes/events.js +3 -3
- package/dist/routes/projects.js +2 -2
- package/dist/share.js +5 -1
- package/dist/summarizer.js +1 -1
- package/package.json +4 -3
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
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.
|
|
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 @
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
330
|
-
.description('
|
|
331
|
-
.option('--
|
|
332
|
-
.option('--
|
|
333
|
-
.action(async (
|
|
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
|
|
336
|
-
|
|
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('
|
|
347
|
-
.description('
|
|
348
|
-
.option('--
|
|
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
|
|
353
|
-
|
|
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;
|