glm-quota-line 0.8.0 → 1.0.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/CHANGELOG.md +21 -0
- package/README.en.md +63 -39
- package/README.md +65 -41
- package/package.json +1 -1
- package/src/claude/sessionStart.js +1 -1
- package/src/claude/settings.js +0 -5
- package/src/cli/args.js +0 -8
- package/src/cli/commands.js +19 -10
- package/src/cli/index.js +12 -18
- package/src/core/quota/cache.js +9 -18
- package/src/core/quota/service.js +31 -34
- package/src/core/status/bar.js +4 -13
- package/src/core/status/format.js +21 -18
- package/src/core/status/theme.js +34 -9
- package/src/core/status/viewModel.js +3 -1
- package/src/shared/config.js +23 -7
- package/src/shared/constants.js +10 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- Replaced `theme`/`palette` two-config system with unified `theme` presets: `dark`, `light`, `mono`
|
|
6
|
+
- Removed `palette` config and CLI flag entirely
|
|
7
|
+
- Removed `display=both` option; display now supports only `left` (default) and `used`
|
|
8
|
+
- Removed `bar-width` config option; bar width is fixed at 10
|
|
9
|
+
- Changed default style from `text` to `bar`
|
|
10
|
+
- Bar fill now represents remaining quota by default (was used); `display=used` fills by usage
|
|
11
|
+
- Added `light` theme with blue accents for light terminals
|
|
12
|
+
- `loadConfig` now reads `~/.claude/settings.json` env as a fallback auth source
|
|
13
|
+
- `loadConfig` changed from sync to async; all callers updated
|
|
14
|
+
- Updated README with complete config reference, display docs, and terminal quick-check usage
|
|
15
|
+
|
|
16
|
+
## 0.9.0
|
|
17
|
+
|
|
18
|
+
- Replaced progressive tier-based backoff with diamond-shaped refresh strategy: high quota (80–100%) refreshes every 2 min, medium (30–79%) every 5 min, low (0–29%) every 2 min — frequent updates when usage is active or quota is near exhaustion, relaxed in between
|
|
19
|
+
- Added failure-type-aware retry TTLs: rate-limited (429) retries after 3 min, unavailable retries after 2 min, instead of reusing the quota-based TTL
|
|
20
|
+
- Removed `refreshCount`/`tierIndex` cache fields and `advanceTier()` — cache format is now simpler, and TTL is derived purely from quota percentage
|
|
21
|
+
- Aligned severity threshold (danger/warn boundary) from hardcoded 25% to `LOW_QUOTA_THRESHOLD` (30%), consistent with the refresh band boundary
|
|
22
|
+
- Unknown CLI commands and invalid config subcommands now print an error and exit with code 1 instead of silently showing quota status
|
|
23
|
+
|
|
3
24
|
## 0.8.0
|
|
4
25
|
|
|
5
26
|
- Added progressive refresh backoff: new sessions start at 3-minute intervals, advance to 5 minutes after 5 refreshes, and cap at 10 minutes after 5 more — giving new users fast feedback while reducing API pressure in long sessions
|
package/README.en.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1 align="center">glm-quota-line</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
|
|
4
|
+
A Zhipu GLM Coding Plan quota monitor built for Claude Code. Accurate, real-time data from the official site — no window switching, just glance and go.
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
<a href="./README.md">简体中文</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
`glm-quota-line` reads the Zhipu GLM Coding Plan quota API, caches results, and prints a short status line for Claude Code's `statusLine.command`. A `SessionStart` hook pre-refreshes the cache so new sessions never show stale data.
|
|
19
|
-
|
|
20
18
|
## Features
|
|
21
19
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
20
|
+
- **Terminal quick check** — run `glm-quota-line` in any terminal to view your quota without launching Claude Code
|
|
21
|
+
- **Claude Code status line** — auto-embeds in the status bar after install, shows quota balance and reset time in real time
|
|
22
|
+
- **Bar visualization** — default bar style shows remaining quota at a glance
|
|
23
|
+
- **Smart caching** — tiered refresh by session, TTL, and token usage; `SessionStart` hook pre-refreshes so new sessions never show stale data
|
|
24
24
|
- **Domestic + international endpoints** — auto-detects `open.bigmodel.cn` and `api.z.ai`
|
|
25
25
|
- **Zero dependencies** — no runtime deps, single-purpose CLI
|
|
26
|
-
- **Smart caching** — refreshes by session, TTL, and token usage; supports manual auth overrides for proxy/gateway setups
|
|
27
26
|
|
|
28
27
|
## Quick Start
|
|
29
28
|
|
|
@@ -32,13 +31,13 @@ npm install -g glm-quota-line
|
|
|
32
31
|
glm-quota-line install
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
Done. Your Claude Code status bar will now show the quota:
|
|
36
35
|
|
|
37
|
-
```
|
|
38
|
-
GLM Lite
|
|
36
|
+
```
|
|
37
|
+
GLM Lite █████████░ 91% | W 47% | 14:47
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
You can also run `glm-quota-line` directly in the terminal to check your quota without launching Claude Code.
|
|
42
41
|
|
|
43
42
|
Upgrade:
|
|
44
43
|
|
|
@@ -47,38 +46,52 @@ npm install -g glm-quota-line
|
|
|
47
46
|
glm-quota-line check-update
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
##
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
All options are optional. Persist with `glm-quota-line config set`, or override per-invocation with CLI flags.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
### style — Output layout
|
|
54
|
+
|
|
55
|
+
| Value | Description | Example |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `bar` (default) | Progress bar | `GLM Lite █████████░ 91% \| W 47% \| 14:47` |
|
|
58
|
+
| `text` | Full text | `GLM Lite \| 5h 91% \| week 47% \| reset 14:47` |
|
|
59
|
+
| `compact` | Compact mode | `GLM 5h 91% W 47% \| 14:47` |
|
|
53
60
|
|
|
54
61
|
```bash
|
|
55
|
-
glm-quota-line config set style
|
|
56
|
-
glm-quota-line config set theme ansi # color: plain / ansi
|
|
57
|
-
glm-quota-line config set palette dark # palette: dark / mono (only when theme=ansi)
|
|
62
|
+
glm-quota-line config set style compact
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
# style=text (default)
|
|
64
|
-
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
65
|
+
### theme — Color theme
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
| Value | Description |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `dark` (default) | Dark terminal, strongest color contrast |
|
|
70
|
+
| `light` | White/light terminal, blue-tinted accents |
|
|
71
|
+
| `mono` | Grayscale, minimal distraction |
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
```bash
|
|
74
|
+
glm-quota-line config set theme light
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
Quota percentage colors change automatically based on remaining amount:
|
|
78
|
+
|
|
79
|
+
- Green — remaining >= 60%
|
|
80
|
+
- Yellow — remaining 30%–60%
|
|
81
|
+
- Red — remaining < 30%
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
| -------------- | --------------------------------------------- |
|
|
77
|
-
| Dark terminal | `style=bar`, `theme=ansi`, `palette=dark` |
|
|
78
|
-
| Light terminal | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
79
|
-
| Plain fallback | any `style`, `theme=plain` |
|
|
83
|
+
### display — Quota metric
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
| Value | Description |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `left` (default) | Show remaining quota (bar fills with remaining) |
|
|
88
|
+
| `used` | Show used quota (bar fills with used) |
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
glm-quota-line config set display used
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### auth-token / base-url — Custom auth
|
|
82
95
|
|
|
83
96
|
If Claude Code runs behind a gateway or proxy and the injected `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` are not the real values:
|
|
84
97
|
|
|
@@ -89,27 +102,38 @@ glm-quota-line config set base-url https://open.bigmodel.cn/api/anthropic
|
|
|
89
102
|
glm-quota-line config set base-url https://api.z.ai/api/anthropic
|
|
90
103
|
```
|
|
91
104
|
|
|
92
|
-
Clear with `glm-quota-line config unset auth-token` / `base-url`.
|
|
105
|
+
Clear with `glm-quota-line config unset auth-token` / `base-url`.
|
|
93
106
|
|
|
94
|
-
|
|
107
|
+
Auth source priority (highest to lowest):
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
1. Values persisted via `config set`
|
|
110
|
+
2. Environment variables `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL`
|
|
111
|
+
3. `env` field in `~/.claude/settings.json`
|
|
112
|
+
|
|
113
|
+
## Recommended Combinations
|
|
99
114
|
|
|
115
|
+
| Use case | Config |
|
|
116
|
+
|---|---|
|
|
117
|
+
| Dark terminal (default) | `style=bar`, `theme=dark` |
|
|
118
|
+
| Light terminal | `style=bar`, `theme=light` |
|
|
119
|
+
| Minimal setup | `style=compact`, `theme=mono` |
|
|
120
|
+
| Track usage | `style=bar`, `display=used` |
|
|
121
|
+
|
|
122
|
+
## Command Reference
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
glm-quota-line [--style text|compact|bar] [--display left|used] [--theme dark|light|mono]
|
|
100
126
|
glm-quota-line install [--force]
|
|
101
127
|
glm-quota-line uninstall
|
|
102
128
|
glm-quota-line version
|
|
103
129
|
glm-quota-line check-update
|
|
104
130
|
glm-quota-line config show
|
|
105
|
-
glm-quota-line config set <style|display|theme|
|
|
131
|
+
glm-quota-line config set <style|display|theme|auth-token|base-url> <value>
|
|
106
132
|
glm-quota-line config unset <key>
|
|
107
133
|
```
|
|
108
134
|
|
|
109
135
|
Run `glm-quota-line --help` for full descriptions.
|
|
110
136
|
|
|
111
|
-
Use `glm-quota-line --version` to print the installed version. `check-update` only checks and prints the suggested upgrade command; it does not modify your environment.
|
|
112
|
-
|
|
113
137
|
## Notes
|
|
114
138
|
|
|
115
139
|
- Only `TOKENS_LIMIT` quotas are shown; `TIME_LIMIT` / MCP usage is ignored
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1 align="center">glm-quota-line</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
|
|
4
|
+
为 Claude Code 打造的智谱 GLM Coding Plan 配额监控工具。数据精准同步官网,无需切换窗口,随时掌握用量。
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -15,15 +15,14 @@
|
|
|
15
15
|
<a href="./README.en.md">English</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
`glm-quota-line` 读取智谱(Zhipu)GLM Coding Plan 配额接口、缓存结果,并输出一行适合 Claude Code `statusLine.command` 的状态文本。通过 `SessionStart` hook 预刷新缓存,新会话不会显示旧配额。
|
|
19
|
-
|
|
20
18
|
## 功能
|
|
21
19
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
20
|
+
- **终端快速查询** — 直接运行 `glm-quota-line` 即可在终端查看配额,无需启动 Claude Code
|
|
21
|
+
- **Claude Code 状态栏** — 安装后自动嵌入状态栏,实时显示配额余额和重置时间
|
|
22
|
+
- **进度条可视化** — 默认 bar 风格,一眼看清剩余比例
|
|
23
|
+
- **智能缓存** — 按会话、TTL 和 token 用量分级刷新;`SessionStart` hook 预刷新,新会话不显示旧数据
|
|
24
24
|
- **国内 + 国际端点** — 自动识别 `open.bigmodel.cn` 和 `api.z.ai`
|
|
25
25
|
- **零依赖** — 无运行时依赖,单一用途
|
|
26
|
-
- **智能缓存** — 按会话、TTL 和 token 用量刷新;支持代理/网关手动覆盖鉴权
|
|
27
26
|
|
|
28
27
|
## 快速开始
|
|
29
28
|
|
|
@@ -32,13 +31,13 @@ npm install -g glm-quota-line
|
|
|
32
31
|
glm-quota-line install
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
安装完成。Claude Code 底部状态栏会自动显示配额:
|
|
36
35
|
|
|
37
|
-
```
|
|
38
|
-
GLM Lite
|
|
36
|
+
```
|
|
37
|
+
GLM Lite █████████░ 91% | W 47% | 14:47
|
|
39
38
|
```
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
也可以直接在终端运行 `glm-quota-line` 快速查看用量,无需启动 Claude Code。
|
|
42
41
|
|
|
43
42
|
升级版本:
|
|
44
43
|
|
|
@@ -47,40 +46,54 @@ npm install -g glm-quota-line
|
|
|
47
46
|
glm-quota-line check-update
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
##
|
|
49
|
+
## 配置项
|
|
50
|
+
|
|
51
|
+
所有配置均为可选,按需调整。通过 `glm-quota-line config set` 持久化,或用 CLI 参数临时覆盖。
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
### style — 输出布局
|
|
54
|
+
|
|
55
|
+
| 值 | 说明 | 示例 |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `bar`(默认) | 进度条可视化 | `GLM Lite █████████░ 91% \| W 47% \| 14:47` |
|
|
58
|
+
| `text` | 完整文本 | `GLM Lite \| 5h 91% \| week 47% \| reset 14:47` |
|
|
59
|
+
| `compact` | 紧凑模式 | `GLM 5h 91% W 47% \| 14:47` |
|
|
53
60
|
|
|
54
61
|
```bash
|
|
55
|
-
glm-quota-line config set style
|
|
56
|
-
glm-quota-line config set theme ansi # 配色: plain / ansi
|
|
57
|
-
glm-quota-line config set palette dark # 色板: dark / mono(仅 theme=ansi 时生效)
|
|
62
|
+
glm-quota-line config set style compact
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```text
|
|
63
|
-
# style=text(默认)
|
|
64
|
-
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
65
|
+
### theme — 主题配色
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
| 值 | 说明 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `dark`(默认) | 深色终端,彩色对比最强 |
|
|
70
|
+
| `light` | 浅色/白色终端,蓝调配色 |
|
|
71
|
+
| `mono` | 灰阶,极简低干扰 |
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
```bash
|
|
74
|
+
glm-quota-line config set theme light
|
|
71
75
|
```
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
配额百分比会根据剩余量自动变色:
|
|
74
78
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| 浅色终端 | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
79
|
-
| 纯文本回退 | 任意 `style`,`theme=plain` |
|
|
79
|
+
- 绿色 — 剩余 >= 60%
|
|
80
|
+
- 黄色 — 剩余 30%–60%
|
|
81
|
+
- 红色 — 剩余 < 30%
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
### display — 显示指标
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
| 值 | 说明 |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `left`(默认) | 显示剩余量(进度条填充 = 剩余比例) |
|
|
88
|
+
| `used` | 显示已用量(进度条填充 = 已用比例) |
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
glm-quota-line config set display used
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### auth-token / base-url — 自定义鉴权
|
|
95
|
+
|
|
96
|
+
当 Claude Code 运行在代理或网关后面时,注入的 `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` 可能不是实际值,可手动覆盖:
|
|
84
97
|
|
|
85
98
|
```bash
|
|
86
99
|
glm-quota-line config set auth-token <your-real-token>
|
|
@@ -89,26 +102,37 @@ glm-quota-line config set base-url https://open.bigmodel.cn/api/anthropic
|
|
|
89
102
|
glm-quota-line config set base-url https://api.z.ai/api/anthropic
|
|
90
103
|
```
|
|
91
104
|
|
|
92
|
-
清除:`glm-quota-line config unset auth-token` / `base-url
|
|
105
|
+
清除:`glm-quota-line config unset auth-token` / `base-url`。
|
|
93
106
|
|
|
94
|
-
|
|
107
|
+
鉴权来源优先级(从高到低):
|
|
95
108
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
1. `config set` 持久化的值
|
|
110
|
+
2. 环境变量 `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL`
|
|
111
|
+
3. `~/.claude/settings.json` 中的 `env` 字段
|
|
112
|
+
|
|
113
|
+
## 推荐搭配
|
|
99
114
|
|
|
115
|
+
| 使用场景 | 配置 |
|
|
116
|
+
|---|---|
|
|
117
|
+
| 深色终端(默认) | `style=bar`, `theme=dark` |
|
|
118
|
+
| 浅色终端 | `style=bar`, `theme=light` |
|
|
119
|
+
| 极简工作流 | `style=compact`, `theme=mono` |
|
|
120
|
+
| 关注已用量 | `style=bar`, `display=used` |
|
|
121
|
+
|
|
122
|
+
## 命令参考
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
glm-quota-line [--style text|compact|bar] [--display left|used] [--theme dark|light|mono]
|
|
100
126
|
glm-quota-line install [--force]
|
|
101
127
|
glm-quota-line uninstall
|
|
102
128
|
glm-quota-line version
|
|
103
129
|
glm-quota-line check-update
|
|
104
130
|
glm-quota-line config show
|
|
105
|
-
glm-quota-line config set <style|display|theme|
|
|
131
|
+
glm-quota-line config set <style|display|theme|auth-token|base-url> <value>
|
|
106
132
|
glm-quota-line config unset <key>
|
|
107
133
|
```
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`glm-quota-line --version` 可直接查看当前安装版本;`check-update` 只检查并提示升级命令,不会自动修改环境。
|
|
135
|
+
运行 `glm-quota-line --help` 查看完整说明。
|
|
112
136
|
|
|
113
137
|
## 说明
|
|
114
138
|
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ export async function refreshQuotaOnSessionStart(options = {}) {
|
|
|
11
11
|
const loadConfigFn = options.loadConfigFn ?? loadConfig;
|
|
12
12
|
const resolveQuotaStatusFn = options.resolveQuotaStatusFn ?? resolveQuotaStatus;
|
|
13
13
|
const config = {
|
|
14
|
-
...loadConfigFn(options.env ?? process.env, userConfig),
|
|
14
|
+
...(await loadConfigFn(options.env ?? process.env, userConfig)),
|
|
15
15
|
sessionId: hookInput?.session_id || ""
|
|
16
16
|
};
|
|
17
17
|
|
package/src/claude/settings.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
TOOL_CONFIG_MANAGED_BY,
|
|
6
6
|
TOOL_CONFIG_SCHEMA_VERSION,
|
|
7
7
|
isValidDisplayMode,
|
|
8
|
-
isValidPalette,
|
|
9
8
|
isValidStatusStyle,
|
|
10
9
|
isValidTheme
|
|
11
10
|
} from "../shared/constants.js";
|
|
@@ -56,10 +55,6 @@ function normalizeToolConfig(config) {
|
|
|
56
55
|
normalized.theme = base.theme;
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
if (isValidPalette(base.palette)) {
|
|
60
|
-
normalized.palette = base.palette;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
58
|
const authToken = normalizeOptionalString(base.authToken);
|
|
64
59
|
if (authToken) {
|
|
65
60
|
normalized.authToken = authToken;
|
package/src/cli/args.js
CHANGED
|
@@ -59,14 +59,6 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
59
59
|
}
|
|
60
60
|
continue;
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
if (arg === "--palette" || arg.startsWith("--palette=")) {
|
|
64
|
-
options.palette = takeValue(argv, index);
|
|
65
|
-
if (!arg.includes("=")) {
|
|
66
|
-
index += 1;
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
options.positionals = positionals;
|
package/src/cli/commands.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isValidDisplayMode,
|
|
3
|
-
isValidPalette,
|
|
4
3
|
isValidStatusStyle,
|
|
5
4
|
isValidTheme
|
|
6
5
|
} from "../shared/constants.js";
|
|
@@ -42,17 +41,12 @@ const CONFIG_KEYS = {
|
|
|
42
41
|
display: {
|
|
43
42
|
property: "displayMode",
|
|
44
43
|
validate: isValidDisplayMode,
|
|
45
|
-
invalidMessage: "Invalid display. Use: left
|
|
44
|
+
invalidMessage: "Invalid display. Use: left or used."
|
|
46
45
|
},
|
|
47
46
|
theme: {
|
|
48
47
|
property: "theme",
|
|
49
48
|
validate: isValidTheme,
|
|
50
|
-
invalidMessage: "Invalid theme. Use:
|
|
51
|
-
},
|
|
52
|
-
palette: {
|
|
53
|
-
property: "palette",
|
|
54
|
-
validate: isValidPalette,
|
|
55
|
-
invalidMessage: "Invalid palette. Use: dark or mono."
|
|
49
|
+
invalidMessage: "Invalid theme. Use: dark, light, or mono."
|
|
56
50
|
},
|
|
57
51
|
"auth-token": {
|
|
58
52
|
property: "authToken",
|
|
@@ -149,11 +143,13 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
149
143
|
if (command === "config" && subcommand === "set") {
|
|
150
144
|
const configKey = CONFIG_KEYS[key];
|
|
151
145
|
if (!configKey) {
|
|
152
|
-
|
|
146
|
+
process.exitCode = 1;
|
|
147
|
+
output.write("Supported config keys: style, display, theme, auth-token, base-url\n");
|
|
153
148
|
return true;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
if (!configKey.validate(value)) {
|
|
152
|
+
process.exitCode = 1;
|
|
157
153
|
output.write(`${configKey.invalidMessage}\n`);
|
|
158
154
|
return true;
|
|
159
155
|
}
|
|
@@ -168,7 +164,8 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
168
164
|
if (command === "config" && subcommand === "unset") {
|
|
169
165
|
const configKey = CONFIG_KEYS[key];
|
|
170
166
|
if (!configKey) {
|
|
171
|
-
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
output.write("Supported config keys: style, display, theme, auth-token, base-url\n");
|
|
172
169
|
return true;
|
|
173
170
|
}
|
|
174
171
|
|
|
@@ -177,5 +174,17 @@ export async function handleCommand(args, output = process.stdout, dependencies
|
|
|
177
174
|
return true;
|
|
178
175
|
}
|
|
179
176
|
|
|
177
|
+
if (command === "config") {
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
output.write("Supported config subcommands: show, set, unset\n");
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (command) {
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
output.write(`Unknown command: ${command}\nRun 'glm-quota-line -h' for usage.\n`);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
180
189
|
return false;
|
|
181
190
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -10,7 +10,6 @@ import { resolveQuotaStatus } from "../core/quota/service.js";
|
|
|
10
10
|
import { getPackageVersion } from "../shared/packageInfo.js";
|
|
11
11
|
import {
|
|
12
12
|
isValidDisplayMode,
|
|
13
|
-
isValidPalette,
|
|
14
13
|
isValidStatusStyle,
|
|
15
14
|
isValidTheme
|
|
16
15
|
} from "../shared/constants.js";
|
|
@@ -19,20 +18,19 @@ function printHelp() {
|
|
|
19
18
|
process.stdout.write(`glm-quota-line
|
|
20
19
|
|
|
21
20
|
Usage:
|
|
22
|
-
glm-quota-line [--style text|compact|bar] [--display left|used
|
|
23
|
-
[--theme
|
|
21
|
+
glm-quota-line [--style text|compact|bar] [--display left|used]
|
|
22
|
+
[--theme dark|light|mono]
|
|
24
23
|
glm-quota-line --version
|
|
25
24
|
glm-quota-line install [--force]
|
|
26
25
|
glm-quota-line uninstall
|
|
27
26
|
glm-quota-line version
|
|
28
27
|
glm-quota-line check-update
|
|
29
28
|
glm-quota-line config set style <text|compact|bar>
|
|
30
|
-
glm-quota-line config set display <left|used
|
|
31
|
-
glm-quota-line config set theme <
|
|
32
|
-
glm-quota-line config set palette <dark|mono>
|
|
29
|
+
glm-quota-line config set display <left|used>
|
|
30
|
+
glm-quota-line config set theme <dark|light|mono>
|
|
33
31
|
glm-quota-line config set auth-token <token>
|
|
34
32
|
glm-quota-line config set base-url <url>
|
|
35
|
-
glm-quota-line config unset <style|display|theme|
|
|
33
|
+
glm-quota-line config unset <style|display|theme|auth-token|base-url>
|
|
36
34
|
glm-quota-line config show
|
|
37
35
|
|
|
38
36
|
Commands:
|
|
@@ -47,9 +45,8 @@ Commands:
|
|
|
47
45
|
|
|
48
46
|
Options:
|
|
49
47
|
--style Output layout: text, compact, or bar.
|
|
50
|
-
--display Quota metric: left
|
|
51
|
-
--theme
|
|
52
|
-
--palette ANSI palette: dark for dark terminals, mono for light terminals.
|
|
48
|
+
--display Quota metric: left or used.
|
|
49
|
+
--theme Theme preset: dark, light, or mono.
|
|
53
50
|
--force Allow install to replace an unmanaged Claude status line.
|
|
54
51
|
-v, --version Show the installed version.
|
|
55
52
|
-h, --help Show this help text.
|
|
@@ -57,10 +54,10 @@ Options:
|
|
|
57
54
|
Examples:
|
|
58
55
|
glm-quota-line
|
|
59
56
|
glm-quota-line --version
|
|
60
|
-
glm-quota-line --style bar --theme
|
|
57
|
+
glm-quota-line --style bar --theme dark
|
|
61
58
|
glm-quota-line check-update
|
|
62
59
|
glm-quota-line config set style compact
|
|
63
|
-
glm-quota-line config set theme
|
|
60
|
+
glm-quota-line config set theme light
|
|
64
61
|
glm-quota-line config set auth-token <your-real-token>
|
|
65
62
|
glm-quota-line install
|
|
66
63
|
|
|
@@ -74,8 +71,7 @@ function getStoredDisplayOverrides(userConfig) {
|
|
|
74
71
|
return {
|
|
75
72
|
...(isValidStatusStyle(userConfig.style) ? { style: userConfig.style } : {}),
|
|
76
73
|
...(isValidDisplayMode(userConfig.displayMode) ? { displayMode: userConfig.displayMode } : {}),
|
|
77
|
-
...(isValidTheme(userConfig.theme) ? { theme: userConfig.theme } : {})
|
|
78
|
-
...(isValidPalette(userConfig.palette) ? { palette: userConfig.palette } : {})
|
|
74
|
+
...(isValidTheme(userConfig.theme) ? { theme: userConfig.theme } : {})
|
|
79
75
|
};
|
|
80
76
|
}
|
|
81
77
|
|
|
@@ -101,7 +97,7 @@ export async function main() {
|
|
|
101
97
|
const config = {
|
|
102
98
|
// Stored auth/base-url must override Claude's injected env so users can
|
|
103
99
|
// bypass gateway/proxy credentials when necessary.
|
|
104
|
-
...loadConfig(process.env, userConfig),
|
|
100
|
+
...(await loadConfig(process.env, userConfig)),
|
|
105
101
|
// Display config precedence is env defaults -> persisted config -> CLI flags.
|
|
106
102
|
...getStoredDisplayOverrides(userConfig),
|
|
107
103
|
...args,
|
|
@@ -113,9 +109,7 @@ export async function main() {
|
|
|
113
109
|
`${formatStatus(quotaStatus, {
|
|
114
110
|
displayMode: config.displayMode,
|
|
115
111
|
style: config.style,
|
|
116
|
-
theme: config.theme
|
|
117
|
-
palette: config.palette,
|
|
118
|
-
barWidth: config.barWidth
|
|
112
|
+
theme: config.theme
|
|
119
113
|
})}\n`
|
|
120
114
|
);
|
|
121
115
|
} catch {
|
package/src/core/quota/cache.js
CHANGED
|
@@ -80,6 +80,10 @@ function normalizeCache(parsed) {
|
|
|
80
80
|
const savedAt = asFiniteNumber(parsed.savedAt);
|
|
81
81
|
const lastAttemptAt = asFiniteNumber(parsed.lastAttemptAt) ?? savedAt;
|
|
82
82
|
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
|
|
83
|
+
const lastFailureKind =
|
|
84
|
+
parsed.lastFailureKind === "rate_limited" || parsed.lastFailureKind === "unavailable"
|
|
85
|
+
? parsed.lastFailureKind
|
|
86
|
+
: null;
|
|
83
87
|
|
|
84
88
|
if (result && savedAt === null) {
|
|
85
89
|
return null;
|
|
@@ -89,21 +93,11 @@ function normalizeCache(parsed) {
|
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
const refreshCount =
|
|
93
|
-
typeof parsed.refreshCount === "number" && Number.isFinite(parsed.refreshCount)
|
|
94
|
-
? Math.max(0, Math.floor(parsed.refreshCount))
|
|
95
|
-
: 0;
|
|
96
|
-
const tierIndex =
|
|
97
|
-
typeof parsed.tierIndex === "number" && Number.isFinite(parsed.tierIndex)
|
|
98
|
-
? Math.max(0, Math.floor(parsed.tierIndex))
|
|
99
|
-
: 0;
|
|
100
|
-
|
|
101
96
|
return {
|
|
102
97
|
savedAt,
|
|
103
98
|
lastAttemptAt,
|
|
104
99
|
sessionId,
|
|
105
|
-
|
|
106
|
-
tierIndex,
|
|
100
|
+
lastFailureKind,
|
|
107
101
|
result
|
|
108
102
|
};
|
|
109
103
|
}
|
|
@@ -114,8 +108,7 @@ async function writeCache(cacheFilePath, cache) {
|
|
|
114
108
|
...(Number.isFinite(cache.savedAt) ? { savedAt: cache.savedAt } : {}),
|
|
115
109
|
...(Number.isFinite(cache.lastAttemptAt) ? { lastAttemptAt: cache.lastAttemptAt } : {}),
|
|
116
110
|
...(cache.sessionId ? { sessionId: cache.sessionId } : {}),
|
|
117
|
-
|
|
118
|
-
tierIndex: cache.tierIndex ?? 0,
|
|
111
|
+
...(cache.lastFailureKind ? { lastFailureKind: cache.lastFailureKind } : {}),
|
|
119
112
|
...(cache.result ? { result: cache.result } : {})
|
|
120
113
|
},
|
|
121
114
|
null,
|
|
@@ -142,21 +135,19 @@ export async function writeSuccessCache(cacheFilePath, result, options = {}) {
|
|
|
142
135
|
savedAt: now,
|
|
143
136
|
lastAttemptAt: now,
|
|
144
137
|
sessionId: options.sessionId || "",
|
|
145
|
-
|
|
146
|
-
tierIndex: options.tierIndex ?? 0,
|
|
138
|
+
lastFailureKind: null,
|
|
147
139
|
result
|
|
148
140
|
});
|
|
149
141
|
}
|
|
150
142
|
|
|
151
|
-
export async function
|
|
143
|
+
export async function writeFailureCache(cacheFilePath, cached, options = {}) {
|
|
152
144
|
const now = options.now ?? Date.now();
|
|
153
145
|
|
|
154
146
|
await writeCache(cacheFilePath, {
|
|
155
147
|
savedAt: cached?.savedAt ?? null,
|
|
156
148
|
lastAttemptAt: now,
|
|
157
149
|
sessionId: options.sessionId || cached?.sessionId || "",
|
|
158
|
-
|
|
159
|
-
tierIndex: cached?.tierIndex ?? 0,
|
|
150
|
+
lastFailureKind: options.failureKind || "unavailable",
|
|
160
151
|
result: cached?.result ?? null
|
|
161
152
|
});
|
|
162
153
|
}
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readCache,
|
|
3
|
-
|
|
3
|
+
writeFailureCache,
|
|
4
4
|
writeSuccessCache
|
|
5
5
|
} from "./cache.js";
|
|
6
6
|
import { fetchQuota } from "./fetch.js";
|
|
7
7
|
import { parseQuotaResponse } from "./parse.js";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
RATE_LIMIT_RETRY_TTL_MS,
|
|
10
|
+
REFRESH_BANDS,
|
|
11
|
+
UNAVAILABLE_RETRY_TTL_MS
|
|
11
12
|
} from "../../shared/constants.js";
|
|
12
13
|
|
|
13
14
|
function getCachedStatus(cached) {
|
|
14
15
|
return cached?.result ?? { kind: "unavailable" };
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function
|
|
18
|
-
if (Number.isFinite(leftPercent)
|
|
19
|
-
return
|
|
18
|
+
function getRefreshTtlMs(leftPercent) {
|
|
19
|
+
if (!Number.isFinite(leftPercent)) {
|
|
20
|
+
return REFRESH_BANDS[1].ttlMs;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
return
|
|
23
|
+
const matchedBand = REFRESH_BANDS.find((band) => leftPercent >= band.minLeftPercent);
|
|
24
|
+
return matchedBand ? matchedBand.ttlMs : REFRESH_BANDS[REFRESH_BANDS.length - 1].ttlMs;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
function getEffectiveTtlMs(cached) {
|
|
28
|
+
if (cached?.lastFailureKind === "rate_limited") {
|
|
29
|
+
return RATE_LIMIT_RETRY_TTL_MS;
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
32
|
+
if (cached?.lastFailureKind === "unavailable") {
|
|
33
|
+
return UNAVAILABLE_RETRY_TTL_MS;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
return
|
|
36
|
+
return getRefreshTtlMs(cached?.result?.leftPercent);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function shouldRefreshQuota(cached, cacheTtlMs, now, sessionId) {
|
|
@@ -60,9 +62,7 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const cached = await readCache(config.cacheFilePath);
|
|
63
|
-
const
|
|
64
|
-
const leftPercent = cached?.result?.leftPercent;
|
|
65
|
-
const effectiveTtlMs = getEffectiveTtlMs(tierIndex, leftPercent);
|
|
65
|
+
const effectiveTtlMs = getEffectiveTtlMs(cached);
|
|
66
66
|
|
|
67
67
|
const shouldRefresh = options.forceRefresh
|
|
68
68
|
? true
|
|
@@ -76,23 +76,9 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
76
76
|
const parsed = parseQuotaResponse(response);
|
|
77
77
|
|
|
78
78
|
if (parsed.kind === "success") {
|
|
79
|
-
const isNewSession = Boolean(sessionId) && cached?.sessionId !== sessionId;
|
|
80
|
-
const isLowQuota = Number.isFinite(parsed.leftPercent) && parsed.leftPercent < LOW_QUOTA_THRESHOLD;
|
|
81
|
-
|
|
82
|
-
let nextTier;
|
|
83
|
-
if (isNewSession) {
|
|
84
|
-
nextTier = { refreshCount: 1, tierIndex: 0 };
|
|
85
|
-
} else if (isLowQuota) {
|
|
86
|
-
nextTier = { refreshCount: cached?.refreshCount ?? 0, tierIndex };
|
|
87
|
-
} else {
|
|
88
|
-
nextTier = advanceTier(cached?.refreshCount ?? 0, tierIndex);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
79
|
await writeSuccessCache(config.cacheFilePath, parsed, {
|
|
92
80
|
now,
|
|
93
|
-
sessionId
|
|
94
|
-
refreshCount: nextTier.refreshCount,
|
|
95
|
-
tierIndex: nextTier.tierIndex
|
|
81
|
+
sessionId
|
|
96
82
|
});
|
|
97
83
|
return parsed;
|
|
98
84
|
}
|
|
@@ -102,16 +88,27 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
102
88
|
}
|
|
103
89
|
|
|
104
90
|
if (parsed.kind === "rate_limited") {
|
|
105
|
-
await
|
|
91
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
106
92
|
now,
|
|
107
|
-
sessionId
|
|
93
|
+
sessionId,
|
|
94
|
+
failureKind: "rate_limited"
|
|
108
95
|
});
|
|
109
96
|
return getCachedStatus(cached);
|
|
110
97
|
}
|
|
111
98
|
|
|
112
99
|
if (cached?.result) {
|
|
100
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
101
|
+
now,
|
|
102
|
+
sessionId,
|
|
103
|
+
failureKind: "unavailable"
|
|
104
|
+
});
|
|
113
105
|
return cached.result;
|
|
114
106
|
}
|
|
115
107
|
|
|
108
|
+
await writeFailureCache(config.cacheFilePath, cached, {
|
|
109
|
+
now,
|
|
110
|
+
sessionId,
|
|
111
|
+
failureKind: "unavailable"
|
|
112
|
+
});
|
|
116
113
|
return parsed;
|
|
117
114
|
}
|
package/src/core/status/bar.js
CHANGED
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STATUS_BAR_CHARACTERS } from "../../shared/constants.js";
|
|
2
2
|
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
return Math.min(20, Math.max(5, Math.round(barWidth)));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function buildBar(usedPercent, barWidth, characters = STATUS_BAR_CHARACTERS) {
|
|
12
|
-
const safePercent = Math.min(100, Math.max(0, usedPercent));
|
|
13
|
-
const width = normalizeBarWidth(barWidth);
|
|
3
|
+
export function buildBar(percent, characters = STATUS_BAR_CHARACTERS) {
|
|
4
|
+
const safePercent = Math.min(100, Math.max(0, percent));
|
|
5
|
+
const width = 10;
|
|
14
6
|
let filledUnits;
|
|
15
7
|
|
|
16
8
|
if (safePercent <= 0) {
|
|
@@ -29,4 +21,3 @@ export function buildBar(usedPercent, barWidth, characters = STATUS_BAR_CHARACTE
|
|
|
29
21
|
emptyText: characters.empty.repeat(width - filledUnits)
|
|
30
22
|
};
|
|
31
23
|
}
|
|
32
|
-
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeDisplayMode,
|
|
3
|
-
normalizePalette,
|
|
4
3
|
normalizeStatusStyle,
|
|
5
4
|
normalizeTheme
|
|
6
5
|
} from "../../shared/constants.js";
|
|
@@ -30,15 +29,6 @@ function createQuotaTextSegments(quota, displayMode, tone) {
|
|
|
30
29
|
];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
if (mode === "both") {
|
|
34
|
-
return [
|
|
35
|
-
{ text: `${quota.label} left `, tone: "muted" },
|
|
36
|
-
{ text: quota.leftText, tone },
|
|
37
|
-
{ text: " used ", tone: "muted" },
|
|
38
|
-
{ text: quota.usedText, tone }
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
return [
|
|
43
33
|
{ text: `${quota.label} `, tone: "muted" },
|
|
44
34
|
{ text: quota.leftText, tone }
|
|
@@ -113,7 +103,21 @@ function createCompactSegments(model) {
|
|
|
113
103
|
return segments;
|
|
114
104
|
}
|
|
115
105
|
|
|
116
|
-
function
|
|
106
|
+
function createBarMetric(quota, displayMode) {
|
|
107
|
+
if (normalizeDisplayMode(displayMode) === "used") {
|
|
108
|
+
return {
|
|
109
|
+
percent: quota.usedPercent,
|
|
110
|
+
text: quota.usedText
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
percent: quota.leftPercent,
|
|
116
|
+
text: quota.leftText
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createBarSegments(model, displayMode) {
|
|
117
121
|
if (
|
|
118
122
|
!Number.isFinite(model.primaryQuota?.leftPercent) ||
|
|
119
123
|
!Number.isFinite(model.primaryQuota?.usedPercent)
|
|
@@ -121,7 +125,8 @@ function createBarSegments(model, barWidth) {
|
|
|
121
125
|
return createErrorSegments({ kind: "unavailable" });
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
const
|
|
128
|
+
const metric = createBarMetric(model.primaryQuota, displayMode);
|
|
129
|
+
const bar = buildBar(metric.percent);
|
|
125
130
|
const severityTone = model.severity;
|
|
126
131
|
const segments = [
|
|
127
132
|
{ text: model.levelLabel, tone: "label" },
|
|
@@ -129,7 +134,7 @@ function createBarSegments(model, barWidth) {
|
|
|
129
134
|
{ text: bar.filledText, tone: severityTone },
|
|
130
135
|
{ text: bar.emptyText, tone: "barEmpty" },
|
|
131
136
|
{ text: " ", tone: "plain" },
|
|
132
|
-
{ text:
|
|
137
|
+
{ text: metric.text, tone: severityTone }
|
|
133
138
|
];
|
|
134
139
|
|
|
135
140
|
if (model.secondaryQuota) {
|
|
@@ -152,8 +157,7 @@ export function formatStatus(result, options = {}) {
|
|
|
152
157
|
|
|
153
158
|
if (model.kind !== "success") {
|
|
154
159
|
return applyTheme(createErrorSegments(model), {
|
|
155
|
-
theme: normalizeTheme(options.theme)
|
|
156
|
-
palette: normalizePalette(options.palette)
|
|
160
|
+
theme: normalizeTheme(options.theme)
|
|
157
161
|
});
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -163,13 +167,12 @@ export function formatStatus(result, options = {}) {
|
|
|
163
167
|
if (style === "compact") {
|
|
164
168
|
segments = createCompactSegments(model);
|
|
165
169
|
} else if (style === "bar") {
|
|
166
|
-
segments = createBarSegments(model, options.
|
|
170
|
+
segments = createBarSegments(model, options.displayMode);
|
|
167
171
|
} else {
|
|
168
172
|
segments = createTextSegments(model, options.displayMode);
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
return applyTheme(segments, {
|
|
172
|
-
theme: normalizeTheme(options.theme)
|
|
173
|
-
palette: normalizePalette(options.palette)
|
|
176
|
+
theme: normalizeTheme(options.theme)
|
|
174
177
|
});
|
|
175
178
|
}
|
package/src/core/status/theme.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DEFAULT_PALETTE,
|
|
3
2
|
DEFAULT_THEME,
|
|
4
|
-
normalizePalette,
|
|
5
3
|
normalizeTheme
|
|
6
4
|
} from "../../shared/constants.js";
|
|
7
5
|
|
|
@@ -9,8 +7,11 @@ const ANSI = {
|
|
|
9
7
|
reset: "\u001b[0m",
|
|
10
8
|
bold: "\u001b[1m",
|
|
11
9
|
dim: "\u001b[2m",
|
|
10
|
+
underline: "\u001b[4m",
|
|
11
|
+
black: "\u001b[30m",
|
|
12
12
|
gray: "\u001b[90m",
|
|
13
13
|
white: "\u001b[37m",
|
|
14
|
+
blue: "\u001b[34m",
|
|
14
15
|
cyan: "\u001b[36m",
|
|
15
16
|
green: "\u001b[32m",
|
|
16
17
|
yellow: "\u001b[33m",
|
|
@@ -50,9 +51,11 @@ function getDarkCodes(tone) {
|
|
|
50
51
|
function getMonoCodes(tone) {
|
|
51
52
|
switch (tone) {
|
|
52
53
|
case "muted":
|
|
54
|
+
return [ANSI.gray];
|
|
53
55
|
case "barEmpty":
|
|
54
|
-
return [ANSI.dim];
|
|
56
|
+
return [ANSI.dim, ANSI.gray];
|
|
55
57
|
case "reset":
|
|
58
|
+
return [ANSI.underline];
|
|
56
59
|
case "label":
|
|
57
60
|
case "good":
|
|
58
61
|
case "warn":
|
|
@@ -64,17 +67,39 @@ function getMonoCodes(tone) {
|
|
|
64
67
|
}
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
function getLightCodes(tone) {
|
|
71
|
+
switch (tone) {
|
|
72
|
+
case "label":
|
|
73
|
+
case "reset":
|
|
74
|
+
return [ANSI.blue];
|
|
75
|
+
case "muted":
|
|
76
|
+
return [ANSI.gray];
|
|
77
|
+
case "barEmpty":
|
|
78
|
+
return [ANSI.dim, ANSI.gray];
|
|
79
|
+
case "good":
|
|
80
|
+
return [ANSI.green];
|
|
81
|
+
case "warn":
|
|
82
|
+
return [ANSI.yellow];
|
|
83
|
+
case "danger":
|
|
84
|
+
return [ANSI.red];
|
|
85
|
+
case "neutral":
|
|
86
|
+
return [ANSI.black];
|
|
87
|
+
default:
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
export function applyTheme(segments, options = {}) {
|
|
68
93
|
const theme = normalizeTheme(options.theme || DEFAULT_THEME);
|
|
69
|
-
const palette = normalizePalette(options.palette || DEFAULT_PALETTE);
|
|
70
|
-
|
|
71
|
-
if (theme !== "ansi") {
|
|
72
|
-
return segments.map((segment) => segment.text).join("");
|
|
73
|
-
}
|
|
74
94
|
|
|
75
95
|
return segments
|
|
76
96
|
.map((segment) => {
|
|
77
|
-
const codes =
|
|
97
|
+
const codes =
|
|
98
|
+
theme === "mono"
|
|
99
|
+
? getMonoCodes(segment.tone)
|
|
100
|
+
: theme === "light"
|
|
101
|
+
? getLightCodes(segment.tone)
|
|
102
|
+
: getDarkCodes(segment.tone);
|
|
78
103
|
return applyCodes(segment.text, codes);
|
|
79
104
|
})
|
|
80
105
|
.join("");
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LOW_QUOTA_THRESHOLD } from "../../shared/constants.js";
|
|
2
|
+
|
|
1
3
|
function padTwoDigits(value) {
|
|
2
4
|
return String(value).padStart(2, "0");
|
|
3
5
|
}
|
|
@@ -37,7 +39,7 @@ function getSeverity(leftPercent) {
|
|
|
37
39
|
return "good";
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
if (leftPercent >=
|
|
42
|
+
if (leftPercent >= LOW_QUOTA_THRESHOLD) {
|
|
41
43
|
return "warn";
|
|
42
44
|
}
|
|
43
45
|
|
package/src/shared/config.js
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
|
-
DEFAULT_BAR_WIDTH,
|
|
7
7
|
DEFAULT_CACHE_TTL_MS,
|
|
8
8
|
DEFAULT_CN_BASE_URL,
|
|
9
9
|
DEFAULT_INTL_BASE_URL,
|
|
10
10
|
DEFAULT_DISPLAY_MODE,
|
|
11
|
-
DEFAULT_PALETTE,
|
|
12
11
|
DEFAULT_QUOTA_URL,
|
|
13
12
|
DEFAULT_STYLE,
|
|
14
13
|
DEFAULT_THEME,
|
|
15
14
|
DEFAULT_TIMEOUT_MS
|
|
16
15
|
} from "./constants.js";
|
|
17
16
|
|
|
17
|
+
function getClaudeSettingsPath() {
|
|
18
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readClaudeEnv(claudeSettingsPath) {
|
|
22
|
+
try {
|
|
23
|
+
const filePath = claudeSettingsPath || getClaudeSettingsPath();
|
|
24
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
25
|
+
const settings = JSON.parse(raw);
|
|
26
|
+
return settings?.env && typeof settings.env === "object" ? settings.env : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
function getCacheRoot() {
|
|
19
33
|
if (process.platform === "darwin") {
|
|
20
34
|
return path.join(os.homedir(), "Library", "Caches");
|
|
@@ -63,13 +77,17 @@ export function normalizeOptionalString(value) {
|
|
|
63
77
|
return value.trim();
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
export function loadConfig(env = process.env, overrides = {}) {
|
|
80
|
+
export async function loadConfig(env = process.env, overrides = {}, options = {}) {
|
|
81
|
+
const claudeEnv = await readClaudeEnv(options.claudeSettingsPath);
|
|
67
82
|
const anthropicBaseUrl =
|
|
68
|
-
normalizeOptionalString(overrides.baseUrl) ||
|
|
83
|
+
normalizeOptionalString(overrides.baseUrl) ||
|
|
84
|
+
normalizeOptionalString(env.ANTHROPIC_BASE_URL) ||
|
|
85
|
+
normalizeOptionalString(claudeEnv?.ANTHROPIC_BASE_URL);
|
|
69
86
|
const derivedQuotaUrl = deriveQuotaUrl(anthropicBaseUrl);
|
|
70
87
|
const authorization =
|
|
71
88
|
normalizeOptionalString(overrides.authToken) ||
|
|
72
|
-
normalizeOptionalString(env.ANTHROPIC_AUTH_TOKEN)
|
|
89
|
+
normalizeOptionalString(env.ANTHROPIC_AUTH_TOKEN) ||
|
|
90
|
+
normalizeOptionalString(claudeEnv?.ANTHROPIC_AUTH_TOKEN);
|
|
73
91
|
const tokenHash = authorization
|
|
74
92
|
? crypto.createHash("sha256").update(authorization).digest("hex").slice(0, 12)
|
|
75
93
|
: "anonymous";
|
|
@@ -84,8 +102,6 @@ export function loadConfig(env = process.env, overrides = {}) {
|
|
|
84
102
|
displayMode: DEFAULT_DISPLAY_MODE,
|
|
85
103
|
style: DEFAULT_STYLE,
|
|
86
104
|
theme: DEFAULT_THEME,
|
|
87
|
-
palette: DEFAULT_PALETTE,
|
|
88
|
-
barWidth: DEFAULT_BAR_WIDTH,
|
|
89
105
|
cacheFilePath: path.join(getCacheRoot(), "glm-quota-line", cacheFileName)
|
|
90
106
|
};
|
|
91
107
|
}
|
package/src/shared/constants.js
CHANGED
|
@@ -4,18 +4,18 @@ export const DEFAULT_INTL_BASE_URL = "https://api.z.ai";
|
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
5
5
|
export const DEFAULT_CACHE_TTL_MS = 600_000;
|
|
6
6
|
|
|
7
|
-
export const
|
|
8
|
-
{
|
|
9
|
-
{
|
|
10
|
-
{
|
|
7
|
+
export const REFRESH_BANDS = [
|
|
8
|
+
{ minLeftPercent: 80, ttlMs: 120_000 }, // 2 min
|
|
9
|
+
{ minLeftPercent: 30, ttlMs: 300_000 }, // 5 min
|
|
10
|
+
{ minLeftPercent: 0, ttlMs: 120_000 } // 2 min
|
|
11
11
|
];
|
|
12
12
|
|
|
13
13
|
export const LOW_QUOTA_THRESHOLD = 30;
|
|
14
|
+
export const RATE_LIMIT_RETRY_TTL_MS = 180_000;
|
|
15
|
+
export const UNAVAILABLE_RETRY_TTL_MS = 120_000;
|
|
14
16
|
export const DEFAULT_DISPLAY_MODE = "left";
|
|
15
|
-
export const DEFAULT_STYLE = "
|
|
16
|
-
export const
|
|
17
|
-
export const DEFAULT_THEME = "ansi";
|
|
18
|
-
export const DEFAULT_PALETTE = "dark";
|
|
17
|
+
export const DEFAULT_STYLE = "bar";
|
|
18
|
+
export const DEFAULT_THEME = "dark";
|
|
19
19
|
|
|
20
20
|
export const STATUS_BAR_CHARACTERS = {
|
|
21
21
|
filled: "█",
|
|
@@ -30,15 +30,11 @@ export function isValidStatusStyle(value) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isValidDisplayMode(value) {
|
|
33
|
-
return value === "left" || value === "used"
|
|
33
|
+
return value === "left" || value === "used";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function isValidTheme(value) {
|
|
37
|
-
return value === "
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function isValidPalette(value) {
|
|
41
|
-
return value === "dark" || value === "mono";
|
|
37
|
+
return value === "dark" || value === "light" || value === "mono";
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
export function normalizeStatusStyle(value) {
|
|
@@ -52,7 +48,3 @@ export function normalizeDisplayMode(value) {
|
|
|
52
48
|
export function normalizeTheme(value) {
|
|
53
49
|
return isValidTheme(value) ? value : DEFAULT_THEME;
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
export function normalizePalette(value) {
|
|
57
|
-
return isValidPalette(value) ? value : DEFAULT_PALETTE;
|
|
58
|
-
}
|