@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/bin/usage/index.js
CHANGED
|
@@ -16,6 +16,7 @@ exports.syncUsageFromStatuslineInput = syncUsageFromStatuslineInput;
|
|
|
16
16
|
exports.logProfileUse = logProfileUse;
|
|
17
17
|
exports.logSessionBinding = logSessionBinding;
|
|
18
18
|
exports.readSessionBindingIndex = readSessionBindingIndex;
|
|
19
|
+
exports.clearUsageHistory = clearUsageHistory;
|
|
19
20
|
exports.readUsageRecords = readUsageRecords;
|
|
20
21
|
exports.syncUsageFromSessions = syncUsageFromSessions;
|
|
21
22
|
/**
|
|
@@ -26,6 +27,7 @@ const path = require("path");
|
|
|
26
27
|
const os = require("os");
|
|
27
28
|
const utils_1 = require("../shell/utils");
|
|
28
29
|
const type_1 = require("../profile/type");
|
|
30
|
+
const debug_1 = require("../statusline/debug");
|
|
29
31
|
const pricing_1 = require("./pricing");
|
|
30
32
|
function resolveProfileForRecord(config, type, record) {
|
|
31
33
|
if (record.profileKey && config.profiles && config.profiles[record.profileKey]) {
|
|
@@ -393,17 +395,32 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
|
|
|
393
395
|
let deltaCacheRead = cacheReadTokens - prevCacheRead;
|
|
394
396
|
let deltaCacheWrite = cacheWriteTokens - prevCacheWrite;
|
|
395
397
|
let deltaTotal = totalTokens - prevTotal;
|
|
396
|
-
if (deltaTotal < 0
|
|
397
|
-
|
|
398
|
-
deltaOutput < 0 ||
|
|
399
|
-
deltaCacheRead < 0 ||
|
|
400
|
-
deltaCacheWrite < 0) {
|
|
398
|
+
if (deltaTotal < 0) {
|
|
399
|
+
// Session reset: treat current totals as fresh usage.
|
|
401
400
|
deltaInput = inputTokens;
|
|
402
401
|
deltaOutput = outputTokens;
|
|
403
402
|
deltaCacheRead = cacheReadTokens;
|
|
404
403
|
deltaCacheWrite = cacheWriteTokens;
|
|
405
404
|
deltaTotal = totalTokens;
|
|
406
405
|
}
|
|
406
|
+
else {
|
|
407
|
+
// Clamp negatives caused by reclassification (e.g. cache splits).
|
|
408
|
+
if (deltaInput < 0)
|
|
409
|
+
deltaInput = 0;
|
|
410
|
+
if (deltaOutput < 0)
|
|
411
|
+
deltaOutput = 0;
|
|
412
|
+
if (deltaCacheRead < 0)
|
|
413
|
+
deltaCacheRead = 0;
|
|
414
|
+
if (deltaCacheWrite < 0)
|
|
415
|
+
deltaCacheWrite = 0;
|
|
416
|
+
const breakdownTotal = deltaInput + deltaOutput + deltaCacheRead + deltaCacheWrite;
|
|
417
|
+
if (deltaTotal === 0 && breakdownTotal === 0) {
|
|
418
|
+
deltaTotal = 0;
|
|
419
|
+
}
|
|
420
|
+
else if (breakdownTotal > deltaTotal) {
|
|
421
|
+
deltaTotal = breakdownTotal;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
407
424
|
if (deltaTotal > 0) {
|
|
408
425
|
const record = {
|
|
409
426
|
ts: new Date().toISOString(),
|
|
@@ -432,8 +449,11 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
|
|
|
432
449
|
endTs: now,
|
|
433
450
|
cwd: cwd || (prev ? prev.cwd : null),
|
|
434
451
|
model: resolvedModel,
|
|
452
|
+
profileKey: profileKey || null,
|
|
453
|
+
profileName: profileName || null,
|
|
435
454
|
};
|
|
436
455
|
state.sessions = sessions;
|
|
456
|
+
updateUsageStateMetadata(state, usagePath);
|
|
437
457
|
writeUsageState(statePath, state);
|
|
438
458
|
}
|
|
439
459
|
finally {
|
|
@@ -588,7 +608,15 @@ function readUsageState(statePath) {
|
|
|
588
608
|
}
|
|
589
609
|
const files = parsed.files && typeof parsed.files === "object" ? parsed.files : {};
|
|
590
610
|
const sessions = parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
|
|
591
|
-
|
|
611
|
+
const usageMtimeMs = Number(parsed.usageMtimeMs);
|
|
612
|
+
const usageSize = Number(parsed.usageSize);
|
|
613
|
+
return {
|
|
614
|
+
version: 1,
|
|
615
|
+
files,
|
|
616
|
+
sessions,
|
|
617
|
+
usageMtimeMs: Number.isFinite(usageMtimeMs) ? usageMtimeMs : undefined,
|
|
618
|
+
usageSize: Number.isFinite(usageSize) ? usageSize : undefined,
|
|
619
|
+
};
|
|
592
620
|
}
|
|
593
621
|
catch {
|
|
594
622
|
return { version: 1, files: {}, sessions: {} };
|
|
@@ -599,7 +627,93 @@ function writeUsageState(statePath, state) {
|
|
|
599
627
|
if (!fs.existsSync(dir)) {
|
|
600
628
|
fs.mkdirSync(dir, { recursive: true });
|
|
601
629
|
}
|
|
602
|
-
|
|
630
|
+
const payload = `${JSON.stringify(state, null, 2)}\n`;
|
|
631
|
+
const tmpPath = `${statePath}.tmp`;
|
|
632
|
+
try {
|
|
633
|
+
fs.writeFileSync(tmpPath, payload, "utf8");
|
|
634
|
+
fs.renameSync(tmpPath, statePath);
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
try {
|
|
638
|
+
if (fs.existsSync(tmpPath))
|
|
639
|
+
fs.unlinkSync(tmpPath);
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
// ignore cleanup failures
|
|
643
|
+
}
|
|
644
|
+
fs.writeFileSync(statePath, payload, "utf8");
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function addSiblingBackupPaths(targets, filePath) {
|
|
648
|
+
if (!filePath)
|
|
649
|
+
return;
|
|
650
|
+
const dir = path.dirname(filePath);
|
|
651
|
+
let entries = [];
|
|
652
|
+
try {
|
|
653
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const base = path.basename(filePath);
|
|
659
|
+
for (const entry of entries) {
|
|
660
|
+
if (!entry.isFile())
|
|
661
|
+
continue;
|
|
662
|
+
if (entry.name === base)
|
|
663
|
+
continue;
|
|
664
|
+
if (entry.name.startsWith(`${base}.`)) {
|
|
665
|
+
targets.add(path.join(dir, entry.name));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function clearUsageHistory(config, configPath) {
|
|
670
|
+
const targets = new Set();
|
|
671
|
+
const usagePath = getUsagePath(config, configPath);
|
|
672
|
+
if (usagePath) {
|
|
673
|
+
targets.add(usagePath);
|
|
674
|
+
addSiblingBackupPaths(targets, usagePath);
|
|
675
|
+
}
|
|
676
|
+
const statePath = usagePath ? getUsageStatePath(usagePath, config) : null;
|
|
677
|
+
if (statePath) {
|
|
678
|
+
targets.add(statePath);
|
|
679
|
+
targets.add(`${statePath}.lock`);
|
|
680
|
+
addSiblingBackupPaths(targets, statePath);
|
|
681
|
+
}
|
|
682
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
683
|
+
if (profileLogPath) {
|
|
684
|
+
targets.add(profileLogPath);
|
|
685
|
+
addSiblingBackupPaths(targets, profileLogPath);
|
|
686
|
+
}
|
|
687
|
+
const debugPath = (0, debug_1.getStatuslineDebugPath)(configPath);
|
|
688
|
+
if (debugPath) {
|
|
689
|
+
targets.add(debugPath);
|
|
690
|
+
addSiblingBackupPaths(targets, debugPath);
|
|
691
|
+
}
|
|
692
|
+
const removed = [];
|
|
693
|
+
const missing = [];
|
|
694
|
+
const failed = [];
|
|
695
|
+
for (const target of targets) {
|
|
696
|
+
if (!target)
|
|
697
|
+
continue;
|
|
698
|
+
if (!fs.existsSync(target)) {
|
|
699
|
+
missing.push(target);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const stat = fs.statSync(target);
|
|
704
|
+
if (!stat.isFile())
|
|
705
|
+
continue;
|
|
706
|
+
fs.unlinkSync(target);
|
|
707
|
+
removed.push(target);
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
failed.push({
|
|
711
|
+
path: target,
|
|
712
|
+
error: err instanceof Error ? err.message : String(err),
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return { removed, missing, failed };
|
|
603
717
|
}
|
|
604
718
|
function collectSessionFiles(root) {
|
|
605
719
|
if (!root || !fs.existsSync(root))
|
|
@@ -708,10 +822,12 @@ function parseCodexSessionFile(filePath) {
|
|
|
708
822
|
let maxTotal = 0;
|
|
709
823
|
let maxInput = 0;
|
|
710
824
|
let maxOutput = 0;
|
|
825
|
+
let maxCachedInput = 0;
|
|
711
826
|
let hasTotal = false;
|
|
712
827
|
let sumLast = 0;
|
|
713
828
|
let sumLastInput = 0;
|
|
714
829
|
let sumLastOutput = 0;
|
|
830
|
+
let sumLastCachedInput = 0;
|
|
715
831
|
const tsRange = { start: null, end: null };
|
|
716
832
|
let cwd = null;
|
|
717
833
|
let sessionId = null;
|
|
@@ -759,12 +875,16 @@ function parseCodexSessionFile(filePath) {
|
|
|
759
875
|
maxTotal = totalTokens;
|
|
760
876
|
const totalInput = Number(totalUsage.input_tokens);
|
|
761
877
|
const totalOutput = Number(totalUsage.output_tokens);
|
|
878
|
+
const totalCached = Number(totalUsage.cached_input_tokens);
|
|
762
879
|
if (Number.isFinite(totalInput) && totalInput > maxInput) {
|
|
763
880
|
maxInput = totalInput;
|
|
764
881
|
}
|
|
765
882
|
if (Number.isFinite(totalOutput) && totalOutput > maxOutput) {
|
|
766
883
|
maxOutput = totalOutput;
|
|
767
884
|
}
|
|
885
|
+
if (Number.isFinite(totalCached) && totalCached > maxCachedInput) {
|
|
886
|
+
maxCachedInput = totalCached;
|
|
887
|
+
}
|
|
768
888
|
}
|
|
769
889
|
else {
|
|
770
890
|
const lastTokens = Number(lastUsage.total_tokens);
|
|
@@ -772,10 +892,13 @@ function parseCodexSessionFile(filePath) {
|
|
|
772
892
|
sumLast += lastTokens;
|
|
773
893
|
const lastInput = Number(lastUsage.input_tokens);
|
|
774
894
|
const lastOutput = Number(lastUsage.output_tokens);
|
|
895
|
+
const lastCached = Number(lastUsage.cached_input_tokens);
|
|
775
896
|
if (Number.isFinite(lastInput))
|
|
776
897
|
sumLastInput += lastInput;
|
|
777
898
|
if (Number.isFinite(lastOutput))
|
|
778
899
|
sumLastOutput += lastOutput;
|
|
900
|
+
if (Number.isFinite(lastCached))
|
|
901
|
+
sumLastCachedInput += lastCached;
|
|
779
902
|
}
|
|
780
903
|
}
|
|
781
904
|
catch {
|
|
@@ -786,13 +909,18 @@ function parseCodexSessionFile(filePath) {
|
|
|
786
909
|
maxTotal = sumLast;
|
|
787
910
|
maxInput = sumLastInput;
|
|
788
911
|
maxOutput = sumLastOutput;
|
|
912
|
+
maxCachedInput = sumLastCachedInput;
|
|
789
913
|
}
|
|
914
|
+
const cacheReadTokens = Math.max(0, maxCachedInput);
|
|
915
|
+
const inputTokens = cacheReadTokens > 0 ? Math.max(0, maxInput - cacheReadTokens) : maxInput;
|
|
916
|
+
const computedTotal = inputTokens + maxOutput + cacheReadTokens;
|
|
917
|
+
const totalTokens = Math.max(maxTotal, computedTotal);
|
|
790
918
|
return {
|
|
791
|
-
inputTokens
|
|
919
|
+
inputTokens,
|
|
792
920
|
outputTokens: maxOutput,
|
|
793
|
-
cacheReadTokens
|
|
921
|
+
cacheReadTokens,
|
|
794
922
|
cacheWriteTokens: 0,
|
|
795
|
-
totalTokens
|
|
923
|
+
totalTokens,
|
|
796
924
|
startTs: tsRange.start,
|
|
797
925
|
endTs: tsRange.end,
|
|
798
926
|
cwd,
|
|
@@ -955,6 +1083,92 @@ function releaseLock(lockPath, fd) {
|
|
|
955
1083
|
// ignore
|
|
956
1084
|
}
|
|
957
1085
|
}
|
|
1086
|
+
function readUsageFileStat(usagePath) {
|
|
1087
|
+
if (!usagePath || !fs.existsSync(usagePath))
|
|
1088
|
+
return null;
|
|
1089
|
+
try {
|
|
1090
|
+
return fs.statSync(usagePath);
|
|
1091
|
+
}
|
|
1092
|
+
catch {
|
|
1093
|
+
return null;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
function buildUsageRecordKey(record) {
|
|
1097
|
+
var _a, _b, _c, _d;
|
|
1098
|
+
return JSON.stringify([
|
|
1099
|
+
record.ts,
|
|
1100
|
+
record.type,
|
|
1101
|
+
(_a = record.profileKey) !== null && _a !== void 0 ? _a : null,
|
|
1102
|
+
(_b = record.profileName) !== null && _b !== void 0 ? _b : null,
|
|
1103
|
+
(_c = record.model) !== null && _c !== void 0 ? _c : null,
|
|
1104
|
+
(_d = record.sessionId) !== null && _d !== void 0 ? _d : null,
|
|
1105
|
+
toUsageNumber(record.inputTokens),
|
|
1106
|
+
toUsageNumber(record.outputTokens),
|
|
1107
|
+
toUsageNumber(record.cacheReadTokens),
|
|
1108
|
+
toUsageNumber(record.cacheWriteTokens),
|
|
1109
|
+
toUsageNumber(record.totalTokens),
|
|
1110
|
+
]);
|
|
1111
|
+
}
|
|
1112
|
+
function buildUsageSessionsFromRecords(records) {
|
|
1113
|
+
const sessions = {};
|
|
1114
|
+
const seen = new Set();
|
|
1115
|
+
for (const record of records) {
|
|
1116
|
+
if (!record.sessionId)
|
|
1117
|
+
continue;
|
|
1118
|
+
const normalizedType = normalizeUsageType(record.type);
|
|
1119
|
+
if (!normalizedType)
|
|
1120
|
+
continue;
|
|
1121
|
+
const recordKey = buildUsageRecordKey(record);
|
|
1122
|
+
if (seen.has(recordKey))
|
|
1123
|
+
continue;
|
|
1124
|
+
seen.add(recordKey);
|
|
1125
|
+
const sessionKey = buildSessionKey(normalizedType, record.sessionId);
|
|
1126
|
+
let entry = sessions[sessionKey];
|
|
1127
|
+
if (!entry) {
|
|
1128
|
+
entry = {
|
|
1129
|
+
type: normalizedType,
|
|
1130
|
+
inputTokens: 0,
|
|
1131
|
+
outputTokens: 0,
|
|
1132
|
+
cacheReadTokens: 0,
|
|
1133
|
+
cacheWriteTokens: 0,
|
|
1134
|
+
totalTokens: 0,
|
|
1135
|
+
startTs: null,
|
|
1136
|
+
endTs: null,
|
|
1137
|
+
cwd: null,
|
|
1138
|
+
model: record.model || null,
|
|
1139
|
+
};
|
|
1140
|
+
sessions[sessionKey] = entry;
|
|
1141
|
+
}
|
|
1142
|
+
entry.inputTokens += toUsageNumber(record.inputTokens);
|
|
1143
|
+
entry.outputTokens += toUsageNumber(record.outputTokens);
|
|
1144
|
+
entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
|
|
1145
|
+
entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
|
|
1146
|
+
entry.totalTokens += toUsageNumber(record.totalTokens);
|
|
1147
|
+
if (record.profileKey)
|
|
1148
|
+
entry.profileKey = record.profileKey;
|
|
1149
|
+
if (record.profileName)
|
|
1150
|
+
entry.profileName = record.profileName;
|
|
1151
|
+
if (!entry.model && record.model)
|
|
1152
|
+
entry.model = record.model;
|
|
1153
|
+
if (record.ts) {
|
|
1154
|
+
const range = { start: entry.startTs, end: entry.endTs };
|
|
1155
|
+
updateMinMaxTs(range, record.ts);
|
|
1156
|
+
entry.startTs = range.start;
|
|
1157
|
+
entry.endTs = range.end;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return sessions;
|
|
1161
|
+
}
|
|
1162
|
+
function updateUsageStateMetadata(state, usagePath) {
|
|
1163
|
+
const stat = readUsageFileStat(usagePath);
|
|
1164
|
+
if (!stat || !stat.isFile()) {
|
|
1165
|
+
state.usageMtimeMs = undefined;
|
|
1166
|
+
state.usageSize = undefined;
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
state.usageMtimeMs = stat.mtimeMs;
|
|
1170
|
+
state.usageSize = stat.size;
|
|
1171
|
+
}
|
|
958
1172
|
function appendUsageRecord(usagePath, record) {
|
|
959
1173
|
const dir = path.dirname(usagePath);
|
|
960
1174
|
if (!fs.existsSync(dir)) {
|
|
@@ -969,6 +1183,7 @@ function readUsageRecords(usagePath) {
|
|
|
969
1183
|
const raw = fs.readFileSync(usagePath, "utf8");
|
|
970
1184
|
const lines = raw.split(/\r?\n/);
|
|
971
1185
|
const records = [];
|
|
1186
|
+
const seen = new Set();
|
|
972
1187
|
for (const line of lines) {
|
|
973
1188
|
const trimmed = line.trim();
|
|
974
1189
|
if (!trimmed)
|
|
@@ -977,19 +1192,29 @@ function readUsageRecords(usagePath) {
|
|
|
977
1192
|
const parsed = JSON.parse(trimmed);
|
|
978
1193
|
if (!parsed || typeof parsed !== "object")
|
|
979
1194
|
continue;
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
const
|
|
1195
|
+
const type = (0, type_1.normalizeType)(parsed.type) || String((_a = parsed.type) !== null && _a !== void 0 ? _a : "unknown");
|
|
1196
|
+
let input = Number((_b = parsed.inputTokens) !== null && _b !== void 0 ? _b : 0);
|
|
1197
|
+
const output = Number((_c = parsed.outputTokens) !== null && _c !== void 0 ? _c : 0);
|
|
1198
|
+
const cacheRead = Number((_f = (_e = (_d = parsed.cacheReadTokens) !== null && _d !== void 0 ? _d : parsed.cache_read_input_tokens) !== null && _e !== void 0 ? _e : parsed.cacheReadInputTokens) !== null && _f !== void 0 ? _f : 0);
|
|
1199
|
+
const cacheWrite = Number((_k = (_j = (_h = (_g = parsed.cacheWriteTokens) !== null && _g !== void 0 ? _g : parsed.cache_creation_input_tokens) !== null && _h !== void 0 ? _h : parsed.cache_write_input_tokens) !== null && _j !== void 0 ? _j : parsed.cacheWriteInputTokens) !== null && _k !== void 0 ? _k : 0);
|
|
1200
|
+
if (type === "codex" &&
|
|
1201
|
+
Number.isFinite(cacheRead) &&
|
|
1202
|
+
cacheRead > 0 &&
|
|
1203
|
+
Number.isFinite(input) &&
|
|
1204
|
+
Number.isFinite(output)) {
|
|
1205
|
+
const rawTotal = Number(parsed.totalTokens);
|
|
1206
|
+
if (Number.isFinite(rawTotal) && rawTotal <= input + output) {
|
|
1207
|
+
input = Math.max(0, input - cacheRead);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
984
1210
|
const computedTotal = (Number.isFinite(input) ? input : 0) +
|
|
985
1211
|
(Number.isFinite(output) ? output : 0) +
|
|
986
1212
|
(Number.isFinite(cacheRead) ? cacheRead : 0) +
|
|
987
1213
|
(Number.isFinite(cacheWrite) ? cacheWrite : 0);
|
|
988
|
-
const total = Number((
|
|
1214
|
+
const total = Number((_l = parsed.totalTokens) !== null && _l !== void 0 ? _l : computedTotal);
|
|
989
1215
|
const finalTotal = Number.isFinite(total)
|
|
990
1216
|
? Math.max(total, computedTotal)
|
|
991
1217
|
: computedTotal;
|
|
992
|
-
const type = (0, type_1.normalizeType)(parsed.type) || String((_l = parsed.type) !== null && _l !== void 0 ? _l : "unknown");
|
|
993
1218
|
const model = normalizeModelValue(parsed.model) ||
|
|
994
1219
|
normalizeModelValue(parsed.model_name) ||
|
|
995
1220
|
normalizeModelValue(parsed.modelName) ||
|
|
@@ -1001,7 +1226,7 @@ function readUsageRecords(usagePath) {
|
|
|
1001
1226
|
parsed.sessionID ||
|
|
1002
1227
|
parsed.session ||
|
|
1003
1228
|
null;
|
|
1004
|
-
|
|
1229
|
+
const record = {
|
|
1005
1230
|
ts: String((_m = parsed.ts) !== null && _m !== void 0 ? _m : ""),
|
|
1006
1231
|
type,
|
|
1007
1232
|
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
@@ -1013,7 +1238,12 @@ function readUsageRecords(usagePath) {
|
|
|
1013
1238
|
cacheReadTokens: Number.isFinite(cacheRead) ? cacheRead : 0,
|
|
1014
1239
|
cacheWriteTokens: Number.isFinite(cacheWrite) ? cacheWrite : 0,
|
|
1015
1240
|
totalTokens: Number.isFinite(finalTotal) ? finalTotal : 0,
|
|
1016
|
-
}
|
|
1241
|
+
};
|
|
1242
|
+
const key = buildUsageRecordKey(record);
|
|
1243
|
+
if (seen.has(key))
|
|
1244
|
+
continue;
|
|
1245
|
+
seen.add(key);
|
|
1246
|
+
records.push(record);
|
|
1017
1247
|
}
|
|
1018
1248
|
catch {
|
|
1019
1249
|
// ignore invalid lines
|
|
@@ -1022,6 +1252,7 @@ function readUsageRecords(usagePath) {
|
|
|
1022
1252
|
return records;
|
|
1023
1253
|
}
|
|
1024
1254
|
function syncUsageFromSessions(config, configPath, usagePath) {
|
|
1255
|
+
var _a, _b;
|
|
1025
1256
|
const statePath = getUsageStatePath(usagePath, config);
|
|
1026
1257
|
const lockPath = `${statePath}.lock`;
|
|
1027
1258
|
const lockFd = acquireLock(lockPath);
|
|
@@ -1032,7 +1263,23 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1032
1263
|
const logEntries = readProfileLogEntries([profileLogPath]);
|
|
1033
1264
|
const state = readUsageState(statePath);
|
|
1034
1265
|
const files = state.files || {};
|
|
1035
|
-
|
|
1266
|
+
let sessions = state.sessions || {};
|
|
1267
|
+
const usageStat = readUsageFileStat(usagePath);
|
|
1268
|
+
const hasUsageData = !!usageStat && usageStat.isFile() && usageStat.size > 0;
|
|
1269
|
+
const sessionsEmpty = Object.keys(sessions).length === 0;
|
|
1270
|
+
const hasUsageMeta = Number.isFinite((_a = state.usageMtimeMs) !== null && _a !== void 0 ? _a : Number.NaN) &&
|
|
1271
|
+
Number.isFinite((_b = state.usageSize) !== null && _b !== void 0 ? _b : Number.NaN);
|
|
1272
|
+
const usageOutOfSync = hasUsageData &&
|
|
1273
|
+
(!hasUsageMeta ||
|
|
1274
|
+
state.usageMtimeMs !== usageStat.mtimeMs ||
|
|
1275
|
+
state.usageSize !== usageStat.size);
|
|
1276
|
+
if (hasUsageData && (sessionsEmpty || usageOutOfSync)) {
|
|
1277
|
+
const records = readUsageRecords(usagePath);
|
|
1278
|
+
if (records.length > 0) {
|
|
1279
|
+
sessions = buildUsageSessionsFromRecords(records);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
state.sessions = sessions;
|
|
1036
1283
|
const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
|
|
1037
1284
|
const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
|
|
1038
1285
|
const processFile = (filePath, type) => {
|
|
@@ -1059,11 +1306,19 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1059
1306
|
catch {
|
|
1060
1307
|
return;
|
|
1061
1308
|
}
|
|
1062
|
-
const resolved = resolveProfileForSession(config, logEntries, type, filePath, stats.sessionId);
|
|
1063
|
-
if (!resolved.match)
|
|
1064
|
-
return;
|
|
1065
1309
|
const sessionKey = stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
|
|
1066
1310
|
const sessionPrev = sessionKey ? sessions[sessionKey] : null;
|
|
1311
|
+
const sessionProfile = sessionPrev && (sessionPrev.profileKey || sessionPrev.profileName)
|
|
1312
|
+
? {
|
|
1313
|
+
profileKey: sessionPrev.profileKey || null,
|
|
1314
|
+
profileName: sessionPrev.profileName || null,
|
|
1315
|
+
}
|
|
1316
|
+
: null;
|
|
1317
|
+
const resolved = sessionProfile
|
|
1318
|
+
? { match: sessionProfile, ambiguous: false }
|
|
1319
|
+
: resolveProfileForSession(config, logEntries, type, filePath, stats.sessionId);
|
|
1320
|
+
if (!resolved.match)
|
|
1321
|
+
return;
|
|
1067
1322
|
const resolvedModel = (sessionPrev && sessionPrev.model) ||
|
|
1068
1323
|
stats.model ||
|
|
1069
1324
|
(prev && prev.model) ||
|
|
@@ -1156,6 +1411,8 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1156
1411
|
endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
|
|
1157
1412
|
cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
|
|
1158
1413
|
model: resolvedModel,
|
|
1414
|
+
profileKey: resolved.match.profileKey,
|
|
1415
|
+
profileName: resolved.match.profileName,
|
|
1159
1416
|
};
|
|
1160
1417
|
}
|
|
1161
1418
|
files[filePath] = {
|
|
@@ -1179,6 +1436,7 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1179
1436
|
processFile(filePath, "claude");
|
|
1180
1437
|
state.files = files;
|
|
1181
1438
|
state.sessions = sessions;
|
|
1439
|
+
updateUsageStateMetadata(state, usagePath);
|
|
1182
1440
|
writeUsageState(statePath, state);
|
|
1183
1441
|
}
|
|
1184
1442
|
finally {
|
package/docs/README.md
ADDED
package/docs/usage.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Usage tracking
|
|
2
|
+
|
|
3
|
+
Usage stats are derived from two sources (statusline input + session logs) and appended to `usage.jsonl`.
|
|
4
|
+
|
|
5
|
+
## Files and paths
|
|
6
|
+
|
|
7
|
+
- `usage.jsonl`: JSONL records with `ts`, `type`, `profileKey`/`profileName`, `model`, `sessionId`, and token breakdowns.
|
|
8
|
+
- `usage.jsonl.state.json`: per-session totals + per-session-file metadata (mtime/size) used to compute deltas and avoid double counting.
|
|
9
|
+
- `profile-log.jsonl`: profile usage + session binding log (`use` and `session` events).
|
|
10
|
+
- `statusline-debug.jsonl`: optional debug capture when `CODE_ENV_STATUSLINE_DEBUG` is enabled.
|
|
11
|
+
- Paths can be overridden via `usagePath`, `usageStatePath`, `profileLogPath`, `codexSessionsPath`, `claudeSessionsPath`.
|
|
12
|
+
|
|
13
|
+
## Session binding (profile -> session)
|
|
14
|
+
|
|
15
|
+
- `codenv init` installs a shell wrapper so `codex`/`claude` run via `codenv launch`.
|
|
16
|
+
- `codenv launch` logs profile usage and then finds the latest unbound session file (prefers matching `cwd`, within a small grace window) and records the binding in `profile-log.jsonl`.
|
|
17
|
+
- Sync uses these bindings to attribute session usage to a profile.
|
|
18
|
+
|
|
19
|
+
## Statusline sync (`codenv statusline --sync-usage`)
|
|
20
|
+
|
|
21
|
+
- Requires `sessionId`, model, and a profile (`profileKey` or `profileName`) to write usage.
|
|
22
|
+
- Reads stdin JSON for totals and computes a delta against the last stored totals in the state file.
|
|
23
|
+
- If totals decrease (session reset), the current totals are treated as fresh usage; otherwise negative sub-deltas are clamped to zero.
|
|
24
|
+
- If the token breakdown exceeds the reported total, the breakdown sum wins.
|
|
25
|
+
- Appends a delta record to `usage.jsonl` and updates the per-session totals in the state file.
|
|
26
|
+
|
|
27
|
+
## Session log sync (`--sync-usage` and `codenv list`)
|
|
28
|
+
|
|
29
|
+
- Scans Codex sessions under `CODEX_HOME/sessions` (or `~/.codex/sessions`) and Claude sessions under `CLAUDE_HOME/projects` (or `~/.claude/projects`).
|
|
30
|
+
- Codex: parses `event_msg` token_count records and uses max totals; cached input is recorded as cache read and subtracted from input when possible.
|
|
31
|
+
- Claude: sums `message.usage` input/output/cache tokens across the file.
|
|
32
|
+
- Deltas are computed against prior file metadata and per-session maxima in the state file; files without a resolved binding are skipped.
|
|
33
|
+
|
|
34
|
+
## Daily totals
|
|
35
|
+
|
|
36
|
+
- "Today" is computed in local time between 00:00 and the next 00:00.
|
|
37
|
+
|
|
38
|
+
## Cost calculation
|
|
39
|
+
|
|
40
|
+
- Uses pricing from the profile (or `pricing.models`, plus defaults) and requires token splits; if splits are missing, cost is omitted.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
### `usage.jsonl` record
|
|
45
|
+
|
|
46
|
+
Each line is a JSON object (fields may be null/omitted depending on the source):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"ts":"2026-01-25T12:34:56.789Z","type":"codex","profileKey":"p_a1b2c3","profileName":"primary","model":"gpt-5.1-codex","sessionId":"a6f9c4d8-1234-5678-9abc-def012345678","inputTokens":1200,"outputTokens":300,"cacheReadTokens":200,"cacheWriteTokens":0,"totalTokens":1700}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`totalTokens` is the max of the reported total and the breakdown sum, so it can be >= input + output + cache.
|
|
53
|
+
|
|
54
|
+
## Statusline input examples (Codex/Claude)
|
|
55
|
+
|
|
56
|
+
To see real payloads, set `CODE_ENV_STATUSLINE_DEBUG=1` and read the JSONL entries in
|
|
57
|
+
`statusline-debug.jsonl` (or the path from `CODE_ENV_STATUSLINE_DEBUG_PATH`).
|
|
58
|
+
|
|
59
|
+
### Codex (token_usage totals)
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"type": "codex",
|
|
64
|
+
"session_id": "a6f9c4d8-1234-5678-9abc-def012345678",
|
|
65
|
+
"profile": { "key": "p_a1b2c3", "name": "primary", "type": "codex" },
|
|
66
|
+
"model": "gpt-5.1-codex",
|
|
67
|
+
"token_usage": {
|
|
68
|
+
"total_token_usage": {
|
|
69
|
+
"input_tokens": 1200,
|
|
70
|
+
"output_tokens": 300,
|
|
71
|
+
"cached_input_tokens": 200,
|
|
72
|
+
"cache_creation_input_tokens": 50,
|
|
73
|
+
"total_tokens": 1750
|
|
74
|
+
},
|
|
75
|
+
"last_token_usage": {
|
|
76
|
+
"input_tokens": 100,
|
|
77
|
+
"output_tokens": 20,
|
|
78
|
+
"cached_input_tokens": 10,
|
|
79
|
+
"cache_creation_input_tokens": 0,
|
|
80
|
+
"total_tokens": 130
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Accepted aliases (non-exhaustive):
|
|
87
|
+
- `token_usage.total_token_usage` or `token_usage.totalTokenUsage`
|
|
88
|
+
- `last_token_usage` or `lastTokenUsage`
|
|
89
|
+
- `input_tokens` / `inputTokens` / `input`
|
|
90
|
+
- `output_tokens` / `outputTokens` / `output` / `reasoning_output_tokens`
|
|
91
|
+
- `cached_input_tokens` / `cache_read_input_tokens`
|
|
92
|
+
- `cache_creation_input_tokens` / `cache_write_input_tokens`
|
|
93
|
+
- `total_tokens` / `totalTokens` / `total`
|
|
94
|
+
|
|
95
|
+
`token_usage` can also be a number, or the payload can provide `usage` directly.
|
|
96
|
+
|
|
97
|
+
### Claude (context_window totals)
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"type": "claude",
|
|
102
|
+
"session_id": "1f2e3d4c-5678-90ab-cdef-1234567890ab",
|
|
103
|
+
"profile": { "key": "p_c3d4e5", "name": "default", "type": "claude" },
|
|
104
|
+
"model": { "display_name": "Claude Sonnet 4.5" },
|
|
105
|
+
"context_window": {
|
|
106
|
+
"total_input_tokens": 800,
|
|
107
|
+
"total_output_tokens": 250,
|
|
108
|
+
"current_usage": {
|
|
109
|
+
"cache_read_input_tokens": 100,
|
|
110
|
+
"cache_creation_input_tokens": 40
|
|
111
|
+
},
|
|
112
|
+
"context_window_size": 200000
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Accepted aliases (non-exhaustive):
|
|
118
|
+
- `context_window` or `contextWindow`
|
|
119
|
+
- `current_usage` or `currentUsage`
|
|
120
|
+
- `total_input_tokens` / `totalInputTokens`
|
|
121
|
+
- `total_output_tokens` / `totalOutputTokens`
|
|
122
|
+
- `cache_read_input_tokens` / `cacheReadInputTokens`
|
|
123
|
+
- `cache_creation_input_tokens` / `cacheWriteInputTokens`
|
|
124
|
+
|
|
125
|
+
`usage` can also be provided directly with `todayTokens` / `totalTokens` / `inputTokens` /
|
|
126
|
+
`outputTokens` / `cacheReadTokens` / `cacheWriteTokens`.
|