@praeviso/code-env-switch 0.1.5 → 0.1.7
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 +12 -0
- package/README_zh.md +12 -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 +280 -22
- package/docs/README.md +3 -0
- package/docs/README_zh.md +3 -0
- package/docs/usage.md +126 -0
- package/docs/usage_zh.md +126 -0
- 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 +293 -25
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;
|
|
@@ -79,12 +91,16 @@ interface UsageSessionEntry {
|
|
|
79
91
|
endTs: string | null;
|
|
80
92
|
cwd: string | null;
|
|
81
93
|
model?: string | null;
|
|
94
|
+
profileKey?: string | null;
|
|
95
|
+
profileName?: string | null;
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
interface UsageStateFile {
|
|
85
99
|
version: number;
|
|
86
100
|
files: Record<string, UsageStateEntry>;
|
|
87
101
|
sessions?: Record<string, UsageSessionEntry>;
|
|
102
|
+
usageMtimeMs?: number;
|
|
103
|
+
usageSize?: number;
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
interface ProfileLogEntry {
|
|
@@ -573,18 +589,26 @@ export function syncUsageFromStatuslineInput(
|
|
|
573
589
|
let deltaCacheRead = cacheReadTokens - prevCacheRead;
|
|
574
590
|
let deltaCacheWrite = cacheWriteTokens - prevCacheWrite;
|
|
575
591
|
let deltaTotal = totalTokens - prevTotal;
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
deltaInput < 0 ||
|
|
579
|
-
deltaOutput < 0 ||
|
|
580
|
-
deltaCacheRead < 0 ||
|
|
581
|
-
deltaCacheWrite < 0
|
|
582
|
-
) {
|
|
592
|
+
if (deltaTotal < 0) {
|
|
593
|
+
// Session reset: treat current totals as fresh usage.
|
|
583
594
|
deltaInput = inputTokens;
|
|
584
595
|
deltaOutput = outputTokens;
|
|
585
596
|
deltaCacheRead = cacheReadTokens;
|
|
586
597
|
deltaCacheWrite = cacheWriteTokens;
|
|
587
598
|
deltaTotal = totalTokens;
|
|
599
|
+
} else {
|
|
600
|
+
// Clamp negatives caused by reclassification (e.g. cache splits).
|
|
601
|
+
if (deltaInput < 0) deltaInput = 0;
|
|
602
|
+
if (deltaOutput < 0) deltaOutput = 0;
|
|
603
|
+
if (deltaCacheRead < 0) deltaCacheRead = 0;
|
|
604
|
+
if (deltaCacheWrite < 0) deltaCacheWrite = 0;
|
|
605
|
+
const breakdownTotal =
|
|
606
|
+
deltaInput + deltaOutput + deltaCacheRead + deltaCacheWrite;
|
|
607
|
+
if (deltaTotal === 0 && breakdownTotal === 0) {
|
|
608
|
+
deltaTotal = 0;
|
|
609
|
+
} else if (breakdownTotal > deltaTotal) {
|
|
610
|
+
deltaTotal = breakdownTotal;
|
|
611
|
+
}
|
|
588
612
|
}
|
|
589
613
|
|
|
590
614
|
if (deltaTotal > 0) {
|
|
@@ -616,8 +640,11 @@ export function syncUsageFromStatuslineInput(
|
|
|
616
640
|
endTs: now,
|
|
617
641
|
cwd: cwd || (prev ? prev.cwd : null),
|
|
618
642
|
model: resolvedModel,
|
|
643
|
+
profileKey: profileKey || null,
|
|
644
|
+
profileName: profileName || null,
|
|
619
645
|
};
|
|
620
646
|
state.sessions = sessions;
|
|
647
|
+
updateUsageStateMetadata(state, usagePath);
|
|
621
648
|
writeUsageState(statePath, state);
|
|
622
649
|
} finally {
|
|
623
650
|
releaseLock(lockPath, lockFd);
|
|
@@ -848,7 +875,15 @@ function readUsageState(statePath: string): UsageStateFile {
|
|
|
848
875
|
parsed.files && typeof parsed.files === "object" ? parsed.files : {};
|
|
849
876
|
const sessions =
|
|
850
877
|
parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
|
|
851
|
-
|
|
878
|
+
const usageMtimeMs = Number(parsed.usageMtimeMs);
|
|
879
|
+
const usageSize = Number(parsed.usageSize);
|
|
880
|
+
return {
|
|
881
|
+
version: 1,
|
|
882
|
+
files,
|
|
883
|
+
sessions,
|
|
884
|
+
usageMtimeMs: Number.isFinite(usageMtimeMs) ? usageMtimeMs : undefined,
|
|
885
|
+
usageSize: Number.isFinite(usageSize) ? usageSize : undefined,
|
|
886
|
+
};
|
|
852
887
|
} catch {
|
|
853
888
|
return { version: 1, files: {}, sessions: {} };
|
|
854
889
|
}
|
|
@@ -859,7 +894,91 @@ function writeUsageState(statePath: string, state: UsageStateFile) {
|
|
|
859
894
|
if (!fs.existsSync(dir)) {
|
|
860
895
|
fs.mkdirSync(dir, { recursive: true });
|
|
861
896
|
}
|
|
862
|
-
|
|
897
|
+
const payload = `${JSON.stringify(state, null, 2)}\n`;
|
|
898
|
+
const tmpPath = `${statePath}.tmp`;
|
|
899
|
+
try {
|
|
900
|
+
fs.writeFileSync(tmpPath, payload, "utf8");
|
|
901
|
+
fs.renameSync(tmpPath, statePath);
|
|
902
|
+
} catch {
|
|
903
|
+
try {
|
|
904
|
+
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
905
|
+
} catch {
|
|
906
|
+
// ignore cleanup failures
|
|
907
|
+
}
|
|
908
|
+
fs.writeFileSync(statePath, payload, "utf8");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function addSiblingBackupPaths(targets: Set<string>, filePath: string | null) {
|
|
913
|
+
if (!filePath) return;
|
|
914
|
+
const dir = path.dirname(filePath);
|
|
915
|
+
let entries: fs.Dirent[] = [];
|
|
916
|
+
try {
|
|
917
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
918
|
+
} catch {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const base = path.basename(filePath);
|
|
922
|
+
for (const entry of entries) {
|
|
923
|
+
if (!entry.isFile()) continue;
|
|
924
|
+
if (entry.name === base) continue;
|
|
925
|
+
if (entry.name.startsWith(`${base}.`)) {
|
|
926
|
+
targets.add(path.join(dir, entry.name));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function clearUsageHistory(
|
|
932
|
+
config: Config,
|
|
933
|
+
configPath: string | null
|
|
934
|
+
): UsageCleanupResult {
|
|
935
|
+
const targets = new Set<string>();
|
|
936
|
+
const usagePath = getUsagePath(config, configPath);
|
|
937
|
+
if (usagePath) {
|
|
938
|
+
targets.add(usagePath);
|
|
939
|
+
addSiblingBackupPaths(targets, usagePath);
|
|
940
|
+
}
|
|
941
|
+
const statePath = usagePath ? getUsageStatePath(usagePath, config) : null;
|
|
942
|
+
if (statePath) {
|
|
943
|
+
targets.add(statePath);
|
|
944
|
+
targets.add(`${statePath}.lock`);
|
|
945
|
+
addSiblingBackupPaths(targets, statePath);
|
|
946
|
+
}
|
|
947
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
948
|
+
if (profileLogPath) {
|
|
949
|
+
targets.add(profileLogPath);
|
|
950
|
+
addSiblingBackupPaths(targets, profileLogPath);
|
|
951
|
+
}
|
|
952
|
+
const debugPath = getStatuslineDebugPath(configPath);
|
|
953
|
+
if (debugPath) {
|
|
954
|
+
targets.add(debugPath);
|
|
955
|
+
addSiblingBackupPaths(targets, debugPath);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const removed: string[] = [];
|
|
959
|
+
const missing: string[] = [];
|
|
960
|
+
const failed: UsageCleanupFailure[] = [];
|
|
961
|
+
|
|
962
|
+
for (const target of targets) {
|
|
963
|
+
if (!target) continue;
|
|
964
|
+
if (!fs.existsSync(target)) {
|
|
965
|
+
missing.push(target);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const stat = fs.statSync(target);
|
|
970
|
+
if (!stat.isFile()) continue;
|
|
971
|
+
fs.unlinkSync(target);
|
|
972
|
+
removed.push(target);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
failed.push({
|
|
975
|
+
path: target,
|
|
976
|
+
error: err instanceof Error ? err.message : String(err),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return { removed, missing, failed };
|
|
863
982
|
}
|
|
864
983
|
|
|
865
984
|
function collectSessionFiles(root: string | null): string[] {
|
|
@@ -969,10 +1088,12 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
969
1088
|
let maxTotal = 0;
|
|
970
1089
|
let maxInput = 0;
|
|
971
1090
|
let maxOutput = 0;
|
|
1091
|
+
let maxCachedInput = 0;
|
|
972
1092
|
let hasTotal = false;
|
|
973
1093
|
let sumLast = 0;
|
|
974
1094
|
let sumLastInput = 0;
|
|
975
1095
|
let sumLastOutput = 0;
|
|
1096
|
+
let sumLastCachedInput = 0;
|
|
976
1097
|
const tsRange = { start: null as string | null, end: null as string | null };
|
|
977
1098
|
let cwd: string | null = null;
|
|
978
1099
|
let sessionId: string | null = null;
|
|
@@ -1012,19 +1133,25 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
1012
1133
|
if (totalTokens > maxTotal) maxTotal = totalTokens;
|
|
1013
1134
|
const totalInput = Number(totalUsage.input_tokens);
|
|
1014
1135
|
const totalOutput = Number(totalUsage.output_tokens);
|
|
1136
|
+
const totalCached = Number(totalUsage.cached_input_tokens);
|
|
1015
1137
|
if (Number.isFinite(totalInput) && totalInput > maxInput) {
|
|
1016
1138
|
maxInput = totalInput;
|
|
1017
1139
|
}
|
|
1018
1140
|
if (Number.isFinite(totalOutput) && totalOutput > maxOutput) {
|
|
1019
1141
|
maxOutput = totalOutput;
|
|
1020
1142
|
}
|
|
1143
|
+
if (Number.isFinite(totalCached) && totalCached > maxCachedInput) {
|
|
1144
|
+
maxCachedInput = totalCached;
|
|
1145
|
+
}
|
|
1021
1146
|
} else {
|
|
1022
1147
|
const lastTokens = Number(lastUsage.total_tokens);
|
|
1023
1148
|
if (Number.isFinite(lastTokens)) sumLast += lastTokens;
|
|
1024
1149
|
const lastInput = Number(lastUsage.input_tokens);
|
|
1025
1150
|
const lastOutput = Number(lastUsage.output_tokens);
|
|
1151
|
+
const lastCached = Number(lastUsage.cached_input_tokens);
|
|
1026
1152
|
if (Number.isFinite(lastInput)) sumLastInput += lastInput;
|
|
1027
1153
|
if (Number.isFinite(lastOutput)) sumLastOutput += lastOutput;
|
|
1154
|
+
if (Number.isFinite(lastCached)) sumLastCachedInput += lastCached;
|
|
1028
1155
|
}
|
|
1029
1156
|
} catch {
|
|
1030
1157
|
// ignore invalid lines
|
|
@@ -1035,14 +1162,21 @@ function parseCodexSessionFile(filePath: string): SessionStats {
|
|
|
1035
1162
|
maxTotal = sumLast;
|
|
1036
1163
|
maxInput = sumLastInput;
|
|
1037
1164
|
maxOutput = sumLastOutput;
|
|
1165
|
+
maxCachedInput = sumLastCachedInput;
|
|
1038
1166
|
}
|
|
1039
1167
|
|
|
1168
|
+
const cacheReadTokens = Math.max(0, maxCachedInput);
|
|
1169
|
+
const inputTokens =
|
|
1170
|
+
cacheReadTokens > 0 ? Math.max(0, maxInput - cacheReadTokens) : maxInput;
|
|
1171
|
+
const computedTotal = inputTokens + maxOutput + cacheReadTokens;
|
|
1172
|
+
const totalTokens = Math.max(maxTotal, computedTotal);
|
|
1173
|
+
|
|
1040
1174
|
return {
|
|
1041
|
-
inputTokens
|
|
1175
|
+
inputTokens,
|
|
1042
1176
|
outputTokens: maxOutput,
|
|
1043
|
-
cacheReadTokens
|
|
1177
|
+
cacheReadTokens,
|
|
1044
1178
|
cacheWriteTokens: 0,
|
|
1045
|
-
totalTokens
|
|
1179
|
+
totalTokens,
|
|
1046
1180
|
startTs: tsRange.start,
|
|
1047
1181
|
endTs: tsRange.end,
|
|
1048
1182
|
cwd,
|
|
@@ -1194,6 +1328,93 @@ function releaseLock(lockPath: string, fd: number | null) {
|
|
|
1194
1328
|
}
|
|
1195
1329
|
}
|
|
1196
1330
|
|
|
1331
|
+
function readUsageFileStat(usagePath: string): fs.Stats | null {
|
|
1332
|
+
if (!usagePath || !fs.existsSync(usagePath)) return null;
|
|
1333
|
+
try {
|
|
1334
|
+
return fs.statSync(usagePath);
|
|
1335
|
+
} catch {
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function buildUsageRecordKey(record: UsageRecord): string {
|
|
1341
|
+
return JSON.stringify([
|
|
1342
|
+
record.ts,
|
|
1343
|
+
record.type,
|
|
1344
|
+
record.profileKey ?? null,
|
|
1345
|
+
record.profileName ?? null,
|
|
1346
|
+
record.model ?? null,
|
|
1347
|
+
record.sessionId ?? null,
|
|
1348
|
+
toUsageNumber(record.inputTokens),
|
|
1349
|
+
toUsageNumber(record.outputTokens),
|
|
1350
|
+
toUsageNumber(record.cacheReadTokens),
|
|
1351
|
+
toUsageNumber(record.cacheWriteTokens),
|
|
1352
|
+
toUsageNumber(record.totalTokens),
|
|
1353
|
+
]);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function buildUsageSessionsFromRecords(
|
|
1357
|
+
records: UsageRecord[]
|
|
1358
|
+
): Record<string, UsageSessionEntry> {
|
|
1359
|
+
const sessions: Record<string, UsageSessionEntry> = {};
|
|
1360
|
+
const seen = new Set<string>();
|
|
1361
|
+
for (const record of records) {
|
|
1362
|
+
if (!record.sessionId) continue;
|
|
1363
|
+
const normalizedType = normalizeUsageType(record.type);
|
|
1364
|
+
if (!normalizedType) continue;
|
|
1365
|
+
const recordKey = buildUsageRecordKey(record);
|
|
1366
|
+
if (seen.has(recordKey)) continue;
|
|
1367
|
+
seen.add(recordKey);
|
|
1368
|
+
|
|
1369
|
+
const sessionKey = buildSessionKey(
|
|
1370
|
+
normalizedType as ProfileType,
|
|
1371
|
+
record.sessionId
|
|
1372
|
+
);
|
|
1373
|
+
let entry = sessions[sessionKey];
|
|
1374
|
+
if (!entry) {
|
|
1375
|
+
entry = {
|
|
1376
|
+
type: normalizedType as ProfileType,
|
|
1377
|
+
inputTokens: 0,
|
|
1378
|
+
outputTokens: 0,
|
|
1379
|
+
cacheReadTokens: 0,
|
|
1380
|
+
cacheWriteTokens: 0,
|
|
1381
|
+
totalTokens: 0,
|
|
1382
|
+
startTs: null,
|
|
1383
|
+
endTs: null,
|
|
1384
|
+
cwd: null,
|
|
1385
|
+
model: record.model || null,
|
|
1386
|
+
};
|
|
1387
|
+
sessions[sessionKey] = entry;
|
|
1388
|
+
}
|
|
1389
|
+
entry.inputTokens += toUsageNumber(record.inputTokens);
|
|
1390
|
+
entry.outputTokens += toUsageNumber(record.outputTokens);
|
|
1391
|
+
entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
|
|
1392
|
+
entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
|
|
1393
|
+
entry.totalTokens += toUsageNumber(record.totalTokens);
|
|
1394
|
+
if (record.profileKey) entry.profileKey = record.profileKey;
|
|
1395
|
+
if (record.profileName) entry.profileName = record.profileName;
|
|
1396
|
+
if (!entry.model && record.model) entry.model = record.model;
|
|
1397
|
+
if (record.ts) {
|
|
1398
|
+
const range = { start: entry.startTs, end: entry.endTs };
|
|
1399
|
+
updateMinMaxTs(range, record.ts);
|
|
1400
|
+
entry.startTs = range.start;
|
|
1401
|
+
entry.endTs = range.end;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return sessions;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function updateUsageStateMetadata(state: UsageStateFile, usagePath: string) {
|
|
1408
|
+
const stat = readUsageFileStat(usagePath);
|
|
1409
|
+
if (!stat || !stat.isFile()) {
|
|
1410
|
+
state.usageMtimeMs = undefined;
|
|
1411
|
+
state.usageSize = undefined;
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
state.usageMtimeMs = stat.mtimeMs;
|
|
1415
|
+
state.usageSize = stat.size;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1197
1418
|
function appendUsageRecord(usagePath: string, record: UsageRecord) {
|
|
1198
1419
|
const dir = path.dirname(usagePath);
|
|
1199
1420
|
if (!fs.existsSync(dir)) {
|
|
@@ -1207,13 +1428,15 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1207
1428
|
const raw = fs.readFileSync(usagePath, "utf8");
|
|
1208
1429
|
const lines = raw.split(/\r?\n/);
|
|
1209
1430
|
const records: UsageRecord[] = [];
|
|
1431
|
+
const seen = new Set<string>();
|
|
1210
1432
|
for (const line of lines) {
|
|
1211
1433
|
const trimmed = line.trim();
|
|
1212
1434
|
if (!trimmed) continue;
|
|
1213
1435
|
try {
|
|
1214
1436
|
const parsed = JSON.parse(trimmed);
|
|
1215
1437
|
if (!parsed || typeof parsed !== "object") continue;
|
|
1216
|
-
const
|
|
1438
|
+
const type = normalizeType(parsed.type) || String(parsed.type ?? "unknown");
|
|
1439
|
+
let input = Number(parsed.inputTokens ?? 0);
|
|
1217
1440
|
const output = Number(parsed.outputTokens ?? 0);
|
|
1218
1441
|
const cacheRead = Number(
|
|
1219
1442
|
parsed.cacheReadTokens ??
|
|
@@ -1228,6 +1451,18 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1228
1451
|
parsed.cacheWriteInputTokens ??
|
|
1229
1452
|
0
|
|
1230
1453
|
);
|
|
1454
|
+
if (
|
|
1455
|
+
type === "codex" &&
|
|
1456
|
+
Number.isFinite(cacheRead) &&
|
|
1457
|
+
cacheRead > 0 &&
|
|
1458
|
+
Number.isFinite(input) &&
|
|
1459
|
+
Number.isFinite(output)
|
|
1460
|
+
) {
|
|
1461
|
+
const rawTotal = Number(parsed.totalTokens);
|
|
1462
|
+
if (Number.isFinite(rawTotal) && rawTotal <= input + output) {
|
|
1463
|
+
input = Math.max(0, input - cacheRead);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1231
1466
|
const computedTotal =
|
|
1232
1467
|
(Number.isFinite(input) ? input : 0) +
|
|
1233
1468
|
(Number.isFinite(output) ? output : 0) +
|
|
@@ -1239,7 +1474,6 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1239
1474
|
const finalTotal = Number.isFinite(total)
|
|
1240
1475
|
? Math.max(total, computedTotal)
|
|
1241
1476
|
: computedTotal;
|
|
1242
|
-
const type = normalizeType(parsed.type) || String(parsed.type ?? "unknown");
|
|
1243
1477
|
const model =
|
|
1244
1478
|
normalizeModelValue(parsed.model) ||
|
|
1245
1479
|
normalizeModelValue(parsed.model_name) ||
|
|
@@ -1253,7 +1487,7 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1253
1487
|
parsed.sessionID ||
|
|
1254
1488
|
parsed.session ||
|
|
1255
1489
|
null;
|
|
1256
|
-
|
|
1490
|
+
const record: UsageRecord = {
|
|
1257
1491
|
ts: String(parsed.ts ?? ""),
|
|
1258
1492
|
type,
|
|
1259
1493
|
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
@@ -1265,7 +1499,11 @@ export function readUsageRecords(usagePath: string): UsageRecord[] {
|
|
|
1265
1499
|
cacheReadTokens: Number.isFinite(cacheRead) ? cacheRead : 0,
|
|
1266
1500
|
cacheWriteTokens: Number.isFinite(cacheWrite) ? cacheWrite : 0,
|
|
1267
1501
|
totalTokens: Number.isFinite(finalTotal) ? finalTotal : 0,
|
|
1268
|
-
}
|
|
1502
|
+
};
|
|
1503
|
+
const key = buildUsageRecordKey(record);
|
|
1504
|
+
if (seen.has(key)) continue;
|
|
1505
|
+
seen.add(key);
|
|
1506
|
+
records.push(record);
|
|
1269
1507
|
} catch {
|
|
1270
1508
|
// ignore invalid lines
|
|
1271
1509
|
}
|
|
@@ -1288,7 +1526,25 @@ export function syncUsageFromSessions(
|
|
|
1288
1526
|
|
|
1289
1527
|
const state = readUsageState(statePath);
|
|
1290
1528
|
const files = state.files || {};
|
|
1291
|
-
|
|
1529
|
+
let sessions = state.sessions || {};
|
|
1530
|
+
const usageStat = readUsageFileStat(usagePath);
|
|
1531
|
+
const hasUsageData = !!usageStat && usageStat.isFile() && usageStat.size > 0;
|
|
1532
|
+
const sessionsEmpty = Object.keys(sessions).length === 0;
|
|
1533
|
+
const hasUsageMeta =
|
|
1534
|
+
Number.isFinite(state.usageMtimeMs ?? Number.NaN) &&
|
|
1535
|
+
Number.isFinite(state.usageSize ?? Number.NaN);
|
|
1536
|
+
const usageOutOfSync =
|
|
1537
|
+
hasUsageData &&
|
|
1538
|
+
(!hasUsageMeta ||
|
|
1539
|
+
state.usageMtimeMs !== usageStat.mtimeMs ||
|
|
1540
|
+
state.usageSize !== usageStat.size);
|
|
1541
|
+
if (hasUsageData && (sessionsEmpty || usageOutOfSync)) {
|
|
1542
|
+
const records = readUsageRecords(usagePath);
|
|
1543
|
+
if (records.length > 0) {
|
|
1544
|
+
sessions = buildUsageSessionsFromRecords(records);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
state.sessions = sessions;
|
|
1292
1548
|
const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
|
|
1293
1549
|
const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
|
|
1294
1550
|
|
|
@@ -1313,17 +1569,26 @@ export function syncUsageFromSessions(
|
|
|
1313
1569
|
} catch {
|
|
1314
1570
|
return;
|
|
1315
1571
|
}
|
|
1316
|
-
const resolved = resolveProfileForSession(
|
|
1317
|
-
config,
|
|
1318
|
-
logEntries,
|
|
1319
|
-
type,
|
|
1320
|
-
filePath,
|
|
1321
|
-
stats.sessionId
|
|
1322
|
-
);
|
|
1323
|
-
if (!resolved.match) return;
|
|
1324
1572
|
const sessionKey =
|
|
1325
1573
|
stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
|
|
1326
1574
|
const sessionPrev = sessionKey ? sessions[sessionKey] : null;
|
|
1575
|
+
const sessionProfile =
|
|
1576
|
+
sessionPrev && (sessionPrev.profileKey || sessionPrev.profileName)
|
|
1577
|
+
? {
|
|
1578
|
+
profileKey: sessionPrev.profileKey || null,
|
|
1579
|
+
profileName: sessionPrev.profileName || null,
|
|
1580
|
+
}
|
|
1581
|
+
: null;
|
|
1582
|
+
const resolved = sessionProfile
|
|
1583
|
+
? { match: sessionProfile, ambiguous: false }
|
|
1584
|
+
: resolveProfileForSession(
|
|
1585
|
+
config,
|
|
1586
|
+
logEntries,
|
|
1587
|
+
type,
|
|
1588
|
+
filePath,
|
|
1589
|
+
stats.sessionId
|
|
1590
|
+
);
|
|
1591
|
+
if (!resolved.match) return;
|
|
1327
1592
|
const resolvedModel =
|
|
1328
1593
|
(sessionPrev && sessionPrev.model) ||
|
|
1329
1594
|
stats.model ||
|
|
@@ -1433,6 +1698,8 @@ export function syncUsageFromSessions(
|
|
|
1433
1698
|
endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
|
|
1434
1699
|
cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
|
|
1435
1700
|
model: resolvedModel,
|
|
1701
|
+
profileKey: resolved.match.profileKey,
|
|
1702
|
+
profileName: resolved.match.profileName,
|
|
1436
1703
|
};
|
|
1437
1704
|
}
|
|
1438
1705
|
files[filePath] = {
|
|
@@ -1456,6 +1723,7 @@ export function syncUsageFromSessions(
|
|
|
1456
1723
|
|
|
1457
1724
|
state.files = files;
|
|
1458
1725
|
state.sessions = sessions;
|
|
1726
|
+
updateUsageStateMetadata(state, usagePath);
|
|
1459
1727
|
writeUsageState(statePath, state);
|
|
1460
1728
|
} finally {
|
|
1461
1729
|
releaseLock(lockPath, lockFd);
|