codexuse-cli 2.4.16 → 2.4.18
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 +4 -0
- package/dist/index.js +455 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -566,6 +566,10 @@ async function persistLicense(license) {
|
|
|
566
566
|
}
|
|
567
567
|
});
|
|
568
568
|
}
|
|
569
|
+
async function getStoredAutoRollSettings() {
|
|
570
|
+
const state = await getAppState();
|
|
571
|
+
return normalizeAutoRollSettings(state.autoRoll);
|
|
572
|
+
}
|
|
569
573
|
async function getCodexCliChannel() {
|
|
570
574
|
const state = await getAppState();
|
|
571
575
|
return normalizeCodexCliChannel(state.app.cliChannel);
|
|
@@ -1410,16 +1414,35 @@ var ProfileManager = class {
|
|
|
1410
1414
|
if ("tokens" in data) {
|
|
1411
1415
|
return data;
|
|
1412
1416
|
}
|
|
1413
|
-
|
|
1417
|
+
const lastRefresh = typeof data.last_refresh === "string" ? data.last_refresh.trim() : "";
|
|
1418
|
+
let normalizedLastRefresh = lastRefresh.length > 0 ? lastRefresh : void 0;
|
|
1419
|
+
if (!normalizedLastRefresh) {
|
|
1420
|
+
const accessPayload = this.decodeJwtPayload(data.access_token);
|
|
1421
|
+
const tokenIssuedAt = typeof accessPayload?.iat === "number" ? this.toIsoStringFromSeconds(accessPayload.iat) : void 0;
|
|
1422
|
+
if (tokenIssuedAt) {
|
|
1423
|
+
normalizedLastRefresh = tokenIssuedAt;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (!normalizedLastRefresh) {
|
|
1427
|
+
const createdAt = typeof data.created_at === "string" ? data.created_at.trim() : "";
|
|
1428
|
+
if (createdAt) {
|
|
1429
|
+
normalizedLastRefresh = createdAt;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
const codexAuth = {
|
|
1414
1433
|
OPENAI_API_KEY: null,
|
|
1415
1434
|
tokens: {
|
|
1416
1435
|
id_token: data.id_token,
|
|
1417
1436
|
access_token: data.access_token,
|
|
1418
1437
|
refresh_token: data.refresh_token,
|
|
1419
|
-
account_id: data.account_id
|
|
1420
|
-
|
|
1421
|
-
|
|
1438
|
+
account_id: data.account_id,
|
|
1439
|
+
...normalizedLastRefresh ? { last_refresh: normalizedLastRefresh } : {}
|
|
1440
|
+
}
|
|
1422
1441
|
};
|
|
1442
|
+
if (normalizedLastRefresh) {
|
|
1443
|
+
codexAuth.last_refresh = normalizedLastRefresh;
|
|
1444
|
+
}
|
|
1445
|
+
return codexAuth;
|
|
1423
1446
|
}
|
|
1424
1447
|
/**
|
|
1425
1448
|
* Normalize Codex auth data to flat profile format
|
|
@@ -1435,6 +1458,12 @@ var ProfileManager = class {
|
|
|
1435
1458
|
refresh_token: tokens.refresh_token,
|
|
1436
1459
|
account_id: tokens.account_id
|
|
1437
1460
|
};
|
|
1461
|
+
const tokenLastRefresh = typeof tokens.last_refresh === "string" ? tokens.last_refresh.trim() : "";
|
|
1462
|
+
const rootLastRefresh = typeof data.last_refresh === "string" ? data.last_refresh.trim() : "";
|
|
1463
|
+
const normalizedLastRefresh = tokenLastRefresh || rootLastRefresh;
|
|
1464
|
+
if (normalizedLastRefresh) {
|
|
1465
|
+
profile.last_refresh = normalizedLastRefresh;
|
|
1466
|
+
}
|
|
1438
1467
|
if (data.email) {
|
|
1439
1468
|
profile.email = data.email;
|
|
1440
1469
|
}
|
|
@@ -1470,7 +1499,14 @@ var ProfileManager = class {
|
|
|
1470
1499
|
return { profile: merged, metadata };
|
|
1471
1500
|
}
|
|
1472
1501
|
hasTokenChanges(existing, incoming) {
|
|
1473
|
-
const tokenKeys = [
|
|
1502
|
+
const tokenKeys = [
|
|
1503
|
+
"id_token",
|
|
1504
|
+
"access_token",
|
|
1505
|
+
"refresh_token",
|
|
1506
|
+
"last_refresh",
|
|
1507
|
+
"account_id",
|
|
1508
|
+
"workspace_id"
|
|
1509
|
+
];
|
|
1474
1510
|
return tokenKeys.some((key) => {
|
|
1475
1511
|
const nextValue = incoming[key];
|
|
1476
1512
|
if (typeof nextValue !== "string" || nextValue.length === 0) {
|
|
@@ -1479,12 +1515,12 @@ var ProfileManager = class {
|
|
|
1479
1515
|
return nextValue !== existing[key];
|
|
1480
1516
|
});
|
|
1481
1517
|
}
|
|
1482
|
-
async
|
|
1518
|
+
async flagAuthIssue(name, issue, reason, options) {
|
|
1483
1519
|
await this.initialize();
|
|
1484
1520
|
const profileName = this.normalizeProfileName(name);
|
|
1485
1521
|
const record = await this.getProfileRowByName(profileName);
|
|
1486
1522
|
if (!record) {
|
|
1487
|
-
logWarn(`Cannot flag
|
|
1523
|
+
logWarn(`Cannot flag auth issue '${issue}' for missing profile '${profileName}'.`);
|
|
1488
1524
|
return;
|
|
1489
1525
|
}
|
|
1490
1526
|
const observedAt = options?.observedAt ? Date.parse(options.observedAt) : null;
|
|
@@ -1495,9 +1531,9 @@ var ProfileManager = class {
|
|
|
1495
1531
|
const data = record.data;
|
|
1496
1532
|
const trimmedReason = typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : void 0;
|
|
1497
1533
|
const normalized = trimmedReason?.toLowerCase() ?? "";
|
|
1498
|
-
const storedReason = normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET) ? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason ?? REFRESH_TOKEN_REDEEMED_REASON;
|
|
1534
|
+
const storedReason = issue === "refresh-redeemed" ? normalized.includes(REFRESH_TOKEN_REDEEMED_SNIPPET) ? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason ?? REFRESH_TOKEN_REDEEMED_REASON : trimmedReason;
|
|
1499
1535
|
const alert = {
|
|
1500
|
-
issue
|
|
1536
|
+
issue,
|
|
1501
1537
|
reason: storedReason,
|
|
1502
1538
|
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1503
1539
|
};
|
|
@@ -1509,6 +1545,9 @@ var ProfileManager = class {
|
|
|
1509
1545
|
logError(`Failed to persist token alert for profile '${profileName}':`, error);
|
|
1510
1546
|
}
|
|
1511
1547
|
}
|
|
1548
|
+
async flagRefreshTokenRedeemed(name, reason, options) {
|
|
1549
|
+
await this.flagAuthIssue(name, "refresh-redeemed", reason, options);
|
|
1550
|
+
}
|
|
1512
1551
|
async syncProfileTokensFromActiveAuth(profileName, baselineProfile, authPath) {
|
|
1513
1552
|
const targetPath = authPath ?? this.activeAuth;
|
|
1514
1553
|
try {
|
|
@@ -1791,7 +1830,12 @@ var ProfileManager = class {
|
|
|
1791
1830
|
if (!record) {
|
|
1792
1831
|
throw new Error(`Profile '${profileName}' not found!`);
|
|
1793
1832
|
}
|
|
1794
|
-
|
|
1833
|
+
let profileData = record.data;
|
|
1834
|
+
await this.syncProfileTokensFromActiveAuth(profileName, profileData);
|
|
1835
|
+
const updatedRecord = await this.getProfileRowByName(profileName);
|
|
1836
|
+
if (updatedRecord) {
|
|
1837
|
+
profileData = updatedRecord.data;
|
|
1838
|
+
}
|
|
1795
1839
|
const profileHome = this.getProfileHomePath(profileName);
|
|
1796
1840
|
await import_fs.promises.mkdir(profileHome, { recursive: true });
|
|
1797
1841
|
if (profileData.auth_method && profileData.auth_method !== "codex-cli") {
|
|
@@ -4017,7 +4061,6 @@ async function runCodexLogin(mode) {
|
|
|
4017
4061
|
// ../../lib/codex-rpc.ts
|
|
4018
4062
|
var import_node_child_process3 = require("child_process");
|
|
4019
4063
|
var import_node_readline = __toESM(require("readline"));
|
|
4020
|
-
var import_node_events = require("events");
|
|
4021
4064
|
var import_node_fs6 = require("fs");
|
|
4022
4065
|
var import_node_os2 = __toESM(require("os"));
|
|
4023
4066
|
var import_node_path8 = __toESM(require("path"));
|
|
@@ -4026,20 +4069,50 @@ async function sendPayload(child, payload) {
|
|
|
4026
4069
|
child.stdin?.write(JSON.stringify(payload));
|
|
4027
4070
|
child.stdin?.write("\n");
|
|
4028
4071
|
}
|
|
4029
|
-
|
|
4030
|
-
|
|
4072
|
+
function isRpcResponseForRequest(message, requestId) {
|
|
4073
|
+
if (message.id !== requestId) {
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
return Object.prototype.hasOwnProperty.call(message, "result") || Object.prototype.hasOwnProperty.call(message, "error");
|
|
4077
|
+
}
|
|
4078
|
+
async function readRpcResponseById(rl, requestId, timeoutMs) {
|
|
4079
|
+
return new Promise((resolve, reject) => {
|
|
4031
4080
|
const timer = setTimeout(() => {
|
|
4032
|
-
|
|
4081
|
+
cleanup();
|
|
4033
4082
|
reject(new Error("codex RPC timed out"));
|
|
4034
|
-
}, timeoutMs);
|
|
4083
|
+
}, Math.max(1, timeoutMs));
|
|
4084
|
+
const cleanup = () => {
|
|
4085
|
+
clearTimeout(timer);
|
|
4086
|
+
rl.off("line", onLine);
|
|
4087
|
+
rl.off("close", onClose);
|
|
4088
|
+
};
|
|
4089
|
+
const onClose = () => {
|
|
4090
|
+
cleanup();
|
|
4091
|
+
reject(new Error("codex RPC stream closed before response"));
|
|
4092
|
+
};
|
|
4093
|
+
const onLine = (line) => {
|
|
4094
|
+
let parsed;
|
|
4095
|
+
try {
|
|
4096
|
+
parsed = JSON.parse(line);
|
|
4097
|
+
} catch {
|
|
4098
|
+
cleanup();
|
|
4099
|
+
reject(new Error("codex RPC returned malformed JSON"));
|
|
4100
|
+
return;
|
|
4101
|
+
}
|
|
4102
|
+
if (!isRpcResponseForRequest(parsed, requestId)) {
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
cleanup();
|
|
4106
|
+
resolve(parsed);
|
|
4107
|
+
};
|
|
4108
|
+
rl.on("line", onLine);
|
|
4109
|
+
rl.on("close", onClose);
|
|
4035
4110
|
});
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
}
|
|
4041
|
-
throw new Error("codex RPC returned malformed JSON");
|
|
4042
|
-
}
|
|
4111
|
+
}
|
|
4112
|
+
function formatRpcError(method, error) {
|
|
4113
|
+
const codeText = typeof error.code === "number" ? ` (code ${error.code})` : "";
|
|
4114
|
+
const message = typeof error.message === "string" && error.message.trim().length > 0 ? error.message.trim() : "Unknown RPC error";
|
|
4115
|
+
return `${method} failed${codeText}: ${message}`;
|
|
4043
4116
|
}
|
|
4044
4117
|
function toWindow(rpc) {
|
|
4045
4118
|
if (!rpc) return void 0;
|
|
@@ -4070,9 +4143,33 @@ function toWindow(rpc) {
|
|
|
4070
4143
|
}
|
|
4071
4144
|
return window;
|
|
4072
4145
|
}
|
|
4146
|
+
function parseRateLimitSnapshotFromRpcMessage(message) {
|
|
4147
|
+
if (message.error) {
|
|
4148
|
+
throw new Error(formatRpcError("account/rateLimits/read", message.error));
|
|
4149
|
+
}
|
|
4150
|
+
const result = message.result;
|
|
4151
|
+
const limits = result?.rateLimits;
|
|
4152
|
+
const primary = toWindow(limits?.primary ?? null);
|
|
4153
|
+
const secondary = toWindow(limits?.secondary ?? null);
|
|
4154
|
+
if (!primary && !secondary) {
|
|
4155
|
+
return null;
|
|
4156
|
+
}
|
|
4157
|
+
const snapshot = {
|
|
4158
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4159
|
+
source: "codex-rpc"
|
|
4160
|
+
};
|
|
4161
|
+
if (primary) {
|
|
4162
|
+
snapshot.primary = primary;
|
|
4163
|
+
}
|
|
4164
|
+
if (secondary) {
|
|
4165
|
+
snapshot.secondary = secondary;
|
|
4166
|
+
}
|
|
4167
|
+
return snapshot;
|
|
4168
|
+
}
|
|
4073
4169
|
async function fetchRateLimitsViaRpc(envOverride, options = {}) {
|
|
4074
4170
|
const binaryPath = options.codexPath ?? await requireCodexCli();
|
|
4075
4171
|
const tempHome = await import_node_fs6.promises.mkdtemp(import_node_path8.default.join(import_node_os2.default.tmpdir(), "codex-rpc-"));
|
|
4172
|
+
const tempAuthPath = import_node_path8.default.join(tempHome, "auth.json");
|
|
4076
4173
|
const sourceAuthPath = options.authPath ?? (envOverride?.CODEX_HOME ? import_node_path8.default.join(envOverride.CODEX_HOME, "auth.json") : import_node_path8.default.join(
|
|
4077
4174
|
envOverride?.HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? import_node_os2.default.homedir(),
|
|
4078
4175
|
".codex",
|
|
@@ -4083,7 +4180,7 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
|
|
|
4083
4180
|
if (!authContent) {
|
|
4084
4181
|
return null;
|
|
4085
4182
|
}
|
|
4086
|
-
await import_node_fs6.promises.writeFile(
|
|
4183
|
+
await import_node_fs6.promises.writeFile(tempAuthPath, authContent, "utf8");
|
|
4087
4184
|
} catch {
|
|
4088
4185
|
await import_node_fs6.promises.rm(tempHome, { recursive: true, force: true }).catch(() => {
|
|
4089
4186
|
});
|
|
@@ -4111,31 +4208,24 @@ async function fetchRateLimitsViaRpc(envOverride, options = {}) {
|
|
|
4111
4208
|
method: "initialize",
|
|
4112
4209
|
params: { clientInfo: { name: "codexuse", version: "0.0.0" } }
|
|
4113
4210
|
});
|
|
4114
|
-
await
|
|
4211
|
+
const initializeResponse = await readRpcResponseById(rl, 1, RPC_TIMEOUT_MS);
|
|
4212
|
+
if (initializeResponse.error) {
|
|
4213
|
+
throw new Error(formatRpcError("initialize", initializeResponse.error));
|
|
4214
|
+
}
|
|
4115
4215
|
await sendPayload(child, { method: "initialized", params: {} });
|
|
4116
4216
|
await sendPayload(child, { id: 2, method: "account/rateLimits/read", params: {} });
|
|
4117
|
-
const message = await
|
|
4118
|
-
|
|
4119
|
-
const limits = result?.rateLimits;
|
|
4120
|
-
const primary = toWindow(limits?.primary ?? null);
|
|
4121
|
-
const secondary = toWindow(limits?.secondary ?? null);
|
|
4122
|
-
if (!primary && !secondary) {
|
|
4123
|
-
return null;
|
|
4124
|
-
}
|
|
4125
|
-
const snapshot = {
|
|
4126
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4127
|
-
source: "codex-rpc"
|
|
4128
|
-
};
|
|
4129
|
-
if (primary) {
|
|
4130
|
-
snapshot.primary = primary;
|
|
4131
|
-
}
|
|
4132
|
-
if (secondary) {
|
|
4133
|
-
snapshot.secondary = secondary;
|
|
4134
|
-
}
|
|
4135
|
-
return snapshot;
|
|
4217
|
+
const message = await readRpcResponseById(rl, 2, RPC_TIMEOUT_MS);
|
|
4218
|
+
return parseRateLimitSnapshotFromRpcMessage(message);
|
|
4136
4219
|
} finally {
|
|
4137
4220
|
child.kill();
|
|
4138
4221
|
rl.close();
|
|
4222
|
+
try {
|
|
4223
|
+
const updatedAuth = await import_node_fs6.promises.readFile(tempAuthPath, "utf8");
|
|
4224
|
+
if (updatedAuth.trim().length > 0) {
|
|
4225
|
+
await import_node_fs6.promises.writeFile(sourceAuthPath, updatedAuth, "utf8");
|
|
4226
|
+
}
|
|
4227
|
+
} catch {
|
|
4228
|
+
}
|
|
4139
4229
|
await import_node_fs6.promises.rm(tempHome, { recursive: true, force: true }).catch(() => {
|
|
4140
4230
|
});
|
|
4141
4231
|
}
|
|
@@ -4920,7 +5010,7 @@ async function importLegacyLocalStorageOnce(payload) {
|
|
|
4920
5010
|
}
|
|
4921
5011
|
|
|
4922
5012
|
// src/index.ts
|
|
4923
|
-
var VERSION = true ? "2.4.
|
|
5013
|
+
var VERSION = true ? "2.4.18" : "0.0.0";
|
|
4924
5014
|
var cliStorageReadyPromise = null;
|
|
4925
5015
|
async function ensureCliStorageReady() {
|
|
4926
5016
|
if (cliStorageReadyPromise) {
|
|
@@ -4935,7 +5025,7 @@ async function ensureCliStorageReady() {
|
|
|
4935
5025
|
const state = await getAppState();
|
|
4936
5026
|
if (state.migration.status !== "complete") {
|
|
4937
5027
|
throw new Error(
|
|
4938
|
-
`Storage migration is not complete (status: ${state.migration.status}).
|
|
5028
|
+
`Storage migration is not complete (status: ${state.migration.status}). CLI cannot continue until migration succeeds.`
|
|
4939
5029
|
);
|
|
4940
5030
|
}
|
|
4941
5031
|
})().catch((error) => {
|
|
@@ -4953,6 +5043,7 @@ Usage:
|
|
|
4953
5043
|
codexuse profile add <name> [--skip-login] [--device-auth] [--login=browser|device]
|
|
4954
5044
|
codexuse profile refresh <name> [--skip-login] [--device-auth] [--login=browser|device]
|
|
4955
5045
|
codexuse profile switch <name>
|
|
5046
|
+
codexuse profile autoroll [--threshold=50-100] [--dry-run] [--watch] [--interval=seconds]
|
|
4956
5047
|
codexuse profile delete <name>
|
|
4957
5048
|
codexuse profile rename <old> <new>
|
|
4958
5049
|
|
|
@@ -4970,6 +5061,10 @@ Flags:
|
|
|
4970
5061
|
--compact Names only
|
|
4971
5062
|
--device-auth Use device auth for Codex login
|
|
4972
5063
|
--login=MODE Login mode: browser | device
|
|
5064
|
+
--threshold=NN Auto-roll switch threshold percent (50-100)
|
|
5065
|
+
--watch Keep checking and auto-switch when threshold is reached
|
|
5066
|
+
--interval=SEC Watch interval in seconds (default: 30)
|
|
5067
|
+
--dry-run Print planned switch without changing active profile
|
|
4973
5068
|
`);
|
|
4974
5069
|
}
|
|
4975
5070
|
function hasFlag(args, flag) {
|
|
@@ -4988,6 +5083,26 @@ function parseLoginMode(flags) {
|
|
|
4988
5083
|
}
|
|
4989
5084
|
return null;
|
|
4990
5085
|
}
|
|
5086
|
+
var DEFAULT_AUTOROLL_INTERVAL_SECONDS = 30;
|
|
5087
|
+
var DEFAULT_AUTOROLL_THRESHOLD = 95;
|
|
5088
|
+
function parseNumericFlag(flags, name) {
|
|
5089
|
+
const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
|
|
5090
|
+
if (!explicit) {
|
|
5091
|
+
return null;
|
|
5092
|
+
}
|
|
5093
|
+
const raw = explicit.slice(`${name}=`.length);
|
|
5094
|
+
const value = Number.parseFloat(raw);
|
|
5095
|
+
return Number.isFinite(value) ? value : Number.NaN;
|
|
5096
|
+
}
|
|
5097
|
+
function parseIntegerFlag(flags, name) {
|
|
5098
|
+
const explicit = flags.find((flag) => flag.startsWith(`${name}=`));
|
|
5099
|
+
if (!explicit) {
|
|
5100
|
+
return null;
|
|
5101
|
+
}
|
|
5102
|
+
const raw = explicit.slice(`${name}=`.length);
|
|
5103
|
+
const value = Number.parseInt(raw, 10);
|
|
5104
|
+
return Number.isFinite(value) ? value : Number.NaN;
|
|
5105
|
+
}
|
|
4991
5106
|
function formatProfileLabel(name, displayName) {
|
|
4992
5107
|
if (displayName && displayName.trim() && displayName !== name) {
|
|
4993
5108
|
return `${displayName} (${name})`;
|
|
@@ -5099,14 +5214,99 @@ function formatWindowUsage(window) {
|
|
|
5099
5214
|
reset: resetText ? `reset ${resetText}` : null
|
|
5100
5215
|
};
|
|
5101
5216
|
}
|
|
5217
|
+
var EMPTY_USAGE_SUMMARY = {
|
|
5218
|
+
windowKey: "primary",
|
|
5219
|
+
usagePercent: 0,
|
|
5220
|
+
hasUsage: false,
|
|
5221
|
+
resetsInSeconds: null,
|
|
5222
|
+
windowMinutes: null
|
|
5223
|
+
};
|
|
5224
|
+
function toFiniteNumberOrNull(value) {
|
|
5225
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
5226
|
+
}
|
|
5227
|
+
function summarizeRateLimitSnapshot(snapshot) {
|
|
5228
|
+
if (!snapshot) {
|
|
5229
|
+
return { ...EMPTY_USAGE_SUMMARY };
|
|
5230
|
+
}
|
|
5231
|
+
const entries = [
|
|
5232
|
+
{ key: "primary", window: snapshot.primary },
|
|
5233
|
+
{ key: "secondary", window: snapshot.secondary }
|
|
5234
|
+
];
|
|
5235
|
+
let summary = { ...EMPTY_USAGE_SUMMARY };
|
|
5236
|
+
let initialized = false;
|
|
5237
|
+
for (const entry of entries) {
|
|
5238
|
+
if (!entry.window) {
|
|
5239
|
+
continue;
|
|
5240
|
+
}
|
|
5241
|
+
const usageValueRaw = toFiniteNumberOrNull(entry.window.usedPercent);
|
|
5242
|
+
const usageValue = usageValueRaw === null ? 0 : Math.max(0, Math.min(100, usageValueRaw));
|
|
5243
|
+
const hasUsage = usageValueRaw !== null;
|
|
5244
|
+
const resetsInSeconds = toFiniteNumberOrNull(entry.window.resetsInSeconds);
|
|
5245
|
+
const windowMinutes = toFiniteNumberOrNull(entry.window.windowMinutes);
|
|
5246
|
+
if (!initialized) {
|
|
5247
|
+
initialized = true;
|
|
5248
|
+
summary = {
|
|
5249
|
+
windowKey: entry.key,
|
|
5250
|
+
usagePercent: usageValue,
|
|
5251
|
+
hasUsage,
|
|
5252
|
+
resetsInSeconds,
|
|
5253
|
+
windowMinutes
|
|
5254
|
+
};
|
|
5255
|
+
continue;
|
|
5256
|
+
}
|
|
5257
|
+
if (hasUsage && summary.hasUsage && usageValue > summary.usagePercent) {
|
|
5258
|
+
summary = {
|
|
5259
|
+
windowKey: entry.key,
|
|
5260
|
+
usagePercent: usageValue,
|
|
5261
|
+
hasUsage,
|
|
5262
|
+
resetsInSeconds,
|
|
5263
|
+
windowMinutes
|
|
5264
|
+
};
|
|
5265
|
+
continue;
|
|
5266
|
+
}
|
|
5267
|
+
if (!hasUsage && !summary.hasUsage && summary.resetsInSeconds !== null) {
|
|
5268
|
+
if (resetsInSeconds !== null && resetsInSeconds < summary.resetsInSeconds) {
|
|
5269
|
+
summary = {
|
|
5270
|
+
windowKey: entry.key,
|
|
5271
|
+
usagePercent: usageValue,
|
|
5272
|
+
hasUsage,
|
|
5273
|
+
resetsInSeconds,
|
|
5274
|
+
windowMinutes
|
|
5275
|
+
};
|
|
5276
|
+
}
|
|
5277
|
+
continue;
|
|
5278
|
+
}
|
|
5279
|
+
if (!hasUsage && !summary.hasUsage && summary.resetsInSeconds === null && resetsInSeconds !== null) {
|
|
5280
|
+
summary = {
|
|
5281
|
+
windowKey: entry.key,
|
|
5282
|
+
usagePercent: usageValue,
|
|
5283
|
+
hasUsage,
|
|
5284
|
+
resetsInSeconds,
|
|
5285
|
+
windowMinutes
|
|
5286
|
+
};
|
|
5287
|
+
continue;
|
|
5288
|
+
}
|
|
5289
|
+
if (hasUsage && !summary.hasUsage) {
|
|
5290
|
+
summary = {
|
|
5291
|
+
windowKey: entry.key,
|
|
5292
|
+
usagePercent: usageValue,
|
|
5293
|
+
hasUsage,
|
|
5294
|
+
resetsInSeconds,
|
|
5295
|
+
windowMinutes
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
return summary;
|
|
5300
|
+
}
|
|
5102
5301
|
function fallbackUsage(profile) {
|
|
5302
|
+
const usageSummary = summarizeRateLimitSnapshot(profile.rateLimit ?? null);
|
|
5103
5303
|
if (!profile.isValid) {
|
|
5104
|
-
return { summary: "invalid", rows: null };
|
|
5304
|
+
return { summary: "invalid", rows: null, usageSummary };
|
|
5105
5305
|
}
|
|
5106
5306
|
if (profile.tokenStatus?.requiresUserAction) {
|
|
5107
|
-
return { summary: "auth required", rows: null };
|
|
5307
|
+
return { summary: "auth required", rows: null, usageSummary };
|
|
5108
5308
|
}
|
|
5109
|
-
return { summary: "n/a", rows: null };
|
|
5309
|
+
return { summary: "n/a", rows: null, usageSummary };
|
|
5110
5310
|
}
|
|
5111
5311
|
async function resolveProfileUsage(manager, profile, codexPath) {
|
|
5112
5312
|
if (!profile.isValid || profile.tokenStatus?.requiresUserAction) {
|
|
@@ -5118,7 +5318,8 @@ async function resolveProfileUsage(manager, profile, codexPath) {
|
|
|
5118
5318
|
(env) => fetchRateLimitsViaRpc(env, { codexPath })
|
|
5119
5319
|
);
|
|
5120
5320
|
if (!snapshot) {
|
|
5121
|
-
|
|
5321
|
+
const usageSummary = summarizeRateLimitSnapshot(profile.rateLimit ?? null);
|
|
5322
|
+
return { summary: "n/a", rows: null, usageSummary };
|
|
5122
5323
|
}
|
|
5123
5324
|
const rows = [];
|
|
5124
5325
|
if (snapshot.primary) {
|
|
@@ -5131,10 +5332,61 @@ async function resolveProfileUsage(manager, profile, codexPath) {
|
|
|
5131
5332
|
}
|
|
5132
5333
|
const maxPercent = maxUsedPercent(snapshot);
|
|
5133
5334
|
const summary = typeof maxPercent === "number" ? `${Math.round(maxPercent)}%` : "n/a";
|
|
5134
|
-
return { summary, rows: rows.length > 0 ? rows : null };
|
|
5335
|
+
return { summary, rows: rows.length > 0 ? rows : null, usageSummary: summarizeRateLimitSnapshot(snapshot) };
|
|
5135
5336
|
} catch {
|
|
5136
|
-
return
|
|
5337
|
+
return fallbackUsage(profile);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
function compareAutoRollCandidates(a, b) {
|
|
5341
|
+
if (a.usageSummary.usagePercent !== b.usageSummary.usagePercent) {
|
|
5342
|
+
return a.usageSummary.usagePercent - b.usageSummary.usagePercent;
|
|
5343
|
+
}
|
|
5344
|
+
const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5345
|
+
const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5346
|
+
if (aReset !== bReset) {
|
|
5347
|
+
return aReset - bReset;
|
|
5348
|
+
}
|
|
5349
|
+
return a.profile.name.localeCompare(b.profile.name);
|
|
5350
|
+
}
|
|
5351
|
+
function computeAutoRollCandidate(profiles, usageMap, currentProfileName, currentSummary, switchThreshold) {
|
|
5352
|
+
const candidates = profiles.filter((profile) => profile.name !== currentProfileName && profile.isValid && !profile.tokenStatus?.requiresUserAction).map((profile) => ({
|
|
5353
|
+
profile,
|
|
5354
|
+
usageSummary: usageMap.get(profile.name)?.usageSummary ?? summarizeRateLimitSnapshot(profile.rateLimit ?? null)
|
|
5355
|
+
}));
|
|
5356
|
+
if (candidates.length === 0) {
|
|
5357
|
+
return null;
|
|
5137
5358
|
}
|
|
5359
|
+
const belowThreshold = candidates.filter((candidate) => candidate.usageSummary.hasUsage && candidate.usageSummary.usagePercent < switchThreshold).sort(compareAutoRollCandidates);
|
|
5360
|
+
let selected = belowThreshold[0] ?? null;
|
|
5361
|
+
if (!selected) {
|
|
5362
|
+
selected = candidates.filter((candidate) => candidate.usageSummary.hasUsage).sort(compareAutoRollCandidates)[0] ?? null;
|
|
5363
|
+
}
|
|
5364
|
+
if (!selected) {
|
|
5365
|
+
selected = candidates.filter((candidate) => !candidate.usageSummary.hasUsage).sort((a, b) => {
|
|
5366
|
+
const aReset = a.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5367
|
+
const bReset = b.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5368
|
+
if (aReset !== bReset) {
|
|
5369
|
+
return aReset - bReset;
|
|
5370
|
+
}
|
|
5371
|
+
return a.profile.name.localeCompare(b.profile.name);
|
|
5372
|
+
})[0] ?? null;
|
|
5373
|
+
}
|
|
5374
|
+
if (!selected) {
|
|
5375
|
+
return null;
|
|
5376
|
+
}
|
|
5377
|
+
if (selected.usageSummary.hasUsage && currentSummary.hasUsage) {
|
|
5378
|
+
if (selected.usageSummary.usagePercent > currentSummary.usagePercent) {
|
|
5379
|
+
return null;
|
|
5380
|
+
}
|
|
5381
|
+
if (selected.usageSummary.usagePercent === currentSummary.usagePercent) {
|
|
5382
|
+
const selectedReset = selected.usageSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5383
|
+
const currentReset = currentSummary.resetsInSeconds ?? Number.POSITIVE_INFINITY;
|
|
5384
|
+
if (selectedReset >= currentReset) {
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
return selected;
|
|
5138
5390
|
}
|
|
5139
5391
|
function formatPlan(profile) {
|
|
5140
5392
|
const metadata = profile.metadata;
|
|
@@ -5210,6 +5462,118 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
5210
5462
|
await Promise.all(workers);
|
|
5211
5463
|
return results;
|
|
5212
5464
|
}
|
|
5465
|
+
function parseAutoRollThreshold(flags, fallbackThreshold) {
|
|
5466
|
+
const explicitThreshold = parseNumericFlag(flags, "--threshold");
|
|
5467
|
+
if (explicitThreshold === null) {
|
|
5468
|
+
return fallbackThreshold;
|
|
5469
|
+
}
|
|
5470
|
+
if (Number.isNaN(explicitThreshold)) {
|
|
5471
|
+
throw new Error("Invalid --threshold value. Use --threshold=50-100.");
|
|
5472
|
+
}
|
|
5473
|
+
if (explicitThreshold < 50 || explicitThreshold > 100) {
|
|
5474
|
+
throw new Error("Invalid --threshold value. Use a number between 50 and 100.");
|
|
5475
|
+
}
|
|
5476
|
+
return Math.round(explicitThreshold);
|
|
5477
|
+
}
|
|
5478
|
+
function parseAutoRollIntervalSeconds(flags) {
|
|
5479
|
+
const explicitInterval = parseIntegerFlag(flags, "--interval");
|
|
5480
|
+
if (explicitInterval === null) {
|
|
5481
|
+
return DEFAULT_AUTOROLL_INTERVAL_SECONDS;
|
|
5482
|
+
}
|
|
5483
|
+
if (Number.isNaN(explicitInterval) || explicitInterval <= 0) {
|
|
5484
|
+
throw new Error("Invalid --interval value. Use --interval=<seconds> with a positive integer.");
|
|
5485
|
+
}
|
|
5486
|
+
return explicitInterval;
|
|
5487
|
+
}
|
|
5488
|
+
async function collectUsageMap(manager, profiles, codexPath) {
|
|
5489
|
+
const usageMap = /* @__PURE__ */ new Map();
|
|
5490
|
+
const limitEnv = Number.parseInt(process.env.CODEXUSE_CLI_USAGE_CONCURRENCY ?? "3", 10);
|
|
5491
|
+
const limit = Number.isFinite(limitEnv) && limitEnv > 0 ? limitEnv : 3;
|
|
5492
|
+
const results = await mapWithConcurrency(
|
|
5493
|
+
profiles,
|
|
5494
|
+
limit,
|
|
5495
|
+
(profile) => resolveProfileUsage(manager, profile, codexPath)
|
|
5496
|
+
);
|
|
5497
|
+
for (const [index, profile] of profiles.entries()) {
|
|
5498
|
+
usageMap.set(profile.name, results[index]);
|
|
5499
|
+
}
|
|
5500
|
+
return usageMap;
|
|
5501
|
+
}
|
|
5502
|
+
async function runAutoRollPass(manager, options) {
|
|
5503
|
+
const profiles = await manager.listProfiles();
|
|
5504
|
+
if (!profiles.length) {
|
|
5505
|
+
return { switched: false, message: "No profiles found.", source: null, target: null };
|
|
5506
|
+
}
|
|
5507
|
+
const current = await manager.getCurrentProfile();
|
|
5508
|
+
if (!current.name) {
|
|
5509
|
+
return { switched: false, message: "No active profile.", source: null, target: null };
|
|
5510
|
+
}
|
|
5511
|
+
const codexPath = resolveCodexBinary();
|
|
5512
|
+
if (!codexPath) {
|
|
5513
|
+
throw new Error("Codex CLI binary not found on PATH. Auto-roll requires codex for live rate limits.");
|
|
5514
|
+
}
|
|
5515
|
+
const usageMap = await collectUsageMap(manager, profiles, codexPath);
|
|
5516
|
+
const currentProfile = profiles.find((profile) => profile.name === current.name);
|
|
5517
|
+
if (!currentProfile) {
|
|
5518
|
+
return {
|
|
5519
|
+
switched: false,
|
|
5520
|
+
message: `Current profile '${current.name}' is missing from saved profiles.`,
|
|
5521
|
+
source: current.name,
|
|
5522
|
+
target: null
|
|
5523
|
+
};
|
|
5524
|
+
}
|
|
5525
|
+
const currentUsage = usageMap.get(current.name)?.usageSummary ?? summarizeRateLimitSnapshot(currentProfile.rateLimit ?? null);
|
|
5526
|
+
if (!currentUsage.hasUsage) {
|
|
5527
|
+
return {
|
|
5528
|
+
switched: false,
|
|
5529
|
+
message: `No live rate-limit usage for '${current.name}'. Skipping auto-roll.`,
|
|
5530
|
+
source: current.name,
|
|
5531
|
+
target: null
|
|
5532
|
+
};
|
|
5533
|
+
}
|
|
5534
|
+
if (currentUsage.usagePercent < options.threshold) {
|
|
5535
|
+
return {
|
|
5536
|
+
switched: false,
|
|
5537
|
+
message: `'${current.name}' at ${Math.round(currentUsage.usagePercent)}%, below threshold ${options.threshold}%.`,
|
|
5538
|
+
source: current.name,
|
|
5539
|
+
target: null
|
|
5540
|
+
};
|
|
5541
|
+
}
|
|
5542
|
+
const candidate = computeAutoRollCandidate(
|
|
5543
|
+
profiles,
|
|
5544
|
+
usageMap,
|
|
5545
|
+
current.name,
|
|
5546
|
+
currentUsage,
|
|
5547
|
+
options.threshold
|
|
5548
|
+
);
|
|
5549
|
+
if (!candidate) {
|
|
5550
|
+
return {
|
|
5551
|
+
switched: false,
|
|
5552
|
+
message: `No eligible profile to switch from '${current.name}' at ${Math.round(currentUsage.usagePercent)}%.`,
|
|
5553
|
+
source: current.name,
|
|
5554
|
+
target: null
|
|
5555
|
+
};
|
|
5556
|
+
}
|
|
5557
|
+
const candidateUsage = candidate.usageSummary.hasUsage ? `${Math.round(candidate.usageSummary.usagePercent)}%` : "n/a";
|
|
5558
|
+
if (options.dryRun) {
|
|
5559
|
+
return {
|
|
5560
|
+
switched: false,
|
|
5561
|
+
message: `[dry-run] Would switch '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
|
|
5562
|
+
source: current.name,
|
|
5563
|
+
target: candidate.profile.name
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
await manager.switchToProfile(candidate.profile.name);
|
|
5567
|
+
return {
|
|
5568
|
+
switched: true,
|
|
5569
|
+
message: `Auto-rolled '${current.name}' (${Math.round(currentUsage.usagePercent)}%) -> '${candidate.profile.name}' (${candidateUsage}).`,
|
|
5570
|
+
source: current.name,
|
|
5571
|
+
target: candidate.profile.name
|
|
5572
|
+
};
|
|
5573
|
+
}
|
|
5574
|
+
function delay(ms) {
|
|
5575
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5576
|
+
}
|
|
5213
5577
|
async function handleProfile(args) {
|
|
5214
5578
|
const flags = args.filter((arg) => arg.startsWith("-"));
|
|
5215
5579
|
const params = stripFlags(args);
|
|
@@ -5231,24 +5595,18 @@ async function handleProfile(args) {
|
|
|
5231
5595
|
const withUsage = !hasFlag(flags, "--no-usage");
|
|
5232
5596
|
let usageMap = null;
|
|
5233
5597
|
if (withUsage) {
|
|
5234
|
-
usageMap = /* @__PURE__ */ new Map();
|
|
5235
5598
|
const codexPath = resolveCodexBinary();
|
|
5236
|
-
|
|
5237
|
-
|
|
5599
|
+
if (!codexPath) {
|
|
5600
|
+
usageMap = /* @__PURE__ */ new Map();
|
|
5238
5601
|
for (const profile of profiles) {
|
|
5239
|
-
usageMap.set(profile.name, {
|
|
5602
|
+
usageMap.set(profile.name, {
|
|
5603
|
+
summary: "codex cli missing",
|
|
5604
|
+
rows: null,
|
|
5605
|
+
usageSummary: summarizeRateLimitSnapshot(profile.rateLimit ?? null)
|
|
5606
|
+
});
|
|
5240
5607
|
}
|
|
5241
5608
|
} else {
|
|
5242
|
-
|
|
5243
|
-
const limit = Number.isFinite(limitEnv) && limitEnv > 0 ? limitEnv : 3;
|
|
5244
|
-
const results = await mapWithConcurrency(
|
|
5245
|
-
profiles,
|
|
5246
|
-
limit,
|
|
5247
|
-
(profile) => resolveProfileUsage(manager, profile, codexPath)
|
|
5248
|
-
);
|
|
5249
|
-
for (const [index, profile] of profiles.entries()) {
|
|
5250
|
-
usageMap.set(profile.name, results[index]);
|
|
5251
|
-
}
|
|
5609
|
+
usageMap = await collectUsageMap(manager, profiles, codexPath);
|
|
5252
5610
|
}
|
|
5253
5611
|
}
|
|
5254
5612
|
for (const [index, profile] of profiles.entries()) {
|
|
@@ -5312,6 +5670,38 @@ async function handleProfile(args) {
|
|
|
5312
5670
|
console.log(`Switched to profile: ${name}`);
|
|
5313
5671
|
return;
|
|
5314
5672
|
}
|
|
5673
|
+
case "autoroll":
|
|
5674
|
+
case "auto-roll": {
|
|
5675
|
+
const watch = hasFlag(flags, "--watch");
|
|
5676
|
+
const dryRun = hasFlag(flags, "--dry-run");
|
|
5677
|
+
const settings = await getStoredAutoRollSettings().catch(() => null);
|
|
5678
|
+
const fallbackThreshold = typeof settings?.switchThreshold === "number" && Number.isFinite(settings.switchThreshold) ? Math.round(settings.switchThreshold) : DEFAULT_AUTOROLL_THRESHOLD;
|
|
5679
|
+
const threshold = parseAutoRollThreshold(flags, fallbackThreshold);
|
|
5680
|
+
const intervalSeconds = parseAutoRollIntervalSeconds(flags);
|
|
5681
|
+
if (watch) {
|
|
5682
|
+
console.log(`Auto-roll watch: threshold ${threshold}% | interval ${intervalSeconds}s${dryRun ? " | dry-run" : ""}`);
|
|
5683
|
+
let stop = false;
|
|
5684
|
+
process.on("SIGINT", () => {
|
|
5685
|
+
stop = true;
|
|
5686
|
+
});
|
|
5687
|
+
process.on("SIGTERM", () => {
|
|
5688
|
+
stop = true;
|
|
5689
|
+
});
|
|
5690
|
+
while (!stop) {
|
|
5691
|
+
const result2 = await runAutoRollPass(manager, { threshold, dryRun });
|
|
5692
|
+
console.log(result2.message);
|
|
5693
|
+
if (stop) {
|
|
5694
|
+
break;
|
|
5695
|
+
}
|
|
5696
|
+
await delay(intervalSeconds * 1e3);
|
|
5697
|
+
}
|
|
5698
|
+
console.log("Auto-roll watch stopped.");
|
|
5699
|
+
return;
|
|
5700
|
+
}
|
|
5701
|
+
const result = await runAutoRollPass(manager, { threshold, dryRun });
|
|
5702
|
+
console.log(result.message);
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5315
5705
|
case "delete": {
|
|
5316
5706
|
const name = params[1];
|
|
5317
5707
|
if (!name) {
|