@openxiaobu/codexl 0.1.6 → 0.1.8
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 +25 -9
- package/dist/cli.js +133 -41
- package/dist/server.js +1 -1
- package/dist/status.js +19 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
- Reuse the official `~/.codex` login state
|
|
10
10
|
- Manage multiple accounts or workspaces as separate slots
|
|
11
|
-
-
|
|
11
|
+
- Refresh and cache the latest usage from the official usage endpoint
|
|
12
12
|
- Expose a local provider endpoint for Codex
|
|
13
13
|
- Apply local block rules for temporary, 5-hour, and weekly limits
|
|
14
|
-
- Automatically switch `~/.codex/config.toml` to the `codexl` provider while the local proxy is running
|
|
14
|
+
- Automatically switch `~/.codex/config.toml` to the `codexl` provider while the local proxy is running (and restore it on stop)
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -44,6 +44,25 @@ Check latest usage:
|
|
|
44
44
|
codexl status
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
By default, `status` will:
|
|
48
|
+
|
|
49
|
+
- Refresh usage for all managed accounts
|
|
50
|
+
- Render a compact table with:
|
|
51
|
+
- Remaining 5-hour / weekly quotas
|
|
52
|
+
- Reset times
|
|
53
|
+
- A status column with local block reasons and countdowns (for example: `5h_limited(2h27m)`)
|
|
54
|
+
- Enter an interactive mode where you can toggle `enabled` for accounts:
|
|
55
|
+
- Up/Down: move selection
|
|
56
|
+
- Space: toggle `[x]` enabled / `[ ]` disabled
|
|
57
|
+
- Enter: confirm and save
|
|
58
|
+
- `q`: quit
|
|
59
|
+
|
|
60
|
+
If you only want a non-interactive snapshot of the current state:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
codexl status --no-interactive
|
|
64
|
+
```
|
|
65
|
+
|
|
47
66
|
Start the local proxy:
|
|
48
67
|
|
|
49
68
|
```bash
|
|
@@ -81,24 +100,21 @@ Instead it:
|
|
|
81
100
|
|
|
82
101
|
## Managed Codex Config
|
|
83
102
|
|
|
84
|
-
`codexl start` writes a
|
|
103
|
+
`codexl start` writes or updates a provider block like this, based on the current `~/.codexl/config.yaml`:
|
|
85
104
|
|
|
86
105
|
```toml
|
|
87
|
-
# >>> codexl managed start >>>
|
|
88
106
|
[model_providers.codexl]
|
|
89
107
|
name = "codexl"
|
|
90
108
|
base_url = "http://127.0.0.1:4389/v1"
|
|
91
109
|
http_headers = { Authorization = "Bearer codexl-defaultkey" }
|
|
92
110
|
wire_api = "responses"
|
|
93
|
-
# <<< codexl managed end <<<
|
|
94
111
|
```
|
|
95
112
|
|
|
96
113
|
Behavior:
|
|
97
114
|
|
|
98
|
-
- If `
|
|
99
|
-
- If
|
|
100
|
-
-
|
|
101
|
-
- Global `model` is kept unchanged
|
|
115
|
+
- If global `model_provider` or `# model_provider = ...` exists, it is normalized to `model_provider = "codexl"`
|
|
116
|
+
- If `[model_providers.codexl]` already exists, only that provider block is replaced with the fresh one above
|
|
117
|
+
- Other providers and settings in `config.toml` are left untouched
|
|
102
118
|
- If you start with `--port`, the port is saved to `~/.codexl/config.yaml`
|
|
103
119
|
- `codexl stop` comments out the active `model_provider = "codexl"` line and keeps the rest of the file unchanged
|
|
104
120
|
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
10
11
|
const commander_1 = require("commander");
|
|
11
12
|
const account_store_1 = require("./account-store");
|
|
12
13
|
const config_1 = require("./config");
|
|
@@ -39,7 +40,7 @@ function getCliVersion() {
|
|
|
39
40
|
*
|
|
40
41
|
* @returns Promise,无返回值。
|
|
41
42
|
*/
|
|
42
|
-
async function handleStatus() {
|
|
43
|
+
async function handleStatus(options) {
|
|
43
44
|
await (0, usage_sync_1.refreshAllAccountUsage)();
|
|
44
45
|
const statuses = (0, status_1.collectAccountStatuses)();
|
|
45
46
|
console.log((0, status_1.renderStatusTable)(statuses));
|
|
@@ -48,6 +49,91 @@ async function handleStatus() {
|
|
|
48
49
|
const weeklyLimited = statuses.filter((item) => item.isWeeklyLimited).length;
|
|
49
50
|
console.log("");
|
|
50
51
|
console.log(`available=${available} 5h_limited=${fiveHourLimited} weekly_limited=${weeklyLimited}`);
|
|
52
|
+
const interactive = options?.interactive ?? true;
|
|
53
|
+
if (interactive) {
|
|
54
|
+
await handleInteractiveToggle();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function handleInteractiveToggle() {
|
|
58
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
59
|
+
console.log("当前环境不支持交互式操作,请直接编辑配置文件或使用 --no-interactive 选项。");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const stdin = process.stdin;
|
|
63
|
+
node_readline_1.default.emitKeypressEvents(stdin);
|
|
64
|
+
stdin.setRawMode?.(true);
|
|
65
|
+
const config = (0, config_1.loadConfig)();
|
|
66
|
+
if (config.accounts.length === 0) {
|
|
67
|
+
console.log("当前没有已录入账号。");
|
|
68
|
+
stdin.setRawMode?.(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// 按名称排序,方便浏览。
|
|
72
|
+
const accounts = [...config.accounts].sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
let changed = false;
|
|
75
|
+
const render = () => {
|
|
76
|
+
// 清屏并将光标移动到左上角。
|
|
77
|
+
process.stdout.write("\x1b[2J\x1b[0f");
|
|
78
|
+
console.log("空格切换选中账号启用状态,回车确认,q 退出。\n");
|
|
79
|
+
for (let i = 0; i < accounts.length; i += 1) {
|
|
80
|
+
const account = accounts[i];
|
|
81
|
+
const prefix = i === cursor ? ">" : " ";
|
|
82
|
+
const checkbox = account.enabled ? "[x]" : "[ ]";
|
|
83
|
+
console.log(`${prefix} ${checkbox} ${account.name} (${account.codex_home})`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const applyChanges = () => {
|
|
87
|
+
if (!changed)
|
|
88
|
+
return;
|
|
89
|
+
const latest = (0, config_1.loadConfig)();
|
|
90
|
+
for (const account of accounts) {
|
|
91
|
+
const index = latest.accounts.findIndex((item) => item.id === account.id);
|
|
92
|
+
if (index >= 0) {
|
|
93
|
+
latest.accounts[index] = account;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
(0, config_1.saveConfig)(latest);
|
|
97
|
+
};
|
|
98
|
+
render();
|
|
99
|
+
const onKeypress = (_str, key) => {
|
|
100
|
+
if (key.name === "up") {
|
|
101
|
+
cursor = (cursor - 1 + accounts.length) % accounts.length;
|
|
102
|
+
render();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (key.name === "down") {
|
|
106
|
+
cursor = (cursor + 1) % accounts.length;
|
|
107
|
+
render();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (key.name === "space") {
|
|
111
|
+
accounts[cursor].enabled = !accounts[cursor].enabled;
|
|
112
|
+
changed = true;
|
|
113
|
+
render();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.name === "return" || key.name === "enter") {
|
|
117
|
+
applyChanges();
|
|
118
|
+
stdin.off("keypress", onKeypress);
|
|
119
|
+
stdin.setRawMode?.(false);
|
|
120
|
+
console.log("\n已保存账号启用状态变更。");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
124
|
+
stdin.off("keypress", onKeypress);
|
|
125
|
+
stdin.setRawMode?.(false);
|
|
126
|
+
if (changed) {
|
|
127
|
+
applyChanges();
|
|
128
|
+
console.log("\n已保存账号启用状态变更。");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log("\n未做任何变更。");
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
stdin.on("keypress", onKeypress);
|
|
51
137
|
}
|
|
52
138
|
/**
|
|
53
139
|
* 将已有的 Codex HOME 目录中的登录态复制到 codexl 自己的隔离目录并纳入管理。
|
|
@@ -175,6 +261,15 @@ function handleStop() {
|
|
|
175
261
|
function escapeRegExp(input) {
|
|
176
262
|
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
263
|
}
|
|
264
|
+
function ensureParentDir(filePath) {
|
|
265
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
function writeFileAtomic(targetFile, content) {
|
|
268
|
+
ensureParentDir(targetFile);
|
|
269
|
+
const tmpFile = `${targetFile}.tmp-${process.pid}-${Date.now()}`;
|
|
270
|
+
node_fs_1.default.writeFileSync(tmpFile, content, "utf8");
|
|
271
|
+
node_fs_1.default.renameSync(tmpFile, targetFile);
|
|
272
|
+
}
|
|
178
273
|
/**
|
|
179
274
|
* 返回默认的 `codex config.toml` 路径。
|
|
180
275
|
*
|
|
@@ -184,22 +279,18 @@ function getDefaultCodexConfigPath() {
|
|
|
184
279
|
return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
|
|
185
280
|
}
|
|
186
281
|
/**
|
|
187
|
-
* 生成 codexl
|
|
282
|
+
* 生成 codexl provider 配置块。
|
|
188
283
|
*
|
|
189
284
|
* @returns 可直接写入 `config.toml` 的配置块内容。
|
|
190
285
|
*/
|
|
191
286
|
function buildManagedConfigBlock() {
|
|
192
287
|
const config = (0, config_1.loadConfig)();
|
|
193
|
-
const startMarker = "# >>> codexl managed start >>>";
|
|
194
|
-
const endMarker = "# <<< codexl managed end <<<";
|
|
195
288
|
return [
|
|
196
|
-
startMarker,
|
|
197
289
|
"[model_providers.codexl]",
|
|
198
290
|
'name = "codexl"',
|
|
199
291
|
`base_url = "http://${config.server.host}:${config.server.port}/v1"`,
|
|
200
292
|
`http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
|
|
201
|
-
'wire_api = "responses"'
|
|
202
|
-
endMarker
|
|
293
|
+
'wire_api = "responses"'
|
|
203
294
|
].join("\n");
|
|
204
295
|
}
|
|
205
296
|
/**
|
|
@@ -211,73 +302,71 @@ function buildManagedConfigBlock() {
|
|
|
211
302
|
function applyManagedCodexConfig(targetPathOrDir, options) {
|
|
212
303
|
const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
|
|
213
304
|
const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
|
|
214
|
-
const startMarker = "# >>> codexl managed start >>>";
|
|
215
|
-
const endMarker = "# <<< codexl managed end <<<";
|
|
216
305
|
const block = buildManagedConfigBlock();
|
|
217
306
|
let original = "";
|
|
218
307
|
if (node_fs_1.default.existsSync(targetFile)) {
|
|
219
308
|
original = node_fs_1.default.readFileSync(targetFile, "utf8");
|
|
220
309
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
const lines = original.replace(managedBlockPattern, "").split(/\r?\n/);
|
|
226
|
-
let insertAfterIndex = -1;
|
|
227
|
-
let hasGlobalModelProvider = false;
|
|
310
|
+
// 更稳定的策略:仅按 provider 名称和 model_provider 改动,不引入额外的 marker。
|
|
311
|
+
const lines = original.length > 0 ? original.split(/\r?\n/) : [];
|
|
312
|
+
let replacedModelProvider = false;
|
|
313
|
+
const modelProviderLine = 'model_provider = "codexl"';
|
|
228
314
|
for (let i = 0; i < lines.length; i += 1) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
if (/^#\s*model_provider\s*=/.test(trimmed)) {
|
|
232
|
-
lines[i] =
|
|
233
|
-
|
|
315
|
+
const trimmed = lines[i].trim();
|
|
316
|
+
// 优先替换被注释掉的默认行(只替换第一处,减少对用户文件的干扰)。
|
|
317
|
+
if (!replacedModelProvider && /^#\s*model_provider\s*=/.test(trimmed)) {
|
|
318
|
+
lines[i] = modelProviderLine;
|
|
319
|
+
replacedModelProvider = true;
|
|
234
320
|
continue;
|
|
235
321
|
}
|
|
236
|
-
|
|
322
|
+
// 替换真实生效的 model_provider 行。
|
|
323
|
+
if (/^model_provider\s*=/.test(trimmed) && !trimmed.startsWith("#")) {
|
|
324
|
+
lines[i] = modelProviderLine;
|
|
325
|
+
replacedModelProvider = true;
|
|
237
326
|
continue;
|
|
238
327
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
328
|
+
}
|
|
329
|
+
if (!replacedModelProvider) {
|
|
330
|
+
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
331
|
+
if (firstNonEmptyIndex >= 0) {
|
|
332
|
+
lines.splice(firstNonEmptyIndex, 0, modelProviderLine, "");
|
|
243
333
|
}
|
|
334
|
+
else {
|
|
335
|
+
lines.push(modelProviderLine, "");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 替换或追加 [model_providers.codexl] 这一段配置。
|
|
339
|
+
const blockLines = block.split("\n");
|
|
340
|
+
let insertAfterIndex = -1;
|
|
341
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
342
|
+
const trimmed = lines[i].trim();
|
|
244
343
|
if (trimmed === "[model_providers.codexl]") {
|
|
245
344
|
let j = i;
|
|
246
345
|
while (j < lines.length) {
|
|
247
|
-
const
|
|
248
|
-
const currentTrimmed = current.trim();
|
|
346
|
+
const currentTrimmed = lines[j].trim();
|
|
249
347
|
if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
|
|
250
348
|
break;
|
|
251
349
|
}
|
|
252
350
|
insertAfterIndex = j;
|
|
253
351
|
j += 1;
|
|
254
352
|
}
|
|
353
|
+
// 删除旧的 codexl provider 表块,准备写入新的。
|
|
255
354
|
lines.splice(i, j - i);
|
|
256
355
|
insertAfterIndex = i - 1;
|
|
257
356
|
i = j - 1;
|
|
258
357
|
}
|
|
259
358
|
}
|
|
260
|
-
const blockLines = block.split("\n");
|
|
261
359
|
if (insertAfterIndex >= 0) {
|
|
262
360
|
lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
|
|
263
361
|
}
|
|
264
362
|
else {
|
|
265
|
-
if (!hasGlobalModelProvider) {
|
|
266
|
-
const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
|
|
267
|
-
if (firstNonEmptyIndex >= 0) {
|
|
268
|
-
lines.splice(firstNonEmptyIndex, 0, 'model_provider = "codexl"', "");
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
lines.push('model_provider = "codexl"', "");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
363
|
if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
|
|
275
364
|
lines.push("");
|
|
276
365
|
}
|
|
277
366
|
lines.push(...blockLines);
|
|
278
367
|
}
|
|
279
368
|
const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
|
|
280
|
-
|
|
369
|
+
writeFileAtomic(targetFile, nextContent);
|
|
281
370
|
if (!options?.silent) {
|
|
282
371
|
const config = (0, config_1.loadConfig)();
|
|
283
372
|
console.log(`已写入: ${targetFile}`);
|
|
@@ -312,6 +401,8 @@ function deactivateManagedCodexConfig() {
|
|
|
312
401
|
*/
|
|
313
402
|
async function main() {
|
|
314
403
|
const program = new commander_1.Command();
|
|
404
|
+
// 禁用内置 help 子命令,仅保留 --help / -h 形式。
|
|
405
|
+
program.addHelpCommand(false);
|
|
315
406
|
(0, config_1.getCodexSwHome)();
|
|
316
407
|
(0, config_1.loadConfig)();
|
|
317
408
|
program
|
|
@@ -339,8 +430,9 @@ async function main() {
|
|
|
339
430
|
program
|
|
340
431
|
.command("status")
|
|
341
432
|
.description("刷新并查看所有已录入账号或工作空间的最新额度")
|
|
342
|
-
.
|
|
343
|
-
|
|
433
|
+
.option("--no-interactive", "仅输出状态表,不进入交互式切换")
|
|
434
|
+
.action(async (options) => {
|
|
435
|
+
await handleStatus(options);
|
|
344
436
|
});
|
|
345
437
|
program
|
|
346
438
|
.command("start")
|
package/dist/server.js
CHANGED
|
@@ -49,7 +49,7 @@ function resolveBlockWindow(picked, errorText) {
|
|
|
49
49
|
picked.status.isFiveHourLimited) {
|
|
50
50
|
return {
|
|
51
51
|
until: picked.status.fiveHourResetsAt ?? Math.floor(Date.now() / 1000) + 5 * 60,
|
|
52
|
-
reason: "
|
|
52
|
+
reason: "5h_limited"
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
return {
|
package/dist/status.js
CHANGED
|
@@ -31,6 +31,22 @@ function formatReset(unixSeconds) {
|
|
|
31
31
|
hour12: false
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
function formatLimitStatus(label, resetAt) {
|
|
35
|
+
const remaining = formatRemainingDuration(resetAt);
|
|
36
|
+
if (!remaining) {
|
|
37
|
+
return label;
|
|
38
|
+
}
|
|
39
|
+
return `${label}(${remaining})`;
|
|
40
|
+
}
|
|
41
|
+
function normalizeBlockReason(reason) {
|
|
42
|
+
if (!reason) {
|
|
43
|
+
return "blocked";
|
|
44
|
+
}
|
|
45
|
+
if (reason === "five_hour_limited") {
|
|
46
|
+
return "5h_limited";
|
|
47
|
+
}
|
|
48
|
+
return reason;
|
|
49
|
+
}
|
|
34
50
|
/**
|
|
35
51
|
* 将剩余秒数格式化为紧凑的人类可读文本,便于在状态列中展示熔断剩余时间。
|
|
36
52
|
*
|
|
@@ -64,7 +80,7 @@ function formatRemainingDuration(unixSeconds) {
|
|
|
64
80
|
* @returns 适合在终端表格中展示的状态文本。
|
|
65
81
|
*/
|
|
66
82
|
function formatBlockedStatus(reason, until) {
|
|
67
|
-
const label = reason
|
|
83
|
+
const label = normalizeBlockReason(reason);
|
|
68
84
|
const remaining = formatRemainingDuration(until ?? null);
|
|
69
85
|
if (!remaining) {
|
|
70
86
|
return label;
|
|
@@ -137,10 +153,10 @@ function renderStatusTable(statuses) {
|
|
|
137
153
|
status = formatBlockedStatus(item.localBlockReason, item.localBlockUntil);
|
|
138
154
|
}
|
|
139
155
|
else if (item.isWeeklyLimited) {
|
|
140
|
-
status = "weekly_limited";
|
|
156
|
+
status = formatLimitStatus("weekly_limited", item.weeklyResetsAt);
|
|
141
157
|
}
|
|
142
158
|
else if (item.isFiveHourLimited) {
|
|
143
|
-
status = "5h_limited";
|
|
159
|
+
status = formatLimitStatus("5h_limited", item.fiveHourResetsAt);
|
|
144
160
|
}
|
|
145
161
|
else if (item.isAvailable) {
|
|
146
162
|
status = "available";
|