codex-team 0.0.1 → 0.0.3
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 +4 -4
- package/dist/cli.cjs +205 -69
- package/dist/main.cjs +205 -69
- package/dist/main.js +205 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ After install, use the `codexm` command.
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
codexm current
|
|
19
|
-
codexm list
|
|
19
|
+
codexm list [name]
|
|
20
20
|
codexm save <name>
|
|
21
21
|
codexm update
|
|
22
22
|
codexm switch <name>
|
|
23
|
+
codexm switch --auto --dry-run
|
|
23
24
|
codexm remove <name> --yes
|
|
24
25
|
codexm rename <old> <new>
|
|
25
26
|
codexm quota refresh [name]
|
|
26
|
-
codexm quota list
|
|
27
27
|
codexm doctor
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -34,8 +34,8 @@ Use `--json` on query and mutation commands when you need machine-readable outpu
|
|
|
34
34
|
1. Log into a target account with the native Codex CLI.
|
|
35
35
|
2. Save the current auth snapshot with `codexm save <name>`.
|
|
36
36
|
3. Repeat for other accounts.
|
|
37
|
-
4. Switch between saved accounts with `codexm switch <name
|
|
38
|
-
5. Refresh and inspect quota usage with `codexm
|
|
37
|
+
4. Switch between saved accounts with `codexm switch <name>` or let the tool choose with `codexm switch --auto`.
|
|
38
|
+
5. Refresh and inspect quota usage with `codexm list` or `codexm quota refresh`.
|
|
39
39
|
|
|
40
40
|
## Development
|
|
41
41
|
|
package/dist/cli.cjs
CHANGED
|
@@ -409,6 +409,7 @@ var __webpack_modules__ = {
|
|
|
409
409
|
const FILE_MODE = 384;
|
|
410
410
|
const SCHEMA_VERSION = 1;
|
|
411
411
|
const ACCOUNT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
412
|
+
const QUOTA_REFRESH_CONCURRENCY = 3;
|
|
412
413
|
function defaultPaths(homeDir = (0, external_node_os_namespaceObject.homedir)()) {
|
|
413
414
|
const codexDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex");
|
|
414
415
|
const codexTeamDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex-team");
|
|
@@ -759,17 +760,36 @@ var __webpack_modules__ = {
|
|
|
759
760
|
const { accounts } = await this.listAccounts();
|
|
760
761
|
const targets = targetName ? accounts.filter((account)=>account.name === targetName) : accounts;
|
|
761
762
|
if (targetName && 0 === targets.length) throw new Error(`Account "${targetName}" does not exist.`);
|
|
763
|
+
const results = new Array(targets.length);
|
|
764
|
+
let nextIndex = 0;
|
|
765
|
+
const workerCount = Math.min(QUOTA_REFRESH_CONCURRENCY, targets.length);
|
|
766
|
+
await Promise.all(Array.from({
|
|
767
|
+
length: workerCount
|
|
768
|
+
}, async ()=>{
|
|
769
|
+
while(true){
|
|
770
|
+
const index = nextIndex;
|
|
771
|
+
nextIndex += 1;
|
|
772
|
+
if (index >= targets.length) return;
|
|
773
|
+
const account = targets[index];
|
|
774
|
+
try {
|
|
775
|
+
const refreshed = await this.refreshQuotaForAccount(account.name);
|
|
776
|
+
results[index] = {
|
|
777
|
+
success: await this.quotaSummaryForAccount(refreshed.account)
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
results[index] = {
|
|
781
|
+
failure: {
|
|
782
|
+
name: account.name,
|
|
783
|
+
error: error.message
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}));
|
|
762
789
|
const successes = [];
|
|
763
790
|
const failures = [];
|
|
764
|
-
for (const
|
|
765
|
-
|
|
766
|
-
successes.push(await this.quotaSummaryForAccount(refreshed.account));
|
|
767
|
-
} catch (error) {
|
|
768
|
-
failures.push({
|
|
769
|
-
name: account.name,
|
|
770
|
-
error: error.message
|
|
771
|
-
});
|
|
772
|
-
}
|
|
791
|
+
for (const result of results)if (result) if ("success" in result) successes.push(result.success);
|
|
792
|
+
else failures.push(result.failure);
|
|
773
793
|
return {
|
|
774
794
|
successes,
|
|
775
795
|
failures
|
|
@@ -885,12 +905,12 @@ var __webpack_modules__ = {
|
|
|
885
905
|
|
|
886
906
|
Usage:
|
|
887
907
|
codexm current [--json]
|
|
888
|
-
codexm list [--json]
|
|
908
|
+
codexm list [name] [--json]
|
|
889
909
|
codexm save <name> [--force] [--json]
|
|
890
910
|
codexm update [--json]
|
|
891
911
|
codexm quota refresh [name] [--json]
|
|
892
|
-
codexm quota list [--json]
|
|
893
912
|
codexm switch <name> [--json]
|
|
913
|
+
codexm switch --auto [--dry-run] [--json]
|
|
894
914
|
codexm remove <name> [--yes] [--json]
|
|
895
915
|
codexm rename <old> <new> [--json]
|
|
896
916
|
codexm doctor [--json]
|
|
@@ -911,47 +931,6 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
911
931
|
for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
|
|
912
932
|
return lines.join("\n");
|
|
913
933
|
}
|
|
914
|
-
function describeAccounts(accounts, warnings) {
|
|
915
|
-
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
916
|
-
const table = formatTable(accounts.map((account)=>({
|
|
917
|
-
name: account.name,
|
|
918
|
-
account_id: maskAccountId(account.account_id),
|
|
919
|
-
auth_mode: account.auth_mode,
|
|
920
|
-
saved: account.created_at,
|
|
921
|
-
switched: account.last_switched_at ?? "-",
|
|
922
|
-
flags: account.duplicateAccountId ? "duplicate-account-id" : "-"
|
|
923
|
-
})), [
|
|
924
|
-
{
|
|
925
|
-
key: "name",
|
|
926
|
-
label: "NAME"
|
|
927
|
-
},
|
|
928
|
-
{
|
|
929
|
-
key: "account_id",
|
|
930
|
-
label: "ACCOUNT ID"
|
|
931
|
-
},
|
|
932
|
-
{
|
|
933
|
-
key: "auth_mode",
|
|
934
|
-
label: "AUTH MODE"
|
|
935
|
-
},
|
|
936
|
-
{
|
|
937
|
-
key: "saved",
|
|
938
|
-
label: "SAVED AT"
|
|
939
|
-
},
|
|
940
|
-
{
|
|
941
|
-
key: "switched",
|
|
942
|
-
label: "LAST SWITCHED"
|
|
943
|
-
},
|
|
944
|
-
{
|
|
945
|
-
key: "flags",
|
|
946
|
-
label: "FLAGS"
|
|
947
|
-
}
|
|
948
|
-
]);
|
|
949
|
-
const lines = [
|
|
950
|
-
table
|
|
951
|
-
];
|
|
952
|
-
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
953
|
-
return lines.join("\n");
|
|
954
|
-
}
|
|
955
934
|
function describeDoctor(report) {
|
|
956
935
|
const lines = [
|
|
957
936
|
report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
|
|
@@ -970,17 +949,106 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
970
949
|
if (!window?.reset_at) return "-";
|
|
971
950
|
return external_dayjs_default().utc(window.reset_at).tz(external_dayjs_default().tz.guess()).format("MM-DD HH:mm");
|
|
972
951
|
}
|
|
952
|
+
function computeAvailability(account) {
|
|
953
|
+
if ("ok" !== account.status) return null;
|
|
954
|
+
const usedPercents = [
|
|
955
|
+
account.five_hour?.used_percent,
|
|
956
|
+
account.one_week?.used_percent
|
|
957
|
+
].filter((value)=>"number" == typeof value);
|
|
958
|
+
if (0 === usedPercents.length) return null;
|
|
959
|
+
if (usedPercents.some((value)=>value >= 100)) return "unavailable";
|
|
960
|
+
if (usedPercents.some((value)=>100 - value < 10)) return "almost unavailable";
|
|
961
|
+
return "available";
|
|
962
|
+
}
|
|
963
|
+
function toCliQuotaSummary(account) {
|
|
964
|
+
const { status, ...rest } = account;
|
|
965
|
+
return {
|
|
966
|
+
...rest,
|
|
967
|
+
available: computeAvailability(account),
|
|
968
|
+
refresh_status: status
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function toCliQuotaRefreshResult(result) {
|
|
972
|
+
return {
|
|
973
|
+
successes: result.successes.map(toCliQuotaSummary),
|
|
974
|
+
failures: result.failures
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function computeRemainingPercent(usedPercent) {
|
|
978
|
+
if ("number" != typeof usedPercent) return null;
|
|
979
|
+
return Math.max(0, 100 - usedPercent);
|
|
980
|
+
}
|
|
981
|
+
function toAutoSwitchCandidate(account) {
|
|
982
|
+
if ("ok" !== account.status) return null;
|
|
983
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
984
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
985
|
+
if (null === remain5h || null === remain1w) return null;
|
|
986
|
+
return {
|
|
987
|
+
name: account.name,
|
|
988
|
+
account_id: account.account_id,
|
|
989
|
+
plan_type: account.plan_type,
|
|
990
|
+
available: computeAvailability(account),
|
|
991
|
+
refresh_status: "ok",
|
|
992
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
993
|
+
remain_5h: remain5h,
|
|
994
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
995
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
996
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
997
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
998
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function compareNullableDateAscending(left, right) {
|
|
1002
|
+
if (left === right) return 0;
|
|
1003
|
+
if (null === left) return 1;
|
|
1004
|
+
if (null === right) return -1;
|
|
1005
|
+
return left.localeCompare(right);
|
|
1006
|
+
}
|
|
1007
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
1008
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
1009
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1010
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1011
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1012
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1013
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1014
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1015
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1016
|
+
return left.name.localeCompare(right.name);
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1020
|
+
const lines = [
|
|
1021
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1022
|
+
`Score: ${candidate.effective_score}`,
|
|
1023
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1024
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1025
|
+
];
|
|
1026
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1027
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
1030
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1031
|
+
const lines = [
|
|
1032
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1033
|
+
`Score: ${candidate.effective_score}`,
|
|
1034
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1035
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1036
|
+
];
|
|
1037
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1038
|
+
return lines.join("\n");
|
|
1039
|
+
}
|
|
973
1040
|
function describeQuotaAccounts(accounts, warnings) {
|
|
974
1041
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
975
1042
|
const table = formatTable(accounts.map((account)=>({
|
|
976
1043
|
name: account.name,
|
|
977
1044
|
account_id: maskAccountId(account.account_id),
|
|
978
1045
|
plan_type: account.plan_type ?? "-",
|
|
1046
|
+
available: computeAvailability(account) ?? "-",
|
|
979
1047
|
five_hour: formatUsagePercent(account.five_hour),
|
|
980
1048
|
five_hour_reset: formatResetAt(account.five_hour),
|
|
981
1049
|
one_week: formatUsagePercent(account.one_week),
|
|
982
1050
|
one_week_reset: formatResetAt(account.one_week),
|
|
983
|
-
|
|
1051
|
+
refresh_status: account.status
|
|
984
1052
|
})), [
|
|
985
1053
|
{
|
|
986
1054
|
key: "name",
|
|
@@ -994,6 +1062,10 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
994
1062
|
key: "plan_type",
|
|
995
1063
|
label: "PLAN TYPE"
|
|
996
1064
|
},
|
|
1065
|
+
{
|
|
1066
|
+
key: "available",
|
|
1067
|
+
label: "AVAILABLE"
|
|
1068
|
+
},
|
|
997
1069
|
{
|
|
998
1070
|
key: "five_hour",
|
|
999
1071
|
label: "5H USED"
|
|
@@ -1011,8 +1083,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1011
1083
|
label: "1W RESET AT"
|
|
1012
1084
|
},
|
|
1013
1085
|
{
|
|
1014
|
-
key: "
|
|
1015
|
-
label: "STATUS"
|
|
1086
|
+
key: "refresh_status",
|
|
1087
|
+
label: "REFRESH STATUS"
|
|
1016
1088
|
}
|
|
1017
1089
|
]);
|
|
1018
1090
|
const lines = [
|
|
@@ -1068,10 +1140,11 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1068
1140
|
}
|
|
1069
1141
|
case "list":
|
|
1070
1142
|
{
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1143
|
+
const targetName = parsed.positionals[0];
|
|
1144
|
+
const result = await store.refreshAllQuotas(targetName);
|
|
1145
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1146
|
+
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1147
|
+
return 0 === result.failures.length ? 0 : 1;
|
|
1075
1148
|
}
|
|
1076
1149
|
case "save":
|
|
1077
1150
|
{
|
|
@@ -1099,7 +1172,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1099
1172
|
try {
|
|
1100
1173
|
const quotaResult = await store.refreshQuotaForAccount(result.account.name);
|
|
1101
1174
|
const quotaList = await store.listQuotaSummaries();
|
|
1102
|
-
|
|
1175
|
+
const matched = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
|
|
1176
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1103
1177
|
} catch (error) {
|
|
1104
1178
|
warnings.push(error.message);
|
|
1105
1179
|
}
|
|
@@ -1124,31 +1198,93 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
|
|
|
1124
1198
|
case "quota":
|
|
1125
1199
|
{
|
|
1126
1200
|
const quotaCommand = parsed.positionals[0];
|
|
1127
|
-
if ("list" === quotaCommand) {
|
|
1128
|
-
const result = await store.listQuotaSummaries();
|
|
1129
|
-
if (json) writeJson(streams.stdout, result);
|
|
1130
|
-
else streams.stdout.write(`${describeQuotaAccounts(result.accounts, result.warnings)}\n`);
|
|
1131
|
-
return 0;
|
|
1132
|
-
}
|
|
1133
1201
|
if ("refresh" === quotaCommand) {
|
|
1134
1202
|
const targetName = parsed.positionals[1];
|
|
1135
1203
|
const result = await store.refreshAllQuotas(targetName);
|
|
1136
|
-
if (json) writeJson(streams.stdout, result);
|
|
1204
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1137
1205
|
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1138
1206
|
return 0 === result.failures.length ? 0 : 1;
|
|
1139
1207
|
}
|
|
1140
|
-
throw new Error("Usage: codexm quota
|
|
1208
|
+
throw new Error("Usage: codexm quota refresh [name] [--json]");
|
|
1141
1209
|
}
|
|
1142
1210
|
case "switch":
|
|
1143
1211
|
{
|
|
1212
|
+
const auto = parsed.flags.has("--auto");
|
|
1213
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1144
1214
|
const name = parsed.positionals[0];
|
|
1215
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1216
|
+
if (auto) {
|
|
1217
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1218
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1219
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1220
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1221
|
+
const selected = candidates[0];
|
|
1222
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1223
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1224
|
+
if (dryRun) {
|
|
1225
|
+
const payload = {
|
|
1226
|
+
ok: true,
|
|
1227
|
+
action: "switch",
|
|
1228
|
+
mode: "auto",
|
|
1229
|
+
dry_run: true,
|
|
1230
|
+
selected,
|
|
1231
|
+
candidates,
|
|
1232
|
+
warnings
|
|
1233
|
+
};
|
|
1234
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1235
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1236
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1237
|
+
}
|
|
1238
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1239
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1240
|
+
const payload = {
|
|
1241
|
+
ok: true,
|
|
1242
|
+
action: "switch",
|
|
1243
|
+
mode: "auto",
|
|
1244
|
+
skipped: true,
|
|
1245
|
+
reason: "already_current_best",
|
|
1246
|
+
account: {
|
|
1247
|
+
name: selected.name,
|
|
1248
|
+
account_id: selected.account_id
|
|
1249
|
+
},
|
|
1250
|
+
selected,
|
|
1251
|
+
candidates,
|
|
1252
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1253
|
+
warnings
|
|
1254
|
+
};
|
|
1255
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1256
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1257
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1258
|
+
}
|
|
1259
|
+
const result = await store.switchAccount(selected.name);
|
|
1260
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1261
|
+
const payload = {
|
|
1262
|
+
ok: true,
|
|
1263
|
+
action: "switch",
|
|
1264
|
+
mode: "auto",
|
|
1265
|
+
account: {
|
|
1266
|
+
name: result.account.name,
|
|
1267
|
+
account_id: result.account.account_id,
|
|
1268
|
+
auth_mode: result.account.auth_mode
|
|
1269
|
+
},
|
|
1270
|
+
selected,
|
|
1271
|
+
candidates,
|
|
1272
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1273
|
+
backup_path: result.backup_path,
|
|
1274
|
+
warnings: result.warnings
|
|
1275
|
+
};
|
|
1276
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1277
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1278
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1279
|
+
}
|
|
1145
1280
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1146
1281
|
const result = await store.switchAccount(name);
|
|
1147
1282
|
let quota = null;
|
|
1148
1283
|
try {
|
|
1149
1284
|
await store.refreshQuotaForAccount(result.account.name);
|
|
1150
1285
|
const quotaList = await store.listQuotaSummaries();
|
|
1151
|
-
|
|
1286
|
+
const matched = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
|
|
1287
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1152
1288
|
} catch (error) {
|
|
1153
1289
|
result.warnings.push(error.message);
|
|
1154
1290
|
}
|
package/dist/main.cjs
CHANGED
|
@@ -439,6 +439,7 @@ const DIRECTORY_MODE = 448;
|
|
|
439
439
|
const FILE_MODE = 384;
|
|
440
440
|
const SCHEMA_VERSION = 1;
|
|
441
441
|
const ACCOUNT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
442
|
+
const QUOTA_REFRESH_CONCURRENCY = 3;
|
|
442
443
|
function defaultPaths(homeDir = (0, external_node_os_namespaceObject.homedir)()) {
|
|
443
444
|
const codexDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex");
|
|
444
445
|
const codexTeamDir = (0, external_node_path_namespaceObject.join)(homeDir, ".codex-team");
|
|
@@ -789,17 +790,36 @@ class AccountStore {
|
|
|
789
790
|
const { accounts } = await this.listAccounts();
|
|
790
791
|
const targets = targetName ? accounts.filter((account)=>account.name === targetName) : accounts;
|
|
791
792
|
if (targetName && 0 === targets.length) throw new Error(`Account "${targetName}" does not exist.`);
|
|
793
|
+
const results = new Array(targets.length);
|
|
794
|
+
let nextIndex = 0;
|
|
795
|
+
const workerCount = Math.min(QUOTA_REFRESH_CONCURRENCY, targets.length);
|
|
796
|
+
await Promise.all(Array.from({
|
|
797
|
+
length: workerCount
|
|
798
|
+
}, async ()=>{
|
|
799
|
+
while(true){
|
|
800
|
+
const index = nextIndex;
|
|
801
|
+
nextIndex += 1;
|
|
802
|
+
if (index >= targets.length) return;
|
|
803
|
+
const account = targets[index];
|
|
804
|
+
try {
|
|
805
|
+
const refreshed = await this.refreshQuotaForAccount(account.name);
|
|
806
|
+
results[index] = {
|
|
807
|
+
success: await this.quotaSummaryForAccount(refreshed.account)
|
|
808
|
+
};
|
|
809
|
+
} catch (error) {
|
|
810
|
+
results[index] = {
|
|
811
|
+
failure: {
|
|
812
|
+
name: account.name,
|
|
813
|
+
error: error.message
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}));
|
|
792
819
|
const successes = [];
|
|
793
820
|
const failures = [];
|
|
794
|
-
for (const
|
|
795
|
-
|
|
796
|
-
successes.push(await this.quotaSummaryForAccount(refreshed.account));
|
|
797
|
-
} catch (error) {
|
|
798
|
-
failures.push({
|
|
799
|
-
name: account.name,
|
|
800
|
-
error: error.message
|
|
801
|
-
});
|
|
802
|
-
}
|
|
821
|
+
for (const result of results)if (result) if ("success" in result) successes.push(result.success);
|
|
822
|
+
else failures.push(result.failure);
|
|
803
823
|
return {
|
|
804
824
|
successes,
|
|
805
825
|
failures
|
|
@@ -915,12 +935,12 @@ function printHelp(stream) {
|
|
|
915
935
|
|
|
916
936
|
Usage:
|
|
917
937
|
codexm current [--json]
|
|
918
|
-
codexm list [--json]
|
|
938
|
+
codexm list [name] [--json]
|
|
919
939
|
codexm save <name> [--force] [--json]
|
|
920
940
|
codexm update [--json]
|
|
921
941
|
codexm quota refresh [name] [--json]
|
|
922
|
-
codexm quota list [--json]
|
|
923
942
|
codexm switch <name> [--json]
|
|
943
|
+
codexm switch --auto [--dry-run] [--json]
|
|
924
944
|
codexm remove <name> [--yes] [--json]
|
|
925
945
|
codexm rename <old> <new> [--json]
|
|
926
946
|
codexm doctor [--json]
|
|
@@ -941,47 +961,6 @@ function describeCurrentStatus(status) {
|
|
|
941
961
|
for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
|
|
942
962
|
return lines.join("\n");
|
|
943
963
|
}
|
|
944
|
-
function describeAccounts(accounts, warnings) {
|
|
945
|
-
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
946
|
-
const table = formatTable(accounts.map((account)=>({
|
|
947
|
-
name: account.name,
|
|
948
|
-
account_id: maskAccountId(account.account_id),
|
|
949
|
-
auth_mode: account.auth_mode,
|
|
950
|
-
saved: account.created_at,
|
|
951
|
-
switched: account.last_switched_at ?? "-",
|
|
952
|
-
flags: account.duplicateAccountId ? "duplicate-account-id" : "-"
|
|
953
|
-
})), [
|
|
954
|
-
{
|
|
955
|
-
key: "name",
|
|
956
|
-
label: "NAME"
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
key: "account_id",
|
|
960
|
-
label: "ACCOUNT ID"
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
key: "auth_mode",
|
|
964
|
-
label: "AUTH MODE"
|
|
965
|
-
},
|
|
966
|
-
{
|
|
967
|
-
key: "saved",
|
|
968
|
-
label: "SAVED AT"
|
|
969
|
-
},
|
|
970
|
-
{
|
|
971
|
-
key: "switched",
|
|
972
|
-
label: "LAST SWITCHED"
|
|
973
|
-
},
|
|
974
|
-
{
|
|
975
|
-
key: "flags",
|
|
976
|
-
label: "FLAGS"
|
|
977
|
-
}
|
|
978
|
-
]);
|
|
979
|
-
const lines = [
|
|
980
|
-
table
|
|
981
|
-
];
|
|
982
|
-
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
983
|
-
return lines.join("\n");
|
|
984
|
-
}
|
|
985
964
|
function describeDoctor(report) {
|
|
986
965
|
const lines = [
|
|
987
966
|
report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
|
|
@@ -1000,17 +979,106 @@ function formatResetAt(window) {
|
|
|
1000
979
|
if (!window?.reset_at) return "-";
|
|
1001
980
|
return external_dayjs_default().utc(window.reset_at).tz(external_dayjs_default().tz.guess()).format("MM-DD HH:mm");
|
|
1002
981
|
}
|
|
982
|
+
function computeAvailability(account) {
|
|
983
|
+
if ("ok" !== account.status) return null;
|
|
984
|
+
const usedPercents = [
|
|
985
|
+
account.five_hour?.used_percent,
|
|
986
|
+
account.one_week?.used_percent
|
|
987
|
+
].filter((value)=>"number" == typeof value);
|
|
988
|
+
if (0 === usedPercents.length) return null;
|
|
989
|
+
if (usedPercents.some((value)=>value >= 100)) return "unavailable";
|
|
990
|
+
if (usedPercents.some((value)=>100 - value < 10)) return "almost unavailable";
|
|
991
|
+
return "available";
|
|
992
|
+
}
|
|
993
|
+
function toCliQuotaSummary(account) {
|
|
994
|
+
const { status, ...rest } = account;
|
|
995
|
+
return {
|
|
996
|
+
...rest,
|
|
997
|
+
available: computeAvailability(account),
|
|
998
|
+
refresh_status: status
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function toCliQuotaRefreshResult(result) {
|
|
1002
|
+
return {
|
|
1003
|
+
successes: result.successes.map(toCliQuotaSummary),
|
|
1004
|
+
failures: result.failures
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
function computeRemainingPercent(usedPercent) {
|
|
1008
|
+
if ("number" != typeof usedPercent) return null;
|
|
1009
|
+
return Math.max(0, 100 - usedPercent);
|
|
1010
|
+
}
|
|
1011
|
+
function toAutoSwitchCandidate(account) {
|
|
1012
|
+
if ("ok" !== account.status) return null;
|
|
1013
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
1014
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
1015
|
+
if (null === remain5h || null === remain1w) return null;
|
|
1016
|
+
return {
|
|
1017
|
+
name: account.name,
|
|
1018
|
+
account_id: account.account_id,
|
|
1019
|
+
plan_type: account.plan_type,
|
|
1020
|
+
available: computeAvailability(account),
|
|
1021
|
+
refresh_status: "ok",
|
|
1022
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
1023
|
+
remain_5h: remain5h,
|
|
1024
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
1025
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
1026
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
1027
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
1028
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
function compareNullableDateAscending(left, right) {
|
|
1032
|
+
if (left === right) return 0;
|
|
1033
|
+
if (null === left) return 1;
|
|
1034
|
+
if (null === right) return -1;
|
|
1035
|
+
return left.localeCompare(right);
|
|
1036
|
+
}
|
|
1037
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
1038
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
1039
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1040
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1041
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1042
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1043
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1044
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1045
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1046
|
+
return left.name.localeCompare(right.name);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1050
|
+
const lines = [
|
|
1051
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1052
|
+
`Score: ${candidate.effective_score}`,
|
|
1053
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1054
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1055
|
+
];
|
|
1056
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1057
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1058
|
+
return lines.join("\n");
|
|
1059
|
+
}
|
|
1060
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1061
|
+
const lines = [
|
|
1062
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1063
|
+
`Score: ${candidate.effective_score}`,
|
|
1064
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1065
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1066
|
+
];
|
|
1067
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1068
|
+
return lines.join("\n");
|
|
1069
|
+
}
|
|
1003
1070
|
function describeQuotaAccounts(accounts, warnings) {
|
|
1004
1071
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
1005
1072
|
const table = formatTable(accounts.map((account)=>({
|
|
1006
1073
|
name: account.name,
|
|
1007
1074
|
account_id: maskAccountId(account.account_id),
|
|
1008
1075
|
plan_type: account.plan_type ?? "-",
|
|
1076
|
+
available: computeAvailability(account) ?? "-",
|
|
1009
1077
|
five_hour: formatUsagePercent(account.five_hour),
|
|
1010
1078
|
five_hour_reset: formatResetAt(account.five_hour),
|
|
1011
1079
|
one_week: formatUsagePercent(account.one_week),
|
|
1012
1080
|
one_week_reset: formatResetAt(account.one_week),
|
|
1013
|
-
|
|
1081
|
+
refresh_status: account.status
|
|
1014
1082
|
})), [
|
|
1015
1083
|
{
|
|
1016
1084
|
key: "name",
|
|
@@ -1024,6 +1092,10 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
1024
1092
|
key: "plan_type",
|
|
1025
1093
|
label: "PLAN TYPE"
|
|
1026
1094
|
},
|
|
1095
|
+
{
|
|
1096
|
+
key: "available",
|
|
1097
|
+
label: "AVAILABLE"
|
|
1098
|
+
},
|
|
1027
1099
|
{
|
|
1028
1100
|
key: "five_hour",
|
|
1029
1101
|
label: "5H USED"
|
|
@@ -1041,8 +1113,8 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
1041
1113
|
label: "1W RESET AT"
|
|
1042
1114
|
},
|
|
1043
1115
|
{
|
|
1044
|
-
key: "
|
|
1045
|
-
label: "STATUS"
|
|
1116
|
+
key: "refresh_status",
|
|
1117
|
+
label: "REFRESH STATUS"
|
|
1046
1118
|
}
|
|
1047
1119
|
]);
|
|
1048
1120
|
const lines = [
|
|
@@ -1098,10 +1170,11 @@ async function runCli(argv, options = {}) {
|
|
|
1098
1170
|
}
|
|
1099
1171
|
case "list":
|
|
1100
1172
|
{
|
|
1101
|
-
const
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1173
|
+
const targetName = parsed.positionals[0];
|
|
1174
|
+
const result = await store.refreshAllQuotas(targetName);
|
|
1175
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1176
|
+
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1177
|
+
return 0 === result.failures.length ? 0 : 1;
|
|
1105
1178
|
}
|
|
1106
1179
|
case "save":
|
|
1107
1180
|
{
|
|
@@ -1129,7 +1202,8 @@ async function runCli(argv, options = {}) {
|
|
|
1129
1202
|
try {
|
|
1130
1203
|
const quotaResult = await store.refreshQuotaForAccount(result.account.name);
|
|
1131
1204
|
const quotaList = await store.listQuotaSummaries();
|
|
1132
|
-
|
|
1205
|
+
const matched = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
|
|
1206
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1133
1207
|
} catch (error) {
|
|
1134
1208
|
warnings.push(error.message);
|
|
1135
1209
|
}
|
|
@@ -1154,31 +1228,93 @@ async function runCli(argv, options = {}) {
|
|
|
1154
1228
|
case "quota":
|
|
1155
1229
|
{
|
|
1156
1230
|
const quotaCommand = parsed.positionals[0];
|
|
1157
|
-
if ("list" === quotaCommand) {
|
|
1158
|
-
const result = await store.listQuotaSummaries();
|
|
1159
|
-
if (json) writeJson(streams.stdout, result);
|
|
1160
|
-
else streams.stdout.write(`${describeQuotaAccounts(result.accounts, result.warnings)}\n`);
|
|
1161
|
-
return 0;
|
|
1162
|
-
}
|
|
1163
1231
|
if ("refresh" === quotaCommand) {
|
|
1164
1232
|
const targetName = parsed.positionals[1];
|
|
1165
1233
|
const result = await store.refreshAllQuotas(targetName);
|
|
1166
|
-
if (json) writeJson(streams.stdout, result);
|
|
1234
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1167
1235
|
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1168
1236
|
return 0 === result.failures.length ? 0 : 1;
|
|
1169
1237
|
}
|
|
1170
|
-
throw new Error("Usage: codexm quota
|
|
1238
|
+
throw new Error("Usage: codexm quota refresh [name] [--json]");
|
|
1171
1239
|
}
|
|
1172
1240
|
case "switch":
|
|
1173
1241
|
{
|
|
1242
|
+
const auto = parsed.flags.has("--auto");
|
|
1243
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1174
1244
|
const name = parsed.positionals[0];
|
|
1245
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1246
|
+
if (auto) {
|
|
1247
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1248
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1249
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1250
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1251
|
+
const selected = candidates[0];
|
|
1252
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1253
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1254
|
+
if (dryRun) {
|
|
1255
|
+
const payload = {
|
|
1256
|
+
ok: true,
|
|
1257
|
+
action: "switch",
|
|
1258
|
+
mode: "auto",
|
|
1259
|
+
dry_run: true,
|
|
1260
|
+
selected,
|
|
1261
|
+
candidates,
|
|
1262
|
+
warnings
|
|
1263
|
+
};
|
|
1264
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1265
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1266
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1267
|
+
}
|
|
1268
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1269
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1270
|
+
const payload = {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
action: "switch",
|
|
1273
|
+
mode: "auto",
|
|
1274
|
+
skipped: true,
|
|
1275
|
+
reason: "already_current_best",
|
|
1276
|
+
account: {
|
|
1277
|
+
name: selected.name,
|
|
1278
|
+
account_id: selected.account_id
|
|
1279
|
+
},
|
|
1280
|
+
selected,
|
|
1281
|
+
candidates,
|
|
1282
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1283
|
+
warnings
|
|
1284
|
+
};
|
|
1285
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1286
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1287
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1288
|
+
}
|
|
1289
|
+
const result = await store.switchAccount(selected.name);
|
|
1290
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1291
|
+
const payload = {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
action: "switch",
|
|
1294
|
+
mode: "auto",
|
|
1295
|
+
account: {
|
|
1296
|
+
name: result.account.name,
|
|
1297
|
+
account_id: result.account.account_id,
|
|
1298
|
+
auth_mode: result.account.auth_mode
|
|
1299
|
+
},
|
|
1300
|
+
selected,
|
|
1301
|
+
candidates,
|
|
1302
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1303
|
+
backup_path: result.backup_path,
|
|
1304
|
+
warnings: result.warnings
|
|
1305
|
+
};
|
|
1306
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1307
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1308
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1309
|
+
}
|
|
1175
1310
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1176
1311
|
const result = await store.switchAccount(name);
|
|
1177
1312
|
let quota = null;
|
|
1178
1313
|
try {
|
|
1179
1314
|
await store.refreshQuotaForAccount(result.account.name);
|
|
1180
1315
|
const quotaList = await store.listQuotaSummaries();
|
|
1181
|
-
|
|
1316
|
+
const matched = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
|
|
1317
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1182
1318
|
} catch (error) {
|
|
1183
1319
|
result.warnings.push(error.message);
|
|
1184
1320
|
}
|
package/dist/main.js
CHANGED
|
@@ -399,6 +399,7 @@ const DIRECTORY_MODE = 448;
|
|
|
399
399
|
const FILE_MODE = 384;
|
|
400
400
|
const SCHEMA_VERSION = 1;
|
|
401
401
|
const ACCOUNT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
402
|
+
const QUOTA_REFRESH_CONCURRENCY = 3;
|
|
402
403
|
function defaultPaths(homeDir = homedir()) {
|
|
403
404
|
const codexDir = join(homeDir, ".codex");
|
|
404
405
|
const codexTeamDir = join(homeDir, ".codex-team");
|
|
@@ -749,17 +750,36 @@ class AccountStore {
|
|
|
749
750
|
const { accounts } = await this.listAccounts();
|
|
750
751
|
const targets = targetName ? accounts.filter((account)=>account.name === targetName) : accounts;
|
|
751
752
|
if (targetName && 0 === targets.length) throw new Error(`Account "${targetName}" does not exist.`);
|
|
753
|
+
const results = new Array(targets.length);
|
|
754
|
+
let nextIndex = 0;
|
|
755
|
+
const workerCount = Math.min(QUOTA_REFRESH_CONCURRENCY, targets.length);
|
|
756
|
+
await Promise.all(Array.from({
|
|
757
|
+
length: workerCount
|
|
758
|
+
}, async ()=>{
|
|
759
|
+
while(true){
|
|
760
|
+
const index = nextIndex;
|
|
761
|
+
nextIndex += 1;
|
|
762
|
+
if (index >= targets.length) return;
|
|
763
|
+
const account = targets[index];
|
|
764
|
+
try {
|
|
765
|
+
const refreshed = await this.refreshQuotaForAccount(account.name);
|
|
766
|
+
results[index] = {
|
|
767
|
+
success: await this.quotaSummaryForAccount(refreshed.account)
|
|
768
|
+
};
|
|
769
|
+
} catch (error) {
|
|
770
|
+
results[index] = {
|
|
771
|
+
failure: {
|
|
772
|
+
name: account.name,
|
|
773
|
+
error: error.message
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}));
|
|
752
779
|
const successes = [];
|
|
753
780
|
const failures = [];
|
|
754
|
-
for (const
|
|
755
|
-
|
|
756
|
-
successes.push(await this.quotaSummaryForAccount(refreshed.account));
|
|
757
|
-
} catch (error) {
|
|
758
|
-
failures.push({
|
|
759
|
-
name: account.name,
|
|
760
|
-
error: error.message
|
|
761
|
-
});
|
|
762
|
-
}
|
|
781
|
+
for (const result of results)if (result) if ("success" in result) successes.push(result.success);
|
|
782
|
+
else failures.push(result.failure);
|
|
763
783
|
return {
|
|
764
784
|
successes,
|
|
765
785
|
failures
|
|
@@ -875,12 +895,12 @@ function printHelp(stream) {
|
|
|
875
895
|
|
|
876
896
|
Usage:
|
|
877
897
|
codexm current [--json]
|
|
878
|
-
codexm list [--json]
|
|
898
|
+
codexm list [name] [--json]
|
|
879
899
|
codexm save <name> [--force] [--json]
|
|
880
900
|
codexm update [--json]
|
|
881
901
|
codexm quota refresh [name] [--json]
|
|
882
|
-
codexm quota list [--json]
|
|
883
902
|
codexm switch <name> [--json]
|
|
903
|
+
codexm switch --auto [--dry-run] [--json]
|
|
884
904
|
codexm remove <name> [--yes] [--json]
|
|
885
905
|
codexm rename <old> <new> [--json]
|
|
886
906
|
codexm doctor [--json]
|
|
@@ -901,47 +921,6 @@ function describeCurrentStatus(status) {
|
|
|
901
921
|
for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
|
|
902
922
|
return lines.join("\n");
|
|
903
923
|
}
|
|
904
|
-
function describeAccounts(accounts, warnings) {
|
|
905
|
-
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
906
|
-
const table = formatTable(accounts.map((account)=>({
|
|
907
|
-
name: account.name,
|
|
908
|
-
account_id: maskAccountId(account.account_id),
|
|
909
|
-
auth_mode: account.auth_mode,
|
|
910
|
-
saved: account.created_at,
|
|
911
|
-
switched: account.last_switched_at ?? "-",
|
|
912
|
-
flags: account.duplicateAccountId ? "duplicate-account-id" : "-"
|
|
913
|
-
})), [
|
|
914
|
-
{
|
|
915
|
-
key: "name",
|
|
916
|
-
label: "NAME"
|
|
917
|
-
},
|
|
918
|
-
{
|
|
919
|
-
key: "account_id",
|
|
920
|
-
label: "ACCOUNT ID"
|
|
921
|
-
},
|
|
922
|
-
{
|
|
923
|
-
key: "auth_mode",
|
|
924
|
-
label: "AUTH MODE"
|
|
925
|
-
},
|
|
926
|
-
{
|
|
927
|
-
key: "saved",
|
|
928
|
-
label: "SAVED AT"
|
|
929
|
-
},
|
|
930
|
-
{
|
|
931
|
-
key: "switched",
|
|
932
|
-
label: "LAST SWITCHED"
|
|
933
|
-
},
|
|
934
|
-
{
|
|
935
|
-
key: "flags",
|
|
936
|
-
label: "FLAGS"
|
|
937
|
-
}
|
|
938
|
-
]);
|
|
939
|
-
const lines = [
|
|
940
|
-
table
|
|
941
|
-
];
|
|
942
|
-
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
943
|
-
return lines.join("\n");
|
|
944
|
-
}
|
|
945
924
|
function describeDoctor(report) {
|
|
946
925
|
const lines = [
|
|
947
926
|
report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
|
|
@@ -960,17 +939,106 @@ function formatResetAt(window) {
|
|
|
960
939
|
if (!window?.reset_at) return "-";
|
|
961
940
|
return dayjs.utc(window.reset_at).tz(dayjs.tz.guess()).format("MM-DD HH:mm");
|
|
962
941
|
}
|
|
942
|
+
function computeAvailability(account) {
|
|
943
|
+
if ("ok" !== account.status) return null;
|
|
944
|
+
const usedPercents = [
|
|
945
|
+
account.five_hour?.used_percent,
|
|
946
|
+
account.one_week?.used_percent
|
|
947
|
+
].filter((value)=>"number" == typeof value);
|
|
948
|
+
if (0 === usedPercents.length) return null;
|
|
949
|
+
if (usedPercents.some((value)=>value >= 100)) return "unavailable";
|
|
950
|
+
if (usedPercents.some((value)=>100 - value < 10)) return "almost unavailable";
|
|
951
|
+
return "available";
|
|
952
|
+
}
|
|
953
|
+
function toCliQuotaSummary(account) {
|
|
954
|
+
const { status, ...rest } = account;
|
|
955
|
+
return {
|
|
956
|
+
...rest,
|
|
957
|
+
available: computeAvailability(account),
|
|
958
|
+
refresh_status: status
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function toCliQuotaRefreshResult(result) {
|
|
962
|
+
return {
|
|
963
|
+
successes: result.successes.map(toCliQuotaSummary),
|
|
964
|
+
failures: result.failures
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function computeRemainingPercent(usedPercent) {
|
|
968
|
+
if ("number" != typeof usedPercent) return null;
|
|
969
|
+
return Math.max(0, 100 - usedPercent);
|
|
970
|
+
}
|
|
971
|
+
function toAutoSwitchCandidate(account) {
|
|
972
|
+
if ("ok" !== account.status) return null;
|
|
973
|
+
const remain5h = computeRemainingPercent(account.five_hour?.used_percent);
|
|
974
|
+
const remain1w = computeRemainingPercent(account.one_week?.used_percent);
|
|
975
|
+
if (null === remain5h || null === remain1w) return null;
|
|
976
|
+
return {
|
|
977
|
+
name: account.name,
|
|
978
|
+
account_id: account.account_id,
|
|
979
|
+
plan_type: account.plan_type,
|
|
980
|
+
available: computeAvailability(account),
|
|
981
|
+
refresh_status: "ok",
|
|
982
|
+
effective_score: Math.min(remain5h, 3 * remain1w),
|
|
983
|
+
remain_5h: remain5h,
|
|
984
|
+
remain_1w_eq_5h: 3 * remain1w,
|
|
985
|
+
five_hour_used: account.five_hour?.used_percent ?? 0,
|
|
986
|
+
one_week_used: account.one_week?.used_percent ?? 0,
|
|
987
|
+
five_hour_reset_at: account.five_hour?.reset_at ?? null,
|
|
988
|
+
one_week_reset_at: account.one_week?.reset_at ?? null
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
function compareNullableDateAscending(left, right) {
|
|
992
|
+
if (left === right) return 0;
|
|
993
|
+
if (null === left) return 1;
|
|
994
|
+
if (null === right) return -1;
|
|
995
|
+
return left.localeCompare(right);
|
|
996
|
+
}
|
|
997
|
+
function rankAutoSwitchCandidates(accounts) {
|
|
998
|
+
return accounts.map(toAutoSwitchCandidate).filter((candidate)=>null !== candidate).sort((left, right)=>{
|
|
999
|
+
if (right.effective_score !== left.effective_score) return right.effective_score - left.effective_score;
|
|
1000
|
+
if (right.remain_5h !== left.remain_5h) return right.remain_5h - left.remain_5h;
|
|
1001
|
+
if (right.remain_1w_eq_5h !== left.remain_1w_eq_5h) return right.remain_1w_eq_5h - left.remain_1w_eq_5h;
|
|
1002
|
+
const fiveHourResetOrder = compareNullableDateAscending(left.five_hour_reset_at, right.five_hour_reset_at);
|
|
1003
|
+
if (0 !== fiveHourResetOrder) return fiveHourResetOrder;
|
|
1004
|
+
const oneWeekResetOrder = compareNullableDateAscending(left.one_week_reset_at, right.one_week_reset_at);
|
|
1005
|
+
if (0 !== oneWeekResetOrder) return oneWeekResetOrder;
|
|
1006
|
+
return left.name.localeCompare(right.name);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
|
|
1010
|
+
const lines = [
|
|
1011
|
+
dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
|
|
1012
|
+
`Score: ${candidate.effective_score}`,
|
|
1013
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1014
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1015
|
+
];
|
|
1016
|
+
if (backupPath) lines.push(`Backup: ${backupPath}`);
|
|
1017
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1018
|
+
return lines.join("\n");
|
|
1019
|
+
}
|
|
1020
|
+
function describeAutoSwitchNoop(candidate, warnings) {
|
|
1021
|
+
const lines = [
|
|
1022
|
+
`Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
|
|
1023
|
+
`Score: ${candidate.effective_score}`,
|
|
1024
|
+
`5H remaining: ${candidate.remain_5h}%`,
|
|
1025
|
+
`1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
|
|
1026
|
+
];
|
|
1027
|
+
for (const warning of warnings)lines.push(`Warning: ${warning}`);
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
963
1030
|
function describeQuotaAccounts(accounts, warnings) {
|
|
964
1031
|
if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
|
|
965
1032
|
const table = formatTable(accounts.map((account)=>({
|
|
966
1033
|
name: account.name,
|
|
967
1034
|
account_id: maskAccountId(account.account_id),
|
|
968
1035
|
plan_type: account.plan_type ?? "-",
|
|
1036
|
+
available: computeAvailability(account) ?? "-",
|
|
969
1037
|
five_hour: formatUsagePercent(account.five_hour),
|
|
970
1038
|
five_hour_reset: formatResetAt(account.five_hour),
|
|
971
1039
|
one_week: formatUsagePercent(account.one_week),
|
|
972
1040
|
one_week_reset: formatResetAt(account.one_week),
|
|
973
|
-
|
|
1041
|
+
refresh_status: account.status
|
|
974
1042
|
})), [
|
|
975
1043
|
{
|
|
976
1044
|
key: "name",
|
|
@@ -984,6 +1052,10 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
984
1052
|
key: "plan_type",
|
|
985
1053
|
label: "PLAN TYPE"
|
|
986
1054
|
},
|
|
1055
|
+
{
|
|
1056
|
+
key: "available",
|
|
1057
|
+
label: "AVAILABLE"
|
|
1058
|
+
},
|
|
987
1059
|
{
|
|
988
1060
|
key: "five_hour",
|
|
989
1061
|
label: "5H USED"
|
|
@@ -1001,8 +1073,8 @@ function describeQuotaAccounts(accounts, warnings) {
|
|
|
1001
1073
|
label: "1W RESET AT"
|
|
1002
1074
|
},
|
|
1003
1075
|
{
|
|
1004
|
-
key: "
|
|
1005
|
-
label: "STATUS"
|
|
1076
|
+
key: "refresh_status",
|
|
1077
|
+
label: "REFRESH STATUS"
|
|
1006
1078
|
}
|
|
1007
1079
|
]);
|
|
1008
1080
|
const lines = [
|
|
@@ -1058,10 +1130,11 @@ async function runCli(argv, options = {}) {
|
|
|
1058
1130
|
}
|
|
1059
1131
|
case "list":
|
|
1060
1132
|
{
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1133
|
+
const targetName = parsed.positionals[0];
|
|
1134
|
+
const result = await store.refreshAllQuotas(targetName);
|
|
1135
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1136
|
+
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1137
|
+
return 0 === result.failures.length ? 0 : 1;
|
|
1065
1138
|
}
|
|
1066
1139
|
case "save":
|
|
1067
1140
|
{
|
|
@@ -1089,7 +1162,8 @@ async function runCli(argv, options = {}) {
|
|
|
1089
1162
|
try {
|
|
1090
1163
|
const quotaResult = await store.refreshQuotaForAccount(result.account.name);
|
|
1091
1164
|
const quotaList = await store.listQuotaSummaries();
|
|
1092
|
-
|
|
1165
|
+
const matched = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
|
|
1166
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1093
1167
|
} catch (error) {
|
|
1094
1168
|
warnings.push(error.message);
|
|
1095
1169
|
}
|
|
@@ -1114,31 +1188,93 @@ async function runCli(argv, options = {}) {
|
|
|
1114
1188
|
case "quota":
|
|
1115
1189
|
{
|
|
1116
1190
|
const quotaCommand = parsed.positionals[0];
|
|
1117
|
-
if ("list" === quotaCommand) {
|
|
1118
|
-
const result = await store.listQuotaSummaries();
|
|
1119
|
-
if (json) writeJson(streams.stdout, result);
|
|
1120
|
-
else streams.stdout.write(`${describeQuotaAccounts(result.accounts, result.warnings)}\n`);
|
|
1121
|
-
return 0;
|
|
1122
|
-
}
|
|
1123
1191
|
if ("refresh" === quotaCommand) {
|
|
1124
1192
|
const targetName = parsed.positionals[1];
|
|
1125
1193
|
const result = await store.refreshAllQuotas(targetName);
|
|
1126
|
-
if (json) writeJson(streams.stdout, result);
|
|
1194
|
+
if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
|
|
1127
1195
|
else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
|
|
1128
1196
|
return 0 === result.failures.length ? 0 : 1;
|
|
1129
1197
|
}
|
|
1130
|
-
throw new Error("Usage: codexm quota
|
|
1198
|
+
throw new Error("Usage: codexm quota refresh [name] [--json]");
|
|
1131
1199
|
}
|
|
1132
1200
|
case "switch":
|
|
1133
1201
|
{
|
|
1202
|
+
const auto = parsed.flags.has("--auto");
|
|
1203
|
+
const dryRun = parsed.flags.has("--dry-run");
|
|
1134
1204
|
const name = parsed.positionals[0];
|
|
1205
|
+
if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1206
|
+
if (auto) {
|
|
1207
|
+
if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
|
|
1208
|
+
const refreshResult = await store.refreshAllQuotas();
|
|
1209
|
+
const candidates = rankAutoSwitchCandidates(refreshResult.successes);
|
|
1210
|
+
if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
|
|
1211
|
+
const selected = candidates[0];
|
|
1212
|
+
const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
|
|
1213
|
+
const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
|
|
1214
|
+
if (dryRun) {
|
|
1215
|
+
const payload = {
|
|
1216
|
+
ok: true,
|
|
1217
|
+
action: "switch",
|
|
1218
|
+
mode: "auto",
|
|
1219
|
+
dry_run: true,
|
|
1220
|
+
selected,
|
|
1221
|
+
candidates,
|
|
1222
|
+
warnings
|
|
1223
|
+
};
|
|
1224
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1225
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
|
|
1226
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1227
|
+
}
|
|
1228
|
+
const currentStatus = await store.getCurrentStatus();
|
|
1229
|
+
if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
|
|
1230
|
+
const payload = {
|
|
1231
|
+
ok: true,
|
|
1232
|
+
action: "switch",
|
|
1233
|
+
mode: "auto",
|
|
1234
|
+
skipped: true,
|
|
1235
|
+
reason: "already_current_best",
|
|
1236
|
+
account: {
|
|
1237
|
+
name: selected.name,
|
|
1238
|
+
account_id: selected.account_id
|
|
1239
|
+
},
|
|
1240
|
+
selected,
|
|
1241
|
+
candidates,
|
|
1242
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1243
|
+
warnings
|
|
1244
|
+
};
|
|
1245
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1246
|
+
else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
|
|
1247
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1248
|
+
}
|
|
1249
|
+
const result = await store.switchAccount(selected.name);
|
|
1250
|
+
for (const warning of warnings)result.warnings.push(warning);
|
|
1251
|
+
const payload = {
|
|
1252
|
+
ok: true,
|
|
1253
|
+
action: "switch",
|
|
1254
|
+
mode: "auto",
|
|
1255
|
+
account: {
|
|
1256
|
+
name: result.account.name,
|
|
1257
|
+
account_id: result.account.account_id,
|
|
1258
|
+
auth_mode: result.account.auth_mode
|
|
1259
|
+
},
|
|
1260
|
+
selected,
|
|
1261
|
+
candidates,
|
|
1262
|
+
quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
|
|
1263
|
+
backup_path: result.backup_path,
|
|
1264
|
+
warnings: result.warnings
|
|
1265
|
+
};
|
|
1266
|
+
if (json) writeJson(streams.stdout, payload);
|
|
1267
|
+
else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
|
|
1268
|
+
return 0 === refreshResult.failures.length ? 0 : 1;
|
|
1269
|
+
}
|
|
1135
1270
|
if (!name) throw new Error("Usage: codexm switch <name>");
|
|
1136
1271
|
const result = await store.switchAccount(name);
|
|
1137
1272
|
let quota = null;
|
|
1138
1273
|
try {
|
|
1139
1274
|
await store.refreshQuotaForAccount(result.account.name);
|
|
1140
1275
|
const quotaList = await store.listQuotaSummaries();
|
|
1141
|
-
|
|
1276
|
+
const matched = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
|
|
1277
|
+
quota = matched ? toCliQuotaSummary(matched) : null;
|
|
1142
1278
|
} catch (error) {
|
|
1143
1279
|
result.warnings.push(error.message);
|
|
1144
1280
|
}
|