@statforge/claudestat 1.0.1 → 1.1.1
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 +97 -1
- package/dist/daemon.js +6 -4
- package/dist/enricher.js +33 -27
- package/dist/index.js +43 -1
- package/dist/pricing.d.ts +20 -0
- package/dist/pricing.js +25 -0
- package/dist/project-scanner.js +3 -18
- package/dist/quota-tracker.d.ts +2 -0
- package/dist/quota-tracker.js +11 -7
- package/dist/render.d.ts +1 -0
- package/dist/render.js +12 -7
- package/dist/roast.d.ts +4 -0
- package/dist/roast.js +95 -0
- package/dist/routes/events.js +4 -4
- package/dist/share.d.ts +5 -0
- package/dist/share.js +117 -0
- package/dist/watch.js +27 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
|
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
14
|
[]()
|
|
15
|
-
[]()
|
|
16
16
|
[](CONTRIBUTING.md)
|
|
17
17
|
|
|
18
18
|
[Installation](#installation) • [Quick Start](#quick-start) • [Commands](#commands) • [Dashboard](#dashboard) • [Contributing](#contributing)
|
|
@@ -33,6 +33,8 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
|
|
|
33
33
|
|
|
34
34
|
## Why?
|
|
35
35
|
|
|
36
|
+
You're burning tokens right now and you have no idea how many, on what, or whether Claude is stuck in a loop.
|
|
37
|
+
|
|
36
38
|
Claude Code is powerful — but it's a black box while it runs. You can't see what it's spending, how deep the context is, whether it's looping, or if you're about to hit your quota limit.
|
|
37
39
|
|
|
38
40
|
**claudestat fixes that.** It taps into Claude Code's hook system to capture every event, stores it locally in SQLite, and shows you everything in a live dashboard or terminal trace.
|
|
@@ -43,6 +45,8 @@ Claude Code is powerful — but it's a black box while it runs. You can't see wh
|
|
|
43
45
|
- Per-session cost breakdown + cache savings + burn rate
|
|
44
46
|
- AI-generated weekly usage reports
|
|
45
47
|
|
|
48
|
+
> If claudestat is useful, give it a ⭐ — it helps other developers find it.
|
|
49
|
+
|
|
46
50
|
---
|
|
47
51
|
|
|
48
52
|
## How it works
|
|
@@ -133,9 +137,12 @@ That's it. Start a Claude Code session and watch the events flow in.
|
|
|
133
137
|
| `claudestat uninstall` | Remove hooks from Claude Code |
|
|
134
138
|
| `claudestat watch` | Live terminal trace view |
|
|
135
139
|
| `claudestat status` | Show quota, cost, and burn rate |
|
|
140
|
+
| `claudestat status --compact` | One-line output for tmux status bar |
|
|
136
141
|
| `claudestat config` | View or edit configuration |
|
|
137
142
|
| `claudestat top` | Rank tools by cost, call count, or duration |
|
|
138
143
|
| `claudestat export [format]` | Export session data to JSON or CSV |
|
|
144
|
+
| `claudestat share [session-id]` | Generate shareable session card (ASCII/JSON) |
|
|
145
|
+
| `claudestat roast` | Sarcastic usage analysis with roast jokes |
|
|
139
146
|
| `claudestat doctor` | Check installation health and diagnose issues |
|
|
140
147
|
|
|
141
148
|
### `claudestat watch`
|
|
@@ -187,6 +194,68 @@ claudestat status
|
|
|
187
194
|
Burn rate 1,240 tok/min
|
|
188
195
|
```
|
|
189
196
|
|
|
197
|
+
### `claudestat status --compact`
|
|
198
|
+
|
|
199
|
+
One-line output for tmux status bar or scripting. Shows the 5h cycle quota percentage.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
claudestat status --compact
|
|
203
|
+
Current 45%🟡 pro
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `claudestat share`
|
|
207
|
+
|
|
208
|
+
Generate a shareable session card — perfect for sharing on social media or in bug reports.
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
claudestat share
|
|
212
|
+
╔═══════════════════════════════════╗
|
|
213
|
+
║ Session Report · claudestat ║
|
|
214
|
+
╠═══════════════════════════════════╣
|
|
215
|
+
║ Project my-project ║
|
|
216
|
+
║ Duration 2h 14m ║
|
|
217
|
+
║ Tools 847 calls ║
|
|
218
|
+
║ Cost $0.84 ║
|
|
219
|
+
║ Cache hit 27% saved ($0.31) ║
|
|
220
|
+
║ Top tool Bash (38%) ║
|
|
221
|
+
║ Efficiency 91 / 100 ║
|
|
222
|
+
╚═══════════════════════════════════╝
|
|
223
|
+
github.com/DeibyGS/claudestat
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Options:
|
|
227
|
+
- `--format ascii|json` — output format (default: ascii)
|
|
228
|
+
- `--copy` — copy to clipboard automatically (macOS only)
|
|
229
|
+
|
|
230
|
+
### `claudestat roast`
|
|
231
|
+
|
|
232
|
+
Get a sarcastic analysis of your Claude Code usage — humor with insights.
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
claudestat roast
|
|
236
|
+
|
|
237
|
+
=== Claude Code Stats (last 30 days) ===
|
|
238
|
+
Sessions: 47
|
|
239
|
+
Total cost: $12.40
|
|
240
|
+
Bash calls: 1,240
|
|
241
|
+
Loops: 8
|
|
242
|
+
Efficiency: 72/100
|
|
243
|
+
|
|
244
|
+
🔥 Your Claude Code Roast
|
|
245
|
+
|
|
246
|
+
You called Bash 1,240 times last month.
|
|
247
|
+
That's once every 2.3 minutes.
|
|
248
|
+
Are you okay?
|
|
249
|
+
|
|
250
|
+
You hit 90%+ context in 12 sessions.
|
|
251
|
+
Claude was writing with amnesia half the time.
|
|
252
|
+
|
|
253
|
+
You spent $4.20 on loops you never noticed.
|
|
254
|
+
That's 14 coffees. Just saying.
|
|
255
|
+
|
|
256
|
+
Efficiency: 72/100 — room for growth, champ.
|
|
257
|
+
```
|
|
258
|
+
|
|
190
259
|
### `claudestat doctor`
|
|
191
260
|
|
|
192
261
|
Diagnoses common installation problems — useful if `claudestat start` fails or hooks are not firing.
|
|
@@ -432,6 +501,33 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
|
|
|
432
501
|
|
|
433
502
|
---
|
|
434
503
|
|
|
504
|
+
## FAQ
|
|
505
|
+
|
|
506
|
+
**What is claudestat?**
|
|
507
|
+
claudestat is a real-time token monitoring and cost analytics tool for Claude Code.
|
|
508
|
+
It captures every tool call, token usage, and API cost as it happens — locally, with zero cloud dependencies.
|
|
509
|
+
|
|
510
|
+
**How do I monitor Claude Code token usage?**
|
|
511
|
+
Install with `npm install -g @statforge/claudestat`, run `claudestat start`, and open `http://localhost:7337` for the live dashboard.
|
|
512
|
+
|
|
513
|
+
**How do I track Claude Code costs?**
|
|
514
|
+
claudestat records every session's token usage and estimates API cost per tool call.
|
|
515
|
+
Use `claudestat status` for a quick summary or `claudestat export` for full data export.
|
|
516
|
+
|
|
517
|
+
**How do I get alerted when Claude Code hits the rate limit?**
|
|
518
|
+
claudestat polls your quota every 60 seconds and sends desktop notifications when you cross 70%, 85%, or 95%. Configure with `claudestat config --alerts true`.
|
|
519
|
+
|
|
520
|
+
**Does claudestat work with Claude Pro, Max 5, and Max 20?**
|
|
521
|
+
Yes. claudestat auto-detects your plan. You can also force it with `claudestat config --plan max5`.
|
|
522
|
+
|
|
523
|
+
**Is my data sent to any server?**
|
|
524
|
+
No. All data is stored locally in SQLite at `~/.claudestat/`. Zero cloud dependencies.
|
|
525
|
+
|
|
526
|
+
**Does claudestat work on Windows?**
|
|
527
|
+
Yes — macOS, Linux, and Windows are all supported.
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
435
531
|
## License
|
|
436
532
|
|
|
437
533
|
MIT — use it, fork it, ship it.
|
package/dist/daemon.js
CHANGED
|
@@ -79,7 +79,7 @@ function migrateSessionProjects() {
|
|
|
79
79
|
const sessions = db_1.dbOps.getAllSessions();
|
|
80
80
|
let tagged = 0;
|
|
81
81
|
for (const session of sessions) {
|
|
82
|
-
if (session
|
|
82
|
+
if (session?.project_path)
|
|
83
83
|
continue;
|
|
84
84
|
const events = db_1.dbOps.getSessionEvents(session.id);
|
|
85
85
|
const projectCwd = (0, projects_1.inferProjectCwd)(events);
|
|
@@ -97,7 +97,7 @@ function migrateSessionProjects() {
|
|
|
97
97
|
*/
|
|
98
98
|
async function migrateSessionSummaries(limit = 5) {
|
|
99
99
|
const sessions = db_1.dbOps.getAllSessions()
|
|
100
|
-
.filter(s => !s
|
|
100
|
+
.filter(s => !s?.ai_summary)
|
|
101
101
|
.slice(0, limit);
|
|
102
102
|
for (const s of sessions) {
|
|
103
103
|
try {
|
|
@@ -192,8 +192,10 @@ function startDaemon() {
|
|
|
192
192
|
_server = app.listen(PORT, '127.0.0.1', () => {
|
|
193
193
|
writePid();
|
|
194
194
|
process.on('exit', cleanPid);
|
|
195
|
-
process.on('SIGTERM', () => {
|
|
196
|
-
|
|
195
|
+
process.on('SIGTERM', () => { if (_server)
|
|
196
|
+
shutdown(_server); process.exit(0); });
|
|
197
|
+
process.on('SIGINT', () => { if (_server)
|
|
198
|
+
shutdown(_server); process.exit(0); });
|
|
197
199
|
console.log(`\n● claudestat daemon → http://localhost:${PORT}`);
|
|
198
200
|
console.log(` Waiting for Claude Code events...\n`);
|
|
199
201
|
console.log(` In another terminal: \x1b[36mclaudestat watch\x1b[0m\n`);
|
package/dist/enricher.js
CHANGED
|
@@ -32,13 +32,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
32
32
|
const path_1 = __importDefault(require("path"));
|
|
33
33
|
const chokidar_1 = __importDefault(require("chokidar"));
|
|
34
34
|
const paths_1 = require("./paths");
|
|
35
|
-
const
|
|
36
|
-
'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
|
|
37
|
-
'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
|
|
38
|
-
'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
39
|
-
'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
40
|
-
};
|
|
41
|
-
const DEFAULT_PRICING = PRICING['claude-sonnet-4-6'];
|
|
35
|
+
const pricing_1 = require("./pricing");
|
|
42
36
|
// ─── Context window dinámico ──────────────────────────────────────────────────
|
|
43
37
|
const KNOWN_CONTEXT_WINDOWS = {
|
|
44
38
|
'claude-opus-4-6': 200000,
|
|
@@ -48,16 +42,8 @@ const KNOWN_CONTEXT_WINDOWS = {
|
|
|
48
42
|
function getContextWindow(model) {
|
|
49
43
|
return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
|
|
50
44
|
}
|
|
51
|
-
// ─── Calculo de coste ─────────────────────────────────────────────────────────
|
|
52
|
-
function calcCost(model, usage) {
|
|
53
|
-
const price = PRICING[model] ?? DEFAULT_PRICING;
|
|
54
|
-
const M = 1000000;
|
|
55
|
-
return ((usage.input_tokens * price.input) / M +
|
|
56
|
-
(usage.output_tokens * price.output) / M +
|
|
57
|
-
(usage.cache_read_input_tokens * price.cacheRead) / M +
|
|
58
|
-
(usage.cache_creation_input_tokens * price.cacheCreate) / M);
|
|
59
|
-
}
|
|
60
45
|
const fileOffsets = new Map();
|
|
46
|
+
const fileLocks = new Map(); // Lock per file
|
|
61
47
|
const FILE_OFFSET_TTL = 30 * 60000; // 30 minutos
|
|
62
48
|
function cleanupStaleOffsets() {
|
|
63
49
|
const now = Date.now();
|
|
@@ -67,6 +53,10 @@ function cleanupStaleOffsets() {
|
|
|
67
53
|
}
|
|
68
54
|
}
|
|
69
55
|
async function processJSONL(filePath) {
|
|
56
|
+
// Skip if already processing this file
|
|
57
|
+
if (fileLocks.has(filePath))
|
|
58
|
+
return null;
|
|
59
|
+
fileLocks.set(filePath, Promise.resolve());
|
|
70
60
|
let fileContent;
|
|
71
61
|
try {
|
|
72
62
|
fileContent = await promises_1.default.readFile(filePath, 'utf8');
|
|
@@ -99,27 +89,27 @@ async function processJSONL(filePath) {
|
|
|
99
89
|
if (obj.type !== 'assistant')
|
|
100
90
|
continue;
|
|
101
91
|
const msg = obj.message;
|
|
102
|
-
|
|
103
|
-
const model = msg?.model ?? undefined;
|
|
104
|
-
if (!usage)
|
|
92
|
+
if (!msg?.usage)
|
|
105
93
|
continue;
|
|
94
|
+
const usage = msg.usage;
|
|
95
|
+
const model = msg.model ?? 'claude-sonnet-4-6';
|
|
106
96
|
if (firstTs === undefined && obj.timestamp) {
|
|
107
97
|
try {
|
|
108
98
|
firstTs = new Date(obj.timestamp).getTime();
|
|
109
99
|
}
|
|
110
|
-
catch { }
|
|
100
|
+
catch (e) { /* ignore invalid timestamp */ }
|
|
111
101
|
}
|
|
112
102
|
totals.input_tokens += usage.input_tokens ?? 0;
|
|
113
103
|
totals.output_tokens += usage.output_tokens ?? 0;
|
|
114
104
|
totals.cache_read += usage.cache_read_input_tokens ?? 0;
|
|
115
105
|
totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
|
|
116
106
|
const resolvedModel = model ?? 'claude-sonnet-4-6';
|
|
117
|
-
totals.cost_usd += calcCost(resolvedModel, usage);
|
|
107
|
+
totals.cost_usd += (0, pricing_1.calcCost)(resolvedModel, usage);
|
|
118
108
|
totals.context_used = (usage.input_tokens ?? 0)
|
|
119
109
|
+ (usage.cache_read_input_tokens ?? 0)
|
|
120
110
|
+ (usage.cache_creation_input_tokens ?? 0);
|
|
121
111
|
totals.context_window = getContextWindow(resolvedModel);
|
|
122
|
-
const price = PRICING[resolvedModel] ?? DEFAULT_PRICING;
|
|
112
|
+
const price = pricing_1.PRICING[resolvedModel] ?? pricing_1.DEFAULT_PRICING;
|
|
123
113
|
const M = 1000000;
|
|
124
114
|
lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
|
|
125
115
|
(usage.cache_read_input_tokens ?? 0) * price.cacheRead +
|
|
@@ -129,7 +119,9 @@ async function processJSONL(filePath) {
|
|
|
129
119
|
lastOutputTokens = usage.output_tokens ?? 0;
|
|
130
120
|
lastModel = model ?? lastModel;
|
|
131
121
|
}
|
|
132
|
-
catch {
|
|
122
|
+
catch (e) {
|
|
123
|
+
console.warn('[enricher] Error calculating cost:', e);
|
|
124
|
+
}
|
|
133
125
|
}
|
|
134
126
|
if (lastInputUsd + lastOutputUsd > 0) {
|
|
135
127
|
totals.lastEntry = {
|
|
@@ -143,14 +135,21 @@ async function processJSONL(filePath) {
|
|
|
143
135
|
totals.lastModel = lastModel;
|
|
144
136
|
totals.firstTs = firstTs;
|
|
145
137
|
fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
|
|
138
|
+
fileLocks.delete(filePath);
|
|
146
139
|
return totals;
|
|
147
140
|
}
|
|
148
141
|
const blockCostCache = new Map();
|
|
142
|
+
const costCacheLocks = new Map(); // Simple lock flag
|
|
149
143
|
const BLOCK_COST_TTL = 5 * 60000;
|
|
150
144
|
async function getAllBlockCostsForSession(sessionId) {
|
|
145
|
+
// Return cached if available and not expired
|
|
151
146
|
const cached = blockCostCache.get(sessionId);
|
|
152
147
|
if (cached && Date.now() - cached.ts < BLOCK_COST_TTL)
|
|
153
148
|
return cached.data;
|
|
149
|
+
// Skip if already calculating for this session
|
|
150
|
+
if (costCacheLocks.get(sessionId))
|
|
151
|
+
return cached?.data ?? [];
|
|
152
|
+
costCacheLocks.set(sessionId, true);
|
|
154
153
|
try {
|
|
155
154
|
if (!fs_1.default.existsSync(PROJECTS_DIR))
|
|
156
155
|
return [];
|
|
@@ -199,7 +198,7 @@ async function getAllBlockCostsForSession(sessionId) {
|
|
|
199
198
|
const model = obj.message?.model ?? 'claude-sonnet-4-6';
|
|
200
199
|
if (!usage)
|
|
201
200
|
continue;
|
|
202
|
-
const price = PRICING[model] ?? DEFAULT_PRICING;
|
|
201
|
+
const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
|
|
203
202
|
const M = 1000000;
|
|
204
203
|
const inUsd = ((usage.input_tokens ?? 0) * price.input +
|
|
205
204
|
(usage.cache_read_input_tokens ?? 0) * price.cacheRead +
|
|
@@ -214,15 +213,22 @@ async function getAllBlockCostsForSession(sessionId) {
|
|
|
214
213
|
current.outputTokens += outTok;
|
|
215
214
|
}
|
|
216
215
|
}
|
|
217
|
-
catch {
|
|
216
|
+
catch (e) {
|
|
217
|
+
console.warn('[enricher] Error reading JSONL block:', e);
|
|
218
|
+
}
|
|
218
219
|
}
|
|
219
220
|
const filtered = result.filter(b => b.totalUsd > 0);
|
|
220
221
|
blockCostCache.set(sessionId, { data: filtered, ts: Date.now() });
|
|
221
222
|
return filtered;
|
|
222
223
|
}
|
|
223
224
|
}
|
|
224
|
-
catch {
|
|
225
|
-
|
|
225
|
+
catch (e) {
|
|
226
|
+
console.warn('[enricher] Error calculating block costs:', e);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
costCacheLocks.delete(sessionId);
|
|
230
|
+
}
|
|
231
|
+
return cached?.data ?? [];
|
|
226
232
|
}
|
|
227
233
|
async function getSessionPrompts(sessionId) {
|
|
228
234
|
try {
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,8 @@ 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
|
+
const roast_1 = require("./roast");
|
|
30
32
|
const paths_1 = require("./paths");
|
|
31
33
|
const program = new commander_1.Command();
|
|
32
34
|
const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
@@ -144,6 +146,7 @@ program
|
|
|
144
146
|
.command('status')
|
|
145
147
|
.description('Show current quota, cost and burn rate')
|
|
146
148
|
.option('--json', 'Output raw JSON instead of formatted text')
|
|
149
|
+
.option('--compact', 'One-line output for tmux')
|
|
147
150
|
.action(async (opts) => {
|
|
148
151
|
try {
|
|
149
152
|
const [quotaRes, healthRes] = await Promise.all([
|
|
@@ -154,6 +157,12 @@ program
|
|
|
154
157
|
throw new Error('Daemon unavailable');
|
|
155
158
|
const q = await quotaRes.json();
|
|
156
159
|
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
|
+
}
|
|
157
166
|
if (opts.json) {
|
|
158
167
|
console.log(JSON.stringify({
|
|
159
168
|
cyclePrompts: q.cyclePrompts,
|
|
@@ -167,7 +176,7 @@ program
|
|
|
167
176
|
weeklyLimitOpus: q.weeklyLimitOpus,
|
|
168
177
|
burnRateTokensPerMin: q.burnRateTokensPerMin,
|
|
169
178
|
}));
|
|
170
|
-
|
|
179
|
+
process.exit(0);
|
|
171
180
|
}
|
|
172
181
|
const R = '\x1b[0m';
|
|
173
182
|
const pctColor = q.cyclePct >= 95 ? '\x1b[31m'
|
|
@@ -316,4 +325,37 @@ program
|
|
|
316
325
|
console.error('\n❌ Error:', err.message);
|
|
317
326
|
process.exit(1);
|
|
318
327
|
}));
|
|
328
|
+
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) => {
|
|
334
|
+
try {
|
|
335
|
+
const format = (opts.format ?? 'ascii');
|
|
336
|
+
const copy = !!opts.copy;
|
|
337
|
+
await (0, share_1.runShare)({ sessionId, format, copy });
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
console.error('\n❌ Error:', err.message);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
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')
|
|
350
|
+
.action(async (opts) => {
|
|
351
|
+
try {
|
|
352
|
+
const months = parseInt(opts.months || '1', 10);
|
|
353
|
+
await (0, roast_1.runRoast)({ stats: !!opts.stats, months });
|
|
354
|
+
process.exit(0);
|
|
355
|
+
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
console.error('\n❌ Error:', err.message);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
319
361
|
program.parse();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pricing.ts — Model pricing constants and cost calculation
|
|
3
|
+
*
|
|
4
|
+
* Centralized pricing data to avoid duplication across enricher.ts and project-scanner.ts.
|
|
5
|
+
* Prices are in USD per million tokens.
|
|
6
|
+
*/
|
|
7
|
+
export interface ModelPricing {
|
|
8
|
+
input: number;
|
|
9
|
+
output: number;
|
|
10
|
+
cacheRead: number;
|
|
11
|
+
cacheCreate: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const PRICING: Record<string, ModelPricing>;
|
|
14
|
+
export declare const DEFAULT_PRICING: ModelPricing;
|
|
15
|
+
export declare function calcCost(model: string, usage: {
|
|
16
|
+
input_tokens: number;
|
|
17
|
+
output_tokens: number;
|
|
18
|
+
cache_read_input_tokens: number;
|
|
19
|
+
cache_creation_input_tokens: number;
|
|
20
|
+
}): number;
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* pricing.ts — Model pricing constants and cost calculation
|
|
4
|
+
*
|
|
5
|
+
* Centralized pricing data to avoid duplication across enricher.ts and project-scanner.ts.
|
|
6
|
+
* Prices are in USD per million tokens.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DEFAULT_PRICING = exports.PRICING = void 0;
|
|
10
|
+
exports.calcCost = calcCost;
|
|
11
|
+
exports.PRICING = {
|
|
12
|
+
'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
|
|
13
|
+
'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
|
|
14
|
+
'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
15
|
+
'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
16
|
+
};
|
|
17
|
+
exports.DEFAULT_PRICING = exports.PRICING['claude-sonnet-4-6'];
|
|
18
|
+
function calcCost(model, usage) {
|
|
19
|
+
const price = exports.PRICING[model] ?? exports.DEFAULT_PRICING;
|
|
20
|
+
const M = 1000000;
|
|
21
|
+
return ((usage.input_tokens * price.input) / M +
|
|
22
|
+
(usage.output_tokens * price.output) / M +
|
|
23
|
+
(usage.cache_read_input_tokens * price.cacheRead) / M +
|
|
24
|
+
(usage.cache_creation_input_tokens * price.cacheCreate) / M);
|
|
25
|
+
}
|
package/dist/project-scanner.js
CHANGED
|
@@ -19,6 +19,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
20
|
const os_1 = __importDefault(require("os"));
|
|
21
21
|
const paths_1 = require("./paths");
|
|
22
|
+
const pricing_1 = require("./pricing");
|
|
22
23
|
// ─── Decode ───────────────────────────────────────────────────────────────────
|
|
23
24
|
/**
|
|
24
25
|
* Decodifica el nombre de directorio de Claude Code al path real.
|
|
@@ -143,22 +144,6 @@ function parseHandoffProgress(content) {
|
|
|
143
144
|
}
|
|
144
145
|
// ─── JSONL stats (datos históricos sin daemon) ────────────────────────────────
|
|
145
146
|
const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
|
|
146
|
-
// Precios en USD por millón de tokens (misma tabla que enricher.ts)
|
|
147
|
-
const PRICING = {
|
|
148
|
-
'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
|
|
149
|
-
'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
|
|
150
|
-
'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
151
|
-
'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
|
|
152
|
-
};
|
|
153
|
-
const DEFAULT_PRICING = PRICING['claude-sonnet-4-6'];
|
|
154
|
-
function calcCost(model, usage) {
|
|
155
|
-
const p = PRICING[model] ?? DEFAULT_PRICING;
|
|
156
|
-
const M = 1000000;
|
|
157
|
-
return ((usage.input_tokens * p.input) / M +
|
|
158
|
-
(usage.output_tokens * p.output) / M +
|
|
159
|
-
(usage.cache_read_input_tokens * p.cacheRead) / M +
|
|
160
|
-
(usage.cache_creation_input_tokens * p.cacheCreate) / M);
|
|
161
|
-
}
|
|
162
147
|
/**
|
|
163
148
|
* Lee todos los JSONL del directorio codificado de un proyecto y acumula
|
|
164
149
|
* tokens y coste. No requiere que el daemon haya estado corriendo.
|
|
@@ -196,7 +181,7 @@ function getJSONLStats(encodedDir) {
|
|
|
196
181
|
continue;
|
|
197
182
|
hasAssistant = true;
|
|
198
183
|
const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
199
|
-
totalCost += calcCost(model, usage);
|
|
184
|
+
totalCost += (0, pricing_1.calcCost)(model, usage);
|
|
200
185
|
totalTokens += tokens;
|
|
201
186
|
if (model.includes('opus'))
|
|
202
187
|
modelUsage.opusTokens += tokens;
|
|
@@ -314,7 +299,7 @@ function getJSONLStatsByProject(dirPath) {
|
|
|
314
299
|
continue;
|
|
315
300
|
hasAssistant = true;
|
|
316
301
|
const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
|
|
317
|
-
cost += calcCost(model, usage);
|
|
302
|
+
cost += (0, pricing_1.calcCost)(model, usage);
|
|
318
303
|
tokens += t;
|
|
319
304
|
if (model.includes('opus'))
|
|
320
305
|
mu.opusTokens += t;
|
package/dist/quota-tracker.d.ts
CHANGED
package/dist/quota-tracker.js
CHANGED
|
@@ -26,10 +26,10 @@ const path_1 = __importDefault(require("path"));
|
|
|
26
26
|
const claude_auth_1 = require("./claude-auth");
|
|
27
27
|
const paths_1 = require("./paths");
|
|
28
28
|
const PLAN_LIMITS = {
|
|
29
|
-
free: { prompts5h: 10, weeklyHoursSonnet: 40, weeklyHoursOpus: 0 },
|
|
30
|
-
pro: { prompts5h: 45, weeklyHoursSonnet: 80, weeklyHoursOpus: 0 },
|
|
31
|
-
max5: { prompts5h: 225, weeklyHoursSonnet: 280, weeklyHoursOpus: 35 },
|
|
32
|
-
max20: { prompts5h: 900, weeklyHoursSonnet: 480, weeklyHoursOpus: 40 },
|
|
29
|
+
free: { prompts5h: 10, tokens5h: 100000, weeklyHoursSonnet: 40, weeklyHoursOpus: 0 },
|
|
30
|
+
pro: { prompts5h: 45, tokens5h: 382000, weeklyHoursSonnet: 80, weeklyHoursOpus: 0 },
|
|
31
|
+
max5: { prompts5h: 225, tokens5h: 2000000, weeklyHoursSonnet: 280, weeklyHoursOpus: 35 },
|
|
32
|
+
max20: { prompts5h: 900, tokens5h: 8000000, weeklyHoursSonnet: 480, weeklyHoursOpus: 40 },
|
|
33
33
|
};
|
|
34
34
|
// ─── Helpers de ventanas temporales ──────────────────────────────────────────
|
|
35
35
|
const CYCLE_MS = 5 * 60 * 60 * 1000; // 5 horas en ms
|
|
@@ -253,8 +253,10 @@ function computeQuota(forcePlan) {
|
|
|
253
253
|
const fiveHAgo = now - CYCLE_MS;
|
|
254
254
|
const cycleResetAt = computeResetAt(entries, now);
|
|
255
255
|
const cycleStart = fiveHAgo; // inicio real de la ventana de conteo
|
|
256
|
-
|
|
257
|
-
const
|
|
256
|
+
// Basado en tokens (como claude.ai/settings/usage)
|
|
257
|
+
const cycleEntries = entries.filter(e => e.ts >= fiveHAgo);
|
|
258
|
+
const cycleTokens = cycleEntries.reduce((sum, e) => sum + (e.inputTokens ?? 0) + (e.outputTokens ?? 0), 0);
|
|
259
|
+
const cyclePct = Math.min(100, Math.round(cycleTokens / limits.tokens5h * 100));
|
|
258
260
|
const cycleResetMs = Math.max(0, cycleResetAt - now);
|
|
259
261
|
// ─ Semanal por modelo: ventanas de 5 min con actividad ─
|
|
260
262
|
// Contamos ventanas de 5 min distintas con al menos 1 respuesta por modelo
|
|
@@ -292,9 +294,11 @@ function computeQuota(forcePlan) {
|
|
|
292
294
|
? Math.round(totalRecentTok / 30)
|
|
293
295
|
: 0;
|
|
294
296
|
const data = {
|
|
295
|
-
cyclePrompts,
|
|
297
|
+
cyclePrompts: cycleEntries.filter(e => e.type === 'human').length,
|
|
296
298
|
cycleLimit: limits.prompts5h,
|
|
297
299
|
cyclePct,
|
|
300
|
+
cycleTokens,
|
|
301
|
+
cycleLimitTokens: limits.tokens5h,
|
|
298
302
|
cycleResetMs,
|
|
299
303
|
cycleResetAt,
|
|
300
304
|
cycleStartTs: cycleStart,
|
package/dist/render.d.ts
CHANGED
package/dist/render.js
CHANGED
|
@@ -136,10 +136,10 @@ function renderTrace(state) {
|
|
|
136
136
|
const remaining = 100 - pct;
|
|
137
137
|
lines.push(` ${C.dim}auto-compact en:${C.reset} ${bar} ` +
|
|
138
138
|
`${barColor}${remaining}% restante${C.reset} ` +
|
|
139
|
-
`${C.dim}${fmtTok(cost.context_used)} / ${fmtTok(cost.context_window)} tokens
|
|
139
|
+
`${C.dim}${fmtTok(cost.context_used)} / ${fmtTok(cost.context_window)} tokens used${C.reset}`);
|
|
140
140
|
}
|
|
141
141
|
else {
|
|
142
|
-
lines.push(` ${C.dim}
|
|
142
|
+
lines.push(` ${C.dim}context: calculating...${C.reset}`);
|
|
143
143
|
}
|
|
144
144
|
lines.push(C.dim + '─'.repeat(72) + C.reset);
|
|
145
145
|
// ── Bloques de respuesta ──────────────────────────────────────────────────
|
|
@@ -199,20 +199,25 @@ function renderTrace(state) {
|
|
|
199
199
|
const score = (cost.efficiency_score === 0 && cost.cost_usd < 0.001) ? 100 : cost.efficiency_score;
|
|
200
200
|
const scoreColor = score >= 90 ? C.green : score >= 70 ? C.yellow : C.red;
|
|
201
201
|
const scoreBar = progressBar(score, 14, scoreColor);
|
|
202
|
+
// Barra de current cycle (5h quota)
|
|
203
|
+
const cyclePct = state.cyclePct ?? 0;
|
|
204
|
+
const pctColor = cyclePct >= 90 ? C.red : cyclePct >= 70 ? C.yellow : C.green;
|
|
205
|
+
const pctBar = progressBar(cyclePct, 7, pctColor);
|
|
202
206
|
// Tokens
|
|
203
207
|
const tokenLine = `${C.dim}↑${C.reset}${fmtTok(cost.input_tokens)} ` +
|
|
204
208
|
`${C.dim}↓${C.reset}${fmtTok(cost.output_tokens)} ` +
|
|
205
209
|
`${C.dim}🗄${C.reset}${fmtTok(cost.cache_read)}`;
|
|
206
210
|
lines.push(` ${C.bold}💰 $${cost.cost_usd.toFixed(4)}${C.reset} ` +
|
|
207
211
|
`${tokenLine} ` +
|
|
208
|
-
`
|
|
212
|
+
`current: ${pctBar} ${pctColor}${cyclePct}% | efficiency: ${scoreBar} ${scoreColor}${score}/100${C.reset}`);
|
|
209
213
|
}
|
|
210
214
|
else {
|
|
211
215
|
const totalDone = events.filter(e => e.type === 'Done').length;
|
|
212
216
|
const elapsed = fmtMs((events.at(-1)?.ts ?? startedAt) - startedAt);
|
|
213
|
-
|
|
217
|
+
const cyclePct = state.cyclePct ?? 0;
|
|
218
|
+
lines.push(` ${C.dim}⏱ ${elapsed} ✅ ${totalDone} tools 💰 calculating... current: ${cyclePct}%${C.reset}`);
|
|
214
219
|
}
|
|
215
|
-
// ──
|
|
220
|
+
// ── Weekly bar (stats-cache.json) ──────────────────────────────────────
|
|
216
221
|
if (state.weekly && state.weekly.totalTokens > 0) {
|
|
217
222
|
const { totalTokens, byDay, lastUpdated } = state.weekly;
|
|
218
223
|
// Mini sparkline: un char por día de la semana
|
|
@@ -220,9 +225,9 @@ function renderTrace(state) {
|
|
|
220
225
|
const BARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
221
226
|
const spark = byDay.map(d => BARS[Math.min(7, Math.floor(d.tokens / maxDay * 7))]).join('');
|
|
222
227
|
const padded = spark.padStart(7, '▁'); // garantizar 7 chars (1 por día)
|
|
223
|
-
lines.push(` ${C.dim}
|
|
228
|
+
lines.push(` ${C.dim}weekly:${C.reset} ${C.cyan}${padded}${C.reset} ` +
|
|
224
229
|
`${C.bold}${fmtTok(totalTokens)} tokens${C.reset} ` +
|
|
225
|
-
`${C.dim}(
|
|
230
|
+
`${C.dim}(last 7 days${lastUpdated ? ' · data from ' + lastUpdated : ''})${C.reset}`);
|
|
226
231
|
}
|
|
227
232
|
lines.push('');
|
|
228
233
|
return lines.join('\n');
|
package/dist/roast.d.ts
ADDED
package/dist/roast.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRoast = runRoast;
|
|
4
|
+
const db_js_1 = require("./db.js");
|
|
5
|
+
function formatMinutes(totalMinutes) {
|
|
6
|
+
if (totalMinutes < 60)
|
|
7
|
+
return `${Math.round(totalMinutes)} minutes`;
|
|
8
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
9
|
+
if (hours < 24)
|
|
10
|
+
return `${hours} hours`;
|
|
11
|
+
const days = Math.floor(hours / 24);
|
|
12
|
+
return `${days} days`;
|
|
13
|
+
}
|
|
14
|
+
function getRoastRating(avgEfficiency) {
|
|
15
|
+
if (avgEfficiency >= 90)
|
|
16
|
+
return "You're a machine. Or maybe you're just not using Claude enough. 😏";
|
|
17
|
+
if (avgEfficiency >= 70)
|
|
18
|
+
return "Solid. Not great, not terrible. The AI equivalent of a C+ student.";
|
|
19
|
+
if (avgEfficiency >= 50)
|
|
20
|
+
return "Room for growth, champ. 📈";
|
|
21
|
+
return "Oof. That's a lot of money down the drain. Are you okay? 💀";
|
|
22
|
+
}
|
|
23
|
+
function getRoastMessage(data) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
lines.push('🔥 Your Claude Code Roast');
|
|
26
|
+
lines.push('');
|
|
27
|
+
if (data.totalBashCalls > 0) {
|
|
28
|
+
const minutesPerCall = (data.days * 24 * 60) / data.totalBashCalls;
|
|
29
|
+
if (minutesPerCall < 60) {
|
|
30
|
+
lines.push(` You called Bash ${data.totalBashCalls} times in ${data.days} days.`);
|
|
31
|
+
lines.push(` That's once every ${minutesPerCall.toFixed(1)} minutes.`);
|
|
32
|
+
lines.push(' Are you okay?');
|
|
33
|
+
lines.push('');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (data.contextHits > 0) {
|
|
37
|
+
lines.push(` You hit 90%+ context in ${data.contextHits} sessions.`);
|
|
38
|
+
lines.push(' Claude was writing with amnesia half the time.');
|
|
39
|
+
lines.push('');
|
|
40
|
+
}
|
|
41
|
+
if (data.totalLoops > 0) {
|
|
42
|
+
const loopCost = data.totalCost * 0.15;
|
|
43
|
+
lines.push(` You spent $${loopCost.toFixed(2)} on loops you never noticed.`);
|
|
44
|
+
const coffees = Math.floor(loopCost / 0.3);
|
|
45
|
+
if (coffees > 0) {
|
|
46
|
+
lines.push(` That's ${coffees} coffees. Just saying.`);
|
|
47
|
+
lines.push('');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
lines.push(` Efficiency score: ${Math.round(data.avgEfficiency)}/100`);
|
|
51
|
+
lines.push(` ${getRoastRating(data.avgEfficiency)}`);
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
async function runRoast(opts) {
|
|
55
|
+
const days = opts.months ?? 30;
|
|
56
|
+
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
57
|
+
const sessions = db_js_1.dbOps.getAllSessions(500).filter(s => s.started_at >= sinceMs);
|
|
58
|
+
let totalBashCalls = 0;
|
|
59
|
+
let totalLoops = 0;
|
|
60
|
+
let contextHits = 0;
|
|
61
|
+
for (const session of sessions) {
|
|
62
|
+
totalLoops += session.loops_detected || 0;
|
|
63
|
+
if ((session.total_input_tokens || 0) + (session.total_output_tokens || 0) > 150000) {
|
|
64
|
+
contextHits++;
|
|
65
|
+
}
|
|
66
|
+
const events = db_js_1.dbOps.getSessionEvents(session.id);
|
|
67
|
+
const bashCalls = events.filter(e => e.type === 'Done' && e.tool_name === 'Bash').length;
|
|
68
|
+
totalBashCalls += bashCalls;
|
|
69
|
+
}
|
|
70
|
+
const totalCost = sessions.reduce((a, s) => a + (s.total_cost_usd || 0), 0);
|
|
71
|
+
const avgEfficiency = sessions.length > 0
|
|
72
|
+
? sessions.reduce((a, s) => a + (s.efficiency_score || 0), 0) / sessions.length
|
|
73
|
+
: 100;
|
|
74
|
+
const data = {
|
|
75
|
+
totalCost,
|
|
76
|
+
totalSessions: sessions.length,
|
|
77
|
+
totalBashCalls,
|
|
78
|
+
totalLoops,
|
|
79
|
+
avgEfficiency,
|
|
80
|
+
contextHits,
|
|
81
|
+
days,
|
|
82
|
+
};
|
|
83
|
+
if (opts.stats) {
|
|
84
|
+
console.log(`=== Claude Code Stats (${days} days) ===`);
|
|
85
|
+
console.log(`Sessions: ${sessions.length}`);
|
|
86
|
+
console.log(`Total cost: $${totalCost.toFixed(2)}`);
|
|
87
|
+
console.log(`Bash calls: ${totalBashCalls}`);
|
|
88
|
+
console.log(`Loops: ${totalLoops}`);
|
|
89
|
+
console.log(`Effficiency: ${Math.round(avgEfficiency)}/100`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(getRoastMessage(data));
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(' github.com/DeibyGS/claudestat');
|
|
95
|
+
}
|
package/dist/routes/events.js
CHANGED
|
@@ -93,7 +93,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
93
93
|
// Activar skill parent para los eventos siguientes si este fue un Skill Done
|
|
94
94
|
if (tool_name === 'Skill') {
|
|
95
95
|
try {
|
|
96
|
-
const inp = typeof tool_input === 'object' ? tool_input : JSON.parse(tool_input
|
|
96
|
+
const inp = typeof tool_input === 'object' ? tool_input : (typeof tool_input === 'string' ? JSON.parse(tool_input) : {});
|
|
97
97
|
activeSkillBySession.set(session_id, inp?.skill || inp?.name || 'skill');
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
@@ -123,9 +123,9 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
123
123
|
const FILE_TOOLS = new Set(['Read', 'Write', 'Edit', 'Glob', 'Grep']);
|
|
124
124
|
if (FILE_TOOLS.has(tool_name || '') && tool_input) {
|
|
125
125
|
try {
|
|
126
|
-
const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : tool_input;
|
|
127
|
-
const filePath =
|
|
128
|
-
if (filePath
|
|
126
|
+
const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : (tool_input ?? {});
|
|
127
|
+
const filePath = inp?.file_path ?? inp?.path;
|
|
128
|
+
if (typeof filePath === 'string' && filePath.startsWith('/')) {
|
|
129
129
|
const projectCwd = findProjectCwdForFile(filePath);
|
|
130
130
|
if (projectCwd)
|
|
131
131
|
db_1.dbOps.updateSessionProject(session_id, projectCwd);
|
package/dist/share.d.ts
ADDED
package/dist/share.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runShare = runShare;
|
|
4
|
+
const db_js_1 = require("./db.js");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
function formatDuration(ms) {
|
|
7
|
+
const seconds = Math.floor(ms / 1000);
|
|
8
|
+
const minutes = Math.floor(seconds / 60);
|
|
9
|
+
const hours = Math.floor(minutes / 60);
|
|
10
|
+
if (hours > 0) {
|
|
11
|
+
const mins = minutes % 60;
|
|
12
|
+
return `${hours}h ${mins}m`;
|
|
13
|
+
}
|
|
14
|
+
return `${minutes}m`;
|
|
15
|
+
}
|
|
16
|
+
async function getSessionData(sessionId) {
|
|
17
|
+
const sessions = db_js_1.dbOps.getAllSessions(1);
|
|
18
|
+
let session;
|
|
19
|
+
if (sessionId) {
|
|
20
|
+
session = db_js_1.dbOps.getSession(sessionId);
|
|
21
|
+
if (!session)
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
else if (sessions.length > 0) {
|
|
25
|
+
session = sessions[0];
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const events = db_js_1.dbOps.getSessionEvents(session.id);
|
|
31
|
+
const toolCalls = events.filter((e) => e.type === 'Done' && e.tool_name);
|
|
32
|
+
const totalTokens = (session.total_input_tokens || 0) + (session.total_output_tokens || 0);
|
|
33
|
+
const cacheTokens = (session.total_cache_read || 0) + (session.total_cache_creation || 0);
|
|
34
|
+
const cacheSavedCost = (session.total_cost_usd || 0) * 0.1 * 0.9;
|
|
35
|
+
const toolCounts = {};
|
|
36
|
+
toolCalls.forEach((e) => {
|
|
37
|
+
const tool = e.tool_name || 'Unknown';
|
|
38
|
+
toolCounts[tool] = (toolCounts[tool] || 0) + 1;
|
|
39
|
+
});
|
|
40
|
+
let topTool = '—';
|
|
41
|
+
let topToolPct = 0;
|
|
42
|
+
if (Object.keys(toolCounts).length > 0) {
|
|
43
|
+
const sorted = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
|
|
44
|
+
topTool = sorted[0][0];
|
|
45
|
+
topToolPct = Math.round((sorted[0][1] / toolCalls.length) * 100);
|
|
46
|
+
}
|
|
47
|
+
const durationMs = (session.last_event_at || session.started_at) - session.started_at;
|
|
48
|
+
const project = session.project_path?.split('/').pop() || 'unknown';
|
|
49
|
+
return {
|
|
50
|
+
id: session.id,
|
|
51
|
+
project: project.length > 18 ? project.slice(0, 15) + '...' : project,
|
|
52
|
+
duration: formatDuration(durationMs),
|
|
53
|
+
tools: toolCalls.length,
|
|
54
|
+
cost: `$${(session.total_cost_usd || 0).toFixed(2)}`,
|
|
55
|
+
cacheSaved: `$${cacheSavedCost.toFixed(2)}`,
|
|
56
|
+
cachePct: Math.min(100, totalTokens > 0 ? Math.round((cacheTokens / totalTokens) * 100) : 0),
|
|
57
|
+
topTool: topTool.length > 12 ? topTool.slice(0, 9) + '...' : topTool,
|
|
58
|
+
topToolPct,
|
|
59
|
+
efficiency: session.efficiency_score || 100,
|
|
60
|
+
efficiencyEmoji: (session.efficiency_score || 100) >= 90 ? '🔥' : (session.efficiency_score || 100) >= 70 ? '👍' : '💀',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function generateAsciiCard(data) {
|
|
64
|
+
const FIELD_WIDTH = 25;
|
|
65
|
+
const toolsLabel = `${data.tools} calls`;
|
|
66
|
+
const cacheLabel = `${data.cachePct}% saved (${data.cacheSaved})`;
|
|
67
|
+
const topLabel = `${data.topTool} (${data.topToolPct}%)`;
|
|
68
|
+
const effLabel = `${data.efficiency} / 100 ${data.efficiencyEmoji}`;
|
|
69
|
+
const lines = [
|
|
70
|
+
'╔═══════════════════════════════════╗',
|
|
71
|
+
'║ Session Report · claudestat ║',
|
|
72
|
+
'╠═══════════════════════════════════╣',
|
|
73
|
+
`║ Project ${data.project.padEnd(FIELD_WIDTH)}║`,
|
|
74
|
+
`║ Duration ${data.duration.padEnd(FIELD_WIDTH)}║`,
|
|
75
|
+
`║ Tools ${toolsLabel.padEnd(FIELD_WIDTH)}║`,
|
|
76
|
+
`║ Cost ${data.cost.padEnd(FIELD_WIDTH)}║`,
|
|
77
|
+
`║ Cache hit ${cacheLabel.padEnd(FIELD_WIDTH)}║`,
|
|
78
|
+
`║ Top tool ${topLabel.padEnd(FIELD_WIDTH)}║`,
|
|
79
|
+
`║ Efficiency ${effLabel.padEnd(FIELD_WIDTH)}║`,
|
|
80
|
+
'╚═══════════════════════════════════╝',
|
|
81
|
+
' github.com/DeibyGS/claudestat',
|
|
82
|
+
];
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
function generateJson(data) {
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
project: data.project,
|
|
88
|
+
duration: data.duration,
|
|
89
|
+
tools: data.tools,
|
|
90
|
+
cost: data.cost,
|
|
91
|
+
cache_saved: data.cacheSaved,
|
|
92
|
+
cache_pct: data.cachePct,
|
|
93
|
+
top_tool: data.topTool,
|
|
94
|
+
top_tool_pct: data.topToolPct,
|
|
95
|
+
efficiency: data.efficiency,
|
|
96
|
+
}, null, 2);
|
|
97
|
+
}
|
|
98
|
+
async function runShare(args) {
|
|
99
|
+
const data = await getSessionData(args.sessionId);
|
|
100
|
+
if (!data) {
|
|
101
|
+
console.error('Error: No sessions found. Run Claude Code first.');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const output = args.format === 'json' ? generateJson(data) : generateAsciiCard(data);
|
|
105
|
+
console.log(output);
|
|
106
|
+
if (args.copy) {
|
|
107
|
+
try {
|
|
108
|
+
const p = (0, child_process_1.spawn)('pbcopy', [], { stdio: ['pipe', process.stderr, process.stderr] });
|
|
109
|
+
p.stdin.write(output);
|
|
110
|
+
p.stdin.end();
|
|
111
|
+
p.on('close', () => console.log('\n✓ Copied to clipboard'));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
console.warn('⚠ Clipboard not available (macOS only)');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/watch.js
CHANGED
|
@@ -63,8 +63,23 @@ async function startWatch() {
|
|
|
63
63
|
}
|
|
64
64
|
let state = {
|
|
65
65
|
sessionId: '', cwd: '', startedAt: Date.now(), events: [],
|
|
66
|
-
weekly: (0, weekly_1.readWeeklyStats)()
|
|
66
|
+
weekly: (0, weekly_1.readWeeklyStats)(),
|
|
67
|
+
cyclePct: 0
|
|
67
68
|
};
|
|
69
|
+
// Fetch quota para cyclePct cada 30 segundos
|
|
70
|
+
async function fetchQuota() {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch('http://localhost:7337/quota');
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
const q = await res.json();
|
|
75
|
+
return q.cyclePct;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
fetchQuota().then(pct => { state.cyclePct = pct; });
|
|
82
|
+
setInterval(async () => { state.cyclePct = await fetchQuota(); }, 30000);
|
|
68
83
|
// Refrescar stats semanales cada 5 minutos
|
|
69
84
|
setInterval(() => { state.weekly = (0, weekly_1.readWeeklyStats)(); }, 5 * 60 * 1000);
|
|
70
85
|
function draw() {
|
|
@@ -80,16 +95,19 @@ async function startWatch() {
|
|
|
80
95
|
sessionId: msg.session.id,
|
|
81
96
|
cwd: msg.session.cwd || '',
|
|
82
97
|
startedAt: msg.session.started_at,
|
|
83
|
-
events: (msg.events
|
|
84
|
-
cost: buildCostFromSession(msg.session)
|
|
98
|
+
events: Array.isArray(msg.events) ? msg.events : [],
|
|
99
|
+
cost: buildCostFromSession(msg.session),
|
|
100
|
+
cyclePct: state.cyclePct // preservar de inicial o anterior
|
|
85
101
|
};
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
104
|
else if (msg.type === 'event') {
|
|
105
|
+
if (!msg.payload?.session_id)
|
|
106
|
+
return;
|
|
89
107
|
const evt = msg.payload;
|
|
90
108
|
// Nueva sesión → resetear estado
|
|
91
109
|
if (evt.session_id && evt.session_id !== state.sessionId && state.sessionId !== '') {
|
|
92
|
-
state = { sessionId: evt.session_id, cwd: evt.cwd || '', startedAt: evt.ts, events: [] };
|
|
110
|
+
state = { sessionId: evt.session_id, cwd: evt.cwd || '', startedAt: evt.ts, events: [], cyclePct: state.cyclePct };
|
|
93
111
|
}
|
|
94
112
|
else if (!state.sessionId && evt.session_id) {
|
|
95
113
|
state.sessionId = evt.session_id;
|
|
@@ -98,7 +116,7 @@ async function startWatch() {
|
|
|
98
116
|
}
|
|
99
117
|
if (evt.type === 'Done' && evt.tool_name) {
|
|
100
118
|
// Actualizar el PreToolUse pendiente a Done
|
|
101
|
-
const pending = [...state.events].reverse()
|
|
119
|
+
const pending = [...(state.events ?? [])].reverse()
|
|
102
120
|
.find(e => e.type === 'PreToolUse' && e.tool_name === evt.tool_name);
|
|
103
121
|
if (pending) {
|
|
104
122
|
pending.type = 'Done';
|
|
@@ -131,8 +149,12 @@ async function startWatch() {
|
|
|
131
149
|
}
|
|
132
150
|
clearScreen();
|
|
133
151
|
process.stdout.write('\x1b[36m● claudestat watch\x1b[0m — connecting...\n');
|
|
152
|
+
// Fetch quota antes del primer render para evitar 0%
|
|
153
|
+
const initialCyclePct = await fetchQuota();
|
|
134
154
|
while (true) {
|
|
135
155
|
try {
|
|
156
|
+
// Inicializar state con cyclePct obtenido
|
|
157
|
+
state.cyclePct = initialCyclePct;
|
|
136
158
|
await connectSSE(handleMessage);
|
|
137
159
|
}
|
|
138
160
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statforge/claudestat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Observability layer for Claude Code — live token tracking, cost analytics, quota guard, loop detection, and usage dashboard. The htop for Claude Code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"dev": "tsx src/index.ts",
|
|
59
59
|
"dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
|
|
60
60
|
"start": "node dist/index.js",
|
|
61
|
-
"test": "
|
|
61
|
+
"test": "bash run-tests.sh"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/sdk": "^0.88.0",
|