codex-team 0.0.8 → 0.0.9

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
@@ -36,6 +36,8 @@ Use `--json` on query and mutation commands when you need machine-readable outpu
36
36
  4. Switch between saved accounts with `codexm switch <name>` or let the tool choose with `codexm switch --auto`.
37
37
  5. Refresh and inspect quota usage with `codexm list`.
38
38
 
39
+ For ChatGPT auth snapshots, `codex-team` can save and switch different users under the same ChatGPT account/workspace as separate managed entries when the local login tokens distinguish them.
40
+
39
41
  ## Development
40
42
 
41
43
  ```bash
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.8"
16
+ rE: "0.0.9"
17
17
  };
18
18
  const external_node_crypto_namespaceObject = require("node:crypto");
19
19
  const promises_namespaceObject = require("node:fs/promises");
@@ -48,10 +48,44 @@ var __webpack_modules__ = {
48
48
  const normalized = normalizeAuthMode(authMode);
49
49
  return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
50
50
  }
51
+ function extractAuthClaim(payload) {
52
+ const value = payload["https://api.openai.com/auth"];
53
+ return isRecord(value) ? value : void 0;
54
+ }
55
+ function extractStringClaim(payload, key) {
56
+ const value = payload[key];
57
+ return "string" == typeof value && "" !== value.trim() ? value : void 0;
58
+ }
59
+ function extractSnapshotJwtPayloads(snapshot) {
60
+ const tokens = snapshot.tokens ?? {};
61
+ const payloads = [];
62
+ for (const tokenName of [
63
+ "id_token",
64
+ "access_token"
65
+ ]){
66
+ const token = tokens[tokenName];
67
+ if ("string" == typeof token && "" !== token.trim()) try {
68
+ payloads.push(decodeJwtPayload(token));
69
+ } catch {}
70
+ }
71
+ return payloads;
72
+ }
73
+ function getSnapshotUserId(snapshot) {
74
+ for (const payload of extractSnapshotJwtPayloads(snapshot)){
75
+ const authClaim = extractAuthClaim(payload);
76
+ const chatGPTUserId = authClaim?.chatgpt_user_id;
77
+ if ("string" == typeof chatGPTUserId && "" !== chatGPTUserId.trim()) return chatGPTUserId;
78
+ const userId = extractStringClaim(payload, "user_id");
79
+ if (userId) return userId;
80
+ }
81
+ }
51
82
  function fingerprintApiKey(apiKey) {
52
83
  return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
53
84
  }
54
- function getSnapshotIdentity(snapshot) {
85
+ function composeIdentity(accountId, userId) {
86
+ return "string" == typeof userId && "" !== userId.trim() ? `${accountId}:${userId}` : accountId;
87
+ }
88
+ function getSnapshotAccountId(snapshot) {
55
89
  if (isApiKeyAuthMode(snapshot.auth_mode)) {
56
90
  const apiKey = snapshot.OPENAI_API_KEY;
57
91
  if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
@@ -61,6 +95,12 @@ var __webpack_modules__ = {
61
95
  if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
62
96
  return accountId;
63
97
  }
98
+ function getSnapshotIdentity(snapshot) {
99
+ return composeIdentity(getSnapshotAccountId(snapshot), isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0);
100
+ }
101
+ function getMetaIdentity(meta) {
102
+ return composeIdentity(meta.account_id, meta.user_id);
103
+ }
64
104
  function defaultQuotaSnapshot() {
65
105
  return {
66
106
  status: "stale"
@@ -138,7 +178,8 @@ var __webpack_modules__ = {
138
178
  return {
139
179
  name,
140
180
  auth_mode: snapshot.auth_mode,
141
- account_id: getSnapshotIdentity(snapshot),
181
+ account_id: getSnapshotAccountId(snapshot),
182
+ user_id: isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0,
142
183
  created_at: existingCreatedAt ?? timestamp,
143
184
  updated_at: timestamp,
144
185
  last_switched_at: null,
@@ -159,6 +200,7 @@ var __webpack_modules__ = {
159
200
  name: asNonEmptyString(parsed.name, "name"),
160
201
  auth_mode: asNonEmptyString(parsed.auth_mode, "auth_mode"),
161
202
  account_id: asNonEmptyString(parsed.account_id, "account_id"),
203
+ user_id: asOptionalString(parsed.user_id, "user_id"),
162
204
  created_at: asNonEmptyString(parsed.created_at, "created_at"),
163
205
  updated_at: asNonEmptyString(parsed.updated_at, "updated_at"),
164
206
  last_switched_at: lastSwitchedAt,
@@ -187,11 +229,11 @@ var __webpack_modules__ = {
187
229
  function quota_client_isRecord(value) {
188
230
  return "object" == typeof value && null !== value && !Array.isArray(value);
189
231
  }
190
- function extractAuthClaim(payload) {
232
+ function quota_client_extractAuthClaim(payload) {
191
233
  const value = payload["https://api.openai.com/auth"];
192
234
  return quota_client_isRecord(value) ? value : void 0;
193
235
  }
194
- function extractStringClaim(payload, key) {
236
+ function quota_client_extractStringClaim(payload, key) {
195
237
  const value = payload[key];
196
238
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
197
239
  }
@@ -204,7 +246,7 @@ var __webpack_modules__ = {
204
246
  const token = tokens[tokenName];
205
247
  if ("string" == typeof token && "" !== token.trim()) try {
206
248
  const payload = decodeJwtPayload(token);
207
- const authClaim = extractAuthClaim(payload);
249
+ const authClaim = quota_client_extractAuthClaim(payload);
208
250
  const planType = authClaim?.chatgpt_plan_type;
209
251
  if ("string" == typeof planType && "" !== planType.trim()) return planType;
210
252
  } catch {}
@@ -228,7 +270,7 @@ var __webpack_modules__ = {
228
270
  const token = tokens[tokenName];
229
271
  if ("string" == typeof token && "" !== token.trim()) try {
230
272
  const payload = decodeJwtPayload(token);
231
- const authClaim = extractAuthClaim(payload);
273
+ const authClaim = quota_client_extractAuthClaim(payload);
232
274
  if (!accountId) {
233
275
  const maybeAccountId = authClaim?.chatgpt_account_id;
234
276
  if ("string" == typeof maybeAccountId && "" !== maybeAccountId.trim()) accountId = maybeAccountId;
@@ -237,8 +279,8 @@ var __webpack_modules__ = {
237
279
  const maybePlanType = authClaim?.chatgpt_plan_type;
238
280
  if ("string" == typeof maybePlanType && "" !== maybePlanType.trim()) planType = maybePlanType;
239
281
  }
240
- issuer ??= extractStringClaim(payload, "iss");
241
- clientId ??= extractStringClaim(payload, "client_id") ?? extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
282
+ issuer ??= quota_client_extractStringClaim(payload, "iss");
283
+ clientId ??= quota_client_extractStringClaim(payload, "client_id") ?? quota_client_extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
242
284
  } catch {}
243
285
  }
244
286
  if (!supported) return {
@@ -508,6 +550,13 @@ var __webpack_modules__ = {
508
550
  async function readJsonFile(path) {
509
551
  return (0, promises_namespaceObject.readFile)(path, "utf8");
510
552
  }
553
+ function canAutoMigrateLegacyChatGPTMeta(meta, snapshot) {
554
+ if (!isSupportedChatGPTAuthMode(meta.auth_mode) || !isSupportedChatGPTAuthMode(snapshot.auth_mode)) return false;
555
+ if ("string" == typeof meta.user_id && "" !== meta.user_id.trim()) return false;
556
+ const snapshotUserId = getSnapshotUserId(snapshot);
557
+ if (!snapshotUserId) return false;
558
+ return meta.account_id === getSnapshotAccountId(snapshot);
559
+ }
511
560
  async function detectRunningCodexProcesses() {
512
561
  try {
513
562
  const { stdout } = await execFile("ps", [
@@ -605,6 +654,8 @@ var __webpack_modules__ = {
605
654
  return {
606
655
  name: account.name,
607
656
  account_id: account.account_id,
657
+ user_id: account.user_id ?? null,
658
+ identity: account.identity,
608
659
  plan_type: planType,
609
660
  credits_balance: account.quota.credits_balance ?? null,
610
661
  status: account.quota.status,
@@ -645,11 +696,21 @@ var __webpack_modules__ = {
645
696
  readJsonFile(metaPath),
646
697
  readAuthSnapshotFile(authPath)
647
698
  ]);
648
- const meta = parseSnapshotMeta(rawMeta);
699
+ let meta = parseSnapshotMeta(rawMeta);
649
700
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
650
- if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
701
+ const snapshotIdentity = getSnapshotIdentity(snapshot);
702
+ if (getMetaIdentity(meta) !== snapshotIdentity) if (canAutoMigrateLegacyChatGPTMeta(meta, snapshot)) {
703
+ meta = {
704
+ ...meta,
705
+ account_id: getSnapshotAccountId(snapshot),
706
+ user_id: getSnapshotUserId(snapshot)
707
+ };
708
+ await this.writeAccountMeta(name, meta);
709
+ } else throw new Error(`Account metadata account_id mismatch for "${name}".`);
710
+ if (getMetaIdentity(meta) !== snapshotIdentity) throw new Error(`Account metadata account_id mismatch for "${name}".`);
651
711
  return {
652
712
  ...meta,
713
+ identity: getMetaIdentity(meta),
653
714
  authPath,
654
715
  metaPath,
655
716
  configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
@@ -669,12 +730,12 @@ var __webpack_modules__ = {
669
730
  warnings.push(`Account "${entry.name}" is invalid: ${error.message}`);
670
731
  }
671
732
  const counts = new Map();
672
- for (const account of accounts)counts.set(account.account_id, (counts.get(account.account_id) ?? 0) + 1);
733
+ for (const account of accounts)counts.set(account.identity, (counts.get(account.identity) ?? 0) + 1);
673
734
  accounts.sort((left, right)=>left.name.localeCompare(right.name));
674
735
  return {
675
736
  accounts: accounts.map((account)=>({
676
737
  ...account,
677
- duplicateAccountId: (counts.get(account.account_id) ?? 0) > 1
738
+ duplicateAccountId: (counts.get(account.identity) ?? 0) > 1
678
739
  })),
679
740
  warnings
680
741
  };
@@ -685,6 +746,8 @@ var __webpack_modules__ = {
685
746
  exists: false,
686
747
  auth_mode: null,
687
748
  account_id: null,
749
+ user_id: null,
750
+ identity: null,
688
751
  matched_accounts: [],
689
752
  managed: false,
690
753
  duplicate_match: false,
@@ -692,11 +755,15 @@ var __webpack_modules__ = {
692
755
  };
693
756
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
694
757
  const currentIdentity = getSnapshotIdentity(snapshot);
695
- const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
758
+ const currentAccountId = getSnapshotAccountId(snapshot);
759
+ const currentUserId = getSnapshotUserId(snapshot) ?? null;
760
+ const matchedAccounts = accounts.filter((account)=>account.identity === currentIdentity).map((account)=>account.name);
696
761
  return {
697
762
  exists: true,
698
763
  auth_mode: snapshot.auth_mode,
699
- account_id: currentIdentity,
764
+ account_id: currentAccountId,
765
+ user_id: currentUserId,
766
+ identity: currentIdentity,
700
767
  matched_accounts: matchedAccounts,
701
768
  managed: matchedAccounts.length > 0,
702
769
  duplicate_match: matchedAccounts.length > 1,
@@ -719,7 +786,7 @@ var __webpack_modules__ = {
719
786
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
720
787
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
721
788
  const { accounts } = await this.listAccounts();
722
- const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
789
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.identity === identity);
723
790
  if (duplicateIdentityAccounts.length > 0) {
724
791
  const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
725
792
  throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
@@ -794,7 +861,7 @@ var __webpack_modules__ = {
794
861
  await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
795
862
  }
796
863
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
797
- if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
864
+ if (getSnapshotIdentity(writtenSnapshot) !== account.identity) throw new Error(`Switch verification failed for account "${name}".`);
798
865
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
799
866
  meta.last_switched_at = new Date().toISOString();
800
867
  meta.updated_at = meta.last_switched_at;
@@ -838,7 +905,8 @@ var __webpack_modules__ = {
838
905
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
839
906
  }
840
907
  meta.auth_mode = result.authSnapshot.auth_mode;
841
- meta.account_id = getSnapshotIdentity(result.authSnapshot);
908
+ meta.account_id = getSnapshotAccountId(result.authSnapshot);
909
+ meta.user_id = getSnapshotUserId(result.authSnapshot);
842
910
  meta.updated_at = now.toISOString();
843
911
  meta.quota = result.quota;
844
912
  await this.writeAccountMeta(name, meta);
@@ -951,7 +1019,7 @@ var __webpack_modules__ = {
951
1019
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
952
1020
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
953
1021
  if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
954
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
1022
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.identity} with another saved account.`);
955
1023
  }
