@carjms/codexswitch 0.3.0 → 0.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carjms/codexswitch",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Multi-account manager for OpenAI Codex CLI with quota-based rotation (inspired by KarpelesLab/teamclaude)",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/cli.js CHANGED
@@ -80,6 +80,9 @@ function storeAccount(name, auth) {
80
80
  store.writeAccountAuth(name, auth);
81
81
  const meta = store.loadMeta();
82
82
  if (!meta.accounts[name]) meta.accounts[name] = { priority: 0, addedAt: Date.now() };
83
+ // fresh credentials fix whatever got the account disabled or limited
84
+ delete meta.accounts[name].disabled;
85
+ delete meta.accounts[name].limitedUntil;
83
86
  store.saveMeta(meta);
84
87
  out(`${existed ? 'updated' : 'added'} account "${name}"${info.email ? ` (${info.email}, ${info.plan || 'unknown plan'})` : ''}`);
85
88
  return name;
@@ -184,7 +187,10 @@ function cmdCurrent() {
184
187
  return;
185
188
  }
186
189
  const info = authInfo(live);
187
- const match = store.listAccounts().find((a) => a.accountId === info.accountId);
190
+ const accounts = store.listAccounts();
191
+ const match =
192
+ (info.email && accounts.find((a) => a.email === info.email)) ||
193
+ accounts.find((a) => a.accountId === info.accountId);
188
194
  const name = match ? match.name : '(not stored — run "codexswitch import")';
189
195
  const note = meta.active && match && meta.active !== match.name ? ` (meta says "${meta.active}" — out of sync)` : '';
190
196
  out(`active: ${name}${note}`);
@@ -442,6 +448,7 @@ function buildExecArgs(rest, meta) {
442
448
  }
443
449
 
444
450
  async function cmdRun(args) {
451
+ store.syncBack(); // pick up tokens refreshed by plain codex before overlaying
445
452
  let name = null;
446
453
  let rest = args;
447
454
  if (args[0] && store.accountExists(args[0])) {
@@ -486,6 +493,7 @@ async function cmdExec(args) {
486
493
  if (explicit && !store.accountExists(explicit)) throw new Error(`no such account: ${explicit}`);
487
494
  if (rest[0] === 'resume') allowResume = false; // user drives resume themselves
488
495
 
496
+ store.syncBack(); // pick up tokens refreshed by plain codex before overlaying
489
497
  const meta = store.loadMeta();
490
498
  const total = store.listAccounts().length;
491
499
  if (total === 0) throw new Error('no accounts — add one with "codexswitch login"');
@@ -529,6 +537,15 @@ async function cmdExec(args) {
529
537
  );
530
538
  continue;
531
539
  }
540
+ if (runner.looksAuthFailed(res.output)) {
541
+ // A revoked token won't heal by itself — take the account out of
542
+ // rotation and keep the task going on the next one.
543
+ setFlag(name, { disabled: true });
544
+ console.error(
545
+ `[codexswitch] "${name}" has a revoked/invalid login — disabled. Fix it with: codexswitch login ${name}`
546
+ );
547
+ continue;
548
+ }
532
549
  return res.code; // real failure, don't burn other accounts on it
533
550
  }
534
551
  console.error('[codexswitch] all accounts are rate-limited, over threshold, or disabled');
package/src/runner.js CHANGED
@@ -286,6 +286,15 @@ function sessionTouchedSince(profile, sinceMs) {
286
286
  const LIMIT_RE =
287
287
  /usage[ _]limit|rate[ _]limit|too many requests|quota (?:exceeded|reached)|\b429\b|hit your (?:usage|weekly|5h) limit/i;
288
288
 
289
+ // Credential failures (revoked/invalidated tokens) — re-login required, so
290
+ // rotation should skip this account and tell the user how to fix it.
291
+ const AUTH_RE =
292
+ /refresh[ _]token[ _](?:was )?(?:revoked|invalidated)|token_invalidated|please log ?out and sign in again|please try signing in again|401 unauthorized/i;
293
+
294
+ function looksAuthFailed(output) {
295
+ return AUTH_RE.test(output || '');
296
+ }
297
+
289
298
  function looksRateLimited(output, extraPatterns = []) {
290
299
  if (LIMIT_RE.test(output || '')) return true;
291
300
  for (const p of extraPatterns) {
@@ -313,6 +322,7 @@ module.exports = {
313
322
  buildProfile,
314
323
  runCodex,
315
324
  looksRateLimited,
325
+ looksAuthFailed,
316
326
  limitCooldownMs,
317
327
  scanUsage,
318
328
  recordUsage,
package/src/store.js CHANGED
@@ -95,14 +95,19 @@ function listAccounts() {
95
95
 
96
96
  // Copy refreshed tokens from a live auth.json back into the store, so a
97
97
  // token refresh done by codex itself is never lost when we switch accounts.
98
- // Matches by account_id; prefers the account we believe deployed the file.
98
+ // Matches by email first: team-plan accounts share one chatgpt_account_id
99
+ // per workspace, so matching by account_id alone would let two teammates'
100
+ // tokens overwrite each other. Falls back to account_id for tokens without
101
+ // an email claim. Prefers the account we believe deployed the file.
99
102
  function syncBackFrom(authFile) {
100
103
  const cur = readJSONSafe(authFile);
101
104
  if (!cur) return null;
102
105
  const curInfo = authInfo(cur);
103
106
  if (!curInfo.accountId) return null;
104
107
  const meta = loadMeta();
105
- const candidates = listAccounts().filter((a) => a.accountId === curInfo.accountId);
108
+ const all = listAccounts();
109
+ const byEmail = curInfo.email ? all.filter((a) => a.email === curInfo.email) : [];
110
+ const candidates = byEmail.length > 0 ? byEmail : all.filter((a) => a.accountId === curInfo.accountId);
106
111
  if (candidates.length === 0) return null;
107
112
  const target = candidates.find((a) => a.name === meta.active) || candidates[0];
108
113
  const stored = readAccountAuth(target.name);