claude-code-hud 0.2.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/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # claude-code-hud
2
+
3
+ A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status, and project info in a separate terminal window or tmux pane.
4
+
5
+ ```
6
+ ┌────────────────────────────────────────────────────────┐
7
+ │ ◆ HUD [1 TOKENS] 2 PROJECT 3 GIT sonnet-4-6 │
8
+ ├────────────────────────────────────────────────────────┤
9
+ │ CONTEXT WINDOW │
10
+ │ ████████████████░░░░░░░░░░░ 34% 67K / 200K OK │
11
+ ├────────────────────────────────────────────────────────┤
12
+ │ USAGE WINDOW (Anthropic API) │
13
+ │ 5h ████████████████░░░░ 62.0% resets in 4h │
14
+ │ wk ████░░░░░░░░░░░░░░░░ 15.0% resets in 144h │
15
+ ├────────────────────────────────────────────────────────┤
16
+ │ INPUT ██████░░░░░░░░░░░░░░ 48.2K $0.0145 │
17
+ │ OUTPUT ██░░░░░░░░░░░░░░░░░░ 8.1K $0.0122 │
18
+ │ CACHE ████████████░░░░░░░░ 52.0K $0.0047 │
19
+ │ $0.0314 │
20
+ └────────────────────────────────────────────────────────┘
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Features
26
+
27
+ ### TOKENS tab
28
+ - Context window usage gauge (█░ progress bar) with percentage and token counts
29
+ - 5-hour and weekly usage window from Anthropic API (real %)
30
+ - Input / output / cache-read / cache-write breakdown with cost
31
+ - Processing sparkline (▁▂▃▄▅▆▇█) over recent turns
32
+ - Model name display
33
+
34
+ ### PROJECT tab
35
+ - Total file count, package count, detected endpoints
36
+ - Package dependency tree (├─ └─)
37
+ - Endpoint summary (GET / POST / PUT / DELETE counts)
38
+ - Alerts and anomalies
39
+
40
+ ### GIT tab
41
+ - Current branch, ahead/behind counts
42
+ - Changed file list (MOD / ADD / DEL)
43
+ - Per-file diff visualization (+/- bars)
44
+ - Recent commit history with hash, message, and time
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ### Option 1 — Claude Code Plugin (recommended)
51
+
52
+ ```bash
53
+ /plugin install letsgojh0810/hud-plugin
54
+ ```
55
+
56
+ Then use the `/hud` command inside Claude Code to get a status snapshot.
57
+
58
+ ### Option 2 — npx (no install required)
59
+
60
+ ```bash
61
+ npx claude-code-hud
62
+ ```
63
+
64
+ Runs the full interactive TUI in your current terminal. Open a separate terminal window or tmux pane first.
65
+
66
+ ### Option 3 — npm global install
67
+
68
+ ```bash
69
+ npm install -g claude-code-hud
70
+ claude-hud
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Usage
76
+
77
+ Run in a **separate terminal window** or **tmux split pane** while Claude Code is active in another pane:
78
+
79
+ ```bash
80
+ # Separate terminal
81
+ npx claude-code-hud
82
+
83
+ # tmux split (open right pane with HUD)
84
+ tmux split-window -h "npx claude-code-hud"
85
+
86
+ # Point to a specific project directory
87
+ CLAUDE_PROJECT_ROOT=/path/to/project npx claude-code-hud
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Keyboard Shortcuts
93
+
94
+ | Key | Action |
95
+ |-------|----------------------------|
96
+ | `1` | Switch to TOKENS tab |
97
+ | `2` | Switch to PROJECT tab |
98
+ | `3` | Switch to GIT tab |
99
+ | `j` | Scroll down |
100
+ | `k` | Scroll up |
101
+ | `d` | Toggle dark / light mode |
102
+ | `q` | Quit |
103
+
104
+ ---
105
+
106
+ ## Requirements
107
+
108
+ - **Node.js 18+**
109
+ - **Claude Code** installed and active (for token data from JSONL session files)
110
+ - **Claude Pro or Max plan** recommended for full 5h/7d usage window data from Anthropic API
111
+ - Git (for git status features)
112
+
113
+ ---
114
+
115
+ ## Environment Variables
116
+
117
+ | Variable | Default | Description |
118
+ |-----------------------|-------------|-----------------------------------------------------|
119
+ | `CLAUDE_PROJECT_ROOT` | `process.cwd()` | Root directory of the project to monitor |
120
+
121
+ ---
122
+
123
+ ## How it works
124
+
125
+ - **Token data**: Parses `~/.claude/projects/<hash>/sessions/*.jsonl` in real-time using chokidar file watching
126
+ - **Usage window**: Reads Anthropic API usage limits (5h / weekly) when available
127
+ - **Git status**: Polls `simple-git` every 3–5 seconds for branch, diff, and commit info
128
+ - **Project scan**: Uses `fast-glob` to scan files and detect packages/endpoints once, then caches
129
+
130
+ ---
131
+
132
+ ## Color Theme
133
+
134
+ Toss Blue (`#3182F6`) based palette with full dark and light mode support.
135
+
136
+ Dark mode uses `#0E1117` background. Light mode uses `#FFFFFF`.
137
+ Toggle with the `d` key at any time.
138
+
139
+ ---
140
+
141
+ ## Development
142
+
143
+ ```bash
144
+ git clone https://github.com/letsgojh0810/hud-plugin.git
145
+ cd hud-plugin
146
+ npm install
147
+ npm run hud # launches TUI in dev mode
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Notes for Korean users
153
+
154
+ 이 플러그인은 Claude Code를 터미널에서 집중적으로 사용하는 개발자를 위해 만들어졌습니다.
155
+ 토큰 사용량, Git 상태, 프로젝트 구조를 별도 터미널 창에서 실시간으로 확인할 수 있습니다.
156
+
157
+ ---
158
+
159
+ ## License
160
+
161
+ MIT — [letsgojh0810](https://github.com/letsgojh0810)
package/bin/claude-hud ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claude-code-hud entry point
4
+ * Launches the Ink TUI for Claude Code token/git monitoring
5
+ */
6
+ import { spawn } from 'child_process';
7
+ import { existsSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __dir = dirname(fileURLToPath(import.meta.url));
12
+ const hudFile = join(__dir, '..', 'tui', 'hud.tsx');
13
+
14
+ // Use local tsx if available, otherwise try PATH
15
+ const localTsx = join(__dir, '..', 'node_modules', '.bin', 'tsx');
16
+ const tsxBin = existsSync(localTsx) ? localTsx : 'tsx';
17
+
18
+ const proc = spawn(tsxBin, [hudFile], {
19
+ stdio: 'inherit',
20
+ env: { ...process.env, CLAUDE_PROJECT_ROOT: process.env.CLAUDE_PROJECT_ROOT || process.cwd() },
21
+ });
22
+
23
+ proc.on('exit', (code) => process.exit(code ?? 0));
24
+ proc.on('error', (err) => {
25
+ if (err.code === 'ENOENT') {
26
+ console.error('tsx not found. Run: npm install -g tsx');
27
+ } else {
28
+ console.error('Failed to start HUD:', err.message);
29
+ }
30
+ process.exit(1);
31
+ });
package/bin/start.mjs ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claude-code-hud entry point
4
+ * Launches the Ink TUI for Claude Code token/git monitoring
5
+ */
6
+ import { spawn } from 'child_process';
7
+ import { existsSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __dir = dirname(fileURLToPath(import.meta.url));
12
+ const hudFile = join(__dir, '..', 'tui', 'hud.tsx');
13
+
14
+ // Use local tsx if available, otherwise try PATH
15
+ const localTsx = join(__dir, '..', 'node_modules', '.bin', 'tsx');
16
+ const tsxBin = existsSync(localTsx) ? localTsx : 'tsx';
17
+
18
+ const proc = spawn(tsxBin, [hudFile], {
19
+ stdio: 'inherit',
20
+ env: { ...process.env, CLAUDE_PROJECT_ROOT: process.env.CLAUDE_PROJECT_ROOT || process.cwd() },
21
+ });
22
+
23
+ proc.on('exit', (code) => process.exit(code ?? 0));
24
+ proc.on('error', (err) => {
25
+ if (err.code === 'ENOENT') {
26
+ console.error('tsx not found. Run: npm install -g tsx');
27
+ } else {
28
+ console.error('Failed to start HUD:', err.message);
29
+ }
30
+ process.exit(1);
31
+ });
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: hud
3
+ description: Show HUD status — token usage, git info, 5h/7d usage percentages
4
+ ---
5
+
6
+ Run the following and display output in a code block:
7
+
8
+ ```bash
9
+ node -e "
10
+ import('${CLAUDE_PLUGIN_ROOT}/scripts/lib/token-reader.mjs').then(async ({ readTokenUsage }) => {
11
+ const { readGitInfo } = await import('${CLAUDE_PLUGIN_ROOT}/scripts/lib/git-info.mjs');
12
+ const { getUsage } = await import('${CLAUDE_PLUGIN_ROOT}/scripts/lib/usage-api.mjs');
13
+
14
+ const usage = readTokenUsage();
15
+ const git = readGitInfo(process.cwd());
16
+ const limits = await getUsage();
17
+
18
+ const ctxPct = Math.round(usage.totalTokens / usage.contextWindow * 100);
19
+ const model = usage.model.replace('claude-', '').replace(/-202\d+(-\d+)?$/, '');
20
+
21
+ console.log('◆ HUD ─ ' + model);
22
+ console.log('');
23
+ console.log('Context ' + ctxPct + '% (' + Math.round(usage.totalTokens/1000) + 'K / ' + Math.round(usage.contextWindow/1000) + 'K)');
24
+ if (limits) {
25
+ console.log('5h usage ' + limits.fiveHourPercent.toFixed(1) + '%');
26
+ console.log('wk usage ' + limits.weeklyPercent.toFixed(1) + '%');
27
+ }
28
+ console.log('');
29
+ console.log('Branch ' + git.branch + (git.ahead ? ' +' + git.ahead : '') + (git.behind ? ' -' + git.behind : ''));
30
+ if (git.totalChanges > 0) {
31
+ const changes = [...git.modified.map(f => 'M ' + f), ...git.added.map(f => 'A ' + f), ...git.deleted.map(f => 'D ' + f)].slice(0, 5);
32
+ changes.forEach(c => console.log(' ' + c));
33
+ }
34
+ console.log('');
35
+ console.log('TUI → run in separate terminal: npx claude-code-hud');
36
+ });
37
+ "
38
+ ```
39
+
40
+ Display the output exactly as-is.
@@ -0,0 +1,29 @@
1
+ {
2
+ "description": "HUD hooks for Claude Code — token tracking, git status, session summary",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "matcher": "*",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\"",
11
+ "timeout": 8
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "Stop": [
17
+ {
18
+ "matcher": "*",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop-hud.mjs\"",
23
+ "timeout": 5
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "claude-code-hud",
3
+ "version": "0.2.0",
4
+ "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-hud": "./bin/claude-hud"
8
+ },
9
+ "keywords": [
10
+ "claude",
11
+ "claude-code",
12
+ "hud",
13
+ "token",
14
+ "git",
15
+ "dashboard",
16
+ "tui",
17
+ "terminal"
18
+ ],
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/letsgojh0810/hud-plugin.git"
23
+ },
24
+ "scripts": {
25
+ "hud": "tsx tui/hud.tsx",
26
+ "start": "node bin/start.mjs"
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "tui",
31
+ "scripts",
32
+ "commands",
33
+ "hooks",
34
+ "skills"
35
+ ],
36
+ "dependencies": {
37
+ "@types/react": "^19.2.14",
38
+ "chokidar": "^5.0.0",
39
+ "fast-glob": "^3.3.3",
40
+ "ink": "^6.8.0",
41
+ "react": "^19.2.4",
42
+ "tsx": "^4.21.0"
43
+ }
44
+ }
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HUD — Full dashboard
4
+ * Shows complete token breakdown, cost, git status, recent commits.
5
+ */
6
+ import { createRequire } from 'module';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+
10
+ const __dir = dirname(fileURLToPath(import.meta.url));
11
+ const { readTokenUsage } = await import(join(__dir, 'lib/token-reader.mjs'));
12
+ const { readGitInfo } = await import(join(__dir, 'lib/git-info.mjs'));
13
+ const { tokenPanel, gitPanel, divider } = await import(join(__dir, 'lib/formatter.mjs'));
14
+
15
+ const cwd = process.env.CLAUDE_PROJECT_ROOT || process.cwd();
16
+
17
+ const usage = readTokenUsage();
18
+ const git = readGitInfo(cwd);
19
+
20
+ const D = divider(54);
21
+
22
+ const lines = [
23
+ `◆ HUD — Full Dashboard`,
24
+ D,
25
+ '',
26
+ '[TOKENS]',
27
+ tokenPanel(usage),
28
+ '',
29
+ '[GIT]',
30
+ gitPanel(git),
31
+ '',
32
+ D,
33
+ ];
34
+
35
+ process.stdout.write(lines.join('\n') + '\n');
@@ -0,0 +1,131 @@
1
+ /**
2
+ * ASCII/Unicode formatting for HUD output.
3
+ * Inspired by the toss-blue design system.
4
+ */
5
+
6
+ const BLOCK_FULL = '█';
7
+ const BLOCK_EMPTY = '░';
8
+ const SPARK = ['▁','▂','▃','▄','▅','▆','▇','█'];
9
+
10
+ export function bar(value, max, width = 20) {
11
+ const pct = Math.min(value / max, 1);
12
+ const filled = Math.round(pct * width);
13
+ return BLOCK_FULL.repeat(filled) + BLOCK_EMPTY.repeat(width - filled);
14
+ }
15
+
16
+ export function pct(value, max) {
17
+ return Math.round((value / max) * 100);
18
+ }
19
+
20
+ export function fmtK(n) {
21
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
22
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}K`;
23
+ return String(n);
24
+ }
25
+
26
+ export function fmtCost(n) {
27
+ if (n === 0) return '$0.0000';
28
+ if (n < 0.001) return `$${n.toFixed(5)}`;
29
+ if (n < 0.01) return `$${n.toFixed(4)}`;
30
+ return `$${n.toFixed(3)}`;
31
+ }
32
+
33
+ export function statusLabel(used, max) {
34
+ const p = used / max;
35
+ if (p >= 0.9) return 'CRITICAL';
36
+ if (p >= 0.75) return 'WARN';
37
+ if (p >= 0.5) return 'MID';
38
+ return 'OK';
39
+ }
40
+
41
+ /** Compact 1-line token bar */
42
+ export function tokenLine(usage) {
43
+ const { totalTokens, contextWindow, model } = usage;
44
+ const b = bar(totalTokens, contextWindow, 20);
45
+ const p = pct(totalTokens, contextWindow);
46
+ const st = statusLabel(totalTokens, contextWindow);
47
+ const cost = fmtCost(usage.cost.total);
48
+ const shortModel = model.replace('claude-', '').replace(/-\d{8}$/, '');
49
+ return `ctx ${b} ${fmtK(totalTokens)}/${fmtK(contextWindow)} ${p}% ${st} ${cost} [${shortModel}]`;
50
+ }
51
+
52
+ /** Full token panel (multi-line) */
53
+ export function tokenPanel(usage) {
54
+ const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens, contextWindow, model, cost } = usage;
55
+ const W = 24;
56
+
57
+ const lines = [];
58
+ lines.push(`CONTEXT WINDOW`);
59
+ lines.push(` ${bar(totalTokens, contextWindow, W)} ${fmtK(totalTokens)} / ${fmtK(contextWindow)} ${statusLabel(totalTokens, contextWindow)}`);
60
+ lines.push('');
61
+ lines.push('BREAKDOWN');
62
+
63
+ const rows = [
64
+ { label: 'input ', val: inputTokens, cost: cost.input },
65
+ { label: 'output ', val: outputTokens, cost: cost.output },
66
+ { label: 'cache·r ', val: cacheReadTokens, cost: cost.cacheRead },
67
+ { label: 'cache·w ', val: cacheWriteTokens, cost: cost.cacheWrite },
68
+ ];
69
+
70
+ for (const r of rows) {
71
+ const b = bar(r.val, totalTokens || 1, 16);
72
+ lines.push(` ${r.label} ${b} ${fmtK(r.val).padStart(7)} ${fmtCost(r.cost)}`);
73
+ }
74
+
75
+ lines.push('');
76
+ lines.push(`COST TOTAL ${fmtCost(cost.total)}`);
77
+ lines.push(`MODEL ${model}`);
78
+ return lines.join('\n');
79
+ }
80
+
81
+ /** Git summary line */
82
+ export function gitLine(git) {
83
+ if (!git.isRepo) return 'git (not a git repository)';
84
+ const branch = `⎇ ${git.branch}`;
85
+ const changes = git.totalChanges > 0 ? ` ${git.totalChanges} changed` : ' clean';
86
+ const sync = git.ahead > 0 ? ` ↑${git.ahead}` : git.behind > 0 ? ` ↓${git.behind}` : '';
87
+ return `git ${branch}${sync}${changes}`;
88
+ }
89
+
90
+ /** Full git panel */
91
+ export function gitPanel(git) {
92
+ if (!git.isRepo) return 'Not a git repository.';
93
+ const lines = [];
94
+
95
+ lines.push(`BRANCH ⎇ ${git.branch}`);
96
+ if (git.ahead > 0 || git.behind > 0) {
97
+ lines.push(` ↑ ${git.ahead} ahead ↓ ${git.behind} behind`);
98
+ }
99
+ lines.push('');
100
+
101
+ const allChanges = [
102
+ ...git.modified.map(f => ({ st: 'MOD', f })),
103
+ ...git.added.map(f => ({ st: 'ADD', f })),
104
+ ...git.deleted.map(f => ({ st: 'DEL', f })),
105
+ ];
106
+
107
+ if (allChanges.length > 0) {
108
+ lines.push(`CHANGED FILES (${allChanges.length})`);
109
+ for (const { st, f } of allChanges.slice(0, 10)) {
110
+ lines.push(` ${st} ${f}`);
111
+ }
112
+ if (allChanges.length > 10) lines.push(` ... and ${allChanges.length - 10} more`);
113
+ } else {
114
+ lines.push(' working tree clean');
115
+ }
116
+
117
+ if (git.recentCommits.length > 0) {
118
+ lines.push('');
119
+ lines.push('RECENT COMMITS');
120
+ for (const c of git.recentCommits) {
121
+ lines.push(` ${c.hash} ${c.msg} (${c.time})`);
122
+ }
123
+ }
124
+
125
+ return lines.join('\n');
126
+ }
127
+
128
+ /** Divider line */
129
+ export function divider(width = 52) {
130
+ return '─'.repeat(width);
131
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Git status via child_process. No external deps.
3
+ */
4
+ import { execSync } from 'child_process';
5
+
6
+ function run(cmd, cwd) {
7
+ try {
8
+ return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
9
+ } catch {
10
+ return '';
11
+ }
12
+ }
13
+
14
+ export function readGitInfo(cwd = process.cwd()) {
15
+ const branch = run('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown';
16
+ if (branch === 'unknown' || branch === 'HEAD') {
17
+ return { isRepo: false, branch: 'unknown', ahead: 0, behind: 0, modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 };
18
+ }
19
+
20
+ // ahead/behind
21
+ const aheadBehind = run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo "0\t0"', cwd);
22
+ const [behind = 0, ahead = 0] = aheadBehind.split('\t').map(Number);
23
+
24
+ // status
25
+ const statusOut = run('git status --porcelain', cwd);
26
+ const modified = [], added = [], deleted = [];
27
+ for (const line of statusOut.split('\n').filter(Boolean)) {
28
+ const st = line.slice(0, 2).trim();
29
+ const file = line.slice(2).trimStart();
30
+ if (st === 'M' || st === 'MM' || st === 'AM') modified.push(file);
31
+ else if (st === 'A' || st === '??' ) added.push(file);
32
+ else if (st === 'D') deleted.push(file);
33
+ }
34
+
35
+ // recent commits
36
+ const logOut = run('git log --oneline -5 --format="%h|%s|%cr"', cwd);
37
+ const recentCommits = logOut.split('\n').filter(Boolean).map(l => {
38
+ const [hash, ...rest] = l.split('|');
39
+ const time = rest.pop();
40
+ const msg = rest.join('|');
41
+ return { hash, msg, time };
42
+ });
43
+
44
+ // diff stats: actual +/- line counts per file
45
+ const numstatOut = run('git diff --numstat HEAD 2>/dev/null', cwd);
46
+ const diffStats = {};
47
+ for (const line of numstatOut.split('\n').filter(Boolean)) {
48
+ const [addStr, delStr, ...fileParts] = line.split('\t');
49
+ const file = fileParts.join('\t');
50
+ const add = parseInt(addStr) || 0;
51
+ const del = parseInt(delStr) || 0;
52
+ if (file) diffStats[file] = { add, del };
53
+ }
54
+
55
+ return {
56
+ isRepo: true,
57
+ branch,
58
+ ahead: Number(ahead),
59
+ behind: Number(behind),
60
+ modified,
61
+ added,
62
+ deleted,
63
+ recentCommits,
64
+ diffStats,
65
+ totalChanges: modified.length + added.length + deleted.length,
66
+ };
67
+ }