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 CHANGED
@@ -134,24 +134,24 @@ tokenleak --format json --upload gist
134
134
 
135
135
  ## All flags
136
136
 
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 |
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** | Set `CLAUDE_CONFIG_DIR` environment variable |
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** | Set `CODEX_HOME` environment variable |
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/sessions.db` (primary) or `~/.opencode/sessions/*.json` (fallback) |
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
- { "model": "claude-sonnet-4", "inputTokens": 10000, "outputTokens": 3000, "totalTokens": 13000, "cost": 0.075 }
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.50,
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 | Default | Description |
278
- |----------|---------|-------------|
279
- | `TOKENLEAK_FORMAT` | `terminal` | Default output format |
280
- | `TOKENLEAK_THEME` | `dark` | Default colour theme |
281
- | `TOKENLEAK_DAYS` | `90` | Default lookback period in 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` | `~/.claude` | Claude Code configuration directory |
284
- | `CODEX_HOME` | `~/.codex` | Codex home directory |
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 | Models |
313
- |--------|--------|
314
- | Claude 3 | `claude-3-haiku`, `claude-3-sonnet`, `claude-3-opus` |
315
- | Claude 3.5 | `claude-3.5-haiku`, `claude-3.5-sonnet` |
316
- | Claude 4 | `claude-sonnet-4`, `claude-opus-4` |
317
- | GPT-4o | `gpt-4o`, `gpt-4o-mini` |
318
- | o-series | `o1`, `o1-mini`, `o3`, `o3-mini`, `o4-mini` |
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.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": "./tokenleak.js"
7
+ "tokenleak": "tokenleak"
8
8
  },
9
9
  "files": [
10
- "tokenleak.js"
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.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 DEFAULT_BASE_DIR = join(homedir(), ".claude", "projects");
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 ?? DEFAULT_BASE_DIR;
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
- allRecords.push(usage);
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
- let files;
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 filePath = join2(this.sessionsDir, file);
1433
+ const context = {
1434
+ model: "gpt-5",
1435
+ previousTotals: null
1436
+ };
1219
1437
  try {
1220
- for await (const record of splitJsonlRecords(filePath)) {
1221
- const event = parseResponseEvent(record);
1222
- if (!event) {
1438
+ for await (const record of splitJsonlRecords(file)) {
1439
+ const usage = parseUsageRecord(record, context);
1440
+ if (!usage) {
1223
1441
  continue;
1224
1442
  }
1225
- const date = extractDate(event.timestamp);
1226
- if (!date || !isInRange(date, range)) {
1443
+ if (!isInRange(usage.date, range)) {
1227
1444
  continue;
1228
1445
  }
1229
- const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
1230
- const inputTokens = event.usage.input_tokens;
1231
- const outputTokens = event.usage.output_tokens;
1232
- const cacheReadTokens = 0;
1233
- const cacheWriteTokens = 0;
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
- if (typeof createdAt === "number") {
1307
- return new Date(createdAt * 1000).toISOString().slice(0, 10);
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
- const asNum = Number(createdAt);
1310
- if (!Number.isNaN(asNum) && String(asNum) === String(createdAt).trim()) {
1311
- return new Date(asNum * 1000).toISOString().slice(0, 10);
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 new Date(createdAt).toISOString().slice(0, 10);
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
- let dayInput = 0;
1340
- let dayOutput = 0;
1341
- let dayCost = 0;
1342
- for (const [model, usage] of modelMap) {
1343
- const cost = estimateCost(model, usage.inputTokens, usage.outputTokens, 0, 0);
1344
- const cacheReadTokens = 0;
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: dayInput,
1366
- outputTokens: dayOutput,
1367
- cacheReadTokens: 0,
1368
- cacheWriteTokens: 0,
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 loadFromJson(sessionsDir, range) {
1416
- const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
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 ?? join3(homedir3(), ".opencode");
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 hasDb = existsSync3(join3(this.baseDir, "sessions.db"));
1460
- const hasSessionsDir = existsSync3(join3(this.baseDir, "sessions"));
1461
- return hasDb || hasSessionsDir;
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 dbPath = join3(this.baseDir, "sessions.db");
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(dbPath)) {
1471
- records = loadFromSqlite(dbPath, range);
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 = loadFromJson(sessionsDir, range);
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&amp;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)).png().toBuffer();
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 = formatPercent(model.percentage);
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);