tokenleak 0.5.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} +481 -98
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": "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);
|
|
@@ -663,6 +663,26 @@ function computeTotalDays(daily) {
|
|
|
663
663
|
return Math.round((last - first) / ONE_DAY_MS) + 1;
|
|
664
664
|
}
|
|
665
665
|
// packages/core/dist/aggregation/merge.js
|
|
666
|
+
function mergeModelArrays(existing, incoming) {
|
|
667
|
+
const map = new Map;
|
|
668
|
+
for (const m of existing) {
|
|
669
|
+
map.set(m.model, { ...m });
|
|
670
|
+
}
|
|
671
|
+
for (const m of incoming) {
|
|
672
|
+
const prev = map.get(m.model);
|
|
673
|
+
if (prev) {
|
|
674
|
+
prev.inputTokens += m.inputTokens;
|
|
675
|
+
prev.outputTokens += m.outputTokens;
|
|
676
|
+
prev.cacheReadTokens += m.cacheReadTokens;
|
|
677
|
+
prev.cacheWriteTokens += m.cacheWriteTokens;
|
|
678
|
+
prev.totalTokens += m.totalTokens;
|
|
679
|
+
prev.cost += m.cost;
|
|
680
|
+
} else {
|
|
681
|
+
map.set(m.model, { ...m });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return [...map.values()];
|
|
685
|
+
}
|
|
666
686
|
function mergeProviderData(providers) {
|
|
667
687
|
const dateMap = new Map;
|
|
668
688
|
for (const provider of providers) {
|
|
@@ -675,7 +695,7 @@ function mergeProviderData(providers) {
|
|
|
675
695
|
existing.cacheWriteTokens += entry.cacheWriteTokens;
|
|
676
696
|
existing.totalTokens += entry.totalTokens;
|
|
677
697
|
existing.cost += entry.cost;
|
|
678
|
-
existing.models =
|
|
698
|
+
existing.models = mergeModelArrays(existing.models, entry.models);
|
|
679
699
|
} else {
|
|
680
700
|
dateMap.set(entry.date, {
|
|
681
701
|
date: entry.date,
|
|
@@ -736,7 +756,7 @@ function computePreviousPeriod(current) {
|
|
|
736
756
|
};
|
|
737
757
|
}
|
|
738
758
|
// packages/core/dist/index.js
|
|
739
|
-
var VERSION = "0.
|
|
759
|
+
var VERSION = "1.0.1";
|
|
740
760
|
|
|
741
761
|
// packages/registry/dist/models/normalizer.js
|
|
742
762
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -830,6 +850,54 @@ var MODEL_PRICING = {
|
|
|
830
850
|
cacheRead: 0.075,
|
|
831
851
|
cacheWrite: 0.15
|
|
832
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
|
+
},
|
|
833
901
|
o1: {
|
|
834
902
|
input: 15,
|
|
835
903
|
output: 60,
|
|
@@ -963,12 +1031,19 @@ function isInRange(date, range) {
|
|
|
963
1031
|
}
|
|
964
1032
|
|
|
965
1033
|
// packages/registry/dist/providers/claude-code.js
|
|
966
|
-
var
|
|
1034
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".claude");
|
|
967
1035
|
var CLAUDE_CODE_COLORS = {
|
|
968
1036
|
primary: "#ff6b35",
|
|
969
1037
|
secondary: "#ffa366",
|
|
970
1038
|
gradient: ["#ff6b35", "#ffa366"]
|
|
971
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
|
+
}
|
|
972
1047
|
function collectJsonlFiles(dir) {
|
|
973
1048
|
const results = [];
|
|
974
1049
|
if (!existsSync(dir)) {
|
|
@@ -1016,6 +1091,10 @@ function extractUsage(record) {
|
|
|
1016
1091
|
const outputTokens = typeof u["output_tokens"] === "number" ? u["output_tokens"] : 0;
|
|
1017
1092
|
const cacheReadTokens = typeof u["cache_read_input_tokens"] === "number" ? u["cache_read_input_tokens"] : 0;
|
|
1018
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
|
+
}
|
|
1019
1098
|
const date = timestamp.slice(0, 10);
|
|
1020
1099
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
|
|
1021
1100
|
return null;
|
|
@@ -1026,7 +1105,8 @@ function extractUsage(record) {
|
|
|
1026
1105
|
inputTokens,
|
|
1027
1106
|
outputTokens,
|
|
1028
1107
|
cacheReadTokens,
|
|
1029
|
-
cacheWriteTokens
|
|
1108
|
+
cacheWriteTokens,
|
|
1109
|
+
messageId: typeof msg["id"] === "string" ? msg["id"] : undefined
|
|
1030
1110
|
};
|
|
1031
1111
|
}
|
|
1032
1112
|
function buildDailyUsage(records) {
|
|
@@ -1089,7 +1169,7 @@ class ClaudeCodeProvider {
|
|
|
1089
1169
|
colors = CLAUDE_CODE_COLORS;
|
|
1090
1170
|
baseDir;
|
|
1091
1171
|
constructor(baseDir) {
|
|
1092
|
-
this.baseDir = baseDir
|
|
1172
|
+
this.baseDir = resolveBaseDir(baseDir);
|
|
1093
1173
|
}
|
|
1094
1174
|
async isAvailable() {
|
|
1095
1175
|
try {
|
|
@@ -1102,16 +1182,23 @@ class ClaudeCodeProvider {
|
|
|
1102
1182
|
const files = collectJsonlFiles(this.baseDir);
|
|
1103
1183
|
const allRecords = [];
|
|
1104
1184
|
for (const file of files) {
|
|
1185
|
+
const latestRecordsByMessageId = new Map;
|
|
1186
|
+
const anonymousRecords = [];
|
|
1105
1187
|
try {
|
|
1106
1188
|
for await (const record of splitJsonlRecords(file)) {
|
|
1107
1189
|
const usage = extractUsage(record);
|
|
1108
1190
|
if (usage !== null && isInRange(usage.date, range)) {
|
|
1109
|
-
|
|
1191
|
+
if (usage.messageId) {
|
|
1192
|
+
latestRecordsByMessageId.set(usage.messageId, usage);
|
|
1193
|
+
} else {
|
|
1194
|
+
anonymousRecords.push(usage);
|
|
1195
|
+
}
|
|
1110
1196
|
}
|
|
1111
1197
|
}
|
|
1112
1198
|
} catch {
|
|
1113
1199
|
continue;
|
|
1114
1200
|
}
|
|
1201
|
+
allRecords.push(...latestRecordsByMessageId.values(), ...anonymousRecords);
|
|
1115
1202
|
}
|
|
1116
1203
|
const daily = buildDailyUsage(allRecords);
|
|
1117
1204
|
const totalTokens = daily.reduce((sum, d) => sum + d.totalTokens, 0);
|
|
@@ -1127,7 +1214,7 @@ class ClaudeCodeProvider {
|
|
|
1127
1214
|
}
|
|
1128
1215
|
}
|
|
1129
1216
|
// packages/registry/dist/providers/codex.js
|
|
1130
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
1217
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1131
1218
|
import { join as join2 } from "path";
|
|
1132
1219
|
import { homedir as homedir2 } from "os";
|
|
1133
1220
|
var CODEX_COLORS = {
|
|
@@ -1135,7 +1222,7 @@ var CODEX_COLORS = {
|
|
|
1135
1222
|
secondary: "#4ade80",
|
|
1136
1223
|
gradient: ["#10a37f", "#4ade80"]
|
|
1137
1224
|
};
|
|
1138
|
-
var DEFAULT_SESSIONS_DIR = join2(homedir2(), ".codex", "sessions");
|
|
1225
|
+
var DEFAULT_SESSIONS_DIR = join2(process.env["CODEX_HOME"] ?? join2(homedir2(), ".codex"), "sessions");
|
|
1139
1226
|
function parseResponseEvent(record) {
|
|
1140
1227
|
if (typeof record !== "object" || record === null || !("type" in record)) {
|
|
1141
1228
|
return null;
|
|
@@ -1170,6 +1257,159 @@ function extractDate(timestamp) {
|
|
|
1170
1257
|
const match = /^(\d{4}-\d{2}-\d{2})/.exec(timestamp);
|
|
1171
1258
|
return match ? match[1] : null;
|
|
1172
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
|
+
}
|
|
1173
1413
|
|
|
1174
1414
|
class CodexProvider {
|
|
1175
1415
|
name = "codex";
|
|
@@ -1188,34 +1428,31 @@ class CodexProvider {
|
|
|
1188
1428
|
}
|
|
1189
1429
|
async load(range) {
|
|
1190
1430
|
const dailyMap = new Map;
|
|
1191
|
-
|
|
1192
|
-
try {
|
|
1193
|
-
files = readdirSync2(this.sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1194
|
-
} catch {
|
|
1195
|
-
files = [];
|
|
1196
|
-
}
|
|
1431
|
+
const files = collectJsonlFiles2(this.sessionsDir);
|
|
1197
1432
|
for (const file of files) {
|
|
1198
|
-
const
|
|
1433
|
+
const context = {
|
|
1434
|
+
model: "gpt-5",
|
|
1435
|
+
previousTotals: null
|
|
1436
|
+
};
|
|
1199
1437
|
try {
|
|
1200
|
-
for await (const record of splitJsonlRecords(
|
|
1201
|
-
const
|
|
1202
|
-
if (!
|
|
1438
|
+
for await (const record of splitJsonlRecords(file)) {
|
|
1439
|
+
const usage = parseUsageRecord(record, context);
|
|
1440
|
+
if (!usage) {
|
|
1203
1441
|
continue;
|
|
1204
1442
|
}
|
|
1205
|
-
|
|
1206
|
-
if (!date || !isInRange(date, range)) {
|
|
1443
|
+
if (!isInRange(usage.date, range)) {
|
|
1207
1444
|
continue;
|
|
1208
1445
|
}
|
|
1209
|
-
const normalizedModel = normalizeModelName(compactModelDateSuffix(
|
|
1210
|
-
const inputTokens =
|
|
1211
|
-
const outputTokens =
|
|
1212
|
-
const cacheReadTokens =
|
|
1213
|
-
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;
|
|
1214
1451
|
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1215
|
-
if (!dailyMap.has(date)) {
|
|
1216
|
-
dailyMap.set(date, new Map);
|
|
1452
|
+
if (!dailyMap.has(usage.date)) {
|
|
1453
|
+
dailyMap.set(usage.date, new Map);
|
|
1217
1454
|
}
|
|
1218
|
-
const modelMap = dailyMap.get(date);
|
|
1455
|
+
const modelMap = dailyMap.get(usage.date);
|
|
1219
1456
|
if (!modelMap.has(normalizedModel)) {
|
|
1220
1457
|
modelMap.set(normalizedModel, {
|
|
1221
1458
|
model: normalizedModel,
|
|
@@ -1282,15 +1519,41 @@ var COLORS = {
|
|
|
1282
1519
|
secondary: "#a78bfa",
|
|
1283
1520
|
gradient: ["#6366f1", "#a78bfa"]
|
|
1284
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
|
+
}
|
|
1285
1540
|
function extractDate2(createdAt) {
|
|
1286
|
-
|
|
1287
|
-
|
|
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;
|
|
1288
1549
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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;
|
|
1292
1555
|
}
|
|
1293
|
-
return
|
|
1556
|
+
return estimateCost(record.model, record.inputTokens, record.outputTokens, record.cacheReadTokens, record.cacheWriteTokens);
|
|
1294
1557
|
}
|
|
1295
1558
|
function buildProviderData(records) {
|
|
1296
1559
|
const byDate = new Map;
|
|
@@ -1302,50 +1565,44 @@ function buildProviderData(records) {
|
|
|
1302
1565
|
}
|
|
1303
1566
|
const normalized = normalizeModelName(record.model);
|
|
1304
1567
|
const existing = dateMap.get(normalized);
|
|
1568
|
+
const recordCost = getRecordCost(record);
|
|
1305
1569
|
if (existing) {
|
|
1306
1570
|
existing.inputTokens += record.inputTokens;
|
|
1307
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;
|
|
1308
1576
|
} else {
|
|
1309
1577
|
dateMap.set(normalized, {
|
|
1578
|
+
model: normalized,
|
|
1310
1579
|
inputTokens: record.inputTokens,
|
|
1311
|
-
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
|
|
1312
1585
|
});
|
|
1313
1586
|
}
|
|
1314
1587
|
}
|
|
1315
1588
|
let totalTokens = 0;
|
|
1316
1589
|
let totalCost = 0;
|
|
1317
1590
|
const daily = [...byDate.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, modelMap]) => {
|
|
1318
|
-
const models = [];
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const cacheWriteTokens = 0;
|
|
1326
|
-
const modelTotal = usage.inputTokens + usage.outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
1327
|
-
models.push({
|
|
1328
|
-
model,
|
|
1329
|
-
inputTokens: usage.inputTokens,
|
|
1330
|
-
outputTokens: usage.outputTokens,
|
|
1331
|
-
cacheReadTokens,
|
|
1332
|
-
cacheWriteTokens,
|
|
1333
|
-
totalTokens: modelTotal,
|
|
1334
|
-
cost
|
|
1335
|
-
});
|
|
1336
|
-
dayInput += usage.inputTokens;
|
|
1337
|
-
dayOutput += usage.outputTokens;
|
|
1338
|
-
dayCost += cost;
|
|
1339
|
-
}
|
|
1340
|
-
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);
|
|
1341
1598
|
totalTokens += dayTotal;
|
|
1342
1599
|
totalCost += dayCost;
|
|
1343
1600
|
return {
|
|
1344
1601
|
date,
|
|
1345
|
-
inputTokens
|
|
1346
|
-
outputTokens
|
|
1347
|
-
cacheReadTokens
|
|
1348
|
-
cacheWriteTokens
|
|
1602
|
+
inputTokens,
|
|
1603
|
+
outputTokens,
|
|
1604
|
+
cacheReadTokens,
|
|
1605
|
+
cacheWriteTokens,
|
|
1349
1606
|
totalTokens: dayTotal,
|
|
1350
1607
|
cost: dayCost,
|
|
1351
1608
|
models
|
|
@@ -1361,52 +1618,131 @@ function buildProviderData(records) {
|
|
|
1361
1618
|
};
|
|
1362
1619
|
}
|
|
1363
1620
|
function loadFromSqlite(dbPath, range) {
|
|
1364
|
-
|
|
1621
|
+
let db;
|
|
1365
1622
|
try {
|
|
1623
|
+
db = new Database(dbPath, { readonly: true });
|
|
1624
|
+
} catch {
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
try {
|
|
1628
|
+
const tables = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='messages'").all();
|
|
1629
|
+
if (tables.length === 0) {
|
|
1630
|
+
return [];
|
|
1631
|
+
}
|
|
1366
1632
|
const rows = db.query("SELECT model, input_tokens, output_tokens, created_at FROM messages WHERE role = 'assistant'").all();
|
|
1367
1633
|
const records = [];
|
|
1368
1634
|
for (const row of rows) {
|
|
1369
1635
|
const date = extractDate2(row.created_at);
|
|
1370
|
-
if (isInRange(date, range)) {
|
|
1636
|
+
if (date && isInRange(date, range)) {
|
|
1371
1637
|
records.push({
|
|
1372
1638
|
date,
|
|
1373
1639
|
model: row.model,
|
|
1374
1640
|
inputTokens: row.input_tokens,
|
|
1375
|
-
outputTokens: row.output_tokens
|
|
1641
|
+
outputTokens: row.output_tokens,
|
|
1642
|
+
cacheReadTokens: 0,
|
|
1643
|
+
cacheWriteTokens: 0
|
|
1376
1644
|
});
|
|
1377
1645
|
}
|
|
1378
1646
|
}
|
|
1379
1647
|
return records;
|
|
1648
|
+
} catch {
|
|
1649
|
+
return [];
|
|
1380
1650
|
} finally {
|
|
1381
1651
|
db.close();
|
|
1382
1652
|
}
|
|
1383
1653
|
}
|
|
1384
|
-
function
|
|
1385
|
-
const files = readdirSync3(sessionsDir).filter((
|
|
1654
|
+
function loadFromLegacyJson(sessionsDir, range) {
|
|
1655
|
+
const files = readdirSync3(sessionsDir).filter((file) => file.endsWith(".json"));
|
|
1386
1656
|
const records = [];
|
|
1387
1657
|
for (const file of files) {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
}
|
|
1393
|
-
for (const msg of session.messages) {
|
|
1394
|
-
if (msg.role !== "assistant" || !msg.usage) {
|
|
1658
|
+
try {
|
|
1659
|
+
const content = readFileSync(join3(sessionsDir, file), "utf-8");
|
|
1660
|
+
const session = JSON.parse(content);
|
|
1661
|
+
if (!Array.isArray(session.messages)) {
|
|
1395
1662
|
continue;
|
|
1396
1663
|
}
|
|
1397
|
-
const
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1664
|
+
for (const msg of session.messages) {
|
|
1665
|
+
if (msg.role !== "assistant" || !msg.usage) {
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
const date = extractDate2(msg.created_at);
|
|
1669
|
+
if (date && isInRange(date, range)) {
|
|
1670
|
+
records.push({
|
|
1671
|
+
date,
|
|
1672
|
+
model: msg.model,
|
|
1673
|
+
inputTokens: msg.usage.input_tokens,
|
|
1674
|
+
outputTokens: msg.usage.output_tokens,
|
|
1675
|
+
cacheReadTokens: 0,
|
|
1676
|
+
cacheWriteTokens: 0
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1405
1679
|
}
|
|
1680
|
+
} catch {
|
|
1681
|
+
continue;
|
|
1406
1682
|
}
|
|
1407
1683
|
}
|
|
1408
1684
|
return records;
|
|
1409
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
|
+
}
|
|
1410
1746
|
|
|
1411
1747
|
class OpenCodeProvider {
|
|
1412
1748
|
name = PROVIDER_NAME;
|
|
@@ -1414,30 +1750,37 @@ class OpenCodeProvider {
|
|
|
1414
1750
|
colors = COLORS;
|
|
1415
1751
|
baseDir;
|
|
1416
1752
|
constructor(baseDir) {
|
|
1417
|
-
this.baseDir = baseDir
|
|
1753
|
+
this.baseDir = resolveBaseDir2(baseDir);
|
|
1418
1754
|
}
|
|
1419
1755
|
async isAvailable() {
|
|
1420
1756
|
try {
|
|
1421
1757
|
if (!existsSync3(this.baseDir)) {
|
|
1422
1758
|
return false;
|
|
1423
1759
|
}
|
|
1424
|
-
const
|
|
1425
|
-
const
|
|
1426
|
-
|
|
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;
|
|
1427
1764
|
} catch {
|
|
1428
1765
|
return false;
|
|
1429
1766
|
}
|
|
1430
1767
|
}
|
|
1431
1768
|
async load(range) {
|
|
1432
|
-
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");
|
|
1433
1776
|
const sessionsDir = join3(this.baseDir, "sessions");
|
|
1434
|
-
let records;
|
|
1435
|
-
if (existsSync3(
|
|
1436
|
-
records = loadFromSqlite(
|
|
1777
|
+
let records = [];
|
|
1778
|
+
if (existsSync3(opencodeDbPath)) {
|
|
1779
|
+
records = loadFromSqlite(opencodeDbPath, range);
|
|
1780
|
+
} else if (existsSync3(sessionsDbPath)) {
|
|
1781
|
+
records = loadFromSqlite(sessionsDbPath, range);
|
|
1437
1782
|
} else if (existsSync3(sessionsDir)) {
|
|
1438
|
-
records =
|
|
1439
|
-
} else {
|
|
1440
|
-
records = [];
|
|
1783
|
+
records = loadFromLegacyJson(sessionsDir, range);
|
|
1441
1784
|
}
|
|
1442
1785
|
return buildProviderData(records);
|
|
1443
1786
|
}
|
|
@@ -1984,7 +2327,6 @@ function renderTerminalCardSvg(output, options) {
|
|
|
1984
2327
|
const contentWidth = cardWidth - pad * 2;
|
|
1985
2328
|
let y = 0;
|
|
1986
2329
|
const sections = [];
|
|
1987
|
-
sections.push(`<defs><style>@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');</style></defs>`);
|
|
1988
2330
|
sections.push(`<rect width="${cardWidth}" height="__CARD_HEIGHT__" rx="12" fill="${escapeXml(theme.bg)}" stroke="${escapeXml(theme.border)}" stroke-width="1"/>`);
|
|
1989
2331
|
sections.push(`<clipPath id="titlebar-clip"><rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" rx="12"/></clipPath>`);
|
|
1990
2332
|
sections.push(`<rect width="${cardWidth}" height="${TITLEBAR_HEIGHT}" fill="${escapeXml(theme.bg)}" clip-path="url(#titlebar-clip)"/>`);
|
|
@@ -2088,7 +2430,7 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2088
2430
|
const svg = sections.join(`
|
|
2089
2431
|
`).replace("__CARD_HEIGHT__", String(cardHeight));
|
|
2090
2432
|
return [
|
|
2091
|
-
`<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">`,
|
|
2092
2434
|
svg,
|
|
2093
2435
|
"</svg>"
|
|
2094
2436
|
].join(`
|
|
@@ -2097,12 +2439,17 @@ function renderTerminalCardSvg(output, options) {
|
|
|
2097
2439
|
|
|
2098
2440
|
// packages/renderers/dist/png/png-renderer.js
|
|
2099
2441
|
import sharp from "sharp";
|
|
2442
|
+
var PNG_DENSITY = 288;
|
|
2100
2443
|
|
|
2101
2444
|
class PngRenderer {
|
|
2102
2445
|
format = "png";
|
|
2103
2446
|
async render(output, options) {
|
|
2104
2447
|
const svgString = renderTerminalCardSvg(output, options);
|
|
2105
|
-
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();
|
|
2106
2453
|
return pngBuffer;
|
|
2107
2454
|
}
|
|
2108
2455
|
}
|
|
@@ -2271,6 +2618,9 @@ function formatCost2(cost) {
|
|
|
2271
2618
|
function formatPercent(rate) {
|
|
2272
2619
|
return `${(rate * 100).toFixed(1)}%`;
|
|
2273
2620
|
}
|
|
2621
|
+
function formatSharePercent(percentage) {
|
|
2622
|
+
return `${percentage.toFixed(0)}%`;
|
|
2623
|
+
}
|
|
2274
2624
|
function divider(width) {
|
|
2275
2625
|
return BOX_H.repeat(width);
|
|
2276
2626
|
}
|
|
@@ -2337,7 +2687,7 @@ function renderDayOfWeek(stats, width, noColor2) {
|
|
|
2337
2687
|
function renderTopModels(stats, width, noColor2) {
|
|
2338
2688
|
const lines = [];
|
|
2339
2689
|
for (const model of stats.topModels.slice(0, 5)) {
|
|
2340
|
-
const pct =
|
|
2690
|
+
const pct = formatSharePercent(model.percentage);
|
|
2341
2691
|
const tokens = formatTokens(model.tokens);
|
|
2342
2692
|
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2343
2693
|
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
@@ -3063,8 +3413,21 @@ function inferFormatFromPath(filePath) {
|
|
|
3063
3413
|
return null;
|
|
3064
3414
|
}
|
|
3065
3415
|
}
|
|
3416
|
+
var DATE_FORMAT = /^\d{4}-\d{2}-\d{2}$/;
|
|
3417
|
+
function isValidDate(dateStr) {
|
|
3418
|
+
if (!DATE_FORMAT.test(dateStr))
|
|
3419
|
+
return false;
|
|
3420
|
+
const d = new Date(dateStr + "T00:00:00Z");
|
|
3421
|
+
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === dateStr;
|
|
3422
|
+
}
|
|
3066
3423
|
function computeDateRange(args) {
|
|
3067
3424
|
const until = args.until ?? new Date().toISOString().slice(0, 10);
|
|
3425
|
+
if (args.until && !isValidDate(args.until)) {
|
|
3426
|
+
throw new TokenleakError(`Invalid --until date: "${args.until}". Use YYYY-MM-DD format.`);
|
|
3427
|
+
}
|
|
3428
|
+
if (args.since && !isValidDate(args.since)) {
|
|
3429
|
+
throw new TokenleakError(`Invalid --since date: "${args.since}". Use YYYY-MM-DD format.`);
|
|
3430
|
+
}
|
|
3068
3431
|
let since;
|
|
3069
3432
|
if (args.since) {
|
|
3070
3433
|
since = args.since;
|
|
@@ -3074,6 +3437,9 @@ function computeDateRange(args) {
|
|
|
3074
3437
|
d.setDate(d.getDate() - daysBack);
|
|
3075
3438
|
since = d.toISOString().slice(0, 10);
|
|
3076
3439
|
}
|
|
3440
|
+
if (since > until) {
|
|
3441
|
+
throw new TokenleakError(`--since (${since}) must not be after --until (${until}).`);
|
|
3442
|
+
}
|
|
3077
3443
|
return { since, until };
|
|
3078
3444
|
}
|
|
3079
3445
|
function resolveConfig(cliArgs) {
|
|
@@ -3230,6 +3596,10 @@ async function run(cliArgs) {
|
|
|
3230
3596
|
throw new TokenleakError("No provider data found");
|
|
3231
3597
|
}
|
|
3232
3598
|
if (config.compare) {
|
|
3599
|
+
if (config.format !== "json" && config.format !== "terminal") {
|
|
3600
|
+
process.stderr.write(`Warning: --compare only supports JSON output. Ignoring --format ${config.format}.
|
|
3601
|
+
`);
|
|
3602
|
+
}
|
|
3233
3603
|
const compareOutput = await runCompare(config.compare, dateRange, registry, available);
|
|
3234
3604
|
const rendered2 = JSON.stringify(compareOutput, null, 2);
|
|
3235
3605
|
if (config.output) {
|
|
@@ -3261,6 +3631,19 @@ async function run(cliArgs) {
|
|
|
3261
3631
|
aggregated: stats
|
|
3262
3632
|
};
|
|
3263
3633
|
if (config.liveServer) {
|
|
3634
|
+
const ignoredFlags = [];
|
|
3635
|
+
if (config.output)
|
|
3636
|
+
ignoredFlags.push("--output");
|
|
3637
|
+
if (config.clipboard)
|
|
3638
|
+
ignoredFlags.push("--clipboard");
|
|
3639
|
+
if (config.open)
|
|
3640
|
+
ignoredFlags.push("--open");
|
|
3641
|
+
if (config.upload)
|
|
3642
|
+
ignoredFlags.push("--upload");
|
|
3643
|
+
if (ignoredFlags.length > 0) {
|
|
3644
|
+
process.stderr.write(`Warning: ${ignoredFlags.join(", ")} ignored in --live-server mode.
|
|
3645
|
+
`);
|
|
3646
|
+
}
|
|
3264
3647
|
const renderOptions2 = {
|
|
3265
3648
|
format: config.format,
|
|
3266
3649
|
theme: config.theme,
|