956
1024
  let currentAuthPresent = false;
957
1025
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -1032,7 +1100,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1032
1100
  if (status.exists) {
1033
1101
  lines.push("Current auth: present");
1034
1102
  lines.push(`Auth mode: ${status.auth_mode}`);
1035
- lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
1103
+ lines.push(`Identity: ${maskAccountId(status.identity ?? "")}`);
1036
1104
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
1037
1105
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
1038
1106
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1085,6 +1153,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1085
1153
  return {
1086
1154
  name: account.name,
1087
1155
  account_id: account.account_id,
1156
+ identity: account.identity,
1088
1157
  plan_type: account.plan_type,
1089
1158
  available: computeAvailability(account),
1090
1159
  refresh_status: "ok",
@@ -1117,7 +1186,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1117
1186
  }
1118
1187
  function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
1119
1188
  const lines = [
1120
- dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
1189
+ dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.identity)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.identity)}).`,
1121
1190
  `Score: ${candidate.effective_score}`,
1122
1191
  `5H remaining: ${candidate.remain_5h}%`,
1123
1192
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1128,7 +1197,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1128
1197
  }
1129
1198
  function describeAutoSwitchNoop(candidate, warnings) {
1130
1199
  const lines = [
1131
- `Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
1200
+ `Current account "${candidate.name}" (${maskAccountId(candidate.identity)}) is already the best available account.`,
1132
1201
  `Score: ${candidate.effective_score}`,
