codex-team 0.0.6 → 0.0.8

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
@@ -24,8 +24,6 @@ codexm switch <name>
24
24
  codexm switch --auto --dry-run
25
25
  codexm remove <name> --yes
26
26
  codexm rename <old> <new>
27
- codexm quota refresh [name]
28
- codexm doctor
29
27
  ```
30
28
 
31
29
  Use `--json` on query and mutation commands when you need machine-readable output.
@@ -36,7 +34,7 @@ Use `--json` on query and mutation commands when you need machine-readable outpu
36
34
  2. Save the current auth snapshot with `codexm save <name>`.
37
35
  3. Repeat for other accounts.
38
36
  4. Switch between saved accounts with `codexm switch <name>` or let the tool choose with `codexm switch --auto`.
39
- 5. Refresh and inspect quota usage with `codexm list` or `codexm quota refresh`.
37
+ 5. Refresh and inspect quota usage with `codexm list`.
40
38
 
41
39
  ## Development
42
40
 
package/dist/cli.cjs CHANGED
@@ -13,7 +13,7 @@ var __webpack_modules__ = {
13
13
  const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
14
14
  var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
15
15
  var package_namespaceObject = {
16
- rE: "0.0.6"
16
+ rE: "0.0.8"
17
17
  };
18
18
  const external_node_crypto_namespaceObject = require("node:crypto");
19
19
  const promises_namespaceObject = require("node:fs/promises");
@@ -456,6 +456,7 @@ var __webpack_modules__ = {
456
456
  codexDir,
457
457
  codexTeamDir,
458
458
  currentAuthPath: (0, external_node_path_namespaceObject.join)(codexDir, "auth.json"),
459
+ currentConfigPath: (0, external_node_path_namespaceObject.join)(codexDir, "config.toml"),
459
460
  accountsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "accounts"),
460
461
  backupsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "backups"),
461
462
  statePath: (0, external_node_path_namespaceObject.join)(codexTeamDir, "state.json")
@@ -548,12 +549,44 @@ var __webpack_modules__ = {
548
549
  accountMetaPath(name) {
549
550
  return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "meta.json");
550
551
  }
552
+ accountConfigPath(name) {
553
+ return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "config.toml");
554
+ }
551
555
  async writeAccountAuthSnapshot(name, snapshot) {
552
556
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
553
557
  }
554
558
  async writeAccountMeta(name, meta) {
555
559
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
556
560
  }
561
+ validateConfigSnapshot(name, snapshot, rawConfig) {
562
+ if ("apikey" !== snapshot.auth_mode) return;
563
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
564
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
565
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
566
+ }
567
+ sanitizeConfigForAccountAuth(rawConfig) {
568
+ const lines = rawConfig.split(/\r?\n/u);
569
+ const result = [];
570
+ let skippingProviderSection = false;
571
+ for (const line of lines){
572
+ const trimmed = line.trim();
573
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
574
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
575
+ if (skippingProviderSection) continue;
576
+ }
577
+ if (!skippingProviderSection) {
578
+ if (!/^\s*model_provider\s*=/u.test(line)) {
579
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
580
+ }
581
+ }
582
+ }
583
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
584
+ }
585
+ async ensureEmptyAccountConfigSnapshot(name) {
586
+ const configPath = this.accountConfigPath(name);
587
+ await atomicWriteFile(configPath, "");
588
+ return configPath;
589
+ }
557
590
  async syncCurrentAuthIfMatching(snapshot) {
558
591
  if (!await pathExists(this.paths.currentAuthPath)) return;
559
592
  try {
@@ -619,6 +652,7 @@ var __webpack_modules__ = {
619
652
  ...meta,
620
653
  authPath,
621
654
  metaPath,
655
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
622
656
  duplicateAccountId: false
623
657
  };
624
658
  }
@@ -675,14 +709,28 @@ var __webpack_modules__ = {
675
709
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
676
710
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
677
711
  const snapshot = parseAuthSnapshot(rawSnapshot);
712
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
678
713
  const accountDir = this.accountDirectory(name);
679
714
  const authPath = this.accountAuthPath(name);
680
715
  const metaPath = this.accountMetaPath(name);
716
+ const configPath = this.accountConfigPath(name);
717
+ const identity = getSnapshotIdentity(snapshot);
681
718
  const accountExists = await pathExists(accountDir);
682
719
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
683
720
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
721
+ const { accounts } = await this.listAccounts();
722
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
723
+ if (duplicateIdentityAccounts.length > 0) {
724
+ const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
725
+ throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
726
+ }
727
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
684
728
  await ensureDirectory(accountDir, DIRECTORY_MODE);
685
729
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
730
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
731
+ else if (await pathExists(configPath)) await (0, promises_namespaceObject.rm)(configPath, {
732
+ force: true
733
+ });
686
734
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
687
735
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
688
736
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -698,9 +746,15 @@ var __webpack_modules__ = {
698
746
  const name = current.matched_accounts[0];
699
747
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
700
748
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
749
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
701
750
  const metaPath = this.accountMetaPath(name);
702
751
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
752
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
703
753
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
754
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
755
+ else if (await pathExists(this.accountConfigPath(name))) await (0, promises_namespaceObject.rm)(this.accountConfigPath(name), {
756
+ force: true
757
+ });
704
758
  await atomicWriteFile(metaPath, stringifyJson({
705
759
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
706
760
  last_switched_at: existingMeta.last_switched_at,
@@ -722,8 +776,23 @@ var __webpack_modules__ = {
722
776
  await (0, promises_namespaceObject.copyFile)(this.paths.currentAuthPath, backupPath);
723
777
  await chmodIfPossible(backupPath, FILE_MODE);
724
778
  }
779
+ if (await pathExists(this.paths.currentConfigPath)) {
780
+ const configBackupPath = (0, external_node_path_namespaceObject.join)(this.paths.backupsDir, "last-active-config.toml");
781
+ await (0, promises_namespaceObject.copyFile)(this.paths.currentConfigPath, configBackupPath);
782
+ await chmodIfPossible(configBackupPath, FILE_MODE);
783
+ }
725
784
  const rawAuth = await readJsonFile(account.authPath);
726
785
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
786
+ if ("apikey" === account.auth_mode && account.configPath) {
787
+ const rawConfig = await readJsonFile(account.configPath);
788
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
789
+ } else if ("apikey" === account.auth_mode) {
790
+ await this.ensureEmptyAccountConfigSnapshot(name);
791
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
792
+ } else if (await pathExists(this.paths.currentConfigPath)) {
793
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
794
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
795
+ }
727
796
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
728
797
  if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
729
798
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
@@ -881,6 +950,7 @@ var __webpack_modules__ = {
881
950
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
882
951
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
883
952
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
953
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
884
954
  if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
885
955
  }
886
956
  let currentAuthPresent = false;
@@ -949,12 +1019,10 @@ Usage:
949
1019
  codexm list [name] [--json]
950
1020
  codexm save <name> [--force] [--json]
951
1021
  codexm update [--json]
952
- codexm quota refresh [name] [--json]
953
1022
  codexm switch <name> [--json]
954
1023
  codexm switch --auto [--dry-run] [--json]
955
1024
  codexm remove <name> [--yes] [--json]
956
1025
  codexm rename <old> <new> [--json]
957
- codexm doctor [--json]
958
1026
 
959
1027
  Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
960
1028
  `);
