codex-team 0.0.2 → 0.0.4

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
@@ -20,6 +20,7 @@ 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]
@@ -33,7 +34,7 @@ Use `--json` on query and mutation commands when you need machine-readable outpu
33
34
  1. Log into a target account with the native Codex CLI.
34
35
  2. Save the current auth snapshot with `codexm save <name>`.
35
36
  3. Repeat for other accounts.
36
- 4. Switch between saved accounts with `codexm switch <name>`.
37
+ 4. Switch between saved accounts with `codexm switch <name>` or let the tool choose with `codexm switch --auto`.
37
38
  5. Refresh and inspect quota usage with `codexm list` or `codexm quota refresh`.
38
39
 
39
40
  ## Development
package/dist/cli.cjs CHANGED
@@ -910,6 +910,7 @@ Usage:
910
910
  codexm update [--json]
911
911
  codexm quota refresh [name] [--json]
912
912
  codexm switch <name> [--json]
913
+ codexm switch --auto [--dry-run] [--json]
913
914
  codexm remove <name> [--yes] [--json]
914
915
  codexm rename <old> <new> [--json]
915
916
  codexm doctor [--json]
@@ -973,6 +974,69 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
973
974
  failures: result.failures
974
975
  };
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
+ }
976
1040
  function describeQuotaAccounts(accounts, warnings) {
977
1041
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
978
1042
  const table = formatTable(accounts.map((account)=>({
@@ -1043,12 +1107,17 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1043
1107
  if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
1044
1108
  streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
1045
1109
  return await new Promise((resolve)=>{
1110
+ const cleanup = ()=>{
1111
+ streams.stdin.off("data", onData);
1112
+ streams.stdin.pause();
1113
+ };
1046
1114
  const onData = (buffer)=>{
1047
1115
  const answer = buffer.toString("utf8").trim().toLowerCase();
1048
- streams.stdin.off("data", onData);
1116
+ cleanup();
1049
1117
  streams.stdout.write("\n");
1050
1118
  resolve("y" === answer || "yes" === answer);
1051
1119
  };
1120
+ streams.stdin.resume();
1052
1121
  streams.stdin.on("data", onData);
1053
1122
  });
1054
1123
  }
@@ -1145,7 +1214,74 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1145
1214
  }
1146
1215
  case "switch":
1147
1216
  {
1217
+ const auto = parsed.flags.has("--auto");
1218
+ const dryRun = parsed.flags.has("--dry-run");
1148
1219
  const name = parsed.positionals[0];
1220
+ if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1221
+ if (auto) {
1222
+ if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1223
+ const refreshResult = await store.refreshAllQuotas();
1224
+ const candidates = rankAutoSwitchCandidates(refreshResult.successes);
1225
+ if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
1226
+ const selected = candidates[0];
1227
+ const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
1228
+ const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
1229
+ if (dryRun) {
1230
+ const payload = {
1231
+ ok: true,
1232
+ action: "switch",
1233
+ mode: "auto",
1234
+ dry_run: true,
1235
+ selected,
1236
+ candidates,
1237
+ warnings
1238
+ };
1239
+ if (json) writeJson(streams.stdout, payload);
1240
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
1241
+ return 0 === refreshResult.failures.length ? 0 : 1;
1242
+ }
1243
+ const currentStatus = await store.getCurrentStatus();
1244
+ if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
1245
+ const payload = {
1246
+ ok: true,
1247
+ action: "switch",
1248
+ mode: "auto",
1249
+ skipped: true,
1250
+ reason: "already_current_best",
1251
+ account: {
1252
+ name: selected.name,
1253
+ account_id: selected.account_id
1254
+ },
1255
+ selected,
1256
+ candidates,
1257
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1258
+ warnings
1259
+ };
1260
+ if (json) writeJson(streams.stdout, payload);
1261
+ else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
1262
+ return 0 === refreshResult.failures.length ? 0 : 1;
1263
+ }
1264
+ const result = await store.switchAccount(selected.name);
1265
+ for (const warning of warnings)result.warnings.push(warning);
1266
+ const payload = {
1267
+ ok: true,
1268
+ action: "switch",
1269
+ mode: "auto",
1270
+ account: {
1271
+ name: result.account.name,
1272
+ account_id: result.account.account_id,
1273
+ auth_mode: result.account.auth_mode
1274
+ },
1275
+ selected,
1276
+ candidates,
1277
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1278
+ backup_path: result.backup_path,
1279
+ warnings: result.warnings
1280
+ };
1281
+ if (json) writeJson(streams.stdout, payload);
1282
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
1283
+ return 0 === refreshResult.failures.length ? 0 : 1;
1284
+ }
1149
1285
  if (!name) throw new Error("Usage: codexm switch <name>");
1150
1286
  const result = await store.switchAccount(name);
1151
1287
  let quota = null;
package/dist/main.cjs CHANGED
@@ -940,6 +940,7 @@ Usage:
940
940
  codexm update [--json]
941
941
  codexm quota refresh [name] [--json]
942
942
  codexm switch <name> [--json]
943
+ codexm switch --auto [--dry-run] [--json]
943
944
  codexm remove <name> [--yes] [--json]
944
945
  codexm rename <old> <new> [--json]
945
946
  codexm doctor [--json]
@@ -1003,6 +1004,69 @@ function toCliQuotaRefreshResult(result) {
1003
1004
  failures: result.failures
1004
1005
  };
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
+ }
1006
1070
  function describeQuotaAccounts(accounts, warnings) {
1007
1071
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
1008
1072
  const table = formatTable(accounts.map((account)=>({
@@ -1073,12 +1137,17 @@ async function confirmRemoval(name, streams) {
1073
1137
  if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
1074
1138
  streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
1075
1139
  return await new Promise((resolve)=>{
1140
+ const cleanup = ()=>{
1141
+ streams.stdin.off("data", onData);
1142
+ streams.stdin.pause();
1143
+ };
1076
1144
  const onData = (buffer)=>{
1077
1145
  const answer = buffer.toString("utf8").trim().toLowerCase();
1078
- streams.stdin.off("data", onData);
1146
+ cleanup();
1079
1147
  streams.stdout.write("\n");
1080
1148
  resolve("y" === answer || "yes" === answer);
1081
1149
  };
1150
+ streams.stdin.resume();
1082
1151
  streams.stdin.on("data", onData);
1083
1152
  });
1084
1153
  }
@@ -1175,7 +1244,74 @@ async function runCli(argv, options = {}) {
1175
1244
  }
1176
1245
  case "switch":
1177
1246
  {
1247
+ const auto = parsed.flags.has("--auto");
1248
+ const dryRun = parsed.flags.has("--dry-run");
1178
1249
  const name = parsed.positionals[0];
1250
+ if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1251
+ if (auto) {
1252
+ if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1253
+ const refreshResult = await store.refreshAllQuotas();
1254
+ const candidates = rankAutoSwitchCandidates(refreshResult.successes);
1255
+ if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
1256
+ const selected = candidates[0];
1257
+ const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
1258
+ const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
1259
+ if (dryRun) {
1260
+ const payload = {
1261
+ ok: true,
1262
+ action: "switch",
1263
+ mode: "auto",
1264
+ dry_run: true,
1265
+ selected,
1266
+ candidates,
1267
+ warnings
1268
+ };
1269
+ if (json) writeJson(streams.stdout, payload);
1270
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
1271
+ return 0 === refreshResult.failures.length ? 0 : 1;
1272
+ }
1273
+ const currentStatus = await store.getCurrentStatus();
1274
+ if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
1275
+ const payload = {
1276
+ ok: true,
1277
+ action: "switch",
1278
+ mode: "auto",
1279
+ skipped: true,
1280
+ reason: "already_current_best",
1281
+ account: {
1282
+ name: selected.name,
1283
+ account_id: selected.account_id
1284
+ },
1285
+ selected,
1286
+ candidates,
1287
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1288
+ warnings
1289
+ };
1290
+ if (json) writeJson(streams.stdout, payload);
1291
+ else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
1292
+ return 0 === refreshResult.failures.length ? 0 : 1;
1293
+ }
1294
+ const result = await store.switchAccount(selected.name);
1295
+ for (const warning of warnings)result.warnings.push(warning);
1296
+ const payload = {
1297
+ ok: true,
1298
+ action: "switch",
1299
+ mode: "auto",
1300
+ account: {
1301
+ name: result.account.name,
1302
+ account_id: result.account.account_id,
1303
+ auth_mode: result.account.auth_mode
1304
+ },
1305
+ selected,
1306
+ candidates,
1307
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1308
+ backup_path: result.backup_path,
1309
+ warnings: result.warnings
1310
+ };
1311
+ if (json) writeJson(streams.stdout, payload);
1312
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
1313
+ return 0 === refreshResult.failures.length ? 0 : 1;
1314
+ }
1179
1315
  if (!name) throw new Error("Usage: codexm switch <name>");
1180
1316
  const result = await store.switchAccount(name);
1181
1317
  let quota = null;
package/dist/main.js CHANGED
@@ -900,6 +900,7 @@ Usage:
900
900
  codexm update [--json]
901
901
  codexm quota refresh [name] [--json]
902
902
  codexm switch <name> [--json]
903
+ codexm switch --auto [--dry-run] [--json]
903
904
  codexm remove <name> [--yes] [--json]
904
905
  codexm rename <old> <new> [--json]
905
906
  codexm doctor [--json]
@@ -963,6 +964,69 @@ function toCliQuotaRefreshResult(result) {
963
964
  failures: result.failures
964
965
  };
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
+ }
966
1030
  function describeQuotaAccounts(accounts, warnings) {
967
1031
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
968
1032
  const table = formatTable(accounts.map((account)=>({
@@ -1033,12 +1097,17 @@ async function confirmRemoval(name, streams) {
1033
1097
  if (!streams.stdin.isTTY) throw new Error(`Refusing to remove "${name}" without --yes in a non-interactive terminal.`);
1034
1098
  streams.stdout.write(`Remove saved account "${name}"? [y/N] `);
1035
1099
  return await new Promise((resolve)=>{
1100
+ const cleanup = ()=>{
1101
+ streams.stdin.off("data", onData);
1102
+ streams.stdin.pause();
1103
+ };
1036
1104
  const onData = (buffer)=>{
1037
1105
  const answer = buffer.toString("utf8").trim().toLowerCase();
1038
- streams.stdin.off("data", onData);
1106
+ cleanup();
1039
1107
  streams.stdout.write("\n");
1040
1108
  resolve("y" === answer || "yes" === answer);
1041
1109
  };
1110
+ streams.stdin.resume();
1042
1111
  streams.stdin.on("data", onData);
1043
1112
  });
1044
1113
  }
@@ -1135,7 +1204,74 @@ async function runCli(argv, options = {}) {
1135
1204
  }
1136
1205
  case "switch":
1137
1206
  {
1207
+ const auto = parsed.flags.has("--auto");
1208
+ const dryRun = parsed.flags.has("--dry-run");
1138
1209
  const name = parsed.positionals[0];
1210
+ if (dryRun && !auto) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1211
+ if (auto) {
1212
+ if (name) throw new Error("Usage: codexm switch --auto [--dry-run] [--json]");
1213
+ const refreshResult = await store.refreshAllQuotas();
1214
+ const candidates = rankAutoSwitchCandidates(refreshResult.successes);
1215
+ if (0 === candidates.length) throw new Error("No auto-switch candidate has both 5H and 1W quota data available.");
1216
+ const selected = candidates[0];
1217
+ const selectedQuota = refreshResult.successes.find((account)=>account.name === selected.name) ?? null;
1218
+ const warnings = refreshResult.failures.map((failure)=>`${failure.name}: ${failure.error}`);
1219
+ if (dryRun) {
1220
+ const payload = {
1221
+ ok: true,
1222
+ action: "switch",
1223
+ mode: "auto",
1224
+ dry_run: true,
1225
+ selected,
1226
+ candidates,
1227
+ warnings
1228
+ };
1229
+ if (json) writeJson(streams.stdout, payload);
1230
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, true, null, warnings)}\n`);
1231
+ return 0 === refreshResult.failures.length ? 0 : 1;
1232
+ }
1233
+ const currentStatus = await store.getCurrentStatus();
1234
+ if ("available" === selected.available && currentStatus.matched_accounts.includes(selected.name)) {
1235
+ const payload = {
1236
+ ok: true,
1237
+ action: "switch",
1238
+ mode: "auto",
1239
+ skipped: true,
1240
+ reason: "already_current_best",
1241
+ account: {
1242
+ name: selected.name,
1243
+ account_id: selected.account_id
1244
+ },
1245
+ selected,
1246
+ candidates,
1247
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1248
+ warnings
1249
+ };
1250
+ if (json) writeJson(streams.stdout, payload);
1251
+ else streams.stdout.write(`${describeAutoSwitchNoop(selected, warnings)}\n`);
1252
+ return 0 === refreshResult.failures.length ? 0 : 1;
1253
+ }
1254
+ const result = await store.switchAccount(selected.name);
1255
+ for (const warning of warnings)result.warnings.push(warning);
1256
+ const payload = {
1257
+ ok: true,
1258
+ action: "switch",
1259
+ mode: "auto",
1260
+ account: {
1261
+ name: result.account.name,
1262
+ account_id: result.account.account_id,
1263
+ auth_mode: result.account.auth_mode
1264
+ },
1265
+ selected,
1266
+ candidates,
1267
+ quota: selectedQuota ? toCliQuotaSummary(selectedQuota) : null,
1268
+ backup_path: result.backup_path,
1269
+ warnings: result.warnings
1270
+ };
1271
+ if (json) writeJson(streams.stdout, payload);
1272
+ else streams.stdout.write(`${describeAutoSwitchSelection(selected, false, result.backup_path, result.warnings)}\n`);
1273
+ return 0 === refreshResult.failures.length ? 0 : 1;
1274
+ }
1139
1275
  if (!name) throw new Error("Usage: codexm switch <name>");
1140
1276
  const result = await store.switchAccount(name);
1141
1277
  let quota = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",