@wangxt0223/codex-switcher 0.6.1 → 0.6.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/CHANGELOG.md +13 -0
- package/README.en.md +43 -1
- package/README.md +43 -1
- package/package.json +1 -1
- package/plugins/codex-switcher/README.en.md +4 -0
- package/plugins/codex-switcher/README.md +4 -0
- package/plugins/codex-switcher/scripts/codex-switcher +231 -3
- package/plugins/codex-switcher/scripts/profile-metrics.py +13 -6
- package/plugins/codex-switcher/scripts/test-switcher.sh +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.3 - 2026-04-12
|
|
4
|
+
|
|
5
|
+
- Changed `list` email rendering to show plain email only (removed `(account)` prefix).
|
|
6
|
+
- Reworked root Chinese/English README command reference into full command tables based on current capabilities.
|
|
7
|
+
- Updated smoke-test assertions for the new email column format.
|
|
8
|
+
|
|
9
|
+
## 0.6.2 - 2026-04-12
|
|
10
|
+
|
|
11
|
+
- Added usage-API proxy auto-detection (manual proxy > env proxy > macOS system proxy).
|
|
12
|
+
- Added `proxy` source display: `(manual)`, `(auto:env)`, `(auto:system)`, or `off`.
|
|
13
|
+
- Kept proxy scope limited to usage API calls used by `list`/`proxy test`.
|
|
14
|
+
- Added smoke-test coverage for env-proxy auto-detection and isolated proxy behavior.
|
|
15
|
+
|
|
3
16
|
## 0.6.1 - 2026-04-12
|
|
4
17
|
|
|
5
18
|
- Fixed symlink invocation path resolution for `codex-sw` / `codex-switcher`, so global npm installs can always find bundled scripts.
|
package/README.en.md
CHANGED
|
@@ -63,7 +63,49 @@ codex-switcher account use corp --env project-a
|
|
|
63
63
|
|
|
64
64
|
## Command reference
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Commands below use `codex-sw` (all are equivalent under `codex-switcher`):
|
|
67
|
+
|
|
68
|
+
| Category | Command | Description |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| Env | `codex-sw env list` | List all envs with CLI/App current markers |
|
|
71
|
+
| Env | `codex-sw env create <env> [--empty\|--from-default\|--from-env <src>]` | Create env from empty/default/another env |
|
|
72
|
+
| Env | `codex-sw env use <env> [--target cli\|app\|both]` | Switch env pointer for CLI/App |
|
|
73
|
+
| Env | `codex-sw env remove <env> [--force]` | Remove env |
|
|
74
|
+
| Env | `codex-sw env current [cli\|app]` | Show current env pointer |
|
|
75
|
+
| Env | `codex-sw env path [env]` | Print exportable `CODEX_HOME` path |
|
|
76
|
+
| Account | `codex-sw account list [--env <env>]` | List accounts in env with current markers |
|
|
77
|
+
| Account | `codex-sw account add <account> [--env <env>]` | Add account slot |
|
|
78
|
+
| Account | `codex-sw account remove <account> [--env <env>] [--force]` | Remove account slot |
|
|
79
|
+
| Account | `codex-sw account login <account> [--env <env>] [--target cli\|app\|both] [--sync\|--no-sync]` | Login account and persist auth |
|
|
80
|
+
| Account | `codex-sw account use <account> [--env <env>] [--target cli\|app\|both] [--sync\|--no-sync]` | Switch to account |
|
|
81
|
+
| Account | `codex-sw account logout [account] [--env <env>] [--target cli\|app\|both]` | Logout account |
|
|
82
|
+
| Account | `codex-sw account current [cli\|app]` | Show current env/account pointer |
|
|
83
|
+
| Usage proxy | `codex-sw proxy [<host:port>\|off\|test]` | Configure/test usage API proxy (only affects `list`) |
|
|
84
|
+
| Query/Run | `codex-sw list` | Show `ENV/ACCOUNT/EMAIL/PLAN/5H/WEEKLY/LAST ACTIVITY` |
|
|
85
|
+
| Query/Run | `codex-sw status` | Show login status for current CLI/App pointers |
|
|
86
|
+
| Query/Run | `codex-sw current [cli\|app]` | Show current env/account |
|
|
87
|
+
| Query/Run | `codex-sw exec -- <codex args...>` | Run `codex` under current CLI env/account |
|
|
88
|
+
| Compat | `codex-sw login [account] [--sync\|--no-sync]` | Compatibility alias for CLI login |
|
|
89
|
+
| Compat | `codex-sw logout [account]` | Compatibility alias for CLI logout |
|
|
90
|
+
| Compat | `codex-sw use <account> [--sync\|--no-sync] [--launch\|--no-launch] [-- <codex args...>]` | Compatibility alias for CLI switch |
|
|
91
|
+
| Compat | `codex-sw switch <account> [--sync\|--no-sync] [--launch\|--no-launch] [-- <codex args...>]` | Same as `use` |
|
|
92
|
+
| Compat | `codex-sw add <account>` | Compatibility alias for `account add` |
|
|
93
|
+
| Compat | `codex-sw remove <account> [--force]` | Compatibility alias for `account remove` |
|
|
94
|
+
| Compat | `codex-sw import-default <env> [--with-auth] [--force]` | Import default env data into target env |
|
|
95
|
+
| App | `codex-sw app open [account] [-- <app args...>]` | Open Codex App under account |
|
|
96
|
+
| App | `codex-sw app use <account> [-- <app args...>]` | Switch App account (alias of open) |
|
|
97
|
+
| App | `codex-sw app logout [account]` | Logout App account |
|
|
98
|
+
| App | `codex-sw app status` | Show managed App process status |
|
|
99
|
+
| App | `codex-sw app stop` | Stop managed App process |
|
|
100
|
+
| App | `codex-sw app current` | Show current App env/account |
|
|
101
|
+
| Maintenance | `codex-sw init [--shell zsh\|bash] [--dry-run]` | Initialize PATH bootstrap |
|
|
102
|
+
| Maintenance | `codex-sw upgrade [--dry-run]` | Upgrade from npm |
|
|
103
|
+
| Maintenance | `codex-sw recover [--dry-run]` | Recover corrupted pointers |
|
|
104
|
+
| Maintenance | `codex-sw check` | Basic health checks |
|
|
105
|
+
| Maintenance | `codex-sw doctor [--fix]` | Deep diagnostics and optional auto-fix |
|
|
106
|
+
| Maintenance | `codex-sw --help` | Show full help |
|
|
107
|
+
|
|
108
|
+
Plugin-level docs:
|
|
67
109
|
- Chinese: `plugins/codex-switcher/README.md`
|
|
68
110
|
- English: `plugins/codex-switcher/README.en.md`
|
|
69
111
|
|
package/README.md
CHANGED
|
@@ -60,7 +60,49 @@ codex-switcher account use corp --env project-a
|
|
|
60
60
|
|
|
61
61
|
## 命令参考
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
以下以 `codex-sw` 为例(`codex-switcher` 为等价兼容命令):
|
|
64
|
+
|
|
65
|
+
| 分类 | 命令 | 说明 |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Env 管理 | `codex-sw env list` | 列出所有 env,并标记 CLI/App 当前 env |
|
|
68
|
+
| Env 管理 | `codex-sw env create <env> [--empty\|--from-default\|--from-env <src>]` | 创建 env(空目录或从已有 env 同步数据) |
|
|
69
|
+
| Env 管理 | `codex-sw env use <env> [--target cli\|app\|both]` | 切换 CLI/App 使用的 env |
|
|
70
|
+
| Env 管理 | `codex-sw env remove <env> [--force]` | 删除 env(必要时强制) |
|
|
71
|
+
| Env 管理 | `codex-sw env current [cli\|app]` | 查看当前 env 指针 |
|
|
72
|
+
| Env 管理 | `codex-sw env path [env]` | 输出 env 对应 `CODEX_HOME` 导出语句 |
|
|
73
|
+
| 账号管理 | `codex-sw account list [--env <env>]` | 列出 env 下账号并标记当前账号 |
|
|
74
|
+
| 账号管理 | `codex-sw account add <account> [--env <env>]` | 创建账号槽位(不登录) |
|
|
75
|
+
| 账号管理 | `codex-sw account remove <account> [--env <env>] [--force]` | 删除账号槽位 |
|
|
76
|
+
| 账号管理 | `codex-sw account login <account> [--env <env>] [--target cli\|app\|both] [--sync\|--no-sync]` | 在目标 env 登录并保存账号 `auth.json` |
|
|
77
|
+
| 账号管理 | `codex-sw account use <account> [--env <env>] [--target cli\|app\|both] [--sync\|--no-sync]` | 切换到目标账号 |
|
|
78
|
+
| 账号管理 | `codex-sw account logout [account] [--env <env>] [--target cli\|app\|both]` | 注销账号(删除对应 auth) |
|
|
79
|
+
| 账号管理 | `codex-sw account current [cli\|app]` | 查看当前 env/account 指针 |
|
|
80
|
+
| 用量代理 | `codex-sw proxy [<host:port>\|off\|test]` | 设置/关闭/测试“用量 API”代理(仅影响 `list`) |
|
|
81
|
+
| 查询执行 | `codex-sw list` | 展示 `ENV/ACCOUNT/EMAIL/PLAN/5H/WEEKLY/LAST ACTIVITY` |
|
|
82
|
+
| 查询执行 | `codex-sw status` | 检查 CLI/App 当前登录状态 |
|
|
83
|
+
| 查询执行 | `codex-sw current [cli\|app]` | 查看当前 env/account |
|
|
84
|
+
| 查询执行 | `codex-sw exec -- <codex args...>` | 在当前 CLI env/account 下执行 `codex` |
|
|
85
|
+
| 兼容命令 | `codex-sw login [account] [--sync\|--no-sync]` | 登录当前 CLI 账号(兼容入口) |
|
|
86
|
+
| 兼容命令 | `codex-sw logout [account]` | 注销当前 CLI 账号(兼容入口) |
|
|
87
|
+
| 兼容命令 | `codex-sw use <account> [--sync\|--no-sync] [--launch\|--no-launch] [-- <codex args...>]` | 切换 CLI 账号(兼容入口) |
|
|
88
|
+
| 兼容命令 | `codex-sw switch <account> [--sync\|--no-sync] [--launch\|--no-launch] [-- <codex args...>]` | `use` 的等价命令 |
|
|
89
|
+
| 兼容命令 | `codex-sw add <account>` | `account add` 的兼容入口 |
|
|
90
|
+
| 兼容命令 | `codex-sw remove <account> [--force]` | `account remove` 的兼容入口 |
|
|
91
|
+
| 兼容命令 | `codex-sw import-default <env> [--with-auth] [--force]` | 从默认 env 导入数据到指定 env |
|
|
92
|
+
| App 管理 | `codex-sw app open [account] [-- <app args...>]` | 以指定账号打开 Codex App |
|
|
93
|
+
| App 管理 | `codex-sw app use <account> [-- <app args...>]` | 切换 App 到指定账号(内部等价于 open) |
|
|
94
|
+
| App 管理 | `codex-sw app logout [account]` | 注销 App 当前账号 |
|
|
95
|
+
| App 管理 | `codex-sw app status` | 查看 App 管理进程状态 |
|
|
96
|
+
| App 管理 | `codex-sw app stop` | 停止由工具托管启动的 App 进程 |
|
|
97
|
+
| App 管理 | `codex-sw app current` | 查看 App 当前 env/account |
|
|
98
|
+
| 维护命令 | `codex-sw init [--shell zsh\|bash] [--dry-run]` | 初始化 PATH 快捷命令 |
|
|
99
|
+
| 维护命令 | `codex-sw upgrade [--dry-run]` | 升级到最新 npm 版本 |
|
|
100
|
+
| 维护命令 | `codex-sw recover [--dry-run]` | 自动恢复损坏指针 |
|
|
101
|
+
| 维护命令 | `codex-sw check` | 基础健康检查 |
|
|
102
|
+
| 维护命令 | `codex-sw doctor [--fix]` | 深度检查并可选自动修复 |
|
|
103
|
+
| 维护命令 | `codex-sw --help` | 查看完整帮助 |
|
|
104
|
+
|
|
105
|
+
插件侧详细说明:
|
|
64
106
|
|
|
65
107
|
- 中文:`plugins/codex-switcher/README.md`
|
|
66
108
|
- English:`plugins/codex-switcher/README.en.md`
|
package/package.json
CHANGED
|
@@ -34,6 +34,8 @@ codex-switcher account use <account> [--env <env>] [--target cli|app|both] [--sy
|
|
|
34
34
|
codex-switcher account logout [account] [--env <env>] [--target cli|app|both]
|
|
35
35
|
codex-switcher account current [cli|app]
|
|
36
36
|
|
|
37
|
+
codex-switcher proxy [<host:port>|off|test]
|
|
38
|
+
|
|
37
39
|
codex-switcher list
|
|
38
40
|
codex-switcher status
|
|
39
41
|
codex-switcher current [cli|app]
|
|
@@ -77,6 +79,8 @@ Usage data strategy:
|
|
|
77
79
|
- API first (`chatgpt.com/backend-api/wham/usage`)
|
|
78
80
|
- Auto fallback to local `sessions/*.jsonl` on API failure
|
|
79
81
|
- `LAST ACTIVITY` appends source marker `(api)` or `(local)`
|
|
82
|
+
- You can configure a dedicated proxy for this usage API via `codex-switcher proxy 127.0.0.1:7899` (only affects `list`)
|
|
83
|
+
- If no manual proxy is configured, env/system proxy settings are auto-detected for usage API requests
|
|
80
84
|
|
|
81
85
|
## Compatibility Commands
|
|
82
86
|
|
|
@@ -34,6 +34,8 @@ codex-switcher account use <account> [--env <env>] [--target cli|app|both] [--sy
|
|
|
34
34
|
codex-switcher account logout [account] [--env <env>] [--target cli|app|both]
|
|
35
35
|
codex-switcher account current [cli|app]
|
|
36
36
|
|
|
37
|
+
codex-switcher proxy [<host:port>|off|test]
|
|
38
|
+
|
|
37
39
|
codex-switcher list
|
|
38
40
|
codex-switcher status
|
|
39
41
|
codex-switcher current [cli|app]
|
|
@@ -77,6 +79,8 @@ codex-switcher account use corp --env project-a
|
|
|
77
79
|
- 默认优先 API(`chatgpt.com/backend-api/wham/usage`)
|
|
78
80
|
- API 失败自动回退本地 `sessions/*.jsonl`
|
|
79
81
|
- `LAST ACTIVITY` 列会追加 `(api)` 或 `(local)` 标记来源
|
|
82
|
+
- 可通过 `codex-switcher proxy 127.0.0.1:7899` 为该用量 API 单独配置代理(仅影响 `list`)
|
|
83
|
+
- 未手动设置时会自动检测环境变量/系统代理并用于用量 API 请求
|
|
80
84
|
|
|
81
85
|
## 兼容命令
|
|
82
86
|
|
|
@@ -28,6 +28,9 @@ LOCK_DIR="$STATE_DIR/.lock"
|
|
|
28
28
|
LOCK_WAIT_SECONDS="${CODEX_SWITCHER_LOCK_WAIT_SECONDS:-10}"
|
|
29
29
|
|
|
30
30
|
SCRIPT_NAME="${CODEX_SWITCHER_INVOKED_AS:-$(basename "$0")}"
|
|
31
|
+
USAGE_API_ENDPOINT="https://chatgpt.com/backend-api/wham/usage"
|
|
32
|
+
USAGE_PROXY_FILE="$STATE_DIR/usage_proxy"
|
|
33
|
+
DISABLE_SYSTEM_PROXY_DETECT="${CODEX_SWITCHER_DISABLE_SYSTEM_PROXY_DETECT:-false}"
|
|
31
34
|
|
|
32
35
|
usage() {
|
|
33
36
|
cat <<'USAGE'
|
|
@@ -47,6 +50,8 @@ Usage:
|
|
|
47
50
|
codex-sw account logout [account] [--env <env>] [--target cli|app|both]
|
|
48
51
|
codex-sw account current [cli|app]
|
|
49
52
|
|
|
53
|
+
codex-sw proxy [<host:port>|off|test]
|
|
54
|
+
|
|
50
55
|
codex-sw list
|
|
51
56
|
codex-sw status
|
|
52
57
|
codex-sw current [cli|app]
|
|
@@ -79,6 +84,8 @@ Compatibility:
|
|
|
79
84
|
Notes:
|
|
80
85
|
- Built-in env "default" always maps to ~/.codex (or CODEX_SWITCHER_DEFAULT_HOME).
|
|
81
86
|
- Same-env account switch only swaps auth.json and does not sync shared data.
|
|
87
|
+
- `proxy` only affects usage API calls used by `list`.
|
|
88
|
+
- If manual proxy is not set, usage API proxy is auto-detected from env/system proxy settings.
|
|
82
89
|
- `list` prints: EMAIL / PLAN / 5H USAGE / WEEKLY USAGE / LAST ACTIVITY (+ env/account context).
|
|
83
90
|
- Usage metrics are API-first and automatically fallback to local sessions; source is shown as (api|local).
|
|
84
91
|
USAGE
|
|
@@ -197,6 +204,116 @@ current_account_file() {
|
|
|
197
204
|
echo "$STATE_DIR/current_${target}_account"
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
normalize_usage_proxy_value() {
|
|
208
|
+
local raw="${1:-}"
|
|
209
|
+
local value
|
|
210
|
+
value="$(echo "$raw" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
211
|
+
[[ -n "$value" ]] || return 1
|
|
212
|
+
[[ "$value" != *[[:space:]]* ]] || return 1
|
|
213
|
+
case "$value" in
|
|
214
|
+
http://*|https://*|socks5://*)
|
|
215
|
+
echo "$value"
|
|
216
|
+
;;
|
|
217
|
+
*)
|
|
218
|
+
echo "http://$value"
|
|
219
|
+
;;
|
|
220
|
+
esac
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
read_usage_proxy() {
|
|
224
|
+
local value
|
|
225
|
+
[[ -f "$USAGE_PROXY_FILE" ]] || return 1
|
|
226
|
+
value="$(head -n 1 "$USAGE_PROXY_FILE" 2>/dev/null || true)"
|
|
227
|
+
normalize_usage_proxy_value "$value"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
detect_usage_proxy_from_env() {
|
|
231
|
+
local key raw normalized
|
|
232
|
+
for key in CODEX_SWITCHER_USAGE_PROXY HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy; do
|
|
233
|
+
raw="${!key:-}"
|
|
234
|
+
[[ -n "$raw" ]] || continue
|
|
235
|
+
normalized="$(normalize_usage_proxy_value "$raw" || true)"
|
|
236
|
+
[[ -n "$normalized" ]] || continue
|
|
237
|
+
echo "$normalized"
|
|
238
|
+
return 0
|
|
239
|
+
done
|
|
240
|
+
return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
detect_usage_proxy_from_system_macos() {
|
|
244
|
+
[[ "$DISABLE_SYSTEM_PROXY_DETECT" != "true" ]] || return 1
|
|
245
|
+
[[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] || return 1
|
|
246
|
+
command -v scutil >/dev/null 2>&1 || return 1
|
|
247
|
+
|
|
248
|
+
local dump host port enabled normalized
|
|
249
|
+
dump="$(scutil --proxy 2>/dev/null || true)"
|
|
250
|
+
[[ -n "$dump" ]] || return 1
|
|
251
|
+
|
|
252
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
253
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
254
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPSEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
255
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
256
|
+
normalized="$(normalize_usage_proxy_value "http://$host:$port" || true)"
|
|
257
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
261
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
262
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*HTTPEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
263
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
264
|
+
normalized="$(normalize_usage_proxy_value "http://$host:$port" || true)"
|
|
265
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
host="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSProxy[[:space:]]*:[[:space:]]*(.+)$/\1/p' | head -n 1)"
|
|
269
|
+
port="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSPort[[:space:]]*:[[:space:]]*([0-9]+)$/\1/p' | head -n 1)"
|
|
270
|
+
enabled="$(echo "$dump" | sed -nE 's/^[[:space:]]*SOCKSEnable[[:space:]]*:[[:space:]]*([01])$/\1/p' | head -n 1)"
|
|
271
|
+
if [[ "$enabled" == "1" && -n "$host" && -n "$port" ]]; then
|
|
272
|
+
normalized="$(normalize_usage_proxy_value "socks5://$host:$port" || true)"
|
|
273
|
+
[[ -n "$normalized" ]] && echo "$normalized" && return 0
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
return 1
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
resolve_usage_proxy_with_source() {
|
|
280
|
+
local value
|
|
281
|
+
|
|
282
|
+
value="$(read_usage_proxy || true)"
|
|
283
|
+
if [[ -n "$value" ]]; then
|
|
284
|
+
printf 'manual\t%s\n' "$value"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
value="$(detect_usage_proxy_from_env || true)"
|
|
289
|
+
if [[ -n "$value" ]]; then
|
|
290
|
+
printf 'auto-env\t%s\n' "$value"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
value="$(detect_usage_proxy_from_system_macos || true)"
|
|
295
|
+
if [[ -n "$value" ]]; then
|
|
296
|
+
printf 'auto-system\t%s\n' "$value"
|
|
297
|
+
return 0
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
return 1
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
set_usage_proxy() {
|
|
304
|
+
local value="$1"
|
|
305
|
+
local file tmp
|
|
306
|
+
file="$USAGE_PROXY_FILE"
|
|
307
|
+
tmp="${file}.tmp"
|
|
308
|
+
printf '%s\n' "$value" > "$tmp"
|
|
309
|
+
chmod 600 "$tmp" 2>/dev/null || true
|
|
310
|
+
mv "$tmp" "$file"
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
clear_usage_proxy() {
|
|
314
|
+
rm -f -- "$USAGE_PROXY_FILE"
|
|
315
|
+
}
|
|
316
|
+
|
|
200
317
|
set_current_env() {
|
|
201
318
|
local target="$1"
|
|
202
319
|
local env="$2"
|
|
@@ -586,10 +703,15 @@ collect_profile_metrics_tsv() {
|
|
|
586
703
|
local account="$1"
|
|
587
704
|
local auth_file="$2"
|
|
588
705
|
local data_path="$3"
|
|
706
|
+
local usage_proxy="${4:-}"
|
|
589
707
|
local script_path
|
|
590
708
|
script_path="$(metrics_script_path)"
|
|
591
709
|
[[ -f "$script_path" ]] || return 1
|
|
592
|
-
|
|
710
|
+
if [[ -n "$usage_proxy" ]]; then
|
|
711
|
+
python3 "$script_path" --account-name "$account" --auth-file "$auth_file" --data-path "$data_path" --usage-proxy "$usage_proxy" 2>/dev/null
|
|
712
|
+
else
|
|
713
|
+
python3 "$script_path" --account-name "$account" --auth-file "$auth_file" --data-path "$data_path" 2>/dev/null
|
|
714
|
+
fi
|
|
593
715
|
}
|
|
594
716
|
|
|
595
717
|
cmd_env_list() {
|
|
@@ -924,15 +1046,104 @@ cmd_account_current() {
|
|
|
924
1046
|
esac
|
|
925
1047
|
}
|
|
926
1048
|
|
|
1049
|
+
cmd_proxy_show() {
|
|
1050
|
+
local info source value
|
|
1051
|
+
info="$(resolve_usage_proxy_with_source || true)"
|
|
1052
|
+
if [[ -z "$info" ]]; then
|
|
1053
|
+
echo "usage_api_proxy: off"
|
|
1054
|
+
return 0
|
|
1055
|
+
fi
|
|
1056
|
+
IFS=$'\t' read -r source value <<< "$info"
|
|
1057
|
+
case "$source" in
|
|
1058
|
+
manual)
|
|
1059
|
+
echo "usage_api_proxy: $value (manual)"
|
|
1060
|
+
;;
|
|
1061
|
+
auto-env)
|
|
1062
|
+
echo "usage_api_proxy: $value (auto:env)"
|
|
1063
|
+
;;
|
|
1064
|
+
auto-system)
|
|
1065
|
+
echo "usage_api_proxy: $value (auto:system)"
|
|
1066
|
+
;;
|
|
1067
|
+
*)
|
|
1068
|
+
echo "usage_api_proxy: $value"
|
|
1069
|
+
;;
|
|
1070
|
+
esac
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
cmd_proxy_set() {
|
|
1074
|
+
local raw="$1"
|
|
1075
|
+
local normalized
|
|
1076
|
+
normalized="$(normalize_usage_proxy_value "$raw")" || err "invalid proxy '$raw' (expected host:port or scheme://host:port)"
|
|
1077
|
+
set_usage_proxy "$normalized"
|
|
1078
|
+
log_event INFO "usage_proxy_set value=$normalized"
|
|
1079
|
+
echo "Set usage API proxy: $normalized"
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
cmd_proxy_off() {
|
|
1083
|
+
clear_usage_proxy
|
|
1084
|
+
log_event INFO "usage_proxy_off"
|
|
1085
|
+
echo "Manual usage API proxy disabled"
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
cmd_proxy_test() {
|
|
1089
|
+
local proxy env_name account auth_file token body_file http_code body_preview info source
|
|
1090
|
+
info="$(resolve_usage_proxy_with_source || true)"
|
|
1091
|
+
[[ -n "$info" ]] || err "usage API proxy is off and no auto proxy detected. run: $SCRIPT_NAME proxy <host:port>"
|
|
1092
|
+
IFS=$'\t' read -r source proxy <<< "$info"
|
|
1093
|
+
|
|
1094
|
+
env_name="$(read_current_env cli || true)"
|
|
1095
|
+
account="$(read_current_account cli || true)"
|
|
1096
|
+
auth_file="$(account_auth_path "$env_name" "$account")"
|
|
1097
|
+
[[ -f "$auth_file" ]] || auth_file="$(env_home_path "$env_name")/auth.json"
|
|
1098
|
+
[[ -f "$auth_file" ]] || err "auth.json not found for current CLI env/account: $env_name/$account"
|
|
1099
|
+
|
|
1100
|
+
token="$(python3 - "$auth_file" <<'PY'
|
|
1101
|
+
import json, sys
|
|
1102
|
+
path = sys.argv[1]
|
|
1103
|
+
try:
|
|
1104
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
1105
|
+
data = json.load(f)
|
|
1106
|
+
token = ((data.get("tokens") or {}).get("access_token") or "").strip()
|
|
1107
|
+
print(token)
|
|
1108
|
+
except Exception:
|
|
1109
|
+
print("")
|
|
1110
|
+
PY
|
|
1111
|
+
)"
|
|
1112
|
+
[[ -n "$token" ]] || err "access_token missing in $auth_file"
|
|
1113
|
+
|
|
1114
|
+
body_file="$(mktemp /tmp/codex-sw-proxy-test.XXXXXX)"
|
|
1115
|
+
http_code="$(HTTPS_PROXY="$proxy" HTTP_PROXY="$proxy" curl -sS -o "$body_file" -w '%{http_code}' \
|
|
1116
|
+
-H "Authorization: Bearer $token" \
|
|
1117
|
+
-H "Accept: application/json" \
|
|
1118
|
+
-H "User-Agent: Mozilla/5.0" \
|
|
1119
|
+
"$USAGE_API_ENDPOINT" || true)"
|
|
1120
|
+
body_preview="$(head -c 160 "$body_file" 2>/dev/null || true)"
|
|
1121
|
+
rm -f -- "$body_file"
|
|
1122
|
+
|
|
1123
|
+
if [[ "$http_code" == "200" ]]; then
|
|
1124
|
+
echo "usage_api_proxy_test: ok (http=200, source=$source, proxy=$proxy, env/account=$env_name/$account)"
|
|
1125
|
+
return 0
|
|
1126
|
+
fi
|
|
1127
|
+
|
|
1128
|
+
echo "usage_api_proxy_test: failed (http=${http_code:-000}, source=$source, proxy=$proxy, env/account=$env_name/$account)" >&2
|
|
1129
|
+
[[ -n "$body_preview" ]] && echo "response_preview: $body_preview" >&2
|
|
1130
|
+
return 1
|
|
1131
|
+
}
|
|
1132
|
+
|
|
927
1133
|
cmd_list() {
|
|
928
1134
|
local cli_env app_env cli_account app_account
|
|
929
1135
|
local env account marks auth_file home metrics
|
|
930
1136
|
local email plan usage_5h usage_weekly last_activity source
|
|
1137
|
+
local usage_proxy="" usage_info="" usage_proxy_source=""
|
|
931
1138
|
|
|
932
1139
|
cli_env="$(read_current_env cli || true)"
|
|
933
1140
|
app_env="$(read_current_env app || true)"
|
|
934
1141
|
cli_account="$(read_current_account cli || true)"
|
|
935
1142
|
app_account="$(read_current_account app || true)"
|
|
1143
|
+
usage_info="$(resolve_usage_proxy_with_source || true)"
|
|
1144
|
+
if [[ -n "$usage_info" ]]; then
|
|
1145
|
+
IFS=$'\t' read -r usage_proxy_source usage_proxy <<< "$usage_info"
|
|
1146
|
+
fi
|
|
936
1147
|
|
|
937
1148
|
printf '%-12s %-30s %-44s %-10s %-16s %-18s %s\n' "ENV" "ACCOUNT" "EMAIL" "PLAN" "5H USAGE" "WEEKLY USAGE" "LAST ACTIVITY"
|
|
938
1149
|
while IFS= read -r env; do
|
|
@@ -953,7 +1164,7 @@ cmd_list() {
|
|
|
953
1164
|
auth_file="$(account_auth_path "$env" "$account")"
|
|
954
1165
|
[[ -f "$auth_file" ]] || auth_file="$home/auth.json"
|
|
955
1166
|
|
|
956
|
-
metrics="$(collect_profile_metrics_tsv "$account" "$auth_file" "$home" || true)"
|
|
1167
|
+
metrics="$(collect_profile_metrics_tsv "$account" "$auth_file" "$home" "$usage_proxy" || true)"
|
|
957
1168
|
if [[ -n "$metrics" ]]; then
|
|
958
1169
|
IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
|
|
959
1170
|
fi
|
|
@@ -976,7 +1187,7 @@ cmd_list() {
|
|
|
976
1187
|
usage_weekly=""
|
|
977
1188
|
last_activity=""
|
|
978
1189
|
source=""
|
|
979
|
-
metrics="$(collect_profile_metrics_tsv "-" "$home/auth.json" "$home" || true)"
|
|
1190
|
+
metrics="$(collect_profile_metrics_tsv "-" "$home/auth.json" "$home" "$usage_proxy" || true)"
|
|
980
1191
|
if [[ -n "$metrics" ]]; then
|
|
981
1192
|
IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
|
|
982
1193
|
fi
|
|
@@ -1722,6 +1933,23 @@ main() {
|
|
|
1722
1933
|
;;
|
|
1723
1934
|
esac
|
|
1724
1935
|
;;
|
|
1936
|
+
proxy)
|
|
1937
|
+
[[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME proxy [<host:port>|off|test]"
|
|
1938
|
+
case "${1:-}" in
|
|
1939
|
+
"")
|
|
1940
|
+
cmd_proxy_show
|
|
1941
|
+
;;
|
|
1942
|
+
off)
|
|
1943
|
+
with_lock cmd_proxy_off
|
|
1944
|
+
;;
|
|
1945
|
+
test)
|
|
1946
|
+
cmd_proxy_test
|
|
1947
|
+
;;
|
|
1948
|
+
*)
|
|
1949
|
+
with_lock cmd_proxy_set "$1"
|
|
1950
|
+
;;
|
|
1951
|
+
esac
|
|
1952
|
+
;;
|
|
1725
1953
|
add)
|
|
1726
1954
|
[[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME add <account>"
|
|
1727
1955
|
with_lock cmd_add "$1"
|
|
@@ -24,6 +24,7 @@ def parse_args() -> argparse.Namespace:
|
|
|
24
24
|
parser.add_argument("--account-name", required=True)
|
|
25
25
|
parser.add_argument("--auth-file", required=True)
|
|
26
26
|
parser.add_argument("--data-path", required=True)
|
|
27
|
+
parser.add_argument("--usage-proxy", default="")
|
|
27
28
|
parser.add_argument("--timeout-seconds", type=int, default=4)
|
|
28
29
|
return parser.parse_args()
|
|
29
30
|
|
|
@@ -169,7 +170,7 @@ def extract_windows_from_usage_blob(blob: Dict[str, Any]) -> Dict[int, Dict[str,
|
|
|
169
170
|
return windows
|
|
170
171
|
|
|
171
172
|
|
|
172
|
-
def request_usage(access_token: str, account_id: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
173
|
+
def request_usage(access_token: str, account_id: str, usage_proxy: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
173
174
|
if not access_token:
|
|
174
175
|
return None
|
|
175
176
|
cmd = [
|
|
@@ -190,12 +191,18 @@ def request_usage(access_token: str, account_id: str, timeout_seconds: int) -> O
|
|
|
190
191
|
cmd.extend(["-H", f"ChatGPT-Account-Id: {account_id}"])
|
|
191
192
|
cmd.append(USAGE_ENDPOINT)
|
|
192
193
|
try:
|
|
194
|
+
env = os.environ.copy()
|
|
195
|
+
proxy = (usage_proxy or "").strip()
|
|
196
|
+
if proxy:
|
|
197
|
+
env["HTTPS_PROXY"] = proxy
|
|
198
|
+
env["HTTP_PROXY"] = proxy
|
|
193
199
|
result = subprocess.run(
|
|
194
200
|
cmd,
|
|
195
201
|
check=False,
|
|
196
202
|
stdout=subprocess.PIPE,
|
|
197
203
|
stderr=subprocess.PIPE,
|
|
198
204
|
text=True,
|
|
205
|
+
env=env,
|
|
199
206
|
)
|
|
200
207
|
except Exception:
|
|
201
208
|
return None
|
|
@@ -286,8 +293,8 @@ def collect_local_metrics(data_path: str) -> Dict[str, Any]:
|
|
|
286
293
|
return {"source": "local"}
|
|
287
294
|
|
|
288
295
|
|
|
289
|
-
def collect_api_metrics(access_token: str, account_id: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
290
|
-
payload = request_usage(access_token, account_id, timeout_seconds)
|
|
296
|
+
def collect_api_metrics(access_token: str, account_id: str, usage_proxy: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
297
|
+
payload = request_usage(access_token, account_id, usage_proxy, timeout_seconds)
|
|
291
298
|
if not payload:
|
|
292
299
|
return None
|
|
293
300
|
|
|
@@ -344,7 +351,7 @@ def main() -> int:
|
|
|
344
351
|
if plan_from_claims == "unknown":
|
|
345
352
|
plan_from_claims = normalize_plan(auth_data.get("chatgpt_plan_type") or auth_data.get("plan_type"))
|
|
346
353
|
|
|
347
|
-
api_metrics = collect_api_metrics(access_token, account_id, args.timeout_seconds)
|
|
354
|
+
api_metrics = collect_api_metrics(access_token, account_id, args.usage_proxy, args.timeout_seconds)
|
|
348
355
|
metrics = api_metrics if api_metrics is not None else collect_local_metrics(args.data_path)
|
|
349
356
|
|
|
350
357
|
windows = metrics.get("windows")
|
|
@@ -371,9 +378,9 @@ def main() -> int:
|
|
|
371
378
|
last_activity = format_relative(last_activity_epoch, now_epoch)
|
|
372
379
|
|
|
373
380
|
if email:
|
|
374
|
-
display_email =
|
|
381
|
+
display_email = email
|
|
375
382
|
else:
|
|
376
|
-
display_email =
|
|
383
|
+
display_email = "-"
|
|
377
384
|
|
|
378
385
|
out_fields = [
|
|
379
386
|
display_email,
|
|
@@ -66,6 +66,7 @@ cat > "$BIN/curl" <<'CURL'
|
|
|
66
66
|
#!/usr/bin/env bash
|
|
67
67
|
set -euo pipefail
|
|
68
68
|
mode="${CODEX_SWITCHER_TEST_CURL_MODE:-success}"
|
|
69
|
+
echo "proxy=${HTTPS_PROXY:-}" >> "${CODEX_SWITCHER_TEST_CURL_LOG:?}"
|
|
69
70
|
if [[ "$mode" == "success" ]]; then
|
|
70
71
|
cat <<'JSON'
|
|
71
72
|
{"rate_limit":{"plan_type":"plus","primary_window":{"used_percent":40,"limit_window_seconds":18000,"reset_at":"2099-01-01T06:30:00Z"},"secondary_window":{"used_percent":20,"limit_window_seconds":604800,"reset_at":"2099-01-03T08:00:00Z"}},"last_activity_at":"2099-01-01T04:30:00Z"}
|
|
@@ -84,10 +85,14 @@ export CODEX_SWITCHER_ACCOUNTS_DIR="$STATE/env-accounts"
|
|
|
84
85
|
export CODEX_SWITCHER_APP_BIN="$BIN/fake-codex-app"
|
|
85
86
|
export CODEX_SWITCHER_LOCK_WAIT_SECONDS=2
|
|
86
87
|
export CODEX_SWITCHER_DEFAULT_HOME="$DEFAULT_HOME"
|
|
88
|
+
export CODEX_SWITCHER_DISABLE_SYSTEM_PROXY_DETECT=true
|
|
87
89
|
export CODEX_SWITCHER_TEST_NPM_LOG="$TMPBASE/npm-args.log"
|
|
88
90
|
export CODEX_SWITCHER_TEST_CODEX_LOG="$TMPBASE/codex-args.log"
|
|
91
|
+
export CODEX_SWITCHER_TEST_CURL_LOG="$TMPBASE/curl-args.log"
|
|
89
92
|
export CODEX_SWITCHER_TEST_CURL_MODE="success"
|
|
93
|
+
unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy
|
|
90
94
|
: > "$CODEX_SWITCHER_TEST_CODEX_LOG"
|
|
95
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
91
96
|
|
|
92
97
|
echo '{"memo":"persist"}' > "$DEFAULT_HOME/shared.json"
|
|
93
98
|
|
|
@@ -97,6 +102,9 @@ echo "$check_out" | grep -q "check: ok"
|
|
|
97
102
|
link_check_out="$("$SW_LINK" check)"
|
|
98
103
|
echo "$link_check_out" | grep -Eq '^version: [0-9]+\.[0-9]+\.[0-9]+$'
|
|
99
104
|
echo "$link_check_out" | grep -q "check: ok"
|
|
105
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: off" ]]
|
|
106
|
+
"$SW" proxy 127.0.0.1:7899
|
|
107
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: http://127.0.0.1:7899 (manual)" ]]
|
|
100
108
|
init_out="$("$SW" init --dry-run)"
|
|
101
109
|
echo "$init_out" | grep -q "\[dry-run\]"
|
|
102
110
|
"$SW" upgrade
|
|
@@ -172,13 +180,14 @@ grep -q "PLAN" /tmp/codex_sw_list_api
|
|
|
172
180
|
grep -q "5H USAGE" /tmp/codex_sw_list_api
|
|
173
181
|
grep -q "WEEKLY USAGE" /tmp/codex_sw_list_api
|
|
174
182
|
grep -q "LAST ACTIVITY" /tmp/codex_sw_list_api
|
|
175
|
-
grep -q "
|
|
183
|
+
grep -q "personal@example.com" /tmp/codex_sw_list_api
|
|
176
184
|
grep -q "(api)" /tmp/codex_sw_list_api
|
|
177
185
|
grep -q "60% (" /tmp/codex_sw_list_api
|
|
178
186
|
grep -q "80% (" /tmp/codex_sw_list_api
|
|
187
|
+
grep -q "proxy=http://127.0.0.1:7899" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
179
188
|
|
|
180
189
|
"$SW_LINK" list >/tmp/codex_sw_list_symlink
|
|
181
|
-
grep -q "
|
|
190
|
+
grep -q "personal@example.com" /tmp/codex_sw_list_symlink
|
|
182
191
|
grep -q "(api)" /tmp/codex_sw_list_symlink
|
|
183
192
|
|
|
184
193
|
mkdir -p "$ENVS/project/home/sessions/2026/04/12"
|
|
@@ -186,10 +195,22 @@ cat > "$ENVS/project/home/sessions/2026/04/12/rollout-test.jsonl" <<'JSONL'
|
|
|
186
195
|
{"timestamp":"2026-04-12T09:00:00Z","type":"event_msg","payload":{"type":"token_count","rate_limits":{"plan_type":"business","primary":{"used_percent":25,"window_minutes":300,"resets_at":1776004200},"secondary":{"used_percent":70,"window_minutes":10080,"resets_at":1776519000}}}}
|
|
187
196
|
JSONL
|
|
188
197
|
export CODEX_SWITCHER_TEST_CURL_MODE="fail"
|
|
198
|
+
"$SW" proxy off
|
|
199
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: off" ]]
|
|
200
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
189
201
|
"$SW" list >/tmp/codex_sw_list_local
|
|
190
202
|
grep -q "(local)" /tmp/codex_sw_list_local
|
|
191
203
|
grep -q "75% (" /tmp/codex_sw_list_local
|
|
192
204
|
grep -q "30% (" /tmp/codex_sw_list_local
|
|
205
|
+
grep -q "^proxy=$" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
206
|
+
|
|
207
|
+
export CODEX_SWITCHER_TEST_CURL_MODE="success"
|
|
208
|
+
export HTTPS_PROXY="http://10.10.10.10:18080"
|
|
209
|
+
[[ "$("$SW" proxy)" == "usage_api_proxy: http://10.10.10.10:18080 (auto:env)" ]]
|
|
210
|
+
: > "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
211
|
+
"$SW" list >/tmp/codex_sw_list_auto_env
|
|
212
|
+
grep -q "proxy=http://10.10.10.10:18080" "$CODEX_SWITCHER_TEST_CURL_LOG"
|
|
213
|
+
unset HTTPS_PROXY
|
|
193
214
|
|
|
194
215
|
"$SW" app status >/tmp/codex_sw_app_status_1
|
|
195
216
|
[[ "$?" -eq 0 ]]
|