glm-quota-line 0.7.0 → 0.8.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 +10 -0
- package/README.en.md +22 -17
- package/README.md +23 -18
- package/package.json +6 -4
- package/src/claude/install.js +27 -43
- package/src/claude/sessionStart.js +2 -5
- package/src/claude/settings.js +2 -10
- package/src/cli/index.js +1 -25
- package/src/cli/update.js +1 -1
- package/src/core/quota/cache.js +19 -29
- package/src/core/quota/parse.js +1 -1
- package/src/core/quota/service.js +49 -37
- package/src/core/status/bar.js +1 -1
- package/src/core/status/format.js +5 -11
- package/src/core/status/theme.js +1 -11
- package/src/shared/config.js +9 -22
- package/src/shared/constants.js +14 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
- 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
|
|
6
|
+
- Added low-quota override: when remaining quota drops below 30%, the refresh interval is forced back to 3 minutes regardless of current tier, so users see accurate data when it matters most
|
|
7
|
+
- Low-quota refreshes do not advance the tier counter, preserving the previous tier when quota recovers
|
|
8
|
+
- SessionStart (startup / resume / clear) resets the tier to level 0, ensuring fresh sessions always start responsive
|
|
9
|
+
- Rate-limited and failed responses no longer advance the tier counter
|
|
10
|
+
- Old cache files without tier fields automatically migrate to tier 0
|
|
11
|
+
- Simplified README quick start to two commands with style config clearly marked as optional
|
|
12
|
+
|
|
3
13
|
## 0.7.0
|
|
4
14
|
|
|
5
15
|
- Added `--version` and `version` so the installed CLI version is visible directly from the command line
|
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
|
-
One-command install, zero dependencies. Shows accurate GLM quota balance and reset time in the Claude Code status line — data from the official API, not estimated.
|
|
4
|
+
One-command install, zero dependencies. Shows accurate Zhipu GLM Coding Plan quota balance and reset time in the Claude Code status line — data from the official API, not estimated.
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="./README.md">简体中文</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
`glm-quota-line` reads the GLM 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.
|
|
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
19
|
|
|
20
20
|
## Features
|
|
21
21
|
|
|
@@ -25,13 +25,20 @@
|
|
|
25
25
|
- **Zero dependencies** — no runtime deps, single-purpose CLI
|
|
26
26
|
- **Smart caching** — refreshes by session, TTL, and token usage; supports manual auth overrides for proxy/gateway setups
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Quick Start
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install -g glm-quota-line
|
|
32
|
+
glm-quota-line install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That's it — works out of the box. Default output (`style=text`, `theme=ansi`, `palette=dark`):
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
Both the 5-hour quota (primary) and weekly quota (secondary) are shown with automatic reset time.
|
|
35
42
|
|
|
36
43
|
Upgrade:
|
|
37
44
|
|
|
@@ -40,28 +47,30 @@ npm install -g glm-quota-line
|
|
|
40
47
|
glm-quota-line check-update
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
##
|
|
50
|
+
## Custom Styles
|
|
51
|
+
|
|
52
|
+
All style options are optional — adjust only if needed:
|
|
44
53
|
|
|
45
54
|
```bash
|
|
46
|
-
glm-quota-line
|
|
47
|
-
glm-quota-line config set
|
|
48
|
-
glm-quota-line config set theme
|
|
49
|
-
glm-quota-line config set palette dark
|
|
55
|
+
glm-quota-line config set style bar # layout: text / compact / bar
|
|
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)
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
Different `style` outputs:
|
|
53
61
|
|
|
54
62
|
```text
|
|
63
|
+
# style=text (default)
|
|
55
64
|
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
56
65
|
|
|
66
|
+
# style=compact
|
|
57
67
|
GLM 5h 91% W 47% | 14:47
|
|
58
68
|
|
|
69
|
+
# style=bar
|
|
59
70
|
GLM Lite █░░░░░░░░░ 91% | W 47% | 14:47
|
|
60
71
|
```
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Styles & Colors
|
|
73
|
+
Recommended combinations:
|
|
65
74
|
|
|
66
75
|
| Use case | Config |
|
|
67
76
|
| -------------- | --------------------------------------------- |
|
|
@@ -69,10 +78,6 @@ Both the 5-hour quota (primary) and weekly quota (secondary) are shown with auto
|
|
|
69
78
|
| Light terminal | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
70
79
|
| Plain fallback | any `style`, `theme=plain` |
|
|
71
80
|
|
|
72
|
-
`style` controls layout (`text` / `compact` / `bar`). `theme` enables ANSI color. `palette` only applies when `theme=ansi`.
|
|
73
|
-
|
|
74
|
-
Set `NO_COLOR=1` to force plain output.
|
|
75
|
-
|
|
76
81
|
## Custom Auth
|
|
77
82
|
|
|
78
83
|
If Claude Code runs behind a gateway or proxy and the injected `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` are not the real values:
|
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
|
-
一行命令安装、零依赖,在 Claude Code
|
|
4
|
+
一行命令安装、零依赖,在 Claude Code 状态栏实时显示智谱(Zhipu)GLM Coding Plan 配额余额与重置时间。数据来自官方接口,精准非估算。
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<a href="./README.en.md">English</a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
|
-
`glm-quota-line`
|
|
18
|
+
`glm-quota-line` 读取智谱(Zhipu)GLM Coding Plan 配额接口、缓存结果,并输出一行适合 Claude Code `statusLine.command` 的状态文本。通过 `SessionStart` hook 预刷新缓存,新会话不会显示旧配额。
|
|
19
19
|
|
|
20
20
|
## 功能
|
|
21
21
|
|
|
@@ -25,43 +25,52 @@
|
|
|
25
25
|
- **零依赖** — 无运行时依赖,单一用途
|
|
26
26
|
- **智能缓存** — 按会话、TTL 和 token 用量刷新;支持代理/网关手动覆盖鉴权
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## 快速开始
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npm install -g glm-quota-line
|
|
32
|
+
glm-quota-line install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
两步完成,开箱即用。默认输出(`style=text`,`theme=ansi`,`palette=dark`):
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
32
39
|
```
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
同时显示 5 小时配额(主要)和周配额(次要),并自动展示重置时间。
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
升级版本:
|
|
37
44
|
|
|
38
45
|
```bash
|
|
39
46
|
npm install -g glm-quota-line
|
|
40
47
|
glm-quota-line check-update
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
##
|
|
50
|
+
## 自定义样式
|
|
51
|
+
|
|
52
|
+
所有样式配置均为可选,按需调整:
|
|
44
53
|
|
|
45
54
|
```bash
|
|
46
|
-
glm-quota-line
|
|
47
|
-
glm-quota-line config set
|
|
48
|
-
glm-quota-line config set theme
|
|
49
|
-
glm-quota-line config set palette dark
|
|
55
|
+
glm-quota-line config set style bar # 布局: text / compact / bar
|
|
56
|
+
glm-quota-line config set theme ansi # 配色: plain / ansi
|
|
57
|
+
glm-quota-line config set palette dark # 色板: dark / mono(仅 theme=ansi 时生效)
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
不同 style 效果:
|
|
53
61
|
|
|
54
62
|
```text
|
|
63
|
+
# style=text(默认)
|
|
55
64
|
GLM Lite | 5h 91% | week 47% | reset 14:47
|
|
56
65
|
|
|
66
|
+
# style=compact
|
|
57
67
|
GLM 5h 91% W 47% | 14:47
|
|
58
68
|
|
|
69
|
+
# style=bar
|
|
59
70
|
GLM Lite █░░░░░░░░░ 91% | W 47% | 14:47
|
|
60
71
|
```
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## 样式与配色
|
|
73
|
+
推荐搭配:
|
|
65
74
|
|
|
66
75
|
| 使用场景 | 配置 |
|
|
67
76
|
| ---------- | --------------------------------------------- |
|
|
@@ -69,10 +78,6 @@ GLM Lite █░░░░░░░░░ 91% | W 47% | 14:47
|
|
|
69
78
|
| 浅色终端 | `style=compact`, `theme=ansi`, `palette=mono` |
|
|
70
79
|
| 纯文本回退 | 任意 `style`,`theme=plain` |
|
|
71
80
|
|
|
72
|
-
`style` 控制布局(`text` / `compact` / `bar`),`theme` 控制是否启用 ANSI 颜色,`palette` 仅在 `theme=ansi` 时生效。
|
|
73
|
-
|
|
74
|
-
设置 `NO_COLOR=1` 可强制关闭颜色。
|
|
75
|
-
|
|
76
81
|
## 自定义鉴权
|
|
77
82
|
|
|
78
83
|
如果 Claude Code 运行在代理或网关后面,注入的 `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` 不是实际值:
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glm-quota-line",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Zhipu GLM Coding Plan quota monitor for Claude Code status line.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "caojian",
|
|
8
8
|
"preferGlobal": true,
|
|
9
9
|
"keywords": [
|
|
10
10
|
"claude-code",
|
|
11
|
-
"statusline",
|
|
12
11
|
"glm",
|
|
12
|
+
"zhipu",
|
|
13
|
+
"z.ai",
|
|
14
|
+
"coding-plan",
|
|
13
15
|
"quota",
|
|
14
|
-
"
|
|
16
|
+
"statusline"
|
|
15
17
|
],
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
package/src/claude/install.js
CHANGED
|
@@ -32,7 +32,7 @@ function isToolManagedStatusLine(statusLine) {
|
|
|
32
32
|
return isToolManagedCommand(statusLine?.command);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
function getStatusLineEntryPath() {
|
|
36
36
|
return path.resolve(__dirname, "..", "cli", "index.js");
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -53,38 +53,41 @@ function isToolManagedHook(hook) {
|
|
|
53
53
|
return hook?.type === "command" && isToolManagedCommand(hook.command);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const existingGroups = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : null;
|
|
62
|
-
if (!existingGroups) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
56
|
+
function filterManagedHooksFromGroups(groups) {
|
|
57
|
+
let hadManaged = false;
|
|
58
|
+
const filtered = [];
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
const nextGroups = [];
|
|
68
|
-
|
|
69
|
-
for (const group of existingGroups) {
|
|
60
|
+
for (const group of groups) {
|
|
70
61
|
if (!group || typeof group !== "object" || !Array.isArray(group.hooks)) {
|
|
71
|
-
|
|
62
|
+
filtered.push(group);
|
|
72
63
|
continue;
|
|
73
64
|
}
|
|
74
65
|
|
|
75
66
|
const nextHooks = group.hooks.filter((hook) => !isToolManagedHook(hook));
|
|
76
67
|
if (nextHooks.length !== group.hooks.length) {
|
|
77
|
-
|
|
68
|
+
hadManaged = true;
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
if (nextHooks.length > 0) {
|
|
81
|
-
|
|
82
|
-
...group,
|
|
83
|
-
hooks: nextHooks
|
|
84
|
-
});
|
|
72
|
+
filtered.push({ ...group, hooks: nextHooks });
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
|
|
76
|
+
return { groups: filtered, hadManaged };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function removeManagedSessionStartHooks(settings) {
|
|
80
|
+
if (!settings.hooks || typeof settings.hooks !== "object") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const existingGroups = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : null;
|
|
85
|
+
if (!existingGroups) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { groups: nextGroups, hadManaged: removed } = filterManagedHooksFromGroups(existingGroups);
|
|
90
|
+
|
|
88
91
|
if (nextGroups.length > 0) {
|
|
89
92
|
settings.hooks.SessionStart = nextGroups;
|
|
90
93
|
} else {
|
|
@@ -101,21 +104,10 @@ function removeManagedSessionStartHooks(settings) {
|
|
|
101
104
|
function installManagedSessionStartHooks(settings, command) {
|
|
102
105
|
const existingGroups =
|
|
103
106
|
settings.hooks && typeof settings.hooks === "object" && Array.isArray(settings.hooks.SessionStart)
|
|
104
|
-
? settings.hooks.SessionStart
|
|
105
|
-
group && typeof group === "object" ? { ...group, hooks: Array.isArray(group.hooks) ? [...group.hooks] : group.hooks } : group
|
|
106
|
-
)
|
|
107
|
+
? settings.hooks.SessionStart
|
|
107
108
|
: [];
|
|
108
109
|
|
|
109
|
-
const nextGroups = existingGroups
|
|
110
|
-
if (!group || typeof group !== "object" || !Array.isArray(group.hooks)) {
|
|
111
|
-
return group;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
...group,
|
|
116
|
-
hooks: group.hooks.filter((hook) => !isToolManagedHook(hook))
|
|
117
|
-
};
|
|
118
|
-
});
|
|
110
|
+
const { groups: nextGroups } = filterManagedHooksFromGroups(existingGroups);
|
|
119
111
|
|
|
120
112
|
for (const matcher of SESSION_START_MATCHERS) {
|
|
121
113
|
const targetGroup = nextGroups.find(
|
|
@@ -123,21 +115,13 @@ function installManagedSessionStartHooks(settings, command) {
|
|
|
123
115
|
);
|
|
124
116
|
|
|
125
117
|
if (targetGroup) {
|
|
126
|
-
targetGroup.hooks.push({
|
|
127
|
-
type: "command",
|
|
128
|
-
command
|
|
129
|
-
});
|
|
118
|
+
targetGroup.hooks.push({ type: "command", command });
|
|
130
119
|
continue;
|
|
131
120
|
}
|
|
132
121
|
|
|
133
122
|
nextGroups.push({
|
|
134
123
|
matcher,
|
|
135
|
-
hooks: [
|
|
136
|
-
{
|
|
137
|
-
type: "command",
|
|
138
|
-
command
|
|
139
|
-
}
|
|
140
|
-
]
|
|
124
|
+
hooks: [{ type: "command", command }]
|
|
141
125
|
});
|
|
142
126
|
}
|
|
143
127
|
|
|
@@ -3,7 +3,7 @@ import { resolveQuotaStatus } from "../core/quota/service.js";
|
|
|
3
3
|
import { readStatusLineInput } from "./input.js";
|
|
4
4
|
import { readToolConfig } from "./settings.js";
|
|
5
5
|
|
|
6
|
-
export const SESSION_START_MATCHERS = ["startup", "resume", "clear"
|
|
6
|
+
export const SESSION_START_MATCHERS = ["startup", "resume", "clear"];
|
|
7
7
|
|
|
8
8
|
export async function refreshQuotaOnSessionStart(options = {}) {
|
|
9
9
|
const hookInput = await readStatusLineInput(options.stdin ?? process.stdin);
|
|
@@ -12,10 +12,7 @@ export async function refreshQuotaOnSessionStart(options = {}) {
|
|
|
12
12
|
const resolveQuotaStatusFn = options.resolveQuotaStatusFn ?? resolveQuotaStatus;
|
|
13
13
|
const config = {
|
|
14
14
|
...loadConfigFn(options.env ?? process.env, userConfig),
|
|
15
|
-
sessionId: hookInput?.session_id || ""
|
|
16
|
-
// SessionStart runs before statusLine has token totals, so treat a new session
|
|
17
|
-
// as starting from zero to preserve later token-threshold refreshes.
|
|
18
|
-
observedTokens: 0
|
|
15
|
+
sessionId: hookInput?.session_id || ""
|
|
19
16
|
};
|
|
20
17
|
|
|
21
18
|
return resolveQuotaStatusFn(config, {
|
package/src/claude/settings.js
CHANGED
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
isValidTheme
|
|
11
11
|
} from "../shared/constants.js";
|
|
12
12
|
import { readJsonFile, writeJsonFile } from "../shared/jsonFile.js";
|
|
13
|
+
import { normalizeOptionalString } from "../shared/config.js";
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
function getClaudeDir() {
|
|
15
16
|
return path.join(os.homedir(), ".claude");
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -23,15 +24,6 @@ export function getToolConfigPath() {
|
|
|
23
24
|
return path.join(getClaudeDir(), "glm-quota-line.json");
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function normalizeOptionalString(value) {
|
|
27
|
-
if (typeof value !== "string") {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const trimmed = value.trim();
|
|
32
|
-
return trimmed || undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
27
|
function redactSecret(value) {
|
|
36
28
|
if (!value) {
|
|
37
29
|
return value;
|
package/src/cli/index.js
CHANGED
|
@@ -67,14 +67,6 @@ Examples:
|
|
|
67
67
|
Environment:
|
|
68
68
|
ANTHROPIC_AUTH_TOKEN
|
|
69
69
|
ANTHROPIC_BASE_URL
|
|
70
|
-
GLM_DISPLAY_MODE
|
|
71
|
-
GLM_STYLE
|
|
72
|
-
GLM_BAR_WIDTH
|
|
73
|
-
GLM_TIMEOUT_MS
|
|
74
|
-
GLM_CACHE_TTL_MS
|
|
75
|
-
GLM_THEME
|
|
76
|
-
GLM_PALETTE
|
|
77
|
-
NO_COLOR
|
|
78
70
|
`);
|
|
79
71
|
}
|
|
80
72
|
|
|
@@ -87,21 +79,6 @@ function getStoredDisplayOverrides(userConfig) {
|
|
|
87
79
|
};
|
|
88
80
|
}
|
|
89
81
|
|
|
90
|
-
function asNonNegativeNumber(value) {
|
|
91
|
-
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getObservedTokens(statusLineInput) {
|
|
95
|
-
const inputTokens = asNonNegativeNumber(statusLineInput?.context_window?.total_input_tokens);
|
|
96
|
-
const outputTokens = asNonNegativeNumber(statusLineInput?.context_window?.total_output_tokens);
|
|
97
|
-
|
|
98
|
-
if (inputTokens === null && outputTokens === null) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return (inputTokens ?? 0) + (outputTokens ?? 0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
82
|
export async function main() {
|
|
106
83
|
try {
|
|
107
84
|
const args = parseArgs();
|
|
@@ -128,8 +105,7 @@ export async function main() {
|
|
|
128
105
|
// Display config precedence is env defaults -> persisted config -> CLI flags.
|
|
129
106
|
...getStoredDisplayOverrides(userConfig),
|
|
130
107
|
...args,
|
|
131
|
-
sessionId: statusLineInput?.session_id || ""
|
|
132
|
-
observedTokens: getObservedTokens(statusLineInput)
|
|
108
|
+
sessionId: statusLineInput?.session_id || ""
|
|
133
109
|
};
|
|
134
110
|
const quotaStatus = await resolveQuotaStatus(config);
|
|
135
111
|
|
package/src/cli/update.js
CHANGED
package/src/core/quota/cache.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { asFiniteNumber } from "./parse.js";
|
|
5
|
+
|
|
4
6
|
function isValidQuotaShape(value) {
|
|
5
7
|
if (!value || typeof value.key !== "string") {
|
|
6
8
|
return false;
|
|
@@ -69,10 +71,6 @@ function isSuccessCacheShape(value) {
|
|
|
69
71
|
return isMultiQuotaSuccessCacheShape(value) || isLegacyPercentSuccessCacheShape(value);
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
function asFiniteNumber(value) {
|
|
73
|
-
return Number.isFinite(value) ? value : null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
74
|
function normalizeCache(parsed) {
|
|
77
75
|
if (!parsed || typeof parsed !== "object") {
|
|
78
76
|
return null;
|
|
@@ -81,9 +79,7 @@ function normalizeCache(parsed) {
|
|
|
81
79
|
const result = isSuccessCacheShape(parsed.result) ? parsed.result : null;
|
|
82
80
|
const savedAt = asFiniteNumber(parsed.savedAt);
|
|
83
81
|
const lastAttemptAt = asFiniteNumber(parsed.lastAttemptAt) ?? savedAt;
|
|
84
|
-
const lastObservedTokensAtFetch = asFiniteNumber(parsed.lastObservedTokensAtFetch);
|
|
85
82
|
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
|
|
86
|
-
const skipNextTokenTrigger = parsed.skipNextTokenTrigger === true;
|
|
87
83
|
|
|
88
84
|
if (result && savedAt === null) {
|
|
89
85
|
return null;
|
|
@@ -93,12 +89,21 @@ function normalizeCache(parsed) {
|
|
|
93
89
|
return null;
|
|
94
90
|
}
|
|
95
91
|
|
|
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
|
+
|
|
96
101
|
return {
|
|
97
102
|
savedAt,
|
|
98
103
|
lastAttemptAt,
|
|
99
|
-
lastObservedTokensAtFetch,
|
|
100
104
|
sessionId,
|
|
101
|
-
|
|
105
|
+
refreshCount,
|
|
106
|
+
tierIndex,
|
|
102
107
|
result
|
|
103
108
|
};
|
|
104
109
|
}
|
|
@@ -109,10 +114,8 @@ async function writeCache(cacheFilePath, cache) {
|
|
|
109
114
|
...(Number.isFinite(cache.savedAt) ? { savedAt: cache.savedAt } : {}),
|
|
110
115
|
...(Number.isFinite(cache.lastAttemptAt) ? { lastAttemptAt: cache.lastAttemptAt } : {}),
|
|
111
116
|
...(cache.sessionId ? { sessionId: cache.sessionId } : {}),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
: {}),
|
|
115
|
-
...(cache.skipNextTokenTrigger ? { skipNextTokenTrigger: true } : {}),
|
|
117
|
+
refreshCount: cache.refreshCount ?? 0,
|
|
118
|
+
tierIndex: cache.tierIndex ?? 0,
|
|
116
119
|
...(cache.result ? { result: cache.result } : {})
|
|
117
120
|
},
|
|
118
121
|
null,
|
|
@@ -139,8 +142,8 @@ export async function writeSuccessCache(cacheFilePath, result, options = {}) {
|
|
|
139
142
|
savedAt: now,
|
|
140
143
|
lastAttemptAt: now,
|
|
141
144
|
sessionId: options.sessionId || "",
|
|
142
|
-
|
|
143
|
-
|
|
145
|
+
refreshCount: options.refreshCount ?? 0,
|
|
146
|
+
tierIndex: options.tierIndex ?? 0,
|
|
144
147
|
result
|
|
145
148
|
});
|
|
146
149
|
}
|
|
@@ -152,21 +155,8 @@ export async function writeRateLimitedCache(cacheFilePath, cached, options = {})
|
|
|
152
155
|
savedAt: cached?.savedAt ?? null,
|
|
153
156
|
lastAttemptAt: now,
|
|
154
157
|
sessionId: options.sessionId || cached?.sessionId || "",
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
skipNextTokenTrigger: true,
|
|
158
|
-
result: cached?.result ?? null
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export async function writeSkippedTokenTriggerCache(cacheFilePath, cached, options = {}) {
|
|
163
|
-
await writeCache(cacheFilePath, {
|
|
164
|
-
savedAt: cached?.savedAt ?? null,
|
|
165
|
-
lastAttemptAt: cached?.lastAttemptAt ?? null,
|
|
166
|
-
sessionId: options.sessionId || cached?.sessionId || "",
|
|
167
|
-
lastObservedTokensAtFetch:
|
|
168
|
-
asFiniteNumber(options.observedTokens) ?? cached?.lastObservedTokensAtFetch ?? null,
|
|
169
|
-
skipNextTokenTrigger: false,
|
|
158
|
+
refreshCount: cached?.refreshCount ?? 0,
|
|
159
|
+
tierIndex: cached?.tierIndex ?? 0,
|
|
170
160
|
result: cached?.result ?? null
|
|
171
161
|
});
|
|
172
162
|
}
|
package/src/core/quota/parse.js
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readCache,
|
|
3
3
|
writeRateLimitedCache,
|
|
4
|
-
writeSkippedTokenTriggerCache,
|
|
5
4
|
writeSuccessCache
|
|
6
5
|
} from "./cache.js";
|
|
7
6
|
import { fetchQuota } from "./fetch.js";
|
|
8
7
|
import { parseQuotaResponse } from "./parse.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
14
|
-
}
|
|
8
|
+
import {
|
|
9
|
+
LOW_QUOTA_THRESHOLD,
|
|
10
|
+
REFRESH_TIERS
|
|
11
|
+
} from "../../shared/constants.js";
|
|
15
12
|
|
|
16
13
|
function getCachedStatus(cached) {
|
|
17
14
|
return cached?.result ?? { kind: "unavailable" };
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
17
|
+
function getEffectiveTtlMs(tierIndex, leftPercent) {
|
|
18
|
+
if (Number.isFinite(leftPercent) && leftPercent < LOW_QUOTA_THRESHOLD) {
|
|
19
|
+
return REFRESH_TIERS[0].ttlMs;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const tier = REFRESH_TIERS[tierIndex];
|
|
23
|
+
return tier ? tier.ttlMs : REFRESH_TIERS[REFRESH_TIERS.length - 1].ttlMs;
|
|
24
|
+
}
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
function advanceTier(refreshCount, tierIndex) {
|
|
27
|
+
const nextCount = refreshCount + 1;
|
|
28
|
+
const tier = REFRESH_TIERS[tierIndex] ?? REFRESH_TIERS[REFRESH_TIERS.length - 1];
|
|
29
|
+
|
|
30
|
+
if (nextCount >= tier.maxRefreshes && tierIndex < REFRESH_TIERS.length - 1) {
|
|
31
|
+
return { refreshCount: 1, tierIndex: tierIndex + 1 };
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
return { refreshCount: nextCount, tierIndex };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shouldRefreshQuota(cached, cacheTtlMs, now, sessionId) {
|
|
38
|
+
if (!cached) {
|
|
39
|
+
return true;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
42
|
+
if (sessionId && cached.sessionId !== sessionId) {
|
|
43
|
+
return true;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
if (cached.
|
|
42
|
-
return
|
|
46
|
+
if (cached.lastAttemptAt === null || now - cached.lastAttemptAt >= cacheTtlMs) {
|
|
47
|
+
return true;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
return
|
|
50
|
+
return false;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export async function resolveQuotaStatus(config, options = {}) {
|
|
49
54
|
const now = options.now ?? Date.now();
|
|
50
55
|
const fetchImpl = options.fetchImpl;
|
|
51
56
|
const sessionId = config.sessionId || "";
|
|
52
|
-
const observedTokens = normalizeObservedTokens(config.observedTokens);
|
|
53
57
|
|
|
54
58
|
if (!config.authorization) {
|
|
55
59
|
return { kind: "auth_error" };
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const cached = await readCache(config.cacheFilePath);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const tierIndex = cached?.tierIndex ?? 0;
|
|
64
|
+
const leftPercent = cached?.result?.leftPercent;
|
|
65
|
+
const effectiveTtlMs = getEffectiveTtlMs(tierIndex, leftPercent);
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
67
|
+
const shouldRefresh = options.forceRefresh
|
|
68
|
+
? true
|
|
69
|
+
: shouldRefreshQuota(cached, effectiveTtlMs, now, sessionId);
|
|
66
70
|
|
|
67
|
-
if (
|
|
68
|
-
await writeSkippedTokenTriggerCache(config.cacheFilePath, cached, {
|
|
69
|
-
sessionId,
|
|
70
|
-
observedTokens
|
|
71
|
-
});
|
|
71
|
+
if (!shouldRefresh) {
|
|
72
72
|
return getCachedStatus(cached);
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -76,10 +76,23 @@ 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
|
+
|
|
79
91
|
await writeSuccessCache(config.cacheFilePath, parsed, {
|
|
80
92
|
now,
|
|
81
93
|
sessionId,
|
|
82
|
-
|
|
94
|
+
refreshCount: nextTier.refreshCount,
|
|
95
|
+
tierIndex: nextTier.tierIndex
|
|
83
96
|
});
|
|
84
97
|
return parsed;
|
|
85
98
|
}
|
|
@@ -91,8 +104,7 @@ export async function resolveQuotaStatus(config, options = {}) {
|
|
|
91
104
|
if (parsed.kind === "rate_limited") {
|
|
92
105
|
await writeRateLimitedCache(config.cacheFilePath, cached, {
|
|
93
106
|
now,
|
|
94
|
-
sessionId
|
|
95
|
-
observedTokens
|
|
107
|
+
sessionId
|
|
96
108
|
});
|
|
97
109
|
return getCachedStatus(cached);
|
|
98
110
|
}
|
package/src/core/status/bar.js
CHANGED
|
@@ -8,10 +8,6 @@ import { buildBar } from "./bar.js";
|
|
|
8
8
|
import { applyTheme } from "./theme.js";
|
|
9
9
|
import { buildStatusViewModel } from "./viewModel.js";
|
|
10
10
|
|
|
11
|
-
function getSeverityTone(model) {
|
|
12
|
-
return model.severity || "neutral";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
11
|
function createErrorSegments(model) {
|
|
16
12
|
const tone = model.kind === "auth_error" ? "danger" : "warn";
|
|
17
13
|
return [
|
|
@@ -75,7 +71,7 @@ function appendResetSegments(segments, model) {
|
|
|
75
71
|
}
|
|
76
72
|
|
|
77
73
|
function createTextSegments(model, displayMode) {
|
|
78
|
-
const severityTone =
|
|
74
|
+
const severityTone = model.severity;
|
|
79
75
|
|
|
80
76
|
return appendResetSegments(
|
|
81
77
|
appendSecondarySegments(
|
|
@@ -91,7 +87,7 @@ function createTextSegments(model, displayMode) {
|
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
function createCompactSegments(model) {
|
|
94
|
-
const severityTone =
|
|
90
|
+
const severityTone = model.severity;
|
|
95
91
|
let segments;
|
|
96
92
|
|
|
97
93
|
if (model.secondaryQuota) {
|
|
@@ -126,7 +122,7 @@ function createBarSegments(model, barWidth) {
|
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
const bar = buildBar(model.primaryQuota.usedPercent, barWidth);
|
|
129
|
-
const severityTone =
|
|
125
|
+
const severityTone = model.severity;
|
|
130
126
|
const segments = [
|
|
131
127
|
{ text: model.levelLabel, tone: "label" },
|
|
132
128
|
{ text: " ", tone: "plain" },
|
|
@@ -157,8 +153,7 @@ export function formatStatus(result, options = {}) {
|
|
|
157
153
|
if (model.kind !== "success") {
|
|
158
154
|
return applyTheme(createErrorSegments(model), {
|
|
159
155
|
theme: normalizeTheme(options.theme),
|
|
160
|
-
palette: normalizePalette(options.palette)
|
|
161
|
-
env: options.env
|
|
156
|
+
palette: normalizePalette(options.palette)
|
|
162
157
|
});
|
|
163
158
|
}
|
|
164
159
|
|
|
@@ -175,7 +170,6 @@ export function formatStatus(result, options = {}) {
|
|
|
175
170
|
|
|
176
171
|
return applyTheme(segments, {
|
|
177
172
|
theme: normalizeTheme(options.theme),
|
|
178
|
-
palette: normalizePalette(options.palette)
|
|
179
|
-
env: options.env
|
|
173
|
+
palette: normalizePalette(options.palette)
|
|
180
174
|
});
|
|
181
175
|
}
|
package/src/core/status/theme.js
CHANGED
|
@@ -17,15 +17,6 @@ const ANSI = {
|
|
|
17
17
|
red: "\u001b[31m"
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
function shouldUseAnsi(theme, env = process.env) {
|
|
21
|
-
// NO_COLOR is the explicit opt-out and wins over local config.
|
|
22
|
-
if (env?.NO_COLOR) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return normalizeTheme(theme || DEFAULT_THEME) === "ansi";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
function applyCodes(text, codes) {
|
|
30
21
|
if (!text || !codes.length) {
|
|
31
22
|
return text;
|
|
@@ -76,9 +67,8 @@ function getMonoCodes(tone) {
|
|
|
76
67
|
export function applyTheme(segments, options = {}) {
|
|
77
68
|
const theme = normalizeTheme(options.theme || DEFAULT_THEME);
|
|
78
69
|
const palette = normalizePalette(options.palette || DEFAULT_PALETTE);
|
|
79
|
-
const env = options.env || process.env;
|
|
80
70
|
|
|
81
|
-
if (
|
|
71
|
+
if (theme !== "ansi") {
|
|
82
72
|
return segments.map((segment) => segment.text).join("");
|
|
83
73
|
}
|
|
84
74
|
|
package/src/shared/config.js
CHANGED
|
@@ -12,22 +12,9 @@ import {
|
|
|
12
12
|
DEFAULT_QUOTA_URL,
|
|
13
13
|
DEFAULT_STYLE,
|
|
14
14
|
DEFAULT_THEME,
|
|
15
|
-
DEFAULT_TIMEOUT_MS
|
|
16
|
-
normalizeDisplayMode,
|
|
17
|
-
normalizePalette,
|
|
18
|
-
normalizeStatusStyle,
|
|
19
|
-
normalizeTheme
|
|
15
|
+
DEFAULT_TIMEOUT_MS
|
|
20
16
|
} from "./constants.js";
|
|
21
17
|
|
|
22
|
-
function parsePositiveInt(value, fallback) {
|
|
23
|
-
if (value === undefined || value === null || value === "") {
|
|
24
|
-
return fallback;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const parsed = Number.parseInt(String(value), 10);
|
|
28
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
18
|
function getCacheRoot() {
|
|
32
19
|
if (process.platform === "darwin") {
|
|
33
20
|
return path.join(os.homedir(), "Library", "Caches");
|
|
@@ -68,7 +55,7 @@ function deriveQuotaUrl(baseUrl) {
|
|
|
68
55
|
return "";
|
|
69
56
|
}
|
|
70
57
|
|
|
71
|
-
function normalizeOptionalString(value) {
|
|
58
|
+
export function normalizeOptionalString(value) {
|
|
72
59
|
if (typeof value !== "string") {
|
|
73
60
|
return "";
|
|
74
61
|
}
|
|
@@ -92,13 +79,13 @@ export function loadConfig(env = process.env, overrides = {}) {
|
|
|
92
79
|
quotaUrl: derivedQuotaUrl || DEFAULT_QUOTA_URL,
|
|
93
80
|
authorization,
|
|
94
81
|
anthropicBaseUrl,
|
|
95
|
-
timeoutMs:
|
|
96
|
-
cacheTtlMs:
|
|
97
|
-
displayMode:
|
|
98
|
-
style:
|
|
99
|
-
theme:
|
|
100
|
-
palette:
|
|
101
|
-
barWidth:
|
|
82
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
83
|
+
cacheTtlMs: DEFAULT_CACHE_TTL_MS,
|
|
84
|
+
displayMode: DEFAULT_DISPLAY_MODE,
|
|
85
|
+
style: DEFAULT_STYLE,
|
|
86
|
+
theme: DEFAULT_THEME,
|
|
87
|
+
palette: DEFAULT_PALETTE,
|
|
88
|
+
barWidth: DEFAULT_BAR_WIDTH,
|
|
102
89
|
cacheFilePath: path.join(getCacheRoot(), "glm-quota-line", cacheFileName)
|
|
103
90
|
};
|
|
104
91
|
}
|
package/src/shared/constants.js
CHANGED
|
@@ -2,18 +2,21 @@ export const DEFAULT_QUOTA_URL = "https://bigmodel.cn/api/monitor/usage/quota/li
|
|
|
2
2
|
export const DEFAULT_CN_BASE_URL = "https://open.bigmodel.cn";
|
|
3
3
|
export const DEFAULT_INTL_BASE_URL = "https://api.z.ai";
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 5000;
|
|
5
|
-
export const DEFAULT_CACHE_TTL_MS =
|
|
5
|
+
export const DEFAULT_CACHE_TTL_MS = 600_000;
|
|
6
|
+
|
|
7
|
+
export const REFRESH_TIERS = [
|
|
8
|
+
{ ttlMs: 180_000, maxRefreshes: 5 }, // 3 min × 5
|
|
9
|
+
{ ttlMs: 300_000, maxRefreshes: 5 }, // 5 min × 5
|
|
10
|
+
{ ttlMs: 600_000, maxRefreshes: Infinity } // 10 min (cap)
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export const LOW_QUOTA_THRESHOLD = 30;
|
|
6
14
|
export const DEFAULT_DISPLAY_MODE = "left";
|
|
7
15
|
export const DEFAULT_STYLE = "text";
|
|
8
16
|
export const DEFAULT_BAR_WIDTH = 10;
|
|
9
|
-
export const DEFAULT_THEME = "
|
|
17
|
+
export const DEFAULT_THEME = "ansi";
|
|
10
18
|
export const DEFAULT_PALETTE = "dark";
|
|
11
19
|
|
|
12
|
-
export const STATUS_STYLES = ["text", "compact", "bar"];
|
|
13
|
-
export const DISPLAY_MODES = ["left", "used", "both"];
|
|
14
|
-
export const THEMES = ["plain", "ansi"];
|
|
15
|
-
export const PALETTES = ["dark", "mono"];
|
|
16
|
-
|
|
17
20
|
export const STATUS_BAR_CHARACTERS = {
|
|
18
21
|
filled: "█",
|
|
19
22
|
empty: "░"
|
|
@@ -22,23 +25,20 @@ export const STATUS_BAR_CHARACTERS = {
|
|
|
22
25
|
export const TOOL_CONFIG_SCHEMA_VERSION = 1;
|
|
23
26
|
export const TOOL_CONFIG_MANAGED_BY = "glm-quota-line";
|
|
24
27
|
|
|
25
|
-
export const QUOTA_UNAVAILABLE_TEXT = "GLM | quota unavailable";
|
|
26
|
-
export const AUTH_EXPIRED_TEXT = "GLM | auth expired";
|
|
27
|
-
|
|
28
28
|
export function isValidStatusStyle(value) {
|
|
29
|
-
return
|
|
29
|
+
return value === "text" || value === "compact" || value === "bar";
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isValidDisplayMode(value) {
|
|
33
|
-
return
|
|
33
|
+
return value === "left" || value === "used" || value === "both";
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function isValidTheme(value) {
|
|
37
|
-
return
|
|
37
|
+
return value === "plain" || value === "ansi";
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function isValidPalette(value) {
|
|
41
|
-
return
|
|
41
|
+
return value === "dark" || value === "mono";
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export function normalizeStatusStyle(value) {
|