tokenleak 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -53
- package/package.json +4 -4
- package/{tokenleak.js → tokenleak} +398 -83
package/README.md
CHANGED
|
@@ -134,24 +134,24 @@ tokenleak --format json --upload gist
|
|
|
134
134
|
|
|
135
135
|
## All flags
|
|
136
136
|
|
|
137
|
-
| Flag
|
|
138
|
-
|
|
139
|
-
| `--format`
|
|
140
|
-
| `--theme`
|
|
141
|
-
| `--since`
|
|
142
|
-
| `--until`
|
|
143
|
-
| `--days`
|
|
144
|
-
| `--output`
|
|
145
|
-
| `--width`
|
|
146
|
-
| `--no-color`
|
|
147
|
-
| `--no-insights` |
|
|
148
|
-
| `--compare`
|
|
149
|
-
| `--provider`
|
|
150
|
-
| `--clipboard`
|
|
151
|
-
| `--open`
|
|
152
|
-
| `--upload`
|
|
153
|
-
| `--version`
|
|
154
|
-
| `--help`
|
|
137
|
+
| Flag | Alias | Default | Description |
|
|
138
|
+
| --------------- | ----- | ---------- | --------------------------------------------------------------- |
|
|
139
|
+
| `--format` | `-f` | `terminal` | Output format: `json`, `svg`, `png`, `terminal` |
|
|
140
|
+
| `--theme` | `-t` | `dark` | Colour theme: `dark`, `light` |
|
|
141
|
+
| `--since` | `-s` | | Start date (`YYYY-MM-DD`). Overrides `--days` |
|
|
142
|
+
| `--until` | `-u` | today | End date (`YYYY-MM-DD`) |
|
|
143
|
+
| `--days` | `-d` | `90` | Number of days to look back |
|
|
144
|
+
| `--output` | `-o` | stdout | Output file path. Format is inferred from extension |
|
|
145
|
+
| `--width` | `-w` | `80` | Terminal width for dashboard layout |
|
|
146
|
+
| `--no-color` | | `false` | Strip ANSI escape codes from terminal output |
|
|
147
|
+
| `--no-insights` | | `false` | Hide the insights panel |
|
|
148
|
+
| `--compare` | | | Compare two date ranges. Use `auto` or `YYYY-MM-DD..YYYY-MM-DD` |
|
|
149
|
+
| `--provider` | `-p` | all | Filter to specific provider(s), comma-separated |
|
|
150
|
+
| `--clipboard` | | `false` | Copy output to clipboard after rendering |
|
|
151
|
+
| `--open` | | `false` | Open output file in default app (requires `--output`) |
|
|
152
|
+
| `--upload` | | | Upload output to a service. Supported: `gist` |
|
|
153
|
+
| `--version` | | | Print version number |
|
|
154
|
+
| `--help` | | | Print usage information |
|
|
155
155
|
|
|
156
156
|
## Supported providers
|
|
157
157
|
|
|
@@ -159,30 +159,30 @@ tokenleak --format json --upload gist
|
|
|
159
159
|
|
|
160
160
|
Reads JSONL conversation logs from the Claude Code projects directory. Each assistant message with a `usage` field is parsed for input/output/cache token counts.
|
|
161
161
|
|
|
162
|
-
|
|
|
163
|
-
|
|
164
|
-
| **Data location** | `~/.claude/projects/*/*.jsonl`
|
|
165
|
-
| **Override**
|
|
166
|
-
| **Provider name** | `claude-code`
|
|
162
|
+
| | |
|
|
163
|
+
| ----------------- | -------------------------------------------- |
|
|
164
|
+
| **Data location** | `~/.claude/projects/*/*.jsonl` |
|
|
165
|
+
| **Override** | Set `CLAUDE_CONFIG_DIR` environment variable |
|
|
166
|
+
| **Provider name** | `claude-code` |
|
|
167
167
|
|
|
168
168
|
### Codex
|
|
169
169
|
|
|
170
170
|
Reads JSONL session logs from the Codex sessions directory. Parses `response` events for token usage with cumulative delta extraction.
|
|
171
171
|
|
|
172
|
-
|
|
|
173
|
-
|
|
174
|
-
| **Data location** | `~/.codex/sessions/*.jsonl`
|
|
175
|
-
| **Override**
|
|
176
|
-
| **Provider name** | `codex`
|
|
172
|
+
| | |
|
|
173
|
+
| ----------------- | ------------------------------------- |
|
|
174
|
+
| **Data location** | `~/.codex/sessions/*.jsonl` |
|
|
175
|
+
| **Override** | Set `CODEX_HOME` environment variable |
|
|
176
|
+
| **Provider name** | `codex` |
|
|
177
177
|
|
|
178
178
|
### Open Code
|
|
179
179
|
|
|
180
180
|
Reads usage data from the Open Code SQLite database. Falls back to legacy JSON session files if no database is found.
|
|
181
181
|
|
|
182
|
-
|
|
|
183
|
-
|
|
184
|
-
| **Data location** | `~/.opencode/
|
|
185
|
-
| **Provider name** | `open-code`
|
|
182
|
+
| | |
|
|
183
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
184
|
+
| **Data location** | `~/.local/share/opencode/storage/message/<session>/*.json` (primary), `~/.opencode/opencode.db` or `~/.opencode/sessions.db` (legacy), `~/.opencode/sessions/*.json` (legacy fallback) |
|
|
185
|
+
| **Provider name** | `open-code` |
|
|
186
186
|
|
|
187
187
|
## Output formats
|
|
188
188
|
|
|
@@ -219,24 +219,30 @@ Structured JSON output containing:
|
|
|
219
219
|
"cacheReadTokens": 2000,
|
|
220
220
|
"cacheWriteTokens": 500,
|
|
221
221
|
"totalTokens": 22500,
|
|
222
|
-
"cost": 0.0825
|
|
223
|
-
}
|
|
222
|
+
"cost": 0.0825,
|
|
223
|
+
},
|
|
224
224
|
// ...
|
|
225
225
|
],
|
|
226
226
|
"models": [
|
|
227
|
-
{
|
|
227
|
+
{
|
|
228
|
+
"model": "claude-sonnet-4",
|
|
229
|
+
"inputTokens": 10000,
|
|
230
|
+
"outputTokens": 3000,
|
|
231
|
+
"totalTokens": 13000,
|
|
232
|
+
"cost": 0.075,
|
|
233
|
+
},
|
|
228
234
|
],
|
|
229
235
|
"totalTokens": 22500,
|
|
230
|
-
"totalCost": 0.0825
|
|
231
|
-
}
|
|
236
|
+
"totalCost": 0.0825,
|
|
237
|
+
},
|
|
232
238
|
],
|
|
233
239
|
"aggregated": {
|
|
234
240
|
"currentStreak": 12,
|
|
235
241
|
"longestStreak": 45,
|
|
236
242
|
"totalTokens": 1500000,
|
|
237
|
-
"totalCost": 52.
|
|
243
|
+
"totalCost": 52.5,
|
|
238
244
|
// ... rolling windows, peaks, averages, day-of-week, top models
|
|
239
|
-
}
|
|
245
|
+
},
|
|
240
246
|
}
|
|
241
247
|
```
|
|
242
248
|
|
|
@@ -274,14 +280,14 @@ All fields are optional. Only include the ones you want to override.
|
|
|
274
280
|
|
|
275
281
|
## Environment variables
|
|
276
282
|
|
|
277
|
-
| Variable
|
|
278
|
-
|
|
279
|
-
| `TOKENLEAK_FORMAT`
|
|
280
|
-
| `TOKENLEAK_THEME`
|
|
281
|
-
| `TOKENLEAK_DAYS`
|
|
283
|
+
| Variable | Default | Description |
|
|
284
|
+
| ---------------------------------- | ------------------ | ------------------------------------------------------- |
|
|
285
|
+
| `TOKENLEAK_FORMAT` | `terminal` | Default output format |
|
|
286
|
+
| `TOKENLEAK_THEME` | `dark` | Default colour theme |
|
|
287
|
+
| `TOKENLEAK_DAYS` | `90` | Default lookback period in days |
|
|
282
288
|
| `TOKENLEAK_MAX_JSONL_RECORD_BYTES` | `10485760` (10 MB) | Max size of a single JSONL record before it is rejected |
|
|
283
|
-
| `CLAUDE_CONFIG_DIR`
|
|
284
|
-
| `CODEX_HOME`
|
|
289
|
+
| `CLAUDE_CONFIG_DIR` | `~/.claude` | Claude Code configuration directory |
|
|
290
|
+
| `CODEX_HOME` | `~/.codex` | Codex home directory |
|
|
285
291
|
|
|
286
292
|
## What Tokenleak tracks
|
|
287
293
|
|
|
@@ -309,13 +315,13 @@ It then computes:
|
|
|
309
315
|
|
|
310
316
|
Tokenleak includes pricing for these model families:
|
|
311
317
|
|
|
312
|
-
| Family
|
|
313
|
-
|
|
314
|
-
| Claude 3
|
|
315
|
-
| Claude 3.5 | `claude-3.5-haiku`, `claude-3.5-sonnet`
|
|
316
|
-
| Claude 4
|
|
317
|
-
| GPT-4o
|
|
318
|
-
| o-series
|
|
318
|
+
| Family | Models |
|
|
319
|
+
| ---------- | ---------------------------------------------------- |
|
|
320
|
+
| Claude 3 | `claude-3-haiku`, `claude-3-sonnet`, `claude-3-opus` |
|
|
321
|
+
| Claude 3.5 | `claude-3.5-haiku`, `claude-3.5-sonnet` |
|
|
322
|
+
| Claude 4 | `claude-sonnet-4`, `claude-opus-4` |
|
|
323
|
+
| GPT-4o | `gpt-4o`, `gpt-4o-mini` |
|
|
324
|
+
| o-series | `o1`, `o1-mini`, `o3`, `o3-mini`, `o4-mini` |
|
|
319
325
|
|
|
320
326
|
Model names with date suffixes (e.g. `claude-sonnet-4-20250514`) are automatically normalised. Unknown models show `$0.00` cost but tokens are still tracked.
|
|
321
327
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenleak",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Visualise your AI coding-assistant token usage across providers — heatmaps, dashboards, and shareable cards.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tokenleak": "
|
|
7
|
+
"tokenleak": "tokenleak"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"tokenleak
|
|
10
|
+
"tokenleak"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"sharp": "^0.34.0"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/ya-nsh/tokenleak.git"
|
|
31
|
+
"url": "git+https://github.com/ya-nsh/tokenleak.git"
|
|
32
32
|
},
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"author": "ya-nsh"
|
|
@@ -580,7 +580,7 @@ function topModels(daily, limit = DEFAULT_LIMIT) {
|
|
|
580
580
|
model,
|
|
581
581
|
tokens,
|
|
582
582
|
cost,
|
|
583
|
-
percentage: grandTotal > 0 ? tokens / grandTotal : 0
|
|
583
|
+
percentage: grandTotal > 0 ? tokens / grandTotal * 100 : 0
|
|
584
584
|
});
|
|
585
585
|
}
|
|
586
586
|
entries.sort((a, b) => b.tokens - a.tokens);
|
|
@@ -756,7 +756,7 @@ function computePreviousPeriod(current) {
|
|
|
756
756
|
};
|
|
757
757
|
}
|
|
758
758
|
// packages/core/dist/index.js
|
|
759
|
-
var VERSION = "1.0.
|
|
759
|
+
var VERSION = "1.0.1";
|
|
760
760
|
|
|
761
761
|
// packages/registry/dist/models/normalizer.js
|
|
762
762
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -850,6 +850,54 @@ var MODEL_PRICING = {
|
|
|
850
850
|
cacheRead: 0.075,
|
|
851
851
|
cacheWrite: 0.15
|
|
852
852
|
},
|
|
853
|
+
"gpt-5": {
|
|
854
|
+
input: 1.25,
|
|
855
|
+
output: 10,
|
|
856
|
+
cacheRead: 0.125,
|
|
857
|
+
cacheWrite: 1.25
|
|
858
|
+
},
|
|
859
|
+
"gpt-5.1": {
|
|
860
|
+
input: 1.25,
|
|
861
|
+
output: 10,
|
|
862
|
+
cacheRead: 0.125,
|
|
863
|
+
cacheWrite: 1.25
|
|
864
|
+
},
|
|
865
|
+
"gpt-5.2": {
|
|
866
|
+
input: 1.75,
|
|
867
|
+
output: 14,
|
|
868
|
+
cacheRead: 0.175,
|
|
869
|
+
cacheWrite: 1.75
|
|
870
|
+
},
|
|
871
|
+
"gpt-5.4": {
|
|
872
|
+
input: 2.5,
|
|
873
|
+
output: 15,
|
|
874
|
+
cacheRead: 0.25,
|
|
875
|
+
cacheWrite: 2.5
|
|
876
|
+
},
|
|
877
|
+
"gpt-5-codex": {
|
|
878
|
+
input: 1.25,
|
|
879
|
+
output: 10,
|
|
880
|
+
cacheRead: 0.125,
|
|
881
|
+
cacheWrite: 1.25
|
|
882
|
+
},
|
|
883
|
+
"gpt-5.1-codex": {
|
|
884
|
+
input: 1.25,
|
|
885
|
+
output: 10,
|
|
886
|
+
cacheRead: 0.125,
|
|
887
|
+
cacheWrite: 1.25
|
|
888
|
+
},
|
|
889
|
+
"gpt-5.1-codex-max": {
|
|
890
|
+
input: 1.25,
|
|
891
|
+
output: 10,
|
|
892
|
+
cacheRead: 0.125,
|
|
893
|
+
cacheWrite: 1.25
|
|
894
|
+
},
|
|
895
|
+
"gpt-5.2-codex": {
|
|
896
|
+
input: 1.75,
|
|
897
|
+
output: 14,
|
|
898
|
+
cacheRead: 0.175,
|
|
899
|
+
cacheWrite: 1.75
|
|
900
|
+
},
|
|
853
901
|
o1: {
|
|
854
902
|
input: 15,
|
|
855
903
|
output: 60,
|
|
@@ -983,12 +1031,19 @@ function isInRange(date, range) {
|
|
|
983
1031
|
}
|
|
984
1032
|
|
|
985
1033
|
// packages/registry/dist/providers/claude-code.js
|
|
986
|
-
var
|
|
1034
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".claude");
|
|
987
1035
|
var CLAUDE_CODE_COLORS = {
|
|
988
1036
|
primary: "#ff6b35",
|
|
989
1037
|
secondary: "#ffa366",
|
|
990
1038
|
gradient: ["#ff6b35", "#ffa366"]
|
|
991
1039
|
};
|
|
1040
|
+
function resolveBaseDir(baseDir) {
|
|
1041
|
+
if (baseDir) {
|
|
1042
|
+
return baseDir;
|
|
1043
|
+
}
|
|
1044
|
+
const configDir = process.env["CLAUDE_CONFIG_DIR"];
|
|
1045
|
+
return join(configDir && configDir.length > 0 ? configDir : DEFAULT_CONFIG_DIR, "projects");
|
|
1046
|
+
}
|
|
992
1047
|
function collectJsonlFiles(dir) {
|
|
993
1048
|
const results = [];
|
|
994
1049
|
if (!existsSync(dir)) {
|
|
@@ -1036,6 +1091,10 @@ function extractUsage(record) {
|
|
|
1036
1091
|
const outputTokens = typeof u["output_tokens"] === "number" ? u["output_tokens"] : 0;
|
|
1037
1092
|
const cacheReadTokens = typeof u["cache_read_input_tokens"] === "number" ? u["cache_read_input_tokens"] : 0;
|
|
1038
1093
|
const cacheWriteTokens = typeof u["cache_creation_input_tokens"] === "number" ? u["cache_creation_input_tokens"] : 0;
|
|
1094
|
+
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1095
|
+
if (totalTokens === 0) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1039
1098
|
const date = timestamp.slice(0, 10);
|
|
1040
1099
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
1041
1100
|
return null;
|
|
@@ -1046,7 +1105,8 @@ function extractUsage(record) {
|
|
|
1046
1105
|
inputTokens,
|
|
1047
1106
|
outputTokens,
|
|
1048
1107
|
cacheReadTokens,
|
|
1049
|
-
cacheWriteTokens
|
|
1108
|
+
cacheWriteTokens,
|
|
1109
|
+
messageId: typeof msg["id"] === "string" ? msg["id"] : undefined
|
|
1050
1110
|
};
|
|
1051
1111
|
}
|
|
1052
1112
|
function buildDailyUsage(records) {
|
|
@@ -1109,7 +1169,7 @@ class ClaudeCodeProvider {
|
|
|
1109
1169
|
colors = CLAUDE_CODE_COLORS;
|
|
1110
1170
|
baseDir;
|
|
1111
1171
|
constructor(baseDir) {
|
|
1112
|
-
this.baseDir = baseDir
|
|
1172
|
+
this.baseDir = resolveBaseDir(baseDir);
|
|
1113
1173
|
}
|
|
1114
1174
|
async isAvailable() {
|
|
1115
1175
|
try {
|
|
@@ -1122,16 +1182,23 @@ class ClaudeCodeProvider {
|
|
|
1122
1182
|
const files = collectJsonlFiles(this.baseDir);
|
|
1123
1183
|
const allRecords = [];
|
|
1124
1184
|
for (const file of files) {
|
|
1185
|
+
const latestRecordsByMessageId = new Map;
|
|
1186
|
+
const anonymousRecords = [];
|
|
1125
1187
|
try {
|
|
1126
1188
|
for await (const record of splitJsonlRecords(file)) {
|
|
1127
1189
|
const usage = extractUsage(record);
|
|
1128
1190
|
if (usage !== null && isInRange(usage.date, range)) {
|
|
1129
|
-
|
|
1191
|
+
if (usage.messageId) {
|
|
1192
|
+
latestRecordsByMessageId.set(usage.messageId, usage);
|
|
1193
|
+
} else {
|
|
1194
|
+
anonymousRecords.push(usage);
|
|
1195
|
+
}
|
|
1130
1196
|
}
|
|
1131
1197
|
}
|
|
1132
1198
|
} catch {
|
|
1133
1199
|
continue;
|
|
1134
1200
|
}
|
|
1201
|
+
allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
|
|
1135
1202
|
}
|
|
1136
1203
|
const daily = buildDailyUsage(allRecords);
|
|
1137
1204
|
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
@@ -1147,7 +1214,7 @@ class ClaudeCodeProvider {
|
|
|
1147
1214
|
}
|
|
1148
1215
|
}
|
|
1149
1216
|
// packages/registry/dist/providers/codex.js
|
|
1150
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
1217
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1151
1218
|
import { join as join2 } from "path";
|
|
1152
1219
|
import { homedir as homedir2 } from "os";
|
|
1153
1220
|
var CODEX_COLORS = {
|
|
@@ -1155,7 +1222,7 @@ var CODEX_COLORS = {
|
|
|
1155
1222
|
secondary: "#4ade80",
|
|
1156
1223
|
gradient: ["#10a37f", "#4ade80"]
|
|
1157
1224
|
};
|
|
1158
|
-
var DEFAULT_SESSIONS_DIR = join2(homedir2(), ".codex", "sessions");
|
|
1225
|
+
var DEFAULT_SESSIONS_DIR = join2(process.env["CODEX_HOME"] ?? join2(homedir2(), ".codex"), "sessions");
|
|
1159
1226
|
function parseResponseEvent(record) {
|
|
1160
1227
|
if (typeof record !== "object" || record === null || !("type" in record)) {
|
|
1161
1228
|
return null;
|
|
@@ -1190,6 +1257,159 @@ function extractDate(timestamp) {
|
|
|
1190
1257
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
1191
1258
|
return match ? match[1] : null;
|
|
1192
1259
|
}
|
|
1260
|
+
function collectJsonlFiles2(dir) {
|
|
1261
|
+
if (!existsSync2(dir)) {
|
|
1262
|
+
return [];
|
|
1263
|
+
}
|
|
1264
|
+
const files = [];
|
|
1265
|
+
for (const entry of readdirSync2(dir)) {
|
|
1266
|
+
const fullPath = join2(dir, entry);
|
|
1267
|
+
const stats = statSync2(fullPath);
|
|
1268
|
+
if (stats.isDirectory()) {
|
|
1269
|
+
files.push(...collectJsonlFiles2(fullPath));
|
|
1270
|
+
} else if (entry.endsWith(".jsonl")) {
|
|
1271
|
+
files.push(fullPath);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return files;
|
|
1275
|
+
}
|
|
1276
|
+
function inferModelFromContext(record) {
|
|
1277
|
+
if (typeof record !== "object" || record === null) {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
const obj = record;
|
|
1281
|
+
if (obj["type"] !== "session_meta" && obj["type"] !== "turn_context") {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
const payload = obj["payload"];
|
|
1285
|
+
if (typeof payload !== "object" || payload === null) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
const meta = payload;
|
|
1289
|
+
const directModelKeys = ["model", "model_name", "model_slug"];
|
|
1290
|
+
for (const key of directModelKeys) {
|
|
1291
|
+
if (typeof meta[key] === "string" && meta[key].trim()) {
|
|
1292
|
+
return meta[key].trim();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
const instructions = meta["base_instructions"];
|
|
1296
|
+
if (typeof instructions === "object" && instructions !== null) {
|
|
1297
|
+
const text = instructions["text"];
|
|
1298
|
+
if (typeof text === "string") {
|
|
1299
|
+
const match = /based on ([A-Za-z0-9.-]+)/i.exec(text);
|
|
1300
|
+
if (match?.[1]) {
|
|
1301
|
+
return match[1].toLowerCase();
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
function parseTokenCountUsage(record, context) {
|
|
1308
|
+
if (typeof record !== "object" || record === null) {
|
|
1309
|
+
return null;
|
|
1310
|
+
}
|
|
1311
|
+
const obj = record;
|
|
1312
|
+
if (obj["type"] !== "event_msg") {
|
|
1313
|
+
return null;
|
|
1314
|
+
}
|
|
1315
|
+
const timestamp = obj["timestamp"];
|
|
1316
|
+
const payload = obj["payload"];
|
|
1317
|
+
if (typeof timestamp !== "string" || typeof payload !== "object" || payload === null) {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
const eventPayload = payload;
|
|
1321
|
+
if (eventPayload["type"] !== "token_count") {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const info = eventPayload["info"];
|
|
1325
|
+
if (typeof info !== "object" || info === null) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
const usageInfo = info;
|
|
1329
|
+
const lastUsage = usageInfo["last_token_usage"];
|
|
1330
|
+
const totalUsage = usageInfo["total_token_usage"];
|
|
1331
|
+
const date = extractDate(timestamp);
|
|
1332
|
+
if (!date) {
|
|
1333
|
+
return null;
|
|
1334
|
+
}
|
|
1335
|
+
const parseUsage = (usage2) => {
|
|
1336
|
+
if (typeof usage2 !== "object" || usage2 === null) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
const usageObj = usage2;
|
|
1340
|
+
const inputTokens2 = usageObj["input_tokens"];
|
|
1341
|
+
const outputTokens = usageObj["output_tokens"];
|
|
1342
|
+
const cachedInputTokens = usageObj["cached_input_tokens"];
|
|
1343
|
+
if (typeof inputTokens2 !== "number" || typeof outputTokens !== "number") {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
return {
|
|
1347
|
+
inputTokens: inputTokens2,
|
|
1348
|
+
outputTokens,
|
|
1349
|
+
cachedInputTokens: typeof cachedInputTokens === "number" ? cachedInputTokens : 0
|
|
1350
|
+
};
|
|
1351
|
+
};
|
|
1352
|
+
let usage = parseUsage(lastUsage);
|
|
1353
|
+
if (!usage) {
|
|
1354
|
+
const cumulative = parseUsage(totalUsage);
|
|
1355
|
+
if (!cumulative) {
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
const previous = context.previousTotals ?? {
|
|
1359
|
+
inputTokens: 0,
|
|
1360
|
+
outputTokens: 0,
|
|
1361
|
+
cachedInputTokens: 0
|
|
1362
|
+
};
|
|
1363
|
+
usage = {
|
|
1364
|
+
inputTokens: Math.max(0, cumulative.inputTokens - previous.inputTokens),
|
|
1365
|
+
outputTokens: Math.max(0, cumulative.outputTokens - previous.outputTokens),
|
|
1366
|
+
cachedInputTokens: Math.max(0, cumulative.cachedInputTokens - previous.cachedInputTokens)
|
|
1367
|
+
};
|
|
1368
|
+
context.previousTotals = cumulative;
|
|
1369
|
+
} else if (parseUsage(totalUsage)) {
|
|
1370
|
+
context.previousTotals = parseUsage(totalUsage);
|
|
1371
|
+
}
|
|
1372
|
+
const cacheReadTokens = Math.min(usage.cachedInputTokens, usage.inputTokens);
|
|
1373
|
+
const inputTokens = Math.max(0, usage.inputTokens - cacheReadTokens);
|
|
1374
|
+
return {
|
|
1375
|
+
date,
|
|
1376
|
+
model: context.model,
|
|
1377
|
+
inputTokens,
|
|
1378
|
+
outputTokens: usage.outputTokens,
|
|
1379
|
+
cacheReadTokens,
|
|
1380
|
+
cacheWriteTokens: 0
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function parseUsageRecord(record, context) {
|
|
1384
|
+
const inferredModel = inferModelFromContext(record);
|
|
1385
|
+
if (inferredModel) {
|
|
1386
|
+
if (context.model !== inferredModel) {
|
|
1387
|
+
context.model = inferredModel;
|
|
1388
|
+
context.previousTotals = null;
|
|
1389
|
+
}
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
const tokenCountUsage = parseTokenCountUsage(record, context);
|
|
1393
|
+
if (tokenCountUsage) {
|
|
1394
|
+
return tokenCountUsage;
|
|
1395
|
+
}
|
|
1396
|
+
const legacyEvent = parseResponseEvent(record);
|
|
1397
|
+
if (!legacyEvent) {
|
|
1398
|
+
return null;
|
|
1399
|
+
}
|
|
1400
|
+
const date = extractDate(legacyEvent.timestamp);
|
|
1401
|
+
if (!date) {
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
return {
|
|
1405
|
+
date,
|
|
1406
|
+
model: compactModelDateSuffix(legacyEvent.model),
|
|
1407
|
+
inputTokens: legacyEvent.usage.input_tokens,
|
|
1408
|
+
outputTokens: legacyEvent.usage.output_tokens,
|
|
1409
|
+
cacheReadTokens: 0,
|
|
1410
|
+
cacheWriteTokens: 0
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1193
1413
|
|
|
1194
1414
|
class CodexProvider {
|
|
1195
1415
|
name = "codex";
|
|
@@ -1208,34 +1428,31 @@ class CodexProvider {
|
|
|
1208
1428
|
}
|
|
1209
1429
|
async load(range) {
|
|
1210
1430
|
const dailyMap = new Map;
|
|
1211
|
-
|
|
1212
|
-
try {
|
|
1213
|
-
files = readdirSync2(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1214
|
-
} catch {
|
|
1215
|
-
files = [];
|
|
1216
|
-
}
|
|
1431
|
+
const files = collectJsonlFiles2(this.sessionsDir);
|
|
1217
1432
|
for (const file of files) {
|
|
1218
|
-
const
|
|
1433
|
+
const context = {
|
|
1434
|
+
model: "gpt-5",
|
|
1435
|
+
previousTotals: null
|
|
1436
|
+
};
|
|
1219
1437
|
try {
|
|
1220
|
-
for await (const record of splitJsonlRecords(
|
|
1221
|
-
const
|
|
1222
|
-
if (!
|
|
1438
|
+
for await (const record of splitJsonlRecords(file)) {
|
|
1439
|
+
const usage = parseUsageRecord(record, context);
|
|
1440
|
+
if (!usage) {
|
|
1223
1441
|
continue;
|
|
1224
1442
|
}
|
|
1225
|
-
|
|
1226
|
-
if (!date || !isInRange(date, range)) {
|
|
1443
|
+
if (!isInRange(usage.date, range)) {
|
|
1227
1444
|
continue;
|
|
1228
1445
|
}
|
|
1229
|
-
const normalizedModel = normalizeModelName(compactModelDateSuffix(
|
|
1230
|
-
const inputTokens =
|
|
1231
|
-
const outputTokens =
|
|
1232
|
-
const cacheReadTokens =
|
|
1233
|
-
const cacheWriteTokens =
|
|
1446
|
+
const normalizedModel = normalizeModelName(compactModelDateSuffix(usage.model));
|
|
1447
|
+
const inputTokens = usage.inputTokens;
|
|
1448
|
+
const outputTokens = usage.outputTokens;
|
|
1449
|
+
const cacheReadTokens = usage.cacheReadTokens;
|
|
1450
|
+
const cacheWriteTokens = usage.cacheWriteTokens;
|
|
1234
1451
|
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1235
|
-
if (!dailyMap.has(date)) {
|
|
1236
|
-
dailyMap.set(date, new Map);
|
|
1452
|
+
if (!dailyMap.has(usage.date)) {
|
|
1453
|
+
dailyMap.set(usage.date, new Map);
|
|
1237
1454
|
}
|
|
1238
|
-
const modelMap = dailyMap.get(date);
|
|
1455
|
+
const modelMap = dailyMap.get(usage.date);
|
|
1239
1456
|
if (!modelMap.has(normalizedModel)) {
|
|
1240
1457
|
modelMap.set(normalizedModel, {
|
|
1241
1458
|
model: normalizedModel,
|
|
@@ -1302,15 +1519,41 @@ var COLORS = {
|
|
|
1302
1519
|
secondary: "#a78bfa",
|
|
1303
1520
|
gradient: ["#6366f1", "#a78bfa"]
|
|
1304
1521
|
};
|
|
1522
|
+
var CURRENT_DEFAULT_BASE_DIR = join3(homedir3(), ".local", "share", "opencode");
|
|
1523
|
+
var LEGACY_DEFAULT_BASE_DIR = join3(homedir3(), ".opencode");
|
|
1524
|
+
var CONFIG_DEFAULT_BASE_DIR = join3(homedir3(), ".config", "opencode");
|
|
1525
|
+
function resolveBaseDir2(baseDir) {
|
|
1526
|
+
if (baseDir) {
|
|
1527
|
+
return baseDir;
|
|
1528
|
+
}
|
|
1529
|
+
for (const candidate of [
|
|
1530
|
+
CURRENT_DEFAULT_BASE_DIR,
|
|
1531
|
+
LEGACY_DEFAULT_BASE_DIR,
|
|
1532
|
+
CONFIG_DEFAULT_BASE_DIR
|
|
1533
|
+
]) {
|
|
1534
|
+
if (existsSync3(candidate)) {
|
|
1535
|
+
return candidate;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
return CURRENT_DEFAULT_BASE_DIR;
|
|
1539
|
+
}
|
|
1305
1540
|
function extractDate2(createdAt) {
|
|
1306
|
-
|
|
1307
|
-
|
|
1541
|
+
const timestamp = typeof createdAt === "number" ? createdAt : Number.isNaN(Number(createdAt)) ? Date.parse(createdAt) : Number(createdAt);
|
|
1542
|
+
if (!Number.isFinite(timestamp)) {
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
const millis = Math.abs(timestamp) >= 1000000000000 ? timestamp : timestamp * 1000;
|
|
1546
|
+
const date = new Date(millis);
|
|
1547
|
+
if (Number.isNaN(date.getTime())) {
|
|
1548
|
+
return null;
|
|
1308
1549
|
}
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1550
|
+
return date.toISOString().slice(0, 10);
|
|
1551
|
+
}
|
|
1552
|
+
function getRecordCost(record) {
|
|
1553
|
+
if (typeof record.explicitCost === "number" && Number.isFinite(record.explicitCost)) {
|
|
1554
|
+
return record.explicitCost;
|
|
1312
1555
|
}
|
|
1313
|
-
return
|
|
1556
|
+
return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1314
1557
|
}
|
|
1315
1558
|
function buildProviderData(records) {
|
|
1316
1559
|
const byDate = new Map;
|
|
@@ -1322,50 +1565,44 @@ function buildProviderData(records) {
|
|
|
1322
1565
|
}
|
|
1323
1566
|
const normalized = normalizeModelName(record.model);
|
|
1324
1567
|
const existing = dateMap.get(normalized);
|
|
1568
|
+
const recordCost = getRecordCost(record);
|
|
1325
1569
|
if (existing) {
|
|
1326
1570
|
existing.inputTokens += record.inputTokens;
|
|
1327
1571
|
existing.outputTokens += record.outputTokens;
|
|
1572
|
+
existing.cacheReadTokens += record.cacheReadTokens;
|
|
1573
|
+
existing.cacheWriteTokens += record.cacheWriteTokens;
|
|
1574
|
+
existing.totalTokens += record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens;
|
|
1575
|
+
existing.cost += recordCost;
|
|
1328
1576
|
} else {
|
|
1329
1577
|
dateMap.set(normalized, {
|
|
1578
|
+
model: normalized,
|
|
1330
1579
|
inputTokens: record.inputTokens,
|
|
1331
|
-
outputTokens: record.outputTokens
|
|
1580
|
+
outputTokens: record.outputTokens,
|
|
1581
|
+
cacheReadTokens: record.cacheReadTokens,
|
|
1582
|
+
cacheWriteTokens: record.cacheWriteTokens,
|
|
1583
|
+
totalTokens: record.inputTokens + record.outputTokens + record.cacheReadTokens + record.cacheWriteTokens,
|
|
1584
|
+
cost: recordCost
|
|
1332
1585
|
});
|
|
1333
1586
|
}
|
|
1334
1587
|
}
|
|
1335
1588
|
let totalTokens = 0;
|
|
1336
1589
|
let totalCost = 0;
|
|
1337
1590
|
const daily = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, modelMap]) => {
|
|
1338
|
-
const models = [];
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const cacheWriteTokens = 0;
|
|
1346
|
-
const modelTotal = usage.inputTokens + usage.outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1347
|
-
models.push({
|
|
1348
|
-
model,
|
|
1349
|
-
inputTokens: usage.inputTokens,
|
|
1350
|
-
outputTokens: usage.outputTokens,
|
|
1351
|
-
cacheReadTokens,
|
|
1352
|
-
cacheWriteTokens,
|
|
1353
|
-
totalTokens: modelTotal,
|
|
1354
|
-
cost
|
|
1355
|
-
});
|
|
1356
|
-
dayInput += usage.inputTokens;
|
|
1357
|
-
dayOutput += usage.outputTokens;
|
|
1358
|
-
dayCost += cost;
|
|
1359
|
-
}
|
|
1360
|
-
const dayTotal = dayInput + dayOutput;
|
|
1591
|
+
const models = [...modelMap.values()];
|
|
1592
|
+
const inputTokens = models.reduce((sum, model) => sum + model.inputTokens, 0);
|
|
1593
|
+
const outputTokens = models.reduce((sum, model) => sum + model.outputTokens, 0);
|
|
1594
|
+
const cacheReadTokens = models.reduce((sum, model) => sum + model.cacheReadTokens, 0);
|
|
1595
|
+
const cacheWriteTokens = models.reduce((sum, model) => sum + model.cacheWriteTokens, 0);
|
|
1596
|
+
const dayTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1597
|
+
const dayCost = models.reduce((sum, model) => sum + model.cost, 0);
|
|
1361
1598
|
totalTokens += dayTotal;
|
|
1362
1599
|
totalCost += dayCost;
|
|
1363
1600
|
return {
|
|
1364
1601
|
date,
|
|
1365
|
-
inputTokens
|
|
1366
|
-
outputTokens
|
|
1367
|
-
cacheReadTokens
|
|
1368
|
-
cacheWriteTokens
|
|
1602
|
+
inputTokens,
|
|
1603
|
+
outputTokens,
|
|
1604
|
+
cacheReadTokens,
|
|
1605
|
+
cacheWriteTokens,
|
|
1369
1606
|
totalTokens: dayTotal,
|
|
1370
1607
|
cost: dayCost,
|
|
1371
1608
|
models
|
|
@@ -1396,12 +1633,14 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1396
1633
|
const records = [];
|
|
1397
1634
|
for (const row of rows) {
|
|
1398
1635
|
const date = extractDate2(row.created_at);
|
|
1399
|
-
if (isInRange(date, range)) {
|
|
1636
|
+
if (date && isInRange(date, range)) {
|
|
1400
1637
|
records.push({
|
|
1401
1638
|
date,
|
|
1402
1639
|
model: row.model,
|
|
1403
1640
|
inputTokens: row.input_tokens,
|
|
1404
|
-
outputTokens: row.output_tokens
|
|
1641
|
+
outputTokens: row.output_tokens,
|
|
1642
|
+
cacheReadTokens: 0,
|
|
1643
|
+
cacheWriteTokens: 0
|
|
1405
1644
|
});
|
|
1406
1645
|
}
|
|
1407
1646
|
}
|
|
@@ -1412,8 +1651,8 @@ function loadFromSqlite(dbPath, range) {
|
|
|
1412
1651
|
db.close();
|
|
1413
1652
|
}
|
|
1414
1653
|
}
|
|
1415
|
-
function
|
|
1416
|
-
const files = readdirSync3(sessionsDir).filter((
|
|
1654
|
+
function loadFromLegacyJson(sessionsDir, range) {
|
|
1655
|
+
const files = readdirSync3(sessionsDir).filter((file) => file.endsWith(".json"));
|
|
1417
1656
|
const records = [];
|
|
1418
1657
|
for (const file of files) {
|
|
1419
1658
|
try {
|
|
@@ -1427,12 +1666,14 @@ function loadFromJson(sessionsDir, range) {
|
|
|
1427
1666
|
continue;
|
|
1428
1667
|
}
|
|
1429
1668
|
const date = extractDate2(msg.created_at);
|
|
1430
|
-
if (isInRange(date, range)) {
|
|
1669
|
+
if (date && isInRange(date, range)) {
|
|
1431
1670
|
records.push({
|
|
1432
1671
|
date,
|
|
1433
1672
|
model: msg.model,
|
|
1434
1673
|
inputTokens: msg.usage.input_tokens,
|
|
1435
|
-
outputTokens: msg.usage.output_tokens
|
|
1674
|
+
outputTokens: msg.usage.output_tokens,
|
|
1675
|
+
cacheReadTokens: 0,
|
|
1676
|
+
cacheWriteTokens: 0
|
|
1436
1677
|
});
|
|
1437
1678
|
}
|
|
1438
1679
|
}
|
|
@@ -1442,6 +1683,66 @@ function loadFromJson(sessionsDir, range) {
|
|
|
1442
1683
|
}
|
|
1443
1684
|
return records;
|
|
1444
1685
|
}
|
|
1686
|
+
function loadFromCurrentStorage(baseDir, range) {
|
|
1687
|
+
const messagesRoot = join3(baseDir, "storage", "message");
|
|
1688
|
+
if (!existsSync3(messagesRoot)) {
|
|
1689
|
+
return [];
|
|
1690
|
+
}
|
|
1691
|
+
const recordsById = new Map;
|
|
1692
|
+
const recordsWithoutId = [];
|
|
1693
|
+
for (const sessionDir of readdirSync3(messagesRoot)) {
|
|
1694
|
+
const sessionPath = join3(messagesRoot, sessionDir);
|
|
1695
|
+
let messageFiles;
|
|
1696
|
+
try {
|
|
1697
|
+
messageFiles = readdirSync3(sessionPath).filter((file) => file.endsWith(".json"));
|
|
1698
|
+
} catch {
|
|
1699
|
+
continue;
|
|
1700
|
+
}
|
|
1701
|
+
for (const file of messageFiles) {
|
|
1702
|
+
try {
|
|
1703
|
+
const content = readFileSync(join3(sessionPath, file), "utf-8");
|
|
1704
|
+
const message = JSON.parse(content);
|
|
1705
|
+
if (message.role !== "assistant") {
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const model = message.modelID;
|
|
1709
|
+
const createdAt = message.time?.created;
|
|
1710
|
+
if (typeof model !== "string" || typeof createdAt !== "string" && typeof createdAt !== "number") {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
const date = extractDate2(createdAt);
|
|
1714
|
+
if (!date || !isInRange(date, range)) {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const inputTokens = typeof message.tokens?.input === "number" ? message.tokens.input : 0;
|
|
1718
|
+
const outputTokens = typeof message.tokens?.output === "number" ? message.tokens.output : 0;
|
|
1719
|
+
const cacheReadTokens = typeof message.tokens?.cache?.read === "number" ? message.tokens.cache.read : 0;
|
|
1720
|
+
const cacheWriteTokens = typeof message.tokens?.cache?.write === "number" ? message.tokens.cache.write : 0;
|
|
1721
|
+
const record = {
|
|
1722
|
+
date,
|
|
1723
|
+
model,
|
|
1724
|
+
inputTokens,
|
|
1725
|
+
outputTokens,
|
|
1726
|
+
cacheReadTokens,
|
|
1727
|
+
cacheWriteTokens,
|
|
1728
|
+
explicitCost: typeof message.cost === "number" ? message.cost : undefined
|
|
1729
|
+
};
|
|
1730
|
+
const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1731
|
+
if (totalTokens === 0 && !(typeof record.explicitCost === "number" && record.explicitCost > 0)) {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof message.id === "string" && message.id.length > 0) {
|
|
1735
|
+
recordsById.set(message.id, record);
|
|
1736
|
+
} else {
|
|
1737
|
+
recordsWithoutId.push(record);
|
|
1738
|
+
}
|
|
1739
|
+
} catch {
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return [...recordsById.values(), ...recordsWithoutId];
|
|
1745
|
+
}
|
|
1445
1746
|
|
|
1446
1747
|
class OpenCodeProvider {
|
|
1447
1748
|
name = PROVIDER_NAME;
|
|
@@ -1449,30 +1750,37 @@ class OpenCodeProvider {
|
|
|
1449
1750
|
colors = COLORS;
|
|
1450
1751
|
baseDir;
|
|
1451
1752
|
constructor(baseDir) {
|
|
1452
|
-
this.baseDir = baseDir
|
|
1753
|
+
this.baseDir = resolveBaseDir2(baseDir);
|
|
1453
1754
|
}
|
|
1454
1755
|
async isAvailable() {
|
|
1455
1756
|
try {
|
|
1456
1757
|
if (!existsSync3(this.baseDir)) {
|
|
1457
1758
|
return false;
|
|
1458
1759
|
}
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1760
|
+
const hasCurrentStorage = existsSync3(join3(this.baseDir, "storage", "message"));
|
|
1761
|
+
const hasLegacyDb = existsSync3(join3(this.baseDir, "opencode.db")) || existsSync3(join3(this.baseDir, "sessions.db"));
|
|
1762
|
+
const hasLegacySessionsDir = existsSync3(join3(this.baseDir, "sessions"));
|
|
1763
|
+
return hasCurrentStorage || hasLegacyDb || hasLegacySessionsDir;
|
|
1462
1764
|
} catch {
|
|
1463
1765
|
return false;
|
|
1464
1766
|
}
|
|
1465
1767
|
}
|
|
1466
1768
|
async load(range) {
|
|
1467
|
-
const
|
|
1769
|
+
const currentMessagesRoot = join3(this.baseDir, "storage", "message");
|
|
1770
|
+
if (existsSync3(currentMessagesRoot)) {
|
|
1771
|
+
const currentRecords = loadFromCurrentStorage(this.baseDir, range);
|
|
1772
|
+
return buildProviderData(currentRecords);
|
|
1773
|
+
}
|
|
1774
|
+
const opencodeDbPath = join3(this.baseDir, "opencode.db");
|
|
1775
|
+
const sessionsDbPath = join3(this.baseDir, "sessions.db");
|
|
1468
1776
|
const sessionsDir = join3(this.baseDir, "sessions");
|
|
1469
|
-
let records;
|
|
1470
|
-
if (existsSync3(
|
|
1471
|
-
records = loadFromSqlite(
|
|
1777
|
+
let records = [];
|
|
1778
|
+
if (existsSync3(opencodeDbPath)) {
|
|
1779
|
+
records = loadFromSqlite(opencodeDbPath, range);
|
|
1780
|
+
} else if (existsSync3(sessionsDbPath)) {
|
|
1781
|
+
records = loadFromSqlite(sessionsDbPath, range);
|
|
1472
1782
|
} else if (existsSync3(sessionsDir)) {
|
|
1473
|
-
records =
|
|
1474
|
-
} else {
|
|
1475
|
-
records = [];
|
|
1783
|
+
records = loadFromLegacyJson(sessionsDir, range);
|
|
1476
1784
|
}
|
|
1477
1785
|
return buildProviderData(records);
|
|
1478
1786
|
}
|
|
@@ -2019,7 +2327,6 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2019
2327
|
const contentWidth = cardWidth - pad * 2;
|
|
2020
2328
|
let y = 0;
|
|
2021
2329
|
const sections = [];
|
|
2022
|
-
sections.push(`<defs><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');</style></defs>`);
|
|
2023
2330
|
sections.push(`<rect width="${cardWidth}" height="__CARD_HEIGHT__" rx="12" fill="${escapeXml(theme.bg)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
2024
2331
|
sections.push(`<clipPath id="titlebar-clip"><rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" rx="12"/></clipPath>`);
|
|
2025
2332
|
sections.push(`<rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" fill="${escapeXml(theme.bg)}" clip-path="url(#titlebar-clip)"/>`);
|
|
@@ -2123,7 +2430,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2123
2430
|
const svg = sections.join(`
|
|
2124
2431
|
`).replace("__CARD_HEIGHT__", String(cardHeight));
|
|
2125
2432
|
return [
|
|
2126
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}">`,
|
|
2433
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${cardWidth}" height="${cardHeight}" viewBox="0 0 ${cardWidth} ${cardHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">`,
|
|
2127
2434
|
svg,
|
|
2128
2435
|
"</svg>"
|
|
2129
2436
|
].join(`
|
|
@@ -2132,12 +2439,17 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2132
2439
|
|
|
2133
2440
|
// packages/renderers/dist/png/png-renderer.js
|
|
2134
2441
|
import sharp from "sharp";
|
|
2442
|
+
var PNG_DENSITY = 288;
|
|
2135
2443
|
|
|
2136
2444
|
class PngRenderer {
|
|
2137
2445
|
format = "png";
|
|
2138
2446
|
async render(output, options) {
|
|
2139
2447
|
const svgString = renderTerminalCardSvg(output, options);
|
|
2140
|
-
const pngBuffer = await sharp(Buffer.from(svgString)
|
|
2448
|
+
const pngBuffer = await sharp(Buffer.from(svgString), {
|
|
2449
|
+
density: PNG_DENSITY
|
|
2450
|
+
}).png({
|
|
2451
|
+
compressionLevel: 9
|
|
2452
|
+
}).toBuffer();
|
|
2141
2453
|
return pngBuffer;
|
|
2142
2454
|
}
|
|
2143
2455
|
}
|
|
@@ -2306,6 +2618,9 @@ function formatCost2(cost) {
|
|
|
2306
2618
|
function formatPercent(rate) {
|
|
2307
2619
|
return `${(rate * 100).toFixed(1)}%`;
|
|
2308
2620
|
}
|
|
2621
|
+
function formatSharePercent(percentage) {
|
|
2622
|
+
return `${percentage.toFixed(0)}%`;
|
|
2623
|
+
}
|
|
2309
2624
|
function divider(width) {
|
|
2310
2625
|
return BOX_H.repeat(width);
|
|
2311
2626
|
}
|
|
@@ -2372,7 +2687,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2372
2687
|
function renderTopModels(stats, width, noColor2) {
|
|
2373
2688
|
const lines = [];
|
|
2374
2689
|
for (const model of stats.topModels.slice(0, 5)) {
|
|
2375
|
-
const pct =
|
|
2690
|
+
const pct = formatSharePercent(model.percentage);
|
|
2376
2691
|
const tokens = formatTokens(model.tokens);
|
|
2377
2692
|
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2378
2693
|
lines.push(line.length > width ? line.slice(0, width) : line);
|