@@ -972,16 +1040,6 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
972
1040
  for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
973
1041
  return lines.join("\n");
974
1042
  }
975
- function describeDoctor(report) {
976
- const lines = [
977
- report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
978
- `Saved accounts: ${report.account_count}`,
979
- `Current auth present: ${report.current_auth_present ? "yes" : "no"}`
980
- ];
981
- for (const issue of report.issues)lines.push(`Issue: ${issue}`);
982
- for (const warning of report.warnings)lines.push(`Warning: ${warning}`);
983
- return lines.join("\n");
984
- }
985
1043
  function formatUsagePercent(window) {
986
1044
  if (!window) return "-";
987
1045
  return `${window.used_percent}%`;
@@ -1245,18 +1303,6 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1245
1303
  }
1246
1304
  return 0;
1247
1305
  }
1248
- case "quota":
1249
- {
1250
- const quotaCommand = parsed.positionals[0];
1251
- if ("refresh" === quotaCommand) {
1252
- const targetName = parsed.positionals[1];
1253
- const result = await store.refreshAllQuotas(targetName);
1254
- if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
1255
- else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
1256
- return 0 === result.failures.length ? 0 : 1;
1257
- }
1258
- throw new Error("Usage: codexm quota refresh [name] [--json]");
1259
- }
1260
1306
  case "switch":
