@statforge/claudestat 1.1.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -70
- package/dist/claude-auth.d.ts +7 -0
- package/dist/claude-auth.js +12 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +12 -0
- package/dist/daemon.js +78 -15
- package/dist/db.d.ts +31 -0
- package/dist/db.js +89 -0
- package/dist/doctor.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +192 -70
- package/dist/insights.d.ts +45 -0
- package/dist/insights.js +257 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +442 -0
- package/dist/notifier.js +24 -6
- package/dist/pattern-analyzer.d.ts +1 -0
- package/dist/pattern-analyzer.js +13 -0
- package/dist/quota-tracker.d.ts +7 -0
- package/dist/quota-tracker.js +83 -31
- package/dist/roast.js +129 -40
- package/dist/routes/events.js +3 -3
- package/dist/routes/projects.js +2 -2
- package/dist/share.js +5 -1
- package/dist/summarizer.js +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
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 config_1 = require("./config");
|
|
54
|
+
const SERVER_NAME = 'claudestat';
|
|
55
|
+
const SERVER_VERSION = '1.2.2';
|
|
56
|
+
const PROTOCOL_VERSION = '2025-03-26';
|
|
57
|
+
const TOOLS = [
|
|
58
|
+
{
|
|
59
|
+
name: 'get_quota_status',
|
|
60
|
+
description: 'Get current Claude Code quota status: 5h cycle usage %, plan type, weekly hours per model, and burn rate (tokens/min)',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {},
|
|
64
|
+
required: []
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'get_current_session',
|
|
69
|
+
description: 'Get details about the most recent Claude Code session: cost, tokens, efficiency score, and loops detected',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {},
|
|
73
|
+
required: []
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'get_session_stats',
|
|
78
|
+
description: 'Get aggregated session statistics for the last N days: session count, total cost, total tokens, loops, and average efficiency',
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
82
|
+
days: {
|
|
83
|
+
type: 'number',
|
|
84
|
+
description: 'Number of days to look back (1–90, default 7)'
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: []
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'get_top_tools',
|
|
92
|
+
description: 'Get the top 10 most used tools by cost, call count, or duration in the last N days',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
days: {
|
|
97
|
+
type: 'number',
|
|
98
|
+
description: 'Days to look back (default 30)'
|
|
99
|
+
},
|
|
100
|
+
sort_by: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Sort by: cost, count, or duration (default cost)'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
required: []
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'get_usage_insights',
|
|
110
|
+
description: 'Get unique usage insights not available in /usage: cost per project, cache savings, output/input ratio, efficiency trend, and peak hours',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
days: {
|
|
115
|
+
type: 'number',
|
|
116
|
+
description: 'Days to look back (default 7)'
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: []
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'get_model_breakdown',
|
|
124
|
+
description: 'Get cost and session count broken down by Claude model (Sonnet, Haiku, Opus) for the last N days',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
days: {
|
|
129
|
+
type: 'number',
|
|
130
|
+
description: 'Days to look back (default 7)'
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
required: []
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'get_weekly_insight',
|
|
138
|
+
description: 'Get the weekly usage summary with an actionable tip (same as claudestat weekly command)',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
days: {
|
|
143
|
+
type: 'number',
|
|
144
|
+
description: 'Days to look back (default 7)'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
required: []
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
function fmtDollar(n) {
|
|
152
|
+
if (n === 0)
|
|
153
|
+
return '$0.00';
|
|
154
|
+
if (n < 0.01)
|
|
155
|
+
return '< $0.01';
|
|
156
|
+
return `$${n.toFixed(2)}`;
|
|
157
|
+
}
|
|
158
|
+
function fmtTok(n) {
|
|
159
|
+
if (n >= 1000000)
|
|
160
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
161
|
+
if (n >= 1000)
|
|
162
|
+
return `${Math.round(n / 1000)}K`;
|
|
163
|
+
return n.toString();
|
|
164
|
+
}
|
|
165
|
+
function toolGetQuotaStatus() {
|
|
166
|
+
const q = (0, quota_tracker_1.computeQuota)();
|
|
167
|
+
const resetMin = Math.ceil(q.cycleResetMs / 60000);
|
|
168
|
+
const resetLabel = resetMin >= 60
|
|
169
|
+
? `${Math.floor(resetMin / 60)}h ${resetMin % 60}m`
|
|
170
|
+
: `${resetMin}m`;
|
|
171
|
+
const weeklyTotalHours = q.weeklyHoursSonnet + q.weeklyHoursOpus;
|
|
172
|
+
const weeklyLimitTotal = q.weeklyLimitSonnet + q.weeklyLimitOpus;
|
|
173
|
+
const planLabel = q.planSource === 'inferred'
|
|
174
|
+
? `${q.detectedPlan.toUpperCase()} plan (unverified — checking API...)`
|
|
175
|
+
: `${q.detectedPlan.toUpperCase()} plan`;
|
|
176
|
+
const parts = [
|
|
177
|
+
`Quota status — ${planLabel}`,
|
|
178
|
+
``,
|
|
179
|
+
`5h cycle: ${q.cyclePct}% · ${q.cyclePrompts > q.cycleLimit ? `${q.cyclePrompts}/${q.cycleLimit} prompts (OVER LIMIT)` : `${q.cyclePrompts}/${q.cycleLimit} prompts`} · resets in ${resetLabel}`,
|
|
180
|
+
`Weekly: ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${q.weeklyPctAll}%)`,
|
|
181
|
+
];
|
|
182
|
+
if (q.weeklyLimitOpus > 0) {
|
|
183
|
+
parts.push(` ├─ Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h`);
|
|
184
|
+
parts.push(` └─ Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h`);
|
|
185
|
+
}
|
|
186
|
+
if (q.burnRateTokensPerMin > 0) {
|
|
187
|
+
parts.push(`Burn rate: ${q.burnRateTokensPerMin.toLocaleString()} tokens/min`);
|
|
188
|
+
}
|
|
189
|
+
// Active alerts — only shown when thresholds are crossed
|
|
190
|
+
const cfg = (0, config_1.readConfig)();
|
|
191
|
+
if (cfg.alertsEnabled) {
|
|
192
|
+
const alerts = [];
|
|
193
|
+
const cycleLevel = (0, config_1.getWarnLevel)(q.cyclePct, cfg.warnThresholds);
|
|
194
|
+
const weeklyLevel = (0, config_1.getWarnLevel)(q.weeklyPctAll, cfg.weeklyWarnThresholds);
|
|
195
|
+
if (cycleLevel === 'red')
|
|
196
|
+
alerts.push(`🔴 5h cycle at ${q.cyclePct}% — critical, limit imminent`);
|
|
197
|
+
else if (cycleLevel)
|
|
198
|
+
alerts.push(`⚠️ 5h cycle at ${q.cyclePct}% — approaching limit`);
|
|
199
|
+
if (weeklyLevel === 'red')
|
|
200
|
+
alerts.push(`🔴 Weekly at ${q.weeklyPctAll}% — critical`);
|
|
201
|
+
else if (weeklyLevel)
|
|
202
|
+
alerts.push(`⚠️ Weekly at ${q.weeklyPctAll}% — approaching weekly limit`);
|
|
203
|
+
if (q.cyclePrompts > q.cycleLimit) {
|
|
204
|
+
alerts.push(`⚠️ Prompt count (${q.cyclePrompts}) exceeds plan limit (${q.cycleLimit}) — plan may be mis-detected`);
|
|
205
|
+
}
|
|
206
|
+
const reminderMins = cfg.resetReminderMins ?? 10;
|
|
207
|
+
if (reminderMins > 0 && resetMin <= reminderMins && resetMin > 0) {
|
|
208
|
+
alerts.push(`⏰ Cycle resets in ${resetMin}m — good time to wrap up or start fresh`);
|
|
209
|
+
}
|
|
210
|
+
if (alerts.length > 0) {
|
|
211
|
+
parts.push(``);
|
|
212
|
+
parts.push(`─── ACTIVE ALERTS ───────────────────────`);
|
|
213
|
+
parts.push(...alerts);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return parts.join('\n');
|
|
217
|
+
}
|
|
218
|
+
function toolGetCurrentSession() {
|
|
219
|
+
const session = db_1.dbOps.getLatestSession();
|
|
220
|
+
if (!session)
|
|
221
|
+
return 'No sessions recorded yet.';
|
|
222
|
+
const cost = fmtDollar(session.total_cost_usd ?? 0);
|
|
223
|
+
const inp = fmtTok(session.total_input_tokens ?? 0);
|
|
224
|
+
const out = fmtTok(session.total_output_tokens ?? 0);
|
|
225
|
+
const cache = fmtTok(session.total_cache_read ?? 0);
|
|
226
|
+
const eff = session.efficiency_score ?? 100;
|
|
227
|
+
const loops = session.loops_detected ?? 0;
|
|
228
|
+
const started = new Date(session.started_at).toISOString();
|
|
229
|
+
const project = session.project_path ?? 'No project';
|
|
230
|
+
const model = session.dominant_model ?? 'unknown';
|
|
231
|
+
return [
|
|
232
|
+
`Latest session: ${session.id.slice(0, 8)}...`,
|
|
233
|
+
``,
|
|
234
|
+
`Project: ${project}`,
|
|
235
|
+
`Model: ${model}`,
|
|
236
|
+
`Started: ${started}`,
|
|
237
|
+
`Cost: ${cost}`,
|
|
238
|
+
`Tokens: ${inp} in + ${out} out (${cache} cache read)`,
|
|
239
|
+
`Efficiency: ${eff}/100`,
|
|
240
|
+
`Loops: ${loops}`,
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
function toolGetSessionStats(days) {
|
|
244
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
245
|
+
const insight = db_1.dbOps.getWeeklyInsight(d);
|
|
246
|
+
if (!insight || insight.total_sessions === 0)
|
|
247
|
+
return `No sessions in the last ${d} days.`;
|
|
248
|
+
const totalTok = insight.input_tokens + insight.output_tokens;
|
|
249
|
+
return [
|
|
250
|
+
`Session stats — last ${d} days`,
|
|
251
|
+
``,
|
|
252
|
+
`Sessions: ${insight.total_sessions}`,
|
|
253
|
+
`Cost: ${fmtDollar(insight.total_cost)}`,
|
|
254
|
+
`Tokens: ${fmtTok(totalTok)} (${fmtTok(insight.input_tokens)} in + ${fmtTok(insight.output_tokens)} out)`,
|
|
255
|
+
`Cache read: ${fmtTok(insight.cache_read)}`,
|
|
256
|
+
`Loops: ${insight.total_loops}`,
|
|
257
|
+
`Efficiency: ${Math.round(insight.avg_efficiency)}/100 avg`,
|
|
258
|
+
].join('\n');
|
|
259
|
+
}
|
|
260
|
+
function toolGetTopTools(days, sortBy) {
|
|
261
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 30)));
|
|
262
|
+
const sort = (sortBy === 'count' || sortBy === 'duration') ? sortBy : 'cost';
|
|
263
|
+
const tools = db_1.dbOps.getTopTools(d, sort, 10);
|
|
264
|
+
if (tools.length === 0)
|
|
265
|
+
return `No tool usage data in the last ${d} days.`;
|
|
266
|
+
const lines = [
|
|
267
|
+
`Top tools — last ${d} days (sorted by ${sort})`,
|
|
268
|
+
'',
|
|
269
|
+
];
|
|
270
|
+
for (let i = 0; i < tools.length; i++) {
|
|
271
|
+
const t = tools[i];
|
|
272
|
+
const idx = `${i + 1}.`.padEnd(4);
|
|
273
|
+
const name = t.tool_name.padEnd(14);
|
|
274
|
+
const cnt = `${t.count} calls`.padEnd(14);
|
|
275
|
+
const dur = t.total_duration_ms > 0
|
|
276
|
+
? `${(t.total_duration_ms / 1000).toFixed(1)}s`.padEnd(10)
|
|
277
|
+
: '—'.padEnd(10);
|
|
278
|
+
const cost = fmtDollar(t.total_cost_usd);
|
|
279
|
+
lines.push(` ${idx}${name}${cnt}${dur}${cost}`);
|
|
280
|
+
}
|
|
281
|
+
return lines.join('\n');
|
|
282
|
+
}
|
|
283
|
+
function toolGetUsageInsights(days) {
|
|
284
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
285
|
+
const i = (0, insights_1.getUsageInsights)(d);
|
|
286
|
+
if (i.total_sessions === 0)
|
|
287
|
+
return `No data for the last ${d} days.`;
|
|
288
|
+
const bar = (pct, width = 20) => '█'.repeat(Math.round(pct / 100 * width)) + '░'.repeat(width - Math.round(pct / 100 * width));
|
|
289
|
+
const lines = [];
|
|
290
|
+
lines.push(`💡 Usage insights — last ${d} days`);
|
|
291
|
+
lines.push('━'.repeat(44));
|
|
292
|
+
lines.push(``);
|
|
293
|
+
lines.push(` 💰 ${fmtDollar(i.avg_cost_per_session)}/session · ${i.total_sessions} sessions · ${fmtDollar(i.total_cost)} total`);
|
|
294
|
+
if (i.project_costs.length > 0) {
|
|
295
|
+
lines.push(``);
|
|
296
|
+
lines.push(` 🗂 Top projects`);
|
|
297
|
+
const topTotal = i.project_costs.reduce((s, p) => s + p.total_cost, 0);
|
|
298
|
+
for (const p of i.project_costs.slice(0, 4)) {
|
|
299
|
+
const pct = topTotal > 0 ? Math.round(p.total_cost / topTotal * 100) : 0;
|
|
300
|
+
const name = (p.project.split('/').pop() ?? p.project).slice(0, 14).padEnd(14);
|
|
301
|
+
lines.push(` ${name} ${bar(pct)} ${fmtDollar(p.total_cost)} ${pct}%`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
lines.push(``);
|
|
305
|
+
lines.push(` ⚡ Cache ~${fmtDollar(i.cache_savings_usd)} saved · ${i.cache_hit_pct}% hit rate`);
|
|
306
|
+
lines.push(``);
|
|
307
|
+
lines.push(` 📊 ${i.output_input_ratio}× output/input · ${i.ratio_label}`);
|
|
308
|
+
lines.push(``);
|
|
309
|
+
const effTrend = i.efficiency_delta !== -999
|
|
310
|
+
? ` ${i.efficiency_delta > 0 ? `↑ +${i.efficiency_delta}` : i.efficiency_delta < 0 ? `↓ ${i.efficiency_delta}` : '→ same'} vs prev period`
|
|
311
|
+
: '';
|
|
312
|
+
lines.push(` 📈 Efficiency ${i.avg_efficiency}/100${effTrend} · ${i.total_loops} loops`);
|
|
313
|
+
if (i.hour_ranges.length > 0) {
|
|
314
|
+
lines.push(``);
|
|
315
|
+
lines.push(` ⏰ Activity by time of day`);
|
|
316
|
+
const maxCount = Math.max(...i.hour_ranges.map(r => r.count));
|
|
317
|
+
for (let j = 0; j < i.hour_ranges.length; j++) {
|
|
318
|
+
const r = i.hour_ranges[j];
|
|
319
|
+
const pct = maxCount > 0 ? Math.round(r.count / maxCount * 100) : 0;
|
|
320
|
+
lines.push(` ${r.emoji} ${r.from}–${r.to} ${bar(pct)} ${r.count} sessions`);
|
|
321
|
+
if (j < i.hour_ranges.length - 1)
|
|
322
|
+
lines.push('');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
lines.push(``);
|
|
326
|
+
lines.push('━'.repeat(44));
|
|
327
|
+
return lines.join('\n');
|
|
328
|
+
}
|
|
329
|
+
function toolGetModelBreakdown(days) {
|
|
330
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
331
|
+
const models = db_1.dbOps.getModelBreakdown(d);
|
|
332
|
+
if (models.length === 0)
|
|
333
|
+
return `No model data in the last ${d} days.`;
|
|
334
|
+
const totalCost = models.reduce((s, m) => s + m.total_cost, 0);
|
|
335
|
+
const lines = [
|
|
336
|
+
`Model breakdown — last ${d} days`,
|
|
337
|
+
'',
|
|
338
|
+
];
|
|
339
|
+
for (const m of models) {
|
|
340
|
+
const pct = totalCost > 0 ? Math.round(m.total_cost / totalCost * 100) : 0;
|
|
341
|
+
const rawName = (m.model ?? 'unknown').replace(/^<|>$/g, '');
|
|
342
|
+
const name = rawName.padEnd(30);
|
|
343
|
+
const cost = fmtDollar(m.total_cost).padEnd(10);
|
|
344
|
+
lines.push(` ${name}${cost}${pct}% ${m.session_count} sessions`);
|
|
345
|
+
}
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
function toolGetWeeklyInsight(days) {
|
|
349
|
+
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
350
|
+
const data = (0, insights_1.getWeeklyInsightData)(d);
|
|
351
|
+
if (data.total_sessions === 0)
|
|
352
|
+
return `No usage data for the last ${d} days.`;
|
|
353
|
+
const fmtDate = (ts) => {
|
|
354
|
+
const dt = new Date(ts);
|
|
355
|
+
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
356
|
+
};
|
|
357
|
+
return [
|
|
358
|
+
`Weekly insight (${fmtDate(data.week_start)} – ${fmtDate(data.week_end)})`,
|
|
359
|
+
`──────────────────────────────────────────────`,
|
|
360
|
+
`Sessions: ${data.total_sessions} · Cost: ${fmtDollar(data.total_cost)} · Loops: ${data.total_loops}`,
|
|
361
|
+
`Top tool: ${data.top_tool} (${data.top_tool_cost_pct}% of cost) · Efficiency: ${data.avg_efficiency}/100`,
|
|
362
|
+
`Tokens: ${fmtTok(data.input_tokens)} in + ${fmtTok(data.output_tokens)} out · Cache hit: ${data.cache_hit_pct}%`,
|
|
363
|
+
`Tip: ${(0, insights_1.generateTip)(data)}`,
|
|
364
|
+
].join('\n');
|
|
365
|
+
}
|
|
366
|
+
async function handleToolCall(name, args) {
|
|
367
|
+
const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'cost';
|
|
368
|
+
switch (name) {
|
|
369
|
+
case 'get_quota_status':
|
|
370
|
+
await (0, quota_tracker_1.refreshFromApi)();
|
|
371
|
+
return toolGetQuotaStatus();
|
|
372
|
+
case 'get_current_session': return toolGetCurrentSession();
|
|
373
|
+
case 'get_session_stats': return toolGetSessionStats(typeof args.days === 'number' ? args.days : 7);
|
|
374
|
+
case 'get_top_tools': return toolGetTopTools(typeof args.days === 'number' ? args.days : 30, sortBy);
|
|
375
|
+
case 'get_usage_insights': return toolGetUsageInsights(typeof args.days === 'number' ? args.days : 7);
|
|
376
|
+
case 'get_model_breakdown': return toolGetModelBreakdown(typeof args.days === 'number' ? args.days : 7);
|
|
377
|
+
case 'get_weekly_insight': return toolGetWeeklyInsight(typeof args.days === 'number' ? args.days : 7);
|
|
378
|
+
default: return `Unknown tool: ${name}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function handleRequest(msg) {
|
|
382
|
+
const { id, method, params } = msg;
|
|
383
|
+
if (id === undefined)
|
|
384
|
+
return null;
|
|
385
|
+
try {
|
|
386
|
+
switch (method) {
|
|
387
|
+
case 'initialize':
|
|
388
|
+
return {
|
|
389
|
+
jsonrpc: '2.0', id,
|
|
390
|
+
result: {
|
|
391
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
392
|
+
capabilities: { tools: {} },
|
|
393
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
case 'tools/list':
|
|
397
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } };
|
|
398
|
+
case 'tools/call': {
|
|
399
|
+
const toolName = params?.name;
|
|
400
|
+
const toolArgs = (params?.arguments ?? {});
|
|
401
|
+
const text = await handleToolCall(toolName, toolArgs);
|
|
402
|
+
return {
|
|
403
|
+
jsonrpc: '2.0', id,
|
|
404
|
+
result: { content: [{ type: 'text', text }], isError: false }
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
default:
|
|
408
|
+
return {
|
|
409
|
+
jsonrpc: '2.0', id,
|
|
410
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
return {
|
|
416
|
+
jsonrpc: '2.0', id,
|
|
417
|
+
result: { content: [{ type: 'text', text: `Error: ${e.message}` }], isError: true }
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
422
|
+
rl.on('line', (line) => {
|
|
423
|
+
const trimmed = line.trim();
|
|
424
|
+
if (!trimmed)
|
|
425
|
+
return;
|
|
426
|
+
try {
|
|
427
|
+
const msg = JSON.parse(trimmed);
|
|
428
|
+
handleRequest(msg).then(response => {
|
|
429
|
+
if (response)
|
|
430
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
431
|
+
}).catch((e) => {
|
|
432
|
+
process.stderr.write(`[claudestat-mcp] Handler error: ${e.message}\n`);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
process.stderr.write(`[claudestat-mcp] Parse error: ${e.message}\n`);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
440
|
+
process.on('SIGINT', () => process.exit(0));
|
|
441
|
+
// API quota is refreshed on-demand per get_quota_status call (disk cache throttles to 1 call/5min)
|
|
442
|
+
process.stderr.write(`[claudestat-mcp] Server ready (stdio, protocol ${PROTOCOL_VERSION})\n`);
|
package/dist/notifier.js
CHANGED
|
@@ -8,15 +8,33 @@ const child_process_1 = require("child_process");
|
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
function sendDesktopNotification(title, body) {
|
|
10
10
|
try {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
(
|
|
11
|
+
const platform = os_1.default.platform();
|
|
12
|
+
if (platform === 'darwin') {
|
|
13
|
+
const t = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
14
|
+
const b = body.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
15
|
+
(0, child_process_1.execSync)(`osascript -e 'display notification "${b}" with title "${t}"'`, { stdio: 'ignore', timeout: 5000 });
|
|
14
16
|
}
|
|
15
|
-
else if (
|
|
16
|
-
(0, child_process_1.execSync)(`notify-send "${title}" "${body}"`, { stdio: 'ignore' });
|
|
17
|
+
else if (platform === 'linux') {
|
|
18
|
+
(0, child_process_1.execSync)(`notify-send "${title}" "${body}"`, { stdio: 'ignore', timeout: 5000 });
|
|
19
|
+
}
|
|
20
|
+
else if (platform === 'win32') {
|
|
21
|
+
// UTF-16LE base64-encoded PowerShell to handle any special characters safely
|
|
22
|
+
const script = [
|
|
23
|
+
`Add-Type -AssemblyName System.Windows.Forms`,
|
|
24
|
+
`$n = New-Object System.Windows.Forms.NotifyIcon`,
|
|
25
|
+
`$n.Icon = [System.Drawing.SystemIcons]::Information`,
|
|
26
|
+
`$n.BalloonTipIcon = [System.Windows.Forms.ToolTipIcon]::Info`,
|
|
27
|
+
`$n.BalloonTipTitle = '${title.replace(/'/g, "''")}'`,
|
|
28
|
+
`$n.BalloonTipText = '${body.replace(/'/g, "''")}'`,
|
|
29
|
+
`$n.Visible = $true`,
|
|
30
|
+
`$n.ShowBalloonTip(5000)`,
|
|
31
|
+
`Start-Sleep -Milliseconds 200`,
|
|
32
|
+
].join('; ');
|
|
33
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
34
|
+
(0, child_process_1.execSync)(`powershell -NoProfile -NonInteractive -EncodedCommand ${encoded}`, { stdio: 'ignore', timeout: 8000 });
|
|
17
35
|
}
|
|
18
36
|
}
|
|
19
37
|
catch {
|
|
20
|
-
// notification
|
|
38
|
+
// notification unavailable — silent fallback
|
|
21
39
|
}
|
|
22
40
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - High cache ratio → positive: great cost efficiency
|
|
13
13
|
* - High avg cost → consider Haiku for simpler tasks
|
|
14
14
|
* - Low efficiency → linked to loops
|
|
15
|
+
* - Write vs Edit → suggest Edit for incremental changes
|
|
15
16
|
*/
|
|
16
17
|
export type InsightLevel = 'tip' | 'warning' | 'positive';
|
|
17
18
|
export interface PatternInsight {
|
package/dist/pattern-analyzer.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - High cache ratio → positive: great cost efficiency
|
|
14
14
|
* - High avg cost → consider Haiku for simpler tasks
|
|
15
15
|
* - Low efficiency → linked to loops
|
|
16
|
+
* - Write vs Edit → suggest Edit for incremental changes
|
|
16
17
|
*/
|
|
17
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
19
|
exports.analyzePatterns = analyzePatterns;
|
|
@@ -52,6 +53,18 @@ function analyzePatterns(toolCounts, stats) {
|
|
|
52
53
|
metric: `${bashPct}% Bash (${bashCount}) vs ${readGrep} Read+Grep+Glob`,
|
|
53
54
|
});
|
|
54
55
|
}
|
|
56
|
+
// ── Heavy Write vs Edit ────────────────────────────────────────────────────
|
|
57
|
+
const writeCount = get('Write');
|
|
58
|
+
const editCount = get('Edit');
|
|
59
|
+
const writePct = Math.round(writeCount / totalTools * 100);
|
|
60
|
+
if (writeCount > editCount * 3 && writeCount >= 10) {
|
|
61
|
+
insights.push({
|
|
62
|
+
level: 'tip',
|
|
63
|
+
title: 'Heavy Write vs Edit usage',
|
|
64
|
+
description: 'More than 3× Write calls vs Edit calls. Editing existing files is cheaper in tokens than writing from scratch. Consider using Edit for incremental changes instead of rewriting entire files.',
|
|
65
|
+
metric: `${writePct}% Write (${writeCount} writes vs ${editCount} edits)`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
55
68
|
// ── High loop rate ────────────────────────────────────────────────────────
|
|
56
69
|
if (stats.avg_loops >= 1.5) {
|
|
57
70
|
insights.push({
|
package/dist/quota-tracker.d.ts
CHANGED
|
@@ -32,11 +32,18 @@ export interface QuotaData {
|
|
|
32
32
|
weeklyTokensHaiku: number;
|
|
33
33
|
weeklyLimitSonnet: number;
|
|
34
34
|
weeklyLimitOpus: number;
|
|
35
|
+
weeklyPctAll: number;
|
|
35
36
|
burnRateTokensPerMin: number;
|
|
36
37
|
detectedPlan: ClaudePlan;
|
|
37
38
|
planSource: 'config' | 'keychain' | 'inferred';
|
|
38
39
|
computedAt: number;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Llama a la API de Anthropic para obtener los % de quota exactos que muestra claude.ai.
|
|
43
|
+
* Actualiza apiCache si la llamada tiene éxito; de lo contrario, no hace nada (silent fallback).
|
|
44
|
+
* Debe llamarse periódicamente desde el daemon y al inicio del MCP server.
|
|
45
|
+
*/
|
|
46
|
+
export declare function refreshFromApi(): Promise<void>;
|
|
40
47
|
/**
|
|
41
48
|
* Calcula y retorna QuotaData.
|
|
42
49
|
* Usa caché de 30s para no re-leer todos los JSONL en cada request del dashboard.
|