claude-cup 0.4.0 → 0.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/web/app.js +1 -1
- package/dist/web/index.html +127 -127
- package/mcp-server/dist/mcp-server.mjs +1 -1
- package/package.json +1 -1
- package/scripts/build-mcp-launcher.mjs +1 -1
- package/src/aggregator.js +317 -297
- package/src/cli.js +26 -16
- package/src/eco.js +1 -1
- package/src/leaderboard.js +243 -48
- package/src/statusline.js +71 -71
- package/src/tui.js +42 -1
- package/src/usage-api.js +250 -250
package/src/aggregator.js
CHANGED
|
@@ -1,297 +1,317 @@
|
|
|
1
|
-
// Aggregates parsed transcript events into "today" stats + persisted daily history.
|
|
2
|
-
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
5
|
-
|
|
6
|
-
// USD per million tokens (estimate; close enough for a jar).
|
|
7
|
-
const PRICING = [
|
|
8
|
-
{ match: /opus/i, in: 5, out: 25 },
|
|
9
|
-
{ match: /sonnet/i, in: 3, out: 15 },
|
|
10
|
-
{ match: /haiku/i, in: 1, out: 5 },
|
|
11
|
-
];
|
|
12
|
-
const DEFAULT_PRICE = { in: 3, out: 15 };
|
|
13
|
-
|
|
14
|
-
export function priceFor(model) {
|
|
15
|
-
for (const p of PRICING) if (p.match.test(model || '')) return p;
|
|
16
|
-
return DEFAULT_PRICE;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function costOf(model, usage) {
|
|
20
|
-
const p = priceFor(model);
|
|
21
|
-
const M = 1e6;
|
|
22
|
-
return (
|
|
23
|
-
(usage.in * p.in +
|
|
24
|
-
usage.out * p.out +
|
|
25
|
-
usage.cacheRead * p.in * 0.1 +
|
|
26
|
-
usage.cacheW5m * p.in * 1.25 +
|
|
27
|
-
usage.cacheW1h * p.in * 2) / M
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function localDateKey(ts) {
|
|
32
|
-
const d = new Date(ts);
|
|
33
|
-
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
34
|
-
const dd = String(d.getDate()).padStart(2, '0');
|
|
35
|
-
return `${d.getFullYear()}-${mm}-${dd}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function localMidnight(now = Date.now()) {
|
|
39
|
-
const d = new Date(now);
|
|
40
|
-
d.setHours(0, 0, 0, 0);
|
|
41
|
-
return d.getTime();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const EDIT_DEDUP_MS = 2 * 60 * 1000;
|
|
45
|
-
|
|
46
|
-
function emptyDay(dateKey) {
|
|
47
|
-
return {
|
|
48
|
-
date: dateKey,
|
|
49
|
-
tokensIn: 0,
|
|
50
|
-
tokensOut: 0,
|
|
51
|
-
cacheRead: 0,
|
|
52
|
-
cacheWrite: 0,
|
|
53
|
-
totalTokens: 0,
|
|
54
|
-
cost: 0,
|
|
55
|
-
toolCalls: 0,
|
|
56
|
-
toolsByCategory: {},
|
|
57
|
-
assistantMessages: 0,
|
|
58
|
-
userPrompts: 0,
|
|
59
|
-
models: {},
|
|
60
|
-
fillMax: 0,
|
|
61
|
-
_editEvents: [],
|
|
62
|
-
_terminalCount: 0,
|
|
63
|
-
_commitCount: 0,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const BURN_WINDOW_MS = 10 * 60 * 1000;
|
|
68
|
-
|
|
69
|
-
export class Aggregator extends EventEmitter {
|
|
70
|
-
constructor({ historyPath, now = () => Date.now() } = {}) {
|
|
71
|
-
super();
|
|
72
|
-
this.historyPath = historyPath || null;
|
|
73
|
-
this.now = now;
|
|
74
|
-
this.day = emptyDay(localDateKey(this.now()));
|
|
75
|
-
this.sessions = new Set();
|
|
76
|
-
this.seen = new Set();
|
|
77
|
-
this.histSeen = new Set();
|
|
78
|
-
this.backfill = {}; // dateKey -> partial day totals from old transcripts
|
|
79
|
-
this.burn = []; // {ts, tokens}
|
|
80
|
-
this.history = { days: {} };
|
|
81
|
-
this._saveTimer = null;
|
|
82
|
-
this._loadHistory();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
_loadHistory() {
|
|
86
|
-
if (!this.historyPath) return;
|
|
87
|
-
try {
|
|
88
|
-
const parsed = JSON.parse(readFileSync(this.historyPath, 'utf8'));
|
|
89
|
-
if (parsed && typeof parsed.days === 'object') this.history = parsed;
|
|
90
|
-
} catch {
|
|
91
|
-
/* first run */
|
|
92
|
-
}
|
|
93
|
-
// Restore today's stats if the server restarted mid-day.
|
|
94
|
-
const saved = this.history.days[this.day.date];
|
|
95
|
-
if (saved && saved._full) {
|
|
96
|
-
this.day = { ...emptyDay(this.day.date), ...saved._full };
|
|
97
|
-
this.sessions = new Set(saved._full.sessionIds || []);
|
|
98
|
-
this.seen = new Set(saved._full.seenUuids || []);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
_rolloverIfNeeded() {
|
|
103
|
-
const key = localDateKey(this.now());
|
|
104
|
-
if (key !== this.day.date) {
|
|
105
|
-
this._snapshotToHistory();
|
|
106
|
-
this.day = emptyDay(key);
|
|
107
|
-
this.sessions = new Set();
|
|
108
|
-
this.seen = new Set();
|
|
109
|
-
this.burn = [];
|
|
110
|
-
this.emit('rollover', key);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
_snapshotToHistory() {
|
|
115
|
-
const d = this.day;
|
|
116
|
-
this.history.days[d.date] = {
|
|
117
|
-
totalTokens: d.totalTokens,
|
|
118
|
-
cost: Math.round(d.cost * 10000) / 10000,
|
|
119
|
-
toolCalls: d.toolCalls,
|
|
120
|
-
messages: d.assistantMessages,
|
|
121
|
-
fillMax: d.fillMax,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (evt.
|
|
180
|
-
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
return
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
return Math.round(
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1
|
+
// Aggregates parsed transcript events into "today" stats + persisted daily history.
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// USD per million tokens (estimate; close enough for a jar).
|
|
7
|
+
const PRICING = [
|
|
8
|
+
{ match: /opus/i, in: 5, out: 25 },
|
|
9
|
+
{ match: /sonnet/i, in: 3, out: 15 },
|
|
10
|
+
{ match: /haiku/i, in: 1, out: 5 },
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_PRICE = { in: 3, out: 15 };
|
|
13
|
+
|
|
14
|
+
export function priceFor(model) {
|
|
15
|
+
for (const p of PRICING) if (p.match.test(model || '')) return p;
|
|
16
|
+
return DEFAULT_PRICE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function costOf(model, usage) {
|
|
20
|
+
const p = priceFor(model);
|
|
21
|
+
const M = 1e6;
|
|
22
|
+
return (
|
|
23
|
+
(usage.in * p.in +
|
|
24
|
+
usage.out * p.out +
|
|
25
|
+
usage.cacheRead * p.in * 0.1 +
|
|
26
|
+
usage.cacheW5m * p.in * 1.25 +
|
|
27
|
+
usage.cacheW1h * p.in * 2) / M
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function localDateKey(ts) {
|
|
32
|
+
const d = new Date(ts);
|
|
33
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
34
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
35
|
+
return `${d.getFullYear()}-${mm}-${dd}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function localMidnight(now = Date.now()) {
|
|
39
|
+
const d = new Date(now);
|
|
40
|
+
d.setHours(0, 0, 0, 0);
|
|
41
|
+
return d.getTime();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EDIT_DEDUP_MS = 2 * 60 * 1000;
|
|
45
|
+
|
|
46
|
+
function emptyDay(dateKey) {
|
|
47
|
+
return {
|
|
48
|
+
date: dateKey,
|
|
49
|
+
tokensIn: 0,
|
|
50
|
+
tokensOut: 0,
|
|
51
|
+
cacheRead: 0,
|
|
52
|
+
cacheWrite: 0,
|
|
53
|
+
totalTokens: 0,
|
|
54
|
+
cost: 0,
|
|
55
|
+
toolCalls: 0,
|
|
56
|
+
toolsByCategory: {},
|
|
57
|
+
assistantMessages: 0,
|
|
58
|
+
userPrompts: 0,
|
|
59
|
+
models: {},
|
|
60
|
+
fillMax: 0,
|
|
61
|
+
_editEvents: [],
|
|
62
|
+
_terminalCount: 0,
|
|
63
|
+
_commitCount: 0,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const BURN_WINDOW_MS = 10 * 60 * 1000;
|
|
68
|
+
|
|
69
|
+
export class Aggregator extends EventEmitter {
|
|
70
|
+
constructor({ historyPath, now = () => Date.now() } = {}) {
|
|
71
|
+
super();
|
|
72
|
+
this.historyPath = historyPath || null;
|
|
73
|
+
this.now = now;
|
|
74
|
+
this.day = emptyDay(localDateKey(this.now()));
|
|
75
|
+
this.sessions = new Set();
|
|
76
|
+
this.seen = new Set();
|
|
77
|
+
this.histSeen = new Set();
|
|
78
|
+
this.backfill = {}; // dateKey -> partial day totals from old transcripts
|
|
79
|
+
this.burn = []; // {ts, tokens}
|
|
80
|
+
this.history = { days: {} };
|
|
81
|
+
this._saveTimer = null;
|
|
82
|
+
this._loadHistory();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_loadHistory() {
|
|
86
|
+
if (!this.historyPath) return;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(readFileSync(this.historyPath, 'utf8'));
|
|
89
|
+
if (parsed && typeof parsed.days === 'object') this.history = parsed;
|
|
90
|
+
} catch {
|
|
91
|
+
/* first run */
|
|
92
|
+
}
|
|
93
|
+
// Restore today's stats if the server restarted mid-day.
|
|
94
|
+
const saved = this.history.days[this.day.date];
|
|
95
|
+
if (saved && saved._full) {
|
|
96
|
+
this.day = { ...emptyDay(this.day.date), ...saved._full };
|
|
97
|
+
this.sessions = new Set(saved._full.sessionIds || []);
|
|
98
|
+
this.seen = new Set(saved._full.seenUuids || []);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_rolloverIfNeeded() {
|
|
103
|
+
const key = localDateKey(this.now());
|
|
104
|
+
if (key !== this.day.date) {
|
|
105
|
+
this._snapshotToHistory();
|
|
106
|
+
this.day = emptyDay(key);
|
|
107
|
+
this.sessions = new Set();
|
|
108
|
+
this.seen = new Set();
|
|
109
|
+
this.burn = [];
|
|
110
|
+
this.emit('rollover', key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_snapshotToHistory() {
|
|
115
|
+
const d = this.day;
|
|
116
|
+
this.history.days[d.date] = {
|
|
117
|
+
totalTokens: d.totalTokens,
|
|
118
|
+
cost: Math.round(d.cost * 10000) / 10000,
|
|
119
|
+
toolCalls: d.toolCalls,
|
|
120
|
+
messages: d.assistantMessages,
|
|
121
|
+
fillMax: d.fillMax,
|
|
122
|
+
buildRate: this.buildRate(),
|
|
123
|
+
_full: {
|
|
124
|
+
...d,
|
|
125
|
+
sessionIds: [...this.sessions].slice(0, 500),
|
|
126
|
+
seenUuids: [...this.seen].slice(-5000),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Weighted 7-day rolling Build Rate: today=1.0, then 0.9, 0.8, ... 0.4 */
|
|
132
|
+
rollingBuildRate() {
|
|
133
|
+
const weights = [0.4, 0.5, 0.6, 0.7, 0.8, 0.9]; // oldest first, today added below
|
|
134
|
+
const past = this.historyDays(6); // 6 prior days, oldest first
|
|
135
|
+
let weightedSum = 0;
|
|
136
|
+
let weightTotal = 0;
|
|
137
|
+
past.forEach((day, i) => {
|
|
138
|
+
const w = weights[i] || 0.4;
|
|
139
|
+
const br = day.buildRate || 0;
|
|
140
|
+
weightedSum += br * w;
|
|
141
|
+
weightTotal += w;
|
|
142
|
+
});
|
|
143
|
+
const todayBr = this.buildRate();
|
|
144
|
+
weightedSum += todayBr * 1.0;
|
|
145
|
+
weightTotal += 1.0;
|
|
146
|
+
return Math.round((weightedSum / weightTotal) * 100) / 100;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
saveNow() {
|
|
150
|
+
if (!this.historyPath) return;
|
|
151
|
+
this._snapshotToHistory();
|
|
152
|
+
try {
|
|
153
|
+
mkdirSync(dirname(this.historyPath), { recursive: true });
|
|
154
|
+
writeFileSync(this.historyPath, JSON.stringify(this.history));
|
|
155
|
+
} catch {
|
|
156
|
+
/* non-fatal */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_saveSoon() {
|
|
161
|
+
if (!this.historyPath || this._saveTimer) return;
|
|
162
|
+
this._saveTimer = setTimeout(() => {
|
|
163
|
+
this._saveTimer = null;
|
|
164
|
+
this.saveNow();
|
|
165
|
+
}, 5000);
|
|
166
|
+
this._saveTimer.unref?.();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
noteFill(pct) {
|
|
170
|
+
this._rolloverIfNeeded();
|
|
171
|
+
if (typeof pct === 'number' && pct > this.day.fillMax) {
|
|
172
|
+
this.day.fillMax = Math.min(100, Math.round(pct * 10) / 10);
|
|
173
|
+
this._saveSoon();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Accumulates an event from a previous day into the history shelf. */
|
|
178
|
+
_addBackfill(evt) {
|
|
179
|
+
if (evt.kind !== 'assistant') return;
|
|
180
|
+
if (evt.uuid) {
|
|
181
|
+
if (this.histSeen.has(evt.uuid)) return;
|
|
182
|
+
this.histSeen.add(evt.uuid);
|
|
183
|
+
}
|
|
184
|
+
const key = localDateKey(evt.ts);
|
|
185
|
+
const b = (this.backfill[key] ||= { totalTokens: 0, cost: 0, toolCalls: 0, messages: 0, fillMax: 0 });
|
|
186
|
+
const u = evt.usage;
|
|
187
|
+
b.totalTokens += u.in + u.out;
|
|
188
|
+
b.cost += costOf(evt.model, u);
|
|
189
|
+
b.toolCalls += evt.tools.length;
|
|
190
|
+
b.messages += 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** @returns {boolean} true if the event counted toward today (and may animate) */
|
|
194
|
+
addEvent(evt) {
|
|
195
|
+
if (!evt) return false;
|
|
196
|
+
this._rolloverIfNeeded();
|
|
197
|
+
if (evt.ts > this.now() + 60_000) return false;
|
|
198
|
+
if (evt.ts < localMidnight(this.now())) {
|
|
199
|
+
if (evt.ts >= localMidnight(this.now()) - 7 * 86400000) this._addBackfill(evt);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (evt.uuid) {
|
|
203
|
+
if (this.seen.has(evt.uuid)) return false;
|
|
204
|
+
this.seen.add(evt.uuid);
|
|
205
|
+
}
|
|
206
|
+
if (evt.sessionId) this.sessions.add(evt.sessionId);
|
|
207
|
+
const d = this.day;
|
|
208
|
+
|
|
209
|
+
if (evt.kind === 'prompt') {
|
|
210
|
+
d.userPrompts += 1;
|
|
211
|
+
this._saveSoon();
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (evt.kind === 'assistant') {
|
|
216
|
+
const u = evt.usage;
|
|
217
|
+
const fresh = u.in + u.out;
|
|
218
|
+
d.tokensIn += u.in;
|
|
219
|
+
d.tokensOut += u.out;
|
|
220
|
+
d.cacheRead += u.cacheRead;
|
|
221
|
+
d.cacheWrite += u.cacheW5m + u.cacheW1h;
|
|
222
|
+
d.totalTokens += fresh;
|
|
223
|
+
d.cost += costOf(evt.model, u);
|
|
224
|
+
d.assistantMessages += 1;
|
|
225
|
+
d.models[evt.model] = (d.models[evt.model] || 0) + fresh;
|
|
226
|
+
let evtEdits = 0;
|
|
227
|
+
let evtTerminals = 0;
|
|
228
|
+
for (const t of evt.tools) {
|
|
229
|
+
d.toolCalls += 1;
|
|
230
|
+
d.toolsByCategory[t.category] = (d.toolsByCategory[t.category] || 0) + 1;
|
|
231
|
+
if (t.category === 'edit') evtEdits++;
|
|
232
|
+
if (t.category === 'terminal') evtTerminals++;
|
|
233
|
+
}
|
|
234
|
+
if (evtEdits > 0) d._editEvents.push({ ts: evt.ts, count: evtEdits });
|
|
235
|
+
d._terminalCount += evtTerminals;
|
|
236
|
+
if (fresh > 0) {
|
|
237
|
+
this.burn.push({ ts: evt.ts, tokens: fresh });
|
|
238
|
+
}
|
|
239
|
+
this._saveSoon();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
burnRate() {
|
|
246
|
+
const cutoff = this.now() - BURN_WINDOW_MS;
|
|
247
|
+
while (this.burn.length && this.burn[0].ts < cutoff) this.burn.shift();
|
|
248
|
+
const total = this.burn.reduce((a, b) => a + b.tokens, 0);
|
|
249
|
+
return Math.round(total / (BURN_WINDOW_MS / 60000));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
_netEdits() {
|
|
253
|
+
const edits = this.day._editEvents;
|
|
254
|
+
if (!edits.length) return 0;
|
|
255
|
+
let net = 0;
|
|
256
|
+
for (let i = 0; i < edits.length; i++) {
|
|
257
|
+
const next = edits[i + 1];
|
|
258
|
+
if (next && (next.ts - edits[i].ts) < EDIT_DEDUP_MS) continue;
|
|
259
|
+
net += edits[i].count;
|
|
260
|
+
}
|
|
261
|
+
return net;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
buildRate() {
|
|
265
|
+
const d = this.day;
|
|
266
|
+
const net = this._netEdits();
|
|
267
|
+
const kiloTokens = Math.max(1, d.totalTokens / 1000);
|
|
268
|
+
return Math.round(((net + d._terminalCount + d._commitCount * 5) / kiloTokens) * 100) / 100;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
snapshot() {
|
|
272
|
+
this._rolloverIfNeeded();
|
|
273
|
+
const d = this.day;
|
|
274
|
+
return {
|
|
275
|
+
date: d.date,
|
|
276
|
+
tokensIn: d.tokensIn,
|
|
277
|
+
tokensOut: d.tokensOut,
|
|
278
|
+
cacheRead: d.cacheRead,
|
|
279
|
+
cacheWrite: d.cacheWrite,
|
|
280
|
+
totalTokens: d.totalTokens,
|
|
281
|
+
cost: Math.round(d.cost * 100) / 100,
|
|
282
|
+
toolCalls: d.toolCalls,
|
|
283
|
+
toolsByCategory: d.toolsByCategory,
|
|
284
|
+
assistantMessages: d.assistantMessages,
|
|
285
|
+
userPrompts: d.userPrompts,
|
|
286
|
+
sessions: this.sessions.size,
|
|
287
|
+
models: d.models,
|
|
288
|
+
burnRate: this.burnRate(),
|
|
289
|
+
fillMax: d.fillMax,
|
|
290
|
+
buildRate: this.buildRate(),
|
|
291
|
+
netEdits: this._netEdits(),
|
|
292
|
+
cleanTerminal: d._terminalCount,
|
|
293
|
+
commits: d._commitCount,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** last N days (excluding today), oldest first. Live records win over backfill. */
|
|
298
|
+
historyDays(n = 7) {
|
|
299
|
+
const out = [];
|
|
300
|
+
for (let i = n; i >= 1; i--) {
|
|
301
|
+
const key = localDateKey(this.now() - i * 86400000);
|
|
302
|
+
const day = this.history.days[key]?._full ? this.history.days[key] : null;
|
|
303
|
+
const fallback = this.backfill[key] || this.history.days[key];
|
|
304
|
+
const d = day || fallback;
|
|
305
|
+
out.push({
|
|
306
|
+
date: key,
|
|
307
|
+
totalTokens: d?.totalTokens || 0,
|
|
308
|
+
cost: Math.round((d?.cost || 0) * 100) / 100,
|
|
309
|
+
toolCalls: d?.toolCalls || 0,
|
|
310
|
+
messages: d?.messages || 0,
|
|
311
|
+
fillMax: d?.fillMax || 0,
|
|
312
|
+
buildRate: d?.buildRate || 0,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
}
|