@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/README.md +130 -547
- package/dist/cost-projector.d.ts +24 -0
- package/dist/cost-projector.js +133 -0
- package/dist/daemon.js +1 -1
- package/dist/db.d.ts +4 -0
- package/dist/db.js +14 -6
- package/dist/enricher.d.ts +18 -26
- package/dist/enricher.js +113 -333
- package/dist/index.js +23 -2
- package/dist/insights.js +0 -2
- package/dist/meta-stats.js +1 -1
- package/dist/middleware/rate-limiter.js +1 -1
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +44 -0
- package/dist/quota-tracker.js +0 -1
- package/dist/roast.js +0 -2
- package/dist/routes/events.js +5 -5
- package/dist/routes/misc.js +3 -21
- package/dist/routes/stream.d.ts +1 -1
- package/dist/routes/stream.js +3 -3
- package/dist/service.js +11 -7
- package/dist/watchers/adapter.d.ts +37 -0
- package/dist/watchers/adapter.js +31 -0
- package/dist/watchers/amp.d.ts +8 -0
- package/dist/watchers/amp.js +42 -0
- package/dist/watchers/claude-code.d.ts +17 -0
- package/dist/watchers/claude-code.js +300 -0
- package/dist/watchers/codebuff.d.ts +8 -0
- package/dist/watchers/codebuff.js +42 -0
- package/dist/watchers/codex.d.ts +9 -0
- package/dist/watchers/codex.js +43 -0
- package/dist/watchers/droid.d.ts +8 -0
- package/dist/watchers/droid.js +42 -0
- package/dist/watchers/opencode.d.ts +9 -0
- package/dist/watchers/opencode.js +43 -0
- package/package.json +10 -2
package/dist/enricher.js
CHANGED
|
@@ -1,347 +1,129 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* enricher.ts —
|
|
3
|
+
* enricher.ts — Watcher multi-CLI usando adapters
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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 =
|
|
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
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
69
|
+
fileLocks.delete(filePath);
|
|
230
70
|
}
|
|
231
|
-
return cached?.data ?? [];
|
|
232
71
|
}
|
|
233
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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(
|
|
344
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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);
|
package/dist/meta-stats.js
CHANGED
|
@@ -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(
|
|
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.
|