@openxiaobu/codexl 0.1.0 → 0.1.1

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
@@ -1,28 +1,19 @@
1
1
  # codexl
2
2
 
3
- Local multi-account / multi-workspace switcher for Codex.
3
+ `codexl` is a local multi-account / multi-workspace switcher for Codex.
4
4
 
5
- `codexl` 是一个本地 `Codex` 多账号 / 多工作空间切换器。
5
+ [中文文档](./docs/zh-CN.md)
6
6
 
7
- ## Overview
8
-
9
- English:
7
+ ## Features
10
8
 
11
9
  - Reuse the official `~/.codex` login state
12
10
  - Manage multiple accounts or workspaces as separate slots
13
11
  - Fetch the latest usage from the official usage endpoint
14
12
  - Expose a local provider endpoint for Codex
15
13
  - Apply local cooldown rules for temporary, 5-hour, and weekly limits
14
+ - Write a managed provider block into `~/.codex/config.toml`
16
15
 
17
- 中文:
18
-
19
- - 复用官方 `~/.codex` 登录态
20
- - 将多个账号或工作空间作为独立槽位管理
21
- - 直接调用官方 usage 接口获取最新额度
22
- - 暴露本地 provider 给 `Codex` 使用
23
- - 对临时限流、5 小时限制、周限制做本地熔断
24
-
25
- ## Install
16
+ ## Installation
26
17
 
27
18
  ```bash
28
19
  npm i -g @openxiaobu/codexl
@@ -34,39 +25,39 @@ Verify:
34
25
  codexl --help
35
26
  ```
36
27
 
28
+ This repository is the source repository.
29
+ GitHub installation from the repository URL is not supported.
30
+
37
31
  ## Quick Start
38
32
 
39
- 1. Import your current Codex login state
33
+ Import your current Codex login state:
40
34
 
41
35
  ```bash
42
36
  codexl import current ~
43
37
  ```
44
38
 
45
- 2. Check latest usage
39
+ `import` copies the official login state into `~/.codexl/homes/<name>` instead of referencing the source HOME directly.
40
+
41
+ Check latest usage:
46
42
 
47
43
  ```bash
48
44
  codexl status
49
45
  ```
50
46
 
51
- 3. Start the local proxy
47
+ Start the local proxy:
52
48
 
53
49
  ```bash
54
50
  codexl start
55
- ```
56
-
57
- Custom port:
58
-
59
- ```bash
60
51
  codexl start --port 4399
61
52
  ```
62
53
 
63
- 4. Show current local endpoint and key
54
+ Show the current local endpoint and key:
64
55
 
65
56
  ```bash
66
57
  codexl get
67
58
  ```
68
59
 
69
- 5. Write provider config into `~/.codex/config.toml`
60
+ Write provider config into `~/.codex/config.toml`:
70
61
 
71
62
  ```bash
72
63
  codexl config
@@ -85,23 +76,16 @@ codexl get
85
76
  codexl config [codexPath]
86
77
  ```
87
78
 
88
- More details: [HELP.md](./HELP.md)
89
-
90
79
  ## How `status` Works
91
80
 
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
81
+ `codexl status` does not render stale data from the official `registry.json` cache.
98
82
 
99
- 中文:
83
+ Instead it:
100
84
 
101
- 1. 从官方登录态中读取 `access_token` / `refresh_token` / `account_id`
102
- 2. 请求 `https://chatgpt.com/backend-api/wham/usage`
103
- 3. 将最新结果写入 `~/.codexl/state.json`
104
- 4. 最后读取本地最新缓存进行展示
85
+ 1. Reads `access_token`, `refresh_token`, and `account_id` from the official Codex login state
86
+ 2. Requests `https://chatgpt.com/backend-api/wham/usage`
87
+ 3. Stores the latest result in `~/.codexl/state.json`
88
+ 4. Renders the latest local cache
105
89
 
106
90
  ## Generated Codex Config
107
91
 
@@ -117,7 +101,7 @@ wire_api = "responses"
117
101
  # <<< codexl managed end <<<
