token-contribution 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +59 -2
- package/assets/preview-overview.png +0 -0
- package/assets/preview-trends.png +0 -0
- package/cordis.patch.yml +4 -0
- package/lib/aggregator.js +328 -0
- package/lib/aggregator.js.map +7 -0
- package/lib/client.js +1241 -0
- package/lib/client.js.map +7 -0
- package/lib/index.js +45 -0
- package/lib/index.js.map +7 -0
- package/package.json +54 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oiupoyt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# token-contribution
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> GitHub-style daily token usage contribution calendar, model analytics, and timeline trends for [DeepSeek Harness](https://github.com/deepseek-ai/dsh).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install with a single command via the official DSH CLI:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
dsh plugin --profile web add token-contribution
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
*(To remove later: `dsh plugin --profile web remove token-contribution`)*
|
|
16
|
+
|
|
17
|
+
### Manual Alternative (pnpm)
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd ~/.dsh/profiles/web
|
|
21
|
+
pnpm add token-contribution
|
|
22
|
+
```
|
|
23
|
+
Then add `"token-contribution"` to `bundles` in `~/.dsh/profiles/web/package.json`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Preview
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="./assets/preview-overview.png" alt="Token Heatmap Overview" width="44%" />
|
|
31
|
+
|
|
32
|
+
<img src="./assets/preview-trends.png" alt="Model Usage Breakdown & Trends" width="44%" />
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **52-Week GitHub Contribution Calendar:** 7 rows × 52 columns tracking daily token activity with a smooth monochromatic gradient anchored on DeepSeek's official brand blue (`#3964FE`).
|
|
40
|
+
- **Interactive Day Inspector:** Click on any day to slide open an inspection drawer showing every session active on that date, exact timestamps, and token breakdown.
|
|
41
|
+
- **Top Model Usage Breakdown:** Real-time distribution showing tokens, percentages, and request steps for every model used across sessions.
|
|
42
|
+
- **Token Usage Trends Timeline:** Stacked bar chart comparing daily Billed Tokens vs. Cache Reads, with **14D / 30D / 90D** range filters and hover tooltips.
|
|
43
|
+
- **Global & Per-Session Filtering:** Toggle between aggregate global history or inspect token activity for individual sessions.
|
|
44
|
+
- **Auto-Expanding Panel:** Smoothly widens the DSH Settings dialog to 1180px so the calendar and charts fit cleanly with zero scrollbars.
|
|
45
|
+
- **Session Token Meter Integration:** Injects a clean action button into the in-session token usage dialog to open the heatmap directly from active sessions.
|
|
46
|
+
- **High-Performance Parser:** Recursively aggregates local `.jsonl.zstd` logs with timestamp and mtime caching for instant loading.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
1. Open the DeepSeek Harness Web GUI (`http://127.0.0.1:3080`).
|
|
53
|
+
2. Go to **Settings** (gear icon at the bottom left) → **Token Heatmap**.
|
|
54
|
+
3. Or during any session, click on the token stats pill at the bottom of the composer and tap **"View Token Usage Heatmap →"**.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT © [oiupoyt](https://git.oiupoyt.space/oiupoyt)
|
|
Binary file
|
|
Binary file
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
6
|
+
function findSessionFiles(dir) {
|
|
7
|
+
const results = [];
|
|
8
|
+
try {
|
|
9
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const fullPath = path.join(dir, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
results.push(...findSessionFiles(fullPath));
|
|
14
|
+
} else if (entry.name.endsWith(".jsonl.zstd") || entry.name.endsWith(".jsonl")) {
|
|
15
|
+
results.push(fullPath);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
async function parseSessionFile(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
const stat = fs.statSync(filePath);
|
|
25
|
+
const cached = fileCache.get(filePath);
|
|
26
|
+
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
27
|
+
return cached.session;
|
|
28
|
+
}
|
|
29
|
+
const isZstd = filePath.endsWith(".zstd");
|
|
30
|
+
const days = {};
|
|
31
|
+
const sessionModels = {};
|
|
32
|
+
let sessionId = path.basename(path.dirname(filePath));
|
|
33
|
+
let title = "";
|
|
34
|
+
let createdAt = stat.birthtimeMs || stat.mtimeMs;
|
|
35
|
+
let totalTokens = 0;
|
|
36
|
+
let totalInputTokens = 0;
|
|
37
|
+
let totalOutputTokens = 0;
|
|
38
|
+
let totalCacheReadTokens = 0;
|
|
39
|
+
let totalTurns = 0;
|
|
40
|
+
await new Promise((resolve) => {
|
|
41
|
+
let buffer = "";
|
|
42
|
+
const handleLine = (line) => {
|
|
43
|
+
if (line.includes('"type":"session"')) {
|
|
44
|
+
try {
|
|
45
|
+
const obj = JSON.parse(line);
|
|
46
|
+
if (obj.id) sessionId = obj.id;
|
|
47
|
+
if (obj.createdAt) createdAt = obj.createdAt;
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!title && line.includes('"session/title"')) {
|
|
52
|
+
try {
|
|
53
|
+
const obj = JSON.parse(line);
|
|
54
|
+
if (obj.data?.title) title = obj.data.title;
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!title && line.includes('"subagent/descriptor"')) {
|
|
59
|
+
try {
|
|
60
|
+
const obj = JSON.parse(line);
|
|
61
|
+
if (obj.data?.label) title = obj.data.label;
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (line.includes('"usage"')) {
|
|
66
|
+
try {
|
|
67
|
+
const obj = JSON.parse(line);
|
|
68
|
+
const time = obj.time || obj.data && obj.data.time;
|
|
69
|
+
const usage = obj.usage || obj.data && obj.data.usage;
|
|
70
|
+
if (time && usage) {
|
|
71
|
+
const d = new Date(time);
|
|
72
|
+
const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
73
|
+
const input = Number(usage.inputTokens) || 0;
|
|
74
|
+
const output = Number(usage.outputTokens) || 0;
|
|
75
|
+
const cacheRead = Number(usage.cacheReadTokens) || 0;
|
|
76
|
+
const cacheWrite = Number(usage.cacheWriteTokens) || 0;
|
|
77
|
+
const total = input + output + cacheRead;
|
|
78
|
+
const model = obj.data?.message?.source?.model || obj.data?.model || obj.model || "gemini-3.8-flash-tiered";
|
|
79
|
+
if (!days[dateKey]) {
|
|
80
|
+
days[dateKey] = {
|
|
81
|
+
totalTokens: 0,
|
|
82
|
+
inputTokens: 0,
|
|
83
|
+
outputTokens: 0,
|
|
84
|
+
cacheReadTokens: 0,
|
|
85
|
+
cacheWriteTokens: 0,
|
|
86
|
+
turns: 0
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
days[dateKey].totalTokens += total;
|
|
90
|
+
days[dateKey].inputTokens += input;
|
|
91
|
+
days[dateKey].outputTokens += output;
|
|
92
|
+
days[dateKey].cacheReadTokens += cacheRead;
|
|
93
|
+
days[dateKey].cacheWriteTokens += cacheWrite;
|
|
94
|
+
days[dateKey].turns += 1;
|
|
95
|
+
if (!sessionModels[model]) {
|
|
96
|
+
sessionModels[model] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };
|
|
97
|
+
}
|
|
98
|
+
sessionModels[model].totalTokens += total;
|
|
99
|
+
sessionModels[model].inputTokens += input;
|
|
100
|
+
sessionModels[model].outputTokens += output;
|
|
101
|
+
sessionModels[model].cacheReadTokens += cacheRead;
|
|
102
|
+
sessionModels[model].turns += 1;
|
|
103
|
+
totalTokens += total;
|
|
104
|
+
totalInputTokens += input;
|
|
105
|
+
totalOutputTokens += output;
|
|
106
|
+
totalCacheReadTokens += cacheRead;
|
|
107
|
+
totalTurns += 1;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (isZstd) {
|
|
114
|
+
const proc = spawn("zstd", ["-dc", filePath], { stdio: ["ignore", "pipe", "ignore"] });
|
|
115
|
+
proc.stdout.on("data", (chunk) => {
|
|
116
|
+
buffer += chunk.toString("utf8");
|
|
117
|
+
let idx;
|
|
118
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
119
|
+
const line = buffer.slice(0, idx);
|
|
120
|
+
buffer = buffer.slice(idx + 1);
|
|
121
|
+
handleLine(line);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
proc.on("close", () => {
|
|
125
|
+
if (buffer.trim().length > 0) handleLine(buffer);
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
proc.on("error", () => resolve());
|
|
129
|
+
} else {
|
|
130
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
131
|
+
stream.on("data", (chunk) => {
|
|
132
|
+
buffer += chunk;
|
|
133
|
+
let idx;
|
|
134
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
135
|
+
const line = buffer.slice(0, idx);
|
|
136
|
+
buffer = buffer.slice(idx + 1);
|
|
137
|
+
handleLine(line);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
stream.on("end", () => {
|
|
141
|
+
if (buffer.trim().length > 0) handleLine(buffer);
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
stream.on("error", () => resolve());
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const session = {
|
|
148
|
+
id: sessionId,
|
|
149
|
+
title: title || `Session ${sessionId.slice(0, 8)}`,
|
|
150
|
+
createdAt,
|
|
151
|
+
totalTokens,
|
|
152
|
+
inputTokens: totalInputTokens,
|
|
153
|
+
outputTokens: totalOutputTokens,
|
|
154
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
155
|
+
turns: totalTurns,
|
|
156
|
+
days,
|
|
157
|
+
models: sessionModels
|
|
158
|
+
};
|
|
159
|
+
fileCache.set(filePath, { mtimeMs: stat.mtimeMs, session });
|
|
160
|
+
return session;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function aggregateTokens(customSessionsRoot) {
|
|
166
|
+
const sessionsRoot = customSessionsRoot || path.join(os.homedir(), ".dsh", "sessions");
|
|
167
|
+
const files = findSessionFiles(sessionsRoot);
|
|
168
|
+
const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean);
|
|
169
|
+
parsedSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
170
|
+
const days = {};
|
|
171
|
+
const allModelsMap = {};
|
|
172
|
+
for (const session of parsedSessions) {
|
|
173
|
+
for (const [date, stats] of Object.entries(session.days)) {
|
|
174
|
+
if (!days[date]) {
|
|
175
|
+
days[date] = {
|
|
176
|
+
totalTokens: 0,
|
|
177
|
+
inputTokens: 0,
|
|
178
|
+
outputTokens: 0,
|
|
179
|
+
cacheReadTokens: 0,
|
|
180
|
+
cacheWriteTokens: 0,
|
|
181
|
+
turns: 0,
|
|
182
|
+
sessions: []
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
days[date].totalTokens += stats.totalTokens;
|
|
186
|
+
days[date].inputTokens += stats.inputTokens;
|
|
187
|
+
days[date].outputTokens += stats.outputTokens;
|
|
188
|
+
days[date].cacheReadTokens += stats.cacheReadTokens;
|
|
189
|
+
days[date].cacheWriteTokens += stats.cacheWriteTokens;
|
|
190
|
+
days[date].turns += stats.turns;
|
|
191
|
+
days[date].sessions.push({
|
|
192
|
+
sessionId: session.id,
|
|
193
|
+
sessionTitle: session.title,
|
|
194
|
+
createdAt: session.createdAt,
|
|
195
|
+
totalTokens: stats.totalTokens,
|
|
196
|
+
inputTokens: stats.inputTokens,
|
|
197
|
+
outputTokens: stats.outputTokens,
|
|
198
|
+
cacheReadTokens: stats.cacheReadTokens,
|
|
199
|
+
turns: stats.turns
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (session.models) {
|
|
203
|
+
for (const [modelName, mStats] of Object.entries(session.models)) {
|
|
204
|
+
if (!allModelsMap[modelName]) {
|
|
205
|
+
allModelsMap[modelName] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };
|
|
206
|
+
}
|
|
207
|
+
allModelsMap[modelName].totalTokens += mStats.totalTokens;
|
|
208
|
+
allModelsMap[modelName].inputTokens += mStats.inputTokens;
|
|
209
|
+
allModelsMap[modelName].outputTokens += mStats.outputTokens;
|
|
210
|
+
allModelsMap[modelName].cacheReadTokens += mStats.cacheReadTokens;
|
|
211
|
+
allModelsMap[modelName].turns += mStats.turns;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const day of Object.values(days)) {
|
|
216
|
+
if (day.sessions) {
|
|
217
|
+
day.sessions.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const positiveValues = Object.values(days).map((d) => d.totalTokens).filter((t) => t > 0).sort((a, b) => a - b);
|
|
221
|
+
let q1 = 5e6;
|
|
222
|
+
let q2 = 25e6;
|
|
223
|
+
let q3 = 75e6;
|
|
224
|
+
if (positiveValues.length >= 4) {
|
|
225
|
+
q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];
|
|
226
|
+
q2 = positiveValues[Math.floor(positiveValues.length * 0.5)];
|
|
227
|
+
q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];
|
|
228
|
+
}
|
|
229
|
+
for (const day of Object.values(days)) {
|
|
230
|
+
if (day.totalTokens === 0) day.level = 0;
|
|
231
|
+
else if (day.totalTokens <= q1) day.level = 1;
|
|
232
|
+
else if (day.totalTokens <= q2) day.level = 2;
|
|
233
|
+
else if (day.totalTokens <= q3) day.level = 3;
|
|
234
|
+
else day.level = 4;
|
|
235
|
+
}
|
|
236
|
+
let totalTokens = 0;
|
|
237
|
+
let totalInputTokens = 0;
|
|
238
|
+
let totalOutputTokens = 0;
|
|
239
|
+
let totalCacheReadTokens = 0;
|
|
240
|
+
let totalTurns = 0;
|
|
241
|
+
let peakDay = null;
|
|
242
|
+
for (const [date, stats] of Object.entries(days)) {
|
|
243
|
+
totalTokens += stats.totalTokens;
|
|
244
|
+
totalInputTokens += stats.inputTokens;
|
|
245
|
+
totalOutputTokens += stats.outputTokens;
|
|
246
|
+
totalCacheReadTokens += stats.cacheReadTokens;
|
|
247
|
+
totalTurns += stats.turns;
|
|
248
|
+
if (!peakDay || stats.totalTokens > peakDay.tokens) {
|
|
249
|
+
peakDay = { date, tokens: stats.totalTokens, turns: stats.turns };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const models = Object.entries(allModelsMap).map(([model, m]) => ({
|
|
253
|
+
model,
|
|
254
|
+
totalTokens: m.totalTokens,
|
|
255
|
+
inputTokens: m.inputTokens,
|
|
256
|
+
outputTokens: m.outputTokens,
|
|
257
|
+
cacheReadTokens: m.cacheReadTokens,
|
|
258
|
+
turns: m.turns,
|
|
259
|
+
percentage: totalTokens > 0 ? Math.round(m.totalTokens / totalTokens * 1e3) / 10 : 0
|
|
260
|
+
})).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
261
|
+
const activeDays = Object.values(days).filter((d) => d.totalTokens > 0).length;
|
|
262
|
+
const dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0;
|
|
263
|
+
const now = /* @__PURE__ */ new Date();
|
|
264
|
+
const formatDate = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
265
|
+
let currentStreak = 0;
|
|
266
|
+
let cursor = new Date(now);
|
|
267
|
+
const todayKey = formatDate(cursor);
|
|
268
|
+
cursor.setDate(cursor.getDate() - 1);
|
|
269
|
+
const yesterdayKey = formatDate(cursor);
|
|
270
|
+
let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null;
|
|
271
|
+
if (streakDate) {
|
|
272
|
+
while (true) {
|
|
273
|
+
const key = formatDate(streakDate);
|
|
274
|
+
if ((days[key]?.totalTokens ?? 0) > 0) {
|
|
275
|
+
currentStreak++;
|
|
276
|
+
streakDate.setDate(streakDate.getDate() - 1);
|
|
277
|
+
} else {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const sortedDates = Object.keys(days).filter((k) => days[k].totalTokens > 0).sort();
|
|
283
|
+
let longestStreak = 0;
|
|
284
|
+
let tempStreak = 0;
|
|
285
|
+
let prevDate = null;
|
|
286
|
+
for (const dateStr of sortedDates) {
|
|
287
|
+
const cur = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
288
|
+
if (!prevDate) {
|
|
289
|
+
tempStreak = 1;
|
|
290
|
+
} else {
|
|
291
|
+
const diffDays = Math.round((cur.getTime() - prevDate.getTime()) / (1e3 * 60 * 60 * 24));
|
|
292
|
+
if (diffDays === 1) {
|
|
293
|
+
tempStreak++;
|
|
294
|
+
} else {
|
|
295
|
+
tempStreak = 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (tempStreak > longestStreak) longestStreak = tempStreak;
|
|
299
|
+
prevDate = cur;
|
|
300
|
+
}
|
|
301
|
+
const endDate = formatDate(now);
|
|
302
|
+
const start = new Date(now);
|
|
303
|
+
start.setDate(start.getDate() - 364);
|
|
304
|
+
const startDate = formatDate(start);
|
|
305
|
+
return {
|
|
306
|
+
summary: {
|
|
307
|
+
totalTokens,
|
|
308
|
+
totalInputTokens,
|
|
309
|
+
totalOutputTokens,
|
|
310
|
+
totalCacheReadTokens,
|
|
311
|
+
totalTurns,
|
|
312
|
+
activeDays,
|
|
313
|
+
currentStreak,
|
|
314
|
+
longestStreak: Math.max(longestStreak, currentStreak),
|
|
315
|
+
peakDay,
|
|
316
|
+
dailyAverage
|
|
317
|
+
},
|
|
318
|
+
days,
|
|
319
|
+
sessions: parsedSessions,
|
|
320
|
+
models,
|
|
321
|
+
startDate,
|
|
322
|
+
endDate
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
export {
|
|
326
|
+
aggregateTokens
|
|
327
|
+
};
|
|
328
|
+
//# sourceMappingURL=aggregator.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/server/aggregator.ts"],
|
|
4
|
+
"sourcesContent": ["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { spawn } from \"node:child_process\";\n\nexport interface DaySessionDetail {\n sessionId: string;\n sessionTitle: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n}\n\nexport interface DayStats {\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n cacheWriteTokens: number;\n turns: number;\n level?: number; // 0..4\n sessions?: DaySessionDetail[];\n}\n\nexport interface ModelUsage {\n model: string;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n percentage: number;\n}\n\nexport interface SessionSummary {\n id: string;\n title: string;\n createdAt: number;\n totalTokens: number;\n inputTokens: number;\n outputTokens: number;\n cacheReadTokens: number;\n turns: number;\n days: Record<string, DayStats>;\n models?: Record<string, { totalTokens: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; turns: number }>;\n}\n\nexport interface SummaryStats {\n totalTokens: number;\n totalInputTokens: number;\n totalOutputTokens: number;\n totalCacheReadTokens: number;\n totalTurns: number;\n activeDays: number;\n currentStreak: number;\n longestStreak: number;\n peakDay: { date: string; tokens: number; turns: number } | null;\n dailyAverage: number;\n}\n\nexport interface HeatmapResponse {\n summary: SummaryStats;\n days: Record<string, DayStats>;\n sessions: SessionSummary[];\n models: ModelUsage[];\n startDate: string;\n endDate: string;\n}\n\ninterface CacheEntry {\n mtimeMs: number;\n session: SessionSummary;\n}\n\nconst fileCache = new Map<string, CacheEntry>();\n\nfunction findSessionFiles(dir: string): string[] {\n const results: string[] = [];\n try {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n results.push(...findSessionFiles(fullPath));\n } else if (entry.name.endsWith(\".jsonl.zstd\") || entry.name.endsWith(\".jsonl\")) {\n results.push(fullPath);\n }\n }\n } catch {\n // Ignore unreadable dirs\n }\n return results;\n}\n\nasync function parseSessionFile(filePath: string): Promise<SessionSummary | null> {\n try {\n const stat = fs.statSync(filePath);\n const cached = fileCache.get(filePath);\n if (cached && cached.mtimeMs === stat.mtimeMs) {\n return cached.session;\n }\n\n const isZstd = filePath.endsWith(\".zstd\");\n const days: Record<string, DayStats> = {};\n const sessionModels: Record<string, { totalTokens: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; turns: number }> = {};\n let sessionId = path.basename(path.dirname(filePath));\n let title = \"\";\n let createdAt = stat.birthtimeMs || stat.mtimeMs;\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n\n await new Promise<void>((resolve) => {\n let buffer = \"\";\n const handleLine = (line: string) => {\n if (line.includes('\"type\":\"session\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.id) sessionId = obj.id;\n if (obj.createdAt) createdAt = obj.createdAt;\n } catch {}\n }\n if (!title && line.includes('\"session/title\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.title) title = obj.data.title;\n } catch {}\n }\n if (!title && line.includes('\"subagent/descriptor\"')) {\n try {\n const obj = JSON.parse(line);\n if (obj.data?.label) title = obj.data.label;\n } catch {}\n }\n if (line.includes('\"usage\"')) {\n try {\n const obj = JSON.parse(line);\n const time = obj.time || (obj.data && obj.data.time);\n const usage = obj.usage || (obj.data && obj.data.usage);\n if (time && usage) {\n const d = new Date(time);\n const dateKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n const input = Number(usage.inputTokens) || 0;\n const output = Number(usage.outputTokens) || 0;\n const cacheRead = Number(usage.cacheReadTokens) || 0;\n const cacheWrite = Number(usage.cacheWriteTokens) || 0;\n const total = input + output + cacheRead;\n const model = obj.data?.message?.source?.model || obj.data?.model || obj.model || \"gemini-3.8-flash-tiered\";\n\n if (!days[dateKey]) {\n days[dateKey] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n };\n }\n days[dateKey].totalTokens += total;\n days[dateKey].inputTokens += input;\n days[dateKey].outputTokens += output;\n days[dateKey].cacheReadTokens += cacheRead;\n days[dateKey].cacheWriteTokens += cacheWrite;\n days[dateKey].turns += 1;\n\n if (!sessionModels[model]) {\n sessionModels[model] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n sessionModels[model].totalTokens += total;\n sessionModels[model].inputTokens += input;\n sessionModels[model].outputTokens += output;\n sessionModels[model].cacheReadTokens += cacheRead;\n sessionModels[model].turns += 1;\n\n totalTokens += total;\n totalInputTokens += input;\n totalOutputTokens += output;\n totalCacheReadTokens += cacheRead;\n totalTurns += 1;\n }\n } catch {\n // Ignore malformed lines\n }\n }\n };\n\n if (isZstd) {\n const proc = spawn(\"zstd\", [\"-dc\", filePath], { stdio: [\"ignore\", \"pipe\", \"ignore\"] });\n proc.stdout.on(\"data\", (chunk: Buffer) => {\n buffer += chunk.toString(\"utf8\");\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n proc.on(\"close\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n proc.on(\"error\", () => resolve());\n } else {\n const stream = fs.createReadStream(filePath, { encoding: \"utf8\" });\n stream.on(\"data\", (chunk: string) => {\n buffer += chunk;\n let idx: number;\n while ((idx = buffer.indexOf(\"\\n\")) !== -1) {\n const line = buffer.slice(0, idx);\n buffer = buffer.slice(idx + 1);\n handleLine(line);\n }\n });\n stream.on(\"end\", () => {\n if (buffer.trim().length > 0) handleLine(buffer);\n resolve();\n });\n stream.on(\"error\", () => resolve());\n }\n });\n\n const session: SessionSummary = {\n id: sessionId,\n title: title || `Session ${sessionId.slice(0, 8)}`,\n createdAt,\n totalTokens,\n inputTokens: totalInputTokens,\n outputTokens: totalOutputTokens,\n cacheReadTokens: totalCacheReadTokens,\n turns: totalTurns,\n days,\n models: sessionModels,\n };\n\n fileCache.set(filePath, { mtimeMs: stat.mtimeMs, session });\n return session;\n } catch {\n return null;\n }\n}\n\nexport async function aggregateTokens(customSessionsRoot?: string): Promise<HeatmapResponse> {\n const sessionsRoot = customSessionsRoot || path.join(os.homedir(), \".dsh\", \"sessions\");\n const files = findSessionFiles(sessionsRoot);\n const parsedSessions = (await Promise.all(files.map(parseSessionFile))).filter(Boolean) as SessionSummary[];\n\n // Sort sessions newest first\n parsedSessions.sort((a, b) => b.createdAt - a.createdAt);\n\n const days: Record<string, DayStats> = {};\n const allModelsMap: Record<string, { totalTokens: number; inputTokens: number; outputTokens: number; cacheReadTokens: number; turns: number }> = {};\n\n for (const session of parsedSessions) {\n for (const [date, stats] of Object.entries(session.days)) {\n if (!days[date]) {\n days[date] = {\n totalTokens: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheReadTokens: 0,\n cacheWriteTokens: 0,\n turns: 0,\n sessions: [],\n };\n }\n days[date].totalTokens += stats.totalTokens;\n days[date].inputTokens += stats.inputTokens;\n days[date].outputTokens += stats.outputTokens;\n days[date].cacheReadTokens += stats.cacheReadTokens;\n days[date].cacheWriteTokens += stats.cacheWriteTokens;\n days[date].turns += stats.turns;\n\n days[date].sessions!.push({\n sessionId: session.id,\n sessionTitle: session.title,\n createdAt: session.createdAt,\n totalTokens: stats.totalTokens,\n inputTokens: stats.inputTokens,\n outputTokens: stats.outputTokens,\n cacheReadTokens: stats.cacheReadTokens,\n turns: stats.turns,\n });\n }\n\n if (session.models) {\n for (const [modelName, mStats] of Object.entries(session.models)) {\n if (!allModelsMap[modelName]) {\n allModelsMap[modelName] = { totalTokens: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, turns: 0 };\n }\n allModelsMap[modelName].totalTokens += mStats.totalTokens;\n allModelsMap[modelName].inputTokens += mStats.inputTokens;\n allModelsMap[modelName].outputTokens += mStats.outputTokens;\n allModelsMap[modelName].cacheReadTokens += mStats.cacheReadTokens;\n allModelsMap[modelName].turns += mStats.turns;\n }\n }\n }\n\n // Sort sessions within each day by totalTokens descending\n for (const day of Object.values(days)) {\n if (day.sessions) {\n day.sessions.sort((a, b) => b.totalTokens - a.totalTokens);\n }\n }\n\n // Calculate Levels (0..4 intensity scale based on positive token days)\n const positiveValues = Object.values(days)\n .map((d) => d.totalTokens)\n .filter((t) => t > 0)\n .sort((a, b) => a - b);\n\n let q1 = 5_000_000;\n let q2 = 25_000_000;\n let q3 = 75_000_000;\n\n if (positiveValues.length >= 4) {\n q1 = positiveValues[Math.floor(positiveValues.length * 0.25)];\n q2 = positiveValues[Math.floor(positiveValues.length * 0.50)];\n q3 = positiveValues[Math.floor(positiveValues.length * 0.75)];\n }\n\n for (const day of Object.values(days)) {\n if (day.totalTokens === 0) day.level = 0;\n else if (day.totalTokens <= q1) day.level = 1;\n else if (day.totalTokens <= q2) day.level = 2;\n else if (day.totalTokens <= q3) day.level = 3;\n else day.level = 4;\n }\n\n // Calculate Summary & Streaks\n let totalTokens = 0;\n let totalInputTokens = 0;\n let totalOutputTokens = 0;\n let totalCacheReadTokens = 0;\n let totalTurns = 0;\n let peakDay: { date: string; tokens: number; turns: number } | null = null;\n\n for (const [date, stats] of Object.entries(days)) {\n totalTokens += stats.totalTokens;\n totalInputTokens += stats.inputTokens;\n totalOutputTokens += stats.outputTokens;\n totalCacheReadTokens += stats.cacheReadTokens;\n totalTurns += stats.turns;\n\n if (!peakDay || stats.totalTokens > peakDay.tokens) {\n peakDay = { date, tokens: stats.totalTokens, turns: stats.turns };\n }\n }\n\n // Build sorted model breakdown array\n const models: ModelUsage[] = Object.entries(allModelsMap)\n .map(([model, m]) => ({\n model,\n totalTokens: m.totalTokens,\n inputTokens: m.inputTokens,\n outputTokens: m.outputTokens,\n cacheReadTokens: m.cacheReadTokens,\n turns: m.turns,\n percentage: totalTokens > 0 ? Math.round((m.totalTokens / totalTokens) * 1000) / 10 : 0,\n }))\n .sort((a, b) => b.totalTokens - a.totalTokens);\n\n const activeDays = Object.values(days).filter((d) => d.totalTokens > 0).length;\n const dailyAverage = activeDays > 0 ? Math.round(totalTokens / activeDays) : 0;\n\n // Streak calculation (iterating backwards from today)\n const now = new Date();\n const formatDate = (d: Date) =>\n `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, \"0\")}-${String(d.getDate()).padStart(2, \"0\")}`;\n\n let currentStreak = 0;\n let cursor = new Date(now);\n const todayKey = formatDate(cursor);\n cursor.setDate(cursor.getDate() - 1);\n const yesterdayKey = formatDate(cursor);\n\n let streakDate = (days[todayKey]?.totalTokens ?? 0) > 0 ? new Date(now) : (days[yesterdayKey]?.totalTokens ?? 0) > 0 ? new Date(cursor) : null;\n\n if (streakDate) {\n while (true) {\n const key = formatDate(streakDate);\n if ((days[key]?.totalTokens ?? 0) > 0) {\n currentStreak++;\n streakDate.setDate(streakDate.getDate() - 1);\n } else {\n break;\n }\n }\n }\n\n // Longest streak\n const sortedDates = Object.keys(days)\n .filter((k) => days[k].totalTokens > 0)\n .sort();\n\n let longestStreak = 0;\n let tempStreak = 0;\n let prevDate: Date | null = null;\n\n for (const dateStr of sortedDates) {\n const cur = new Date(dateStr + \"T00:00:00\");\n if (!prevDate) {\n tempStreak = 1;\n } else {\n const diffDays = Math.round((cur.getTime() - prevDate.getTime()) / (1000 * 60 * 60 * 24));\n if (diffDays === 1) {\n tempStreak++;\n } else {\n tempStreak = 1;\n }\n }\n if (tempStreak > longestStreak) longestStreak = tempStreak;\n prevDate = cur;\n }\n\n const endDate = formatDate(now);\n const start = new Date(now);\n start.setDate(start.getDate() - 364);\n const startDate = formatDate(start);\n\n return {\n summary: {\n totalTokens,\n totalInputTokens,\n totalOutputTokens,\n totalCacheReadTokens,\n totalTurns,\n activeDays,\n currentStreak,\n longestStreak: Math.max(longestStreak, currentStreak),\n peakDay,\n dailyAverage,\n },\n days,\n sessions: parsedSessions,\n models,\n startDate,\n endDate,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|