1133
1202
  `5H remaining: ${candidate.remain_5h}%`,
1134
1203
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1140,7 +1209,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1140
1209
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
1141
1210
  const table = formatTable(accounts.map((account)=>({
1142
1211
  name: account.name,
1143
- account_id: maskAccountId(account.account_id),
1212
+ account_id: maskAccountId(account.identity),
1144
1213
  plan_type: account.plan_type ?? "-",
1145
1214
  available: computeAvailability(account) ?? "-",
1146
1215
  five_hour: formatUsagePercent(account.five_hour),
@@ -1265,11 +1334,13 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1265
1334
  account: {
1266
1335
  name: account.name,
1267
1336
  account_id: account.account_id,
1337
+ user_id: account.user_id ?? null,
1338
+ identity: account.identity,
1268
1339
  auth_mode: account.auth_mode
1269
1340
  }
1270
1341
  };
1271
1342
  if (json) writeJson(streams.stdout, payload);
1272
- else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.account_id)}).\n`);
1343
+ else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.identity)}).\n`);
1273
1344
  return 0;
1274
1345
  }
1275
1346
  case "update":
@@ -1291,6 +1362,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1291
1362
  account: {
1292
1363
  name: result.account.name,
1293
1364
  account_id: result.account.account_id,
1365
+ user_id: result.account.user_id ?? null,
1366
+ identity: result.account.identity,
1294
1367
  auth_mode: result.account.auth_mode
1295
1368
  },
1296
1369
  quota,
@@ -1298,7 +1371,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1298
1371
  };
1299
1372
  if (json) writeJson(streams.stdout, payload);
1300
1373
  else {
1301
- streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1374
+ streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1302
1375
  for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
1303
1376
  }
1304
1377
  return 0;
@@ -1341,7 +1414,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1341
1414
  reason: "already_current_best",
1342
1415
  account: {
1343
1416
  name: selected.name,
1344
- account_id: selected.account_id
1417
+ account_id: selected.account_id,
1418
+ identity: selected.identity
1345
1419
  },
1346
1420
  selected,
1347
1421
  candidates,
@@ -1361,6 +1435,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1361
1435
  account: {
1362
1436
  name: result.account.name,
1363
1437
  account_id: result.account.account_id,
1438
+ user_id: result.account.user_id ?? null,
1439
+ identity: result.account.identity,
1364
1440
  auth_mode: result.account.auth_mode
1365
1441
  },
1366
1442
  selected,
@@ -1390,6 +1466,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1390
1466
  account: {
1391
1467
  name: result.account.name,
1392
1468
  account_id: result.account.account_id,
1469
+ user_id: result.account.user_id ?? null,
1470
+ identity: result.account.identity,
1393
1471
  auth_mode: result.account.auth_mode
1394
1472
  },
1395
1473
  quota,
@@ -1398,7 +1476,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1398
1476
  };
1399
1477
  if (json) writeJson(streams.stdout, payload);
1400
1478
  else {
1401
- streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1479
+ streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1402
1480
  if (result.backup_path) streams.stdout.write(`Backup: ${result.backup_path}\n`);
1403
1481
  for (const warning of result.warnings)streams.stdout.write(`Warning: ${warning}\n`);
1404
1482
  }
@@ -1440,6 +1518,8 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1440
1518
  account: {
1441
1519
  name: account.name,
1442
1520
  account_id: account.account_id,
1521
+ user_id: account.user_id ?? null,
1522
+ identity: account.identity,
1443
1523
  auth_mode: account.auth_mode
1444
1524
  }
1445
1525
  });
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.8"
46
+ rE: "0.0.9"
47
47
  };
48
48
  const external_node_crypto_namespaceObject = require("node:crypto");
49
49
  const promises_namespaceObject = require("node:fs/promises");
@@ -78,10 +78,44 @@ function isSupportedChatGPTAuthMode(authMode) {
78
78
  const normalized = normalizeAuthMode(authMode);
79
79
  return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
80
80
  }
81
+ function extractAuthClaim(payload) {
82
+ const value = payload["https://api.openai.com/auth"];
83
+ return isRecord(value) ? value : void 0;
84
+ }
85
+ function extractStringClaim(payload, key) {
86
+ const value = payload[key];
87
+ return "string" == typeof value && "" !== value.trim() ? value : void 0;
88
+ }
89
+ function extractSnapshotJwtPayloads(snapshot) {
90
+ const tokens = snapshot.tokens ?? {};
91
+ const payloads = [];
92
+ for (const tokenName of [
93
+ "id_token",
94
+ "access_token"
95
+ ]){
96
+ const token = tokens[tokenName];
97
+ if ("string" == typeof token && "" !== token.trim()) try {
98
+ payloads.push(decodeJwtPayload(token));
99
+ } catch {}
100
+ }
101
+ return payloads;
102
+ }
103
+ function getSnapshotUserId(snapshot) {
104
+ for (const payload of extractSnapshotJwtPayloads(snapshot)){
105
+ const authClaim = extractAuthClaim(payload);
106
+ const chatGPTUserId = authClaim?.chatgpt_user_id;
107
+ if ("string" == typeof chatGPTUserId && "" !== chatGPTUserId.trim()) return chatGPTUserId;
108
+ const userId = extractStringClaim(payload, "user_id");
109
+ if (userId) return userId;
110
+ }
111
+ }
81
112
  function fingerprintApiKey(apiKey) {
82
113
  return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
83
114
  }