118
102
  ```
119
103
 
120
- Rules:
104
+ Behavior:
121
105
 
122
106
  - If `[model_providers.codexl]` already exists, it is replaced
123
107
  - If global `model_provider` exists, it is changed to `codexl`
@@ -134,22 +118,14 @@ Rules:
134
118
  - `~/.codexl/codexl.pid`
135
119
  - `~/.codexl/logs/service.log`
136
120
 
137
- If you previously used `~/.codexsw`, it will be migrated automatically.
121
+ If you previously used `~/.codexsw`, it is migrated automatically.
138
122
 
139
123
  ## Limit Handling
140
124
 
141
- English:
142
-
143
- - Weekly limit: blocked until weekly reset time
144
- - 5-hour limit: blocked until 5-hour reset time
125
+ - Weekly limit: blocked until the weekly reset time
126
+ - 5-hour limit: blocked until the 5-hour reset time
145
127
  - Temporary limit: blocked for 5 minutes
146
128
 
147
- 中文:
148
-
149
- - 周限制:禁用到周窗口重置时间
150
- - 5 小时限制:禁用到 5 小时窗口重置时间
151
- - 临时限流:先禁用 5 分钟
152
-
153
129
  ## Repository
154
130
 
155
131
  - GitHub: https://github.com/openxiaobu/codexl
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getCodexDataDir = getCodexDataDir;
7
7
  exports.readRegistry = readRegistry;
8
8
  exports.readAuthFile = readAuthFile;
9
+ exports.cloneCodexAuthState = cloneCodexAuthState;
10
+ exports.hasCompleteCodexAuthState = hasCompleteCodexAuthState;
9
11
  exports.writeAuthFile = writeAuthFile;
10
12
  exports.resolvePrimaryRegistryAccount = resolvePrimaryRegistryAccount;
11
13
  exports.registerManagedAccount = registerManagedAccount;
@@ -49,6 +51,65 @@ function readAuthFile(codexHome) {
49
51
  }
50
52
  return JSON.parse(node_fs_1.default.readFileSync(authPath, "utf8"));
51
53
  }
54
+ /**
55
+ * 将来源 HOME 下的官方 `.codex` 登录态复制到目标 HOME。
56
+ *
57
+ * 只复制认证和账号元数据所需文件,不复制历史日志、缓存等无关内容。
58
+ *
59
+ * @param sourceHome 来源 HOME 目录。
60
+ * @param targetHome 目标 HOME 目录。
61
+ * @returns 无返回值。
62
+ * @throws 当来源目录缺少关键认证文件时抛出错误。
63
+ */
64
+ function cloneCodexAuthState(sourceHome, targetHome) {
65
+ const sourceCodexDir = getCodexDataDir(sourceHome);
66
+ const targetCodexDir = getCodexDataDir(targetHome);
67
+ const sourceAuthPath = node_path_1.default.join(sourceCodexDir, "auth.json");
68
+ const sourceAccountsDir = node_path_1.default.join(sourceCodexDir, "accounts");
69
+ const sourceRegistryPath = node_path_1.default.join(sourceAccountsDir, "registry.json");
70
+ if (!node_fs_1.default.existsSync(sourceAuthPath)) {
71
+ throw new Error(`来源目录缺少 auth.json: ${sourceAuthPath}`);
72
+ }
73
+ if (!node_fs_1.default.existsSync(sourceRegistryPath)) {
74
+ throw new Error(`来源目录缺少 registry.json: ${sourceRegistryPath}`);
75
+ }
76
+ node_fs_1.default.mkdirSync(targetCodexDir, { recursive: true });
77
+ node_fs_1.default.mkdirSync(node_path_1.default.join(targetCodexDir, "accounts"), { recursive: true });
78
+ node_fs_1.default.copyFileSync(sourceAuthPath, node_path_1.default.join(targetCodexDir, "auth.json"));
79
+ node_fs_1.default.copyFileSync(sourceRegistryPath, node_path_1.default.join(targetCodexDir, "accounts", "registry.json"));
80
+ for (const entry of node_fs_1.default.readdirSync(sourceAccountsDir, { withFileTypes: true })) {
81
+ if (!entry.isFile()) {
82
+ continue;
83
+ }
84
+ if (!entry.name.endsWith(".auth.json")) {
85
+ continue;
86
+ }
87
+ node_fs_1.default.copyFileSync(node_path_1.default.join(sourceAccountsDir, entry.name), node_path_1.default.join(targetCodexDir, "accounts", entry.name));
88
+ }
89
+ }
90
+ /**
91
+ * 检查某个 HOME 下的官方登录态是否完整。
92
+ *
93
+ * 完整标准:
94
+ * 1. 存在 `.codex/auth.json`
95
+ * 2. 存在 `.codex/accounts/registry.json`
96
+ * 3. 至少存在一个账户级 `*.auth.json`
97
+ *
98
+ * @param codexHome 待检查的 HOME 目录。
99
+ * @returns 为 `true` 表示登录态完整,可用于调度;否则为 `false`。
100
+ */
101
+ 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"));
112
+ }
52
113
  /**
53
114
  * 将最新认证信息回写到指定账号的 `auth.json`。
54
115
  *
package/dist/cli.js CHANGED
@@ -29,17 +29,20 @@ async function handleStatus() {
29
29
  console.log(`available=${available} cooldown=${cooldown} weekly_limited=${weeklyLimited}`);
30
30
  }
31
31
  /**
32
- * 将已有的 Codex HOME 目录纳入 codexl 管理。
32
+ * 将已有的 Codex HOME 目录中的登录态复制到 codexl 自己的隔离目录并纳入管理。
33
33
  *
34
34
  * @param accountId 本地账号标识。
35
35
  * @param codexHome 现有 HOME 目录;若未传则默认使用当前用户 HOME。
36
36
  * @returns 无返回值。
37
37
  */
38
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);
39
+ const sourceHome = codexHome ? (0, config_1.expandHome)(codexHome) : process.env.HOME ?? "";
40
+ const managedHome = (0, config_1.getManagedHome)(accountId);
41
+ (0, account_store_1.cloneCodexAuthState)(sourceHome, managedHome);
42
+ const account = (0, account_store_1.registerManagedAccount)(accountId, managedHome);
41
43
  console.log(`账号已导入: ${account.id}`);
42
- console.log(`来源 HOME: ${account.codex_home}`);
44
+ console.log(`来源 HOME: ${sourceHome}`);
45
+ console.log(`已复制到: ${account.codex_home}`);
43
46
  }
44
47
  /**
45
48
  * 执行隔离登录流程,将账号录入到 codexl 管理目录。
package/dist/login.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.loginManagedAccount = loginManagedAccount;
4
7
  const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = __importDefault(require("node:fs"));
5
9
  const account_store_1 = require("./account-store");
6
10
  const config_1 = require("./config");
7
11
  /**
@@ -13,7 +17,7 @@ const config_1 = require("./config");
13
17
  */
14
18
  async function loginManagedAccount(accountId) {
15
19
  const managedHome = (0, config_1.getManagedHome)(accountId);
16
- (0, account_store_1.registerManagedAccount)(accountId, managedHome);
20
+ node_fs_1.default.mkdirSync(managedHome, { recursive: true });
17
21
  return await new Promise((resolve, reject) => {
18
22
  const child = (0, node_child_process_1.spawn)("codex", ["login"], {
19
23
  env: {
@@ -24,6 +28,10 @@ async function loginManagedAccount(accountId) {
24
28
  });
25
29
  child.on("exit", (code) => {
26
30
  if (code === 0) {
31
+ if (!(0, account_store_1.hasCompleteCodexAuthState)(managedHome)) {
32
+ reject(new Error("codex login 已退出,但未检测到完整登录态,请重新登录"));
33
+ return;
34
+ }
27
35
  (0, account_store_1.registerManagedAccount)(accountId, managedHome);
28
36
  resolve(managedHome);
29
37
  return;
package/dist/status.js CHANGED
@@ -1,12 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.collectAccountStatuses = collectAccountStatuses;
7
4
  exports.renderStatusTable = renderStatusTable;
8
- const node_fs_1 = __importDefault(require("node:fs"));
9
- const node_path_1 = __importDefault(require("node:path"));
10
5
  const config_1 = require("./config");
11
6
  const account_store_1 = require("./account-store");
12
7
  const state_1 = require("./state");
@@ -44,9 +39,7 @@ function formatReset(unixSeconds) {
44
39
  function collectAccountStatuses() {
45
40
  const config = (0, config_1.loadConfig)();
46
41
  return config.accounts.map((account) => {
47
- const codexDir = (0, account_store_1.getCodexDataDir)(account.codex_home);
48
- const registryPath = node_path_1.default.join(codexDir, "accounts", "registry.json");
49
- const exists = node_fs_1.default.existsSync(registryPath);
42
+ const exists = (0, account_store_1.hasCompleteCodexAuthState)(account.codex_home);
50
43
  const primary = exists ? (0, account_store_1.resolvePrimaryRegistryAccount)(account.codex_home) : null;
51
44
  const usageCache = (0, state_1.getUsageCache)(account.id);
52
45
  const activeEmail = usageCache?.email ?? primary?.email ?? account.email;
@@ -80,7 +73,7 @@ function collectAccountStatuses() {
80
73
  !isFiveHourLimited &&
81
74
  !isWeeklyLimited &&
82
75
  !localBlocked,
83
- sourcePath: codexDir
76
+ sourcePath: account.codex_home
84
77
  };
85
78
  });
86
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openxiaobu/codexl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",
@@ -13,6 +13,7 @@
13
13
  "scripts": {
14
14
  "clean": "rm -rf dist",
15
15
  "build": "tsc -p tsconfig.json && chmod +x dist/cli.js dist/serve.js",
16
+ "prepublishOnly": "npm run build",
16
17
  "dev": "tsx src/cli.ts",
17
18
  "check": "tsc --noEmit -p tsconfig.json"
18
19
  },