@statforge/claudestat 1.0.1 → 1.1.1

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
@@ -12,7 +12,7 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
12
12
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
13
13
  [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
14
14
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-brightgreen)]()
15
- [![Tests](https://img.shields.io/badge/tests-208%2F208-brightgreen)]()
15
+ [![Tests](https://img.shields.io/badge/tests-214%2F214-brightgreen)]()
16
16
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](CONTRIBUTING.md)
17
17
 
18
18
  [Installation](#installation) • [Quick Start](#quick-start) • [Commands](#commands) • [Dashboard](#dashboard) • [Contributing](#contributing)
@@ -33,6 +33,8 @@ Works with Claude Pro, Max 5, and Max 20. Zero cloud dependencies. Pure Node.js.
33
33
 
34
34
  ## Why?
35
35
 
36
+ You're burning tokens right now and you have no idea how many, on what, or whether Claude is stuck in a loop.
37
+
36
38
  Claude Code is powerful — but it's a black box while it runs. You can't see what it's spending, how deep the context is, whether it's looping, or if you're about to hit your quota limit.
37
39
 
38
40
  **claudestat fixes that.** It taps into Claude Code's hook system to capture every event, stores it locally in SQLite, and shows you everything in a live dashboard or terminal trace.
@@ -43,6 +45,8 @@ Claude Code is powerful — but it's a black box while it runs. You can't see wh
43
45
  - Per-session cost breakdown + cache savings + burn rate
44
46
  - AI-generated weekly usage reports
45
47
 
48
+ > If claudestat is useful, give it a ⭐ — it helps other developers find it.
49
+
46
50
  ---
47
51
 
48
52
  ## How it works
@@ -133,9 +137,12 @@ That's it. Start a Claude Code session and watch the events flow in.
133
137
  | `claudestat uninstall` | Remove hooks from Claude Code |
134
138
  | `claudestat watch` | Live terminal trace view |
135
139
  | `claudestat status` | Show quota, cost, and burn rate |
140
+ | `claudestat status --compact` | One-line output for tmux status bar |
136
141
  | `claudestat config` | View or edit configuration |
137
142
  | `claudestat top` | Rank tools by cost, call count, or duration |
138
143
  | `claudestat export [format]` | Export session data to JSON or CSV |
144
+ | `claudestat share [session-id]` | Generate shareable session card (ASCII/JSON) |
145
+ | `claudestat roast` | Sarcastic usage analysis with roast jokes |
139
146
  | `claudestat doctor` | Check installation health and diagnose issues |
140
147
 
141
148
  ### `claudestat watch`
@@ -187,6 +194,68 @@ claudestat status
187
194
  Burn rate 1,240 tok/min
188
195
  ```
189
196
 
197
+ ### `claudestat status --compact`
198
+
199
+ One-line output for tmux status bar or scripting. Shows the 5h cycle quota percentage.
200
+
201
+ ```bash
202
+ claudestat status --compact
203
+ Current 45%🟡 pro
204
+ ```
205
+
206
+ ### `claudestat share`
207
+
208
+ Generate a shareable session card — perfect for sharing on social media or in bug reports.
209
+
210
+ ```bash
211
+ claudestat share
212
+ ╔═══════════════════════════════════╗
213
+ ║ Session Report · claudestat ║
214
+ ╠═══════════════════════════════════╣
215
+ ║ Project my-project ║
216
+ ║ Duration 2h 14m ║
217
+ ║ Tools 847 calls ║
218
+ ║ Cost $0.84 ║
219
+ ║ Cache hit 27% saved ($0.31) ║
220
+ ║ Top tool Bash (38%) ║
221
+ ║ Efficiency 91 / 100 ║
222
+ ╚═══════════════════════════════════╝
223
+ github.com/DeibyGS/claudestat
224
+ ```
225
+
226
+ Options:
227
+ - `--format ascii|json` — output format (default: ascii)
228
+ - `--copy` — copy to clipboard automatically (macOS only)
229
+
230
+ ### `claudestat roast`
231
+
232
+ Get a sarcastic analysis of your Claude Code usage — humor with insights.
233
+
234
+ ```bash
235
+ claudestat roast
236
+
237
+ === Claude Code Stats (last 30 days) ===
238
+ Sessions: 47
239
+ Total cost: $12.40
240
+ Bash calls: 1,240
241
+ Loops: 8
242
+ Efficiency: 72/100
243
+
244
+ 🔥 Your Claude Code Roast
245
+
246
+ You called Bash 1,240 times last month.
247
+ That's once every 2.3 minutes.
248
+ Are you okay?
249
+
250
+ You hit 90%+ context in 12 sessions.
251
+ Claude was writing with amnesia half the time.
252
+
253
+ You spent $4.20 on loops you never noticed.
254
+ That's 14 coffees. Just saying.
255
+
256
+ Efficiency: 72/100 — room for growth, champ.
257
+ ```
258
+
190
259
  ### `claudestat doctor`
191
260
 
192
261
  Diagnoses common installation problems — useful if `claudestat start` fails or hooks are not firing.
@@ -432,6 +501,33 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines.
432
501
 
433
502
  ---
434
503
 
504
+ ## FAQ
505
+
506
+ **What is claudestat?**
507
+ claudestat is a real-time token monitoring and cost analytics tool for Claude Code.
508
+ It captures every tool call, token usage, and API cost as it happens — locally, with zero cloud dependencies.
509
+
510
+ **How do I monitor Claude Code token usage?**
511
+ Install with `npm install -g @statforge/claudestat`, run `claudestat start`, and open `http://localhost:7337` for the live dashboard.
512
+
513
+ **How do I track Claude Code costs?**
514
+ claudestat records every session's token usage and estimates API cost per tool call.
515
+ Use `claudestat status` for a quick summary or `claudestat export` for full data export.
516
+
517
+ **How do I get alerted when Claude Code hits the rate limit?**
518
+ claudestat polls your quota every 60 seconds and sends desktop notifications when you cross 70%, 85%, or 95%. Configure with `claudestat config --alerts true`.
519
+
520
+ **Does claudestat work with Claude Pro, Max 5, and Max 20?**
521
+ Yes. claudestat auto-detects your plan. You can also force it with `claudestat config --plan max5`.
522
+
523
+ **Is my data sent to any server?**
524
+ No. All data is stored locally in SQLite at `~/.claudestat/`. Zero cloud dependencies.
525
+
526
+ **Does claudestat work on Windows?**
527
+ Yes — macOS, Linux, and Windows are all supported.
528
+
529
+ ---
530
+
435
531
  ## License
436
532
 
437
533
  MIT — use it, fork it, ship it.
package/dist/daemon.js CHANGED
@@ -79,7 +79,7 @@ function migrateSessionProjects() {
79
79
  const sessions = db_1.dbOps.getAllSessions();
80
80
  let tagged = 0;
81
81
  for (const session of sessions) {
82
- if (session.project_path)
82
+ if (session?.project_path)
83
83
  continue;
84
84
  const events = db_1.dbOps.getSessionEvents(session.id);
85
85
  const projectCwd = (0, projects_1.inferProjectCwd)(events);
@@ -97,7 +97,7 @@ function migrateSessionProjects() {
97
97
  */
98
98
  async function migrateSessionSummaries(limit = 5) {
99
99
  const sessions = db_1.dbOps.getAllSessions()
100
- .filter(s => !s.ai_summary)
100
+ .filter(s => !s?.ai_summary)
101
101
  .slice(0, limit);
102
102
  for (const s of sessions) {
103
103
  try {
@@ -192,8 +192,10 @@ function startDaemon() {
192
192
  _server = app.listen(PORT, '127.0.0.1', () => {
193
193
  writePid();
194
194
  process.on('exit', cleanPid);
195
- process.on('SIGTERM', () => { shutdown(_server); process.exit(0); });
196
- process.on('SIGINT', () => { shutdown(_server); process.exit(0); });
195
+ process.on('SIGTERM', () => { if (_server)
196
+ shutdown(_server); process.exit(0); });
197
+ process.on('SIGINT', () => { if (_server)
198
+ shutdown(_server); process.exit(0); });
197
199
  console.log(`\n● claudestat daemon → http://localhost:${PORT}`);
198
200
  console.log(` Waiting for Claude Code events...\n`);
199
201
  console.log(` In another terminal: \x1b[36mclaudestat watch\x1b[0m\n`);
package/dist/enricher.js CHANGED
@@ -32,13 +32,7 @@ const fs_1 = __importDefault(require("fs"));
32
32
  const path_1 = __importDefault(require("path"));
33
33
  const chokidar_1 = __importDefault(require("chokidar"));
34
34
  const paths_1 = require("./paths");
35
- const PRICING = {
36
- 'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
37
- 'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
38
- 'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
39
- 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
40
- };
41
- const DEFAULT_PRICING = PRICING['claude-sonnet-4-6'];
35
+ const pricing_1 = require("./pricing");
42
36
  // ─── Context window dinámico ──────────────────────────────────────────────────
43
37
  const KNOWN_CONTEXT_WINDOWS = {
44
38
  'claude-opus-4-6': 200000,
@@ -48,16 +42,8 @@ const KNOWN_CONTEXT_WINDOWS = {
48
42
  function getContextWindow(model) {
49
43
  return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
50
44
  }
51
- // ─── Calculo de coste ─────────────────────────────────────────────────────────
52
- function calcCost(model, usage) {
53
- const price = PRICING[model] ?? DEFAULT_PRICING;
54
- const M = 1000000;
55
- return ((usage.input_tokens * price.input) / M +
56
- (usage.output_tokens * price.output) / M +
57
- (usage.cache_read_input_tokens * price.cacheRead) / M +
58
- (usage.cache_creation_input_tokens * price.cacheCreate) / M);
59
- }
60
45
  const fileOffsets = new Map();
46
+ const fileLocks = new Map(); // Lock per file
61
47
  const FILE_OFFSET_TTL = 30 * 60000; // 30 minutos
62
48
  function cleanupStaleOffsets() {
63
49
  const now = Date.now();
@@ -67,6 +53,10 @@ function cleanupStaleOffsets() {
67
53
  }
68
54
  }
69
55
  async function processJSONL(filePath) {
56
+ // Skip if already processing this file
57
+ if (fileLocks.has(filePath))
58
+ return null;
59
+ fileLocks.set(filePath, Promise.resolve());
70
60
  let fileContent;
71
61
  try {
72
62
  fileContent = await promises_1.default.readFile(filePath, 'utf8');
@@ -99,27 +89,27 @@ async function processJSONL(filePath) {
99
89
  if (obj.type !== 'assistant')
100
90
  continue;
101
91
  const msg = obj.message;
102
- const usage = msg?.usage;
103
- const model = msg?.model ?? undefined;
104
- if (!usage)
92
+ if (!msg?.usage)
105
93
  continue;
94
+ const usage = msg.usage;
95
+ const model = msg.model ?? 'claude-sonnet-4-6';
106
96
  if (firstTs === undefined && obj.timestamp) {
107
97
  try {
108
98
  firstTs = new Date(obj.timestamp).getTime();
109
99
  }
110
- catch { }
100
+ catch (e) { /* ignore invalid timestamp */ }
111
101
  }
112
102
  totals.input_tokens += usage.input_tokens ?? 0;
113
103
  totals.output_tokens += usage.output_tokens ?? 0;
114
104
  totals.cache_read += usage.cache_read_input_tokens ?? 0;
115
105
  totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
116
106
  const resolvedModel = model ?? 'claude-sonnet-4-6';
117
- totals.cost_usd += calcCost(resolvedModel, usage);
107
+ totals.cost_usd += (0, pricing_1.calcCost)(resolvedModel, usage);
118
108
  totals.context_used = (usage.input_tokens ?? 0)
119
109
  + (usage.cache_read_input_tokens ?? 0)
120
110
  + (usage.cache_creation_input_tokens ?? 0);
121
111
  totals.context_window = getContextWindow(resolvedModel);
122
- const price = PRICING[resolvedModel] ?? DEFAULT_PRICING;
112
+ const price = pricing_1.PRICING[resolvedModel] ?? pricing_1.DEFAULT_PRICING;
123
113
  const M = 1000000;
124
114
  lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
125
115
  (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
@@ -129,7 +119,9 @@ async function processJSONL(filePath) {
129
119
  lastOutputTokens = usage.output_tokens ?? 0;
130
120
  lastModel = model ?? lastModel;
131
121
  }
132
- catch { }
122
+ catch (e) {
123
+ console.warn('[enricher] Error calculating cost:', e);
124
+ }
133
125
  }
134
126
  if (lastInputUsd + lastOutputUsd > 0) {
135
127
  totals.lastEntry = {
@@ -143,14 +135,21 @@ async function processJSONL(filePath) {
143
135
  totals.lastModel = lastModel;
144
136
  totals.firstTs = firstTs;
145
137
  fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
138
+ fileLocks.delete(filePath);
146
139
  return totals;
147
140
  }
148
141
  const blockCostCache = new Map();
142
+ const costCacheLocks = new Map(); // Simple lock flag
149
143
  const BLOCK_COST_TTL = 5 * 60000;
150
144
  async function getAllBlockCostsForSession(sessionId) {
145
+ // Return cached if available and not expired
151
146
  const cached = blockCostCache.get(sessionId);
152
147
  if (cached && Date.now() - cached.ts < BLOCK_COST_TTL)
153
148
  return cached.data;
149
+ // Skip if already calculating for this session
150
+ if (costCacheLocks.get(sessionId))
151
+ return cached?.data ?? [];
152
+ costCacheLocks.set(sessionId, true);
154
153
  try {
155
154
  if (!fs_1.default.existsSync(PROJECTS_DIR))
156
155
  return [];
@@ -199,7 +198,7 @@ async function getAllBlockCostsForSession(sessionId) {
199
198
  const model = obj.message?.model ?? 'claude-sonnet-4-6';
200
199
  if (!usage)
201
200
  continue;
202
- const price = PRICING[model] ?? DEFAULT_PRICING;
201
+ const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
203
202
  const M = 1000000;
204
203
  const inUsd = ((usage.input_tokens ?? 0) * price.input +
205
204
  (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
@@ -214,15 +213,22 @@ async function getAllBlockCostsForSession(sessionId) {
214
213
  current.outputTokens += outTok;
215
214
  }
216
215
  }
217
- catch { }
216
+ catch (e) {
217
+ console.warn('[enricher] Error reading JSONL block:', e);
218
+ }
218
219
  }
219
220
  const filtered = result.filter(b => b.totalUsd > 0);
220
221
  blockCostCache.set(sessionId, { data: filtered, ts: Date.now() });
221
222
  return filtered;
222
223
  }
223
224
  }
224
- catch { }
225
- return [];
225
+ catch (e) {
226
+ console.warn('[enricher] Error calculating block costs:', e);
227
+ }
228
+ finally {
229
+ costCacheLocks.delete(sessionId);
230
+ }
231
+ return cached?.data ?? [];
226
232
  }
227
233
  async function getSessionPrompts(sessionId) {
228
234
  try {
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ const install_1 = require("./install");
27
27
  const export_1 = require("./export");
28
28
  const config_1 = require("./config");
29
29
  const doctor_1 = require("./doctor");
30
+ const share_1 = require("./share");
31
+ const roast_1 = require("./roast");
30
32
  const paths_1 = require("./paths");
31
33
  const program = new commander_1.Command();
32
34
  const PKG_VERSION = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', 'package.json'), 'utf8')).version;
@@ -144,6 +146,7 @@ program
144
146
  .command('status')
145
147
  .description('Show current quota, cost and burn rate')
146
148
  .option('--json', 'Output raw JSON instead of formatted text')
149
+ .option('--compact', 'One-line output for tmux')
147
150
  .action(async (opts) => {
148
151
  try {
149
152
  const [quotaRes, healthRes] = await Promise.all([
@@ -154,6 +157,12 @@ program
154
157
  throw new Error('Daemon unavailable');
155
158
  const q = await quotaRes.json();
156
159
  const _h = await healthRes.json().catch(() => ({}));
160
+ if (opts.compact) {
161
+ const pctCycle = q.cyclePct;
162
+ const cycleEmoji = pctCycle >= 95 ? '🔴' : pctCycle >= 70 ? '🟡' : '🟢';
163
+ console.log(`Current ${pctCycle}%${cycleEmoji} ${q.detectedPlan}`);
164
+ process.exit(0);
165
+ }
157
166
  if (opts.json) {
158
167
  console.log(JSON.stringify({
159
168
  cyclePrompts: q.cyclePrompts,
@@ -167,7 +176,7 @@ program
167
176
  weeklyLimitOpus: q.weeklyLimitOpus,
168
177
  burnRateTokensPerMin: q.burnRateTokensPerMin,
169
178
  }));
170
- return;
179
+ process.exit(0);
171
180
  }
172
181
  const R = '\x1b[0m';
173
182
  const pctColor = q.cyclePct >= 95 ? '\x1b[31m'
@@ -316,4 +325,37 @@ program
316
325
  console.error('\n❌ Error:', err.message);
317
326
  process.exit(1);
318
327
  }));
328
+ program
329
+ .command('share [session-id]')
330
+ .description('Generate a shareable session card (ASCII or JSON)')
331
+ .option('--format <type>', 'Output format: ascii, json (default: ascii)')
332
+ .option('--copy', 'Copy to clipboard (macOS only)')
333
+ .action(async (sessionId, opts) => {
334
+ try {
335
+ const format = (opts.format ?? 'ascii');
336
+ const copy = !!opts.copy;
337
+ await (0, share_1.runShare)({ sessionId, format, copy });
338
+ process.exit(0);
339
+ }
340
+ catch (err) {
341
+ console.error('\n❌ Error:', err.message);
342
+ process.exit(1);
343
+ }
344
+ });
345
+ program
346
+ .command('roast')
347
+ .description('Roast your Claude Code usage habits')
348
+ .option('--stats', 'Show raw stats only, no roast')
349
+ .option('--months <n>', 'Look back N months (default: 1)', String, '1')
350
+ .action(async (opts) => {
351
+ try {
352
+ const months = parseInt(opts.months || '1', 10);
353
+ await (0, roast_1.runRoast)({ stats: !!opts.stats, months });
354
+ process.exit(0);
355
+ }
356
+ catch (err) {
357
+ console.error('\n❌ Error:', err.message);
358
+ process.exit(1);
359
+ }
360
+ });
319
361
  program.parse();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * pricing.ts — Model pricing constants and cost calculation
3
+ *
4
+ * Centralized pricing data to avoid duplication across enricher.ts and project-scanner.ts.
5
+ * Prices are in USD per million tokens.
6
+ */
7
+ export interface ModelPricing {
8
+ input: number;
9
+ output: number;
10
+ cacheRead: number;
11
+ cacheCreate: number;
12
+ }
13
+ export declare const PRICING: Record<string, ModelPricing>;
14
+ export declare const DEFAULT_PRICING: ModelPricing;
15
+ export declare function calcCost(model: string, usage: {
16
+ input_tokens: number;
17
+ output_tokens: number;
18
+ cache_read_input_tokens: number;
19
+ cache_creation_input_tokens: number;
20
+ }): number;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * pricing.ts — Model pricing constants and cost calculation
4
+ *
5
+ * Centralized pricing data to avoid duplication across enricher.ts and project-scanner.ts.
6
+ * Prices are in USD per million tokens.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.DEFAULT_PRICING = exports.PRICING = void 0;
10
+ exports.calcCost = calcCost;
11
+ exports.PRICING = {
12
+ 'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
13
+ 'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
14
+ 'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
15
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
16
+ };
17
+ exports.DEFAULT_PRICING = exports.PRICING['claude-sonnet-4-6'];
18
+ function calcCost(model, usage) {
19
+ const price = exports.PRICING[model] ?? exports.DEFAULT_PRICING;
20
+ const M = 1000000;
21
+ return ((usage.input_tokens * price.input) / M +
22
+ (usage.output_tokens * price.output) / M +
23
+ (usage.cache_read_input_tokens * price.cacheRead) / M +
24
+ (usage.cache_creation_input_tokens * price.cacheCreate) / M);
25
+ }
@@ -19,6 +19,7 @@ const fs_1 = __importDefault(require("fs"));
19
19
  const path_1 = __importDefault(require("path"));
20
20
  const os_1 = __importDefault(require("os"));
21
21
  const paths_1 = require("./paths");
22
+ const pricing_1 = require("./pricing");
22
23
  // ─── Decode ───────────────────────────────────────────────────────────────────
23
24
  /**
24
25
  * Decodifica el nombre de directorio de Claude Code al path real.
@@ -143,22 +144,6 @@ function parseHandoffProgress(content) {
143
144
  }
144
145
  // ─── JSONL stats (datos históricos sin daemon) ────────────────────────────────
145
146
  const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
146
- // Precios en USD por millón de tokens (misma tabla que enricher.ts)
147
- const PRICING = {
148
- 'claude-opus-4-6': { input: 15, output: 75, cacheRead: 1.50, cacheCreate: 18.75 },
149
- 'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.30, cacheCreate: 3.75 },
150
- 'claude-haiku-4-5': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
151
- 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheRead: 0.08, cacheCreate: 1.00 },
152
- };
153
- const DEFAULT_PRICING = PRICING['claude-sonnet-4-6'];
154
- function calcCost(model, usage) {
155
- const p = PRICING[model] ?? DEFAULT_PRICING;
156
- const M = 1000000;
157
- return ((usage.input_tokens * p.input) / M +
158
- (usage.output_tokens * p.output) / M +
159
- (usage.cache_read_input_tokens * p.cacheRead) / M +
160
- (usage.cache_creation_input_tokens * p.cacheCreate) / M);
161
- }
162
147
  /**
163
148
  * Lee todos los JSONL del directorio codificado de un proyecto y acumula
164
149
  * tokens y coste. No requiere que el daemon haya estado corriendo.
@@ -196,7 +181,7 @@ function getJSONLStats(encodedDir) {
196
181
  continue;
197
182
  hasAssistant = true;
198
183
  const tokens = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
199
- totalCost += calcCost(model, usage);
184
+ totalCost += (0, pricing_1.calcCost)(model, usage);
200
185
  totalTokens += tokens;
201
186
  if (model.includes('opus'))
202
187
  modelUsage.opusTokens += tokens;
@@ -314,7 +299,7 @@ function getJSONLStatsByProject(dirPath) {
314
299
  continue;
315
300
  hasAssistant = true;
316
301
  const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0);
317
- cost += calcCost(model, usage);
302
+ cost += (0, pricing_1.calcCost)(model, usage);
318
303
  tokens += t;
319
304
  if (model.includes('opus'))
320
305
  mu.opusTokens += t;
@@ -19,6 +19,8 @@ export interface QuotaData {
19
19
  cyclePrompts: number;
20
20
  cycleLimit: number;
21
21
  cyclePct: number;
22
+ cycleTokens: number;
23
+ cycleLimitTokens: number;
22
24
  cycleResetMs: number;
23
25
  cycleResetAt: number;
24
26
  cycleStartTs: number;
@@ -26,10 +26,10 @@ const path_1 = __importDefault(require("path"));
26
26
  const claude_auth_1 = require("./claude-auth");
27
27
  const paths_1 = require("./paths");
28
28
  const PLAN_LIMITS = {
29
- free: { prompts5h: 10, weeklyHoursSonnet: 40, weeklyHoursOpus: 0 },
30
- pro: { prompts5h: 45, weeklyHoursSonnet: 80, weeklyHoursOpus: 0 },
31
- max5: { prompts5h: 225, weeklyHoursSonnet: 280, weeklyHoursOpus: 35 },
32
- max20: { prompts5h: 900, weeklyHoursSonnet: 480, weeklyHoursOpus: 40 },
29
+ free: { prompts5h: 10, tokens5h: 100000, weeklyHoursSonnet: 40, weeklyHoursOpus: 0 },
30
+ pro: { prompts5h: 45, tokens5h: 382000, weeklyHoursSonnet: 80, weeklyHoursOpus: 0 },
31
+ max5: { prompts5h: 225, tokens5h: 2000000, weeklyHoursSonnet: 280, weeklyHoursOpus: 35 },
32
+ max20: { prompts5h: 900, tokens5h: 8000000, weeklyHoursSonnet: 480, weeklyHoursOpus: 40 },
33
33
  };
34
34
  // ─── Helpers de ventanas temporales ──────────────────────────────────────────
35
35
  const CYCLE_MS = 5 * 60 * 60 * 1000; // 5 horas en ms
@@ -253,8 +253,10 @@ function computeQuota(forcePlan) {
253
253
  const fiveHAgo = now - CYCLE_MS;
254
254
  const cycleResetAt = computeResetAt(entries, now);
255
255
  const cycleStart = fiveHAgo; // inicio real de la ventana de conteo
256
- const cyclePrompts = entries.filter(e => e.type === 'human' && e.ts >= fiveHAgo).length;
257
- const cyclePct = Math.min(100, Math.round(cyclePrompts / limits.prompts5h * 100));
256
+ // Basado en tokens (como claude.ai/settings/usage)
257
+ const cycleEntries = entries.filter(e => e.ts >= fiveHAgo);
258
+ const cycleTokens = cycleEntries.reduce((sum, e) => sum + (e.inputTokens ?? 0) + (e.outputTokens ?? 0), 0);
259
+ const cyclePct = Math.min(100, Math.round(cycleTokens / limits.tokens5h * 100));
258
260
  const cycleResetMs = Math.max(0, cycleResetAt - now);
259
261
  // ─ Semanal por modelo: ventanas de 5 min con actividad ─
260
262
  // Contamos ventanas de 5 min distintas con al menos 1 respuesta por modelo
@@ -292,9 +294,11 @@ function computeQuota(forcePlan) {
292
294
  ? Math.round(totalRecentTok / 30)
293
295
  : 0;
294
296
  const data = {
295
- cyclePrompts,
297
+ cyclePrompts: cycleEntries.filter(e => e.type === 'human').length,
296
298
  cycleLimit: limits.prompts5h,
297
299
  cyclePct,
300
+ cycleTokens,
301
+ cycleLimitTokens: limits.tokens5h,
298
302
  cycleResetMs,
299
303
  cycleResetAt,
300
304
  cycleStartTs: cycleStart,
package/dist/render.d.ts CHANGED
@@ -51,5 +51,6 @@ export interface RenderState {
51
51
  events: TraceEvent[];
52
52
  cost?: CostInfo;
53
53
  weekly?: WeeklyStats;
54
+ cyclePct?: number;
54
55
  }
55
56
  export declare function renderTrace(state: RenderState): string;
package/dist/render.js CHANGED
@@ -136,10 +136,10 @@ function renderTrace(state) {
136
136
  const remaining = 100 - pct;
137
137
  lines.push(` ${C.dim}auto-compact en:${C.reset} ${bar} ` +
138
138
  `${barColor}${remaining}% restante${C.reset} ` +
139
- `${C.dim}${fmtTok(cost.context_used)} / ${fmtTok(cost.context_window)} tokens usados${C.reset}`);
139
+ `${C.dim}${fmtTok(cost.context_used)} / ${fmtTok(cost.context_window)} tokens used${C.reset}`);
140
140
  }
141
141
  else {
142
- lines.push(` ${C.dim}contexto: calculando...${C.reset}`);
142
+ lines.push(` ${C.dim}context: calculating...${C.reset}`);
143
143
  }
144
144
  lines.push(C.dim + '─'.repeat(72) + C.reset);
145
145
  // ── Bloques de respuesta ──────────────────────────────────────────────────
@@ -199,20 +199,25 @@ function renderTrace(state) {
199
199
  const score = (cost.efficiency_score === 0 && cost.cost_usd < 0.001) ? 100 : cost.efficiency_score;
200
200
  const scoreColor = score >= 90 ? C.green : score >= 70 ? C.yellow : C.red;
201
201
  const scoreBar = progressBar(score, 14, scoreColor);
202
+ // Barra de current cycle (5h quota)
203
+ const cyclePct = state.cyclePct ?? 0;
204
+ const pctColor = cyclePct >= 90 ? C.red : cyclePct >= 70 ? C.yellow : C.green;
205
+ const pctBar = progressBar(cyclePct, 7, pctColor);
202
206
  // Tokens
203
207
  const tokenLine = `${C.dim}↑${C.reset}${fmtTok(cost.input_tokens)} ` +
204
208
  `${C.dim}↓${C.reset}${fmtTok(cost.output_tokens)} ` +
205
209
  `${C.dim}🗄${C.reset}${fmtTok(cost.cache_read)}`;
206
210
  lines.push(` ${C.bold}💰 $${cost.cost_usd.toFixed(4)}${C.reset} ` +
207
211
  `${tokenLine} ` +
208
- `eficiencia: ${scoreBar} ${scoreColor}${score}/100${C.reset}`);
212
+ `current: ${pctBar} ${pctColor}${cyclePct}% | efficiency: ${scoreBar} ${scoreColor}${score}/100${C.reset}`);
209
213
  }
210
214
  else {
211
215
  const totalDone = events.filter(e => e.type === 'Done').length;
212
216
  const elapsed = fmtMs((events.at(-1)?.ts ?? startedAt) - startedAt);
213
- lines.push(` ${C.dim}⏱ ${elapsed} ✅ ${totalDone} tools 💰 calculando...${C.reset}`);
217
+ const cyclePct = state.cyclePct ?? 0;
218
+ lines.push(` ${C.dim}⏱ ${elapsed} ✅ ${totalDone} tools 💰 calculating... current: ${cyclePct}%${C.reset}`);
214
219
  }
215
- // ── Barra semanal (stats-cache.json) ──────────────────────────────────────
220
+ // ── Weekly bar (stats-cache.json) ──────────────────────────────────────
216
221
  if (state.weekly && state.weekly.totalTokens > 0) {
217
222
  const { totalTokens, byDay, lastUpdated } = state.weekly;
218
223
  // Mini sparkline: un char por día de la semana
@@ -220,9 +225,9 @@ function renderTrace(state) {
220
225
  const BARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
221
226
  const spark = byDay.map(d => BARS[Math.min(7, Math.floor(d.tokens / maxDay * 7))]).join('');
222
227
  const padded = spark.padStart(7, '▁'); // garantizar 7 chars (1 por día)
223
- lines.push(` ${C.dim}semanal:${C.reset} ${C.cyan}${padded}${C.reset} ` +
228
+ lines.push(` ${C.dim}weekly:${C.reset} ${C.cyan}${padded}${C.reset} ` +
224
229
  `${C.bold}${fmtTok(totalTokens)} tokens${C.reset} ` +
225
- `${C.dim}(últimos 7 días${lastUpdated ? ' · datos al ' + lastUpdated : ''})${C.reset}`);
230
+ `${C.dim}(last 7 days${lastUpdated ? ' · data from ' + lastUpdated : ''})${C.reset}`);
226
231
  }
227
232
  lines.push('');
228
233
  return lines.join('\n');
@@ -0,0 +1,4 @@
1
+ export declare function runRoast(opts: {
2
+ stats: boolean;
3
+ months: number;
4
+ }): Promise<void>;
package/dist/roast.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRoast = runRoast;
4
+ const db_js_1 = require("./db.js");
5
+ function formatMinutes(totalMinutes) {
6
+ if (totalMinutes < 60)
7
+ return `${Math.round(totalMinutes)} minutes`;
8
+ const hours = Math.floor(totalMinutes / 60);
9
+ if (hours < 24)
10
+ return `${hours} hours`;
11
+ const days = Math.floor(hours / 24);
12
+ return `${days} days`;
13
+ }
14
+ function getRoastRating(avgEfficiency) {
15
+ if (avgEfficiency >= 90)
16
+ return "You're a machine. Or maybe you're just not using Claude enough. 😏";
17
+ if (avgEfficiency >= 70)
18
+ return "Solid. Not great, not terrible. The AI equivalent of a C+ student.";
19
+ if (avgEfficiency >= 50)
20
+ return "Room for growth, champ. 📈";
21
+ return "Oof. That's a lot of money down the drain. Are you okay? 💀";
22
+ }
23
+ function getRoastMessage(data) {
24
+ const lines = [];
25
+ lines.push('🔥 Your Claude Code Roast');
26
+ lines.push('');
27
+ if (data.totalBashCalls > 0) {
28
+ const minutesPerCall = (data.days * 24 * 60) / data.totalBashCalls;
29
+ if (minutesPerCall < 60) {
30
+ lines.push(` You called Bash ${data.totalBashCalls} times in ${data.days} days.`);
31
+ lines.push(` That's once every ${minutesPerCall.toFixed(1)} minutes.`);
32
+ lines.push(' Are you okay?');
33
+ lines.push('');
34
+ }
35
+ }
36
+ if (data.contextHits > 0) {
37
+ lines.push(` You hit 90%+ context in ${data.contextHits} sessions.`);
38
+ lines.push(' Claude was writing with amnesia half the time.');
39
+ lines.push('');
40
+ }
41
+ if (data.totalLoops > 0) {
42
+ const loopCost = data.totalCost * 0.15;
43
+ lines.push(` You spent $${loopCost.toFixed(2)} on loops you never noticed.`);
44
+ const coffees = Math.floor(loopCost / 0.3);
45
+ if (coffees > 0) {
46
+ lines.push(` That's ${coffees} coffees. Just saying.`);
47
+ lines.push('');
48
+ }
49
+ }
50
+ lines.push(` Efficiency score: ${Math.round(data.avgEfficiency)}/100`);
51
+ lines.push(` ${getRoastRating(data.avgEfficiency)}`);
52
+ return lines.join('\n');
53
+ }
54
+ async function runRoast(opts) {
55
+ const days = opts.months ?? 30;
56
+ const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
57
+ const sessions = db_js_1.dbOps.getAllSessions(500).filter(s => s.started_at >= sinceMs);
58
+ let totalBashCalls = 0;
59
+ let totalLoops = 0;
60
+ let contextHits = 0;
61
+ for (const session of sessions) {
62
+ totalLoops += session.loops_detected || 0;
63
+ if ((session.total_input_tokens || 0) + (session.total_output_tokens || 0) > 150000) {
64
+ contextHits++;
65
+ }
66
+ const events = db_js_1.dbOps.getSessionEvents(session.id);
67
+ const bashCalls = events.filter(e => e.type === 'Done' && e.tool_name === 'Bash').length;
68
+ totalBashCalls += bashCalls;
69
+ }
70
+ const totalCost = sessions.reduce((a, s) => a + (s.total_cost_usd || 0), 0);
71
+ const avgEfficiency = sessions.length > 0
72
+ ? sessions.reduce((a, s) => a + (s.efficiency_score || 0), 0) / sessions.length
73
+ : 100;
74
+ const data = {
75
+ totalCost,
76
+ totalSessions: sessions.length,
77
+ totalBashCalls,
78
+ totalLoops,
79
+ avgEfficiency,
80
+ contextHits,
81
+ days,
82
+ };
83
+ if (opts.stats) {
84
+ console.log(`=== Claude Code Stats (${days} days) ===`);
85
+ console.log(`Sessions: ${sessions.length}`);
86
+ console.log(`Total cost: $${totalCost.toFixed(2)}`);
87
+ console.log(`Bash calls: ${totalBashCalls}`);
88
+ console.log(`Loops: ${totalLoops}`);
89
+ console.log(`Effficiency: ${Math.round(avgEfficiency)}/100`);
90
+ return;
91
+ }
92
+ console.log(getRoastMessage(data));
93
+ console.log('');
94
+ console.log(' github.com/DeibyGS/claudestat');
95
+ }
@@ -93,7 +93,7 @@ exports.eventsRouter.post('/event', (req, res) => {
93
93
  // Activar skill parent para los eventos siguientes si este fue un Skill Done
94
94
  if (tool_name === 'Skill') {
95
95
  try {
96
- const inp = typeof tool_input === 'object' ? tool_input : JSON.parse(tool_input ?? '{}');
96
+ const inp = typeof tool_input === 'object' ? tool_input : (typeof tool_input === 'string' ? JSON.parse(tool_input) : {});
97
97
  activeSkillBySession.set(session_id, inp?.skill || inp?.name || 'skill');
98
98
  }
99
99
  catch {
@@ -123,9 +123,9 @@ exports.eventsRouter.post('/event', (req, res) => {
123
123
  const FILE_TOOLS = new Set(['Read', 'Write', 'Edit', 'Glob', 'Grep']);
124
124
  if (FILE_TOOLS.has(tool_name || '') && tool_input) {
125
125
  try {
126
- const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : tool_input;
127
- const filePath = (inp.file_path || inp.path);
128
- if (filePath?.startsWith('/')) {
126
+ const inp = typeof tool_input === 'string' ? JSON.parse(tool_input) : (tool_input ?? {});
127
+ const filePath = inp?.file_path ?? inp?.path;
128
+ if (typeof filePath === 'string' && filePath.startsWith('/')) {
129
129
  const projectCwd = findProjectCwdForFile(filePath);
130
130
  if (projectCwd)
131
131
  db_1.dbOps.updateSessionProject(session_id, projectCwd);
@@ -0,0 +1,5 @@
1
+ export declare function runShare(args: {
2
+ sessionId?: string;
3
+ format: 'ascii' | 'json';
4
+ copy: boolean;
5
+ }): Promise<void>;
package/dist/share.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runShare = runShare;
4
+ const db_js_1 = require("./db.js");
5
+ const child_process_1 = require("child_process");
6
+ function formatDuration(ms) {
7
+ const seconds = Math.floor(ms / 1000);
8
+ const minutes = Math.floor(seconds / 60);
9
+ const hours = Math.floor(minutes / 60);
10
+ if (hours > 0) {
11
+ const mins = minutes % 60;
12
+ return `${hours}h ${mins}m`;
13
+ }
14
+ return `${minutes}m`;
15
+ }
16
+ async function getSessionData(sessionId) {
17
+ const sessions = db_js_1.dbOps.getAllSessions(1);
18
+ let session;
19
+ if (sessionId) {
20
+ session = db_js_1.dbOps.getSession(sessionId);
21
+ if (!session)
22
+ return null;
23
+ }
24
+ else if (sessions.length > 0) {
25
+ session = sessions[0];
26
+ }
27
+ else {
28
+ return null;
29
+ }
30
+ const events = db_js_1.dbOps.getSessionEvents(session.id);
31
+ const toolCalls = events.filter((e) => e.type === 'Done' && e.tool_name);
32
+ const totalTokens = (session.total_input_tokens || 0) + (session.total_output_tokens || 0);
33
+ const cacheTokens = (session.total_cache_read || 0) + (session.total_cache_creation || 0);
34
+ const cacheSavedCost = (session.total_cost_usd || 0) * 0.1 * 0.9;
35
+ const toolCounts = {};
36
+ toolCalls.forEach((e) => {
37
+ const tool = e.tool_name || 'Unknown';
38
+ toolCounts[tool] = (toolCounts[tool] || 0) + 1;
39
+ });
40
+ let topTool = '—';
41
+ let topToolPct = 0;
42
+ if (Object.keys(toolCounts).length > 0) {
43
+ const sorted = Object.entries(toolCounts).sort((a, b) => b[1] - a[1]);
44
+ topTool = sorted[0][0];
45
+ topToolPct = Math.round((sorted[0][1] / toolCalls.length) * 100);
46
+ }
47
+ const durationMs = (session.last_event_at || session.started_at) - session.started_at;
48
+ const project = session.project_path?.split('/').pop() || 'unknown';
49
+ return {
50
+ id: session.id,
51
+ project: project.length > 18 ? project.slice(0, 15) + '...' : project,
52
+ duration: formatDuration(durationMs),
53
+ tools: toolCalls.length,
54
+ cost: `$${(session.total_cost_usd || 0).toFixed(2)}`,
55
+ cacheSaved: `$${cacheSavedCost.toFixed(2)}`,
56
+ cachePct: Math.min(100, totalTokens > 0 ? Math.round((cacheTokens / totalTokens) * 100) : 0),
57
+ topTool: topTool.length > 12 ? topTool.slice(0, 9) + '...' : topTool,
58
+ topToolPct,
59
+ efficiency: session.efficiency_score || 100,
60
+ efficiencyEmoji: (session.efficiency_score || 100) >= 90 ? '🔥' : (session.efficiency_score || 100) >= 70 ? '👍' : '💀',
61
+ };
62
+ }
63
+ function generateAsciiCard(data) {
64
+ const FIELD_WIDTH = 25;
65
+ const toolsLabel = `${data.tools} calls`;
66
+ const cacheLabel = `${data.cachePct}% saved (${data.cacheSaved})`;
67
+ const topLabel = `${data.topTool} (${data.topToolPct}%)`;
68
+ const effLabel = `${data.efficiency} / 100 ${data.efficiencyEmoji}`;
69
+ const lines = [
70
+ '╔═══════════════════════════════════╗',
71
+ '║ Session Report · claudestat ║',
72
+ '╠═══════════════════════════════════╣',
73
+ `║ Project ${data.project.padEnd(FIELD_WIDTH)}║`,
74
+ `║ Duration ${data.duration.padEnd(FIELD_WIDTH)}║`,
75
+ `║ Tools ${toolsLabel.padEnd(FIELD_WIDTH)}║`,
76
+ `║ Cost ${data.cost.padEnd(FIELD_WIDTH)}║`,
77
+ `║ Cache hit ${cacheLabel.padEnd(FIELD_WIDTH)}║`,
78
+ `║ Top tool ${topLabel.padEnd(FIELD_WIDTH)}║`,
79
+ `║ Efficiency ${effLabel.padEnd(FIELD_WIDTH)}║`,
80
+ '╚═══════════════════════════════════╝',
81
+ ' github.com/DeibyGS/claudestat',
82
+ ];
83
+ return lines.join('\n');
84
+ }
85
+ function generateJson(data) {
86
+ return JSON.stringify({
87
+ project: data.project,
88
+ duration: data.duration,
89
+ tools: data.tools,
90
+ cost: data.cost,
91
+ cache_saved: data.cacheSaved,
92
+ cache_pct: data.cachePct,
93
+ top_tool: data.topTool,
94
+ top_tool_pct: data.topToolPct,
95
+ efficiency: data.efficiency,
96
+ }, null, 2);
97
+ }
98
+ async function runShare(args) {
99
+ const data = await getSessionData(args.sessionId);
100
+ if (!data) {
101
+ console.error('Error: No sessions found. Run Claude Code first.');
102
+ process.exit(1);
103
+ }
104
+ const output = args.format === 'json' ? generateJson(data) : generateAsciiCard(data);
105
+ console.log(output);
106
+ if (args.copy) {
107
+ try {
108
+ const p = (0, child_process_1.spawn)('pbcopy', [], { stdio: ['pipe', process.stderr, process.stderr] });
109
+ p.stdin.write(output);
110
+ p.stdin.end();
111
+ p.on('close', () => console.log('\n✓ Copied to clipboard'));
112
+ }
113
+ catch {
114
+ console.warn('⚠ Clipboard not available (macOS only)');
115
+ }
116
+ }
117
+ }
package/dist/watch.js CHANGED
@@ -63,8 +63,23 @@ async function startWatch() {
63
63
  }
64
64
  let state = {
65
65
  sessionId: '', cwd: '', startedAt: Date.now(), events: [],
66
- weekly: (0, weekly_1.readWeeklyStats)()
66
+ weekly: (0, weekly_1.readWeeklyStats)(),
67
+ cyclePct: 0
67
68
  };
69
+ // Fetch quota para cyclePct cada 30 segundos
70
+ async function fetchQuota() {
71
+ try {
72
+ const res = await fetch('http://localhost:7337/quota');
73
+ if (res.ok) {
74
+ const q = await res.json();
75
+ return q.cyclePct;
76
+ }
77
+ }
78
+ catch { }
79
+ return 0;
80
+ }
81
+ fetchQuota().then(pct => { state.cyclePct = pct; });
82
+ setInterval(async () => { state.cyclePct = await fetchQuota(); }, 30000);
68
83
  // Refrescar stats semanales cada 5 minutos
69
84
  setInterval(() => { state.weekly = (0, weekly_1.readWeeklyStats)(); }, 5 * 60 * 1000);
70
85
  function draw() {
@@ -80,16 +95,19 @@ async function startWatch() {
80
95
  sessionId: msg.session.id,
81
96
  cwd: msg.session.cwd || '',
82
97
  startedAt: msg.session.started_at,
83
- events: (msg.events || []),
84
- cost: buildCostFromSession(msg.session)
98
+ events: Array.isArray(msg.events) ? msg.events : [],
99
+ cost: buildCostFromSession(msg.session),
100
+ cyclePct: state.cyclePct // preservar de inicial o anterior
85
101
  };
86
102
  }
87
103
  }
88
104
  else if (msg.type === 'event') {
105
+ if (!msg.payload?.session_id)
106
+ return;
89
107
  const evt = msg.payload;
90
108
  // Nueva sesión → resetear estado
91
109
  if (evt.session_id && evt.session_id !== state.sessionId && state.sessionId !== '') {
92
- state = { sessionId: evt.session_id, cwd: evt.cwd || '', startedAt: evt.ts, events: [] };
110
+ state = { sessionId: evt.session_id, cwd: evt.cwd || '', startedAt: evt.ts, events: [], cyclePct: state.cyclePct };
93
111
  }
94
112
  else if (!state.sessionId && evt.session_id) {
95
113
  state.sessionId = evt.session_id;
@@ -98,7 +116,7 @@ async function startWatch() {
98
116
  }
99
117
  if (evt.type === 'Done' && evt.tool_name) {
100
118
  // Actualizar el PreToolUse pendiente a Done
101
- const pending = [...state.events].reverse()
119
+ const pending = [...(state.events ?? [])].reverse()
102
120
  .find(e => e.type === 'PreToolUse' && e.tool_name === evt.tool_name);
103
121
  if (pending) {
104
122
  pending.type = 'Done';
@@ -131,8 +149,12 @@ async function startWatch() {
131
149
  }
132
150
  clearScreen();
133
151
  process.stdout.write('\x1b[36m● claudestat watch\x1b[0m — connecting...\n');
152
+ // Fetch quota antes del primer render para evitar 0%
153
+ const initialCyclePct = await fetchQuota();
134
154
  while (true) {
135
155
  try {
156
+ // Inicializar state con cyclePct obtenido
157
+ state.cyclePct = initialCyclePct;
136
158
  await connectSSE(handleMessage);
137
159
  }
138
160
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statforge/claudestat",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
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",
@@ -58,7 +58,7 @@
58
58
  "dev": "tsx src/index.ts",
59
59
  "dev:full": "tsx src/index.ts start & sleep 1 && cd dashboard && npm run dev",
60
60
  "start": "node dist/index.js",
61
- "test": "node --require tsx/cjs tests/index.ts"
61
+ "test": "bash run-tests.sh"
62
62
  },
63
63
  "dependencies": {
64
64
  "@anthropic-ai/sdk": "^0.88.0",