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 +161 -0
- package/bin/claude-hud +31 -0
- package/bin/start.mjs +31 -0
- package/commands/hud.md +40 -0
- package/hooks/hooks.json +29 -0
- package/package.json +44 -0
- package/scripts/full-hud.mjs +35 -0
- package/scripts/lib/formatter.mjs +131 -0
- package/scripts/lib/git-info.mjs +67 -0
- package/scripts/lib/token-reader.mjs +212 -0
- package/scripts/lib/usage-api.mjs +141 -0
- package/scripts/session-start.mjs +42 -0
- package/scripts/statusline.mjs +46 -0
- package/scripts/stop-hud.mjs +31 -0
- package/skills/hud.md +45 -0
- package/tui/hud.tsx +645 -0
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
|
+
});
|
package/commands/hud.md
ADDED
|
@@ -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.
|
package/hooks/hooks.json
ADDED
|
@@ -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
|
+
}
|