@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/README.ko.md +300 -0
- package/README.md +157 -129
- package/package.json +1 -1
- package/src/cli.js +315 -25
- package/src/runner.js +112 -5
- package/src/store.js +78 -11
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
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
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
|
|
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
|
|
138
|
-
|
|
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
|
|
142
|
-
return active ||
|
|
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);
|