glm-quota-line 0.8.0 → 0.9.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ - 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
6
+ - 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
7
+ - Removed `refreshCount`/`tierIndex` cache fields and `advanceTier()` — cache format is now simpler, and TTL is derived purely from quota percentage
8
+ - Aligned severity threshold (danger/warn boundary) from hardcoded 25% to `LOW_QUOTA_THRESHOLD` (30%), consistent with the refresh band boundary
9
+ - Unknown CLI commands and invalid config subcommands now print an error and exit with code 1 instead of silently showing quota status
10
+
3
11
  ## 0.8.0
4
12
 
5
13
  - 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.md CHANGED
@@ -106,7 +106,7 @@ glm-quota-line config set <style|display|theme|palette|auth-token|base-url> <val
106
106
  glm-quota-line config unset <key>
107
107
  ```
108
108
 
109
- 完整说明运行 `glm-quota-line --help`。
109
+ 运行 `glm-quota-line --help` 查看完整说明。
110
110
 
111
111
  `glm-quota-line --version` 可直接查看当前安装版本;`check-update` 只检查并提示升级命令,不会自动修改环境。
112
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glm-quota-line",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Zhipu GLM Coding Plan quota monitor for Claude Code status line.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -149,11 +149,13 @@ export async function handleCommand(args, output = process.stdout, dependencies
149
149
  if (command === "config" && subcommand === "set") {
150
150
  const configKey = CONFIG_KEYS[key];
151
151
  if (!configKey) {
152
+ process.exitCode = 1;
152
153
  output.write("Supported config keys: style, display, theme, palette, auth-token, base-url\n");
153
154
  return true;
154
155
  }
155
156
 
156
157
  if (!configKey.validate(value)) {
158
+ process.exitCode = 1;
157
159
  output.write(`${configKey.invalidMessage}\n`);
158
160
  return true;
159
161
  }
@@ -168,6 +170,7 @@ export async function handleCommand(args, output = process.stdout, dependencies
168
170
  if (command === "config" && subcommand === "unset") {
169
171
  const configKey = CONFIG_KEYS[key];
170
172
  if (!configKey) {
173
+ process.exitCode = 1;
171
174
  output.write("Supported config keys: style, display, theme, palette, auth-token, base-url\n");
172
175
  return true;
173
176
  }
@@ -177,5 +180,17 @@ export async function handleCommand(args, output = process.stdout, dependencies
177
180
  return true;
178
181
  }
179
182
 
183
+ if (command === "config") {
184
+ process.exitCode = 1;
185
+ output.write("Supported config subcommands: show, set, unset\n");
186
+ return true;
187
+ }
188
+
189
+ if (command) {
190
+ process.exitCode = 1;
191
+ output.write(`Unknown command: ${command}\nRun 'glm-quota-line -h' for usage.\n`);
192
+ return true;
193
+ }
194
+
180
195
  return false;
181
196
  }
@@ -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
- refreshCount,
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
- refreshCount: cache.refreshCount ?? 0,
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
- refreshCount: options.refreshCount ?? 0,
146
- tierIndex: options.tierIndex ?? 0,
138
+ lastFailureKind: null,
147
139
  result
148
140
  });
149
141
  }
150
142
 
151
- export async function writeRateLimitedCache(cacheFilePath, cached, options = {}) {
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
- refreshCount: cached?.refreshCount ?? 0,
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
- writeRateLimitedCache,
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
- LOW_QUOTA_THRESHOLD,
10
- REFRESH_TIERS
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 getEffectiveTtlMs(tierIndex, leftPercent) {
18
- if (Number.isFinite(leftPercent) && leftPercent < LOW_QUOTA_THRESHOLD) {
19
- return REFRESH_TIERS[0].ttlMs;
18
+ function getRefreshTtlMs(leftPercent) {
19
+ if (!Number.isFinite(leftPercent)) {
20
+ return REFRESH_BANDS[1].ttlMs;
20
21
  }
21
22
 
22
- const tier = REFRESH_TIERS[tierIndex];
23
- return tier ? tier.ttlMs : REFRESH_TIERS[REFRESH_TIERS.length - 1].ttlMs;
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 advanceTier(refreshCount, tierIndex) {
27
- const nextCount = refreshCount + 1;
28
- const tier = REFRESH_TIERS[tierIndex] ?? REFRESH_TIERS[REFRESH_TIERS.length - 1];
27
+ function getEffectiveTtlMs(cached) {
28
+ if (cached?.lastFailureKind === "rate_limited") {
29
+ return RATE_LIMIT_RETRY_TTL_MS;
30
+ }
29
31
 
30
- if (nextCount >= tier.maxRefreshes && tierIndex < REFRESH_TIERS.length - 1) {
31
- return { refreshCount: 1, tierIndex: tierIndex + 1 };
32
+ if (cached?.lastFailureKind === "unavailable") {
33
+ return UNAVAILABLE_RETRY_TTL_MS;
32
34
  }
33
35
 
34
- return { refreshCount: nextCount, tierIndex };
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 tierIndex = cached?.tierIndex ?? 0;
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 writeRateLimitedCache(config.cacheFilePath, cached, {
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
  }
@@ -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 >= 25) {
42
+ if (leftPercent >= LOW_QUOTA_THRESHOLD) {
41
43
  return "warn";
42
44
  }
43
45
 
@@ -4,13 +4,15 @@ 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 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)
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
17
  export const DEFAULT_STYLE = "text";
16
18
  export const DEFAULT_BAR_WIDTH = 10;