@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
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(),
|
|
@@ -434,6 +451,7 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
|
|
|
434
451
|
model: resolvedModel,
|
|
435
452
|
};
|
|
436
453
|
state.sessions = sessions;
|
|
454
|
+
updateUsageStateMetadata(state, usagePath);
|
|
437
455
|
writeUsageState(statePath, state);
|
|
438
456
|
}
|
|
439
457
|
finally {
|
|
@@ -588,7 +606,15 @@ function readUsageState(statePath) {
|
|
|
588
606
|
}
|
|
589
607
|
const files = parsed.files && typeof parsed.files === "object" ? parsed.files : {};
|
|
590
608
|
const sessions = parsed.sessions && typeof parsed.sessions === "object" ? parsed.sessions : {};
|
|
591
|
-
|
|
609
|
+
const usageMtimeMs = Number(parsed.usageMtimeMs);
|
|
610
|
+
const usageSize = Number(parsed.usageSize);
|
|
611
|
+
return {
|
|
612
|
+
version: 1,
|
|
613
|
+
files,
|
|
614
|
+
sessions,
|
|
615
|
+
usageMtimeMs: Number.isFinite(usageMtimeMs) ? usageMtimeMs : undefined,
|
|
616
|
+
usageSize: Number.isFinite(usageSize) ? usageSize : undefined,
|
|
617
|
+
};
|
|
592
618
|
}
|
|
593
619
|
catch {
|
|
594
620
|
return { version: 1, files: {}, sessions: {} };
|
|
@@ -599,7 +625,93 @@ function writeUsageState(statePath, state) {
|
|
|
599
625
|
if (!fs.existsSync(dir)) {
|
|
600
626
|
fs.mkdirSync(dir, { recursive: true });
|
|
601
627
|
}
|
|
602
|
-
|
|
628
|
+
const payload = `${JSON.stringify(state, null, 2)}\n`;
|
|
629
|
+
const tmpPath = `${statePath}.tmp`;
|
|
630
|
+
try {
|
|
631
|
+
fs.writeFileSync(tmpPath, payload, "utf8");
|
|
632
|
+
fs.renameSync(tmpPath, statePath);
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
try {
|
|
636
|
+
if (fs.existsSync(tmpPath))
|
|
637
|
+
fs.unlinkSync(tmpPath);
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
// ignore cleanup failures
|
|
641
|
+
}
|
|
642
|
+
fs.writeFileSync(statePath, payload, "utf8");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function addSiblingBackupPaths(targets, filePath) {
|
|
646
|
+
if (!filePath)
|
|
647
|
+
return;
|
|
648
|
+
const dir = path.dirname(filePath);
|
|
649
|
+
let entries = [];
|
|
650
|
+
try {
|
|
651
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const base = path.basename(filePath);
|
|
657
|
+
for (const entry of entries) {
|
|
658
|
+
if (!entry.isFile())
|
|
659
|
+
continue;
|
|
660
|
+
if (entry.name === base)
|
|
661
|
+
continue;
|
|
662
|
+
if (entry.name.startsWith(`${base}.`)) {
|
|
663
|
+
targets.add(path.join(dir, entry.name));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function clearUsageHistory(config, configPath) {
|
|
668
|
+
const targets = new Set();
|
|
669
|
+
const usagePath = getUsagePath(config, configPath);
|
|
670
|
+
if (usagePath) {
|
|
671
|
+
targets.add(usagePath);
|
|
672
|
+
addSiblingBackupPaths(targets, usagePath);
|
|
673
|
+
}
|
|
674
|
+
const statePath = usagePath ? getUsageStatePath(usagePath, config) : null;
|
|
675
|
+
if (statePath) {
|
|
676
|
+
targets.add(statePath);
|
|
677
|
+
targets.add(`${statePath}.lock`);
|
|
678
|
+
addSiblingBackupPaths(targets, statePath);
|
|
679
|
+
}
|
|
680
|
+
const profileLogPath = getProfileLogPath(config, configPath);
|
|
681
|
+
if (profileLogPath) {
|
|
682
|
+
targets.add(profileLogPath);
|
|
683
|
+
addSiblingBackupPaths(targets, profileLogPath);
|
|
684
|
+
}
|
|
685
|
+
const debugPath = (0, debug_1.getStatuslineDebugPath)(configPath);
|
|
686
|
+
if (debugPath) {
|
|
687
|
+
targets.add(debugPath);
|
|
688
|
+
addSiblingBackupPaths(targets, debugPath);
|
|
689
|
+
}
|
|
690
|
+
const removed = [];
|
|
691
|
+
const missing = [];
|
|
692
|
+
const failed = [];
|
|
693
|
+
for (const target of targets) {
|
|
694
|
+
if (!target)
|
|
695
|
+
continue;
|
|
696
|
+
if (!fs.existsSync(target)) {
|
|
697
|
+
missing.push(target);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
const stat = fs.statSync(target);
|
|
702
|
+
if (!stat.isFile())
|
|
703
|
+
continue;
|
|
704
|
+
fs.unlinkSync(target);
|
|
705
|
+
removed.push(target);
|
|
706
|
+
}
|
|
707
|
+
catch (err) {
|
|
708
|
+
failed.push({
|
|
709
|
+
path: target,
|
|
710
|
+
error: err instanceof Error ? err.message : String(err),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return { removed, missing, failed };
|
|
603
715
|
}
|
|
604
716
|
function collectSessionFiles(root) {
|
|
605
717
|
if (!root || !fs.existsSync(root))
|
|
@@ -708,10 +820,12 @@ function parseCodexSessionFile(filePath) {
|
|
|
708
820
|
let maxTotal = 0;
|
|
709
821
|
let maxInput = 0;
|
|
710
822
|
let maxOutput = 0;
|
|
823
|
+
let maxCachedInput = 0;
|
|
711
824
|
let hasTotal = false;
|
|
712
825
|
let sumLast = 0;
|
|
713
826
|
let sumLastInput = 0;
|
|
714
827
|
let sumLastOutput = 0;
|
|
828
|
+
let sumLastCachedInput = 0;
|
|
715
829
|
const tsRange = { start: null, end: null };
|
|
716
830
|
let cwd = null;
|
|
717
831
|
let sessionId = null;
|
|
@@ -759,12 +873,16 @@ function parseCodexSessionFile(filePath) {
|
|
|
759
873
|
maxTotal = totalTokens;
|
|
760
874
|
const totalInput = Number(totalUsage.input_tokens);
|
|
761
875
|
const totalOutput = Number(totalUsage.output_tokens);
|
|
876
|
+
const totalCached = Number(totalUsage.cached_input_tokens);
|
|
762
877
|
if (Number.isFinite(totalInput) && totalInput > maxInput) {
|
|
763
878
|
maxInput = totalInput;
|
|
764
879
|
}
|
|
765
880
|
if (Number.isFinite(totalOutput) && totalOutput > maxOutput) {
|
|
766
881
|
maxOutput = totalOutput;
|
|
767
882
|
}
|
|
883
|
+
if (Number.isFinite(totalCached) && totalCached > maxCachedInput) {
|
|
884
|
+
maxCachedInput = totalCached;
|
|
885
|
+
}
|
|
768
886
|
}
|
|
769
887
|
else {
|
|
770
888
|
const lastTokens = Number(lastUsage.total_tokens);
|
|
@@ -772,10 +890,13 @@ function parseCodexSessionFile(filePath) {
|
|
|
772
890
|
sumLast += lastTokens;
|
|
773
891
|
const lastInput = Number(lastUsage.input_tokens);
|
|
774
892
|
const lastOutput = Number(lastUsage.output_tokens);
|
|
893
|
+
const lastCached = Number(lastUsage.cached_input_tokens);
|
|
775
894
|
if (Number.isFinite(lastInput))
|
|
776
895
|
sumLastInput += lastInput;
|
|
777
896
|
if (Number.isFinite(lastOutput))
|
|
778
897
|
sumLastOutput += lastOutput;
|
|
898
|
+
if (Number.isFinite(lastCached))
|
|
899
|
+
sumLastCachedInput += lastCached;
|
|
779
900
|
}
|
|
780
901
|
}
|
|
781
902
|
catch {
|
|
@@ -786,13 +907,18 @@ function parseCodexSessionFile(filePath) {
|
|
|
786
907
|
maxTotal = sumLast;
|
|
787
908
|
maxInput = sumLastInput;
|
|
788
909
|
maxOutput = sumLastOutput;
|
|
910
|
+
maxCachedInput = sumLastCachedInput;
|
|
789
911
|
}
|
|
912
|
+
const cacheReadTokens = Math.max(0, maxCachedInput);
|
|
913
|
+
const inputTokens = cacheReadTokens > 0 ? Math.max(0, maxInput - cacheReadTokens) : maxInput;
|
|
914
|
+
const computedTotal = inputTokens + maxOutput + cacheReadTokens;
|
|
915
|
+
const totalTokens = Math.max(maxTotal, computedTotal);
|
|
790
916
|
return {
|
|
791
|
-
inputTokens
|
|
917
|
+
inputTokens,
|
|
792
918
|
outputTokens: maxOutput,
|
|
793
|
-
cacheReadTokens
|
|
919
|
+
cacheReadTokens,
|
|
794
920
|
cacheWriteTokens: 0,
|
|
795
|
-
totalTokens
|
|
921
|
+
totalTokens,
|
|
796
922
|
startTs: tsRange.start,
|
|
797
923
|
endTs: tsRange.end,
|
|
798
924
|
cwd,
|
|
@@ -955,6 +1081,88 @@ function releaseLock(lockPath, fd) {
|
|
|
955
1081
|
// ignore
|
|
956
1082
|
}
|
|
957
1083
|
}
|
|
1084
|
+
function readUsageFileStat(usagePath) {
|
|
1085
|
+
if (!usagePath || !fs.existsSync(usagePath))
|
|
1086
|
+
return null;
|
|
1087
|
+
try {
|
|
1088
|
+
return fs.statSync(usagePath);
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function buildUsageRecordKey(record) {
|
|
1095
|
+
var _a, _b, _c, _d;
|
|
1096
|
+
return JSON.stringify([
|
|
1097
|
+
record.ts,
|
|
1098
|
+
record.type,
|
|
1099
|
+
(_a = record.profileKey) !== null && _a !== void 0 ? _a : null,
|
|
1100
|
+
(_b = record.profileName) !== null && _b !== void 0 ? _b : null,
|
|
1101
|
+
(_c = record.model) !== null && _c !== void 0 ? _c : null,
|
|
1102
|
+
(_d = record.sessionId) !== null && _d !== void 0 ? _d : null,
|
|
1103
|
+
toUsageNumber(record.inputTokens),
|
|
1104
|
+
toUsageNumber(record.outputTokens),
|
|
1105
|
+
toUsageNumber(record.cacheReadTokens),
|
|
1106
|
+
toUsageNumber(record.cacheWriteTokens),
|
|
1107
|
+
toUsageNumber(record.totalTokens),
|
|
1108
|
+
]);
|
|
1109
|
+
}
|
|
1110
|
+
function buildUsageSessionsFromRecords(records) {
|
|
1111
|
+
const sessions = {};
|
|
1112
|
+
const seen = new Set();
|
|
1113
|
+
for (const record of records) {
|
|
1114
|
+
if (!record.sessionId)
|
|
1115
|
+
continue;
|
|
1116
|
+
const normalizedType = normalizeUsageType(record.type);
|
|
1117
|
+
if (!normalizedType)
|
|
1118
|
+
continue;
|
|
1119
|
+
const recordKey = buildUsageRecordKey(record);
|
|
1120
|
+
if (seen.has(recordKey))
|
|
1121
|
+
continue;
|
|
1122
|
+
seen.add(recordKey);
|
|
1123
|
+
const sessionKey = buildSessionKey(normalizedType, record.sessionId);
|
|
1124
|
+
let entry = sessions[sessionKey];
|
|
1125
|
+
if (!entry) {
|
|
1126
|
+
entry = {
|
|
1127
|
+
type: normalizedType,
|
|
1128
|
+
inputTokens: 0,
|
|
1129
|
+
outputTokens: 0,
|
|
1130
|
+
cacheReadTokens: 0,
|
|
1131
|
+
cacheWriteTokens: 0,
|
|
1132
|
+
totalTokens: 0,
|
|
1133
|
+
startTs: null,
|
|
1134
|
+
endTs: null,
|
|
1135
|
+
cwd: null,
|
|
1136
|
+
model: record.model || null,
|
|
1137
|
+
};
|
|
1138
|
+
sessions[sessionKey] = entry;
|
|
1139
|
+
}
|
|
1140
|
+
entry.inputTokens += toUsageNumber(record.inputTokens);
|
|
1141
|
+
entry.outputTokens += toUsageNumber(record.outputTokens);
|
|
1142
|
+
entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
|
|
1143
|
+
entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
|
|
1144
|
+
entry.totalTokens += toUsageNumber(record.totalTokens);
|
|
1145
|
+
if (!entry.model && record.model)
|
|
1146
|
+
entry.model = record.model;
|
|
1147
|
+
if (record.ts) {
|
|
1148
|
+
const range = { start: entry.startTs, end: entry.endTs };
|
|
1149
|
+
updateMinMaxTs(range, record.ts);
|
|
1150
|
+
entry.startTs = range.start;
|
|
1151
|
+
entry.endTs = range.end;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return sessions;
|
|
1155
|
+
}
|
|
1156
|
+
function updateUsageStateMetadata(state, usagePath) {
|
|
1157
|
+
const stat = readUsageFileStat(usagePath);
|
|
1158
|
+
if (!stat || !stat.isFile()) {
|
|
1159
|
+
state.usageMtimeMs = undefined;
|
|
1160
|
+
state.usageSize = undefined;
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
state.usageMtimeMs = stat.mtimeMs;
|
|
1164
|
+
state.usageSize = stat.size;
|
|
1165
|
+
}
|
|
958
1166
|
function appendUsageRecord(usagePath, record) {
|
|
959
1167
|
const dir = path.dirname(usagePath);
|
|
960
1168
|
if (!fs.existsSync(dir)) {
|
|
@@ -969,6 +1177,7 @@ function readUsageRecords(usagePath) {
|
|
|
969
1177
|
const raw = fs.readFileSync(usagePath, "utf8");
|
|
970
1178
|
const lines = raw.split(/\r?\n/);
|
|
971
1179
|
const records = [];
|
|
1180
|
+
const seen = new Set();
|
|
972
1181
|
for (const line of lines) {
|
|
973
1182
|
const trimmed = line.trim();
|
|
974
1183
|
if (!trimmed)
|
|
@@ -977,19 +1186,29 @@ function readUsageRecords(usagePath) {
|
|
|
977
1186
|
const parsed = JSON.parse(trimmed);
|
|
978
1187
|
if (!parsed || typeof parsed !== "object")
|
|
979
1188
|
continue;
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
const
|
|
1189
|
+
const type = (0, type_1.normalizeType)(parsed.type) || String((_a = parsed.type) !== null && _a !== void 0 ? _a : "unknown");
|
|
1190
|
+
let input = Number((_b = parsed.inputTokens) !== null && _b !== void 0 ? _b : 0);
|
|
1191
|
+
const output = Number((_c = parsed.outputTokens) !== null && _c !== void 0 ? _c : 0);
|
|
1192
|
+
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);
|
|
1193
|
+
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);
|
|
1194
|
+
if (type === "codex" &&
|
|
1195
|
+
Number.isFinite(cacheRead) &&
|
|
1196
|
+
cacheRead > 0 &&
|
|
1197
|
+
Number.isFinite(input) &&
|
|
1198
|
+
Number.isFinite(output)) {
|
|
1199
|
+
const rawTotal = Number(parsed.totalTokens);
|
|
1200
|
+
if (Number.isFinite(rawTotal) && rawTotal <= input + output) {
|
|
1201
|
+
input = Math.max(0, input - cacheRead);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
984
1204
|
const computedTotal = (Number.isFinite(input) ? input : 0) +
|
|
985
1205
|
(Number.isFinite(output) ? output : 0) +
|
|
986
1206
|
(Number.isFinite(cacheRead) ? cacheRead : 0) +
|
|
987
1207
|
(Number.isFinite(cacheWrite) ? cacheWrite : 0);
|
|
988
|
-
const total = Number((
|
|
1208
|
+
const total = Number((_l = parsed.totalTokens) !== null && _l !== void 0 ? _l : computedTotal);
|
|
989
1209
|
const finalTotal = Number.isFinite(total)
|
|
990
1210
|
? Math.max(total, computedTotal)
|
|
991
1211
|
: computedTotal;
|
|
992
|
-
const type = (0, type_1.normalizeType)(parsed.type) || String((_l = parsed.type) !== null && _l !== void 0 ? _l : "unknown");
|
|
993
1212
|
const model = normalizeModelValue(parsed.model) ||
|
|
994
1213
|
normalizeModelValue(parsed.model_name) ||
|
|
995
1214
|
normalizeModelValue(parsed.modelName) ||
|
|
@@ -1001,7 +1220,7 @@ function readUsageRecords(usagePath) {
|
|
|
1001
1220
|
parsed.sessionID ||
|
|
1002
1221
|
parsed.session ||
|
|
1003
1222
|
null;
|
|
1004
|
-
|
|
1223
|
+
const record = {
|
|
1005
1224
|
ts: String((_m = parsed.ts) !== null && _m !== void 0 ? _m : ""),
|
|
1006
1225
|
type,
|
|
1007
1226
|
profileKey: parsed.profileKey ? String(parsed.profileKey) : null,
|
|
@@ -1013,7 +1232,12 @@ function readUsageRecords(usagePath) {
|
|
|
1013
1232
|
cacheReadTokens: Number.isFinite(cacheRead) ? cacheRead : 0,
|
|
1014
1233
|
cacheWriteTokens: Number.isFinite(cacheWrite) ? cacheWrite : 0,
|
|
1015
1234
|
totalTokens: Number.isFinite(finalTotal) ? finalTotal : 0,
|
|
1016
|
-
}
|
|
1235
|
+
};
|
|
1236
|
+
const key = buildUsageRecordKey(record);
|
|
1237
|
+
if (seen.has(key))
|
|
1238
|
+
continue;
|
|
1239
|
+
seen.add(key);
|
|
1240
|
+
records.push(record);
|
|
1017
1241
|
}
|
|
1018
1242
|
catch {
|
|
1019
1243
|
// ignore invalid lines
|
|
@@ -1022,6 +1246,7 @@ function readUsageRecords(usagePath) {
|
|
|
1022
1246
|
return records;
|
|
1023
1247
|
}
|
|
1024
1248
|
function syncUsageFromSessions(config, configPath, usagePath) {
|
|
1249
|
+
var _a, _b;
|
|
1025
1250
|
const statePath = getUsageStatePath(usagePath, config);
|
|
1026
1251
|
const lockPath = `${statePath}.lock`;
|
|
1027
1252
|
const lockFd = acquireLock(lockPath);
|
|
@@ -1032,7 +1257,23 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1032
1257
|
const logEntries = readProfileLogEntries([profileLogPath]);
|
|
1033
1258
|
const state = readUsageState(statePath);
|
|
1034
1259
|
const files = state.files || {};
|
|
1035
|
-
|
|
1260
|
+
let sessions = state.sessions || {};
|
|
1261
|
+
const usageStat = readUsageFileStat(usagePath);
|
|
1262
|
+
const hasUsageData = !!usageStat && usageStat.isFile() && usageStat.size > 0;
|
|
1263
|
+
const sessionsEmpty = Object.keys(sessions).length === 0;
|
|
1264
|
+
const hasUsageMeta = Number.isFinite((_a = state.usageMtimeMs) !== null && _a !== void 0 ? _a : Number.NaN) &&
|
|
1265
|
+
Number.isFinite((_b = state.usageSize) !== null && _b !== void 0 ? _b : Number.NaN);
|
|
1266
|
+
const usageOutOfSync = hasUsageData &&
|
|
1267
|
+
(!hasUsageMeta ||
|
|
1268
|
+
state.usageMtimeMs !== usageStat.mtimeMs ||
|
|
1269
|
+
state.usageSize !== usageStat.size);
|
|
1270
|
+
if (hasUsageData && (sessionsEmpty || usageOutOfSync)) {
|
|
1271
|
+
const records = readUsageRecords(usagePath);
|
|
1272
|
+
if (records.length > 0) {
|
|
1273
|
+
sessions = buildUsageSessionsFromRecords(records);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
state.sessions = sessions;
|
|
1036
1277
|
const codexFiles = collectSessionFiles(getCodexSessionsPath(config));
|
|
1037
1278
|
const claudeFiles = collectSessionFiles(getClaudeSessionsPath(config));
|
|
1038
1279
|
const processFile = (filePath, type) => {
|
|
@@ -1179,6 +1420,7 @@ function syncUsageFromSessions(config, configPath, usagePath) {
|
|
|
1179
1420
|
processFile(filePath, "claude");
|
|
1180
1421
|
state.files = files;
|
|
1181
1422
|
state.sessions = sessions;
|
|
1423
|
+
updateUsageStateMetadata(state, usagePath);
|
|
1182
1424
|
writeUsageState(statePath, state);
|
|
1183
1425
|
}
|
|
1184
1426
|
finally {
|
package/package.json
CHANGED
package/src/cli/args.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ParsedArgs,
|
|
6
6
|
InitArgs,
|
|
7
7
|
AddArgs,
|
|
8
|
+
UsageResetArgs,
|
|
8
9
|
ProfileType,
|
|
9
10
|
StatuslineArgs,
|
|
10
11
|
} from "../types";
|
|
@@ -156,6 +157,19 @@ export function parseAddArgs(args: string[]): AddArgs {
|
|
|
156
157
|
return result;
|
|
157
158
|
}
|
|
158
159
|
|
|
160
|
+
export function parseUsageResetArgs(args: string[]): UsageResetArgs {
|
|
161
|
+
const result: UsageResetArgs = { yes: false };
|
|
162
|
+
for (let i = 0; i < args.length; i++) {
|
|
163
|
+
const arg = args[i];
|
|
164
|
+
if (arg === "-y" || arg === "--yes") {
|
|
165
|
+
result.yes = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`Unknown usage-reset argument: ${arg}`);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
function parseNumberFlag(value: string | null | undefined, flag: string): number {
|
|
160
174
|
if (value === null || value === undefined || value === "") {
|
|
161
175
|
throw new Error(`Missing value for ${flag}.`);
|
package/src/cli/help.ts
CHANGED
|
@@ -27,6 +27,7 @@ Usage:
|
|
|
27
27
|
codenv launch <codex|claude> [--] [args...]
|
|
28
28
|
codenv init
|
|
29
29
|
codenv statusline [options]
|
|
30
|
+
codenv usage-reset [--yes]
|
|
30
31
|
|
|
31
32
|
Options:
|
|
32
33
|
-c, --config <path> Path to config JSON
|
|
@@ -57,6 +58,9 @@ Statusline options:
|
|
|
57
58
|
--usage-output <n> Set output token usage
|
|
58
59
|
--sync-usage Sync usage from sessions before reading
|
|
59
60
|
|
|
61
|
+
Usage reset options:
|
|
62
|
+
-y, --yes Skip confirmation prompt
|
|
63
|
+
|
|
60
64
|
Examples:
|
|
61
65
|
codenv init
|
|
62
66
|
codenv use codex primary
|
|
@@ -67,6 +71,7 @@ Examples:
|
|
|
67
71
|
codenv remove --all
|
|
68
72
|
codenv launch codex -- --help
|
|
69
73
|
codenv statusline --format json
|
|
74
|
+
codenv usage-reset --yes
|
|
70
75
|
CODE_ENV_CONFIG=~/.config/code-env/config.json codenv use claude default
|
|
71
76
|
codenv add --type codex primary OPENAI_BASE_URL=https://api.example.com/v1 OPENAI_API_KEY=YOUR_API_KEY
|
|
72
77
|
codenv add
|
package/src/cli/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI module exports
|
|
3
3
|
*/
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
parseArgs,
|
|
6
|
+
parseInitArgs,
|
|
7
|
+
parseAddArgs,
|
|
8
|
+
parseUsageResetArgs,
|
|
9
|
+
parseStatuslineArgs,
|
|
10
|
+
} from "./args";
|
|
5
11
|
export { printHelp } from "./help";
|
package/src/commands/index.ts
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage history reset command
|
|
3
|
+
*/
|
|
4
|
+
import type { Config, UsageResetArgs } from "../types";
|
|
5
|
+
import { clearUsageHistory } from "../usage";
|
|
6
|
+
import { askConfirm, createReadline } from "../ui";
|
|
7
|
+
|
|
8
|
+
export async function runUsageReset(
|
|
9
|
+
config: Config,
|
|
10
|
+
configPath: string | null,
|
|
11
|
+
args: UsageResetArgs
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (!args.yes) {
|
|
14
|
+
const rl = createReadline();
|
|
15
|
+
try {
|
|
16
|
+
const confirmed = await askConfirm(
|
|
17
|
+
rl,
|
|
18
|
+
"Clear all usage history files? This cannot be undone. (y/N): "
|
|
19
|
+
);
|
|
20
|
+
if (!confirmed) return;
|
|
21
|
+
} finally {
|
|
22
|
+
rl.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = clearUsageHistory(config, configPath);
|
|
27
|
+
const removed = result.removed.sort();
|
|
28
|
+
const missing = result.missing.sort();
|
|
29
|
+
const failed = result.failed.sort((a, b) => a.path.localeCompare(b.path));
|
|
30
|
+
|
|
31
|
+
if (removed.length === 0 && failed.length === 0) {
|
|
32
|
+
console.log("No usage files found.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (removed.length > 0) {
|
|
37
|
+
console.log(`Removed ${removed.length} file(s):`);
|
|
38
|
+
for (const filePath of removed) {
|
|
39
|
+
console.log(`- ${filePath}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (missing.length > 0) {
|
|
44
|
+
console.log(`Skipped ${missing.length} missing file(s).`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (failed.length > 0) {
|
|
48
|
+
for (const failure of failed) {
|
|
49
|
+
console.error(`Failed to remove ${failure.path}: ${failure.error}`);
|
|
50
|
+
}
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
parseArgs,
|
|
11
11
|
parseInitArgs,
|
|
12
12
|
parseAddArgs,
|
|
13
|
+
parseUsageResetArgs,
|
|
13
14
|
parseStatuslineArgs,
|
|
14
15
|
printHelp,
|
|
15
16
|
} from "./cli";
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
printUnset,
|
|
34
35
|
runLaunch,
|
|
35
36
|
printStatusline,
|
|
37
|
+
runUsageReset,
|
|
36
38
|
} from "./commands";
|
|
37
39
|
import { logProfileUse } from "./usage";
|
|
38
40
|
import { createReadline, askConfirm, runInteractiveAdd, runInteractiveUse } from "./ui";
|
|
@@ -152,6 +154,15 @@ async function main() {
|
|
|
152
154
|
return;
|
|
153
155
|
}
|
|
154
156
|
|
|
157
|
+
if (cmd === "usage-reset" || cmd === "reset-usage") {
|
|
158
|
+
const resetArgs = parseUsageResetArgs(args.slice(1));
|
|
159
|
+
const configPath =
|
|
160
|
+
process.env.CODE_ENV_CONFIG_PATH || findConfigPath(parsed.configPath);
|
|
161
|
+
const config = readConfigIfExists(configPath);
|
|
162
|
+
await runUsageReset(config, configPath, resetArgs);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
155
166
|
const configPath = findConfigPath(parsed.configPath);
|
|
156
167
|
const config = readConfig(configPath!);
|
|
157
168
|
|
package/src/statusline/debug.ts
CHANGED
|
@@ -16,7 +16,7 @@ function resolveDefaultConfigDir(configPath: string | null): string {
|
|
|
16
16
|
return path.join(os.homedir(), ".config", "code-env");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function getStatuslineDebugPath(configPath: string | null): string {
|
|
19
|
+
export function getStatuslineDebugPath(configPath: string | null): string {
|
|
20
20
|
const envPath = resolvePath(process.env.CODE_ENV_STATUSLINE_DEBUG_PATH);
|
|
21
21
|
if (envPath) return envPath;
|
|
22
22
|
return path.join(resolveDefaultConfigDir(configPath), "statusline-debug.jsonl");
|