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 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 quota refresh` and `codexm quota list`.
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 account of targets)try {
765
- const refreshed = await this.refreshQuotaForAccount(account.name);
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
- status: account.status
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: "status",
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 result = await store.listAccounts();
1072
- if (json) writeJson(streams.stdout, result);
1073
- else streams.stdout.write(`${describeAccounts(result.accounts, result.warnings)}\n`);
1074
- return 0;
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
- quota = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
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 <refresh [name] | list> [--json]");
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
- quota = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
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 account of targets)try {
795
- const refreshed = await this.refreshQuotaForAccount(account.name);
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
- status: account.status
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: "status",
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 result = await store.listAccounts();
1102
- if (json) writeJson(streams.stdout, result);
1103
- else streams.stdout.write(`${describeAccounts(result.accounts, result.warnings)}\n`);
1104
- return 0;
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
- quota = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
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 <refresh [name] | list> [--json]");
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
- quota = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
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 account of targets)try {
755
- const refreshed = await this.refreshQuotaForAccount(account.name);
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
- status: account.status
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: "status",
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 result = await store.listAccounts();
1062
- if (json) writeJson(streams.stdout, result);
1063
- else streams.stdout.write(`${describeAccounts(result.accounts, result.warnings)}\n`);
1064
- return 0;
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
- quota = quotaList.accounts.find((account)=>account.name === quotaResult.account.name) ?? null;
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 <refresh [name] | list> [--json]");
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
- quota = quotaList.accounts.find((account)=>account.name === result.account.name) ?? null;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",