@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 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
- Sonnet 3.2h / 5h this week
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 the 5h cycle quota percentage.
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
- Current 45%🟡 pro
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.kill(pid, 'SIGTERM');
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
- console.log(`Current ${pctCycle}%${cycleEmoji} ${q.detectedPlan}`);
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
- ` Sonnet ${q.weeklyHoursSonnet}h / ${q.weeklyLimitSonnet}h this week\n` +
210
+ ` Weekly ${weeklyTotalHours}h / ${weeklyLimitTotal}h (${weeklyPctColor}${q.weeklyPctAll}%${R}) this week\n` +
201
211
  (q.weeklyLimitOpus > 0
202
- ? ` Opus ${q.weeklyHoursOpus}h / ${q.weeklyLimitOpus}h this week\n`
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;
@@ -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`);
@@ -32,6 +32,7 @@ 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';
@@ -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,
@@ -75,7 +75,7 @@ exports.eventsRouter.post('/event', (req, res) => {
75
75
  return;
76
76
  }
77
77
  const resolvedCwd = cwd
78
- ?? (transcript_path ? transcript_path.split('/').slice(0, -1).join('/') : undefined);
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' && filePath.startsWith('/')) {
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);
@@ -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?.startsWith('/'))
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?.startsWith('/'))
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?.split('/').pop() || 'unknown';
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,
@@ -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' && fp.startsWith('/')) {
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.1.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": "bash run-tests.sh"
62
+ "test": "node --require tsx/cjs tests/index.ts"
62
63
  },
63
64
  "dependencies": {
64
65
  "@anthropic-ai/sdk": "^0.88.0",