@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.
Files changed (3) hide show
  1. package/README.md +99 -11
  2. package/package.json +31 -27
  3. 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
  [![Claude Code](https://img.shields.io/badge/Claude_Code-D97757?style=flat&logo=claude&logoColor=white)](https://code.claude.com/docs/en/statusline)
@@ -7,29 +9,95 @@ Custom statusline for Claude Code.
7
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
8
10
  [![Bun](https://img.shields.io/badge/Bun-black?style=flat&logo=bun)](https://bun.sh)
9
11
 
10
- ## Preview
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
+ ![scenario1_diff_only](docs/scenario1_diff_only.png)
31
+
32
+ ### PR only
33
+
34
+ ![scenario2_pr_only](docs/scenario2_pr_only.png)
35
+
36
+ ### Git diff + PR
11
37
 
12
- ![preview-1](docs/preview-1.png)
38
+ ![scenario3_diff_pr](docs/scenario3_diff_pr.png)
13
39
 
14
- ![preview-2](docs/preview-2.png)
40
+ ### Usage Metrics
41
+
42
+ ![Screenshot of status line with usage metrics](docs/usage_metrics.png)
15
43
 
16
44
  ## Features
17
45
 
18
46
  - **Session Time**: Current session elapsed time
19
- - **Context %**: Current context window usage (updates immediately)
20
- - **Session Tokens**: Cumulative token usage
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
- ## Installation
57
+ ## Emoji Guide
25
58
 
26
- Add the following to `~/.claude/settings.json`:
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 | Normal (white) | Warning (yellow) | Critical (red) |
46
- | --------- | -------------- | ---------------- | -------------- |
47
- | Context % | < 50% | 50-80% | > 80% |
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
- "name": "@say8425/cc-statusline",
3
- "version": "1.3.0",
4
- "type": "module",
5
- "description": "Custom statusline for Claude Code",
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/say8425/cc-statusline.git"
9
- },
10
- "bin": {
11
- "cc-statusline": "src/index.ts"
12
- },
13
- "files": [
14
- "src/index.ts"
15
- ],
16
- "publishConfig": {
17
- "registry": "https://registry.npmjs.org"
18
- },
19
- "scripts": {
20
- "start": "bun src/index.ts",
21
- "lint": "biome check src/",
22
- "typecheck": "tsc --noEmit"
23
- },
24
- "devDependencies": {
25
- "@biomejs/biome": "^2.3.9",
26
- "@types/bun": "latest",
27
- "typescript": "^5.9.3"
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 { $ } from "bun";
4
-
5
- // 공식 Claude Code JSON input 타입 정의
6
- interface ClaudeStatusInput {
7
- cost: {
8
- total_duration_ms: number;
9
- total_cost_usd: number;
10
- };
11
- context_window: {
12
- context_window_size: number;
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
- // 1. stdin에서 Claude Code JSON 읽기
144
- const claudeJson: ClaudeStatusInput = JSON.parse(await readStdin());
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
- // 2. 폴더 이름 추출 (프로젝트 루트 디렉토리)
147
- const folder = claudeJson.workspace?.project_dir?.split("/").pop() || "";
20
+ // 1. stdin에서 Claude Code JSON 읽기 (empty stdin 처리)
21
+ const claudeJson: ClaudeStatusInput = JSON.parse((await readStdin()) || "{}");
148
22
 
149
- // 3. 세션 시간 계산
150
- const sessionMs = claudeJson.cost?.total_duration_ms || 0;
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
- // 7. 출력
180
- // 1번째 줄: 폴더 | 브랜치
181
- let line1 = `${C.WHITE}📁 ${folder}${C.RESET}`;
182
- if (branch) {
183
- line1 += ` | ${C.WHITE}🌿 ${branch}${C.RESET}`;
184
- }
185
- console.log(line1);
186
-
187
- // 2번째 줄: 세션 시간 | 비용 | 컨텍스트
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
- // 3번째 줄: git changes | PR URL
195
- const hasGitChanges =
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
- main().catch(console.error);
46
+ if (import.meta.main) {
47
+ main().catch(console.error);
48
+ }