cc-hud 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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "cc-hud",
3
+ "owner": {
4
+ "name": "Water"
5
+ },
6
+ "metadata": {
7
+ "description": "Compact statusline HUD for Claude Code",
8
+ "version": "0.1.0"
9
+ },
10
+ "plugins": [
11
+ {
12
+ "name": "cc-hud",
13
+ "source": "./",
14
+ "description": "Compact single-line statusline showing context usage, active agents, and rate limits.",
15
+ "category": "monitoring",
16
+ "tags": ["hud", "statusline", "status-bar", "monitoring", "context", "agents", "rate-limit", "windows", "crash-free", "node", "zero-dependency", "catppuccin", "progress-bar", "usage"]
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "cc-hud",
3
+ "description": "Compact statusline HUD for Claude Code — context, agents, rate limits",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Water"
7
+ },
8
+ "commands": [
9
+ "./commands/setup.md"
10
+ ],
11
+ "homepage": "https://github.com/WaterTian/cc-hud",
12
+ "repository": "https://github.com/WaterTian/cc-hud",
13
+ "license": "MIT",
14
+ "keywords": ["hud", "statusline", "status-bar", "monitoring", "context", "agents", "rate-limit", "windows", "crash-free", "node", "zero-dependency", "catppuccin", "progress-bar", "usage"]
15
+ }
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <!--
2
+ cc-hud — Claude Code statusline plugin
3
+ Shows: model name, context window usage (progress bar), active subagents, rate limits (5h/7d)
4
+ Pure Node.js, zero dependencies, crash-free on Windows (no Bun)
5
+ Install: /plugin marketplace add WaterTian/cc-hud && /plugin install cc-hud && /cc-hud:setup
6
+ Keywords: claude-code plugin statusline status-bar hud monitoring context-window rate-limit agents windows crash-free zero-dependency catppuccin
7
+ -->
8
+
9
+ <p align="center">
10
+ <img src="cc-hud-preview.png" alt="cc-hud preview" width="720" />
11
+ </p>
12
+
13
+ <h1 align="center">CC-HUD</h1>
14
+
15
+ <p align="center">
16
+ <strong>A compact, single-line statusline plugin for <a href="https://claude.ai/claude-code">Claude Code</a></strong><br/>
17
+ <sub>Crash-free, zero-dependency status bar — model · context · agents · rate limits</sub>
18
+ </p>
19
+
20
+ <p align="center">
21
+ <code>Model</code> &nbsp;&rarr;&nbsp; <code>Context</code> &nbsp;&rarr;&nbsp; <code>Agents</code> &nbsp;&rarr;&nbsp; <code>Rate Limits</code>
22
+ <br/>
23
+ <sub>everything you need, nothing you don't.</sub>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#install"><img src="https://img.shields.io/badge/install-3_commands-blueviolet?style=flat-square" alt="install" /></a>
28
+ &nbsp;
29
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen?style=flat-square" alt="zero deps" />
30
+ &nbsp;
31
+ <img src="https://img.shields.io/badge/node-%3E%3D18-blue?style=flat-square" alt="node >= 18" />
32
+ &nbsp;
33
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="MIT" />
34
+ </p>
35
+
36
+ <br/>
37
+
38
+ ## Why CC-HUD?
39
+
40
+ <table>
41
+ <tr><td>
42
+
43
+ ### The Problem
44
+
45
+ Claude Code's native installer bundles [Bun](https://bun.sh), which has a known memory allocator bug on **Windows** ([oven-sh/bun#25082](https://github.com/oven-sh/bun/issues/25082)), causing frequent `pas panic` crashes. Statusline plugins like [jarrodwatts/claude-hud](https://github.com/jarrodwatts/claude-hud) run **on every tick**, amplifying memory pressure and making crashes far more likely.
46
+
47
+ ### The Solution
48
+
49
+ CC-HUD is a **crash-free alternative** — pure Node.js, zero dependencies, stateless per-call, ~60ms execution, 2s hard timeout. Designed to keep your status bar running without taking Claude Code down.
50
+
51
+ </td></tr>
52
+ </table>
53
+
54
+ ## 为什么做 CC-HUD?
55
+
56
+ <table>
57
+ <tr><td>
58
+
59
+ ### 问题
60
+
61
+ Claude Code 原生安装器内嵌 [Bun](https://bun.sh),在 **Windows** 上存在已知内存分配器 bug([oven-sh/bun#25082](https://github.com/oven-sh/bun/issues/25082)),频繁触发 `pas panic` 崩溃。而 [jarrodwatts/claude-hud](https://github.com/jarrodwatts/claude-hud) 等状态栏插件**每次 tick 都会执行**,加剧内存压力,使崩溃更加频繁。
62
+
63
+ ### 解决方案
64
+
65
+ CC-HUD 是**不会崩溃的替代方案** — 纯 Node.js、零依赖、无状态调用、~60ms 执行、2s 硬超时。让状态栏稳定运行,不拖垮 Claude Code。
66
+
67
+ </td></tr>
68
+ </table>
69
+
70
+ > [!TIP]
71
+ > **Windows users:** Use `npm i -g @anthropic-ai/claude-code` instead of the native installer to avoid Bun crashes entirely.
72
+ >
73
+ > **Windows 用户:** 建议用 `npm i -g @anthropic-ai/claude-code` 代替原生安装器,彻底规避 Bun 崩溃。
74
+
75
+ <br/>
76
+
77
+ ## Features
78
+
79
+ <table>
80
+ <tr>
81
+ <td align="center" width="20%"><h3>█▌</h3><b>Context Bar</b><br/><sub>1/8-precision blocks<br/>80-level granularity</sub></td>
82
+ <td align="center" width="20%"><h3>🎨</h3><b>Color</b><br/><sub><a href="https://github.com/catppuccin/catppuccin">Catppuccin Mocha</a><br/>4-stop gradient</sub></td>
83
+ <td align="center" width="20%"><h3>◐</h3><b>Agents</b><br/><sub>Running subagents<br/>with type & model</sub></td>
84
+ <td align="center" width="20%"><h3>%</h3><b>Rate Limits</b><br/><sub>5h / 7d usage<br/>Pro / Max</sub></td>
85
+ <td align="center" width="20%"><h3>0</h3><b>Dependencies</b><br/><sub>Zero. Node.js<br/>built-ins only</sub></td>
86
+ </tr>
87
+ </table>
88
+
89
+ <br/>
90
+
91
+ ## Install
92
+
93
+ Inside Claude Code, run 3 commands:
94
+
95
+ ```
96
+ /plugin marketplace add WaterTian/cc-hud
97
+ /plugin install cc-hud
98
+ /cc-hud:setup
99
+ ```
100
+
101
+ Restart Claude Code. **Done.**
102
+
103
+ <details>
104
+ <summary><b>Manual install</b></summary>
105
+ <br/>
106
+
107
+ ```bash
108
+ git clone https://github.com/WaterTian/cc-hud.git
109
+ cd cc-hud && npm install && npm run build
110
+ ```
111
+
112
+ Add to `~/.claude/settings.json`:
113
+
114
+ ```json
115
+ {
116
+ "statusLine": {
117
+ "type": "command",
118
+ "command": "node /path/to/cc-hud/dist/index.js",
119
+ "padding": 2
120
+ }
121
+ }
122
+ ```
123
+
124
+ </details>
125
+
126
+ <br/>
127
+
128
+ ## How It Works
129
+
130
+ ```
131
+ Claude Code ─── stdin JSON ──→ cc-hud ──→ stdout ──→ status bar
132
+ ↘ transcript JSONL (tail 64KB → active agents)
133
+ ```
134
+
135
+ <table>
136
+ <tr>
137
+ <td align="center"><b>Stateless</b><br/><sub>Fresh process per call<br/>zero memory leaks</sub></td>
138
+ <td align="center"><b>Fast</b><br/><sub>~60ms execution<br/>within 300ms debounce</sub></td>
139
+ <td align="center"><b>Safe</b><br/><sub>2s hard timeout<br/>all IO try-catch</sub></td>
140
+ </tr>
141
+ </table>
142
+
143
+ <br/>
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ npm run build # compile
149
+ npm test # 13 tests
150
+ ```
151
+
152
+ <br/>
153
+
154
+ ---
155
+
156
+ <p align="center">
157
+ <sub>MIT License &copy; <a href="https://github.com/WaterTian">Water</a></sub>
158
+ </p>
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: setup
3
+ description: Configure cc-hud statusline in Claude Code settings
4
+ ---
5
+
6
+ Set up the cc-hud statusline. Write the following `statusLine` config into the user's `~/.claude/settings.json` (merge with existing settings, do not overwrite other fields):
7
+
8
+ ```json
9
+ {
10
+ "statusLine": {
11
+ "type": "command",
12
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js",
13
+ "padding": 2
14
+ }
15
+ }
16
+ ```
17
+
18
+ Use `${CLAUDE_PLUGIN_ROOT}` which resolves to the plugin's install directory.
19
+
20
+ After writing the config, tell the user to restart Claude Code to see the HUD.
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ import { readStdin } from './stdin.js';
2
+ import { parseAgents } from './transcript.js';
3
+ import { render } from './render.js';
4
+ // Hard timeout — never block Claude Code
5
+ const TIMEOUT_MS = 2000;
6
+ setTimeout(() => process.exit(0), TIMEOUT_MS).unref();
7
+ function shortModelName(displayName, id) {
8
+ if (displayName) {
9
+ const stripped = displayName.replace(/\s*\(.*?\)\s*/g, '').trim();
10
+ if (stripped)
11
+ return stripped;
12
+ }
13
+ if (id) {
14
+ const m = id.match(/claude-(\w+)-(\d+)-(\d+)/);
15
+ if (m)
16
+ return `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}`;
17
+ }
18
+ return 'Claude';
19
+ }
20
+ async function main() {
21
+ const data = await readStdin();
22
+ // Parse transcript in parallel with render prep — no dependency
23
+ const agentsPromise = parseAgents(data.transcript_path);
24
+ const contextPercent = data.context_window?.used_percentage ?? 0;
25
+ const agents = await agentsPromise;
26
+ const renderData = {
27
+ model: shortModelName(data.model?.display_name, data.model?.id),
28
+ contextPercent: Math.round(contextPercent),
29
+ agents,
30
+ fiveHourPercent: data.rate_limits?.five_hour?.used_percentage ?? null,
31
+ sevenDayPercent: data.rate_limits?.seven_day?.used_percentage ?? null,
32
+ };
33
+ console.log(render(renderData));
34
+ }
35
+ main().catch(() => process.exit(0));
package/dist/render.js ADDED
@@ -0,0 +1,77 @@
1
+ // — Catppuccin Mocha palette (ANSI 256) —
2
+ const RESET = '\x1b[0m';
3
+ const fg = (n) => `\x1b[38;5;${n}m`;
4
+ const GREEN = fg(151); // #a6e3a1 — ok
5
+ const YELLOW = fg(223); // #f9e2af — caution
6
+ const PEACH = fg(216); // #fab387 — warning
7
+ const RED = fg(211); // #f38ba8 — critical
8
+ const TEAL = fg(115); // #94e2d5 — agent accent
9
+ const BLUE = fg(111); // #89b4fa — info accent
10
+ const OVERLAY = fg(243); // #6c7086 — dim/separator
11
+ const SURFACE = fg(238); // #313244 — bar track
12
+ const TEXT = fg(189); // #cdd6f4 — primary text
13
+ // — Bar config —
14
+ const BAR_WIDTH = 10;
15
+ const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
16
+ const TRACK_CHAR = '░';
17
+ function color(percent) {
18
+ if (percent <= 50)
19
+ return GREEN;
20
+ if (percent <= 70)
21
+ return YELLOW;
22
+ if (percent <= 85)
23
+ return PEACH;
24
+ return RED;
25
+ }
26
+ function progressBar(percent) {
27
+ const clamped = Math.max(0, Math.min(100, percent));
28
+ const total = (clamped / 100) * BAR_WIDTH;
29
+ const full = Math.floor(total);
30
+ const frac = Math.round((total - full) * 8);
31
+ const empty = BAR_WIDTH - full - (frac > 0 ? 1 : 0);
32
+ const c = color(clamped);
33
+ const bar = c + '█'.repeat(full) +
34
+ (frac > 0 ? BLOCKS[frac] : '') +
35
+ RESET + SURFACE +
36
+ TRACK_CHAR.repeat(Math.max(0, empty)) +
37
+ RESET;
38
+ return `${bar} ${c}${clamped}%${RESET}`;
39
+ }
40
+ function rateSegment(label, percent) {
41
+ if (percent == null)
42
+ return null;
43
+ const clamped = Math.round(Math.max(0, Math.min(100, percent)));
44
+ const c = color(clamped);
45
+ return `${OVERLAY}${label}:${RESET} ${c}${clamped}%${RESET}`;
46
+ }
47
+ function agentSegment(agents) {
48
+ if (agents.length === 0)
49
+ return null;
50
+ const parts = agents.slice(0, 3).map(a => {
51
+ const model = a.model ? ` ${OVERLAY}[${a.model}]${RESET}` : '';
52
+ return `${TEAL}◐${RESET} ${TEXT}${a.type}${RESET}${model}`;
53
+ });
54
+ return parts.join(' ');
55
+ }
56
+ export function render(data) {
57
+ const segments = [];
58
+ // Model + context bar
59
+ segments.push(`${OVERLAY}[${RESET}${BLUE}${data.model}${RESET}${OVERLAY}]${RESET} ${progressBar(data.contextPercent)}`);
60
+ // Agents (if any)
61
+ const agentStr = agentSegment(data.agents);
62
+ if (agentStr)
63
+ segments.push(agentStr);
64
+ // Rate limits
65
+ const r5 = rateSegment('5h', data.fiveHourPercent);
66
+ const r7 = rateSegment('7d', data.sevenDayPercent);
67
+ if (r5 && r7) {
68
+ segments.push(`${r5} ${r7}`);
69
+ }
70
+ else if (r5) {
71
+ segments.push(r5);
72
+ }
73
+ else if (r7) {
74
+ segments.push(r7);
75
+ }
76
+ return segments.join(` ${OVERLAY}│${RESET} `);
77
+ }
package/dist/stdin.js ADDED
@@ -0,0 +1,18 @@
1
+ export async function readStdin() {
2
+ if (process.stdin.isTTY)
3
+ return {};
4
+ const chunks = [];
5
+ process.stdin.setEncoding('utf8');
6
+ for await (const chunk of process.stdin) {
7
+ chunks.push(chunk);
8
+ }
9
+ const raw = chunks.join('');
10
+ if (!raw.trim())
11
+ return {};
12
+ try {
13
+ return JSON.parse(raw);
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
@@ -0,0 +1,75 @@
1
+ import { open, stat } from 'node:fs/promises';
2
+ const TAIL_BYTES = 64 * 1024; // 64 KB — agent entries are near the end
3
+ async function readTail(filePath) {
4
+ const info = await stat(filePath);
5
+ if (!info.isFile() || info.size === 0)
6
+ return '';
7
+ const fd = await open(filePath, 'r');
8
+ try {
9
+ const start = Math.max(0, info.size - TAIL_BYTES);
10
+ const len = info.size - start;
11
+ const buf = Buffer.alloc(len);
12
+ await fd.read(buf, 0, len, start);
13
+ const text = buf.toString('utf8');
14
+ if (start > 0) {
15
+ const nl = text.indexOf('\n');
16
+ return nl >= 0 ? text.slice(nl + 1) : '';
17
+ }
18
+ return text;
19
+ }
20
+ finally {
21
+ await fd.close();
22
+ }
23
+ }
24
+ export async function parseAgents(transcriptPath) {
25
+ if (!transcriptPath)
26
+ return [];
27
+ let text;
28
+ try {
29
+ text = await readTail(transcriptPath);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ if (!text)
35
+ return [];
36
+ const agents = new Map();
37
+ const completed = new Set();
38
+ const lines = text.split('\n');
39
+ for (const line of lines) {
40
+ // Fast pre-filter: skip lines that can't contain agent data
41
+ if (!line.includes('"Agent"') && !line.includes('"tool_result"'))
42
+ continue;
43
+ let entry;
44
+ try {
45
+ entry = JSON.parse(line);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ const blocks = entry.message?.content;
51
+ if (!Array.isArray(blocks))
52
+ continue;
53
+ for (const block of blocks) {
54
+ if (block.type === 'tool_use' && block.name === 'Agent' && block.id) {
55
+ const input = block.input ?? {};
56
+ agents.set(block.id, {
57
+ id: block.id,
58
+ type: input.subagent_type ?? 'general-purpose',
59
+ model: input.model,
60
+ description: input.description,
61
+ status: 'running',
62
+ });
63
+ }
64
+ if (block.type === 'tool_result' && block.tool_use_id) {
65
+ completed.add(block.tool_use_id);
66
+ }
67
+ }
68
+ }
69
+ for (const id of completed) {
70
+ const agent = agents.get(id);
71
+ if (agent)
72
+ agent.status = 'completed';
73
+ }
74
+ return [...agents.values()].filter(a => a.status === 'running');
75
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "cc-hud",
3
+ "version": "0.1.0",
4
+ "description": "Compact statusline HUD for Claude Code — context, agents, rate limits",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "test": "npm run build && node --test",
14
+ "test:stdin": "npm run build && echo {\"model\":{\"display_name\":\"Opus\"},\"context_window\":{\"used_percentage\":45,\"context_window_size\":200000},\"rate_limits\":{\"five_hour\":{\"used_percentage\":25},\"seven_day\":{\"used_percentage\":10}}} | node dist/index.js"
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "src/",
19
+ "commands/",
20
+ ".claude-plugin/"
21
+ ],
22
+ "keywords": [
23
+ "claude-code",
24
+ "claude-code-plugin",
25
+ "statusline",
26
+ "status-bar",
27
+ "hud",
28
+ "monitoring",
29
+ "context-window",
30
+ "rate-limit",
31
+ "agents",
32
+ "windows",
33
+ "crash-free",
34
+ "zero-dependency",
35
+ "catppuccin"
36
+ ],
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0",
39
+ "typescript": "^5.0.0"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { readStdin } from './stdin.js';
2
+ import { parseAgents } from './transcript.js';
3
+ import { render } from './render.js';
4
+ import type { RenderData } from './types.js';
5
+
6
+ // Hard timeout — never block Claude Code
7
+ const TIMEOUT_MS = 2000;
8
+ setTimeout(() => process.exit(0), TIMEOUT_MS).unref();
9
+
10
+ function shortModelName(displayName?: string, id?: string): string {
11
+ if (displayName) {
12
+ const stripped = displayName.replace(/\s*\(.*?\)\s*/g, '').trim();
13
+ if (stripped) return stripped;
14
+ }
15
+ if (id) {
16
+ const m = id.match(/claude-(\w+)-(\d+)-(\d+)/);
17
+ if (m) return `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}.${m[3]}`;
18
+ }
19
+ return 'Claude';
20
+ }
21
+
22
+ async function main(): Promise<void> {
23
+ const data = await readStdin();
24
+
25
+ // Parse transcript in parallel with render prep — no dependency
26
+ const agentsPromise = parseAgents(data.transcript_path);
27
+
28
+ const contextPercent = data.context_window?.used_percentage ?? 0;
29
+ const agents = await agentsPromise;
30
+
31
+ const renderData: RenderData = {
32
+ model: shortModelName(data.model?.display_name, data.model?.id),
33
+ contextPercent: Math.round(contextPercent),
34
+ agents,
35
+ fiveHourPercent: data.rate_limits?.five_hour?.used_percentage ?? null,
36
+ sevenDayPercent: data.rate_limits?.seven_day?.used_percentage ?? null,
37
+ };
38
+
39
+ console.log(render(renderData));
40
+ }
41
+
42
+ main().catch(() => process.exit(0));
package/src/render.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { RenderData } from './types.js';
2
+
3
+ // — Catppuccin Mocha palette (ANSI 256) —
4
+ const RESET = '\x1b[0m';
5
+ const fg = (n: number) => `\x1b[38;5;${n}m`;
6
+
7
+ const GREEN = fg(151); // #a6e3a1 — ok
8
+ const YELLOW = fg(223); // #f9e2af — caution
9
+ const PEACH = fg(216); // #fab387 — warning
10
+ const RED = fg(211); // #f38ba8 — critical
11
+ const TEAL = fg(115); // #94e2d5 — agent accent
12
+ const BLUE = fg(111); // #89b4fa — info accent
13
+ const OVERLAY = fg(243); // #6c7086 — dim/separator
14
+ const SURFACE = fg(238); // #313244 — bar track
15
+ const TEXT = fg(189); // #cdd6f4 — primary text
16
+
17
+ // — Bar config —
18
+ const BAR_WIDTH = 10;
19
+ const BLOCKS = [' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'];
20
+ const TRACK_CHAR = '░';
21
+
22
+ function color(percent: number): string {
23
+ if (percent <= 50) return GREEN;
24
+ if (percent <= 70) return YELLOW;
25
+ if (percent <= 85) return PEACH;
26
+ return RED;
27
+ }
28
+
29
+ function progressBar(percent: number): string {
30
+ const clamped = Math.max(0, Math.min(100, percent));
31
+ const total = (clamped / 100) * BAR_WIDTH;
32
+ const full = Math.floor(total);
33
+ const frac = Math.round((total - full) * 8);
34
+ const empty = BAR_WIDTH - full - (frac > 0 ? 1 : 0);
35
+
36
+ const c = color(clamped);
37
+ const bar =
38
+ c + '█'.repeat(full) +
39
+ (frac > 0 ? BLOCKS[frac] : '') +
40
+ RESET + SURFACE +
41
+ TRACK_CHAR.repeat(Math.max(0, empty)) +
42
+ RESET;
43
+
44
+ return `${bar} ${c}${clamped}%${RESET}`;
45
+ }
46
+
47
+ function rateSegment(label: string, percent: number | null): string | null {
48
+ if (percent == null) return null;
49
+ const clamped = Math.round(Math.max(0, Math.min(100, percent)));
50
+ const c = color(clamped);
51
+ return `${OVERLAY}${label}:${RESET} ${c}${clamped}%${RESET}`;
52
+ }
53
+
54
+ function agentSegment(agents: RenderData['agents']): string | null {
55
+ if (agents.length === 0) return null;
56
+ const parts = agents.slice(0, 3).map(a => {
57
+ const model = a.model ? ` ${OVERLAY}[${a.model}]${RESET}` : '';
58
+ return `${TEAL}◐${RESET} ${TEXT}${a.type}${RESET}${model}`;
59
+ });
60
+ return parts.join(' ');
61
+ }
62
+
63
+ export function render(data: RenderData): string {
64
+ const segments: string[] = [];
65
+
66
+ // Model + context bar
67
+ segments.push(`${OVERLAY}[${RESET}${BLUE}${data.model}${RESET}${OVERLAY}]${RESET} ${progressBar(data.contextPercent)}`);
68
+
69
+ // Agents (if any)
70
+ const agentStr = agentSegment(data.agents);
71
+ if (agentStr) segments.push(agentStr);
72
+
73
+ // Rate limits
74
+ const r5 = rateSegment('5h', data.fiveHourPercent);
75
+ const r7 = rateSegment('7d', data.sevenDayPercent);
76
+ if (r5 && r7) {
77
+ segments.push(`${r5} ${r7}`);
78
+ } else if (r5) {
79
+ segments.push(r5);
80
+ } else if (r7) {
81
+ segments.push(r7);
82
+ }
83
+
84
+ return segments.join(` ${OVERLAY}│${RESET} `);
85
+ }
package/src/stdin.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { StdinData } from './types.js';
2
+
3
+ export async function readStdin(): Promise<StdinData> {
4
+ if (process.stdin.isTTY) return {};
5
+
6
+ const chunks: string[] = [];
7
+ process.stdin.setEncoding('utf8');
8
+
9
+ for await (const chunk of process.stdin) {
10
+ chunks.push(chunk as string);
11
+ }
12
+
13
+ const raw = chunks.join('');
14
+ if (!raw.trim()) return {};
15
+
16
+ try {
17
+ return JSON.parse(raw) as StdinData;
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
@@ -0,0 +1,94 @@
1
+ import { open, stat } from 'node:fs/promises';
2
+ import type { AgentEntry } from './types.js';
3
+
4
+ interface ContentBlock {
5
+ type: string;
6
+ id?: string;
7
+ name?: string;
8
+ input?: Record<string, unknown>;
9
+ tool_use_id?: string;
10
+ }
11
+
12
+ interface TranscriptLine {
13
+ message?: { content?: ContentBlock[] };
14
+ }
15
+
16
+ const TAIL_BYTES = 64 * 1024; // 64 KB — agent entries are near the end
17
+
18
+ async function readTail(filePath: string): Promise<string> {
19
+ const info = await stat(filePath);
20
+ if (!info.isFile() || info.size === 0) return '';
21
+
22
+ const fd = await open(filePath, 'r');
23
+ try {
24
+ const start = Math.max(0, info.size - TAIL_BYTES);
25
+ const len = info.size - start;
26
+ const buf = Buffer.alloc(len);
27
+ await fd.read(buf, 0, len, start);
28
+ const text = buf.toString('utf8');
29
+
30
+ if (start > 0) {
31
+ const nl = text.indexOf('\n');
32
+ return nl >= 0 ? text.slice(nl + 1) : '';
33
+ }
34
+ return text;
35
+ } finally {
36
+ await fd.close();
37
+ }
38
+ }
39
+
40
+ export async function parseAgents(transcriptPath: string | undefined): Promise<AgentEntry[]> {
41
+ if (!transcriptPath) return [];
42
+
43
+ let text: string;
44
+ try {
45
+ text = await readTail(transcriptPath);
46
+ } catch {
47
+ return [];
48
+ }
49
+
50
+ if (!text) return [];
51
+
52
+ const agents = new Map<string, AgentEntry>();
53
+ const completed = new Set<string>();
54
+
55
+ const lines = text.split('\n');
56
+ for (const line of lines) {
57
+ // Fast pre-filter: skip lines that can't contain agent data
58
+ if (!line.includes('"Agent"') && !line.includes('"tool_result"')) continue;
59
+
60
+ let entry: TranscriptLine;
61
+ try {
62
+ entry = JSON.parse(line);
63
+ } catch {
64
+ continue;
65
+ }
66
+
67
+ const blocks = entry.message?.content;
68
+ if (!Array.isArray(blocks)) continue;
69
+
70
+ for (const block of blocks) {
71
+ if (block.type === 'tool_use' && block.name === 'Agent' && block.id) {
72
+ const input = block.input ?? {};
73
+ agents.set(block.id, {
74
+ id: block.id,
75
+ type: (input.subagent_type as string) ?? 'general-purpose',
76
+ model: input.model as string | undefined,
77
+ description: input.description as string | undefined,
78
+ status: 'running',
79
+ });
80
+ }
81
+
82
+ if (block.type === 'tool_result' && block.tool_use_id) {
83
+ completed.add(block.tool_use_id);
84
+ }
85
+ }
86
+ }
87
+
88
+ for (const id of completed) {
89
+ const agent = agents.get(id);
90
+ if (agent) agent.status = 'completed';
91
+ }
92
+
93
+ return [...agents.values()].filter(a => a.status === 'running');
94
+ }
package/src/types.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface StdinData {
2
+ model?: { id?: string; display_name?: string };
3
+ context_window?: {
4
+ context_window_size?: number;
5
+ used_percentage?: number | null;
6
+ remaining_percentage?: number | null;
7
+ current_usage?: {
8
+ input_tokens?: number;
9
+ output_tokens?: number;
10
+ cache_creation_input_tokens?: number;
11
+ cache_read_input_tokens?: number;
12
+ } | null;
13
+ };
14
+ rate_limits?: {
15
+ five_hour?: { used_percentage?: number | null; resets_at?: number | null } | null;
16
+ seven_day?: { used_percentage?: number | null; resets_at?: number | null } | null;
17
+ } | null;
18
+ transcript_path?: string;
19
+ cwd?: string;
20
+ }
21
+
22
+ export interface AgentEntry {
23
+ id: string;
24
+ type: string;
25
+ model?: string;
26
+ description?: string;
27
+ status: 'running' | 'completed';
28
+ }
29
+
30
+ export interface RenderData {
31
+ model: string;
32
+ contextPercent: number;
33
+ agents: AgentEntry[];
34
+ fiveHourPercent: number | null;
35
+ sevenDayPercent: number | null;
36
+ }