@statforge/claudestat 1.1.1 → 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 +61 -3
- 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
|
@@ -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
|
|
|
@@ -140,6 +142,7 @@ That's it. Start a Claude Code session and watch the events flow in.
|
|
|
140
142
|
| `claudestat status --compact` | One-line output for tmux status bar |
|
|
141
143
|
| `claudestat config` | View or edit configuration |
|
|
142
144
|
| `claudestat top` | Rank tools by cost, call count, or duration |
|
|
145
|
+
| `claudestat weekly` | Weekly usage summary with actionable tips |
|
|
143
146
|
| `claudestat export [format]` | Export session data to JSON or CSV |
|
|
144
147
|
| `claudestat share [session-id]` | Generate shareable session card (ASCII/JSON) |
|
|
145
148
|
| `claudestat roast` | Sarcastic usage analysis with roast jokes |
|
|
@@ -183,6 +186,22 @@ claudestat top
|
|
|
183
186
|
|
|
184
187
|
Options: `--by cost|count|duration` · `--days 7|30|90` · `--limit N`
|
|
185
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
|
+
|
|
186
205
|
### `claudestat status`
|
|
187
206
|
|
|
188
207
|
```
|
|
@@ -190,17 +209,17 @@ claudestat status
|
|
|
190
209
|
|
|
191
210
|
Quota 5h 45/50 prompts (90%) | reset in 22m
|
|
192
211
|
Plan MAX5
|
|
193
|
-
|
|
212
|
+
Weekly 3.5h / 40h (9%) this week
|
|
194
213
|
Burn rate 1,240 tok/min
|
|
195
214
|
```
|
|
196
215
|
|
|
197
216
|
### `claudestat status --compact`
|
|
198
217
|
|
|
199
|
-
One-line output for tmux status bar or scripting. Shows
|
|
218
|
+
One-line output for tmux status bar or scripting. Shows cycle quota and weekly usage with colored emoji.
|
|
200
219
|
|
|
201
220
|
```bash
|
|
202
221
|
claudestat status --compact
|
|
203
|
-
|
|
222
|
+
C:45%🟡 W:9%🟢 pro
|
|
204
223
|
```
|
|
205
224
|
|
|
206
225
|
### `claudestat share`
|
|
@@ -324,6 +343,35 @@ Config is stored at `~/.claudestat/config.json` (macOS/Linux) or `%USERPROFILE%\
|
|
|
324
343
|
|
|
325
344
|
---
|
|
326
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
|
+
|
|
327
375
|
## Dashboard
|
|
328
376
|
|
|
329
377
|
The dashboard lives at `http://localhost:7337` and has six tabs:
|
|
@@ -501,6 +549,16 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
|
|
|
501
549
|
|
|
502
550
|
---
|
|
503
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
|
+
|
|
504
562
|
## FAQ
|
|
505
563
|
|
|
506
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",
|