@praeviso/code-env-switch 0.1.5 → 0.1.6
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 +40 -0
- package/README_zh.md +40 -0
- package/bin/cli/args.js +13 -0
- package/bin/cli/help.js +5 -0
- package/bin/cli/index.js +2 -1
- package/bin/commands/index.js +3 -1
- package/bin/commands/usage.js +41 -0
- package/bin/index.js +7 -0
- package/bin/statusline/debug.js +1 -0
- package/bin/statusline/usage/codex.js +29 -20
- package/bin/usage/index.js +261 -19
- package/package.json +1 -1
- package/src/cli/args.ts +14 -0
- package/src/cli/help.ts +5 -0
- package/src/cli/index.ts +7 -1
- package/src/commands/index.ts +1 -0
- package/src/commands/usage.ts +53 -0
- package/src/index.ts +11 -0
- package/src/statusline/debug.ts +1 -1
- package/src/statusline/usage/codex.ts +26 -31
- package/src/types.ts +4 -0
- package/src/usage/index.ts +268 -17
|
@@ -18,21 +18,20 @@ function resolveOutputTokens(record: Record<string, unknown>): number | null {
|
|
|
18
18
|
record.reasoningOutputTokens,
|
|
19
19
|
record.reasoning_output
|
|
20
20
|
) ?? null;
|
|
21
|
-
if (outputTokens
|
|
22
|
-
if (reasoningTokens
|
|
23
|
-
return
|
|
21
|
+
if (outputTokens !== null) return outputTokens;
|
|
22
|
+
if (reasoningTokens !== null) return reasoningTokens;
|
|
23
|
+
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function
|
|
26
|
+
function splitInputTokens(
|
|
27
27
|
record: Record<string, unknown>
|
|
28
|
-
):
|
|
29
|
-
const
|
|
28
|
+
): { inputTokens: number | null; cacheReadTokens: number | null } {
|
|
29
|
+
const rawInput =
|
|
30
30
|
firstNumber(
|
|
31
31
|
record.inputTokens,
|
|
32
32
|
record.input,
|
|
33
33
|
record.input_tokens
|
|
34
34
|
) ?? null;
|
|
35
|
-
const outputTokens = resolveOutputTokens(record);
|
|
36
35
|
const cacheRead =
|
|
37
36
|
firstNumber(
|
|
38
37
|
record.cached_input_tokens,
|
|
@@ -42,6 +41,23 @@ function parseCodexUsageTotalsRecord(
|
|
|
42
41
|
record.cache_read,
|
|
43
42
|
record.cacheRead
|
|
44
43
|
) ?? null;
|
|
44
|
+
if (rawInput === null) {
|
|
45
|
+
return { inputTokens: null, cacheReadTokens: cacheRead };
|
|
46
|
+
}
|
|
47
|
+
if (cacheRead === null) {
|
|
48
|
+
return { inputTokens: rawInput, cacheReadTokens: null };
|
|
49
|
+
}
|
|
50
|
+
const nonCachedInput = Math.max(0, rawInput - cacheRead);
|
|
51
|
+
return { inputTokens: nonCachedInput, cacheReadTokens: cacheRead };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseCodexUsageTotalsRecord(
|
|
55
|
+
record: Record<string, unknown>
|
|
56
|
+
): StatuslineUsageTotals | null {
|
|
57
|
+
const split = splitInputTokens(record);
|
|
58
|
+
const inputTokens = split.inputTokens;
|
|
59
|
+
const outputTokens = resolveOutputTokens(record);
|
|
60
|
+
const cacheRead = split.cacheReadTokens;
|
|
45
61
|
const cacheWrite =
|
|
46
62
|
firstNumber(
|
|
47
63
|
record.cache_creation_input_tokens,
|
|
@@ -106,22 +122,10 @@ function parseCodexInputUsageRecord(
|
|
|
106
122
|
record.total,
|
|
107
123
|
record.total_tokens
|
|
108
124
|
) ?? null;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
record.inputTokens,
|
|
112
|
-
record.input,
|
|
113
|
-
record.input_tokens
|
|
114
|
-
) ?? null;
|
|
125
|
+
const split = splitInputTokens(record);
|
|
126
|
+
const inputTokens = split.inputTokens;
|
|
115
127
|
const outputTokens = resolveOutputTokens(record);
|
|
116
|
-
const cacheRead =
|
|
117
|
-
firstNumber(
|
|
118
|
-
record.cached_input_tokens,
|
|
119
|
-
record.cachedInputTokens,
|
|
120
|
-
record.cache_read_input_tokens,
|
|
121
|
-
record.cacheReadInputTokens,
|
|
122
|
-
record.cache_read,
|
|
123
|
-
record.cacheRead
|
|
124
|
-
) ?? null;
|
|
128
|
+
const cacheRead = split.cacheReadTokens;
|
|
125
129
|
const cacheWrite =
|
|
126
130
|
firstNumber(
|
|
127
131
|
record.cache_creation_input_tokens,
|
|
@@ -197,15 +201,6 @@ export function getCodexUsageTotalsFromInput(
|
|
|
197
201
|
const parsed = parseCodexUsageTotalsRecord(totalUsage);
|
|
198
202
|
if (parsed) return parsed;
|
|
199
203
|
}
|
|
200
|
-
const lastUsage = resolveNestedRecord(
|
|
201
|
-
tokenUsage,
|
|
202
|
-
"last_token_usage",
|
|
203
|
-
"lastTokenUsage"
|
|
204
|
-
);
|
|
205
|
-
if (lastUsage) {
|
|
206
|
-
const parsed = parseCodexUsageTotalsRecord(lastUsage);
|
|
207
|
-
if (parsed) return parsed;
|
|
208
|
-
}
|
|
209
204
|
const parsed = parseCodexUsageTotalsRecord(tokenUsage as Record<string, unknown>);
|
|
210
205
|
if (parsed) return parsed;
|
|
211
206
|
}
|
package/src/types.ts
CHANGED
package/src/usage/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import * as os from "os";
|
|
|
7
7
|
import type { Config, Profile, ProfileType } from "../types";
|
|
8
8
|
import { resolvePath } from "../shell/utils";
|
|
9
9
|
import { normalizeType, inferProfileType, getProfileDisplayName } from "../profile/type";
|
|
10
|
+
import { getStatuslineDebugPath } from "../statusline/debug";
|
|
10
11
|
import { calculateUsageCost, resolvePricingForProfile } from "./pricing";
|
|
11
12
|
|
|
12
13
|
interface UsageRecord {
|
|
@@ -53,6 +54,17 @@ interface UsageCostIndex {
|
|
|
53
54
|
byName: Map<string, UsageCostTotals>;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export interface UsageCleanupFailure {
|
|
58
|
+
path: string;
|
|
59
|
+
error: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface UsageCleanupResult {
|
|
63
|
+
removed: string[];
|
|
64
|
+
missing: string[];
|
|
65
|
+
failed: UsageCleanupFailure[];
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
interface UsageStateEntry {
|
|
57
69
|
mtimeMs: number;
|
|
58
70
|
size: number;
|
|
@@ -85,6 +97,8 @@ interface UsageStateFile {
|
|
|
85
97
|
version: number;
|
|
86
98
|
files: Record<string, UsageStateEntry>;
|
|
87
99
|
sessions?: Record<string, UsageSessionEntry>;
|
|
100
|
+
usageMtimeMs?: number;
|
|
101
|
+
usageSize?: number;
|
|
88
102
|
}
|
|
89
103
|
|
|
90
104
|
interface ProfileLogEntry {
|
|
@@ -573,18 +587,26 @@ export function syncUsageFromStatuslineInput(
|
|
|
573
587
|
let deltaCacheRead = cacheReadTokens - prevCacheRead;
|
|
574
588
|
let deltaCacheWrite = cacheWriteTokens - prevCacheWrite;
|
|
575
589
|
let deltaTotal = totalTokens - prevTotal;
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
deltaInput < 0 ||
|
|
579
|
-
deltaOutput < 0 ||
|
|
580
|
-
deltaCacheRead < 0 ||
|
|
581
|
-
deltaCacheWrite < 0
|
|
582
|
-
) {
|
|
590
|
+
if (deltaTotal < 0) {
|
|
591
|
+
// Session reset: treat current totals as fresh usage.
|
|
583
592
|
deltaInput = inputTokens;
|
|
584
593
|
deltaOutput = outputTokens;
|
|
585
594
|
deltaCacheRead = cacheReadTokens;
|
|
586
595
|
deltaCacheWrite = cacheWriteTokens;
|
|
587
596
|
deltaTotal = totalTokens;
|
|
597
|
+
} else {
|
|
598
|
+
// Clamp negatives caused by reclassification (e.g. cache splits).
|
|
599
|
+
if (deltaInput < 0) deltaInput = 0;
|
|
600
|
+
if (deltaOutput < 0) deltaOutput = 0;
|
|
601
|
+
if (deltaCacheRead < 0) deltaCacheRead = 0;
|
|
602
|
+
if (deltaCacheWrite < 0) deltaCacheWrite = 0;
|
|
603
|
+
const breakdownTotal =
|
|
604
|
+
deltaInput + deltaOutput + deltaCacheRead + deltaCacheWrite;
|
|
605
|
+
if (deltaTotal === 0 && breakdownTotal === 0) {
|
|
606
|
+
deltaTotal = 0;
|
|
607
|
+
} else if (breakdownTotal > deltaTotal) {
|
|
608
|
+
deltaTotal = breakdownTotal;
|
|
609
|
+
}
|
|
588
610
|
}
|
|
589
611
|
|
|
590
612
|
if (deltaTotal > 0) {
|
|
@@ -618,6 +640,7 @@ export function syncUsageFromStatuslineInput(
|
|
|
618
640
|
model: resolvedModel,
|
|
619
641
|
};
|
|
620
642
|
state.sessions = sessions;
|
|
643
|
+
updateUsageStateMetadata(state, usagePath);
|
|
621
644
|
writeUsageState(statePath, state);
|
|
622
645
|
} finally {
|
|
623
646
|
releaseLock(lockPath, lockFd);
|
|
@@ -848,7 +871,15 @@ function readUsageState(statePath: string): UsageStateFile {
|
|
|
848
871
|
parsed.files && typeof parsed.files === "object" ? parsed.files : {};
|
|
849
872
|
const sessions =
|
|
850
873
|
parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
|
|
851
|
-
|
|
874
|
+
const usageMtimeMs = Number(parsed.usageMtimeMs);
|
|
875
|
+
const usageSize = Number(parsed.usageSize);
|
|
876
|
+
return {
|
|
877
|
+
version: 1,
|
|
878
|
+
files,
|
|
879
|
+
sessions,
|
|
880
|
+
usageMtimeMs: Number.isFinite(usageMtimeMs) ? usageMtimeMs : undefined,
|
|
881
|
+
usageSize: Number.isFinite(usageSize) ? usageSize : undefined,
|
|
882
|
+
};
|
|
852
883
|
} catch {
|
|
853
884
|
return { version: 1, files: {}, sessions: {} };
|
|
854
885
|
}
|
|
@@ -859,7 +890,91 @@ function writeUsageState(statePath: string, state: UsageStateFile) {
|
|
|
859
890
|
if (!fs.existsSync(dir)) {
|
|
860
891
|
fs.mkdirSync(dir, { recursive: true });
|
|
861
892
|
}
|
|
862
|
-
|
|
893
|
+
const payload = `${JSON.stringify(state, null, 2)}\n`;
|
|
894
|
+
const tmpPath = `${statePath}.tmp`;
|
|
895
|
+
try {
|
|
896
|
+
fs.writeFileSync(tmpPath, payload, "utf8");
|
|
897
|
+
fs.renameSync(tmpPath, statePath);
|
|
898
|
+
} catch {
|
|
899
|
+
try {
|
|
900
|
+
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
901
|
+
} catch {
|
|
902
|
+
// ignore cleanup failures
|
|
903
|
+
}
|
|
904
|
+
fs.writeFileSync(statePath, payload, "utf8");
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function addSiblingBackupPaths(targets: Set<string>, filePath: string | null) {
|
|
909
|
+
if (!filePath) return;
|
|
910
|
+
const dir = path.dirname(filePath);
|
|
911
|
+
let entries: fs.Dirent[] = [];
|
|
912
|
+
try {
|
|
913
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
914
|
+
} catch {
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const base = path.basename(filePath);
|
|
918
|
+
for (const entry of entries) {
|
|
919
|
+
if (!entry.isFile()) continue;
|
|
920
|
+
if (entry.name === base) continue;
|
|
921
|
+
if (entry.name.startsWith(`${base}.`)) {
|
|
922
|
+
targets.add(path.join(dir, entry.name));
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
export function clearUsageHistory(
|
|
928
|
+
config: Config,
|
|
929
|
+
configPath: string | null
|
|
930
|
+
): UsageCleanupResult {
|
|
931
|
+
const targets = new Set<string>();
|
|
932
|
+
const usagePath = getUsagePath(config, configPath);
|
|
933
|
+
if (usagePath) {
|
|
934
|
+
targets.add(usagePath);
|
|
935
|
+
addSiblingBackupPaths(targets, usagePath);
|
|
936
|
+
}
|
|
937
|
+
const statePath = usagePath ? getUsageStatePath(usagePath, config) : null;
|
|
938
|
+
if (statePath) {
|
|
939
|
+
targets.add(statePath);
|
|
940
|
+
targets.add(`${statePath}.lock`);
|
|
941
|
+
addSiblingBackupPaths(targets, statePath);
|
|
942
|
+
}
|
|
943
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
944
|
+
if (profileLogPath) {
|
|
945
|
+
targets.add(profileLogPath);
|
|
946
|
+
addSiblingBackupPaths(targets, profileLogPath);
|
|
947
|
+
}
|
|
948
|
+
const debugPath = getStatuslineDebugPath(configPath);
|
|
949
|
+
if (debugPath) {
|
|
950
|
+
targets.add(debugPath);
|
|
951
|
+
addSiblingBackupPaths(targets, debugPath);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const removed: string[] = [];
|
|
955
|
+
const missing: string[] = [];
|
|
956
|
+
const failed: UsageCleanupFailure[] = [];
|
|
957
|
+
|
|
958
|
+
for (const target of targets) {
|
|
959
|
+
if (!target) continue;
|
|
960
|
+
if (!fs.existsSync(target)) {
|
|
961
|
+
missing.push(target);
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const stat = fs.statSync(target);
|
|
966
|
+
if (!stat.isFile()) continue;
|
|
967
|
+
fs.unlinkSync(target);
|
|
968
|
+
removed.push(target);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
failed.push({
|
|
971
|
+
path: target,
|
|
972
|
+
error: err instanceof Error ? err.message : String(err),
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return { removed, missing, failed };
|
|
863
978
|
}
|
|
864
979
|
|
|
865
980
|
function collectSessionFiles(root: string | null): string[] {
|
|
@@ -969,10 +1084,12 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
969
1084
|
let maxTotal = 0;
|
|
970
1085
|
let maxInput = 0;
|
|
971
1086
|
let maxOutput = 0;
|
|
1087
|
+
let maxCachedInput = 0;
|
|
972
1088
|
let hasTotal = false;
|
|
973
1089
|
let sumLast = 0;
|
|
974
1090
|
let sumLastInput = 0;
|
|
975
1091
|
let sumLastOutput = 0;
|
|
1092
|
+
let sumLastCachedInput = 0;
|
|
976
1093
|
const tsRange = { start: null as string | null, end: null as string | null };
|
|
977
1094
|
let cwd: string | null = null;
|
|
978
1095
|
let sessionId: string | null = null;
|
|
@@ -1012,19 +1129,25 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
1012
1129
|
if (totalTokens > maxTotal) maxTotal = totalTokens;
|
|
1013
1130
|
const totalInput = Number(totalUsage.input_tokens);
|
|
1014
1131
|
const totalOutput = Number(totalUsage.output_tokens);
|
|
1132
|
+
const totalCached = Number(totalUsage.cached_input_tokens);
|
|
1015
1133
|
if (Number.isFinite(totalInput) && totalInput > maxInput) {
|
|
1016
1134
|
maxInput = totalInput;
|
|
1017
1135
|
}
|
|
1018
1136
|
if (Number.isFinite(totalOutput) && totalOutput > maxOutput) {
|
|
1019
1137
|
maxOutput = totalOutput;
|
|
1020
1138
|
}
|
|
1139
|
+
if (Number.isFinite(totalCached) && totalCached > maxCachedInput) {
|
|
1140
|
+
maxCachedInput = totalCached;
|
|
1141
|
+
}
|
|
1021
1142
|
} else {
|
|
1022
1143
|
const lastTokens = Number(lastUsage.total_tokens);
|
|
1023
1144
|
if (Number.isFinite(lastTokens)) sumLast += lastTokens;
|
|
1024
1145
|
const lastInput = Number(lastUsage.input_tokens);
|
|
1025
1146
|
const lastOutput = Number(lastUsage.output_tokens);
|
|
1147
|
+
const lastCached = Number(lastUsage.cached_input_tokens);
|
|
1026
1148
|
if (Number.isFinite(lastInput)) sumLastInput += lastInput;
|
|
1027
1149
|
if (Number.isFinite(lastOutput)) sumLastOutput += lastOutput;
|
|
1150
|
+
if (Number.isFinite(lastCached)) sumLastCachedInput += lastCached;
|
|
1028
1151
|
}
|
|
1029
1152
|
} catch {
|
|
1030
1153
|
// ignore invalid lines
|
|
@@ -1035,14 +1158,21 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
1035
1158
|
maxTotal = sumLast;
|
|
1036
1159
|
maxInput = sumLastInput;
|
|
1037
1160
|
maxOutput = sumLastOutput;
|
|
1161
|
+
maxCachedInput = sumLastCachedInput;
|
|
1038
1162
|
}
|
|
1039
1163
|
|
|
1164
|
+
const cacheReadTokens = Math.max(0, maxCachedInput);
|
|
1165
|
+
const inputTokens =
|
|
1166
|
+
cacheReadTokens > 0 ? Math.max(0, maxInput - cacheReadTokens) : maxInput;
|
|
1167
|
+
const computedTotal = inputTokens + maxOutput + cacheReadTokens;
|
|
1168
|
+
const totalTokens = Math.max(maxTotal, computedTotal);
|
|
1169
|
+
|
|
1040
1170
|
return {
|
|
1041
|
-
inputTokens
|
|
1171
|
+
inputTokens,
|
|
1042
1172
|
outputTokens: maxOutput,
|
|
1043
|
-
cacheReadTokens
|
|
1173
|
+
cacheReadTokens,
|
|
1044
1174
|
cacheWriteTokens: 0,
|
|
1045
|
-
totalTokens
|
|
1175
|
+
totalTokens,
|
|
1046
1176
|
startTs: tsRange.start,
|
|
1047
1177
|
endTs: tsRange.end,
|
|
1048
1178
|
cwd,
|
|
@@ -1194,6 +1324,91 @@ function releaseLock(lockPath: string, fd: number | null) {
|
|
|
1194
1324
|
}
|
|
1195
1325
|
}
|
|
1196
1326
|
|
|
1327
|
+
function readUsageFileStat(usagePath: string): fs.Stats | null {
|
|
1328
|
+
if (!usagePath || !fs.existsSync(usagePath)) return null;
|
|
1329
|
+
try {
|
|
1330
|
+
return fs.statSync(usagePath);
|
|
1331
|
+
} catch {
|
|
1332
|
+
return null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function buildUsageRecordKey(record: UsageRecord): string {
|
|
1337
|
+
return JSON.stringify([
|
|
1338
|
+
record.ts,
|
|
1339
|
+
record.type,
|
|
1340
|
+
record.profileKey ?? null,
|
|
1341
|
+
record.profileName ?? null,
|
|
1342
|
+
record.model ?? null,
|
|
1343
|
+
record.sessionId ?? null,
|
|
1344
|
+
toUsageNumber(record.inputTokens),
|
|
1345
|
+
toUsageNumber(record.outputTokens),
|
|
1346
|
+
toUsageNumber(record.cacheReadTokens),
|
|
1347
|
+
toUsageNumber(record.cacheWriteTokens),
|
|
1348
|
+
toUsageNumber(record.totalTokens),
|
|
1349
|
+
]);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildUsageSessionsFromRecords(
|
|
1353
|
+
records: UsageRecord[]
|
|
1354
|
+
): Record<string, UsageSessionEntry> {
|
|
1355
|
+
const sessions: Record<string, UsageSessionEntry> = {};
|
|
1356
|
+
const seen = new Set<string>();
|
|
1357
|
+
for (const record of records) {
|
|
1358
|
+
if (!record.sessionId) continue;
|
|
1359
|
+
const normalizedType = normalizeUsageType(record.type);
|
|
1360
|
+
if (!normalizedType) continue;
|
|
1361
|
+
const recordKey = buildUsageRecordKey(record);
|
|
1362
|
+
if (seen.has(recordKey)) continue;
|
|
1363
|
+
seen.add(recordKey);
|
|
1364
|
+
|
|
1365
|
+
const sessionKey = buildSessionKey(
|
|
1366
|
+
normalizedType as ProfileType,
|
|
1367
|
+
record.sessionId
|
|
1368
|
+
);
|
|
1369
|
+
let entry = sessions[sessionKey];
|
|
1370
|
+
if (!entry) {
|
|
1371
|
+
entry = {
|
|
1372
|
+
type: normalizedType as ProfileType,
|
|
1373
|
+
inputTokens: 0,
|
|
1374
|
+
outputTokens: 0,
|
|
1375
|
+
cacheReadTokens: 0,
|
|
1376
|
+
cacheWriteTokens: 0,
|
|
1377
|
+
totalTokens: 0,
|
|
1378
|
+
startTs: null,
|
|
1379
|
+
endTs: null,
|
|
1380
|
+
cwd: null,
|
|
1381
|
+
model: record.model || null,
|
|
1382
|
+
};
|
|
1383
|
+
sessions[sessionKey] = entry;
|
|
1384
|
+
}
|
|
1385
|
+
entry.inputTokens += toUsageNumber(record.inputTokens);
|
|
1386
|
+
entry.outputTokens += toUsageNumber(record.outputTokens);
|
|
1387
|
+
entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
|
|
1388
|
+
entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
|
|
1389
|
+
entry.totalTokens += toUsageNumber(record.totalTokens);
|
|
1390
|
+
if (!entry.model && record.model) entry.model = record.model;
|
|
1391
|
+
if (record.ts) {
|
|
1392
|
+
const range = { start: entry.startTs, end: entry.endTs };
|
|
1393
|
+
updateMinMaxTs(range, record.ts);
|
|
1394
|
+
entry.startTs = range.start;
|
|
1395
|
+
entry.endTs = range.end;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return sessions;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function updateUsageStateMetadata(state: UsageStateFile, usagePath: string) {
|
|
1402
|
+
const stat = readUsageFileStat(usagePath);
|
|
1403
|
+
if (!stat || !stat.isFile()) {
|
|
1404
|
+
state.usageMtimeMs = undefined;
|
|
1405
|
+
state.usageSize = undefined;
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
state.usageMtimeMs = stat.mtimeMs;
|
|
1409
|
+
state.usageSize = stat.size;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1197
1412
|
function appendUsageRecord(usagePath: string, record: UsageRecord) {
|
|
1198
1413
|
const dir = path.dirname(usagePath);
|
|
1199
1414
|
if (!fs.existsSync(dir)) {
|
|
@@ -1207,13 +1422,15 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1207
1422
|
const raw = fs.readFileSync(usagePath, "utf8");
|
|
1208
1423
|
const lines = raw.split(/\r?\n/);
|
|
1209
1424
|
const records: UsageRecord[] = [];
|
|
1425
|
+
const seen = new Set<string>();
|
|
1210
1426
|
for (const line of lines) {
|
|
1211
1427
|
const trimmed = line.trim();
|
|
1212
1428
|
if (!trimmed) continue;
|
|
1213
1429
|
try {
|
|
1214
1430
|
const parsed = JSON.parse(trimmed);
|
|
1215
1431
|
if (!parsed || typeof parsed !== "object") continue;
|
|
1216
|
-
const
|
|
1432
|
+
const type = normalizeType(parsed.type) || String(parsed.type ?? "unknown");
|
|
1433
|
+
let input = Number(parsed.inputTokens ?? 0);
|
|
1217
1434
|
const output = Number(parsed.outputTokens ?? 0);
|
|
1218
1435
|
const cacheRead = Number(
|
|
1219
1436
|
parsed.cacheReadTokens ??
|
|
@@ -1228,6 +1445,18 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1228
1445
|
parsed.cacheWriteInputTokens ??
|
|
1229
1446
|
0
|
|
1230
1447
|
);
|
|
1448
|
+
if (
|
|
1449
|
+
type === "codex" &&
|
|
1450
|
+
Number.isFinite(cacheRead) &&
|
|
1451
|
+
cacheRead > 0 &&
|
|
1452
|
+
Number.isFinite(input) &&
|
|
1453
|
+
Number.isFinite(output)
|
|
1454
|
+
) {
|
|
1455
|
+
const rawTotal = Number(parsed.totalTokens);
|
|
1456
|
+
if (Number.isFinite(rawTotal) && rawTotal <= input + output) {
|
|
1457
|
+
input = Math.max(0, input - cacheRead);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1231
1460
|
const computedTotal =
|
|
1232
1461
|
(Number.isFinite(input) ? input : 0) +
|
|
1233
1462
|
(Number.isFinite(output) ? output : 0) +
|
|
@@ -1239,7 +1468,6 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1239
1468
|
const finalTotal = Number.isFinite(total)
|
|
1240
1469
|
? Math.max(total, computedTotal)
|
|
1241
1470
|
: computedTotal;
|
|
1242
|
-
const type = normalizeType(parsed.type) || String(parsed.type ?? "unknown");
|
|
1243
1471
|
const model =
|
|
1244
1472
|
normalizeModelValue(parsed.model) ||
|
|
1245
1473
|
normalizeModelValue(parsed.model_name) ||
|
|
@@ -1253,7 +1481,7 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1253
1481
|
parsed.sessionID ||
|
|
1254
1482
|
parsed.session ||
|
|
1255
1483
|
null;
|
|
1256
|
-
|
|
1484
|
+
const record: UsageRecord = {
|
|
1257
1485
|
ts: String(parsed.ts ?? ""),
|
|
1258
1486
|
type,
|
|
1259
1487
|
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
@@ -1265,7 +1493,11 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1265
1493
|
cacheReadTokens: Number.isFinite(cacheRead) ? cacheRead : 0,
|
|
1266
1494
|
cacheWriteTokens: Number.isFinite(cacheWrite) ? cacheWrite : 0,
|
|
1267
1495
|
totalTokens: Number.isFinite(finalTotal) ? finalTotal : 0,
|
|
1268
|
-
}
|
|
1496
|
+
};
|
|
1497
|
+
const key = buildUsageRecordKey(record);
|
|
1498
|
+
if (seen.has(key)) continue;
|
|
1499
|
+
seen.add(key);
|
|
1500
|
+
records.push(record);
|
|
1269
1501
|
} catch {
|
|
1270
1502
|
// ignore invalid lines
|
|
1271
1503
|
}
|
|
@@ -1288,7 +1520,25 @@ export function syncUsageFromSessions(
|
|
|
1288
1520
|
|
|
1289
1521
|
const state = readUsageState(statePath);
|
|
1290
1522
|
const files = state.files || {};
|
|
1291
|
-
|
|
1523
|
+
let sessions = state.sessions || {};
|
|
1524
|
+
const usageStat = readUsageFileStat(usagePath);
|
|
1525
|
+
const hasUsageData = !!usageStat && usageStat.isFile() && usageStat.size > 0;
|
|
1526
|
+
const sessionsEmpty = Object.keys(sessions).length === 0;
|
|
1527
|
+
const hasUsageMeta =
|
|
1528
|
+
Number.isFinite(state.usageMtimeMs ?? Number.NaN) &&
|
|
1529
|
+
Number.isFinite(state.usageSize ?? Number.NaN);
|
|
1530
|
+
const usageOutOfSync =
|
|
1531
|
+
hasUsageData &&
|
|
1532
|
+
(!hasUsageMeta ||
|
|
1533
|
+
state.usageMtimeMs !== usageStat.mtimeMs ||
|
|
1534
|
+
state.usageSize !== usageStat.size);
|
|
1535
|
+
if (hasUsageData && (sessionsEmpty || usageOutOfSync)) {
|
|
1536
|
+
const records = readUsageRecords(usagePath);
|
|
1537
|
+
if (records.length > 0) {
|
|
1538
|
+
sessions = buildUsageSessionsFromRecords(records);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
state.sessions = sessions;
|
|
1292
1542
|
const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
|
|
1293
1543
|
const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
|
|
1294
1544
|
|
|
@@ -1456,6 +1706,7 @@ export function syncUsageFromSessions(
|
|
|
1456
1706
|
|
|
1457
1707
|
state.files = files;
|
|
1458
1708
|
state.sessions = sessions;
|
|
1709
|
+
updateUsageStateMetadata(state, usagePath);
|
|
1459
1710
|
writeUsageState(statePath, state);
|
|
1460
1711
|
} finally {
|
|
1461
1712
|
releaseLock(lockPath, lockFd);
|