1261
1307
  {
1262
1308
  const auto = parsed.flags.has("--auto");
@@ -1400,13 +1446,6 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1400
1446
  else streams.stdout.write(`Renamed "${oldName}" to "${newName}".\n`);
1401
1447
  return 0;
1402
1448
  }
1403
- case "doctor":
1404
- {
1405
- const report = await store.doctor();
1406
- if (json) writeJson(streams.stdout, report);
1407
- else streams.stdout.write(`${describeDoctor(report)}\n`);
1408
- return report.healthy ? 0 : 1;
1409
- }
1410
1449
  default:
1411
1450
  throw new Error(`Unknown command "${parsed.command}".`);
1412
1451
  }
package/dist/main.cjs CHANGED
@@ -43,7 +43,7 @@ var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namesp
43
43
  const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
44
44
  var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
45
45
  var package_namespaceObject = {
46
- rE: "0.0.6"
46
+ rE: "0.0.8"
47
47
  };
48
48
  const external_node_crypto_namespaceObject = require("node:crypto");
49
49
  const promises_namespaceObject = require("node:fs/promises");
@@ -486,6 +486,7 @@ function defaultPaths(homeDir = (0, external_node_os_namespaceObject.homedir)())
486
486
  codexDir,
487
487
  codexTeamDir,
488
488
  currentAuthPath: (0, external_node_path_namespaceObject.join)(codexDir, "auth.json"),
489
+ currentConfigPath: (0, external_node_path_namespaceObject.join)(codexDir, "config.toml"),
489
490
  accountsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "accounts"),
490
491
  backupsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "backups"),
491
492
  statePath: (0, external_node_path_namespaceObject.join)(codexTeamDir, "state.json")
@@ -578,12 +579,44 @@ class AccountStore {
578
579
  accountMetaPath(name) {
579
580
  return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "meta.json");
580
581
  }
582
+ accountConfigPath(name) {
583
+ return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "config.toml");
584
+ }
581
585
  async writeAccountAuthSnapshot(name, snapshot) {
582
586
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
583
587
  }
584
588
  async writeAccountMeta(name, meta) {
585
589
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
586
590
  }
