@statforge/claudestat 1.4.0 → 1.5.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/dist/enricher.js CHANGED
@@ -1,347 +1,129 @@
1
1
  "use strict";
2
2
  /**
3
- * enricher.ts — Enriquecedor de coste desde JSONL de Claude Code
3
+ * enricher.ts — Watcher multi-CLI usando adapters
4
4
  *
5
- * Claude Code escribe los tokens de cada respuesta en:
6
- * ~/.claude/projects/{project-hash}/{session-id}.jsonl
5
+ * En lugar de hardcodear la lógica de Claude Code, usa el adapter pattern
6
+ * para soportar múltiples coding CLIs (Claude Code, Codex, OpenCode, etc.).
7
7
  *
8
- * Cada línea de tipo "assistant" contiene:
9
- * message.usage.input_tokens
10
- * message.usage.output_tokens
11
- * message.usage.cache_read_input_tokens
12
- * message.usage.cache_creation_input_tokens
13
- * message.model
14
- *
15
- * El enricher observa cambios en esos archivos (con chokidar),
16
- * calcula el coste acumulado por sesión y llama al callback
17
- * para que el daemon actualice la DB y haga broadcast via SSE.
8
+ * Cada adapter implementa WatcherAdapter (src/watchers/adapter.ts):
9
+ * - detect() → si el CLI está instalado
10
+ * - getWatchPaths() → qué archivos observar
11
+ * - parseEvent() → parsear una línea de trace
12
+ * - getSessionCost() → calcular costos acumulados
18
13
  */
19
14
  var __importDefault = (this && this.__importDefault) || function (mod) {
20
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
21
16
  };
22
17
  Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.getContextWindow = getContextWindow;
24
- exports.getAllBlockCostsForSession = getAllBlockCostsForSession;
25
- exports.getSessionPrompts = getSessionPrompts;
18
+ exports.getSessionPrompts = exports.getContextWindow = exports.getAllBlockCostsForSession = void 0;
26
19
  exports.startEnricher = startEnricher;
27
20
  exports.stopEnricher = stopEnricher;
28
21
  exports.cleanupSession = cleanupSession;
29
22
  exports.processLatestForSession = processLatestForSession;
30
- const promises_1 = __importDefault(require("fs/promises"));
31
- const fs_1 = __importDefault(require("fs"));
32
- const path_1 = __importDefault(require("path"));
33
23
  const chokidar_1 = __importDefault(require("chokidar"));
