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 +2 -0
- package/dist/cli.cjs +107 -27
- package/dist/main.cjs +107 -27
- package/dist/main.js +107 -27
- package/package.json +1 -1
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ??=
|
|
241
|
-
clientId ??=
|
|
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
|
-
|
|
699
|
+
let meta = parseSnapshotMeta(rawMeta);
|
|
649
700
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
650
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ??=
|
|
271
|
-
clientId ??=
|
|
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
|
-
|
|
729
|
+
let meta = parseSnapshotMeta(rawMeta);
|
|
679
730
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
680
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ??=
|
|
231
|
-
clientId ??=
|
|
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
|
-
|
|
689
|
+
let meta = parseSnapshotMeta(rawMeta);
|
|
639
690
|
if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
|
|
640
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
});
|