@openxiaobu/codexl 0.1.1 → 0.1.3

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
@@ -11,7 +11,7 @@
11
11
  - Fetch the latest usage from the official usage endpoint
12
12
  - Expose a local provider endpoint for Codex
13
13
  - Apply local cooldown rules for temporary, 5-hour, and weekly limits
14
- - Write a managed provider block into `~/.codex/config.toml`
14
+ - Automatically switch `~/.codex/config.toml` to the `codexl` provider while the local proxy is running
15
15
 
16
16
  ## Installation
17
17
 
@@ -51,16 +51,10 @@ codexl start
51
51
  codexl start --port 4399
52
52
  ```
53
53
 
54
- Show the current local endpoint and key:
54
+ `start` will automatically write the required provider config into `~/.codex/config.toml`:
55
55
 
56
56
  ```bash
57
- codexl get
58
- ```
59
-
60
- Write provider config into `~/.codex/config.toml`:
61
-
62
- ```bash
63
- codexl config
57
+ codexl start
64
58
  ```
65
59
 
66
60
  ## Commands
@@ -72,8 +66,6 @@ codexl import <name> [HOME]
72
66
  codexl status
73
67
  codexl start [--port <port>]
74
68
  codexl stop
75
- codexl get
76
- codexl config [codexPath]
77
69
  ```
78
70
 
79
71
  ## How `status` Works
@@ -87,9 +79,9 @@ Instead it:
87
79
  3. Stores the latest result in `~/.codexl/state.json`
88
80
  4. Renders the latest local cache
89
81
 
90
- ## Generated Codex Config
82
+ ## Managed Codex Config
91
83
 
92
- `codexl config` writes a managed provider block like this:
84
+ `codexl start` writes a managed provider block like this:
93
85
 
94
86
  ```toml
95
87
  # >>> codexl managed start >>>
@@ -107,7 +99,8 @@ Behavior:
107
99
  - If global `model_provider` exists, it is changed to `codexl`
108
100
  - If commented `# model_provider = ...` exists, it is reopened as `model_provider = "codexl"`
109
101
  - Global `model` is kept unchanged
110
- - If you start with `--port`, the port is saved to `~/.codexl/config.yaml`, and later `get` / `config` will use that port
102
+ - If you start with `--port`, the port is saved to `~/.codexl/config.yaml`
103
+ - `codexl stop` comments out the active `model_provider = "codexl"` line and keeps the rest of the file unchanged
111
104
 
112
105
  ## Data Directory
113
106
 
@@ -51,6 +51,25 @@ function readAuthFile(codexHome) {
51
51
  }
52
52
  return JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
53
53
  }
54
+ /**
55
+ * 从 `id_token` 中解析邮箱。
56
+ *
57
+ * @param auth 认证文件对象。
58
+ * @returns 邮箱地址;缺失或解析失败时返回 `undefined`。
59
+ */
60
+ function resolveEmailFromAuth(auth) {
61
+ const idToken = auth?.tokens?.id_token;
62
+ if (!idToken) {
63
+ return undefined;
64
+ }
65
+ try {
66
+ const payload = JSON.parse(Buffer.from(idToken.split(".")[1] ?? "", "base64url").toString("utf8"));
67
+ return payload.email;
68
+ }
69
+ catch {
70
+ return undefined;
71
+ }
72
+ }
54
73
  /**
55
74
  * 将来源 HOME 下的官方 `.codex` 登录态复制到目标 HOME。
56
75
  *
@@ -92,23 +111,18 @@ function cloneCodexAuthState(sourceHome, targetHome) {
92
111
  *
93
112
  * 完整标准:
94
113
  * 1. 存在 `.codex/auth.json`
95
- * 2. 存在 `.codex/accounts/registry.json`
96
- * 3. 至少存在一个账户级 `*.auth.json`
114
+ * 2. `auth.json` 中存在 `access_token`
115
+ * 3. `auth.json` 中存在 `refresh_token`
116
+ * 4. `auth.json` 中存在 `account_id`
97
117
  *
98
118
  * @param codexHome 待检查的 HOME 目录。
99
119
  * @returns 为 `true` 表示登录态完整,可用于调度;否则为 `false`。
100
120
  */