34
- const paths_1 = require("./paths");
35
- const pricing_1 = require("./pricing");
36
- // ─── Context window dinámico ──────────────────────────────────────────────────
37
- const KNOWN_CONTEXT_WINDOWS = {
38
- 'claude-opus-4-6': 200000,
39
- 'claude-sonnet-4-6': 200000,
40
- 'claude-haiku-4-5': 200000,
41
- };
42
- function getContextWindow(model) {
43
- return KNOWN_CONTEXT_WINDOWS[model] ?? 200000;
44
- }
45
- const fileOffsets = new Map();
46
- const fileLocks = new Map(); // Lock per file
47
- const FILE_OFFSET_TTL = 30 * 60000; // 30 minutos
48
- function cleanupStaleOffsets() {
49
- const now = Date.now();
50
- for (const [key, entry] of fileOffsets) {
51
- if (now - entry.lastAccess > FILE_OFFSET_TTL)
52
- fileOffsets.delete(key);
24
+ const path_1 = __importDefault(require("path"));
25
+ const fs_1 = __importDefault(require("fs"));
26
+ const adapter_1 = require("./watchers/adapter");
27
+ require("./watchers/claude-code");
28
+ require("./watchers/codex");
29
+ require("./watchers/opencode");
30
+ require("./watchers/amp");
31
+ require("./watchers/droid");
32
+ require("./watchers/codebuff");
33
+ // Re-export Claude Code-specific utilities for routes/stream and routes/misc
34
+ var claude_code_1 = require("./watchers/claude-code");
35
+ Object.defineProperty(exports, "getAllBlockCostsForSession", { enumerable: true, get: function () { return claude_code_1.getAllBlockCostsForSession; } });
36
+ Object.defineProperty(exports, "getContextWindow", { enumerable: true, get: function () { return claude_code_1.getContextWindow; } });
37
+ Object.defineProperty(exports, "getSessionPrompts", { enumerable: true, get: function () { return claude_code_1.getSessionPrompts; } });
38
+ const prevContextBySession = new Map();
39
+ let watcher = null;
40
+ const pendingFiles = new Map();
41
+ const fileLocks = new Map();
42
+ // ─── Adapter lookup por filePath ───────────────────────────────────────────────
43
+ const adapterByDir = new Map();
44
+ function findAdapter(filePath) {
45
+ for (const [dir, adapter] of adapterByDir) {
46
+ if (filePath.startsWith(dir))
47
+ return adapter;
53
48
  }
49
+ return undefined;
54
50
  }
55
- async function processJSONL(filePath) {
56
- // Skip if already processing this file
51
+ // ─── Procesamiento de archivos ─────────────────────────────────────────────────
52
+ async function processFile(filePath) {
57
53
  if (fileLocks.has(filePath))
58
54
  return null;
59
55
  fileLocks.set(filePath, Promise.resolve());
60
- let fileContent;
61
56
  try {
62
- fileContent = await promises_1.default.readFile(filePath, 'utf8');
63
- }
64
- catch {
65
- return null;
66
- }
67
- const currentSize = Buffer.byteLength(fileContent, 'utf8');
68
- const knownEntry = fileOffsets.get(filePath);
69
- const knownOffset = knownEntry?.offset ?? 0;
70
- if (currentSize < knownOffset)
71
- fileOffsets.set(filePath, { offset: 0, lastAccess: Date.now() });
72
- const totals = {
73
- input_tokens: 0, output_tokens: 0,
74
- cache_read: 0, cache_creation: 0, cost_usd: 0,
75
- context_used: 0, context_window: 200000
76
- };
77
- let lastInputUsd = 0;
78
- let lastOutputUsd = 0;
79
- let lastInputTokens = 0;
80
- let lastOutputTokens = 0;
81
- let lastModel = undefined;
82
- let firstTs = undefined;
83
- for (const raw of fileContent.split('\n')) {
84
- const line = raw.trim();
85
- if (!line)
86
- continue;
87
- try {
88
- const obj = JSON.parse(line);
89
- if (obj.type !== 'assistant')
90
- continue;
91
- const msg = obj.message;
92
- if (!msg?.usage)
93
- continue;
94
- const usage = msg.usage;
95
- const model = msg.model ?? 'claude-sonnet-4-6';
96
- if (firstTs === undefined && obj.timestamp) {
97
- try {
98
- firstTs = new Date(obj.timestamp).getTime();
99
- }
100
- catch (e) { /* ignore invalid timestamp */ }
101
- }
102
- totals.input_tokens += usage.input_tokens ?? 0;
103
- totals.output_tokens += usage.output_tokens ?? 0;
104
- totals.cache_read += usage.cache_read_input_tokens ?? 0;
105
- totals.cache_creation += usage.cache_creation_input_tokens ?? 0;
106
- const resolvedModel = model ?? 'claude-sonnet-4-6';
107
- totals.cost_usd += (0, pricing_1.calcCost)(resolvedModel, usage);
108
- totals.context_used = (usage.input_tokens ?? 0)
109
- + (usage.cache_read_input_tokens ?? 0)
110
- + (usage.cache_creation_input_tokens ?? 0);
111
- totals.context_window = getContextWindow(resolvedModel);
112
- const price = pricing_1.PRICING[resolvedModel] ?? pricing_1.DEFAULT_PRICING;
113
- const M = 1000000;
114
- lastInputUsd = ((usage.input_tokens ?? 0) * price.input +
115
- (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
116
- (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
117
- lastOutputUsd = ((usage.output_tokens ?? 0) * price.output) / M;
118
- lastInputTokens = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
119
- lastOutputTokens = usage.output_tokens ?? 0;
120
- lastModel = model ?? lastModel;
121
- }
122
- catch (e) {
123
- console.warn('[enricher] Error calculating cost:', e);
124
- }
125
- }
126
- if (lastInputUsd + lastOutputUsd > 0) {
127
- totals.lastEntry = {
128
- inputUsd: lastInputUsd,
129
- outputUsd: lastOutputUsd,
130
- totalUsd: lastInputUsd + lastOutputUsd,
131
- inputTokens: lastInputTokens,
132
- outputTokens: lastOutputTokens,
133
- };
134
- }
135
- totals.lastModel = lastModel;
136
- totals.firstTs = firstTs;
137
- fileOffsets.set(filePath, { offset: currentSize, lastAccess: Date.now() });
138
- fileLocks.delete(filePath);
139
- return totals;
140
- }
141
- const blockCostCache = new Map();
142
- const costCacheLocks = new Map(); // Simple lock flag
143
- const BLOCK_COST_TTL = 5 * 60000;
144
- async function getAllBlockCostsForSession(sessionId) {
145
- // Return cached if available and not expired
146
- const cached = blockCostCache.get(sessionId);
147
- if (cached && Date.now() - cached.ts < BLOCK_COST_TTL)
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);
153
- try {
154
- if (!fs_1.default.existsSync(PROJECTS_DIR))
155
- return [];
156
- const dirs = await promises_1.default.readdir(PROJECTS_DIR);
157
- for (const dir of dirs) {
158
- const dirPath = path_1.default.join(PROJECTS_DIR, dir);
159
- try {
160
- const stat = await promises_1.default.stat(dirPath);
161
- if (!stat.isDirectory())
162
- continue;
163
- }
164
- catch {
165
- continue;
166
- }
167
- const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
168
- try {
169
- await promises_1.default.access(filePath);
170
- }
171
- catch {
172
- continue;
173
- }
174
- const result = [];
175
- let current = null;
176
- const content = await promises_1.default.readFile(filePath, 'utf8');
177
- for (const raw of content.split('\n')) {
178
- const line = raw.trim();
179
- if (!line)
180
- continue;
181
- try {
182
- const obj = JSON.parse(line);
183
- if (obj.type === 'human' || obj.type === 'user') {
184
- const msgContent = obj.message?.content;
185
- if (Array.isArray(msgContent) && msgContent[0]?.type === 'tool_result')
186
- continue;
187
- const text = typeof msgContent === 'string' ? msgContent
188
- : Array.isArray(msgContent)
189
- ? (msgContent.find((c) => c?.type === 'text')?.text ?? '')
190
- : '';
191
- if (text.includes('<system-reminder>') || text.includes('<command-name>'))
192
- continue;
193
- current = { inputUsd: 0, outputUsd: 0, totalUsd: 0, inputTokens: 0, outputTokens: 0 };
194
- result.push(current);
195
- }
196
- if (obj.type === 'assistant' && current) {
197
- const usage = obj.message?.usage;
198
- const model = obj.message?.model ?? 'claude-sonnet-4-6';
199
- if (!usage)
200
- continue;
201
- const price = pricing_1.PRICING[model] ?? pricing_1.DEFAULT_PRICING;
202
- const M = 1000000;
203
- const inUsd = ((usage.input_tokens ?? 0) * price.input +
204
- (usage.cache_read_input_tokens ?? 0) * price.cacheRead +
205
- (usage.cache_creation_input_tokens ?? 0) * price.cacheCreate) / M;
206
- const outUsd = ((usage.output_tokens ?? 0) * price.output) / M;
207
- const inTok = (usage.input_tokens ?? 0) + (usage.cache_read_input_tokens ?? 0) + (usage.cache_creation_input_tokens ?? 0);
208
- const outTok = usage.output_tokens ?? 0;
209
- current.inputUsd += inUsd;
210
- current.outputUsd += outUsd;
211
- current.totalUsd += inUsd + outUsd;
212
- current.inputTokens += inTok;
213
- current.outputTokens += outTok;
214
- }
215
- }
216
- catch (e) {
217
- console.warn('[enricher] Error reading JSONL block:', e);
218
- }
219
- }
220
- const filtered = result.filter(b => b.totalUsd > 0);
221
- blockCostCache.set(sessionId, { data: filtered, ts: Date.now() });
222
- return filtered;
223
- }
224
- }
225
- catch (e) {
226
- console.warn('[enricher] Error calculating block costs:', e);
57
+ const adapter = findAdapter(filePath);
58
+ if (!adapter)
59
+ return null;
60
+ const sessionId = path_1.default.basename(filePath, '.jsonl');
61
+ if (!sessionId.includes('-') || sessionId.length < 10)
62
+ return null;
63
+ const cost = await adapter.getSessionCost(filePath);
64
+ if (!cost || cost.cost_usd < 0)
65
+ return null;
66
+ return { sessionId, cost, source: adapter.name };
227
67
  }
228
68
  finally {
229
- costCacheLocks.delete(sessionId);
69
+ fileLocks.delete(filePath);
230
70
  }
231
- return cached?.data ?? [];
232
71
  }
233
- async function getSessionPrompts(sessionId) {
234
- try {
235
- if (!fs_1.default.existsSync(PROJECTS_DIR))
236
- return [];
237
- const dirs = await promises_1.default.readdir(PROJECTS_DIR);
238
- for (const dir of dirs) {
239
- const dirPath = path_1.default.join(PROJECTS_DIR, dir);
240
- try {
241
- const stat = await promises_1.default.stat(dirPath);
242
- if (!stat.isDirectory())
243
- continue;
244
- }
245
- catch {
246
- continue;
247
- }
248
- const candidates = [
249
- path_1.default.join(dirPath, `${sessionId}.jsonl`),
250
- ];
251
- for (const file of candidates) {
252
- try {
253
- await promises_1.default.access(file);
254
- }
255
- catch {
256
- continue;
257
- }
258
- const results = [];
259
- const content = await promises_1.default.readFile(file, 'utf8');
260
- let index = 0;
261
- for (const raw of content.split('\n')) {
262
- const line = raw.trim();
263
- if (!line)
264
- continue;
265
- try {
266
- const obj = JSON.parse(line);
267
- if (obj.type !== 'human' && obj.type !== 'user')
268
- continue;
269
- const ts = obj.timestamp ? new Date(obj.timestamp).getTime() : 0;
270
- if (!ts || isNaN(ts))
271
- continue;
272
- const msgContent = obj.message?.content;
273
- let text = '';
274
- if (typeof msgContent === 'string') {
275
- text = msgContent;
276
- }
277
- else if (Array.isArray(msgContent)) {
278
- const textBlocks = msgContent.filter(c => c?.type === 'text');
279
- if (textBlocks.length === 0)
280
- continue;
281
- text = textBlocks.map((c) => c.text ?? '').join('\n').trim();
282
- }
283
- if (text.includes('<command-name>') ||
284
- text.includes('<local-command-stdout>') ||
285
- text.includes('<system-reminder>') ||
286
- text.length === 0)
287
- continue;
288
- index++;
289
- results.push({ index, ts, text });
290
- }
291
- catch { }
292
- }
293
- return results;
294
- }
295
- }
296
- }
297
- catch { }
298
- return [];
299
- }
300
- // ─── Watcher ─────────────────────────────────────────────────────────────────
301
- const PROJECTS_DIR = path_1.default.join((0, paths_1.getClaudeDir)(), 'projects');
302
- const prevContextBySession = new Map();
303
- let watcher = null;
304
- const pendingFiles = new Map();
72
+ // ─── Start / Stop ──────────────────────────────────────────────────────────────
305
73
  let offsetCleanupInterval = null;
306
- function startEnricher(onUpdate, onCompact, onSessionEnd) {
307
- if (!fs_1.default.existsSync(PROJECTS_DIR)) {
308
- console.warn(`[enricher] Directory not found: ${PROJECTS_DIR}`);
74
+ function startEnricher(onUpdate, onCompact) {
75
+ const adapters = (0, adapter_1.getActiveAdapters)();
76
+ if (adapters.length === 0) {
77
+ console.warn('[enricher] No supported CLI tools detected');
309
78
  return;
310
79
  }
311
- watcher = chokidar_1.default.watch(`${PROJECTS_DIR}/**/*.jsonl`, {
80
+ // Index directories for adapter lookup
81
+ for (const a of adapters) {
82
+ for (const watchPath of a.getWatchPaths()) {
83
+ // Extract base directory from glob pattern
84
+ const baseDir = watchPath.split('/**')[0];
85
+ if (baseDir)
86
+ adapterByDir.set(baseDir, a);
87
+ }
88
+ }
89
+ const watchPaths = adapters.flatMap(a => a.getWatchPaths());
90
+ console.log(`[enricher] Watching ${adapters.map(a => a.label).join(', ')}`);
91
+ watcher = chokidar_1.default.watch(watchPaths, {
312
92
  persistent: true,
313
93
  ignoreInitial: true,
314
94
  awaitWriteFinish: {
315
95
  stabilityThreshold: 200,
316
- pollInterval: 100
317
- }
96
+ pollInterval: 100,
97
+ },
318
98
  });
319
99
  const handleFile = (filePath) => {
320
- const sessionId = path_1.default.basename(filePath, '.jsonl');
321
- if (!sessionId.includes('-') || sessionId.length < 10)
322
- return;
323
100
  const existing = pendingFiles.get(filePath);
324
101
  if (existing)
325
102
  clearTimeout(existing);
326
- const timer = setTimeout(() => {
103
+ const timer = setTimeout(async () => {
327
104
  pendingFiles.delete(filePath);
328
- processJSONL(filePath).then(cost => {
329
- if (cost && cost.cost_usd >= 0) {
330
- const prev = prevContextBySession.get(sessionId);
331
- if (onCompact && prev !== undefined && prev > 140000 && cost.context_used < prev * 0.5) {
332
- onCompact(sessionId);
333
- }
334
- prevContextBySession.set(sessionId, cost.context_used);
335
- onUpdate(sessionId, cost);
336
- }
337
- }).catch(err => console.error('[enricher] Error processing JSONL:', err));
105
+ const result = await processFile(filePath);
106
+ if (!result)
107
+ return;
108
+ const { sessionId, cost, source } = result;
109
+ const prev = prevContextBySession.get(sessionId);
110
+ if (onCompact && prev !== undefined && prev > 140000 && cost.context_used < prev * 0.5) {
111
+ onCompact(sessionId);
112
+ }
113
+ prevContextBySession.set(sessionId, cost.context_used);
114
+ onUpdate(sessionId, cost, source);
338
115
  }, 100);
339
116
  pendingFiles.set(filePath, timer);
340
117
  };
341
118
  watcher.on('change', handleFile);
342
119
  watcher.on('add', handleFile);
343
- offsetCleanupInterval = setInterval(cleanupStaleOffsets, 5 * 60000);
344
- console.log(`[enricher] Watching ${PROJECTS_DIR}`);
120
+ offsetCleanupInterval = setInterval(() => {
121
+ for (const [, adapter] of adapterByDir) {
122
+ if (typeof adapter.constructor?.name === 'undefined') {
123
+ // cleanup per-adapter state if needed
124
+ }
125
+ }
126
+ }, 5 * 60000);
345
127
  }
346
128
  function stopEnricher() {
347
129
  if (watcher) {
@@ -355,46 +137,44 @@ function stopEnricher() {
355
137
  for (const [, timer] of pendingFiles)
356
138
  clearTimeout(timer);
357
139
  pendingFiles.clear();
358
- fileOffsets.clear();
359
140
  prevContextBySession.clear();
360
- blockCostCache.clear();
141
+ adapterByDir.clear();
361
142
  console.log('[enricher] Stopped');
362
143
  }
363
144
  function cleanupSession(sessionId) {
364
- blockCostCache.delete(sessionId);
365
145
  prevContextBySession.delete(sessionId);
366
- for (const [key, entry] of fileOffsets) {
367
- if (key.includes(sessionId))
368
- fileOffsets.delete(key);
369
- }
370
146
  }
371
- async function processLatestForSession(sessionId, onUpdate) {
372
- try {
373
- if (!fs_1.default.existsSync(PROJECTS_DIR))
374
- return;
375
- const dirs = await promises_1.default.readdir(PROJECTS_DIR);
376
- for (const dir of dirs) {
377
- const dirPath = path_1.default.join(PROJECTS_DIR, dir);
378
- try {
379
- const stat = await promises_1.default.stat(dirPath);
380
- if (!stat.isDirectory())
381
- continue;
382
- }
383
- catch {
147
+ // ─── Legacy: processLatestForSession (now adapter-agnostic) ─────────────────────
148
+ async function processLatestForSession(sessionId, onUpdate, source) {
149
+ const adapters = source
150
+ ? [(0, adapter_1.getAdapter)(source)].filter(Boolean)
151
+ : (0, adapter_1.getActiveAdapters)();
152
+ for (const adapter of adapters) {
153
+ for (const watchPath of adapter.getWatchPaths()) {
154
+ const baseDir = watchPath.split('/**')[0];
155
+ if (!baseDir)
384
156
  continue;
385
- }
386
- const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
387
- try {
388
- await promises_1.default.access(filePath);
389
- }
390
- catch {
157
+ if (!fs_1.default.existsSync(baseDir))
391
158
  continue;
159
+ const dirs = fs_1.default.readdirSync(baseDir);
160
+ for (const dir of dirs) {
161
+ const dirPath = path_1.default.join(baseDir, dir);
162
+ try {
163
+ if (!fs_1.default.statSync(dirPath).isDirectory())
164
+ continue;
165
+ }
166
+ catch {
167
+ continue;
168
+ }
169
+ const filePath = path_1.default.join(dirPath, `${sessionId}.jsonl`);
170
+ if (!fs_1.default.existsSync(filePath))
171
+ continue;
172
+ const cost = await adapter.getSessionCost(filePath);
173
+ if (cost && cost.cost_usd >= 0) {
174
+ onUpdate(sessionId, cost, adapter.name);
175
+ }
176
+ return;
392
177
  }
393
- const cost = await processJSONL(filePath);
394
- if (cost && cost.cost_usd >= 0)
395
- onUpdate(sessionId, cost);
396
- return;
397
178
  }
398
179
  }
399
- catch { }
400
180
  }
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ const export_1 = require("./export");
29
29
  const config_1 = require("./config");
30
30
  const doctor_1 = require("./doctor");
31
31
  const roast_1 = require("./roast");
32
+ const cost_projector_1 = require("./cost-projector");
32
33
  const insights_1 = require("./insights");
33
34
  const paths_1 = require("./paths");
34
35
  const quota_tracker_1 = require("./quota-tracker");
@@ -247,14 +248,13 @@ program
247
248
  .action(async (opts) => {
248
249
  try {
249
250
  await (0, quota_tracker_1.refreshFromApi)(); // refresh disk cache on demand; daemon reads from disk
250
- const [quotaRes, healthRes] = await Promise.all([
251
+ const [quotaRes] = await Promise.all([
251
252
  fetch('http://localhost:7337/quota'),
252
253
  fetch('http://localhost:7337/health'),
253
254
  ]);
254
255
  if (!quotaRes.ok)
255
256
  throw new Error('Daemon unavailable');
256
257
  const q = await quotaRes.json();
257
- const _h = await healthRes.json().catch(() => ({}));
258
258
  if (opts.json) {
259
259
  console.log(JSON.stringify({
260
260
  cyclePrompts: q.cyclePrompts,
@@ -556,4 +556,25 @@ program
556
556
  process.exit(1);
557
557
  }
558
558
  });
559
+ program
560
+ .command('project')
561
+ .description('Show cost projection with linear regression')
562
+ .option('--days <number>', 'Look back N days for data (default 90)')
563
+ .option('--json', 'Output raw JSON')
564
+ .action((opts) => {
565
+ try {
566
+ const days = Math.max(7, Math.min(365, parseInt(opts.days ?? '90', 10) || 90));
567
+ const p = (0, cost_projector_1.computeProjection)(days);
568
+ if (opts.json) {
569
+ console.log(JSON.stringify(p, null, 2));
570
+ process.exit(0);
571
+ }
572
+ console.log((0, cost_projector_1.formatProjection)(p));
573
+ process.exit(0);
574
+ }
575
+ catch (err) {
576
+ console.error('\n❌ Error:', err.message);
577
+ process.exit(1);
578
+ }
579
+ });
559
580
  program.parse();
package/dist/insights.js CHANGED
@@ -223,8 +223,6 @@ function renderWeeklyInsight(d) {
223
223
  const R = '\x1b[0m';
224
224
  const B = '\x1b[1m';
225
225
  const D = '\x1b[2m';
226
- const G = '\x1b[32m';
227
- const Y = '\x1b[33m';
228
226
  const C = '\x1b[36m';
229
227
  const bar = (pct, width = 20) => {
230
228
  const filled = Math.round(Math.min(pct, 100) / 100 * width);
@@ -74,7 +74,7 @@ function fmtTok(n) {
74
74
  * - {project}/AGENTS.md — instructions for agent mode
75
75
  * - {project}/.claude/CLAUDE.md — alternative project-level location
76
76
  */
77
- function resolveContextCandidates(homeDir, projectCwd) {
77
+ function resolveContextCandidates(_homeDir, projectCwd) {
78
78
  const claudeDir = (0, paths_1.getClaudeDir)();
79
79
  const candidates = [
80
80
  { label: 'CLAUDE.md (global)', filePath: path_1.default.join(claudeDir, 'CLAUDE.md') },
@@ -23,7 +23,7 @@ const rateLimitCleanupInterval = setInterval(() => {
23
23
  const now = Date.now();
24
24
  rateLimitMap.forEach((v, k) => { if (now - v.windowStart > RATE_LIMIT_WINDOW_MS)
25
25
  rateLimitMap.delete(k); });
26
- }, 5 * 60000);
26
+ }, 5 * 60000).unref();
27
27
  function stopRateLimiter() {
28
28
  clearInterval(rateLimitCleanupInterval);
29
29
  rateLimitMap.clear();
package/dist/paths.d.ts CHANGED
@@ -10,6 +10,23 @@
10
10
  * Claude Code encodes project paths by replacing path separators AND colons with '-'.
11
11
  * This module provides helpers to encode/decode those paths cross-platform.
12
12
  */
13
+ /**
14
+ * Detecta si claudestat está corriendo como binario standalone (Bun compile)
15
+ * vs desde npm (node dist/index.js).
16
+ */
17
+ export declare function isBinary(): boolean;
18
+ /**
19
+ * Retorna el directorio base del binario o del proyecto.
20
+ * En binario: directorio donde está el ejecutable.
21
+ * En npm: root del proyecto (unimos dist/..).
22
+ */
23
+ export declare function getBinaryDir(): string;
24
+ /**
25
+ * Retorna el directorio del dashboard build (dashboard/dist/).
26
+ * En binario: busca relativo al binario o en CLAUDESTAT_DATA_DIR.
27
+ * En npm: usa __dirname para encontrar dist/.
28
+ */
29
+ export declare function getDashboardDir(): string;
13
30
  /**
14
31
  * Returns the Claude Code data directory (~/.claude on all platforms).
15
32
  * Empirically verified: Claude Code CLI stores settings at ~/.claude on macOS, Linux, and Windows.