@luckvd/glmbar 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LuckVd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # glmbar
2
+
3
+ > Claude Code 状态栏,专为 **GLM Coding Plan**(订阅制)优化。
4
+
5
+ ## 关于 glmbar
6
+
7
+ glmbar 是一个独立的 [Claude Code](https://claude.com/claude-code) 状态栏,
8
+ 专为 **GLM Coding Plan**(订阅制)打造。
9
+
10
+ GLM Coding Plan 按套餐额度计费(5h 窗口 / 月度),而非按量计费——
11
+ 真正的约束是「额度还剩多少、什么时候刷新」,而不是「这次花了多少钱」。
12
+ glmbar 围绕这个核心关切组织状态栏:本次会话 token、套餐额度使用率、
13
+ 上下文剩余、Git 状态,让「该不该收着用、何时 /clear」一目了然。
14
+
15
+ ## 状态栏示例
16
+
17
+ ```
18
+ D: glmbar | G: main* +12 -3 ↑2 | 4.2K | agents: 2+1bg | M: GLM-5.2 | Q: 5h ▓▓░░░ 14%(3m) · 周 ▓░░░░ 6% | MCP ▓▓░░░ 233/1000 | 18.5K/200K ▓░░░░ (9.2%)
19
+ ```
20
+
21
+ 百分比字段都带一条 5 格迷你进度条(随阈值变色)。无内容的字段会自动隐藏:
22
+ 非 git 仓库时省略 `G:`,无活跃 agent 时省略 `agents:`,无 transcript 时省略本次 token 与上下文。
23
+
24
+ ## 字段
25
+
26
+ | 字段 | 说明 |
27
+ | --- | --- |
28
+ | `D:` 目录 | 当前 git 仓库根目录名(非仓库则取当前目录名) |
29
+ | `G:` Git | 分支(`*`=有改动) · 增删行数 · 与远程的 `↑`ahead / `↓`behind |
30
+ | 本次 token | 当前 transcript 内累计 token(input + output),非历史总额 |
31
+ | `agents:` | 活跃子 agent 数 + 后台 agent 数(`N+Mbg`),需配置 hook |
32
+ | `M:` 模型 | 当前模型显示名 |
33
+ | `Q:` 额度 | GLM 套餐使用率(5h 窗口 / 周等)+ 进度条 + 距下次刷新倒计时 |
34
+ | MCP | MCP 工具用量 `current/total` + 进度条 |
35
+ | 上下文 | 本次请求已用 token / 模型上下文窗口(%)+ 进度条 |
36
+
37
+ 使用率/上下文占比按阈值变色:绿(<50%)→ 黄(<80%)→ 红(≥80%);进度条同步变色。
38
+
39
+ ## 配色
40
+
41
+ [Catppuccin Mocha](https://catppuccin.com/),truecolor,深色终端下柔和可读。需终端支持 24-bit 颜色。
42
+
43
+ ## 运行
44
+
45
+ ```bash
46
+ cd /opt/projects/glmbar
47
+ npm test # 跑一次示例输入
48
+ echo '{"model":{"display_name":"GLM-5.2"},"workspace":{"current_dir":"/opt/projects/glmbar"}}' | node src/glmbar.cjs
49
+ ```
50
+
51
+ ## 配置到 Claude Code
52
+
53
+ ### 1. 安装
54
+
55
+ ```bash
56
+ npm install -g @luckvd/glmbar # 从 npm 安装,提供 glmbar 命令
57
+ # 或从源码:cd /opt/projects/glmbar && npm link
58
+ ```
59
+
60
+ ### 2. 启用状态栏
61
+
62
+ 在 `~/.claude/settings.json` 配置:
63
+
64
+ ```json
65
+ { "statusLine": { "type": "command", "command": "glmbar", "padding": 0 } }
66
+ ```
67
+
68
+ ### 3.(可选)启用子 agent 计数
69
+
70
+ `agents:` 的「活跃子 agent」部分依赖一个 hook 维护标记文件。在 `~/.claude/settings.json`
71
+ 的 `hooks` 中注册(`SubagentStart` 创建标记,`SubagentStop` 删除,`SessionStart` 清理):
72
+
73
+ ```json
74
+ {
75
+ "hooks": {
76
+ "SessionStart": [{ "hooks": [{ "type": "command", "command": "node /opt/projects/glmbar/src/hooks/subagent-track.cjs" }] }],
77
+ "SubagentStart": [{ "hooks": [{ "type": "command", "command": "node /opt/projects/glmbar/src/hooks/subagent-track.cjs" }] }],
78
+ "SubagentStop": [{ "hooks": [{ "type": "command", "command": "node /opt/projects/glmbar/src/hooks/subagent-track.cjs" }] }]
79
+ }
80
+ }
81
+ ```
82
+
83
+ > 把路径换成实际的安装位置;或为 `subagent-track.cjs` 加一个 `bin` 入口后用短命令。
84
+
85
+ 后台 agent 计数读取 `~/.claude/daemon/roster.json`,无需额外配置。
86
+
87
+ ## 选项
88
+
89
+ - `glmbar --ascii`:进度条用 `#=---`(老终端降级)。
90
+ - 配置文件 `~/.claude/glmbar/config.json` 的 `barAscii: true` 可持久化 ASCII 模式。
91
+
92
+ ## 额度数据来源
93
+
94
+ `Q:` / MCP 通过 GLM API(`/api/monitor/usage/quota/limit`)获取,读取环境变量或
95
+ `settings.json` 中的 `ANTHROPIC_BASE_URL` / `ANTHROPIC_AUTH_TOKEN`。结果缓存 10 分钟
96
+ (`~/.claude/glmbar/quota-cache.json`),API 不可用时回退到缓存,不阻塞状态栏。
97
+
98
+ ## 状态
99
+
100
+ ✅ 已实现:目录 / Git / 本次 token / 活跃 agent / 模型 / 套餐额度 / MCP / 上下文 + 迷你进度条。
101
+
102
+ ## License
103
+
104
+ MIT
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@luckvd/glmbar",
3
+ "version": "0.1.0",
4
+ "description": "Claude Code statusline for GLM Coding Plan (subscription)",
5
+ "type": "commonjs",
6
+ "main": "src/glmbar.cjs",
7
+ "bin": {
8
+ "glmbar": "src/glmbar.cjs"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/glmbar.cjs",
17
+ "test": "echo '{\"transcript_path\":\"/tmp/x\",\"model\":{\"display_name\":\"GLM-5.2\"},\"workspace\":{\"current_dir\":\"/opt/projects/glmbar\"}}' | node src/glmbar.cjs"
18
+ },
19
+ "engines": {
20
+ "node": ">=14"
21
+ },
22
+ "keywords": [
23
+ "claude-code",
24
+ "statusline",
25
+ "glm",
26
+ "glm-coding-plan",
27
+ "cli",
28
+ "terminal"
29
+ ],
30
+ "license": "MIT",
31
+ "author": "LuckVd",
32
+ "homepage": "https://github.com/LuckVd/glmbar#readme",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/LuckVd/glmbar.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/LuckVd/glmbar/issues"
39
+ }
40
+ }
package/src/glmbar.cjs ADDED
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * glmbar — StatusLine for Claude Code, optimized for GLM Coding Plan (订阅制).
4
+ * 配色:Catppuccin Mocha(柔和浅色系,truecolor)。
5
+ *
6
+ * 字段:目录 | Git | 本次token | 活跃agent | 模型 | Token额度(5h/周+刷新倒计时) | MCP用量 | 上下文
7
+ */
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const os = require('node:os');
12
+ const https = require('node:https');
13
+ const { execSync } = require('node:child_process');
14
+
15
+ // ---------- 颜色(Catppuccin Mocha, truecolor)----------
16
+ const C = {
17
+ reset: '\x1b[0m',
18
+ dim: '\x1b[2m',
19
+ bold: '\x1b[1m',
20
+ blue: '\x1b[38;2;137;180;250m', // 目录 #89b4fa
21
+ mauve: '\x1b[38;2;203;166;247m', // git #cba6f7
22
+ lavender: '\x1b[38;2;180;190;254m', // token #b4befe
23
+ sky: '\x1b[38;2;137;220;235m', // 模型 #89dceb
24
+ peach: '\x1b[38;2;250;179;135m', // MCP/agents #fab387
25
+ sapphire: '\x1b[38;2;116;199;236m', // 远程 #74c7ec
26
+ green: '\x1b[38;2;166;227;161m', // #a6e3a1
27
+ yellow: '\x1b[38;2;249;226;175m', // #f9e2af
28
+ red: '\x1b[38;2;243;139;168m', // #f38ba8
29
+ };
30
+
31
+ // ---------- 路径与常量 ----------
32
+ const HOME = os.homedir();
33
+ const SETTINGS_PATH = path.join(HOME, '.claude', 'settings.json');
34
+ const CACHE_DIR = path.join(HOME, '.claude', 'glmbar');
35
+ const QUOTA_CACHE = path.join(CACHE_DIR, 'quota-cache.json');
36
+ const ACTIVE_AGENT_DIR = path.join(CACHE_DIR, 'active');
37
+ const ROSTER_PATH = path.join(HOME, '.claude', 'daemon', 'roster.json');
38
+ const CONFIG_PATH = path.join(CACHE_DIR, 'config.json');
39
+ const QUOTA_TTL_MS = 10 * 60 * 1000;
40
+
41
+ const _settingsCache = { v: undefined };
42
+ const _transcriptCache = new Map();
43
+ const _gitCache = new Map();
44
+
45
+ // ---------- 基础工具 ----------
46
+ function readInput() {
47
+ try {
48
+ const s = fs.readFileSync(0, 'utf-8');
49
+ return s.trim() ? JSON.parse(s) : {};
50
+ }
51
+ catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ function loadSettings() {
57
+ if (_settingsCache.v !== undefined) return _settingsCache.v;
58
+ try {
59
+ _settingsCache.v = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf-8'));
60
+ }
61
+ catch {
62
+ _settingsCache.v = {};
63
+ }
64
+ return _settingsCache.v;
65
+ }
66
+
67
+ function ensureDir(file) {
68
+ try { fs.mkdirSync(path.dirname(file), { recursive: true }); }
69
+ catch { /* ignore */ }
70
+ }
71
+
72
+ function baseModelId(id) {
73
+ return String(id || '').replace(/\s*\[[^\]]*\]\s*$/, '').trim();
74
+ }
75
+
76
+ function fmtTok(n) {
77
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
78
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
79
+ return `${n}`;
80
+ }
81
+
82
+ function fmtUsed(n) {
83
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
84
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
85
+ return `${n}`;
86
+ }
87
+
88
+ function fmtMax(n) {
89
+ if (n >= 1e6) return `${(n / 1e6).toFixed(0)}M`;
90
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
91
+ return `${n}`;
92
+ }
93
+
94
+ function fmtRemaining(ms) {
95
+ if (ms <= 0) return '0m';
96
+ const totalSec = Math.floor(ms / 1000);
97
+ const h = Math.floor(totalSec / 3600);
98
+ const m = Math.floor((totalSec % 3600) / 60);
99
+ if (h >= 24) {
100
+ const d = Math.floor(h / 24);
101
+ return `${d}d${h % 24}h`;
102
+ }
103
+ if (h > 0) return `${h}h${m}m`;
104
+ return `${m}m`;
105
+ }
106
+
107
+ // ---------- 配置 / 命令行参数 ----------
108
+ function parseArgs(argv) {
109
+ for (const a of argv) if (a === '--ascii') return true;
110
+ return false;
111
+ }
112
+
113
+ function loadConfig() {
114
+ try {
115
+ return { barAscii: !!JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')).barAscii };
116
+ }
117
+ catch {
118
+ return { barAscii: false };
119
+ }
120
+ }
121
+
122
+ const ASCII = parseArgs(process.argv.slice(2)) || loadConfig().barAscii;
123
+
124
+ // ---------- 进度条 ----------
125
+ function fmtBar(pct) {
126
+ const segments = 5;
127
+ const filled = ASCII ? '#' : '▓';
128
+ const empty = ASCII ? '-' : '░';
129
+ const on = Math.max(0, Math.min(segments, Math.round(pct / 20)));
130
+ const bar = filled.repeat(on) + empty.repeat(segments - on);
131
+ const color = pct < 50 ? C.green : pct < 80 ? C.yellow : C.red;
132
+ return `${color}${bar}${C.reset}`;
133
+ }
134
+
135
+ // ---------- Git ----------
136
+ function git(args, cwd) {
137
+ try {
138
+ return execSync(`git ${args}`, {
139
+ encoding: 'utf-8',
140
+ stdio: ['pipe', 'pipe', 'ignore'],
141
+ cwd,
142
+ }).trim();
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function isGitRepo(dir) {
150
+ if (_gitCache.has(`repo:${dir}`)) return _gitCache.get(`repo:${dir}`);
151
+ const r = git('rev-parse --is-inside-work-tree', dir) !== null;
152
+ _gitCache.set(`repo:${dir}`, r);
153
+ return r;
154
+ }
155
+
156
+ function gitRoot(dir) {
157
+ if (_gitCache.has(`root:${dir}`)) return _gitCache.get(`root:${dir}`);
158
+ const r = git('rev-parse --show-toplevel', dir) || dir;
159
+ _gitCache.set(`root:${dir}`, r);
160
+ return r;
161
+ }
162
+
163
+ function getGitInfo(dir) {
164
+ if (!isGitRepo(dir)) return null;
165
+ const branch = git('rev-parse --abbrev-ref HEAD', dir) || '?';
166
+ const dirty = git('diff --quiet && git diff --cached --quiet', dir) === null;
167
+ let added = 0, removed = 0;
168
+ for (const a of ['diff --numstat', 'diff --cached --numstat']) {
169
+ const out = git(a, dir);
170
+ if (out) {
171
+ for (const line of out.split('\n')) {
172
+ const p = line.split('\t');
173
+ if (p.length >= 2) { added += parseInt(p[0]) || 0; removed += parseInt(p[1]) || 0; }
174
+ }
175
+ }
176
+ }
177
+ const untracked = git('ls-files --others --exclude-standard', dir);
178
+ if (untracked) added += untracked.split('\n').filter(Boolean).length;
179
+ let ahead = 0, behind = 0;
180
+ const remote = git('rev-list --left-right --count HEAD...@{u}', dir);
181
+ if (remote) {
182
+ const p = remote.split('\t');
183
+ behind = parseInt(p[0]) || 0;
184
+ ahead = parseInt(p[1]) || 0;
185
+ }
186
+ return { branch, dirty, added, removed, ahead, behind };
187
+ }
188
+
189
+ // ---------- Transcript 解析 ----------
190
+ function parseTranscript(tp) {
191
+ if (!tp) return null;
192
+ if (_transcriptCache.has(tp)) return _transcriptCache.get(tp);
193
+ let result = null;
194
+ try {
195
+ if (fs.existsSync(tp)) {
196
+ const lines = fs.readFileSync(tp, 'utf-8').split('\n').filter(l => l.trim());
197
+ let lastInput = 0;
198
+ let sessionTokens = 0;
199
+ for (const line of lines) {
200
+ try {
201
+ const e = JSON.parse(line);
202
+ const usage = e.message?.usage || e.usage;
203
+ if (usage) {
204
+ const input = (usage.input_tokens || 0)
205
+ + (usage.cache_read_input_tokens || 0)
206
+ + (usage.cache_creation_input_tokens || 0);
207
+ const output = usage.output_tokens || 0;
208
+ sessionTokens += input + output;
209
+ }
210
+ if (e.type === 'assistant' && e.message?.usage) {
211
+ const u = e.message.usage;
212
+ lastInput = (u.input_tokens || 0) + (u.cache_read_input_tokens || 0);
213
+ }
214
+ }
215
+ catch { /* skip */ }
216
+ }
217
+ result = { lastInput, sessionTokens };
218
+ }
219
+ }
220
+ catch { /* ignore */ }
221
+ _transcriptCache.set(tp, result);
222
+ return result;
223
+ }
224
+
225
+ // ---------- GLM Quota ----------
226
+ function getGlmConfig() {
227
+ const settings = loadSettings();
228
+ const baseUrl = process.env.ANTHROPIC_BASE_URL || settings.env?.ANTHROPIC_BASE_URL;
229
+ const token = process.env.ANTHROPIC_AUTH_TOKEN || settings.env?.ANTHROPIC_AUTH_TOKEN;
230
+ if (!baseUrl || !token) return null;
231
+ try {
232
+ const u = new URL(baseUrl);
233
+ return { domain: `${u.protocol}//${u.host}`, token };
234
+ }
235
+ catch {
236
+ return null;
237
+ }
238
+ }
239
+
240
+ function limitLabel(limit) {
241
+ if (limit.type === 'TIME_LIMIT') return 'MCP';
242
+ if (limit.type === 'TOKENS_LIMIT') {
243
+ if (limit.unit === 3) return `${limit.number}h`;
244
+ if (limit.unit === 6) return '周';
245
+ if (limit.unit === 4 && limit.number === 7) return '周';
246
+ if (limit.unit === 4) return `${limit.number}d`;
247
+ if (limit.unit === 5) return `${limit.number}月`;
248
+ return `${limit.number}?`;
249
+ }
250
+ return limit.type;
251
+ }
252
+
253
+ function readQuotaCache() {
254
+ try {
255
+ return JSON.parse(fs.readFileSync(QUOTA_CACHE, 'utf-8'));
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ }
261
+
262
+ function writeQuotaCache(data) {
263
+ ensureDir(QUOTA_CACHE);
264
+ try {
265
+ fs.writeFileSync(QUOTA_CACHE, JSON.stringify({ ...data, fetchedAt: Date.now() }), 'utf-8');
266
+ }
267
+ catch { /* ignore */ }
268
+ }
269
+
270
+ function fetchQuota() {
271
+ return new Promise((resolve) => {
272
+ const cfg = getGlmConfig();
273
+ if (!cfg) return resolve(null);
274
+ const url = new URL(`${cfg.domain}/api/monitor/usage/quota/limit`);
275
+ const req = https.request({
276
+ hostname: url.hostname,
277
+ port: 443,
278
+ path: url.pathname,
279
+ method: 'GET',
280
+ headers: {
281
+ Authorization: cfg.token,
282
+ 'Accept-Language': 'en-US,en',
283
+ 'Content-Type': 'application/json',
284
+ },
285
+ timeout: 4000,
286
+ }, (res) => {
287
+ let data = '';
288
+ res.on('data', (c) => { data += c; });
289
+ res.on('end', () => {
290
+ try { resolve(JSON.parse(data)); }
291
+ catch { resolve(null); }
292
+ });
293
+ });
294
+ req.on('error', () => resolve(null));
295
+ req.on('timeout', () => { req.destroy(); resolve(null); });
296
+ req.end();
297
+ });
298
+ }
299
+
300
+ async function getQuota() {
301
+ const cache = readQuotaCache();
302
+ const now = Date.now();
303
+ if (cache?.limits && (now - (cache.fetchedAt || 0)) < QUOTA_TTL_MS) {
304
+ return cache.limits;
305
+ }
306
+ const resp = await fetchQuota();
307
+ if (resp?.data?.limits) {
308
+ writeQuotaCache({ limits: resp.data.limits, level: resp.data.level });
309
+ return resp.data.limits;
310
+ }
311
+ return cache?.limits || null;
312
+ }
313
+
314
+ // ---------- 字段渲染器 ----------
315
+ function renderDir(input) {
316
+ const cwd = input?.workspace?.current_dir || process.cwd();
317
+ const name = path.basename(gitRoot(cwd));
318
+ return `${C.blue}D: ${name}${C.reset}`;
319
+ }
320
+
321
+ function renderGit(input) {
322
+ const cwd = input?.workspace?.current_dir || process.cwd();
323
+ const g = getGitInfo(cwd);
324
+ if (!g) return null;
325
+ const branchColor = g.dirty ? C.red : C.mauve;
326
+ let s = `${branchColor}G: ${g.branch}${g.dirty ? '*' : ''}${C.reset}`;
327
+ if (g.added || g.removed) {
328
+ s += ` ${C.green}+${g.added}${C.reset} ${C.red}-${g.removed}${C.reset}`;
329
+ }
330
+ if (g.ahead || g.behind) {
331
+ const r = [];
332
+ if (g.ahead) r.push(`↑${g.ahead}`);
333
+ if (g.behind) r.push(`↓${g.behind}`);
334
+ s += ` ${C.sapphire}${r.join(' ')}${C.reset}`;
335
+ }
336
+ return s;
337
+ }
338
+
339
+ function renderContext(input) {
340
+ const parsed = parseTranscript(input?.transcript_path);
341
+ if (!parsed) return null;
342
+ const settings = loadSettings();
343
+ const windows = settings.modelContextWindow || {};
344
+ const id = baseModelId(input?.model?.id || '');
345
+ const max = windows[id]
346
+ || windows[Object.keys(windows).find((k) => k.toLowerCase() === id.toLowerCase())]
347
+ || 200000;
348
+ const used = parsed.lastInput;
349
+ const pct = max ? (used / max) * 100 : 0;
350
+ const color = pct < 50 ? C.green : pct < 80 ? C.yellow : C.red;
351
+ return `${color}${fmtUsed(used)}/${fmtMax(max)} ${fmtBar(pct)} (${pct.toFixed(1)}%)${C.reset}`;
352
+ }
353
+
354
+ function renderSessionTokens(input) {
355
+ const parsed = parseTranscript(input?.transcript_path);
356
+ if (!parsed) return null;
357
+ return `${C.lavender}${fmtTok(parsed.sessionTokens)}${C.reset}`;
358
+ }
359
+
360
+ // 活跃子 agent(hook 标记文件)+ 后台 agent(roster.json)
361
+ function renderAgents(input) {
362
+ const sessionId = input?.session_id;
363
+ let count = 0;
364
+ if (sessionId) {
365
+ try {
366
+ count = fs.readdirSync(path.join(ACTIVE_AGENT_DIR, sessionId)).length;
367
+ }
368
+ catch { /* 无活跃子 agent */ }
369
+ }
370
+ let bg = 0;
371
+ try {
372
+ const roster = JSON.parse(fs.readFileSync(ROSTER_PATH, 'utf-8'));
373
+ bg = Array.isArray(roster) ? roster.length : (roster && typeof roster === 'object' ? Object.keys(roster).length : 0);
374
+ }
375
+ catch { /* 无后台 agent */ }
376
+ const total = count + bg;
377
+ if (total === 0) return null;
378
+ const label = bg > 0 ? `${count}+${bg}bg` : `${total}`;
379
+ return `${C.bold}agents:${C.reset} ${C.peach}${label}${C.reset}`;
380
+ }
381
+
382
+ function renderModel(input) {
383
+ const name = input?.model?.display_name || input?.model?.id || '';
384
+ return name ? `${C.sky}M: ${name}${C.reset}` : null;
385
+ }
386
+
387
+ function renderTokenQuota(limits) {
388
+ const tokens = (limits || []).filter((l) => l.type === 'TOKENS_LIMIT');
389
+ if (!tokens.length) return null;
390
+ const parts = tokens.map((l) => {
391
+ const pct = l.percentage ?? 0;
392
+ const color = pct < 50 ? C.green : pct < 80 ? C.yellow : C.red;
393
+ let s = `${color}${limitLabel(l)} ${fmtBar(pct)} ${pct}%${C.reset}`;
394
+ if (l.nextResetTime) {
395
+ const rem = l.nextResetTime - Date.now();
396
+ if (rem > 0) s += `${C.dim}(${fmtRemaining(rem)})${C.reset}`;
397
+ }
398
+ return s;
399
+ });
400
+ return `${C.bold}Q:${C.reset} ${parts.join(' · ')}`;
401
+ }
402
+
403
+ function renderMcp(limits) {
404
+ const mcp = (limits || []).find((l) => l.type === 'TIME_LIMIT');
405
+ if (!mcp) return null;
406
+ const cur = mcp.currentValue ?? 0;
407
+ const total = mcp.usage ?? 0;
408
+ const pct = total > 0 ? (cur / total) * 100 : 0;
409
+ return `${C.peach}MCP ${pct.toFixed(1)}%${C.reset}`;
410
+ }
411
+
412
+ // ---------- 主流程 ----------
413
+ async function main() {
414
+ const input = readInput();
415
+ const limits = await getQuota();
416
+ const renderers = [
417
+ () => renderDir(input),
418
+ () => renderGit(input),
419
+ () => renderSessionTokens(input),
420
+ () => renderAgents(input),
421
+ () => renderModel(input),
422
+ () => renderTokenQuota(limits),
423
+ () => renderContext(input),
424
+ () => renderMcp(limits),
425
+ ];
426
+ const results = renderers.map((f) => { try { return f(); } catch { return null; } });
427
+ const parts = results.filter((p) => p !== null && p !== undefined);
428
+ process.stdout.write(parts.join(' | ') + '\n');
429
+ }
430
+
431
+ main();
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * glmbar 子 agent 计数 hook(SubagentStart / SubagentStop / SessionStart)
4
+ *
5
+ * 用标记文件维护「当前活跃子 agent」:每个活跃子 agent 在
6
+ * ~/.claude/glmbar/active/<session_id>/<agent_id>
7
+ * 留一个文件;statusLine 数该目录的文件数即得活跃计数。
8
+ *
9
+ * - SubagentStart:创建标记
10
+ * - SubagentStop:删除标记
11
+ * - SessionStart:清空本 session 的标记(防崩溃/强退导致计数漂移)
12
+ *
13
+ * 非阻塞:始终 exit 0。
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const os = require('node:os');
19
+
20
+ const ACTIVE_DIR = path.join(os.homedir(), '.claude', 'glmbar', 'active');
21
+
22
+ function readInput() {
23
+ try {
24
+ const s = fs.readFileSync(0, 'utf-8');
25
+ return s.trim() ? JSON.parse(s) : {};
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ const input = readInput();
33
+ const event = input.hook_event_name;
34
+ const sessionId = input.session_id;
35
+
36
+ if (!sessionId || !event) {
37
+ process.exit(0);
38
+ }
39
+
40
+ const sessionDir = path.join(ACTIVE_DIR, sessionId);
41
+
42
+ try {
43
+ if (event === 'SubagentStart') {
44
+ const agentId = input.agent_id || `agent-${Date.now()}`;
45
+ fs.mkdirSync(sessionDir, { recursive: true });
46
+ fs.writeFileSync(path.join(sessionDir, agentId), String(Date.now()));
47
+ }
48
+ else if (event === 'SubagentStop') {
49
+ const agentId = input.agent_id;
50
+ if (agentId) {
51
+ try { fs.unlinkSync(path.join(sessionDir, agentId)); }
52
+ catch { /* 已不存在 */ }
53
+ }
54
+ }
55
+ else if (event === 'SessionStart') {
56
+ // 新会话开始:清空本 session 的历史标记
57
+ try { fs.rmSync(sessionDir, { recursive: true, force: true }); }
58
+ catch { /* ignore */ }
59
+ }
60
+ }
61
+ catch {
62
+ // hook 失败不应影响 Claude Code
63
+ }
64
+
65
+ process.exit(0);