@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 +25 -49
- package/dist/account-store.js +61 -0
- package/dist/cli.js +7 -4
- package/dist/login.js +9 -1
- package/dist/status.js +2 -9
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,28 +1,19 @@
|
|
|
1
1
|
# codexl
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`codexl` is a local multi-account / multi-workspace switcher for Codex.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[中文文档](./docs/zh-CN.md)
|
|
6
6
|
|
|
7
|
-
##
|
|
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
|
-
|
|
33
|
+
Import your current Codex login state:
|
|
40
34
|
|
|
41
35
|
```bash
|
|
42
36
|
codexl import current ~
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
Show the current local endpoint and key:
|
|
64
55
|
|
|
65
56
|
```bash
|
|
66
57
|
codexl get
|
|
67
58
|
```
|
|
68
59
|
|
|
69
|
-
|
|
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
|
-
|
|
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.
|
|
102
|
-
2.
|
|
103
|
-
3.
|
|
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
|
-
|
|
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
|
|
121
|
+
If you previously used `~/.codexsw`, it is migrated automatically.
|
|
138
122
|
|
|
139
123
|
## Limit Handling
|
|
140
124
|
|
|
141
|
-
|
|
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
|
package/dist/account-store.js
CHANGED
|
@@ -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
|
|
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
|
|
40
|
-
const
|
|
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: ${
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
},
|