@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 +1 -1
- package/src/cli.js +18 -1
- package/src/runner.js +10 -0
- package/src/store.js +7 -2
package/package.json
CHANGED
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
|
|
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
|
|
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
|
|
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);
|