@carjms/codexswitch 0.1.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/src/store.js CHANGED
@@ -30,6 +30,9 @@ function loadMeta() {
30
30
  if (!meta.accounts) meta.accounts = {};
31
31
  if (!('active' in meta)) meta.active = null;
32
32
  if (!meta.cooldownMinutes) meta.cooldownMinutes = 60;
33
+ if (!meta.threshold5h) meta.threshold5h = 95;
34
+ if (!meta.thresholdWeekly) meta.thresholdWeekly = 95;
35
+ if (!Array.isArray(meta.limitPatterns)) meta.limitPatterns = [];
33
36
  return meta;
34
37
  }
35
38
 
@@ -83,6 +86,7 @@ function listAccounts() {
83
86
  priority: m.priority ?? 0,
84
87
  disabled: !!m.disabled,
85
88
  limitedUntil: m.limitedUntil || null,
89
+ usage: m.usage || null,
86
90
  active: meta.active === name,
87
91
  };
88
92
  })
@@ -91,14 +95,19 @@ function listAccounts() {
91
95
 
92
96
  // Copy refreshed tokens from a live auth.json back into the store, so a
93
97
  // token refresh done by codex itself is never lost when we switch accounts.
94
- // 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.
95
102
  function syncBackFrom(authFile) {
96
103
  const cur = readJSONSafe(authFile);
97
104
  if (!cur) return null;
98
105
  const curInfo = authInfo(cur);
99
106
  if (!curInfo.accountId) return null;
100
107
  const meta = loadMeta();
101
- 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);
102
111
  if (candidates.length === 0) return null;
103
112
  const target = candidates.find((a) => a.name === meta.active) || candidates[0];
104
113
  const stored = readAccountAuth(target.name);
@@ -122,24 +131,78 @@ function markLimited(name, untilTs) {
122
131
  saveMeta(meta);
123
132
  }
124
133
 
125
- function clearLimited(name) {
134
+ function clearLimited(name, { includeUsage = false } = {}) {
126
135
  const meta = loadMeta();
127
- if (meta.accounts[name] && meta.accounts[name].limitedUntil) {
128
- delete meta.accounts[name].limitedUntil;
136
+ const m = meta.accounts[name];
137
+ if (m && (m.limitedUntil || (includeUsage && m.usage))) {
138
+ delete m.limitedUntil;
139
+ if (includeUsage) delete m.usage;
129
140
  saveMeta(meta);
130
141
  }
131
142
  }
132
143
 
144
+ function saveUsage(name, usage) {
145
+ const meta = loadMeta();
146
+ if (!meta.accounts[name]) meta.accounts[name] = {};
147
+ meta.accounts[name].usage = usage;
148
+ saveMeta(meta);
149
+ }
150
+
151
+ // An account is "over threshold" when its last recorded 5h/weekly usage
152
+ // exceeds the configured percentage and the window has not reset yet.
153
+ // Returns the blocking window ('5h' | 'weekly') or null.
154
+ function overThreshold(account, meta, now = Date.now()) {
155
+ const u = account.usage;
156
+ if (!u) return null;
157
+ const checks = [
158
+ ['5h', u.p5h, meta.threshold5h],
159
+ ['weekly', u.weekly, meta.thresholdWeekly],
160
+ ];
161
+ for (const [label, win, threshold] of checks) {
162
+ if (!win || typeof win.pct !== 'number') continue;
163
+ if (win.pct < threshold) continue;
164
+ // Trust the snapshot only while its window can still be in effect.
165
+ if (win.resetAt) {
166
+ if (win.resetAt > now) return label;
167
+ } else if (win.windowMinutes && u.at + win.windowMinutes * 60000 > now) {
168
+ return label;
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function isUsable(account, meta, now = Date.now()) {
175
+ if (account.disabled) return false;
176
+ if (account.limitedUntil && account.limitedUntil > now) return false;
177
+ if (overThreshold(account, meta, now)) return false;
178
+ return true;
179
+ }
180
+
133
181
  // Pick the best usable account: enabled, not currently rate-limited,
134
- // lowest priority number first. `exclude` skips accounts already tried.
182
+ // lowest priority number first (the order set by "order"/"priority").
183
+ // The active account only wins ties within the same priority, so an
184
+ // explicit order is always respected. `exclude` skips accounts already tried.
135
185
  function pickAccount(exclude = []) {
136
186
  const now = Date.now();
137
- const usable = listAccounts().filter(
138
- (a) => !a.disabled && !exclude.includes(a.name) && (!a.limitedUntil || a.limitedUntil <= now)
139
- );
187
+ const meta = loadMeta();
188
+ const usable = listAccounts().filter((a) => !exclude.includes(a.name) && isUsable(a, meta, now));
140
189
  if (usable.length === 0) return null;
141
- const active = usable.find((a) => a.active);
142
- return active || usable[0];
190
+ const group = usable.filter((a) => a.priority === usable[0].priority);
191
+ return group.find((a) => a.active) || group[0];
192
+ }
193
+
194
+ // Next usable account after the active one, following priority order and
195
+ // wrapping around — so repeated "next" cycles through the configured order.
196
+ function nextAccount() {
197
+ const all = listAccounts();
198
+ const now = Date.now();
199
+ const meta = loadMeta();
200
+ const start = all.findIndex((a) => a.active);
201
+ for (let i = 1; i <= all.length; i++) {
202
+ const cand = all[(start + i) % all.length];
203
+ if (!cand.active && isUsable(cand, meta, now)) return cand;
204
+ }
205
+ return null;
143
206
  }
144
207
 
145
208
  module.exports = {
@@ -155,7 +218,11 @@ module.exports = {
155
218
  syncBackFrom,
156
219
  markLimited,
157
220
  clearLimited,
221
+ saveUsage,
222
+ overThreshold,
223
+ isUsable,
158
224
  pickAccount,
225
+ nextAccount,
159
226
  ensureDirs() {
160
227
  const p = paths();
161
228
  ensureDir(p.home);