101
121
  function hasCompleteCodexAuthState(codexHome) {
102
- const codexDir = getCodexDataDir(codexHome);
103
- const authPath = node_path_1.default.join(codexDir, "auth.json");
104
- const accountsDir = node_path_1.default.join(codexDir, "accounts");
105
- const registryPath = node_path_1.default.join(accountsDir, "registry.json");
106
- if (!node_fs_1.default.existsSync(authPath) || !node_fs_1.default.existsSync(registryPath) || !node_fs_1.default.existsSync(accountsDir)) {
107
- return false;
108
- }
109
- return node_fs_1.default
110
- .readdirSync(accountsDir, { withFileTypes: true })
111
- .some((entry) => entry.isFile() && entry.name.endsWith(".auth.json"));
122
+ const auth = readAuthFile(codexHome);
123
+ return Boolean(auth?.tokens?.access_token &&
124
+ auth?.tokens?.refresh_token &&
125
+ auth?.tokens?.account_id);
112
126
  }
113
127
  /**
114
128
  * 将最新认证信息回写到指定账号的 `auth.json`。
@@ -153,11 +167,12 @@ function registerManagedAccount(accountId, codexHome) {
153
167
  // 预先创建账号隔离目录,方便后续直接执行 codex login。
154
168
  node_fs_1.default.mkdirSync(home, { recursive: true });
155
169
  const primary = resolvePrimaryRegistryAccount(home);
170
+ const auth = readAuthFile(home);
156
171
  const account = {
157
172
  id: accountId,
158
173
  name: accountId,
159
174
  codex_home: home,
160
- email: primary?.email,
175
+ email: primary?.email ?? resolveEmailFromAuth(auth),
161
176
  enabled: true,
162
177
  imported_at: new Date().toISOString()
163
178
  };
package/dist/cli.js CHANGED
@@ -115,6 +115,7 @@ async function handleStart(portOverride) {
115
115
  }
116
116
  return;
117
117
  }
118
+ applyManagedCodexConfig();
118
119
  const logPath = (0, config_1.getServiceLogPath)();
119
120
  const logFd = node_fs_1.default.openSync(logPath, "a");
120
121
  const child = (0, node_child_process_1.spawn)(process.execPath, [__filename.replace(/cli\.js$/, "serve.js"), "--port", String(port)], {
@@ -136,38 +137,41 @@ function handleStop() {
136
137
  const pid = getRunningPid();
137
138
  if (!pid) {
138
139
  console.log("服务未运行");
140
+ deactivateManagedCodexConfig();
139
141
  return;
140
142
  }
141
143
  process.kill(pid, "SIGTERM");
142
144
  node_fs_1.default.rmSync((0, config_1.getPidPath)(), { force: true });
145
+ deactivateManagedCodexConfig();
143
146
  console.log(`服务已停止,PID=${pid}`);
144
147
  }
145
148
  /**
146
- * 输出当前 `codex` 需要的 provider 配置与本地 key 信息。
149
+ * 对正则元字符做转义,供动态构造匹配模式使用。
147
150
  *
148
- * @returns 无返回值。
151
+ * @param input 原始字符串。
152
+ * @returns 经过转义后的安全正则片段。
149
153
  */
