@openxiaobu/codexl 0.1.6 → 0.1.7

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 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
- - Fetch the latest usage from the official usage endpoint
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,21 @@ 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 any account by `NAME`
55
+
56
+ If you only want a non-interactive snapshot of the current state:
57
+
58
+ ```bash
59
+ codexl status --no-interactive
60
+ ```
61
+
47
62
  Start the local proxy:
48
63
 
49
64
  ```bash
@@ -81,24 +96,21 @@ Instead it:
81
96
 
82
97
  ## Managed Codex Config
83
98
 
84
- `codexl start` writes a managed provider block like this:
99
+ `codexl start` writes or updates a provider block like this, based on the current `~/.codexl/config.yaml`:
85
100
 
86
101
  ```toml
87
- # >>> codexl managed start >>>
88
102
  [model_providers.codexl]
89
103
  name = "codexl"
90
104
  base_url = "http://127.0.0.1:4389/v1"
91
105
  http_headers = { Authorization = "Bearer codexl-defaultkey" }
92
106
  wire_api = "responses"
93
- # <<< codexl managed end <<<
94
107
  ```
95
108
 
96
109
  Behavior:
97
110
 
98
- - If `[model_providers.codexl]` already exists, it is replaced
99
- - If global `model_provider` exists, it is changed to `codexl`
100
- - If commented `# model_provider = ...` exists, it is reopened as `model_provider = "codexl"`
101
- - Global `model` is kept unchanged
111
+ - If global `model_provider` or `# model_provider = ...` exists, it is normalized to `model_provider = "codexl"`
112
+ - If `[model_providers.codexl]` already exists, only that provider block is replaced with the fresh one above
113
+ - Other providers and settings in `config.toml` are left untouched
102
114
  - If you start with `--port`, the port is saved to `~/.codexl/config.yaml`
103
115
  - `codexl stop` comments out the active `model_provider = "codexl"` line and keeps the rest of the file unchanged
104
116
 
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,47 @@ 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 rl = node_readline_1.default.createInterface({
63
+ input: process.stdin,
64
+ output: process.stdout
65
+ });
66
+ const ask = (question) => new Promise((resolve) => {
67
+ rl.question(question, (answer) => resolve(answer));
68
+ });
69
+ try {
70
+ // 允许连续切换多个账号,回车空行退出。
71
+ // 这里使用账号 NAME 作为标识,与 status 表格中的首列一致。
72
+ for (;;) {
73
+ const name = (await ask("输入要切换启用状态的账号 NAME(直接回车退出):")).trim();
74
+ if (!name) {
75
+ break;
76
+ }
77
+ const config = (0, config_1.loadConfig)();
78
+ const index = config.accounts.findIndex((item) => item.name === name);
79
+ if (index < 0) {
80
+ console.log(`未找到 NAME 为 "${name}" 的账号,请检查后重试。`);
81
+ continue;
82
+ }
83
+ const account = config.accounts[index];
84
+ account.enabled = !account.enabled;
85
+ config.accounts[index] = account;
86
+ (0, config_1.saveConfig)(config);
87
+ console.log(`账号 ${account.name} 已${account.enabled ? "启用" : "禁用"}(id=${account.id},source=${account.codex_home})。`);
88
+ }
89
+ }
90
+ finally {
91
+ rl.close();
92
+ }
51
93
  }
52
94
  /**
53
95
  * 将已有的 Codex HOME 目录中的登录态复制到 codexl 自己的隔离目录并纳入管理。
@@ -175,6 +217,15 @@ function handleStop() {
175
217
  function escapeRegExp(input) {
176
218
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
177
219
  }
220
+ function ensureParentDir(filePath) {
221
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
222
+ }
223
+ function writeFileAtomic(targetFile, content) {
224
+ ensureParentDir(targetFile);
225
+ const tmpFile = `${targetFile}.tmp-${process.pid}-${Date.now()}`;
226
+ node_fs_1.default.writeFileSync(tmpFile, content, "utf8");
227
+ node_fs_1.default.renameSync(tmpFile, targetFile);
228
+ }
178
229
  /**
179
230
  * 返回默认的 `codex config.toml` 路径。
180
231
  *
@@ -184,22 +235,18 @@ function getDefaultCodexConfigPath() {
184
235
  return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
185
236
  }
186
237
  /**
187
- * 生成 codexl 托管的 provider 配置块。
238
+ * 生成 codexl provider 配置块。
188
239
  *
189
240
  * @returns 可直接写入 `config.toml` 的配置块内容。
190
241
  */
