@statforge/claudestat 1.1.0 → 1.2.0
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 +125 -2
- package/dist/daemon.js +46 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.js +39 -0
- package/dist/index.js +41 -7
- package/dist/insights.d.ts +19 -0
- package/dist/insights.js +105 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +315 -0
- package/dist/quota-tracker.d.ts +1 -0
- package/dist/quota-tracker.js +5 -0
- package/dist/routes/events.js +2 -2
- 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/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)
|
|
@@ -43,7 +43,9 @@ Claude Code is powerful — but it's a black box while it runs. You can't see wh
|
|
|
43
43
|
- Quota guard with configurable kill switch (block new sessions at X%)
|
|
44
44
|
- Pattern analyzer: detects loops, Bash overuse, low cache reuse, and more
|
|
45
45
|
- Per-session cost breakdown + cache savings + burn rate
|
|
46
|
+
- Weekly usage insights with actionable tips
|
|
46
47
|
- AI-generated weekly usage reports
|
|
48
|
+
- MCP server: query quota, sessions, and tools from within Claude Code
|
|
47
49
|
|
|
48
50
|
> If claudestat is useful, give it a ⭐ — it helps other developers find it.
|
|
49
51
|
|
|
@@ -137,9 +139,13 @@ That's it. Start a Claude Code session and watch the events flow in.
|
|
|
137
139
|
| `claudestat uninstall` | Remove hooks from Claude Code |
|
|
138
140
|
| `claudestat watch` | Live terminal trace view |
|
|
139
141
|
| `claudestat status` | Show quota, cost, and burn rate |
|
|
142
|
+
| `claudestat status --compact` | One-line output for tmux status bar |
|
|
140
143
|
| `claudestat config` | View or edit configuration |
|
|
141
144
|
| `claudestat top` | Rank tools by cost, call count, or duration |
|
|
145
|
+
| `claudestat weekly` | Weekly usage summary with actionable tips |
|
|
142
146
|
| `claudestat export [format]` | Export session data to JSON or CSV |
|
|
147
|
+
| `claudestat share [session-id]` | Generate shareable session card (ASCII/JSON) |
|
|
148
|
+
| `claudestat roast` | Sarcastic usage analysis with roast jokes |
|
|
143
149
|
| `claudestat doctor` | Check installation health and diagnose issues |
|
|
144
150
|
|
|
145
151
|
### `claudestat watch`
|
|
@@ -180,6 +186,22 @@ claudestat top
|
|
|
180
186
|
|
|
181
187
|
Options: `--by cost|count|duration` · `--days 7|30|90` · `--limit N`
|
|
182
188
|
|
|
189
|
+
### `claudestat weekly`
|
|
190
|
+
|
|
191
|
+
Weekly usage summary with an actionable tip. Detects patterns like Bash overuse, low efficiency, high session count, and loop frequency.
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
claudestat weekly
|
|
195
|
+
|
|
196
|
+
📊 claudestat weekly insight (May 5 — May 11)
|
|
197
|
+
──────────────────────────────────────────────
|
|
198
|
+
Sessions: 42 · Cost: $146.21 · Loops: 93
|
|
199
|
+
Top tool: Bash (21% of cost) · Efficiency: 93/100
|
|
200
|
+
⚡ Tip: Group bash commands to reduce tool calls — each call costs context
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Options: `--json` for machine-readable output.
|
|
204
|
+
|
|
183
205
|
### `claudestat status`
|
|
184
206
|
|
|
185
207
|
```
|
|
@@ -187,10 +209,72 @@ claudestat status
|
|
|
187
209
|
|
|
188
210
|
Quota 5h 45/50 prompts (90%) | reset in 22m
|
|
189
211
|
Plan MAX5
|
|
190
|
-
|
|
212
|
+
Weekly 3.5h / 40h (9%) this week
|
|
191
213
|
Burn rate 1,240 tok/min
|
|
192
214
|
```
|
|
193
215
|
|
|
216
|
+
### `claudestat status --compact`
|
|
217
|
+
|
|
218
|
+
One-line output for tmux status bar or scripting. Shows cycle quota and weekly usage with colored emoji.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
claudestat status --compact
|
|
222
|
+
C:45%🟡 W:9%🟢 pro
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `claudestat share`
|
|
226
|
+
|
|
227
|
+
Generate a shareable session card — perfect for sharing on social media or in bug reports.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
claudestat share
|
|
231
|
+
╔═══════════════════════════════════╗
|
|
232
|
+
║ Session Report · claudestat ║
|
|
233
|
+
╠═══════════════════════════════════╣
|
|
234
|
+
║ Project my-project ║
|
|
235
|
+
║ Duration 2h 14m ║
|
|
236
|
+
║ Tools 847 calls ║
|
|
237
|
+
║ Cost $0.84 ║
|
|
238
|
+
║ Cache hit 27% saved ($0.31) ║
|
|
239
|
+
║ Top tool Bash (38%) ║
|
|
240
|
+
║ Efficiency 91 / 100 ║
|
|
241
|
+
╚═══════════════════════════════════╝
|
|
242
|
+
github.com/DeibyGS/claudestat
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Options:
|
|
246
|
+
- `--format ascii|json` — output format (default: ascii)
|
|
247
|
+
- `--copy` — copy to clipboard automatically (macOS only)
|
|
248
|
+
|
|
249
|
+
### `claudestat roast`
|
|
250
|
+
|
|
251
|
+
Get a sarcastic analysis of your Claude Code usage — humor with insights.
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
claudestat roast
|
|
255
|
+
|
|
256
|
+
=== Claude Code Stats (last 30 days) ===
|
|
257
|
+
Sessions: 47
|
|
258
|
+
Total cost: $12.40
|
|
259
|
+
Bash calls: 1,240
|
|
260
|
+
Loops: 8
|
|
261
|
+
Efficiency: 72/100
|
|
262
|
+
|
|
263
|
+
🔥 Your Claude Code Roast
|
|
264
|
+
|
|
265
|
+
You called Bash 1,240 times last month.
|
|
266
|
+
That's once every 2.3 minutes.
|
|
267
|
+
Are you okay?
|
|
268
|
+
|
|
269
|
+
You hit 90%+ context in 12 sessions.
|
|
270
|
+
Claude was writing with amnesia half the time.
|
|
271
|
+
|
|
272
|
+
You spent $4.20 on loops you never noticed.
|
|
273
|
+
That's 14 coffees. Just saying.
|
|
274
|
+
|
|
275
|
+
Efficiency: 72/100 — room for growth, champ.
|
|
276
|
+
```
|
|
277
|
+
|
|
194
278
|
### `claudestat doctor`
|
|
195
279
|
|
|
196
280
|
Diagnoses common installation problems — useful if `claudestat start` fails or hooks are not firing.
|
|
@@ -259,6 +343,35 @@ Config is stored at `~/.claudestat/config.json` (macOS/Linux) or `%USERPROFILE%\
|
|
|
259
343
|
|
|
260
344
|
---
|
|
261
345
|
|
|
346
|
+
## MCP Server
|
|
347
|
+
|
|
348
|
+
claudestat includes an MCP (Model Context Protocol) server that lets Claude Code query its own usage stats — Claude can tell you its quota, session cost, and top tools in real time.
|
|
349
|
+
|
|
350
|
+
### Tools exposed
|
|
351
|
+
|
|
352
|
+
| Tool | Description |
|
|
353
|
+
|------|------------|
|
|
354
|
+
| `get_quota_status` | 5h cycle usage %, plan, weekly hours, burn rate |
|
|
355
|
+
| `get_current_session` | Latest session: cost, tokens, efficiency, loops |
|
|
356
|
+
| `get_session_stats` | Aggregated stats for N days |
|
|
357
|
+
| `get_top_tools` | Top 10 tools by cost/count/duration |
|
|
358
|
+
| `get_weekly_insight` | Weekly summary with actionable tip |
|
|
359
|
+
|
|
360
|
+
### Register with Claude Code
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
claude mcp add --transport stdio claudestat -- claudestat-mcp
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Once registered, ask Claude things like:
|
|
367
|
+
- *"What's my current quota status?"*
|
|
368
|
+
- *"Show me my latest session cost"*
|
|
369
|
+
- *"What are my top 5 tools by cost this week?"*
|
|
370
|
+
|
|
371
|
+
Zero extra dependencies — stdio JSON-RPC, works without the daemon running.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
262
375
|
## Dashboard
|
|
263
376
|
|
|
264
377
|
The dashboard lives at `http://localhost:7337` and has six tabs:
|
|
@@ -436,6 +549,16 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
|
|
|
436
549
|
|
|
437
550
|
---
|
|
438
551
|
|
|
552
|
+
## Contributors
|
|
553
|
+
|
|
554
|
+
Thanks to everyone who has contributed to claudestat:
|
|
555
|
+
|
|
556
|
+
[Deiby Gorrin](https://github.com/DeibyGS) — creator and maintainer
|
|
557
|
+
|
|
558
|
+
Want to appear here? Pick a [good-first-issue](https://github.com/DeibyGS/claudestat/labels/good-first-issue) and open a PR.
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
439
562
|
## FAQ
|
|
440
563
|
|
|
441
564
|
**What is claudestat?**
|
package/dist/daemon.js
CHANGED
|
@@ -13,6 +13,39 @@
|
|
|
13
13
|
* - Endpoint GET /meta-stats: KPIs de HANDOFF, Engram, config y alertas
|
|
14
14
|
* - Procesa JSONL al conectar nuevo cliente SSE (contexto inmediato)
|
|
15
15
|
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
16
49
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
50
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
51
|
};
|
|
@@ -199,6 +232,19 @@ function startDaemon() {
|
|
|
199
232
|
console.log(`\n● claudestat daemon → http://localhost:${PORT}`);
|
|
200
233
|
console.log(` Waiting for Claude Code events...\n`);
|
|
201
234
|
console.log(` In another terminal: \x1b[36mclaudestat watch\x1b[0m\n`);
|
|
235
|
+
// Weekly insight — se muestra una vez por semana al iniciar el daemon
|
|
236
|
+
Promise.resolve().then(() => __importStar(require('./insights'))).then(({ getWeeklyInsightData, shouldShowInsight, markInsightShown, renderWeeklyInsight }) => {
|
|
237
|
+
try {
|
|
238
|
+
if (!shouldShowInsight())
|
|
239
|
+
return;
|
|
240
|
+
const data = getWeeklyInsightData();
|
|
241
|
+
if (data.total_sessions >= 3) {
|
|
242
|
+
console.log(renderWeeklyInsight(data));
|
|
243
|
+
}
|
|
244
|
+
markInsightShown();
|
|
245
|
+
}
|
|
246
|
+
catch { /* insight is non-critical */ }
|
|
247
|
+
});
|
|
202
248
|
// Etiquetar sesiones históricas que no tienen proyecto asignado
|
|
203
249
|
migrateSessionProjects();
|
|
204
250
|
// Pre-scan de proyectos al arrancar — garantiza respuesta inmediata en el tab
|
package/dist/db.d.ts
CHANGED
|
@@ -126,6 +126,19 @@ 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;
|
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,
|
|
@@ -539,6 +567,17 @@ exports.dbOps = {
|
|
|
539
567
|
}
|
|
540
568
|
return tools;
|
|
541
569
|
},
|
|
570
|
+
getWeeklyInsight(days = 7) {
|
|
571
|
+
const since = Date.now() - days * 86400000;
|
|
572
|
+
return stmts.getWeeklyInsight.get(since);
|
|
573
|
+
},
|
|
574
|
+
setMeta(key, value) {
|
|
575
|
+
stmts.upsertMeta.run(key, value);
|
|
576
|
+
},
|
|
577
|
+
getMeta(key) {
|
|
578
|
+
const row = stmts.getMeta.get(key);
|
|
579
|
+
return row?.value;
|
|
580
|
+
},
|
|
542
581
|
getCostProjection(days = 7) {
|
|
543
582
|
const since = Date.now() - days * 86400000;
|
|
544
583
|
return stmts.getCostProjection.get(since);
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ const config_1 = require("./config");
|
|
|
29
29
|
const doctor_1 = require("./doctor");
|
|
30
30
|
const share_1 = require("./share");
|
|
31
31
|
const roast_1 = require("./roast");
|
|
32
|
+
const insights_1 = require("./insights");
|
|
32
33
|
const paths_1 = require("./paths");
|
|
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;
|
|
@@ -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
|
}
|
|
@@ -160,7 +166,8 @@ program
|
|
|
160
166
|
if (opts.compact) {
|
|
161
167
|
const pctCycle = q.cyclePct;
|
|
162
168
|
const cycleEmoji = pctCycle >= 95 ? '🔴' : pctCycle >= 70 ? '🟡' : '🟢';
|
|
163
|
-
|
|
169
|
+
const wEmoji = q.weeklyPctAll >= 95 ? '🔴' : q.weeklyPctAll >= 70 ? '🟡' : '🟢';
|
|
170
|
+
console.log(`C:${pctCycle}%${cycleEmoji} W:${q.weeklyPctAll}%${wEmoji} ${q.detectedPlan}`);
|
|
164
171
|
process.exit(0);
|
|
165
172
|
}
|
|
166
173
|
if (opts.json) {
|
|
@@ -174,6 +181,7 @@ program
|
|
|
174
181
|
weeklyLimitSonnet: q.weeklyLimitSonnet,
|
|
175
182
|
weeklyHoursOpus: q.weeklyHoursOpus,
|
|
176
183
|
weeklyLimitOpus: q.weeklyLimitOpus,
|
|
184
|
+
weeklyPctAll: q.weeklyPctAll,
|
|
177
185
|
burnRateTokensPerMin: q.burnRateTokensPerMin,
|
|
178
186
|
}));
|
|
179
187
|
process.exit(0);
|
|
@@ -187,19 +195,22 @@ program
|
|
|
187
195
|
const resetLabel = resetMin >= 60
|
|
188
196
|
? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
|
|
189
197
|
: `${resetMin}m`;
|
|
190
|
-
const burnLabel = q.burnRateTokensPerMin > 0
|
|
191
|
-
? ` │ 🔥 ${q.burnRateTokensPerMin.toLocaleString()} tok/min`
|
|
192
|
-
: '';
|
|
193
198
|
const burnRow = q.burnRateTokensPerMin > 0
|
|
194
199
|
? ` Burn rate ${q.burnRateTokensPerMin.toLocaleString()} tok/min\n`
|
|
195
200
|
: '';
|
|
201
|
+
const weeklyTotalHours = q.weeklyHoursSonnet + q.weeklyHoursOpus;
|
|
202
|
+
const weeklyLimitTotal = q.weeklyLimitSonnet + q.weeklyLimitOpus;
|
|
203
|
+
const weeklyPctColor = q.weeklyPctAll >= 95 ? '\x1b[31m'
|
|
204
|
+
: q.weeklyPctAll >= 70 ? '\x1b[33m'
|
|
205
|
+
: '\x1b[32m';
|
|
196
206
|
console.log(`\n📊 claudestat status\n` +
|
|
197
207
|
`──────────────────────────────────────────\n` +
|
|
198
208
|
` Quota 5h ${pctColor}${q.cyclePrompts}/${q.cycleLimit} prompts (${q.cyclePct}%)${R} │ resets in ${resetLabel}\n` +
|
|
199
209
|
` Plan ${q.detectedPlan.toUpperCase()}\n` +
|
|
200
|
-
`
|
|
210
|
+
` Weekly ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${weeklyPctColor}${q.weeklyPctAll}%${R}) this week\n` +
|
|
201
211
|
(q.weeklyLimitOpus > 0
|
|
202
|
-
? `
|
|
212
|
+
? ` ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h\n` +
|
|
213
|
+
` └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h\n`
|
|
203
214
|
: '') +
|
|
204
215
|
`${burnRow}` +
|
|
205
216
|
`──────────────────────────────────────────\n`);
|
|
@@ -358,4 +369,27 @@ program
|
|
|
358
369
|
process.exit(1);
|
|
359
370
|
}
|
|
360
371
|
});
|
|
372
|
+
program
|
|
373
|
+
.command('weekly')
|
|
374
|
+
.description('Show weekly usage summary')
|
|
375
|
+
.option('--json', 'Output as JSON')
|
|
376
|
+
.action(async (opts) => {
|
|
377
|
+
try {
|
|
378
|
+
const data = (0, insights_1.getWeeklyInsightData)();
|
|
379
|
+
if (data.total_sessions === 0) {
|
|
380
|
+
console.log('\n📊 No usage data yet — start using Claude Code and claudestat will track it.\n');
|
|
381
|
+
process.exit(0);
|
|
382
|
+
}
|
|
383
|
+
if (opts.json) {
|
|
384
|
+
console.log(JSON.stringify(data, null, 2));
|
|
385
|
+
process.exit(0);
|
|
386
|
+
}
|
|
387
|
+
console.log((0, insights_1.renderWeeklyInsight)(data));
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
console.error('\n❌ Error:', err.message);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
361
395
|
program.parse();
|
|
@@ -0,0 +1,19 @@
|
|
|
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 declare function shouldShowInsight(): boolean;
|
|
18
|
+
export declare function markInsightShown(): void;
|
|
19
|
+
export declare function renderWeeklyInsight(d: WeeklyInsightData): string;
|
package/dist/insights.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getWeeklyInsightData = getWeeklyInsightData;
|
|
4
|
+
exports.generateTip = generateTip;
|
|
5
|
+
exports.shouldShowInsight = shouldShowInsight;
|
|
6
|
+
exports.markInsightShown = markInsightShown;
|
|
7
|
+
exports.renderWeeklyInsight = renderWeeklyInsight;
|
|
8
|
+
const db_1 = require("./db");
|
|
9
|
+
const WEEK_MS = 7 * 86400000;
|
|
10
|
+
const META_KEY = 'last_insight_at';
|
|
11
|
+
function getWeeklyInsightData(days = 7) {
|
|
12
|
+
const agg = db_1.dbOps.getWeeklyInsight(days);
|
|
13
|
+
const topTools = db_1.dbOps.getTopTools(days, 'cost', 1);
|
|
14
|
+
const topTool = topTools[0];
|
|
15
|
+
const topToolName = topTool?.tool_name ?? 'Unknown';
|
|
16
|
+
const topToolPct = agg.total_cost > 0
|
|
17
|
+
? Math.round((topTool?.total_cost_usd ?? 0) / agg.total_cost * 100)
|
|
18
|
+
: 0;
|
|
19
|
+
const totalInputWithCache = agg.input_tokens + agg.cache_read;
|
|
20
|
+
const cacheHitPct = totalInputWithCache > 0
|
|
21
|
+
? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
|
|
22
|
+
: 0;
|
|
23
|
+
return {
|
|
24
|
+
total_sessions: agg.total_sessions,
|
|
25
|
+
total_cost: agg.total_cost,
|
|
26
|
+
input_tokens: agg.input_tokens,
|
|
27
|
+
output_tokens: agg.output_tokens,
|
|
28
|
+
cache_read: agg.cache_read,
|
|
29
|
+
cache_hit_pct: cacheHitPct,
|
|
30
|
+
total_loops: agg.total_loops,
|
|
31
|
+
avg_efficiency: Math.round(agg.avg_efficiency),
|
|
32
|
+
top_tool: topToolName,
|
|
33
|
+
top_tool_cost_pct: topToolPct,
|
|
34
|
+
week_start: agg.week_start,
|
|
35
|
+
week_end: agg.week_end ?? agg.week_start,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function generateTip(d) {
|
|
39
|
+
const costPct = d.top_tool_cost_pct;
|
|
40
|
+
const tool = d.top_tool;
|
|
41
|
+
if (tool === 'Bash' && costPct >= 40) {
|
|
42
|
+
return 'Group bash commands to reduce tool calls — each call costs context';
|
|
43
|
+
}
|
|
44
|
+
if (d.total_loops >= 3) {
|
|
45
|
+
return `${d.total_loops} loops detected — consider using /compact earlier to prevent context thrashing`;
|
|
46
|
+
}
|
|
47
|
+
if (d.avg_efficiency < 60) {
|
|
48
|
+
return 'Low efficiency score — try smaller, focused tasks instead of long sessions';
|
|
49
|
+
}
|
|
50
|
+
if (d.total_sessions > 30) {
|
|
51
|
+
return `${d.total_sessions} sessions this week — consider batching related work into fewer sessions`;
|
|
52
|
+
}
|
|
53
|
+
if (d.cache_hit_pct < 10 && d.total_sessions > 5) {
|
|
54
|
+
return 'Low cache hit rate — repetitive context is costing you; use CLAUDE.md for common instructions';
|
|
55
|
+
}
|
|
56
|
+
if (d.total_cost > 20) {
|
|
57
|
+
return `$${d.total_cost.toFixed(0)} spent this week — enable quota alerts with "claudestat config --alerts true" to stay in control`;
|
|
58
|
+
}
|
|
59
|
+
return 'Enable quota alerts with "claudestat config --alerts true" to avoid surprise limits';
|
|
60
|
+
}
|
|
61
|
+
function shouldShowInsight() {
|
|
62
|
+
const last = db_1.dbOps.getMeta(META_KEY);
|
|
63
|
+
if (!last)
|
|
64
|
+
return true;
|
|
65
|
+
return Date.now() - parseInt(last, 10) >= WEEK_MS;
|
|
66
|
+
}
|
|
67
|
+
function markInsightShown() {
|
|
68
|
+
db_1.dbOps.setMeta(META_KEY, Date.now().toString());
|
|
69
|
+
}
|
|
70
|
+
function renderWeeklyInsight(d) {
|
|
71
|
+
const fmtTok = (n) => {
|
|
72
|
+
if (n >= 1000000)
|
|
73
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
74
|
+
if (n >= 1000)
|
|
75
|
+
return `${Math.round(n / 1000)}K`;
|
|
76
|
+
return n.toString();
|
|
77
|
+
};
|
|
78
|
+
const fmtCost = (n) => `$${n.toFixed(2)}`;
|
|
79
|
+
const fmtDate = (ts) => {
|
|
80
|
+
const d = new Date(ts);
|
|
81
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
82
|
+
};
|
|
83
|
+
const R = '\x1b[0m';
|
|
84
|
+
const B = '\x1b[1m';
|
|
85
|
+
const D = '\x1b[2m';
|
|
86
|
+
const C = '\x1b[36m';
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push(`\n${B}📊 claudestat weekly insight${R} ${D}(${fmtDate(d.week_start)} – ${fmtDate(d.week_end)})${R}`);
|
|
89
|
+
lines.push(`${'─'.repeat(60)}`);
|
|
90
|
+
lines.push(` Sessions: ${d.total_sessions} · Cost: ${fmtCost(d.total_cost)} · Loops: ${d.total_loops}`);
|
|
91
|
+
lines.push(` Top tool: ${d.top_tool} (${d.top_tool_cost_pct}% of cost) · Efficiency: ${d.avg_efficiency}/100`);
|
|
92
|
+
const cacheLabel = d.total_sessions > 0
|
|
93
|
+
? ` · Cache hit: ${d.cache_hit_pct}%`
|
|
94
|
+
: '';
|
|
95
|
+
const tokLabel = d.input_tokens + d.output_tokens > 0
|
|
96
|
+
? ` · Tokens: ${fmtTok(d.input_tokens)}+${fmtTok(d.output_tokens)}`
|
|
97
|
+
: '';
|
|
98
|
+
if (cacheLabel || tokLabel) {
|
|
99
|
+
lines.push(` ${D}${tokLabel}${cacheLabel}${R}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(`${'─'.repeat(60)}`);
|
|
102
|
+
lines.push(` ${C}⚡${R} Tip: ${generateTip(d)}`);
|
|
103
|
+
lines.push('');
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mcp-server.ts — MCP (Model Context Protocol) server for claudestat
|
|
4
|
+
*
|
|
5
|
+
* Exposes Claude Code usage metrics as tools that Claude can query.
|
|
6
|
+
* Zero extra dependencies — stdio JSON-RPC 2.0, readline only.
|
|
7
|
+
* Works without the daemon — reads SQLite + JSONL directly.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* mcp-server.ts — MCP (Model Context Protocol) server for claudestat
|
|
5
|
+
*
|
|
6
|
+
* Exposes Claude Code usage metrics as tools that Claude can query.
|
|
7
|
+
* Zero extra dependencies — stdio JSON-RPC 2.0, readline only.
|
|
8
|
+
* Works without the daemon — reads SQLite + JSONL directly.
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
process.on('warning', (w) => {
|
|
45
|
+
if (w.name === 'ExperimentalWarning' && w.message.includes('SQLite'))
|
|
46
|
+
return;
|
|
47
|
+
process.stderr.write(`${w.name}: ${w.message}\n`);
|
|
48
|
+
});
|
|
49
|
+
const readline = __importStar(require("readline"));
|
|
50
|
+
const db_1 = require("./db");
|
|
51
|
+
const quota_tracker_1 = require("./quota-tracker");
|
|
52
|
+
const insights_1 = require("./insights");
|
|
53
|
+
const SERVER_NAME = 'claudestat';
|
|
54
|
+
const SERVER_VERSION = '1.2.0';
|
|
55
|
+
const PROTOCOL_VERSION = '2025-06-18';
|
|
56
|
+
const TOOLS = [
|
|
57
|
+
{
|
|
58
|
+
name: 'get_quota_status',
|
|
59
|
+
description: 'Get current Claude Code quota status: 5h cycle usage %, plan type, weekly hours per model, and burn rate (tokens/min)',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {},
|
|
63
|
+
required: []
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'get_current_session',
|
|
68
|
+
description: 'Get details about the most recent Claude Code session: cost, tokens, efficiency score, and loops detected',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {},
|
|
72
|
+
required: []
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'get_session_stats',
|
|
77
|
+
description: 'Get aggregated session statistics for the last N days: session count, total cost, total tokens, loops, and average efficiency',
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
days: {
|
|
82
|
+
type: 'number',
|
|
83
|
+
description: 'Number of days to look back (1–90, default 7)'
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
required: []
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'get_top_tools',
|
|
91
|
+
description: 'Get the top 10 most used tools by cost, call count, or duration in the last N days',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
days: {
|
|
96
|
+
type: 'number',
|
|
97
|
+
description: 'Days to look back (default 30)'
|
|
98
|
+
},
|
|
99
|
+
sort_by: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: 'Sort by: cost, count, or duration (default cost)'
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: []
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'get_weekly_insight',
|
|
109
|
+
description: 'Get the weekly usage summary with an actionable tip (same as claudestat weekly command)',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
days: {
|
|
114
|
+
type: 'number',
|
|
115
|
+
description: 'Days to look back (default 7)'
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
required: []
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
];
|
|
122
|
+
function fmtDollar(n) {
|
|
123
|
+
if (n === 0)
|
|
124
|
+
return '$0.00';
|
|
125
|
+
if (n < 0.01)
|
|
126
|
+
return '< $0.01';
|
|
127
|
+
return `$${n.toFixed(2)}`;
|
|
128
|
+
}
|
|
129
|
+
function fmtTok(n) {
|
|
130
|
+
if (n >= 1000000)
|
|
131
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
132
|
+
if (n >= 1000)
|
|
133
|
+
return `${Math.round(n / 1000)}K`;
|
|
134
|
+
return n.toString();
|
|
135
|
+
}
|
|
136
|
+
function toolGetQuotaStatus() {
|
|
137
|
+
const q = (0, quota_tracker_1.computeQuota)();
|
|
138
|
+
const resetMin = Math.ceil(q.cycleResetMs / 60000);
|
|
139
|
+
const resetLabel = resetMin >= 60
|
|
140
|
+
? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
|
|
141
|
+
: `${resetMin}m`;
|
|
142
|
+
const weeklyTotalHours = q.weeklyHoursSonnet + q.weeklyHoursOpus;
|
|
143
|
+
const weeklyLimitTotal = q.weeklyLimitSonnet + q.weeklyLimitOpus;
|
|
144
|
+
const parts = [
|
|
145
|
+
`Quota status — ${q.detectedPlan.toUpperCase()} plan`,
|
|
146
|
+
``,
|
|
147
|
+
`5h cycle: ${q.cyclePrompts}/${q.cycleLimit} prompts (${q.cyclePct}%) · resets in ${resetLabel}`,
|
|
148
|
+
`Weekly: ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${q.weeklyPctAll}%)`,
|
|
149
|
+
];
|
|
150
|
+
if (q.weeklyLimitOpus > 0) {
|
|
151
|
+
parts.push(` ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h`);
|
|
152
|
+
parts.push(` └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h`);
|
|
153
|
+
}
|
|
154
|
+
if (q.burnRateTokensPerMin > 0) {
|
|
155
|
+
parts.push(`Burn rate: ${q.burnRateTokensPerMin.toLocaleString()} tokens/min`);
|
|
156
|
+
}
|
|
157
|
+
return parts.join('\n');
|
|
158
|
+
}
|
|
159
|
+
function toolGetCurrentSession() {
|
|
160
|
+
const session = db_1.dbOps.getLatestSession();
|
|
161
|
+
if (!session)
|
|
162
|
+
return 'No sessions recorded yet.';
|
|
163
|
+
const cost = fmtDollar(session.total_cost_usd ?? 0);
|
|
164
|
+
const inp = fmtTok(session.total_input_tokens ?? 0);
|
|
165
|
+
const out = fmtTok(session.total_output_tokens ?? 0);
|
|
166
|
+
const cache = fmtTok(session.total_cache_read ?? 0);
|
|
167
|
+
const eff = session.efficiency_score ?? 100;
|
|
168
|
+
const loops = session.loops_detected ?? 0;
|
|
169
|
+
const started = new Date(session.started_at).toISOString();
|
|
170
|
+
const project = session.project_path ?? 'No project';
|
|
171
|
+
const model = session.dominant_model ?? 'unknown';
|
|
172
|
+
return [
|
|
173
|
+
`Latest session: ${session.id.slice(0, 8)}...`,
|
|
174
|
+
``,
|
|
175
|
+
`Project: ${project}`,
|
|
176
|
+
`Model: ${model}`,
|
|
177
|
+
`Started: ${started}`,
|
|
178
|
+
`Cost: ${cost}`,
|
|
179
|
+
`Tokens: ${inp} in + ${out} out (${cache} cache read)`,
|
|
180
|
+
`Efficiency: ${eff}/100`,
|
|
181
|
+
`Loops: ${loops}`,
|
|
182
|
+
].join('\n');
|
|
183
|
+
}
|
|
184
|
+
function toolGetSessionStats(days) {
|
|
185
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
186
|
+
const insight = db_1.dbOps.getWeeklyInsight(d);
|
|
187
|
+
if (!insight || insight.total_sessions === 0)
|
|
188
|
+
return `No sessions in the last ${d} days.`;
|
|
189
|
+
const totalTok = insight.input_tokens + insight.output_tokens;
|
|
190
|
+
return [
|
|
191
|
+
`Session stats — last ${d} days`,
|
|
192
|
+
``,
|
|
193
|
+
`Sessions: ${insight.total_sessions}`,
|
|
194
|
+
`Cost: ${fmtDollar(insight.total_cost)}`,
|
|
195
|
+
`Tokens: ${fmtTok(totalTok)} (${fmtTok(insight.input_tokens)} in + ${fmtTok(insight.output_tokens)} out)`,
|
|
196
|
+
`Cache read: ${fmtTok(insight.cache_read)}`,
|
|
197
|
+
`Loops: ${insight.total_loops}`,
|
|
198
|
+
`Efficiency: ${Math.round(insight.avg_efficiency)}/100 avg`,
|
|
199
|
+
].join('\n');
|
|
200
|
+
}
|
|
201
|
+
function toolGetTopTools(days, sortBy) {
|
|
202
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 30)));
|
|
203
|
+
const sort = (sortBy === 'count' || sortBy === 'duration') ? sortBy : 'cost';
|
|
204
|
+
const tools = db_1.dbOps.getTopTools(d, sort, 10);
|
|
205
|
+
if (tools.length === 0)
|
|
206
|
+
return `No tool usage data in the last ${d} days.`;
|
|
207
|
+
const lines = [
|
|
208
|
+
`Top tools — last ${d} days (sorted by ${sort})`,
|
|
209
|
+
'',
|
|
210
|
+
];
|
|
211
|
+
for (let i = 0; i < tools.length; i++) {
|
|
212
|
+
const t = tools[i];
|
|
213
|
+
const idx = `${i + 1}.`.padEnd(4);
|
|
214
|
+
const name = t.tool_name.padEnd(14);
|
|
215
|
+
const cnt = `${t.count} calls`.padEnd(14);
|
|
216
|
+
const dur = t.total_duration_ms > 0
|
|
217
|
+
? `${(t.total_duration_ms / 1000).toFixed(1)}s`.padEnd(10)
|
|
218
|
+
: '—'.padEnd(10);
|
|
219
|
+
const cost = fmtDollar(t.total_cost_usd);
|
|
220
|
+
lines.push(` ${idx}${name}${cnt}${dur}${cost}`);
|
|
221
|
+
}
|
|
222
|
+
return lines.join('\n');
|
|
223
|
+
}
|
|
224
|
+
function toolGetWeeklyInsight(days) {
|
|
225
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
226
|
+
const data = (0, insights_1.getWeeklyInsightData)(d);
|
|
227
|
+
if (data.total_sessions === 0)
|
|
228
|
+
return `No usage data for the last ${d} days.`;
|
|
229
|
+
const fmtDate = (ts) => {
|
|
230
|
+
const dt = new Date(ts);
|
|
231
|
+
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
232
|
+
};
|
|
233
|
+
return [
|
|
234
|
+
`Weekly insight (${fmtDate(data.week_start)} – ${fmtDate(data.week_end)})`,
|
|
235
|
+
`──────────────────────────────────────────────`,
|
|
236
|
+
`Sessions: ${data.total_sessions} · Cost: ${fmtDollar(data.total_cost)} · Loops: ${data.total_loops}`,
|
|
237
|
+
`Top tool: ${data.top_tool} (${data.top_tool_cost_pct}% of cost) · Efficiency: ${data.avg_efficiency}/100`,
|
|
238
|
+
`Tokens: ${fmtTok(data.input_tokens)} in + ${fmtTok(data.output_tokens)} out · Cache hit: ${data.cache_hit_pct}%`,
|
|
239
|
+
`Tip: ${(0, insights_1.generateTip)(data)}`,
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
242
|
+
function handleToolCall(name, args) {
|
|
243
|
+
const days = typeof args.days === 'number' ? args.days : 7;
|
|
244
|
+
const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'cost';
|
|
245
|
+
switch (name) {
|
|
246
|
+
case 'get_quota_status': return toolGetQuotaStatus();
|
|
247
|
+
case 'get_current_session': return toolGetCurrentSession();
|
|
248
|
+
case 'get_session_stats': return toolGetSessionStats(days);
|
|
249
|
+
case 'get_top_tools': return toolGetTopTools(days, sortBy);
|
|
250
|
+
case 'get_weekly_insight': return toolGetWeeklyInsight(days);
|
|
251
|
+
default: return `Unknown tool: ${name}`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function handleRequest(msg) {
|
|
255
|
+
const { id, method, params } = msg;
|
|
256
|
+
if (id === undefined) {
|
|
257
|
+
if (method === 'notifications/initialized')
|
|
258
|
+
return null;
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
switch (method) {
|
|
263
|
+
case 'initialize':
|
|
264
|
+
return {
|
|
265
|
+
jsonrpc: '2.0', id,
|
|
266
|
+
result: {
|
|
267
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
268
|
+
capabilities: { tools: {} },
|
|
269
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
case 'tools/list':
|
|
273
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
274
|
+
case 'tools/call': {
|
|
275
|
+
const toolName = params?.name;
|
|
276
|
+
const toolArgs = (params?.arguments ?? {});
|
|
277
|
+
const text = handleToolCall(toolName, toolArgs);
|
|
278
|
+
return {
|
|
279
|
+
jsonrpc: '2.0', id,
|
|
280
|
+
result: { content: [{ type: 'text', text }], isError: false }
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
default:
|
|
284
|
+
return {
|
|
285
|
+
jsonrpc: '2.0', id,
|
|
286
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
return {
|
|
292
|
+
jsonrpc: '2.0', id,
|
|
293
|
+
result: { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true }
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
298
|
+
rl.on('line', (line) => {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
if (!trimmed)
|
|
301
|
+
return;
|
|
302
|
+
try {
|
|
303
|
+
const msg = JSON.parse(trimmed);
|
|
304
|
+
const response = handleRequest(msg);
|
|
305
|
+
if (response) {
|
|
306
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch (e) {
|
|
310
|
+
process.stderr.write(`[claudestat-mcp] Parse error: ${e.message}\n`);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
314
|
+
process.on('SIGINT', () => process.exit(0));
|
|
315
|
+
process.stderr.write(`[claudestat-mcp] Server ready (stdio, protocol ${PROTOCOL_VERSION})\n`);
|
package/dist/quota-tracker.d.ts
CHANGED
package/dist/quota-tracker.js
CHANGED
|
@@ -293,6 +293,10 @@ function computeQuota(forcePlan) {
|
|
|
293
293
|
const burnRateTokensPerMin = recentAssistant.length > 0
|
|
294
294
|
? Math.round(totalRecentTok / 30)
|
|
295
295
|
: 0;
|
|
296
|
+
// ─ % semanal combinado (Sonnet + Opus, coincide con "Todos los modelos" de claude.ai) ─
|
|
297
|
+
const weeklyPctAll = limits.weeklyHoursSonnet + limits.weeklyHoursOpus > 0
|
|
298
|
+
? Math.min(100, Math.round((weeklyHoursSonnet + weeklyHoursOpus) / (limits.weeklyHoursSonnet + limits.weeklyHoursOpus) * 100))
|
|
299
|
+
: 0;
|
|
296
300
|
const data = {
|
|
297
301
|
cyclePrompts: cycleEntries.filter(e => e.type === 'human').length,
|
|
298
302
|
cycleLimit: limits.prompts5h,
|
|
@@ -310,6 +314,7 @@ function computeQuota(forcePlan) {
|
|
|
310
314
|
weeklyTokensHaiku,
|
|
311
315
|
weeklyLimitSonnet: limits.weeklyHoursSonnet,
|
|
312
316
|
weeklyLimitOpus: limits.weeklyHoursOpus,
|
|
317
|
+
weeklyPctAll,
|
|
313
318
|
burnRateTokensPerMin,
|
|
314
319
|
detectedPlan: plan,
|
|
315
320
|
planSource,
|
package/dist/routes/events.js
CHANGED
|
@@ -75,7 +75,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
const resolvedCwd = cwd
|
|
78
|
-
?? (transcript_path ?
|
|
78
|
+
?? (transcript_path ? path_1.default.dirname(transcript_path) || undefined : undefined);
|
|
79
79
|
db_1.dbOps.upsertSession({ id: session_id, cwd: resolvedCwd, started_at: ts, last_event_at: ts });
|
|
80
80
|
// Skill grouping: get current parent BEFORE processing this event
|
|
81
81
|
// (the Skill Done event itself is NOT tagged — only its subsequent sub-calls are)
|
|
@@ -125,7 +125,7 @@ exports.eventsRouter.post('/event', (req, res) => {
|
|
|
125
125
|
try {
|
|
126
126
|
const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : (tool_input ?? {});
|
|
127
127
|
const filePath = inp?.file_path ?? inp?.path;
|
|
128
|
-
if (typeof filePath === 'string' &&
|
|
128
|
+
if (typeof filePath === 'string' && path_1.default.isAbsolute(filePath)) {
|
|
129
129
|
const projectCwd = findProjectCwdForFile(filePath);
|
|
130
130
|
if (projectCwd)
|
|
131
131
|
db_1.dbOps.updateSessionProject(session_id, projectCwd);
|
package/dist/routes/projects.js
CHANGED
|
@@ -39,7 +39,7 @@ function inferProjectCwd(events) {
|
|
|
39
39
|
try {
|
|
40
40
|
const inp = JSON.parse(ev.tool_input);
|
|
41
41
|
const filePath = (inp.file_path || inp.path);
|
|
42
|
-
if (!filePath
|
|
42
|
+
if (!filePath || !path_1.default.isAbsolute(filePath))
|
|
43
43
|
continue;
|
|
44
44
|
const cwd = findProjectCwdForFile(filePath);
|
|
45
45
|
if (cwd)
|
|
@@ -71,7 +71,7 @@ function inferActiveProjectByMajority(events, windowMs) {
|
|
|
71
71
|
try {
|
|
72
72
|
const inp = JSON.parse(ev.tool_input);
|
|
73
73
|
const filePath = (inp.file_path || inp.path);
|
|
74
|
-
if (!filePath
|
|
74
|
+
if (!filePath || !path_1.default.isAbsolute(filePath))
|
|
75
75
|
continue;
|
|
76
76
|
const project = findProjectCwdForFile(filePath);
|
|
77
77
|
if (!project)
|
package/dist/share.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.runShare = runShare;
|
|
4
7
|
const db_js_1 = require("./db.js");
|
|
5
8
|
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
6
10
|
function formatDuration(ms) {
|
|
7
11
|
const seconds = Math.floor(ms / 1000);
|
|
8
12
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -45,7 +49,7 @@ async function getSessionData(sessionId) {
|
|
|
45
49
|
topToolPct = Math.round((sorted[0][1] / toolCalls.length) * 100);
|
|
46
50
|
}
|
|
47
51
|
const durationMs = (session.last_event_at || session.started_at) - session.started_at;
|
|
48
|
-
const project = session.project_path
|
|
52
|
+
const project = path_1.default.basename(session.project_path ?? '') || 'unknown';
|
|
49
53
|
return {
|
|
50
54
|
id: session.id,
|
|
51
55
|
project: project.length > 18 ? project.slice(0, 15) + '...' : project,
|
package/dist/summarizer.js
CHANGED
|
@@ -92,7 +92,7 @@ function buildContext(events, costUsd, projectName) {
|
|
|
92
92
|
try {
|
|
93
93
|
const inp = JSON.parse(e.tool_input);
|
|
94
94
|
const fp = inp.file_path || inp.path;
|
|
95
|
-
if (typeof fp === 'string' &&
|
|
95
|
+
if (typeof fp === 'string' && path_1.default.isAbsolute(fp)) {
|
|
96
96
|
filesSet.add(path_1.default.basename(fp));
|
|
97
97
|
}
|
|
98
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statforge/claudestat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"dashboard/dist/"
|
|
49
49
|
],
|
|
50
50
|
"bin": {
|
|
51
|
-
"claudestat": "dist/index.js"
|
|
51
|
+
"claudestat": "dist/index.js",
|
|
52
|
+
"claudestat-mcp": "dist/mcp-server.js"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"build": "tsc && npm run build:dashboard",
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
"dev": "tsx src/index.ts",
|
|
59
60
|
"dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
|
|
60
61
|
"start": "node dist/index.js",
|
|
61
|
-
"test": "
|
|
62
|
+
"test": "node --require tsx/cjs tests/index.ts"
|
|
62
63
|
},
|
|
63
64
|
"dependencies": {
|
|
64
65
|
"@anthropic-ai/sdk": "^0.88.0",
|