84
- function getSnapshotIdentity(snapshot) {
115
+ function composeIdentity(accountId, userId) {
116
+ return "string" == typeof userId && "" !== userId.trim() ? `${accountId}:${userId}` : accountId;
117
+ }
118
+ function getSnapshotAccountId(snapshot) {
85
119
  if (isApiKeyAuthMode(snapshot.auth_mode)) {
86
120
  const apiKey = snapshot.OPENAI_API_KEY;
87
121
  if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
@@ -91,6 +125,12 @@ function getSnapshotIdentity(snapshot) {
91
125
  if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
92
126
  return accountId;
93
127
  }
128
+ function getSnapshotIdentity(snapshot) {
129
+ return composeIdentity(getSnapshotAccountId(snapshot), isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0);
130
+ }
131
+ function getMetaIdentity(meta) {
132
+ return composeIdentity(meta.account_id, meta.user_id);
133
+ }
94
134
  function defaultQuotaSnapshot() {
95
135
  return {
96
136
  status: "stale"
@@ -168,7 +208,8 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
168
208
  return {
169
209
  name,
170
210
  auth_mode: snapshot.auth_mode,
171
- account_id: getSnapshotIdentity(snapshot),
211
+ account_id: getSnapshotAccountId(snapshot),
212
+ user_id: isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0,
172
213
  created_at: existingCreatedAt ?? timestamp,
173
214
  updated_at: timestamp,
174
215
  last_switched_at: null,
@@ -189,6 +230,7 @@ function parseSnapshotMeta(raw) {
189
230
  name: asNonEmptyString(parsed.name, "name"),
190
231
  auth_mode: asNonEmptyString(parsed.auth_mode, "auth_mode"),
191
232
  account_id: asNonEmptyString(parsed.account_id, "account_id"),
233
+ user_id: asOptionalString(parsed.user_id, "user_id"),
192
234
  created_at: asNonEmptyString(parsed.created_at, "created_at"),
193
235
  updated_at: asNonEmptyString(parsed.updated_at, "updated_at"),
194
236
  last_switched_at: lastSwitchedAt,
@@ -217,11 +259,11 @@ const USER_AGENT = "codexm/0.1";
217
259
  function quota_client_isRecord(value) {
218
260
  return "object" == typeof value && null !== value && !Array.isArray(value);
219
261
  }
220
- function extractAuthClaim(payload) {
262
+ function quota_client_extractAuthClaim(payload) {
221
263
  const value = payload["https://api.openai.com/auth"];
222
264
  return quota_client_isRecord(value) ? value : void 0;
223
265
  }
224
- function extractStringClaim(payload, key) {
266
+ function quota_client_extractStringClaim(payload, key) {
225
267
  const value = payload[key];
226
268
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
227
269
  }
@@ -234,7 +276,7 @@ function parsePlanType(snapshot) {
234
276
  const token = tokens[tokenName];
235
277
  if ("string" == typeof token && "" !== token.trim()) try {
236
278
  const payload = decodeJwtPayload(token);
237
- const authClaim = extractAuthClaim(payload);
279
+ const authClaim = quota_client_extractAuthClaim(payload);
238
280
  const planType = authClaim?.chatgpt_plan_type;
239
281
  if ("string" == typeof planType && "" !== planType.trim()) return planType;
240
282
  } catch {}
@@ -258,7 +300,7 @@ function extractChatGPTAuth(snapshot) {
258
300
  const token = tokens[tokenName];
259
301
  if ("string" == typeof token && "" !== token.trim()) try {
260
302
  const payload = decodeJwtPayload(token);
261
- const authClaim = extractAuthClaim(payload);
303
+ const authClaim = quota_client_extractAuthClaim(payload);
262
304
  if (!accountId) {
263
305
  const maybeAccountId = authClaim?.chatgpt_account_id;
264
306
  if ("string" == typeof maybeAccountId && "" !== maybeAccountId.trim()) accountId = maybeAccountId;
@@ -267,8 +309,8 @@ function extractChatGPTAuth(snapshot) {
267
309
  const maybePlanType = authClaim?.chatgpt_plan_type;
268
310
  if ("string" == typeof maybePlanType && "" !== maybePlanType.trim()) planType = maybePlanType;
269
311
  }
270
- issuer ??= extractStringClaim(payload, "iss");
271
- clientId ??= extractStringClaim(payload, "client_id") ?? extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
312
+ issuer ??= quota_client_extractStringClaim(payload, "iss");
313
+ clientId ??= quota_client_extractStringClaim(payload, "client_id") ?? quota_client_extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
272
314
  } catch {}
273
315
  }
274
316
  if (!supported) return {
@@ -538,6 +580,13 @@ async function pathExists(path) {
538
580
  async function readJsonFile(path) {
539
581
  return (0, promises_namespaceObject.readFile)(path, "utf8");
540
582
  }
583
+ function canAutoMigrateLegacyChatGPTMeta(meta, snapshot) {
584
+ if (!isSupportedChatGPTAuthMode(meta.auth_mode) || !isSupportedChatGPTAuthMode(snapshot.auth_mode)) return false;
585
+ if ("string" == typeof meta.user_id && "" !== meta.user_id.trim()) return false;
586
+ const snapshotUserId = getSnapshotUserId(snapshot);
587
+ if (!snapshotUserId) return false;
588
+ return meta.account_id === getSnapshotAccountId(snapshot);
589
+ }
541
590
  async function detectRunningCodexProcesses() {
542
591
  try {
543
592
  const { stdout } = await execFile("ps", [
@@ -635,6 +684,8 @@ class AccountStore {
635
684
  return {
636
685
  name: account.name,
637
686
  account_id: account.account_id,
687
+ user_id: account.user_id ?? null,
688
+ identity: account.identity,
638
689
  plan_type: planType,
639
690
  credits_balance: account.quota.credits_balance ?? null,
640
691
  status: account.quota.status,
@@ -675,11 +726,21 @@ class AccountStore {
675
726
  readJsonFile(metaPath),
676
727
  readAuthSnapshotFile(authPath)
677
728
  ]);
678
- const meta = parseSnapshotMeta(rawMeta);
729
+ let meta = parseSnapshotMeta(rawMeta);
679
730
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
680
- if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
731
+ const snapshotIdentity = getSnapshotIdentity(snapshot);
732
+ if (getMetaIdentity(meta) !== snapshotIdentity) if (canAutoMigrateLegacyChatGPTMeta(meta, snapshot)) {
733
+ meta = {
734
+ ...meta,
735
+ account_id: getSnapshotAccountId(snapshot),
736
+ user_id: getSnapshotUserId(snapshot)
737
+ };
738
+ await this.writeAccountMeta(name, meta);
739
+ } else throw new Error(`Account metadata account_id mismatch for "${name}".`);
740
+ if (getMetaIdentity(meta) !== snapshotIdentity) throw new Error(`Account metadata account_id mismatch for "${name}".`);
681
741
  return {
682
742
  ...meta,
743
+ identity: getMetaIdentity(meta),
683
744
  authPath,
684
745
  metaPath,
685
746
  configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
@@ -699,12 +760,12 @@ class AccountStore {
699
760
  warnings.push(`Account "${entry.name}" is invalid: ${error.message}`);
700
761
  }
701
762
  const counts = new Map();
702
- for (const account of accounts)counts.set(account.account_id, (counts.get(account.account_id) ?? 0) + 1);
763
+ for (const account of accounts)counts.set(account.identity, (counts.get(account.identity) ?? 0) + 1);
703
764
  accounts.sort((left, right)=>left.name.localeCompare(right.name));
704
765
  return {
705
766
  accounts: accounts.map((account)=>({
706
767
  ...account,
707
- duplicateAccountId: (counts.get(account.account_id) ?? 0) > 1
768
+ duplicateAccountId: (counts.get(account.identity) ?? 0) > 1
708
769
  })),
709
770
  warnings
710
771
  };
@@ -715,6 +776,8 @@ class AccountStore {
715
776
  exists: false,
716
777
  auth_mode: null,
717
778
  account_id: null,
779
+ user_id: null,
780
+ identity: null,
718
781
  matched_accounts: [],
719
782
  managed: false,
720
783
  duplicate_match: false,
@@ -722,11 +785,15 @@ class AccountStore {
722
785
  };
723
786
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
724
787
  const currentIdentity = getSnapshotIdentity(snapshot);
725
- const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
788
+ const currentAccountId = getSnapshotAccountId(snapshot);
789
+ const currentUserId = getSnapshotUserId(snapshot) ?? null;
790
+ const matchedAccounts = accounts.filter((account)=>account.identity === currentIdentity).map((account)=>account.name);
726
791
  return {
727
792
  exists: true,
728
793
  auth_mode: snapshot.auth_mode,
729
- account_id: currentIdentity,
794
+ account_id: currentAccountId,
795
+ user_id: currentUserId,
796
+ identity: currentIdentity,
730
797
  matched_accounts: matchedAccounts,
731
798
  managed: matchedAccounts.length > 0,
732
799
  duplicate_match: matchedAccounts.length > 1,
@@ -749,7 +816,7 @@ class AccountStore {
749
816
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
750
817
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
751
818
  const { accounts } = await this.listAccounts();
752
- const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
819
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.identity === identity);
753
820
  if (duplicateIdentityAccounts.length > 0) {
754
821
  const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
755
822
  throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
@@ -824,7 +891,7 @@ class AccountStore {
824
891
  await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
825
892
  }
826
893
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
827
- if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
894
+ if (getSnapshotIdentity(writtenSnapshot) !== account.identity) throw new Error(`Switch verification failed for account "${name}".`);
828
895
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
829
896
  meta.last_switched_at = new Date().toISOString();
830
897
  meta.updated_at = meta.last_switched_at;
@@ -868,7 +935,8 @@ class AccountStore {
868
935
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
869
936
  }
870
937
  meta.auth_mode = result.authSnapshot.auth_mode;
871
- meta.account_id = getSnapshotIdentity(result.authSnapshot);
938
+ meta.account_id = getSnapshotAccountId(result.authSnapshot);
939
+ meta.user_id = getSnapshotUserId(result.authSnapshot);
872
940
  meta.updated_at = now.toISOString();
873
941
  meta.quota = result.quota;
874
942
  await this.writeAccountMeta(name, meta);
@@ -981,7 +1049,7 @@ class AccountStore {
981
1049
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
982
1050
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
983
1051
  if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
984
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
1052
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.identity} with another saved account.`);
985
1053
  }
986
1054
  let currentAuthPresent = false;
987
1055
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -1062,7 +1130,7 @@ function describeCurrentStatus(status) {
1062
1130
  if (status.exists) {
1063
1131
  lines.push("Current auth: present");
1064
1132
  lines.push(`Auth mode: ${status.auth_mode}`);
1065
- lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
1133
+ lines.push(`Identity: ${maskAccountId(status.identity ?? "")}`);
1066
1134
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
1067
1135
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
1068
1136
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1115,6 +1183,7 @@ function toAutoSwitchCandidate(account) {
1115
1183
  return {
1116
1184
  name: account.name,
1117
1185
  account_id: account.account_id,
1186
+ identity: account.identity,
1118
1187
  plan_type: account.plan_type,
1119
1188
  available: computeAvailability(account),
1120
1189
  refresh_status: "ok",
@@ -1147,7 +1216,7 @@ function rankAutoSwitchCandidates(accounts) {
1147
1216
  }
1148
1217
  function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
1149
1218
  const lines = [
1150
- dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
1219
+ dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.identity)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.identity)}).`,
1151
1220
  `Score: ${candidate.effective_score}`,
1152
1221
  `5H remaining: ${candidate.remain_5h}%`,
1153
1222
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1158,7 +1227,7 @@ function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
1158
1227
  }
1159
1228
  function describeAutoSwitchNoop(candidate, warnings) {
1160
1229
  const lines = [
1161
- `Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
1230
+ `Current account "${candidate.name}" (${maskAccountId(candidate.identity)}) is already the best available account.`,
1162
1231
  `Score: ${candidate.effective_score}`,
1163
1232
  `5H remaining: ${candidate.remain_5h}%`,
1164
1233
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1170,7 +1239,7 @@ function describeQuotaAccounts(accounts, warnings) {
1170
1239
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
1171
1240
  const table = formatTable(accounts.map((account)=>({
1172
1241
  name: account.name,
1173
- account_id: maskAccountId(account.account_id),
1242
+ account_id: maskAccountId(account.identity),
1174
1243
  plan_type: account.plan_type ?? "-",
1175
1244
  available: computeAvailability(account) ?? "-",
1176
1245
  five_hour: formatUsagePercent(account.five_hour),
@@ -1295,11 +1364,13 @@ async function runCli(argv, options = {}) {
1295
1364
  account: {
1296
1365
  name: account.name,
1297
1366
  account_id: account.account_id,
1367
+ user_id: account.user_id ?? null,
1368
+ identity: account.identity,
1298
1369
  auth_mode: account.auth_mode
1299
1370
  }
1300
1371
  };
1301
1372
  if (json) writeJson(streams.stdout, payload);
1302
- else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.account_id)}).\n`);
1373
+ else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.identity)}).\n`);
1303
1374
  return 0;
1304
1375
  }
1305
1376
  case "update":
@@ -1321,6 +1392,8 @@ async function runCli(argv, options = {}) {
1321
1392
  account: {
1322
1393
  name: result.account.name,
1323
1394
  account_id: result.account.account_id,
1395
+ user_id: result.account.user_id ?? null,
1396
+ identity: result.account.identity,
1324
1397
  auth_mode: result.account.auth_mode
1325
1398
  },
1326
1399
  quota,
@@ -1328,7 +1401,7 @@ async function runCli(argv, options = {}) {
1328
1401
  };
1329
1402
  if (json) writeJson(streams.stdout, payload);
1330
1403
  else {
1331
- streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1404
+ streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1332
1405
  for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
1333
1406
  }
1334
1407
  return 0;
@@ -1371,7 +1444,8 @@ async function runCli(argv, options = {}) {
1371
1444
  reason: "already_current_best",
1372
1445
  account: {
1373
1446
  name: selected.name,
1374
- account_id: selected.account_id
1447
+ account_id: selected.account_id,
1448
+ identity: selected.identity
1375
1449
  },
1376
1450
  selected,
1377
1451
  candidates,
@@ -1391,6 +1465,8 @@ async function runCli(argv, options = {}) {
1391
1465
  account: {
1392
1466
  name: result.account.name,
1393
1467
  account_id: result.account.account_id,
1468
+ user_id: result.account.user_id ?? null,
1469
+ identity: result.account.identity,
1394
1470
  auth_mode: result.account.auth_mode
1395
1471
  },
1396
1472
  selected,
@@ -1420,6 +1496,8 @@ async function runCli(argv, options = {}) {
1420
1496
  account: {
1421
1497
  name: result.account.name,
1422
1498
  account_id: result.account.account_id,
1499
+ user_id: result.account.user_id ?? null,
1500
+ identity: result.account.identity,
1423
1501
  auth_mode: result.account.auth_mode
1424
1502
  },
1425
1503
  quota,
@@ -1428,7 +1506,7 @@ async function runCli(argv, options = {}) {
1428
1506
  };
1429
1507
  if (json) writeJson(streams.stdout, payload);
1430
1508
  else {
1431
- streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1509
+ streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1432
1510
  if (result.backup_path) streams.stdout.write(`Backup: ${result.backup_path}\n`);
1433
1511
  for (const warning of result.warnings)streams.stdout.write(`Warning: ${warning}\n`);
1434
1512
  }
@@ -1470,6 +1548,8 @@ async function runCli(argv, options = {}) {
1470
1548
  account: {
1471
1549
  name: account.name,
1472
1550
  account_id: account.account_id,
1551
+ user_id: account.user_id ?? null,
1552
+ identity: account.identity,
1473
1553
  auth_mode: account.auth_mode
1474
1554
  }
1475
1555
  });
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.8"
12
+ rE: "0.0.9"
13
13
  };
14
14
  function isRecord(value) {
15
15
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -42,10 +42,44 @@ function isSupportedChatGPTAuthMode(authMode) {
42
42
  const normalized = normalizeAuthMode(authMode);
43
43
  return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
44
44
  }
45
+ function extractAuthClaim(payload) {
46
+ const value = payload["https://api.openai.com/auth"];
47
+ return isRecord(value) ? value : void 0;
48
+ }
49
+ function extractStringClaim(payload, key) {
50
+ const value = payload[key];
51
+ return "string" == typeof value && "" !== value.trim() ? value : void 0;
52
+ }
53
+ function extractSnapshotJwtPayloads(snapshot) {
54
+ const tokens = snapshot.tokens ?? {};
55
+ const payloads = [];
56
+ for (const tokenName of [
57
+ "id_token",
58
+ "access_token"
59
+ ]){
60
+ const token = tokens[tokenName];
61
+ if ("string" == typeof token && "" !== token.trim()) try {
62
+ payloads.push(decodeJwtPayload(token));
63
+ } catch {}
64
+ }
65
+ return payloads;
66
+ }
67
+ function getSnapshotUserId(snapshot) {
68
+ for (const payload of extractSnapshotJwtPayloads(snapshot)){
69
+ const authClaim = extractAuthClaim(payload);
70
+ const chatGPTUserId = authClaim?.chatgpt_user_id;
71
+ if ("string" == typeof chatGPTUserId && "" !== chatGPTUserId.trim()) return chatGPTUserId;
72
+ const userId = extractStringClaim(payload, "user_id");
73
+ if (userId) return userId;
74
+ }
75
+ }
45
76
  function fingerprintApiKey(apiKey) {
46
77
  return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
47
78
  }
48
- function getSnapshotIdentity(snapshot) {
79
+ function composeIdentity(accountId, userId) {
80
+ return "string" == typeof userId && "" !== userId.trim() ? `${accountId}:${userId}` : accountId;
81
+ }
82
+ function getSnapshotAccountId(snapshot) {
49
83
  if (isApiKeyAuthMode(snapshot.auth_mode)) {
50
84
  const apiKey = snapshot.OPENAI_API_KEY;
51
85
  if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
@@ -55,6 +89,12 @@ function getSnapshotIdentity(snapshot) {
55
89
  if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
56
90
  return accountId;
57
91
  }
92
+ function getSnapshotIdentity(snapshot) {
93
+ return composeIdentity(getSnapshotAccountId(snapshot), isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0);
94
+ }
95
+ function getMetaIdentity(meta) {
96
+ return composeIdentity(meta.account_id, meta.user_id);
97
+ }
58
98
  function defaultQuotaSnapshot() {
59
99
  return {
60
100
  status: "stale"
@@ -132,7 +172,8 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
132
172
  return {
133
173
  name,
134
174
  auth_mode: snapshot.auth_mode,
135
- account_id: getSnapshotIdentity(snapshot),
175
+ account_id: getSnapshotAccountId(snapshot),
176
+ user_id: isSupportedChatGPTAuthMode(snapshot.auth_mode) ? getSnapshotUserId(snapshot) : void 0,
136
177
  created_at: existingCreatedAt ?? timestamp,
137
178
  updated_at: timestamp,
138
179
  last_switched_at: null,
@@ -153,6 +194,7 @@ function parseSnapshotMeta(raw) {
153
194
  name: asNonEmptyString(parsed.name, "name"),
154
195
  auth_mode: asNonEmptyString(parsed.auth_mode, "auth_mode"),
155
196
  account_id: asNonEmptyString(parsed.account_id, "account_id"),
197
+ user_id: asOptionalString(parsed.user_id, "user_id"),
156
198
  created_at: asNonEmptyString(parsed.created_at, "created_at"),
157
199
  updated_at: asNonEmptyString(parsed.updated_at, "updated_at"),
158
200
  last_switched_at: lastSwitchedAt,
@@ -177,11 +219,11 @@ const USER_AGENT = "codexm/0.1";
177
219
  function quota_client_isRecord(value) {
178
220
  return "object" == typeof value && null !== value && !Array.isArray(value);
179
221
  }
180
- function extractAuthClaim(payload) {
222
+ function quota_client_extractAuthClaim(payload) {
181
223
  const value = payload["https://api.openai.com/auth"];
182
224
  return quota_client_isRecord(value) ? value : void 0;
183
225
  }
184
- function extractStringClaim(payload, key) {
226
+ function quota_client_extractStringClaim(payload, key) {
185
227
  const value = payload[key];
186
228
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
187
229
  }
@@ -194,7 +236,7 @@ function parsePlanType(snapshot) {
194
236
  const token = tokens[tokenName];
195
237
  if ("string" == typeof token && "" !== token.trim()) try {
196
238
  const payload = decodeJwtPayload(token);
197
- const authClaim = extractAuthClaim(payload);
239
+ const authClaim = quota_client_extractAuthClaim(payload);
198
240
  const planType = authClaim?.chatgpt_plan_type;
199
241
  if ("string" == typeof planType && "" !== planType.trim()) return planType;
200
242
  } catch {}
@@ -218,7 +260,7 @@ function extractChatGPTAuth(snapshot) {
218
260
  const token = tokens[tokenName];
219
261
  if ("string" == typeof token && "" !== token.trim()) try {
220
262
  const payload = decodeJwtPayload(token);
221
- const authClaim = extractAuthClaim(payload);
263
+ const authClaim = quota_client_extractAuthClaim(payload);
222
264
  if (!accountId) {
223
265
  const maybeAccountId = authClaim?.chatgpt_account_id;
224
266
  if ("string" == typeof maybeAccountId && "" !== maybeAccountId.trim()) accountId = maybeAccountId;
@@ -227,8 +269,8 @@ function extractChatGPTAuth(snapshot) {
227
269
  const maybePlanType = authClaim?.chatgpt_plan_type;
228
270
  if ("string" == typeof maybePlanType && "" !== maybePlanType.trim()) planType = maybePlanType;
229
271
  }
230
- issuer ??= extractStringClaim(payload, "iss");
231
- clientId ??= extractStringClaim(payload, "client_id") ?? extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
272
+ issuer ??= quota_client_extractStringClaim(payload, "iss");
273
+ clientId ??= quota_client_extractStringClaim(payload, "client_id") ?? quota_client_extractStringClaim(payload, "azp") ?? ("string" == typeof payload.aud ? payload.aud : void 0);
232
274
  } catch {}
233
275
  }
234
276
  if (!supported) return {
@@ -498,6 +540,13 @@ async function pathExists(path) {
498
540
  async function readJsonFile(path) {
499
541
  return readFile(path, "utf8");
500
542
  }
543
+ function canAutoMigrateLegacyChatGPTMeta(meta, snapshot) {
544
+ if (!isSupportedChatGPTAuthMode(meta.auth_mode) || !isSupportedChatGPTAuthMode(snapshot.auth_mode)) return false;
545
+ if ("string" == typeof meta.user_id && "" !== meta.user_id.trim()) return false;
546
+ const snapshotUserId = getSnapshotUserId(snapshot);
547
+ if (!snapshotUserId) return false;
548
+ return meta.account_id === getSnapshotAccountId(snapshot);
549
+ }
501
550
  async function detectRunningCodexProcesses() {
502
551
  try {
503
552
  const { stdout } = await account_store_execFile("ps", [
@@ -595,6 +644,8 @@ class AccountStore {
595
644
  return {
596
645
  name: account.name,
597
646
  account_id: account.account_id,
647
+ user_id: account.user_id ?? null,
648
+ identity: account.identity,
598
649
  plan_type: planType,
599
650
  credits_balance: account.quota.credits_balance ?? null,
600
651
  status: account.quota.status,
@@ -635,11 +686,21 @@ class AccountStore {
635
686
  readJsonFile(metaPath),
636
687
  readAuthSnapshotFile(authPath)
637
688
  ]);
638
- const meta = parseSnapshotMeta(rawMeta);
689
+ let meta = parseSnapshotMeta(rawMeta);
639
690
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
640
- if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
691
+ const snapshotIdentity = getSnapshotIdentity(snapshot);
692
+ if (getMetaIdentity(meta) !== snapshotIdentity) if (canAutoMigrateLegacyChatGPTMeta(meta, snapshot)) {
693
+ meta = {
694
+ ...meta,
695
+ account_id: getSnapshotAccountId(snapshot),
696
+ user_id: getSnapshotUserId(snapshot)
697
+ };
698
+ await this.writeAccountMeta(name, meta);
699
+ } else throw new Error(`Account metadata account_id mismatch for "${name}".`);
700
+ if (getMetaIdentity(meta) !== snapshotIdentity) throw new Error(`Account metadata account_id mismatch for "${name}".`);
641
701
  return {
642
702
  ...meta,
703
+ identity: getMetaIdentity(meta),
643
704
  authPath,
644
705
  metaPath,
645
706
  configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
@@ -659,12 +720,12 @@ class AccountStore {
659
720
  warnings.push(`Account "${entry.name}" is invalid: ${error.message}`);
660
721
  }
661
722
  const counts = new Map();
662
- for (const account of accounts)counts.set(account.account_id, (counts.get(account.account_id) ?? 0) + 1);
723
+ for (const account of accounts)counts.set(account.identity, (counts.get(account.identity) ?? 0) + 1);
663
724
  accounts.sort((left, right)=>left.name.localeCompare(right.name));
664
725
  return {
665
726
  accounts: accounts.map((account)=>({
666
727
  ...account,
667
- duplicateAccountId: (counts.get(account.account_id) ?? 0) > 1
728
+ duplicateAccountId: (counts.get(account.identity) ?? 0) > 1
668
729
  })),
669
730
  warnings
670
731
  };
@@ -675,6 +736,8 @@ class AccountStore {
675
736
  exists: false,
676
737
  auth_mode: null,
677
738
  account_id: null,
739
+ user_id: null,
740
+ identity: null,
678
741
  matched_accounts: [],
679
742
  managed: false,
680
743
  duplicate_match: false,
@@ -682,11 +745,15 @@ class AccountStore {
682
745
  };
683
746
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
684
747
  const currentIdentity = getSnapshotIdentity(snapshot);
685
- const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
748
+ const currentAccountId = getSnapshotAccountId(snapshot);
749
+ const currentUserId = getSnapshotUserId(snapshot) ?? null;
750
+ const matchedAccounts = accounts.filter((account)=>account.identity === currentIdentity).map((account)=>account.name);
686
751
  return {
687
752
  exists: true,
688
753
  auth_mode: snapshot.auth_mode,
689
- account_id: currentIdentity,
754
+ account_id: currentAccountId,
755
+ user_id: currentUserId,
756
+ identity: currentIdentity,
690
757
  matched_accounts: matchedAccounts,
691
758
  managed: matchedAccounts.length > 0,
692
759
  duplicate_match: matchedAccounts.length > 1,
@@ -709,7 +776,7 @@ class AccountStore {
709
776
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
710
777
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
711
778
  const { accounts } = await this.listAccounts();
712
- const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.account_id === identity);
779
+ const duplicateIdentityAccounts = accounts.filter((account)=>account.name !== name && account.identity === identity);
713
780
  if (duplicateIdentityAccounts.length > 0) {
714
781
  const joinedNames = duplicateIdentityAccounts.map((account)=>`"${account.name}"`).join(", ");
715
782
  throw new Error(`Identity ${identity} is already managed by ${joinedNames}.`);
@@ -784,7 +851,7 @@ class AccountStore {
784
851
  await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
785
852
  }
786
853
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
787
- if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
854
+ if (getSnapshotIdentity(writtenSnapshot) !== account.identity) throw new Error(`Switch verification failed for account "${name}".`);
788
855
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
789
856
  meta.last_switched_at = new Date().toISOString();
790
857
  meta.updated_at = meta.last_switched_at;
@@ -828,7 +895,8 @@ class AccountStore {
828
895
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
829
896
  }
830
897
  meta.auth_mode = result.authSnapshot.auth_mode;
831
- meta.account_id = getSnapshotIdentity(result.authSnapshot);
898
+ meta.account_id = getSnapshotAccountId(result.authSnapshot);
899
+ meta.user_id = getSnapshotUserId(result.authSnapshot);
832
900
  meta.updated_at = now.toISOString();
833
901
  meta.quota = result.quota;
834
902
  await this.writeAccountMeta(name, meta);
@@ -941,7 +1009,7 @@ class AccountStore {
941
1009
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
942
1010
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
943
1011
  if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
944
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
1012
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.identity} with another saved account.`);
945
1013
  }
946
1014
  let currentAuthPresent = false;
947
1015
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -1022,7 +1090,7 @@ function describeCurrentStatus(status) {
1022
1090
  if (status.exists) {
1023
1091
  lines.push("Current auth: present");
1024
1092
  lines.push(`Auth mode: ${status.auth_mode}`);
1025
- lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
1093
+ lines.push(`Identity: ${maskAccountId(status.identity ?? "")}`);
1026
1094
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
1027
1095
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
1028
1096
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1075,6 +1143,7 @@ function toAutoSwitchCandidate(account) {
1075
1143
  return {
1076
1144
  name: account.name,
1077
1145
  account_id: account.account_id,
1146
+ identity: account.identity,
1078
1147
  plan_type: account.plan_type,
1079
1148
  available: computeAvailability(account),
1080
1149
  refresh_status: "ok",
@@ -1107,7 +1176,7 @@ function rankAutoSwitchCandidates(accounts) {
1107
1176
  }
1108
1177
  function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
1109
1178
  const lines = [
1110
- dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.account_id)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.account_id)}).`,
1179
+ dryRun ? `Best account: "${candidate.name}" (${maskAccountId(candidate.identity)}).` : `Auto-switched to "${candidate.name}" (${maskAccountId(candidate.identity)}).`,
1111
1180
  `Score: ${candidate.effective_score}`,
1112
1181
  `5H remaining: ${candidate.remain_5h}%`,
1113
1182
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1118,7 +1187,7 @@ function describeAutoSwitchSelection(candidate, dryRun, backupPath, warnings) {
1118
1187
  }
1119
1188
  function describeAutoSwitchNoop(candidate, warnings) {
1120
1189
  const lines = [
1121
- `Current account "${candidate.name}" (${maskAccountId(candidate.account_id)}) is already the best available account.`,
1190
+ `Current account "${candidate.name}" (${maskAccountId(candidate.identity)}) is already the best available account.`,
1122
1191
  `Score: ${candidate.effective_score}`,
1123
1192
  `5H remaining: ${candidate.remain_5h}%`,
1124
1193
  `1W remaining (5H-equivalent): ${candidate.remain_1w_eq_5h}%`
@@ -1130,7 +1199,7 @@ function describeQuotaAccounts(accounts, warnings) {
1130
1199
  if (0 === accounts.length) return 0 === warnings.length ? "No saved accounts." : warnings.map((warning)=>`Warning: ${warning}`).join("\n");
1131
1200
  const table = formatTable(accounts.map((account)=>({
1132
1201
  name: account.name,
1133
- account_id: maskAccountId(account.account_id),
1202
+ account_id: maskAccountId(account.identity),
1134
1203
  plan_type: account.plan_type ?? "-",
1135
1204
  available: computeAvailability(account) ?? "-",
1136
1205
  five_hour: formatUsagePercent(account.five_hour),
@@ -1255,11 +1324,13 @@ async function runCli(argv, options = {}) {
1255
1324
  account: {
1256
1325
  name: account.name,
1257
1326
  account_id: account.account_id,
1327
+ user_id: account.user_id ?? null,
1328
+ identity: account.identity,
1258
1329
  auth_mode: account.auth_mode
1259
1330
  }
1260
1331
  };
1261
1332
  if (json) writeJson(streams.stdout, payload);
1262
- else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.account_id)}).\n`);
1333
+ else streams.stdout.write(`Saved account "${account.name}" (${maskAccountId(account.identity)}).\n`);
1263
1334
  return 0;
1264
1335
  }
1265
1336
  case "update":
@@ -1281,6 +1352,8 @@ async function runCli(argv, options = {}) {
1281
1352
  account: {
1282
1353
  name: result.account.name,
1283
1354
  account_id: result.account.account_id,
1355
+ user_id: result.account.user_id ?? null,
1356
+ identity: result.account.identity,
1284
1357
  auth_mode: result.account.auth_mode
1285
1358
  },
1286
1359
  quota,
@@ -1288,7 +1361,7 @@ async function runCli(argv, options = {}) {
1288
1361
  };
1289
1362
  if (json) writeJson(streams.stdout, payload);
1290
1363
  else {
1291
- streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1364
+ streams.stdout.write(`Updated managed account "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1292
1365
  for (const warning of warnings)streams.stdout.write(`Warning: ${warning}\n`);
1293
1366
  }
1294
1367
  return 0;
@@ -1331,7 +1404,8 @@ async function runCli(argv, options = {}) {
1331
1404
  reason: "already_current_best",
1332
1405
  account: {
1333
1406
  name: selected.name,
1334
- account_id: selected.account_id
1407
+ account_id: selected.account_id,
1408
+ identity: selected.identity
1335
1409
  },
1336
1410
  selected,
1337
1411
  candidates,
@@ -1351,6 +1425,8 @@ async function runCli(argv, options = {}) {
1351
1425
  account: {
1352
1426
  name: result.account.name,
1353
1427
  account_id: result.account.account_id,
1428
+ user_id: result.account.user_id ?? null,
1429
+ identity: result.account.identity,
1354
1430
  auth_mode: result.account.auth_mode
1355
1431
  },
1356
1432
  selected,
@@ -1380,6 +1456,8 @@ async function runCli(argv, options = {}) {
1380
1456
  account: {
1381
1457
  name: result.account.name,
1382
1458
  account_id: result.account.account_id,
1459
+ user_id: result.account.user_id ?? null,
1460
+ identity: result.account.identity,
1383
1461
  auth_mode: result.account.auth_mode
1384
1462
  },
1385
1463
  quota,
@@ -1388,7 +1466,7 @@ async function runCli(argv, options = {}) {
1388
1466
  };
1389
1467
  if (json) writeJson(streams.stdout, payload);
1390
1468
  else {
1391
- streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.account_id)}).\n`);
1469
+ streams.stdout.write(`Switched to "${result.account.name}" (${maskAccountId(result.account.identity)}).\n`);
1392
1470
  if (result.backup_path) streams.stdout.write(`Backup: ${result.backup_path}\n`);
1393
1471
  for (const warning of result.warnings)streams.stdout.write(`Warning: ${warning}\n`);
1394
1472
  }
@@ -1430,6 +1508,8 @@ async function runCli(argv, options = {}) {
1430
1508
  account: {
1431
1509
  name: account.name,
1432
1510
  account_id: account.account_id,
1511
+ user_id: account.user_id ?? null,
1512
+ identity: account.identity,
1433
1513
  auth_mode: account.auth_mode
1434
1514
  }
1435
1515
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",