191
242
  function buildManagedConfigBlock() {
192
243
  const config = (0, config_1.loadConfig)();
193
- const startMarker = "# >>> codexl managed start >>>";
194
- const endMarker = "# <<< codexl managed end <<<";
195
244
  return [
196
- startMarker,
197
245
  "[model_providers.codexl]",
198
246
  'name = "codexl"',
199
247
  `base_url = "http://${config.server.host}:${config.server.port}/v1"`,
200
248
  `http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
201
- 'wire_api = "responses"',
202
- endMarker
249
+ 'wire_api = "responses"'
203
250
  ].join("\n");
204
251
  }
205
252
  /**
@@ -211,73 +258,71 @@ function buildManagedConfigBlock() {
211
258
  function applyManagedCodexConfig(targetPathOrDir, options) {
212
259
  const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
213
260
  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
261
  const block = buildManagedConfigBlock();
217
262
  let original = "";
218
263
  if (node_fs_1.default.existsSync(targetFile)) {
219
264
  original = node_fs_1.default.readFileSync(targetFile, "utf8");
220
265
  }
221
- else {
222
- node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
223
- }
224
- const managedBlockPattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
225
- const lines = original.replace(managedBlockPattern, "").split(/\r?\n/);
226
- let insertAfterIndex = -1;
227
- let hasGlobalModelProvider = false;
266
+ // 更稳定的策略:仅按 provider 名称和 model_provider 改动,不引入额外的 marker。
267
+ const lines = original.length > 0 ? original.split(/\r?\n/) : [];
268
+ let replacedModelProvider = false;
269
+ const modelProviderLine = 'model_provider = "codexl"';
228
270
  for (let i = 0; i < lines.length; i += 1) {
229
- const line = lines[i];
230
- const trimmed = line.trim();
231
- if (/^#\s*model_provider\s*=/.test(trimmed)) {
232
- lines[i] = 'model_provider = "codexl"';
233
- hasGlobalModelProvider = true;
271
+ const trimmed = lines[i].trim();
272
+ // 优先替换被注释掉的默认行(只替换第一处,减少对用户文件的干扰)。
273
+ if (!replacedModelProvider && /^#\s*model_provider\s*=/.test(trimmed)) {
274
+ lines[i] = modelProviderLine;
275
+ replacedModelProvider = true;
234
276
  continue;
235
277
  }
236
- if (trimmed.startsWith("#")) {
278
+ // 替换真实生效的 model_provider 行。
279
+ if (/^model_provider\s*=/.test(trimmed) && !trimmed.startsWith("#")) {
280
+ lines[i] = modelProviderLine;
281
+ replacedModelProvider = true;
237
282
  continue;
238
283
  }
239
- if (/^model_provider\s*=/.test(trimmed)) {
240
- lines[i] = 'model_provider = "codexl"';
241
- hasGlobalModelProvider = true;
242
- continue;
284
+ }
285
+ if (!replacedModelProvider) {
286
+ const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
287
+ if (firstNonEmptyIndex >= 0) {
288
+ lines.splice(firstNonEmptyIndex, 0, modelProviderLine, "");
243
289
  }
290
+ else {
291
+ lines.push(modelProviderLine, "");
292
+ }
293
+ }
294
+ // 替换或追加 [model_providers.codexl] 这一段配置。
295
+ const blockLines = block.split("\n");
296
+ let insertAfterIndex = -1;
297
+ for (let i = 0; i < lines.length; i += 1) {
298
+ const trimmed = lines[i].trim();
244
299
  if (trimmed === "[model_providers.codexl]") {
245
300
  let j = i;
246
301
  while (j < lines.length) {
247
- const current = lines[j];
248
- const currentTrimmed = current.trim();
302
+ const currentTrimmed = lines[j].trim();
249
303
  if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
250
304
  break;
251
305
  }
252
306
  insertAfterIndex = j;
253
307
  j += 1;
254
308
  }
309
+ // 删除旧的 codexl provider 表块,准备写入新的。
255
310
  lines.splice(i, j - i);
256
311
  insertAfterIndex = i - 1;
257
312
  i = j - 1;
258
313
  }
259
314
  }
260
- const blockLines = block.split("\n");
261
315
  if (insertAfterIndex >= 0) {
262
316
  lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
263
317
  }
264
318
  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
319
  if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
275
320
  lines.push("");
276
321
  }
277
322
  lines.push(...blockLines);
278
323
  }
279
324
  const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
280
- node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
325
+ writeFileAtomic(targetFile, nextContent);
281
326
  if (!options?.silent) {
282
327
  const config = (0, config_1.loadConfig)();
283
328
  console.log(`已写入: ${targetFile}`);
@@ -312,6 +357,8 @@ function deactivateManagedCodexConfig() {
312
357
  */
313
358
  async function main() {
314
359
  const program = new commander_1.Command();
360
+ // 禁用内置 help 子命令,仅保留 --help / -h 形式。
361
+ program.addHelpCommand(false);
315
362
  (0, config_1.getCodexSwHome)();
316
363
  (0, config_1.loadConfig)();
317
364
  program
@@ -339,8 +386,9 @@ async function main() {
339
386
  program
340
387
  .command("status")
341
388
  .description("刷新并查看所有已录入账号或工作空间的最新额度")
342
- .action(async () => {
343
- await handleStatus();
389
+ .option("--no-interactive", "仅输出状态表,不进入交互式切换")
390
+ .action(async (options) => {
391
+ await handleStatus(options);
344
392
  });
345
393
  program
346
394
  .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: "five_hour_limited"
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 ?? "blocked";
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openxiaobu/codexl",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",