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 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": "0.5.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);
@@ -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 = [...existing.models, ...entry.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.5.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 DEFAULT_BASE_DIR = join(homedir(), ".claude", "projects");
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 ?? DEFAULT_BASE_DIR;
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
- allRecords.push(usage);
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
- let files;
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 filePath = join2(this.sessionsDir, file);
1433
+ const context = {
1434
+ model: "gpt-5",
1435
+ previousTotals: null
1436
+ };
1199
1437
  try {
1200
- for await (const record of splitJsonlRecords(filePath)) {
1201
- const event = parseResponseEvent(record);
1202
- if (!event) {
1438
+ for await (const record of splitJsonlRecords(file)) {
1439
+ const usage = parseUsageRecord(record, context);
1440
+ if (!usage) {
1203
1441
  continue;
1204
1442
  }
1205
- const date = extractDate(event.timestamp);
1206
- if (!date || !isInRange(date, range)) {
1443
+ if (!isInRange(usage.date, range)) {
1207
1444
  continue;
1208
1445
  }
1209
- const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
1210
- const inputTokens = event.usage.input_tokens;
1211
- const outputTokens = event.usage.output_tokens;
1212
- const cacheReadTokens = 0;
1213
- 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;
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
- if (typeof createdAt === "number") {
1287
- 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;
1288
1549
  }
1289
- const asNum = Number(createdAt);
1290
- if (!Number.isNaN(asNum) && String(asNum) === String(createdAt).trim()) {
1291
- 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;
1292
1555
  }
1293
- return new Date(createdAt).toISOString().slice(0, 10);
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
- let dayInput = 0;
1320
- let dayOutput = 0;
1321
- let dayCost = 0;
1322
- for (const [model, usage] of modelMap) {
1323
- const cost = estimateCost(model, usage.inputTokens, usage.outputTokens, 0, 0);
1324
- const cacheReadTokens = 0;
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: dayInput,
1346
- outputTokens: dayOutput,
1347
- cacheReadTokens: 0,
1348
- cacheWriteTokens: 0,
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
- const db = new Database(dbPath, { readonly: true });
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 loadFromJson(sessionsDir, range) {
1385
- const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json"));
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
- const content = readFileSync(join3(sessionsDir, file), "utf-8");
1389
- const session = JSON.parse(content);
1390
- if (!Array.isArray(session.messages)) {
1391
- continue;
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 date = extractDate2(msg.created_at);
1398
- if (isInRange(date, range)) {
1399
- records.push({
1400
- date,
1401
- model: msg.model,
1402
- inputTokens: msg.usage.input_tokens,
1403
- outputTokens: msg.usage.output_tokens
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 ?? join3(homedir3(), ".opencode");
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 hasDb = existsSync3(join3(this.baseDir, "sessions.db"));
1425
- const hasSessionsDir = existsSync3(join3(this.baseDir, "sessions"));
1426
- 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;
1427
1764
  } catch {
1428
1765
  return false;
1429
1766
  }
1430
1767
  }
1431
1768
  async load(range) {
1432
- 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");
1433
1776
  const sessionsDir = join3(this.baseDir, "sessions");
1434
- let records;
1435
- if (existsSync3(dbPath)) {
1436
- 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);
1437
1782
  } else if (existsSync3(sessionsDir)) {
1438
- records = loadFromJson(sessionsDir, range);
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&amp;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)).png().toBuffer();
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 = formatPercent(model.percentage);
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,