@openxiaobu/codexl 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ # codexl
2
+
3
+ Local multi-account / multi-workspace switcher for Codex.
4
+
5
+ `codexl` 是一个本地 `Codex` 多账号 / 多工作空间切换器。
6
+
7
+ ## Overview
8
+
9
+ English:
10
+
11
+ - Reuse the official `~/.codex` login state
12
+ - Manage multiple accounts or workspaces as separate slots
13
+ - Fetch the latest usage from the official usage endpoint
14
+ - Expose a local provider endpoint for Codex
15
+ - Apply local cooldown rules for temporary, 5-hour, and weekly limits
16
+
17
+ 中文:
18
+
19
+ - 复用官方 `~/.codex` 登录态
20
+ - 将多个账号或工作空间作为独立槽位管理
21
+ - 直接调用官方 usage 接口获取最新额度
22
+ - 暴露本地 provider 给 `Codex` 使用
23
+ - 对临时限流、5 小时限制、周限制做本地熔断
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm i -g @openxiaobu/codexl
29
+ ```
30
+
31
+ Verify:
32
+
33
+ ```bash
34
+ codexl --help
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ 1. Import your current Codex login state
40
+
41
+ ```bash
42
+ codexl import current ~
43
+ ```
44
+
45
+ 2. Check latest usage
46
+
47
+ ```bash
48
+ codexl status
49
+ ```
50
+
51
+ 3. Start the local proxy
52
+
53
+ ```bash
54
+ codexl start
55
+ ```
56
+
57
+ Custom port:
58
+
59
+ ```bash
60
+ codexl start --port 4399
61
+ ```
62
+
63
+ 4. Show current local endpoint and key
64
+
65
+ ```bash
66
+ codexl get
67
+ ```
68
+
69
+ 5. Write provider config into `~/.codex/config.toml`
70
+
71
+ ```bash
72
+ codexl config
73
+ ```
74
+
75
+ ## Commands
76
+
77
+ ```bash
78
+ codexl add <name>
79
+ codexl del <name>
80
+ codexl import <name> [HOME]
81
+ codexl status
82
+ codexl start [--port <port>]
83
+ codexl stop
84
+ codexl get
85
+ codexl config [codexPath]
86
+ ```
87
+
88
+ More details: [HELP.md](./HELP.md)
89
+
90
+ ## How `status` Works
91
+
92
+ English:
93
+
94
+ 1. Read `access_token` / `refresh_token` / `account_id` from the official Codex login state
95
+ 2. Request `https://chatgpt.com/backend-api/wham/usage`
96
+ 3. Store the latest result in `~/.codexl/state.json`
97
+ 4. Render the latest local cache
98
+
99
+ 中文:
100
+
101
+ 1. 从官方登录态中读取 `access_token` / `refresh_token` / `account_id`
102
+ 2. 请求 `https://chatgpt.com/backend-api/wham/usage`
103
+ 3. 将最新结果写入 `~/.codexl/state.json`
104
+ 4. 最后读取本地最新缓存进行展示
105
+
106
+ ## Generated Codex Config
107
+
108
+ `codexl config` writes a managed provider block like this:
109
+
110
+ ```toml
111
+ # >>> codexl managed start >>>
112
+ [model_providers.codexl]
113
+ name = "codexl"
114
+ base_url = "http://127.0.0.1:4389/v1"
115
+ http_headers = { Authorization = "Bearer codexl-defaultkey" }
116
+ wire_api = "responses"
117
+ # <<< codexl managed end <<<
118
+ ```
119
+
120
+ Rules:
121
+
122
+ - If `[model_providers.codexl]` already exists, it is replaced
123
+ - If global `model_provider` exists, it is changed to `codexl`
124
+ - If commented `# model_provider = ...` exists, it is reopened as `model_provider = "codexl"`
125
+ - Global `model` is kept unchanged
126
+ - If you start with `--port`, the port is saved to `~/.codexl/config.yaml`, and later `get` / `config` will use that port
127
+
128
+ ## Data Directory
129
+
130
+ `codexl` uses:
131
+
132
+ - `~/.codexl/config.yaml`
133
+ - `~/.codexl/state.json`
134
+ - `~/.codexl/codexl.pid`
135
+ - `~/.codexl/logs/service.log`
136
+
137
+ If you previously used `~/.codexsw`, it will be migrated automatically.
138
+
139
+ ## Limit Handling
140
+
141
+ English:
142
+
143
+ - Weekly limit: blocked until weekly reset time
144
+ - 5-hour limit: blocked until 5-hour reset time
145
+ - Temporary limit: blocked for 5 minutes
146
+
147
+ 中文:
148
+
149
+ - 周限制:禁用到周窗口重置时间
150
+ - 5 小时限制:禁用到 5 小时窗口重置时间
151
+ - 临时限流:先禁用 5 分钟
152
+
153
+ ## Repository
154
+
155
+ - GitHub: https://github.com/openxiaobu/codexl
156
+ - Issues: https://github.com/openxiaobu/codexl/issues
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm run build
163
+ npm run check
164
+ ```
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getCodexDataDir = getCodexDataDir;
7
+ exports.readRegistry = readRegistry;
8
+ exports.readAuthFile = readAuthFile;
9
+ exports.writeAuthFile = writeAuthFile;
10
+ exports.resolvePrimaryRegistryAccount = resolvePrimaryRegistryAccount;
11
+ exports.registerManagedAccount = registerManagedAccount;
12
+ exports.removeManagedAccount = removeManagedAccount;
13
+ exports.findManagedAccount = findManagedAccount;
14
+ const node_fs_1 = __importDefault(require("node:fs"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const config_1 = require("./config");
17
+ /**
18
+ * 读取指定账号 HOME 下的 `.codex` 目录。
19
+ *
20
+ * @param codexHome 账号独立 HOME 目录。
21
+ * @returns `.codex` 目录绝对路径。
22
+ */
23
+ function getCodexDataDir(codexHome) {
24
+ return node_path_1.default.join((0, config_1.expandHome)(codexHome), ".codex");
25
+ }
26
+ /**
27
+ * 读取某账号对应的 `registry.json`。
28
+ *
29
+ * @param codexHome 账号独立 HOME 目录。
30
+ * @returns 解析后的 registry;不存在时返回 `null`。
31
+ */
32
+ function readRegistry(codexHome) {
33
+ const registryPath = node_path_1.default.join(getCodexDataDir(codexHome), "accounts", "registry.json");
34
+ if (!node_fs_1.default.existsSync(registryPath)) {
35
+ return null;
36
+ }
37
+ return JSON.parse(node_fs_1.default.readFileSync(registryPath, "utf8"));
38
+ }
39
+ /**
40
+ * 读取账号目录下当前激活凭据文件。
41
+ *
42
+ * @param codexHome 账号独立 HOME 目录。
43
+ * @returns 解析后的 auth.json;不存在时返回 `null`。
44
+ */
45
+ function readAuthFile(codexHome) {
46
+ const authPath = node_path_1.default.join(getCodexDataDir(codexHome), "auth.json");
47
+ if (!node_fs_1.default.existsSync(authPath)) {
48
+ return null;
49
+ }
50
+ return JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
51
+ }
52
+ /**
53
+ * 将最新认证信息回写到指定账号的 `auth.json`。
54
+ *
55
+ * @param codexHome 账号独立 HOME 目录。
56
+ * @param auth 最新认证信息。
57
+ * @returns 无返回值。
58
+ */
59
+ function writeAuthFile(codexHome, auth) {
60
+ const authPath = node_path_1.default.join(getCodexDataDir(codexHome), "auth.json");
61
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(authPath), { recursive: true });
62
+ node_fs_1.default.writeFileSync(authPath, `${JSON.stringify(auth, null, 2)}\n`, "utf8");
63
+ }
64
+ /**
65
+ * 根据当前账号目录中的 registry 推断主账号信息。
66
+ *
67
+ * @param codexHome 账号独立 HOME 目录。
68
+ * @returns 当前活跃账号元数据;无可用账号时返回 `null`。
69
+ */
70
+ function resolvePrimaryRegistryAccount(codexHome) {
71
+ const registry = readRegistry(codexHome);
72
+ if (!registry || registry.accounts.length === 0) {
73
+ return null;
74
+ }
75
+ if (registry.active_email) {
76
+ const active = registry.accounts.find((item) => item.email === registry.active_email);
77
+ if (active) {
78
+ return active;
79
+ }
80
+ }
81
+ return registry.accounts[0] ?? null;
82
+ }
83
+ /**
84
+ * 将账号注册到 codexl 配置中,并为其准备独立 HOME 目录。
85
+ *
86
+ * @param accountId 本地账号标识。
87
+ * @param codexHome 可选的自定义 HOME 目录;未提供时使用默认路径。
88
+ * @returns 写入后的账号配置。
89
+ */
90
+ function registerManagedAccount(accountId, codexHome) {
91
+ const home = codexHome ? (0, config_1.expandHome)(codexHome) : (0, config_1.getManagedHome)(accountId);
92
+ // 预先创建账号隔离目录,方便后续直接执行 codex login。
93
+ node_fs_1.default.mkdirSync(home, { recursive: true });
94
+ const primary = resolvePrimaryRegistryAccount(home);
95
+ const account = {
96
+ id: accountId,
97
+ name: accountId,
98
+ codex_home: home,
99
+ email: primary?.email,
100
+ enabled: true,
101
+ imported_at: new Date().toISOString()
102
+ };
103
+ (0, config_1.upsertAccount)(account);
104
+ return account;
105
+ }
106
+ /**
107
+ * 从配置中删除指定账号;默认仅删除配置项,不主动删除本地 HOME 目录。
108
+ *
109
+ * @param accountId 本地账号标识。
110
+ * @returns 被删除的账号配置;未命中时返回 `null`。
111
+ */
112
+ function removeManagedAccount(accountId) {
113
+ const config = (0, config_1.loadConfig)();
114
+ const index = config.accounts.findIndex((item) => item.id === accountId);
115
+ if (index < 0) {
116
+ return null;
117
+ }
118
+ const [removed] = config.accounts.splice(index, 1);
119
+ (0, config_1.saveConfig)(config);
120
+ return removed ?? null;
121
+ }
122
+ /**
123
+ * 根据账号标识读取配置中的账号项。
124
+ *
125
+ * @param accountId 本地账号标识。
126
+ * @returns 命中的账号配置;未命中时返回 `null`。
127
+ */
128
+ function findManagedAccount(accountId) {
129
+ const config = (0, config_1.loadConfig)();
130
+ return config.accounts.find((item) => item.id === accountId) ?? null;
131
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const commander_1 = require("commander");
11
+ const account_store_1 = require("./account-store");
12
+ const config_1 = require("./config");
13
+ const login_1 = require("./login");
14
+ const status_1 = require("./status");
15
+ const usage_sync_1 = require("./usage-sync");
16
+ /**
17
+ * 刷新所有已录入账号的远端额度,并输出最新状态表格。
18
+ *
19
+ * @returns Promise,无返回值。
20
+ */
21
+ async function handleStatus() {
22
+ await (0, usage_sync_1.refreshAllAccountUsage)();
23
+ const statuses = (0, status_1.collectAccountStatuses)();
24
+ console.log((0, status_1.renderStatusTable)(statuses));
25
+ const available = statuses.filter((item) => item.isAvailable).length;
26
+ const cooldown = statuses.filter((item) => item.isFiveHourLimited && !item.isWeeklyLimited).length;
27
+ const weeklyLimited = statuses.filter((item) => item.isWeeklyLimited).length;
28
+ console.log("");
29
+ console.log(`available=${available} cooldown=${cooldown} weekly_limited=${weeklyLimited}`);
30
+ }
31
+ /**
32
+ * 将已有的 Codex HOME 目录纳入 codexl 管理。
33
+ *
34
+ * @param accountId 本地账号标识。
35
+ * @param codexHome 现有 HOME 目录;若未传则默认使用当前用户 HOME。
36
+ * @returns 无返回值。
37
+ */
38
+ function handleAccountImport(accountId, codexHome) {
39
+ const home = codexHome ? (0, config_1.expandHome)(codexHome) : process.env.HOME ?? "";
40
+ const account = (0, account_store_1.registerManagedAccount)(accountId, home);
41
+ console.log(`账号已导入: ${account.id}`);
42
+ console.log(`来源 HOME: ${account.codex_home}`);
43
+ }
44
+ /**
45
+ * 执行隔离登录流程,将账号录入到 codexl 管理目录。
46
+ *
47
+ * @param accountId 本地账号标识。
48
+ * @returns Promise,无返回值。
49
+ */
50
+ async function handleAccountLogin(accountId) {
51
+ const home = await (0, login_1.loginManagedAccount)(accountId);
52
+ console.log(`登录完成,账号目录: ${home}`);
53
+ }
54
+ /**
55
+ * 删除配置中的账号项。
56
+ *
57
+ * @param accountId 本地账号标识。
58
+ * @returns 无返回值。
59
+ * @throws 当账号不存在时抛出错误。
60
+ */
61
+ function handleAccountRemove(accountId) {
62
+ const removed = (0, account_store_1.removeManagedAccount)(accountId);
63
+ if (!removed) {
64
+ throw new Error(`未找到账号 ${accountId}`);
65
+ }
66
+ console.log(`已删除账号配置: ${removed.id}`);
67
+ }
68
+ /**
69
+ * 判断后台服务当前是否在运行。
70
+ *
71
+ * @returns 运行中的 PID;未运行时返回 `null`。
72
+ */
73
+ function getRunningPid() {
74
+ const pidPath = (0, config_1.getPidPath)();
75
+ if (!node_fs_1.default.existsSync(pidPath)) {
76
+ return null;
77
+ }
78
+ const raw = node_fs_1.default.readFileSync(pidPath, "utf8").trim();
79
+ const pid = Number(raw);
80
+ if (!Number.isInteger(pid) || pid <= 0) {
81
+ node_fs_1.default.rmSync(pidPath, { force: true });
82
+ return null;
83
+ }
84
+ try {
85
+ process.kill(pid, 0);
86
+ return pid;
87
+ }
88
+ catch {
89
+ node_fs_1.default.rmSync(pidPath, { force: true });
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * 后台启动 codexl 服务并写入 PID 文件。
95
+ *
96
+ * @returns Promise,无返回值。
97
+ * @throws 当服务已在运行或子进程启动失败时抛出异常。
98
+ */
99
+ async function handleStart(portOverride) {
100
+ const config = (0, config_1.loadConfig)();
101
+ const port = portOverride ? Number(portOverride) : config.server.port;
102
+ if (portOverride) {
103
+ config.server.port = port;
104
+ (0, config_1.saveConfig)(config);
105
+ }
106
+ const runningPid = getRunningPid();
107
+ if (runningPid) {
108
+ console.log(`服务已在运行,PID=${runningPid}`);
109
+ if (portOverride) {
110
+ console.log(`已将新端口写入配置: ${port}`);
111
+ console.log("请先执行 codexl stop,再执行 codexl start 使新端口生效。");
112
+ }
113
+ return;
114
+ }
115
+ const logPath = (0, config_1.getServiceLogPath)();
116
+ const logFd = node_fs_1.default.openSync(logPath, "a");
117
+ const child = (0, node_child_process_1.spawn)(process.execPath, [__filename.replace(/cli\.js$/, "serve.js"), "--port", String(port)], {
118
+ detached: true,
119
+ stdio: ["ignore", logFd, logFd]
120
+ });
121
+ child.unref();
122
+ node_fs_1.default.writeFileSync((0, config_1.getPidPath)(), `${child.pid}\n`, "utf8");
123
+ console.log(`服务已启动: http://${config.server.host}:${port}`);
124
+ console.log(`PID: ${child.pid}`);
125
+ console.log(`日志: ${logPath}`);
126
+ }
127
+ /**
128
+ * 停止后台运行的 codexl 服务。
129
+ *
130
+ * @returns 无返回值。
131
+ */
132
+ function handleStop() {
133
+ const pid = getRunningPid();
134
+ if (!pid) {
135
+ console.log("服务未运行");
136
+ return;
137
+ }
138
+ process.kill(pid, "SIGTERM");
139
+ node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
140
+ console.log(`服务已停止,PID=${pid}`);
141
+ }
142
+ /**
143
+ * 输出当前 `codex` 需要的 provider 配置与本地 key 信息。
144
+ *
145
+ * @returns 无返回值。
146
+ */
147
+ function handleGetConfig() {
148
+ const config = (0, config_1.loadConfig)();
149
+ console.log(`base_url=${`http://${config.server.host}:${config.server.port}/v1`}`);
150
+ console.log(`api_key=${config.server.api_key}`);
151
+ }
152
+ function escapeRegExp(input) {
153
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
154
+ }
155
+ /**
156
+ * 将 codexl provider 配置写入指定的 codex config.toml。
157
+ *
158
+ * @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
159
+ * @returns 无返回值。
160
+ */
161
+ function handleConfig(targetPathOrDir) {
162
+ const config = (0, config_1.loadConfig)();
163
+ const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
164
+ const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
165
+ const startMarker = "# >>> codexl managed start >>>";
166
+ const endMarker = "# <<< codexl managed end <<<";
167
+ const block = [
168
+ startMarker,
169
+ "[model_providers.codexl]",
170
+ 'name = "codexl"',
171
+ `base_url = "http://${config.server.host}:${config.server.port}/v1"`,
172
+ `http_headers = { Authorization = "Bearer ${config.server.api_key}" }`,
173
+ 'wire_api = "responses"',
174
+ endMarker
175
+ ].join("\n");
176
+ let original = "";
177
+ if (node_fs_1.default.existsSync(targetFile)) {
178
+ original = node_fs_1.default.readFileSync(targetFile, "utf8");
179
+ }
180
+ else {
181
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(targetFile), { recursive: true });
182
+ }
183
+ const managedBlockPattern = new RegExp(`${escapeRegExp(startMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "g");
184
+ const lines = original.replace(managedBlockPattern, "").split(/\r?\n/);
185
+ let insertAfterIndex = -1;
186
+ let hasGlobalModelProvider = false;
187
+ for (let i = 0; i < lines.length; i += 1) {
188
+ const line = lines[i];
189
+ const trimmed = line.trim();
190
+ if (/^#\s*model_provider\s*=/.test(trimmed)) {
191
+ lines[i] = 'model_provider = "codexl"';
192
+ hasGlobalModelProvider = true;
193
+ continue;
194
+ }
195
+ if (trimmed.startsWith("#")) {
196
+ continue;
197
+ }
198
+ if (/^model_provider\s*=/.test(trimmed)) {
199
+ lines[i] = 'model_provider = "codexl"';
200
+ hasGlobalModelProvider = true;
201
+ continue;
202
+ }
203
+ if (trimmed === "[model_providers.codexl]") {
204
+ let j = i;
205
+ while (j < lines.length) {
206
+ const current = lines[j];
207
+ const currentTrimmed = current.trim();
208
+ if (j > i && currentTrimmed.startsWith("[") && !currentTrimmed.startsWith("[[")) {
209
+ break;
210
+ }
211
+ insertAfterIndex = j;
212
+ j += 1;
213
+ }
214
+ lines.splice(i, j - i);
215
+ insertAfterIndex = i - 1;
216
+ i = j - 1;
217
+ }
218
+ }
219
+ const blockLines = block.split("\n");
220
+ if (insertAfterIndex >= 0) {
221
+ lines.splice(insertAfterIndex + 1, 0, "", ...blockLines);
222
+ }
223
+ else {
224
+ if (!hasGlobalModelProvider) {
225
+ const firstNonEmptyIndex = lines.findIndex((line) => line.trim() !== "");
226
+ if (firstNonEmptyIndex >= 0) {
227
+ lines.splice(firstNonEmptyIndex, 0, 'model_provider = "codexl"', "");
228
+ }
229
+ else {
230
+ lines.push('model_provider = "codexl"', "");
231
+ }
232
+ }
233
+ if (lines.length > 0 && lines[lines.length - 1].trim() !== "") {
234
+ lines.push("");
235
+ }
236
+ lines.push(...blockLines);
237
+ }
238
+ const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
239
+ node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
240
+ console.log(`已写入: ${targetFile}`);
241
+ console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
242
+ console.log(`api_key=${config.server.api_key}`);
243
+ console.log('提示: 已写入 codexl provider;如果原来存在 model_provider,则已切换为 codexl;model 保持不变。');
244
+ }
245
+ /**
246
+ * CLI 主入口,负责命令注册与执行。
247
+ *
248
+ * @returns Promise,无返回值。
249
+ * @throws 当命令执行失败时向上抛出异常。
250
+ */
251
+ async function main() {
252
+ const program = new commander_1.Command();
253
+ (0, config_1.getCodexSwHome)();
254
+ (0, config_1.loadConfig)();
255
+ program
256
+ .name("codexsw")
257
+ .name("codexl")
258
+ .description("本地 Codex 多账号切换与状态管理工具")
259
+ .version("0.1.0");
260
+ program
261
+ .command("add")
262
+ .description("登录并新增一个账号或工作空间")
263
+ .argument("<accountId>", "账号标识")
264
+ .action(async (accountId) => {
265
+ await handleAccountLogin(accountId);
266
+ });
267
+ program
268
+ .command("del")
269
+ .description("删除一个已录入账号")
270
+ .argument("<accountId>", "账号标识")
271
+ .action(handleAccountRemove);
272
+ program
273
+ .command("import")
274
+ .description("导入当前或指定 HOME 下的官方 codex 登录态")
275
+ .argument("<accountId>", "账号标识")
276
+ .argument("[codexHome]", "已有 HOME 目录,默认当前用户 HOME")
277
+ .action(handleAccountImport);
278
+ program
279
+ .command("status")
280
+ .description("刷新并查看所有已录入账号或工作空间的最新额度")
281
+ .action(async () => {
282
+ await handleStatus();
283
+ });
284
+ program
285
+ .command("start")
286
+ .description("后台启动本地代理服务")
287
+ .option("--port <port>", "监听端口")
288
+ .action(async (options) => {
289
+ await handleStart(options.port);
290
+ });
291
+ program.command("stop").description("停止后台代理服务").action(handleStop);
292
+ program.command("get").description("输出当前 base_url 和 api_key").action(handleGetConfig);
293
+ program
294
+ .command("config")
295
+ .description("自动写入 codex 的 config.toml,默认 ~/.codex/config.toml")
296
+ .argument("[codexPath]", "codex 配置目录或 config.toml 文件路径")
297
+ .action(handleConfig);
298
+ await program.parseAsync(process.argv);
299
+ }
300
+ void main().catch((error) => {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ console.error(`codexl 执行失败: ${message}`);
303
+ process.exit(1);
304
+ });