@say8425/cc-statusline 1.3.0 → 2.0.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 +99 -11
- package/package.json +31 -27
- package/src/index.ts +33 -201
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# cc-statusline
|
|
2
2
|
|
|
3
|
+
English | [한국어](docs/README.ko.md) | [日本語](docs/README.ja.md) | [中文](docs/README.zh.md) | [Español](docs/README.es.md)
|
|
4
|
+
|
|
3
5
|
Custom statusline for Claude Code.
|
|
4
6
|
|
|
5
7
|
[](https://code.claude.com/docs/en/statusline)
|
|
@@ -7,29 +9,95 @@ Custom statusline for Claude Code.
|
|
|
7
9
|
[](https://www.typescriptlang.org)
|
|
8
10
|
[](https://bun.sh)
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Add the following to `~/.claude/settings.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"statusLine": {
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bunx @say8425/cc-statusline",
|
|
21
|
+
"padding": 0
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Screenshots
|
|
27
|
+
|
|
28
|
+
### Git diff only
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
32
|
+
### PR only
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
### Git diff + PR
|
|
11
37
|
|
|
12
|
-

|
|
13
39
|
|
|
14
|
-
|
|
40
|
+
### Usage Metrics
|
|
41
|
+
|
|
42
|
+

|
|
15
43
|
|
|
16
44
|
## Features
|
|
17
45
|
|
|
18
46
|
- **Session Time**: Current session elapsed time
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
47
|
+
- **Cost**: Session cost in USD
|
|
48
|
+
- **Context**: Token usage with percentage (color-coded)
|
|
49
|
+
- **Git Diff**: File count, insertions, deletions
|
|
21
50
|
- **PR URL**: Clickable OSC 8 hyperlink
|
|
22
51
|
- **TrueColor**: Dynamic colors based on thresholds
|
|
52
|
+
- **Limit Reset Time**: Reset time display (HH:MM)
|
|
53
|
+
- **Block Usage**: 5-hour utilization percentage (from server API)
|
|
54
|
+
- **Weekly Reset Timer**: Weekly limit reset time (MM/DD HH:MM)
|
|
55
|
+
- **Weekly Usage**: 7-day utilization percentage (from server API)
|
|
23
56
|
|
|
24
|
-
##
|
|
57
|
+
## Emoji Guide
|
|
25
58
|
|
|
26
|
-
|
|
59
|
+
| Emoji | Description |
|
|
60
|
+
| ----- | ------------------------ |
|
|
61
|
+
| 📁 | Project folder name |
|
|
62
|
+
| 🌿 | Current Git branch |
|
|
63
|
+
| ⏱️ | Session elapsed time |
|
|
64
|
+
| 💰 | Session cost in USD |
|
|
65
|
+
| 🧠 | Context window usage |
|
|
66
|
+
| ⏳ | Limit reset time |
|
|
67
|
+
| 📊 | 5-hour utilization % |
|
|
68
|
+
| ⏰ | Weekly limit reset time |
|
|
69
|
+
| 📅 | 7-day utilization % |
|
|
70
|
+
| ✏️ | Uncommitted changes |
|
|
71
|
+
| 📎 | Pull request link |
|
|
72
|
+
|
|
73
|
+
## Usage Metrics
|
|
74
|
+
|
|
75
|
+
Shows usage information from the Anthropic Usage API.
|
|
76
|
+
|
|
77
|
+
> [!WARNING]
|
|
78
|
+
> The `--show-usage` feature uses an unofficial, reverse-engineered Anthropic API endpoint to retrieve usage data. This is not an officially supported API, and may break or change at any time without notice. **Use at your own risk.** The author assumes no responsibility for any consequences, including but not limited to account restrictions or service disruptions, that may arise from the use of this feature.
|
|
79
|
+
|
|
80
|
+
> [!NOTE]
|
|
81
|
+
> This feature is **macOS only** as it reads the OAuth token from macOS Keychain (`Claude Code-credentials`).
|
|
82
|
+
|
|
83
|
+
### How It Works
|
|
84
|
+
|
|
85
|
+
Calls the Anthropic Usage API (`/api/oauth/usage`) using the OAuth access token from macOS Keychain to retrieve:
|
|
86
|
+
|
|
87
|
+
1. **5-hour utilization** - Server-calculated usage percentage for the current billing block
|
|
88
|
+
2. **7-day utilization** - Server-calculated weekly usage percentage
|
|
89
|
+
3. **Reset timer** - Exact reset time from the server (`five_hour.resets_at`)
|
|
90
|
+
4. **Weekly reset timer** - Weekly limit reset time (`seven_day.resets_at`), shown as `MM/DD HH:MM` (e.g., `02/15 17:00`)
|
|
91
|
+
|
|
92
|
+
### Enable
|
|
93
|
+
|
|
94
|
+
Usage metrics are **hidden by default**. To enable, use the `--show-usage` flag:
|
|
27
95
|
|
|
28
96
|
```json
|
|
29
97
|
{
|
|
30
98
|
"statusLine": {
|
|
31
99
|
"type": "command",
|
|
32
|
-
"command": "bunx @say8425/cc-statusline",
|
|
100
|
+
"command": "bunx @say8425/cc-statusline --show-usage",
|
|
33
101
|
"padding": 0
|
|
34
102
|
}
|
|
35
103
|
}
|
|
@@ -40,11 +108,31 @@ Add the following to `~/.claude/settings.json`:
|
|
|
40
108
|
- [Bun](https://bun.sh) - JavaScript runtime
|
|
41
109
|
- [gh](https://cli.github.com) - GitHub CLI (optional, for PR URL)
|
|
42
110
|
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Install dependencies
|
|
115
|
+
bun install
|
|
116
|
+
|
|
117
|
+
# Run tests
|
|
118
|
+
bun test
|
|
119
|
+
|
|
120
|
+
# Run tests with coverage
|
|
121
|
+
bun test --coverage
|
|
122
|
+
|
|
123
|
+
# Type check
|
|
124
|
+
bun run typecheck
|
|
125
|
+
|
|
126
|
+
# Lint
|
|
127
|
+
bun run lint
|
|
128
|
+
```
|
|
129
|
+
|
|
43
130
|
## Color Thresholds
|
|
44
131
|
|
|
45
|
-
| Metric
|
|
46
|
-
|
|
|
47
|
-
| Context %
|
|
132
|
+
| Metric | Normal (white) | Warning (yellow) | Critical (red) |
|
|
133
|
+
| ------------- | -------------- | ---------------- | -------------- |
|
|
134
|
+
| Context % | < 50% | 50-80% | > 80% |
|
|
135
|
+
| Block Usage % | < 50% | 50-80% | > 80% |
|
|
48
136
|
|
|
49
137
|
## License
|
|
50
138
|
|
package/package.json
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
2
|
+
"name": "@say8425/cc-statusline",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "https://github.com/say8425/cc-statusline.git"
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"@biomejs/biome": "^2.3.9",
|
|
10
|
+
"@types/bun": "latest",
|
|
11
|
+
"typescript": "^5.9.3"
|
|
12
|
+
},
|
|
13
|
+
"bin": {
|
|
14
|
+
"cc-statusline": "src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"description": "Custom statusline for Claude Code",
|
|
17
|
+
"files": [
|
|
18
|
+
"src/index.ts",
|
|
19
|
+
"src/lib.ts"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun build.ts",
|
|
26
|
+
"start": "bun src/index.ts",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:coverage": "bun test --coverage",
|
|
29
|
+
"lint": "biome check src/",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"type": "module"
|
|
29
33
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,216 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
current_usage: {
|
|
14
|
-
input_tokens: number;
|
|
15
|
-
output_tokens: number;
|
|
16
|
-
cache_creation_input_tokens: number;
|
|
17
|
-
cache_read_input_tokens: number;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
workspace: {
|
|
21
|
-
current_dir: string;
|
|
22
|
-
project_dir: string;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 캐시 구조
|
|
27
|
-
const cache = {
|
|
28
|
-
branch: { value: "", timestamp: 0 },
|
|
29
|
-
gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
|
|
30
|
-
prUrl: { value: null as string | null, timestamp: 0 },
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// 캐시 TTL (ms)
|
|
34
|
-
const CACHE_TTL = {
|
|
35
|
-
branch: 5000, // 5초
|
|
36
|
-
gitChanges: 3000, // 3초
|
|
37
|
-
prUrl: 30000, // 30초
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// TrueColor 색상 정의
|
|
41
|
-
const C = {
|
|
42
|
-
RESET: "\x1b[0m",
|
|
43
|
-
CYAN: "\x1b[38;2;0;255;255m",
|
|
44
|
-
MAGENTA: "\x1b[38;2;255;100;200m",
|
|
45
|
-
GREEN: "\x1b[38;2;100;255;100m",
|
|
46
|
-
YELLOW: "\x1b[38;2;255;220;100m",
|
|
47
|
-
RED: "\x1b[38;2;255;100;100m",
|
|
48
|
-
BLUE: "\x1b[38;2;100;150;255m",
|
|
49
|
-
WHITE: "\x1b[38;2;200;200;200m",
|
|
50
|
-
UNDERLINE: "\x1b[4m",
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Context 사용률에 따른 색상
|
|
54
|
-
function getContextColor(pct: number): string {
|
|
55
|
-
if (pct < 50) return C.WHITE;
|
|
56
|
-
if (pct < 80) return C.YELLOW;
|
|
57
|
-
return C.RED;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 숫자 포맷팅 (천 단위 콤마)
|
|
61
|
-
function formatNumber(n: number): string {
|
|
62
|
-
return n.toLocaleString("en-US");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 시간 포맷팅 (HH:MM)
|
|
66
|
-
function formatTime(hours: number, mins: number): string {
|
|
67
|
-
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// stdin에서 JSON 읽기
|
|
71
|
-
async function readStdin(): Promise<string> {
|
|
72
|
-
const chunks: Uint8Array[] = [];
|
|
73
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
74
|
-
chunks.push(chunk);
|
|
75
|
-
}
|
|
76
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Git 브랜치 가져오기 (캐싱)
|
|
80
|
-
async function getBranchCached(): Promise<string> {
|
|
81
|
-
if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
|
|
82
|
-
return cache.branch.value;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
const result = await $`git branch --show-current 2>/dev/null`.text();
|
|
86
|
-
cache.branch = { value: result.trim(), timestamp: Date.now() };
|
|
87
|
-
return cache.branch.value;
|
|
88
|
-
} catch {
|
|
89
|
-
return cache.branch.value;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Git 변경사항 가져오기 (캐싱)
|
|
94
|
-
async function getGitChangesCached(): Promise<{
|
|
95
|
-
files: number;
|
|
96
|
-
insertions: number;
|
|
97
|
-
deletions: number;
|
|
98
|
-
}> {
|
|
99
|
-
if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
|
|
100
|
-
return cache.gitChanges;
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
const [diff, staged] = await Promise.all([
|
|
104
|
-
$`git diff --shortstat 2>/dev/null`.text(),
|
|
105
|
-
$`git diff --cached --shortstat 2>/dev/null`.text(),
|
|
106
|
-
]);
|
|
107
|
-
const combined = `${diff}\n${staged}`;
|
|
108
|
-
|
|
109
|
-
// 파일 수, insertions, deletions 추출
|
|
110
|
-
const [files, insertions, deletions] = [
|
|
111
|
-
/(\d+) file/g,
|
|
112
|
-
/(\d+) insertion/g,
|
|
113
|
-
/(\d+) deletion/g,
|
|
114
|
-
].map((regex) =>
|
|
115
|
-
(combined.match(regex) || []).reduce(
|
|
116
|
-
(sum, m) => sum + Number.parseInt(m, 10),
|
|
117
|
-
0,
|
|
118
|
-
),
|
|
119
|
-
);
|
|
120
|
-
cache.gitChanges = { files, insertions, deletions, timestamp: Date.now() };
|
|
121
|
-
return cache.gitChanges;
|
|
122
|
-
} catch {
|
|
123
|
-
return cache.gitChanges;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// PR URL 가져오기 (캐싱)
|
|
128
|
-
async function getPrUrlCached(): Promise<string | null> {
|
|
129
|
-
if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
|
|
130
|
-
return cache.prUrl.value;
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
const result = await $`gh pr view --json url -q .url 2>/dev/null`.text();
|
|
134
|
-
cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
|
|
135
|
-
return cache.prUrl.value;
|
|
136
|
-
} catch {
|
|
137
|
-
return cache.prUrl.value;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
3
|
+
import { parseCliArgs } from "./cli.ts";
|
|
4
|
+
import {
|
|
5
|
+
getBranchCached,
|
|
6
|
+
getGitChangesCached,
|
|
7
|
+
getPrUrlCached,
|
|
8
|
+
} from "./git/index.ts";
|
|
9
|
+
import { renderStatusLine } from "./render.ts";
|
|
10
|
+
import { readStdin } from "./stdin.ts";
|
|
11
|
+
import type { ClaudeStatusInput } from "./types.ts";
|
|
12
|
+
import { getUsageCached } from "./usage/index.ts";
|
|
140
13
|
|
|
141
14
|
// 메인 함수
|
|
142
|
-
async function main() {
|
|
143
|
-
//
|
|
144
|
-
const
|
|
15
|
+
export async function main(cliArgs?: string[]): Promise<void> {
|
|
16
|
+
// CLI 인자 파싱
|
|
17
|
+
const args = cliArgs ?? process.argv.slice(2);
|
|
18
|
+
const { showUsage } = parseCliArgs(args);
|
|
145
19
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
20
|
+
// 1. stdin에서 Claude Code JSON 읽기 (empty stdin 처리)
|
|
21
|
+
const claudeJson: ClaudeStatusInput = JSON.parse((await readStdin()) || "{}");
|
|
148
22
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
const sessionSec = Math.floor(sessionMs / 1000);
|
|
152
|
-
const sessionHrs = Math.floor(sessionSec / 3600);
|
|
153
|
-
const sessionMins = Math.floor((sessionSec % 3600) / 60);
|
|
154
|
-
|
|
155
|
-
// 4. 비용
|
|
156
|
-
const costUsd = claudeJson.cost?.total_cost_usd || 0;
|
|
157
|
-
|
|
158
|
-
// 5. Context 토큰 계산
|
|
159
|
-
const usage = claudeJson.context_window?.current_usage;
|
|
160
|
-
const contextSize = claudeJson.context_window?.context_window_size || 200000;
|
|
161
|
-
|
|
162
|
-
const totalTokens = usage
|
|
163
|
-
? usage.input_tokens +
|
|
164
|
-
usage.output_tokens +
|
|
165
|
-
usage.cache_creation_input_tokens +
|
|
166
|
-
usage.cache_read_input_tokens
|
|
167
|
-
: 0;
|
|
168
|
-
|
|
169
|
-
const contextPct = Math.round((totalTokens / contextSize) * 100);
|
|
170
|
-
const ctxColor = getContextColor(contextPct);
|
|
171
|
-
|
|
172
|
-
// 6. Git 정보 (캐싱, 병렬 실행)
|
|
173
|
-
const [branch, gitChanges, prUrl] = await Promise.all([
|
|
23
|
+
// 2. Git 정보 + 사용량 (캐싱, 병렬 실행)
|
|
24
|
+
const [branch, gitChanges, prUrl, blockUsage] = await Promise.all([
|
|
174
25
|
getBranchCached(),
|
|
175
26
|
getGitChangesCached(),
|
|
176
27
|
getPrUrlCached(),
|
|
28
|
+
showUsage ? getUsageCached() : Promise.resolve(null),
|
|
177
29
|
]);
|
|
178
30
|
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log(
|
|
189
|
-
`${C.WHITE}⏱️ ${formatTime(sessionHrs, sessionMins)}${C.RESET} | ` +
|
|
190
|
-
`${C.WHITE}💰 $${costUsd.toFixed(2)}${C.RESET} | ` +
|
|
191
|
-
`${ctxColor}🧠 ${formatNumber(totalTokens)} (${contextPct}%)${C.RESET}`,
|
|
192
|
-
);
|
|
31
|
+
// 3. 렌더링 및 출력
|
|
32
|
+
const lines = renderStatusLine({
|
|
33
|
+
claudeJson,
|
|
34
|
+
branch,
|
|
35
|
+
gitChanges,
|
|
36
|
+
prUrl,
|
|
37
|
+
blockUsage,
|
|
38
|
+
showUsage,
|
|
39
|
+
});
|
|
193
40
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
gitChanges.files > 0 ||
|
|
197
|
-
gitChanges.insertions > 0 ||
|
|
198
|
-
gitChanges.deletions > 0;
|
|
199
|
-
if (hasGitChanges || prUrl) {
|
|
200
|
-
let line3 = "";
|
|
201
|
-
if (hasGitChanges) {
|
|
202
|
-
line3 += `✏️ ${C.WHITE}${gitChanges.files} files${C.RESET} ${C.GREEN}+${gitChanges.insertions}${C.RESET} ${C.RED}-${gitChanges.deletions}${C.RESET}`;
|
|
203
|
-
}
|
|
204
|
-
if (prUrl) {
|
|
205
|
-
const prLabel = prUrl
|
|
206
|
-
.replace("https://github.com/", "")
|
|
207
|
-
.replace("/pull/", "#");
|
|
208
|
-
if (line3) line3 += " | ";
|
|
209
|
-
// OSC 8 하이퍼링크
|
|
210
|
-
line3 += `📎 ${C.WHITE}${C.UNDERLINE}\x1b]8;;${prUrl}\x07${prLabel}\x1b]8;;\x07${C.RESET}`;
|
|
211
|
-
}
|
|
212
|
-
console.log(line3);
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
console.log(line);
|
|
213
43
|
}
|
|
214
44
|
}
|
|
215
45
|
|
|
216
|
-
|
|
46
|
+
if (import.meta.main) {
|
|
47
|
+
main().catch(console.error);
|
|
48
|
+
}
|