150
- function handleGetConfig() {
151
- const config = (0, config_1.loadConfig)();
152
- console.log(`base_url=${`http://${config.server.host}:${config.server.port}/v1`}`);
153
- console.log(`api_key=${config.server.api_key}`);
154
- }
155
154
  function escapeRegExp(input) {
156
155
  return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
157
156
  }
158
157
  /**
159
- * codexl provider 配置写入指定的 codex config.toml
158
+ * 返回默认的 `codex config.toml` 路径。
160
159
  *
161
- * @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
162
- * @returns 无返回值。
160
+ * @returns 默认 `config.toml` 绝对路径。
163
161
  */
164
- function handleConfig(targetPathOrDir) {
162
+ function getDefaultCodexConfigPath() {
163
+ return node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
164
+ }
165
+ /**
166
+ * 生成 codexl 托管的 provider 配置块。
167
+ *
168
+ * @returns 可直接写入 `config.toml` 的配置块内容。
169
+ */
170
+ function buildManagedConfigBlock() {
165
171
  const config = (0, config_1.loadConfig)();
166
- const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : node_path_1.default.join(process.env.HOME ?? "", ".codex", "config.toml");
167
- const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
168
172
  const startMarker = "# >>> codexl managed start >>>";
169
173
  const endMarker = "# <<< codexl managed end <<<";
170
- const block = [
174
+ return [
171
175
  startMarker,
172
176
  "[model_providers.codexl]",
173
177
  'name = "codexl"',
@@ -176,6 +180,19 @@ function handleConfig(targetPathOrDir) {
176
180
  'wire_api = "responses"',
177
181
  endMarker
178
182
  ].join("\n");
183
+ }
184
+ /**
185
+ * 将 codexl provider 配置写入指定的 codex config.toml。
186
+ *
187
+ * @param targetPathOrDir 可选的 codex 配置目录或 config.toml 文件路径。
188
+ * @returns 实际写入的 `config.toml` 文件路径。
189
+ */
190
+ function applyManagedCodexConfig(targetPathOrDir, options) {
191
+ const rawTarget = targetPathOrDir ? (0, config_1.expandHome)(targetPathOrDir) : getDefaultCodexConfigPath();
192
+ const targetFile = rawTarget.endsWith(".toml") ? rawTarget : node_path_1.default.join(rawTarget, "config.toml");
193
+ const startMarker = "# >>> codexl managed start >>>";
194
+ const endMarker = "# <<< codexl managed end <<<";
195
+ const block = buildManagedConfigBlock();
179
196
  let original = "";
180
197
  if (node_fs_1.default.existsSync(targetFile)) {
181
198
  original = node_fs_1.default.readFileSync(targetFile, "utf8");
@@ -240,10 +257,31 @@ function handleConfig(targetPathOrDir) {
240
257
  }
241
258
  const nextContent = `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}\n`;
242
259
  node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
243
- console.log(`已写入: ${targetFile}`);
244
- console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
245
- console.log(`api_key=${config.server.api_key}`);
246
- console.log('提示: 已写入 codexl provider;如果原来存在 model_provider,则已切换为 codexl;model 保持不变。');
260
+ if (!options?.silent) {
261
+ const config = (0, config_1.loadConfig)();
262
+ console.log(`已写入: ${targetFile}`);
263
+ console.log(`base_url=http://${config.server.host}:${config.server.port}/v1`);
264
+ console.log(`api_key=${config.server.api_key}`);
265
+ console.log("提示: start 会自动接管 codex provider,stop 会自动恢复。");
266
+ }
267
+ return targetFile;
268
+ }
269
+ /**
270
+ * 关闭 codexl 作为当前默认 provider 的接管状态。
271
+ *
272
+ * @returns 无返回值。
273
+ */
274
+ function deactivateManagedCodexConfig() {
275
+ const targetFile = getDefaultCodexConfigPath();
276
+ if (!node_fs_1.default.existsSync(targetFile)) {
277
+ return;
278
+ }
279
+ const original = node_fs_1.default.readFileSync(targetFile, "utf8");
280
+ const nextContent = original.replace(/^(\s*)model_provider\s*=\s*"codexl"\s*$/m, '$1# model_provider = "codexl"');
281
+ if (nextContent !== original) {
282
+ node_fs_1.default.writeFileSync(targetFile, nextContent, "utf8");
283
+ console.log(`已更新: ${targetFile}`);
284
+ }
247
285
  }
248
286
  /**
249
287
  * CLI 主入口,负责命令注册与执行。
@@ -256,10 +294,9 @@ async function main() {
256
294
  (0, config_1.getCodexSwHome)();
257
295
  (0, config_1.loadConfig)();
258
296
  program
259
- .name("codexsw")
260
297
  .name("codexl")
261
298
  .description("本地 Codex 多账号切换与状态管理工具")
262
- .version("0.1.0");
299
+ .version("0.1.2");
263
300
  program
264
301
  .command("add")
265
302
  .description("登录并新增一个账号或工作空间")
@@ -292,12 +329,6 @@ async function main() {
292
329
  await handleStart(options.port);
293
330
  });
294
331
  program.command("stop").description("停止后台代理服务").action(handleStop);
295
- program.command("get").description("输出当前 base_url 和 api_key").action(handleGetConfig);
296
- program
297
- .command("config")
298
- .description("自动写入 codex 的 config.toml,默认 ~/.codex/config.toml")
299
- .argument("[codexPath]", "codex 配置目录或 config.toml 文件路径")
300
- .action(handleConfig);
301
332
  await program.parseAsync(process.argv);
302
333
  }
303
334
  void main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openxiaobu/codexl",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",