@statforge/claudestat 1.2.0 β 1.2.3
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 +199 -87
- 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 +32 -15
- package/dist/db.d.ts +18 -0
- package/dist/db.js +50 -0
- package/dist/doctor.js +28 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.js +199 -76
- package/dist/insights.d.ts +26 -0
- package/dist/insights.js +172 -20
- package/dist/install.js +39 -0
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +148 -21
- 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 +6 -0
- package/dist/quota-tracker.js +79 -32
- package/dist/roast.js +129 -40
- package/dist/routes/events.js +1 -1
- package/package.json +1 -1
package/dist/insights.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
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.getWeeklyInsightData = getWeeklyInsightData;
|
|
4
7
|
exports.generateTip = generateTip;
|
|
8
|
+
exports.getUsageInsights = getUsageInsights;
|
|
9
|
+
exports.renderInsights = renderInsights;
|
|
5
10
|
exports.shouldShowInsight = shouldShowInsight;
|
|
6
11
|
exports.markInsightShown = markInsightShown;
|
|
7
12
|
exports.renderWeeklyInsight = renderWeeklyInsight;
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
8
14
|
const db_1 = require("./db");
|
|
9
15
|
const WEEK_MS = 7 * 86400000;
|
|
10
16
|
const META_KEY = 'last_insight_at';
|
|
@@ -58,6 +64,145 @@ function generateTip(d) {
|
|
|
58
64
|
}
|
|
59
65
|
return 'Enable quota alerts with "claudestat config --alerts true" to avoid surprise limits';
|
|
60
66
|
}
|
|
67
|
+
function getUsageInsights(days = 7) {
|
|
68
|
+
const agg = db_1.dbOps.getWeeklyInsight(days);
|
|
69
|
+
const aggTotal = db_1.dbOps.getWeeklyInsight(days * 2);
|
|
70
|
+
const prevSessions = aggTotal.total_sessions - agg.total_sessions;
|
|
71
|
+
const effDelta = prevSessions > 2
|
|
72
|
+
? Math.round(agg.avg_efficiency -
|
|
73
|
+
(aggTotal.avg_efficiency * aggTotal.total_sessions - agg.avg_efficiency * agg.total_sessions) / prevSessions)
|
|
74
|
+
: -999;
|
|
75
|
+
const totalInputWithCache = agg.input_tokens + agg.cache_read;
|
|
76
|
+
const cacheHitPct = totalInputWithCache > 0
|
|
77
|
+
? Math.min(100, Math.round(agg.cache_read / totalInputWithCache * 100))
|
|
78
|
+
: 0;
|
|
79
|
+
const outputInputRatio = agg.input_tokens > 0
|
|
80
|
+
? parseFloat((agg.output_tokens / agg.input_tokens).toFixed(1))
|
|
81
|
+
: 0;
|
|
82
|
+
const ratioLabel = outputInputRatio > 10 ? 'cache-heavy workload'
|
|
83
|
+
: outputInputRatio > 5 ? 'generation-heavy'
|
|
84
|
+
: outputInputRatio > 2 ? 'balanced'
|
|
85
|
+
: 'reading-heavy';
|
|
86
|
+
const CACHE_READ_PRICE = {
|
|
87
|
+
'claude-haiku-4-5-20251001': 0.30 / 1000000,
|
|
88
|
+
'claude-sonnet-4-6': 2.70 / 1000000,
|
|
89
|
+
'claude-opus-4-6': 3.00 / 1000000,
|
|
90
|
+
};
|
|
91
|
+
const DEFAULT_CACHE_PRICE = 2.70 / 1000000;
|
|
92
|
+
const cacheByModel = db_1.dbOps.getCacheReadByModel(days);
|
|
93
|
+
const cacheSavings = cacheByModel.reduce((total, row) => {
|
|
94
|
+
const price = CACHE_READ_PRICE[row.model] ?? DEFAULT_CACHE_PRICE;
|
|
95
|
+
return total + row.cache_read * price;
|
|
96
|
+
}, 0);
|
|
97
|
+
return {
|
|
98
|
+
days,
|
|
99
|
+
total_sessions: agg.total_sessions,
|
|
100
|
+
total_cost: agg.total_cost,
|
|
101
|
+
avg_cost_per_session: agg.total_sessions > 0 ? agg.total_cost / agg.total_sessions : 0,
|
|
102
|
+
cache_savings_usd: cacheSavings,
|
|
103
|
+
cache_hit_pct: cacheHitPct,
|
|
104
|
+
output_input_ratio: outputInputRatio,
|
|
105
|
+
ratio_label: ratioLabel,
|
|
106
|
+
avg_efficiency: Math.round(agg.avg_efficiency),
|
|
107
|
+
efficiency_delta: effDelta,
|
|
108
|
+
total_loops: agg.total_loops,
|
|
109
|
+
project_costs: db_1.dbOps.getProjectCosts(days),
|
|
110
|
+
hour_ranges: (() => {
|
|
111
|
+
const hours = db_1.dbOps.getHourlyDistribution(days);
|
|
112
|
+
const dawn = hours.filter(h => h.hour >= 0 && h.hour <= 5).reduce((s, h) => s + h.session_count, 0);
|
|
113
|
+
const morn = hours.filter(h => h.hour >= 6 && h.hour <= 11).reduce((s, h) => s + h.session_count, 0);
|
|
114
|
+
const after = hours.filter(h => h.hour >= 12 && h.hour <= 17).reduce((s, h) => s + h.session_count, 0);
|
|
115
|
+
const night = hours.filter(h => h.hour >= 18 && h.hour <= 23).reduce((s, h) => s + h.session_count, 0);
|
|
116
|
+
return [
|
|
117
|
+
{ emoji: 'π', from: '00:00', to: '05:59', count: dawn },
|
|
118
|
+
{ emoji: 'π
', from: '06:00', to: '11:59', count: morn },
|
|
119
|
+
{ emoji: 'βοΈ', from: '12:00', to: '17:59', count: after },
|
|
120
|
+
{ emoji: 'π', from: '18:00', to: '23:59', count: night },
|
|
121
|
+
].filter(r => r.count > 0);
|
|
122
|
+
})(),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function renderInsights(d) {
|
|
126
|
+
const R = '\x1b[0m';
|
|
127
|
+
const B = '\x1b[1m';
|
|
128
|
+
const D = '\x1b[2m';
|
|
129
|
+
const G = '\x1b[32m';
|
|
130
|
+
const Y = '\x1b[33m';
|
|
131
|
+
const C = '\x1b[36m';
|
|
132
|
+
const M = '\x1b[35m';
|
|
133
|
+
const bar = (pct, width = 20) => 'β'.repeat(Math.round(pct / 100 * width)) + 'β'.repeat(width - Math.round(pct / 100 * width));
|
|
134
|
+
const fmtDollar = (n) => n < 0.01 ? '< $0.01' : `$${n.toFixed(2)}`;
|
|
135
|
+
const lines = [];
|
|
136
|
+
lines.push(`\n${B}π‘ claudestat insights${R} ${D}last ${d.days} days${R}`);
|
|
137
|
+
lines.push('β'.repeat(44));
|
|
138
|
+
// Cost summary
|
|
139
|
+
lines.push(`\n π° ${B}${fmtDollar(d.avg_cost_per_session)}/session${R} Β· ${d.total_sessions} sessions Β· ${fmtDollar(d.total_cost)} total`);
|
|
140
|
+
// Top projects
|
|
141
|
+
if (d.project_costs.length > 0) {
|
|
142
|
+
lines.push(`\n π Top projects`);
|
|
143
|
+
const topTotal = d.project_costs.reduce((s, p) => s + p.total_cost, 0);
|
|
144
|
+
const shown = d.project_costs.slice(0, 4);
|
|
145
|
+
const otherCost = d.total_cost - shown.reduce((s, p) => s + p.total_cost, 0);
|
|
146
|
+
for (let i = 0; i < shown.length; i++) {
|
|
147
|
+
const p = shown[i];
|
|
148
|
+
const pct = topTotal > 0 ? Math.round(p.total_cost / topTotal * 100) : 0;
|
|
149
|
+
const name = path_1.default.basename(p.project).slice(0, 14).padEnd(14);
|
|
150
|
+
lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(p.total_cost)} ${D}${pct}%${R}`);
|
|
151
|
+
if (i < shown.length - 1 || (otherCost > 0.01 && d.project_costs.length > 4))
|
|
152
|
+
lines.push('');
|
|
153
|
+
}
|
|
154
|
+
if (otherCost > 0.01 && d.project_costs.length > 4) {
|
|
155
|
+
const pct = topTotal > 0 ? Math.round(otherCost / topTotal * 100) : 0;
|
|
156
|
+
lines.push(` ${'other'.padEnd(14)} ${bar(pct)} ${fmtDollar(otherCost)} ${D}${pct}%${R}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Cache savings
|
|
160
|
+
const savingsLabel = d.cache_savings_usd >= 0.01
|
|
161
|
+
? `${G}~${fmtDollar(d.cache_savings_usd)}${R} saved`
|
|
162
|
+
: 'no savings yet';
|
|
163
|
+
lines.push(`\n β‘ Cache ${savingsLabel} Β· ${d.cache_hit_pct}% hit rate`);
|
|
164
|
+
// Output/input ratio
|
|
165
|
+
lines.push(`\n π ${B}${d.output_input_ratio}Γ${R} output/input Β· ${D}${d.ratio_label}${R}`);
|
|
166
|
+
// Efficiency trend
|
|
167
|
+
let effTrend = '';
|
|
168
|
+
if (d.efficiency_delta !== -999) {
|
|
169
|
+
const arrow = d.efficiency_delta > 0
|
|
170
|
+
? `${G}β +${d.efficiency_delta}${R}`
|
|
171
|
+
: d.efficiency_delta < 0 ? `${Y}β ${d.efficiency_delta}${R}` : 'β same';
|
|
172
|
+
effTrend = ` ${arrow} vs prev period`;
|
|
173
|
+
}
|
|
174
|
+
const loopLabel = d.total_loops > 0 ? ` Β· ${Y}${d.total_loops} loops${R}` : '';
|
|
175
|
+
lines.push(`\n π Efficiency ${B}${d.avg_efficiency}/100${R}${effTrend}${loopLabel}`);
|
|
176
|
+
// Activity by time range
|
|
177
|
+
if (d.hour_ranges.length > 0) {
|
|
178
|
+
lines.push(`\n β° Activity by time of day`);
|
|
179
|
+
const maxCount = Math.max(...d.hour_ranges.map(r => r.count));
|
|
180
|
+
for (let i = 0; i < d.hour_ranges.length; i++) {
|
|
181
|
+
const r = d.hour_ranges[i];
|
|
182
|
+
const pct = maxCount > 0 ? Math.round(r.count / maxCount * 100) : 0;
|
|
183
|
+
lines.push(` ${r.emoji} ${D}${r.from}β${r.to}${R} ${M}${bar(pct)}${R} ${D}${r.count} sessions${R}`);
|
|
184
|
+
if (i < d.hour_ranges.length - 1)
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Model breakdown
|
|
189
|
+
const models = db_1.dbOps.getModelBreakdown(d.days);
|
|
190
|
+
const modelsWithCost = models.filter(m => m.total_cost > 0);
|
|
191
|
+
if (modelsWithCost.length >= 2) {
|
|
192
|
+
lines.push(`\n π€ Models`);
|
|
193
|
+
for (let i = 0; i < modelsWithCost.length; i++) {
|
|
194
|
+
const m = modelsWithCost[i];
|
|
195
|
+
const pct = d.total_cost > 0 ? Math.round(m.total_cost / d.total_cost * 100) : 0;
|
|
196
|
+
const rawName = (m.model ?? 'unknown').replace(/^<|>$/g, '');
|
|
197
|
+
const name = rawName.slice(0, 28).padEnd(28);
|
|
198
|
+
lines.push(` ${C}${name}${R} ${bar(pct)} ${fmtDollar(m.total_cost)} ${D}${pct}% Β· ${m.session_count} sessions${R}`);
|
|
199
|
+
if (i < modelsWithCost.length - 1)
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
lines.push('\n' + 'β'.repeat(44) + '\n');
|
|
204
|
+
return lines.join('\n');
|
|
205
|
+
}
|
|
61
206
|
function shouldShowInsight() {
|
|
62
207
|
const last = db_1.dbOps.getMeta(META_KEY);
|
|
63
208
|
if (!last)
|
|
@@ -75,31 +220,38 @@ function renderWeeklyInsight(d) {
|
|
|
75
220
|
return `${Math.round(n / 1000)}K`;
|
|
76
221
|
return n.toString();
|
|
77
222
|
};
|
|
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
223
|
const R = '\x1b[0m';
|
|
84
224
|
const B = '\x1b[1m';
|
|
85
225
|
const D = '\x1b[2m';
|
|
226
|
+
const G = '\x1b[32m';
|
|
227
|
+
const Y = '\x1b[33m';
|
|
86
228
|
const C = '\x1b[36m';
|
|
229
|
+
const bar = (pct, width = 20) => {
|
|
230
|
+
const filled = Math.round(Math.min(pct, 100) / 100 * width);
|
|
231
|
+
const color = pct >= 90 ? '\x1b[31m' : pct >= 70 ? '\x1b[33m' : '\x1b[32m';
|
|
232
|
+
return `${color}${'β'.repeat(filled)}${R}${D}${'β'.repeat(width - filled)}${R}`;
|
|
233
|
+
};
|
|
234
|
+
const fmtDate = (ts) => {
|
|
235
|
+
const dt = new Date(ts);
|
|
236
|
+
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
237
|
+
};
|
|
87
238
|
const lines = [];
|
|
88
|
-
lines.push(`\n${B}π claudestat weekly
|
|
89
|
-
lines.push(
|
|
90
|
-
lines.push(
|
|
91
|
-
lines.push(`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
lines.push(
|
|
102
|
-
lines.push(
|
|
239
|
+
lines.push(`\n${B}π claudestat weekly${R} ${D}${fmtDate(d.week_start)} β ${fmtDate(d.week_end)}${R}`);
|
|
240
|
+
lines.push('β'.repeat(44));
|
|
241
|
+
lines.push('');
|
|
242
|
+
lines.push(` π° ${B}$${d.total_cost.toFixed(2)}${R} total Β· ${B}${d.total_sessions}${R} sessions Β· ${B}${d.total_loops}${R} loops`);
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push(` π§ Top tool ${B}${d.top_tool}${R} ${D}${d.top_tool_cost_pct}% of cost${R}`);
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(` π Efficiency ${bar(d.avg_efficiency)} ${B}${d.avg_efficiency}/100${R}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push(` πΎ Cache hit ${bar(d.cache_hit_pct)} ${B}${d.cache_hit_pct}%${R}`);
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(` π¦ Tokens ${D}${fmtTok(d.input_tokens)} in + ${fmtTok(d.output_tokens)} out${R}`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
lines.push(` ${C}β‘ Tip:${R} ${generateTip(d)}`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
lines.push('β'.repeat(44));
|
|
103
255
|
lines.push('');
|
|
104
256
|
return lines.join('\n');
|
|
105
257
|
}
|
package/dist/install.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.uninstallHooks = uninstallHooks;
|
|
|
20
20
|
const fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
22
22
|
const readline_1 = __importDefault(require("readline"));
|
|
23
|
+
const child_process_1 = require("child_process");
|
|
23
24
|
const paths_1 = require("./paths");
|
|
24
25
|
const config_1 = require("./config");
|
|
25
26
|
const CLAUDESTAT_DIR = (0, paths_1.getClaudestatDir)();
|
|
@@ -98,7 +99,9 @@ async function runInstall() {
|
|
|
98
99
|
}
|
|
99
100
|
else {
|
|
100
101
|
showInstallStatus();
|
|
102
|
+
installMcp();
|
|
101
103
|
}
|
|
104
|
+
process.exit(0);
|
|
102
105
|
}
|
|
103
106
|
async function runWizard() {
|
|
104
107
|
const nonInteractive = !process.stdin.isTTY;
|
|
@@ -150,6 +153,42 @@ async function runWizard() {
|
|
|
150
153
|
}
|
|
151
154
|
// Paso 5: instalar hooks
|
|
152
155
|
installHooks();
|
|
156
|
+
// Paso 6: registrar MCP server en Claude Code
|
|
157
|
+
installMcp();
|
|
158
|
+
}
|
|
159
|
+
function installMcp() {
|
|
160
|
+
const nodeExec = process.execPath;
|
|
161
|
+
const mcpScript = path_1.default.join(__dirname, 'mcp-server.js');
|
|
162
|
+
const manualCmd = `claude mcp add claudestat -s user -- "${nodeExec}" --disable-warning=ExperimentalWarning "${mcpScript}"`;
|
|
163
|
+
try {
|
|
164
|
+
const result = (0, child_process_1.spawnSync)('claude', ['mcp', 'list'], { encoding: 'utf8', timeout: 15000 });
|
|
165
|
+
const list = (result.stdout ?? '') + (result.stderr ?? '');
|
|
166
|
+
const mcpLine = list.split('\n').find((l) => l.includes('claudestat'));
|
|
167
|
+
if (mcpLine && !mcpLine.includes('Failed') && !mcpLine.includes('β')) {
|
|
168
|
+
console.log(' (already registered): MCP server');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (mcpLine) {
|
|
172
|
+
// Registered but failing β remove from both scopes and re-register
|
|
173
|
+
(0, child_process_1.spawnSync)('claude', ['mcp', 'remove', 'claudestat', '-s', 'user'], { encoding: 'utf8' });
|
|
174
|
+
(0, child_process_1.spawnSync)('claude', ['mcp', 'remove', 'claudestat', '-s', 'local'], { encoding: 'utf8' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
console.log('\nβ Could not reach "claude" CLI β skipping MCP setup.');
|
|
179
|
+
console.log(' To register manually:');
|
|
180
|
+
console.log(` ${manualCmd}\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
(0, child_process_1.execSync)(manualCmd, { stdio: 'pipe' });
|
|
185
|
+
console.log('β MCP server registered (user scope)\n');
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.log('\nβ MCP registration failed.');
|
|
189
|
+
console.log(' To register manually:');
|
|
190
|
+
console.log(` ${manualCmd}\n`);
|
|
191
|
+
}
|
|
153
192
|
}
|
|
154
193
|
function showInstallStatus() {
|
|
155
194
|
const cfg = (0, config_1.readConfig)();
|
package/dist/mcp-server.d.ts
CHANGED
package/dist/mcp-server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
4
|
* mcp-server.ts β MCP (Model Context Protocol) server for claudestat
|
|
@@ -50,9 +50,10 @@ const readline = __importStar(require("readline"));
|
|
|
50
50
|
const db_1 = require("./db");
|
|
51
51
|
const quota_tracker_1 = require("./quota-tracker");
|
|
52
52
|
const insights_1 = require("./insights");
|
|
53
|
+
const config_1 = require("./config");
|
|
53
54
|
const SERVER_NAME = 'claudestat';
|
|
54
|
-
const SERVER_VERSION = '1.2.
|
|
55
|
-
const PROTOCOL_VERSION = '2025-
|
|
55
|
+
const SERVER_VERSION = '1.2.2';
|
|
56
|
+
const PROTOCOL_VERSION = '2025-03-26';
|
|
56
57
|
const TOOLS = [
|
|
57
58
|
{
|
|
58
59
|
name: 'get_quota_status',
|
|
@@ -104,6 +105,34 @@ const TOOLS = [
|
|
|
104
105
|
required: []
|
|
105
106
|
}
|
|
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
|
+
},
|
|
107
136
|
{
|
|
108
137
|
name: 'get_weekly_insight',
|
|
109
138
|
description: 'Get the weekly usage summary with an actionable tip (same as claudestat weekly command)',
|
|
@@ -141,10 +170,13 @@ function toolGetQuotaStatus() {
|
|
|
141
170
|
: `${resetMin}m`;
|
|
142
171
|
const weeklyTotalHours = q.weeklyHoursSonnet + q.weeklyHoursOpus;
|
|
143
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`;
|
|
144
176
|
const parts = [
|
|
145
|
-
`Quota status β ${
|
|
177
|
+
`Quota status β ${planLabel}`,
|
|
146
178
|
``,
|
|
147
|
-
`5h cycle: ${q.cyclePrompts}/${q.cycleLimit} prompts (
|
|
179
|
+
`5h cycle: ${q.cyclePct}% Β· ${q.cyclePrompts > q.cycleLimit ? `${q.cyclePrompts}/${q.cycleLimit} prompts (OVER LIMIT)` : `${q.cyclePrompts}/${q.cycleLimit} prompts`} Β· resets in ${resetLabel}`,
|
|
148
180
|
`Weekly: ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${q.weeklyPctAll}%)`,
|
|
149
181
|
];
|
|
150
182
|
if (q.weeklyLimitOpus > 0) {
|
|
@@ -154,6 +186,33 @@ function toolGetQuotaStatus() {
|
|
|
154
186
|
if (q.burnRateTokensPerMin > 0) {
|
|
155
187
|
parts.push(`Burn rate: ${q.burnRateTokensPerMin.toLocaleString()} tokens/min`);
|
|
156
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
|
+
}
|
|
157
216
|
return parts.join('\n');
|
|
158
217
|
}
|
|
159
218
|
function toolGetCurrentSession() {
|
|
@@ -221,6 +280,71 @@ function toolGetTopTools(days, sortBy) {
|
|
|
221
280
|
}
|
|
222
281
|
return lines.join('\n');
|
|
223
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
|
+
}
|
|
224
348
|
function toolGetWeeklyInsight(days) {
|
|
225
349
|
const d = Math.max(1, Math.min(90, Math.floor(days || 7)));
|
|
226
350
|
const data = (0, insights_1.getWeeklyInsightData)(d);
|
|
@@ -239,25 +363,25 @@ function toolGetWeeklyInsight(days) {
|
|
|
239
363
|
`Tip: ${(0, insights_1.generateTip)(data)}`,
|
|
240
364
|
].join('\n');
|
|
241
365
|
}
|
|
242
|
-
function handleToolCall(name, args) {
|
|
243
|
-
const days = typeof args.days === 'number' ? args.days : 7;
|
|
366
|
+
async function handleToolCall(name, args) {
|
|
244
367
|
const sortBy = typeof args.sort_by === 'string' ? args.sort_by : 'cost';
|
|
245
368
|
switch (name) {
|
|
246
|
-
case 'get_quota_status':
|
|
369
|
+
case 'get_quota_status':
|
|
370
|
+
await (0, quota_tracker_1.refreshFromApi)();
|
|
371
|
+
return toolGetQuotaStatus();
|
|
247
372
|
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 '
|
|
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);
|
|
251
378
|
default: return `Unknown tool: ${name}`;
|
|
252
379
|
}
|
|
253
380
|
}
|
|
254
|
-
function handleRequest(msg) {
|
|
381
|
+
async function handleRequest(msg) {
|
|
255
382
|
const { id, method, params } = msg;
|
|
256
|
-
if (id === undefined)
|
|
257
|
-
if (method === 'notifications/initialized')
|
|
258
|
-
return null;
|
|
383
|
+
if (id === undefined)
|
|
259
384
|
return null;
|
|
260
|
-
}
|
|
261
385
|
try {
|
|
262
386
|
switch (method) {
|
|
263
387
|
case 'initialize':
|
|
@@ -274,7 +398,7 @@ function handleRequest(msg) {
|
|
|
274
398
|
case 'tools/call': {
|
|
275
399
|
const toolName = params?.name;
|
|
276
400
|
const toolArgs = (params?.arguments ?? {});
|
|
277
|
-
const text = handleToolCall(toolName, toolArgs);
|
|
401
|
+
const text = await handleToolCall(toolName, toolArgs);
|
|
278
402
|
return {
|
|
279
403
|
jsonrpc: '2.0', id,
|
|
280
404
|
result: { content: [{ type: 'text', text }], isError: false }
|
|
@@ -301,10 +425,12 @@ rl.on('line', (line) => {
|
|
|
301
425
|
return;
|
|
302
426
|
try {
|
|
303
427
|
const msg = JSON.parse(trimmed);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
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
|
+
});
|
|
308
434
|
}
|
|
309
435
|
catch (e) {
|
|
310
436
|
process.stderr.write(`[claudestat-mcp] Parse error: ${e.message}\n`);
|
|
@@ -312,4 +438,5 @@ rl.on('line', (line) => {
|
|
|
312
438
|
});
|
|
313
439
|
process.on('SIGTERM', () => process.exit(0));
|
|
314
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)
|
|
315
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
|
@@ -38,6 +38,12 @@ export interface QuotaData {
|
|
|
38
38
|
planSource: 'config' | 'keychain' | 'inferred';
|
|
39
39
|
computedAt: number;
|
|
40
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>;
|
|
41
47
|
/**
|
|
42
48
|
* Calcula y retorna QuotaData.
|
|
43
49
|
* Usa cachΓ© de 30s para no re-leer todos los JSONL en cada request del dashboard.
|