codex-team 0.0.4 → 0.0.6

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
@@ -15,6 +15,7 @@ After install, use the `codexm` command.
15
15
  ## Commands
16
16
 
17
17
  ```bash
18
+ codexm --version
18
19
  codexm current
19
20
  codexm list [name]
20
21
  codexm save <name>
package/dist/cli.cjs CHANGED
@@ -12,6 +12,10 @@ var __webpack_modules__ = {
12
12
  var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namespaceObject);
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
+ var package_namespaceObject = {
16
+ rE: "0.0.6"
17
+ };
18
+ const external_node_crypto_namespaceObject = require("node:crypto");
15
19
  const promises_namespaceObject = require("node:fs/promises");
16
20
  function isRecord(value) {
17
21
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -34,6 +38,29 @@ var __webpack_modules__ = {
34
38
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
35
39
  return value;
36
40
  }
41
+ function normalizeAuthMode(authMode) {
42
+ return authMode.trim().toLowerCase();
43
+ }
44
+ function isApiKeyAuthMode(authMode) {
45
+ return "apikey" === normalizeAuthMode(authMode);
46
+ }
47
+ function isSupportedChatGPTAuthMode(authMode) {
48
+ const normalized = normalizeAuthMode(authMode);
49
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
50
+ }
51
+ function fingerprintApiKey(apiKey) {
52
+ return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
53
+ }
54
+ function getSnapshotIdentity(snapshot) {
55
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
56
+ const apiKey = snapshot.OPENAI_API_KEY;
57
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
58
+ return `key_${fingerprintApiKey(apiKey)}`;
59
+ }
60
+ const accountId = snapshot.tokens?.account_id;
61
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
62
+ return accountId;
63
+ }
37
64
  function defaultQuotaSnapshot() {
38
65
  return {
39
66
  status: "stale"
@@ -78,15 +105,28 @@ var __webpack_modules__ = {
78
105
  }
79
106
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
80
107
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
81
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
82
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
108
+ const tokens = parsed.tokens;
109
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
110
+ const apiKeyMode = isApiKeyAuthMode(authMode);
111
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
112
+ if (apiKeyMode) {
113
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
114
+ } else {
115
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
116
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
117
+ }
83
118
  return {
84
119
  ...parsed,
85
120
  auth_mode: authMode,
86
- tokens: {
87
- ...parsed.tokens,
88
- account_id: accountId
89
- }
121
+ OPENAI_API_KEY: normalizedApiKey,
122
+ ...isRecord(tokens) ? {
123
+ tokens: {
124
+ ...tokens,
125
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
126
+ account_id: tokens.account_id
127
+ } : {}
128
+ }
129
+ } : {}
90
130
  };
91
131
  }
92
132
  async function readAuthSnapshotFile(filePath) {
@@ -98,7 +138,7 @@ var __webpack_modules__ = {
98
138
  return {
99
139
  name,
100
140
  auth_mode: snapshot.auth_mode,
101
- account_id: snapshot.tokens.account_id,
141
+ account_id: getSnapshotIdentity(snapshot),
102
142
  created_at: existingCreatedAt ?? timestamp,
103
143
  updated_at: timestamp,
104
144
  last_switched_at: null,
@@ -155,16 +195,13 @@ var __webpack_modules__ = {
155
195
  const value = payload[key];
156
196
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
157
197
  }
158
- function isSupportedChatGPTMode(authMode) {
159
- const normalized = authMode.trim().toLowerCase();
160
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
161
- }
162
198
  function parsePlanType(snapshot) {
199
+ const tokens = snapshot.tokens ?? {};
163
200
  for (const tokenName of [
164
201
  "id_token",
165
202
  "access_token"
166
203
  ]){
167
- const token = snapshot.tokens[tokenName];
204
+ const token = tokens[tokenName];
168
205
  if ("string" == typeof token && "" !== token.trim()) try {
169
206
  const payload = decodeJwtPayload(token);
170
207
  const authClaim = extractAuthClaim(payload);
@@ -175,10 +212,11 @@ var __webpack_modules__ = {
175
212
  }
176
213
  function extractChatGPTAuth(snapshot) {
177
214
  const authMode = snapshot.auth_mode ?? "";
178
- const supported = isSupportedChatGPTMode(authMode);
179
- const accessTokenValue = snapshot.tokens.access_token;
180
- const refreshTokenValue = snapshot.tokens.refresh_token;
181
- const directAccountId = snapshot.tokens.account_id;
215
+ const supported = isSupportedChatGPTAuthMode(authMode);
216
+ const tokens = snapshot.tokens ?? {};
217
+ const accessTokenValue = tokens.access_token;
218
+ const refreshTokenValue = tokens.refresh_token;
219
+ const directAccountId = tokens.account_id;
182
220
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
183
221
  let planType;
184
222
  let issuer;
@@ -187,7 +225,7 @@ var __webpack_modules__ = {
187
225
  "id_token",
188
226
  "access_token"
189
227
  ]){
190
- const token = snapshot.tokens[tokenName];
228
+ const token = tokens[tokenName];
191
229
  if ("string" == typeof token && "" !== token.trim()) try {
192
230
  const payload = decodeJwtPayload(token);
193
231
  const authClaim = extractAuthClaim(payload);
@@ -369,7 +407,7 @@ var __webpack_modules__ = {
369
407
  ...snapshot,
370
408
  last_refresh: (options.now ?? new Date()).toISOString(),
371
409
  tokens: {
372
- ...snapshot.tokens,
410
+ ...snapshot.tokens ?? {},
373
411
  access_token: payload.access_token,
374
412
  id_token: payload.id_token,
375
413
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -520,7 +558,7 @@ var __webpack_modules__ = {
520
558
  if (!await pathExists(this.paths.currentAuthPath)) return;
521
559
  try {
522
560
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
523
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
561
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
524
562
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
525
563
  } catch {}
526
564
  }
@@ -576,7 +614,7 @@ var __webpack_modules__ = {
576
614
  ]);
577
615
  const meta = parseSnapshotMeta(rawMeta);
578
616
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
579
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
617
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
580
618
  return {
581
619
  ...meta,
582
620
  authPath,
@@ -619,11 +657,12 @@ var __webpack_modules__ = {
619
657
  warnings
620
658
  };
621
659
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
622
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
660
+ const currentIdentity = getSnapshotIdentity(snapshot);
661
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
623
662
  return {
624
663
  exists: true,
625
664
  auth_mode: snapshot.auth_mode,
626
- account_id: snapshot.tokens.account_id,
665
+ account_id: currentIdentity,
627
666
  matched_accounts: matchedAccounts,
628
667
  managed: matchedAccounts.length > 0,
629
668
  duplicate_match: matchedAccounts.length > 1,
@@ -686,7 +725,7 @@ var __webpack_modules__ = {
686
725
  const rawAuth = await readJsonFile(account.authPath);
687
726
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
688
727
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
689
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
728
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
690
729
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
691
730
  meta.last_switched_at = new Date().toISOString();
692
731
  meta.updated_at = meta.last_switched_at;
@@ -730,7 +769,7 @@ var __webpack_modules__ = {
730
769
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
731
770
  }
732
771
  meta.auth_mode = result.authSnapshot.auth_mode;
733
- meta.account_id = result.authSnapshot.tokens.account_id;
772
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
734
773
  meta.updated_at = now.toISOString();
735
774
  meta.quota = result.quota;
736
775
  await this.writeAccountMeta(name, meta);
@@ -842,7 +881,7 @@ var __webpack_modules__ = {
842
881
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
843
882
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
844
883
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
845
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
884
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
846
885
  }
847
886
  let currentAuthPresent = false;
848
887
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -904,6 +943,8 @@ var __webpack_modules__ = {
904
943
  stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
905
944
 
906
945
  Usage:
946
+ codexm --version
947
+ codexm --help
907
948
  codexm current [--json]
908
949
  codexm list [name] [--json]
909
950
  codexm save <name> [--force] [--json]
@@ -923,7 +964,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
923
964
  if (status.exists) {
924
965
  lines.push("Current auth: present");
925
966
  lines.push(`Auth mode: ${status.auth_mode}`);
926
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
967
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
927
968
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
928
969
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
929
970
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1056,7 +1097,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1056
1097
  },
1057
1098
  {
1058
1099
  key: "account_id",
1059
- label: "ACCOUNT ID"
1100
+ label: "IDENTITY"
1060
1101
  },
1061
1102
  {
1062
1103
  key: "plan_type",
@@ -1131,6 +1172,10 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1131
1172
  const parsed = parseArgs(argv);
1132
1173
  const json = parsed.flags.has("--json");
1133
1174
  try {
1175
+ if (parsed.flags.has("--version")) {
1176
+ streams.stdout.write(`${package_namespaceObject.rE}\n`);
1177
+ return 0;
1178
+ }
1134
1179
  if (!parsed.command || parsed.flags.has("--help")) {
1135
1180
  printHelp(streams.stdout);
1136
1181
  return 0;
package/dist/main.cjs CHANGED
@@ -42,6 +42,10 @@ const timezone_js_namespaceObject = require("dayjs/plugin/timezone.js");
42
42
  var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namespaceObject);
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
+ var package_namespaceObject = {
46
+ rE: "0.0.6"
47
+ };
48
+ const external_node_crypto_namespaceObject = require("node:crypto");
45
49
  const promises_namespaceObject = require("node:fs/promises");
46
50
  function isRecord(value) {
47
51
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -64,6 +68,29 @@ function asOptionalNumber(value, fieldName) {
64
68
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
65
69
  return value;
66
70
  }
71
+ function normalizeAuthMode(authMode) {
72
+ return authMode.trim().toLowerCase();
73
+ }
74
+ function isApiKeyAuthMode(authMode) {
75
+ return "apikey" === normalizeAuthMode(authMode);
76
+ }
77
+ function isSupportedChatGPTAuthMode(authMode) {
78
+ const normalized = normalizeAuthMode(authMode);
79
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
80
+ }
81
+ function fingerprintApiKey(apiKey) {
82
+ return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
83
+ }
84
+ function getSnapshotIdentity(snapshot) {
85
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
86
+ const apiKey = snapshot.OPENAI_API_KEY;
87
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
88
+ return `key_${fingerprintApiKey(apiKey)}`;
89
+ }
90
+ const accountId = snapshot.tokens?.account_id;
91
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
92
+ return accountId;
93
+ }
67
94
  function defaultQuotaSnapshot() {
68
95
  return {
69
96
  status: "stale"
@@ -108,15 +135,28 @@ function parseAuthSnapshot(raw) {
108
135
  }
109
136
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
110
137
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
111
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
112
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
138
+ const tokens = parsed.tokens;
139
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
140
+ const apiKeyMode = isApiKeyAuthMode(authMode);
141
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
142
+ if (apiKeyMode) {
143
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
144
+ } else {
145
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
146
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
147
+ }
113
148
  return {
114
149
  ...parsed,
115
150
  auth_mode: authMode,
116
- tokens: {
117
- ...parsed.tokens,
118
- account_id: accountId
119
- }
151
+ OPENAI_API_KEY: normalizedApiKey,
152
+ ...isRecord(tokens) ? {
153
+ tokens: {
154
+ ...tokens,
155
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
156
+ account_id: tokens.account_id
157
+ } : {}
158
+ }
159
+ } : {}
120
160
  };
121
161
  }
122
162
  async function readAuthSnapshotFile(filePath) {
@@ -128,7 +168,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
128
168
  return {
129
169
  name,
130
170
  auth_mode: snapshot.auth_mode,
131
- account_id: snapshot.tokens.account_id,
171
+ account_id: getSnapshotIdentity(snapshot),
132
172
  created_at: existingCreatedAt ?? timestamp,
133
173
  updated_at: timestamp,
134
174
  last_switched_at: null,
@@ -185,16 +225,13 @@ function extractStringClaim(payload, key) {
185
225
  const value = payload[key];
186
226
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
187
227
  }
188
- function isSupportedChatGPTMode(authMode) {
189
- const normalized = authMode.trim().toLowerCase();
190
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
191
- }
192
228
  function parsePlanType(snapshot) {
229
+ const tokens = snapshot.tokens ?? {};
193
230
  for (const tokenName of [
194
231
  "id_token",
195
232
  "access_token"
196
233
  ]){
197
- const token = snapshot.tokens[tokenName];
234
+ const token = tokens[tokenName];
198
235
  if ("string" == typeof token && "" !== token.trim()) try {
199
236
  const payload = decodeJwtPayload(token);
200
237
  const authClaim = extractAuthClaim(payload);
@@ -205,10 +242,11 @@ function parsePlanType(snapshot) {
205
242
  }
206
243
  function extractChatGPTAuth(snapshot) {
207
244
  const authMode = snapshot.auth_mode ?? "";
208
- const supported = isSupportedChatGPTMode(authMode);
209
- const accessTokenValue = snapshot.tokens.access_token;
210
- const refreshTokenValue = snapshot.tokens.refresh_token;
211
- const directAccountId = snapshot.tokens.account_id;
245
+ const supported = isSupportedChatGPTAuthMode(authMode);
246
+ const tokens = snapshot.tokens ?? {};
247
+ const accessTokenValue = tokens.access_token;
248
+ const refreshTokenValue = tokens.refresh_token;
249
+ const directAccountId = tokens.account_id;
212
250
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
213
251
  let planType;
214
252
  let issuer;
@@ -217,7 +255,7 @@ function extractChatGPTAuth(snapshot) {
217
255
  "id_token",
218
256
  "access_token"
219
257
  ]){
220
- const token = snapshot.tokens[tokenName];
258
+ const token = tokens[tokenName];
221
259
  if ("string" == typeof token && "" !== token.trim()) try {
222
260
  const payload = decodeJwtPayload(token);
223
261
  const authClaim = extractAuthClaim(payload);
@@ -399,7 +437,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
399
437
  ...snapshot,
400
438
  last_refresh: (options.now ?? new Date()).toISOString(),
401
439
  tokens: {
402
- ...snapshot.tokens,
440
+ ...snapshot.tokens ?? {},
403
441
  access_token: payload.access_token,
404
442
  id_token: payload.id_token,
405
443
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -550,7 +588,7 @@ class AccountStore {
550
588
  if (!await pathExists(this.paths.currentAuthPath)) return;
551
589
  try {
552
590
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
553
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
591
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
554
592
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
555
593
  } catch {}
556
594
  }
@@ -606,7 +644,7 @@ class AccountStore {
606
644
  ]);
607
645
  const meta = parseSnapshotMeta(rawMeta);
608
646
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
609
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
647
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
610
648
  return {
611
649
  ...meta,
612
650
  authPath,
@@ -649,11 +687,12 @@ class AccountStore {
649
687
  warnings
650
688
  };
651
689
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
652
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
690
+ const currentIdentity = getSnapshotIdentity(snapshot);
691
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
653
692
  return {
654
693
  exists: true,
655
694
  auth_mode: snapshot.auth_mode,
656
- account_id: snapshot.tokens.account_id,
695
+ account_id: currentIdentity,
657
696
  matched_accounts: matchedAccounts,
658
697
  managed: matchedAccounts.length > 0,
659
698
  duplicate_match: matchedAccounts.length > 1,
@@ -716,7 +755,7 @@ class AccountStore {
716
755
  const rawAuth = await readJsonFile(account.authPath);
717
756
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
718
757
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
719
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
758
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
720
759
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
721
760
  meta.last_switched_at = new Date().toISOString();
722
761
  meta.updated_at = meta.last_switched_at;
@@ -760,7 +799,7 @@ class AccountStore {
760
799
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
761
800
  }
762
801
  meta.auth_mode = result.authSnapshot.auth_mode;
763
- meta.account_id = result.authSnapshot.tokens.account_id;
802
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
764
803
  meta.updated_at = now.toISOString();
765
804
  meta.quota = result.quota;
766
805
  await this.writeAccountMeta(name, meta);
@@ -872,7 +911,7 @@ class AccountStore {
872
911
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
873
912
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
874
913
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
875
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
914
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
876
915
  }
877
916
  let currentAuthPresent = false;
878
917
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -934,6 +973,8 @@ function printHelp(stream) {
934
973
  stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
935
974
 
936
975
  Usage:
976
+ codexm --version
977
+ codexm --help
937
978
  codexm current [--json]
938
979
  codexm list [name] [--json]
939
980
  codexm save <name> [--force] [--json]
@@ -953,7 +994,7 @@ function describeCurrentStatus(status) {
953
994
  if (status.exists) {
954
995
  lines.push("Current auth: present");
955
996
  lines.push(`Auth mode: ${status.auth_mode}`);
956
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
997
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
957
998
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
958
999
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
959
1000
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1086,7 +1127,7 @@ function describeQuotaAccounts(accounts, warnings) {
1086
1127
  },
1087
1128
  {
1088
1129
  key: "account_id",
1089
- label: "ACCOUNT ID"
1130
+ label: "IDENTITY"
1090
1131
  },
1091
1132
  {
1092
1133
  key: "plan_type",
@@ -1161,6 +1202,10 @@ async function runCli(argv, options = {}) {
1161
1202
  const parsed = parseArgs(argv);
1162
1203
  const json = parsed.flags.has("--json");
1163
1204
  try {
1205
+ if (parsed.flags.has("--version")) {
1206
+ streams.stdout.write(`${package_namespaceObject.rE}\n`);
1207
+ return 0;
1208
+ }
1164
1209
  if (!parsed.command || parsed.flags.has("--help")) {
1165
1210
  printHelp(streams.stdout);
1166
1211
  return 0;
package/dist/main.js CHANGED
@@ -2,11 +2,15 @@ import { stderr, stdin, stdout as external_node_process_stdout } from "node:proc
2
2
  import dayjs from "dayjs";
3
3
  import timezone from "dayjs/plugin/timezone.js";
4
4
  import utc from "dayjs/plugin/utc.js";
5
+ import { createHash } from "node:crypto";
5
6
  import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
7
  import { homedir } from "node:os";
7
8
  import { basename, dirname, join } from "node:path";
8
9
  import { execFile } from "node:child_process";
9
10
  import { promisify } from "node:util";
11
+ var package_namespaceObject = {
12
+ rE: "0.0.6"
13
+ };
10
14
  function isRecord(value) {
11
15
  return "object" == typeof value && null !== value && !Array.isArray(value);
12
16
  }
@@ -28,6 +32,29 @@ function asOptionalNumber(value, fieldName) {
28
32
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
29
33
  return value;
30
34
  }
35
+ function normalizeAuthMode(authMode) {
36
+ return authMode.trim().toLowerCase();
37
+ }
38
+ function isApiKeyAuthMode(authMode) {
39
+ return "apikey" === normalizeAuthMode(authMode);
40
+ }
41
+ function isSupportedChatGPTAuthMode(authMode) {
42
+ const normalized = normalizeAuthMode(authMode);
43
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
44
+ }
45
+ function fingerprintApiKey(apiKey) {
46
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
47
+ }
48
+ function getSnapshotIdentity(snapshot) {
49
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
50
+ const apiKey = snapshot.OPENAI_API_KEY;
51
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
52
+ return `key_${fingerprintApiKey(apiKey)}`;
53
+ }
54
+ const accountId = snapshot.tokens?.account_id;
55
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
56
+ return accountId;
57
+ }
31
58
  function defaultQuotaSnapshot() {
32
59
  return {
33
60
  status: "stale"
@@ -72,15 +99,28 @@ function parseAuthSnapshot(raw) {
72
99
  }
73
100
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
74
101
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
75
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
76
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
102
+ const tokens = parsed.tokens;
103
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
104
+ const apiKeyMode = isApiKeyAuthMode(authMode);
105
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
106
+ if (apiKeyMode) {
107
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
108
+ } else {
109
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
110
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
111
+ }
77
112
  return {
78
113
  ...parsed,
79
114
  auth_mode: authMode,
80
- tokens: {
81
- ...parsed.tokens,
82
- account_id: accountId
83
- }
115
+ OPENAI_API_KEY: normalizedApiKey,
116
+ ...isRecord(tokens) ? {
117
+ tokens: {
118
+ ...tokens,
119
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
120
+ account_id: tokens.account_id
121
+ } : {}
122
+ }
123
+ } : {}
84
124
  };
85
125
  }
86
126
  async function readAuthSnapshotFile(filePath) {
@@ -92,7 +132,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
92
132
  return {
93
133
  name,
94
134
  auth_mode: snapshot.auth_mode,
95
- account_id: snapshot.tokens.account_id,
135
+ account_id: getSnapshotIdentity(snapshot),
96
136
  created_at: existingCreatedAt ?? timestamp,
97
137
  updated_at: timestamp,
98
138
  last_switched_at: null,
@@ -145,16 +185,13 @@ function extractStringClaim(payload, key) {
145
185
  const value = payload[key];
146
186
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
147
187
  }
148
- function isSupportedChatGPTMode(authMode) {
149
- const normalized = authMode.trim().toLowerCase();
150
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
151
- }
152
188
  function parsePlanType(snapshot) {
189
+ const tokens = snapshot.tokens ?? {};
153
190
  for (const tokenName of [
154
191
  "id_token",
155
192
  "access_token"
156
193
  ]){
157
- const token = snapshot.tokens[tokenName];
194
+ const token = tokens[tokenName];
158
195
  if ("string" == typeof token && "" !== token.trim()) try {
159
196
  const payload = decodeJwtPayload(token);
160
197
  const authClaim = extractAuthClaim(payload);
@@ -165,10 +202,11 @@ function parsePlanType(snapshot) {
165
202
  }
166
203
  function extractChatGPTAuth(snapshot) {
167
204
  const authMode = snapshot.auth_mode ?? "";
168
- const supported = isSupportedChatGPTMode(authMode);
169
- const accessTokenValue = snapshot.tokens.access_token;
170
- const refreshTokenValue = snapshot.tokens.refresh_token;
171
- const directAccountId = snapshot.tokens.account_id;
205
+ const supported = isSupportedChatGPTAuthMode(authMode);
206
+ const tokens = snapshot.tokens ?? {};
207
+ const accessTokenValue = tokens.access_token;
208
+ const refreshTokenValue = tokens.refresh_token;
209
+ const directAccountId = tokens.account_id;
172
210
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
173
211
  let planType;
174
212
  let issuer;
@@ -177,7 +215,7 @@ function extractChatGPTAuth(snapshot) {
177
215
  "id_token",
178
216
  "access_token"
179
217
  ]){
180
- const token = snapshot.tokens[tokenName];
218
+ const token = tokens[tokenName];
181
219
  if ("string" == typeof token && "" !== token.trim()) try {
182
220
  const payload = decodeJwtPayload(token);
183
221
  const authClaim = extractAuthClaim(payload);
@@ -359,7 +397,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
359
397
  ...snapshot,
360
398
  last_refresh: (options.now ?? new Date()).toISOString(),
361
399
  tokens: {
362
- ...snapshot.tokens,
400
+ ...snapshot.tokens ?? {},
363
401
  access_token: payload.access_token,
364
402
  id_token: payload.id_token,
365
403
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -510,7 +548,7 @@ class AccountStore {
510
548
  if (!await pathExists(this.paths.currentAuthPath)) return;
511
549
  try {
512
550
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
513
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
551
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
514
552
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
515
553
  } catch {}
516
554
  }
@@ -566,7 +604,7 @@ class AccountStore {
566
604
  ]);
567
605
  const meta = parseSnapshotMeta(rawMeta);
568
606
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
569
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
607
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
570
608
  return {
571
609
  ...meta,
572
610
  authPath,
@@ -609,11 +647,12 @@ class AccountStore {
609
647
  warnings
610
648
  };
611
649
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
612
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
650
+ const currentIdentity = getSnapshotIdentity(snapshot);
651
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
613
652
  return {
614
653
  exists: true,
615
654
  auth_mode: snapshot.auth_mode,
616
- account_id: snapshot.tokens.account_id,
655
+ account_id: currentIdentity,
617
656
  matched_accounts: matchedAccounts,
618
657
  managed: matchedAccounts.length > 0,
619
658
  duplicate_match: matchedAccounts.length > 1,
@@ -676,7 +715,7 @@ class AccountStore {
676
715
  const rawAuth = await readJsonFile(account.authPath);
677
716
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
678
717
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
679
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
718
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
680
719
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
681
720
  meta.last_switched_at = new Date().toISOString();
682
721
  meta.updated_at = meta.last_switched_at;
@@ -720,7 +759,7 @@ class AccountStore {
720
759
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
721
760
  }
722
761
  meta.auth_mode = result.authSnapshot.auth_mode;
723
- meta.account_id = result.authSnapshot.tokens.account_id;
762
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
724
763
  meta.updated_at = now.toISOString();
725
764
  meta.quota = result.quota;
726
765
  await this.writeAccountMeta(name, meta);
@@ -832,7 +871,7 @@ class AccountStore {
832
871
  const metaStat = await stat(account.metaPath);
833
872
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
834
873
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
835
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
874
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
836
875
  }
837
876
  let currentAuthPresent = false;
838
877
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -894,6 +933,8 @@ function printHelp(stream) {
894
933
  stream.write(`codexm - manage multiple Codex ChatGPT auth snapshots