591
+ validateConfigSnapshot(name, snapshot, rawConfig) {
592
+ if ("apikey" !== snapshot.auth_mode) return;
593
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
594
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
595
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
596
+ }
597
+ sanitizeConfigForAccountAuth(rawConfig) {
598
+ const lines = rawConfig.split(/\r?\n/u);
599
+ const result = [];
600
+ let skippingProviderSection = false;
601
+ for (const line of lines){
602
+ const trimmed = line.trim();
603
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
604
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
605
+ if (skippingProviderSection) continue;
606
+ }
607
+ if (!skippingProviderSection) {
608
+ if (!/^\s*model_provider\s*=/u.test(line)) {
609
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
610
+ }
611
+ }
612
+ }
613
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
614
+ }
615
+ async ensureEmptyAccountConfigSnapshot(name) {
616
+ const configPath = this.accountConfigPath(name);
617
+ await atomicWriteFile(configPath, "");
618
+ return configPath;
619
+ }
587
620
  async syncCurrentAuthIfMatching(snapshot) {
588
621
  if (!await pathExists(this.paths.currentAuthPath)) return;
589
622
  try {
@@ -649,6 +682,7 @@ class AccountStore {
649
682
  ...meta,
650
683
  authPath,
651
684
  metaPath,
685
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
652
686
  duplicateAccountId: false
653
687
  };
654
688
  }
@@ -705,14 +739,28 @@ class AccountStore {
705
739
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
706
740
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
707
741
  const snapshot = parseAuthSnapshot(rawSnapshot);
742
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
708
743
  const accountDir = this.accountDirectory(name);
709
744
  const authPath = this.accountAuthPath(name);
710
745
  const metaPath = this.accountMetaPath(name);
746
+ const configPath = this.accountConfigPath(name);
747
+ const identity = getSnapshotIdentity(snapshot);
711
748
  const accountExists = await pathExists(accountDir);
712
749
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
713
750
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
751
+ const { accounts } = await this.listAccounts();
752
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
753
+ if (duplicateIdentityAccounts.length > 0) {
754
+ const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
755
+ throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
756
+ }
757
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
714
758
  await ensureDirectory(accountDir, DIRECTORY_MODE);
715
759
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
760
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
761
+ else if (await pathExists(configPath)) await (0, promises_namespaceObject.rm)(configPath, {
762
+ force: true
763
+ });
716
764
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
717
765
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
718
766
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -728,9 +776,15 @@ class AccountStore {
728
776
  const name = current.matched_accounts[0];
729
777
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
730
778
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
779
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
731
780
  const metaPath = this.accountMetaPath(name);
732
781
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
782
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
733
783
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
784
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
785
+ else if (await pathExists(this.accountConfigPath(name))) await (0, promises_namespaceObject.rm)(this.accountConfigPath(name), {
786
+ force: true
787
+ });
734
788
  await atomicWriteFile(metaPath, stringifyJson({
735
789
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
736
790
  last_switched_at: existingMeta.last_switched_at,
@@ -752,8 +806,23 @@ class AccountStore {
752
806
  await (0, promises_namespaceObject.copyFile)(this.paths.currentAuthPath, backupPath);
753
807
  await chmodIfPossible(backupPath, FILE_MODE);
754
808
  }
809
+ if (await pathExists(this.paths.currentConfigPath)) {
810
+ const configBackupPath = (0, external_node_path_namespaceObject.join)(this.paths.backupsDir, "last-active-config.toml");
811
+ await (0, promises_namespaceObject.copyFile)(this.paths.currentConfigPath, configBackupPath);
812
+ await chmodIfPossible(configBackupPath, FILE_MODE);
813
+ }
755
814
  const rawAuth = await readJsonFile(account.authPath);
756
815
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
816
+ if ("apikey" === account.auth_mode && account.configPath) {
817
+ const rawConfig = await readJsonFile(account.configPath);
818
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
819
+ } else if ("apikey" === account.auth_mode) {
820
+ await this.ensureEmptyAccountConfigSnapshot(name);
821
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
822
+ } else if (await pathExists(this.paths.currentConfigPath)) {
823
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
824
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
825
+ }
757
826
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
758
827
  if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
759
828
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
@@ -911,6 +980,7 @@ class AccountStore {
911
980
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
912
981
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
913
982
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
983
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
914
984
  if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
915
985
  }
916
986
  let currentAuthPresent = false;
@@ -979,12 +1049,10 @@ Usage:
979
1049
  codexm list [name] [--json]
980
1050
  codexm save <name> [--force] [--json]
981
1051
  codexm update [--json]
982
- codexm quota refresh [name] [--json]
983
1052
  codexm switch <name> [--json]
984
1053
  codexm switch --auto [--dry-run] [--json]
985
1054
  codexm remove <name> [--yes] [--json]
986
1055
  codexm rename <old> <new> [--json]
987
- codexm doctor [--json]
988
1056
 
989
1057
  Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
990
1058
  `);
@@ -1002,16 +1070,6 @@ function describeCurrentStatus(status) {
1002
1070
  for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
1003
1071
  return lines.join("\n");
1004
1072
  }
1005
- function describeDoctor(report) {
1006
- const lines = [
1007
- report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
1008
- `Saved accounts: ${report.account_count}`,
1009
- `Current auth present: ${report.current_auth_present ? "yes" : "no"}`
1010
- ];
1011
- for (const issue of report.issues)lines.push(`Issue: ${issue}`);
1012
- for (const warning of report.warnings)lines.push(`Warning: ${warning}`);
1013
- return lines.join("\n");
1014
- }
1015
1073
  function formatUsagePercent(window) {
1016
1074
  if (!window) return "-";
1017
1075
  return `${window.used_percent}%`;
@@ -1275,18 +1333,6 @@ async function runCli(argv, options = {}) {
1275
1333
  }
1276
1334
  return 0;
1277
1335
  }
1278
- case "quota":
1279
- {
1280
- const quotaCommand = parsed.positionals[0];
1281
- if ("refresh" === quotaCommand) {
1282
- const targetName = parsed.positionals[1];
1283
- const result = await store.refreshAllQuotas(targetName);
1284
- if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
1285
- else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
1286
- return 0 === result.failures.length ? 0 : 1;
1287
- }
1288
- throw new Error("Usage: codexm quota refresh [name] [--json]");
1289
- }
1290
1336
  case "switch":
1291
1337
  {
1292
1338
  const auto = parsed.flags.has("--auto");
@@ -1430,13 +1476,6 @@ async function runCli(argv, options = {}) {
1430
1476
  else streams.stdout.write(`Renamed "${oldName}" to "${newName}".\n`);
1431
1477
  return 0;
1432
1478
  }
1433
- case "doctor":
1434
- {
1435
- const report = await store.doctor();
1436
- if (json) writeJson(streams.stdout, report);
1437
- else streams.stdout.write(`${describeDoctor(report)}\n`);
1438
- return report.healthy ? 0 : 1;
1439
- }
1440
1479
  default:
1441
1480
  throw new Error(`Unknown command "${parsed.command}".`);
1442
1481
  }
package/dist/main.js CHANGED
@@ -9,7 +9,7 @@ import { basename, dirname, join } from "node:path";
9
9
  import { execFile } from "node:child_process";
10
10
  import { promisify } from "node:util";
11
11
  var package_namespaceObject = {
12
- rE: "0.0.6"
12
+ rE: "0.0.8"
13
13
  };
14
14
  function isRecord(value) {
15
15
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -446,6 +446,7 @@ function defaultPaths(homeDir = homedir()) {
446
446
  codexDir,
447
447
  codexTeamDir,
448
448
  currentAuthPath: join(codexDir, "auth.json"),
449
+ currentConfigPath: join(codexDir, "config.toml"),
449
450
  accountsDir: join(codexTeamDir, "accounts"),
450
451
  backupsDir: join(codexTeamDir, "backups"),
451
452
  statePath: join(codexTeamDir, "state.json")
@@ -538,12 +539,44 @@ class AccountStore {
538
539
  accountMetaPath(name) {
539
540
  return join(this.accountDirectory(name), "meta.json");
540
541
  }
542
+ accountConfigPath(name) {
543
+ return join(this.accountDirectory(name), "config.toml");
544
+ }
541
545
  async writeAccountAuthSnapshot(name, snapshot) {
542
546
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
543
547
  }
544
548
  async writeAccountMeta(name, meta) {
545
549
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
546
550
  }
551
+ validateConfigSnapshot(name, snapshot, rawConfig) {
552
+ if ("apikey" !== snapshot.auth_mode) return;
553
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
554
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
555
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
556
+ }
557
+ sanitizeConfigForAccountAuth(rawConfig) {
558
+ const lines = rawConfig.split(/\r?\n/u);
559
+ const result = [];
560
+ let skippingProviderSection = false;
561
+ for (const line of lines){
562
+ const trimmed = line.trim();
563
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
564
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
565
+ if (skippingProviderSection) continue;
566
+ }
567
+ if (!skippingProviderSection) {
568
+ if (!/^\s*model_provider\s*=/u.test(line)) {
569
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
570
+ }
571
+ }
572
+ }
573
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
574
+ }
575
+ async ensureEmptyAccountConfigSnapshot(name) {
576
+ const configPath = this.accountConfigPath(name);
577
+ await atomicWriteFile(configPath, "");
578
+ return configPath;
579
+ }
547
580
  async syncCurrentAuthIfMatching(snapshot) {
548
581
  if (!await pathExists(this.paths.currentAuthPath)) return;
549
582
  try {
@@ -609,6 +642,7 @@ class AccountStore {
609
642
  ...meta,
610
643
  authPath,
611
644
  metaPath,
645
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
612
646
  duplicateAccountId: false
613
647
  };
614
648
  }
@@ -665,14 +699,28 @@ class AccountStore {
665
699
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
666
700
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
667
701
  const snapshot = parseAuthSnapshot(rawSnapshot);
702
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
668
703
  const accountDir = this.accountDirectory(name);
669
704
  const authPath = this.accountAuthPath(name);
670
705
  const metaPath = this.accountMetaPath(name);
706
+ const configPath = this.accountConfigPath(name);
707
+ const identity = getSnapshotIdentity(snapshot);
671
708
  const accountExists = await pathExists(accountDir);
672
709
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
673
710
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
711
+ const { accounts } = await this.listAccounts();
712
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
713
+ if (duplicateIdentityAccounts.length > 0) {
714
+ const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
715
+ throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
716
+ }
717
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
674
718
  await ensureDirectory(accountDir, DIRECTORY_MODE);
675
719
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
720
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
721
+ else if (await pathExists(configPath)) await rm(configPath, {
722
+ force: true
723
+ });
676
724
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
677
725
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
678
726
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -688,9 +736,15 @@ class AccountStore {
688
736
  const name = current.matched_accounts[0];
689
737
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
690
738
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
739
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
691
740
  const metaPath = this.accountMetaPath(name);
692
741
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
742
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
693
743
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
744
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
745
+ else if (await pathExists(this.accountConfigPath(name))) await rm(this.accountConfigPath(name), {
746
+ force: true
747
+ });
694
748
  await atomicWriteFile(metaPath, stringifyJson({
695
749
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
696
750
  last_switched_at: existingMeta.last_switched_at,
@@ -712,8 +766,23 @@ class AccountStore {
712
766
  await copyFile(this.paths.currentAuthPath, backupPath);
713
767
  await chmodIfPossible(backupPath, FILE_MODE);
714
768
  }
769
+ if (await pathExists(this.paths.currentConfigPath)) {
770
+ const configBackupPath = join(this.paths.backupsDir, "last-active-config.toml");
771
+ await copyFile(this.paths.currentConfigPath, configBackupPath);
772
+ await chmodIfPossible(configBackupPath, FILE_MODE);
773
+ }
715
774
  const rawAuth = await readJsonFile(account.authPath);
716
775
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
776
+ if ("apikey" === account.auth_mode && account.configPath) {
777
+ const rawConfig = await readJsonFile(account.configPath);
778
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
779
+ } else if ("apikey" === account.auth_mode) {
780
+ await this.ensureEmptyAccountConfigSnapshot(name);
781
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
782
+ } else if (await pathExists(this.paths.currentConfigPath)) {
783
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
784
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
785
+ }
717
786
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
718
787
  if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
719
788
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
@@ -871,6 +940,7 @@ class AccountStore {
871
940
  const metaStat = await stat(account.metaPath);
872
941
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
873
942
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
943
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
874
944
  if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
875
945
  }
876
946
  let currentAuthPresent = false;
@@ -939,12 +1009,10 @@ Usage:
939
1009
  codexm list [name] [--json]
940
1010
  codexm save <name> [--force] [--json]
941
1011
  codexm update [--json]
942
- codexm quota refresh [name] [--json]
943
1012
  codexm switch <name> [--json]
944
1013
  codexm switch --auto [--dry-run] [--json]
945
1014
  codexm remove <name> [--yes] [--json]
946
1015
  codexm rename <old> <new> [--json]
947
- codexm doctor [--json]
948
1016
 
949
1017
  Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
950
1018
  `);
@@ -962,16 +1030,6 @@ function describeCurrentStatus(status) {
962
1030
  for (const warning of status.warnings)lines.push(`Warning: ${warning}`);
963
1031
  return lines.join("\n");
964
1032
  }
965
- function describeDoctor(report) {
966
- const lines = [
967
- report.healthy ? "Doctor checks passed." : "Doctor checks found issues.",
968
- `Saved accounts: ${report.account_count}`,
969
- `Current auth present: ${report.current_auth_present ? "yes" : "no"}`
970
- ];
971
- for (const issue of report.issues)lines.push(`Issue: ${issue}`);
972
- for (const warning of report.warnings)lines.push(`Warning: ${warning}`);
973
- return lines.join("\n");
974
- }
975
1033
  function formatUsagePercent(window) {
976
1034
  if (!window) return "-";
977
1035
  return `${window.used_percent}%`;
@@ -1235,18 +1293,6 @@ async function runCli(argv, options = {}) {
1235
1293
  }
1236
1294
  return 0;
1237
1295
  }
1238
- case "quota":
1239
- {
1240
- const quotaCommand = parsed.positionals[0];
1241
- if ("refresh" === quotaCommand) {
1242
- const targetName = parsed.positionals[1];
1243
- const result = await store.refreshAllQuotas(targetName);
1244
- if (json) writeJson(streams.stdout, toCliQuotaRefreshResult(result));
1245
- else streams.stdout.write(`${describeQuotaRefresh(result)}\n`);
1246
- return 0 === result.failures.length ? 0 : 1;
1247
- }
1248
- throw new Error("Usage: codexm quota refresh [name] [--json]");
1249
- }
1250
1296
  case "switch":
1251
1297
  {
1252
1298
  const auto = parsed.flags.has("--auto");
@@ -1390,13 +1436,6 @@ async function runCli(argv, options = {}) {
1390
1436
  else streams.stdout.write(`Renamed "${oldName}" to "${newName}".\n`);
1391
1437
  return 0;
1392
1438
  }
1393
- case "doctor":
1394
- {
1395
- const report = await store.doctor();
1396
- if (json) writeJson(streams.stdout, report);
1397
- else streams.stdout.write(`${describeDoctor(report)}\n`);
1398
- return report.healthy ? 0 : 1;
1399
- }
1400
1439
  default:
1401
1440
  throw new Error(`Unknown command "${parsed.command}".`);
1402
1441
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",