895
934
 
896
935
  Usage:
936
+ codexm --version
937
+ codexm --help
897
938
  codexm current [--json]
898
939
  codexm list [name] [--json]
899
940
  codexm save <name> [--force] [--json]
@@ -913,7 +954,7 @@ function describeCurrentStatus(status) {
913
954
  if (status.exists) {
914
955
  lines.push("Current auth: present");
915
956
  lines.push(`Auth mode: ${status.auth_mode}`);
916
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
957
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
917
958
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
918
959
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
919
960
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1046,7 +1087,7 @@ function describeQuotaAccounts(accounts, warnings) {
1046
1087
  },
1047
1088
  {
1048
1089
  key: "account_id",
1049
- label: "ACCOUNT ID"
1090
+ label: "IDENTITY"
1050
1091
  },
1051
1092
  {
1052
1093
  key: "plan_type",
@@ -1121,6 +1162,10 @@ async function runCli(argv, options = {}) {
1121
1162
  const parsed = parseArgs(argv);
1122
1163
  const json = parsed.flags.has("--json");
1123
1164
  try {
1165
+ if (parsed.flags.has("--version")) {
1166
+ streams.stdout.write(`${package_namespaceObject.rE}\n`);
1167
+ return 0;
1168
+ }
1124
1169
  if (!parsed.command || parsed.flags.has("--help")) {
1125
1170
  printHelp(streams.stdout);
1126
1171
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,14 +36,6 @@
36
36
  "LICENSE",
37
37
  "README.md"
38
38
  ],
39
- "scripts": {
40
- "build": "rslib build",
41
- "dev": "rslib build --watch",
42
- "prepack": "pnpm build",
43
- "test": "rstest run",
44
- "test:watch": "rstest watch",
45
- "typecheck": "tsc --noEmit"
46
- },
47
39
  "engines": {
48
40
  "node": ">=20"
49
41
  },
@@ -59,5 +51,12 @@
59
51
  },
60
52
  "dependencies": {
61
53
  "dayjs": "^1.11.20"
54
+ },
55
+ "scripts": {
56
+ "build": "rslib build",
57
+ "dev": "rslib build --watch",
58
+ "test": "rstest run",
59
+ "test:watch": "rstest watch",
60
+ "typecheck": "tsc --noEmit"
62
61
  }
63
- }
62
+ }