@say8425/cc-statusline 1.3.0 → 1.4.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 +113 -11
  2. package/package.json +10 -7
  3. package/src/index.ts +185 -16
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,128 @@ 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
+ ## CLI Options
27
+
28
+ | Option | Description | Default |
29
+ |--------|-------------|:-------:|
30
+ | [`--plan <plan>`](#plan-selection) | Set token limit for your subscription (pro, max5x, max20x) | `pro` |
31
+ | [`--no-usage`](#disable) | Hide usage metrics line | - |
32
+
33
+ ## Screenshots
34
+
35
+ ### Git diff only
36
+
37
+ ![scenario1_diff_only](docs/scenario1_diff_only.png)
38
+
39
+ ### PR only
40
+
41
+ ![scenario2_pr_only](docs/scenario2_pr_only.png)
42
+
43
+ ### Git diff + PR
44
+
45
+ ![scenario3_diff_pr](docs/scenario3_diff_pr.png)
46
+
47
+ ### Context Normal (< 50%)
48
+
49
+ ![Screenshot of status line with normal context usage, under 50%](docs/context_normal.png)
50
+
51
+ ### Context Warning (50-80%)
11
52
 
12
- ![preview-1](docs/preview-1.png)
53
+ ![Screenshot of status line with warning context usage, between 50% and 80%](docs/context_warning.png)
13
54
 
14
- ![preview-2](docs/preview-2.png)
55
+ ### Context Critical (> 80%)
56
+
57
+ ![Screenshot of status line with critical context usage, over 80%](docs/context_critical.png)
58
+
59
+ ### Limit Reset Timer
60
+
61
+ ![Screenshot of status line with limit reset timer](docs/limit_reset.png)
15
62
 
16
63
  ## Features
17
64
 
18
65
  - **Session Time**: Current session elapsed time
19
- - **Context %**: Current context window usage (updates immediately)
20
- - **Session Tokens**: Cumulative token usage
66
+ - **Cost**: Session cost in USD
67
+ - **Context**: Token usage with percentage (color-coded)
68
+ - **Git Diff**: File count, insertions, deletions
21
69
  - **PR URL**: Clickable OSC 8 hyperlink
22
70
  - **TrueColor**: Dynamic colors based on thresholds
71
+ - **Limit Reset Timer**: Countdown to usage limit reset
72
+ - **Block Usage**: 5-hour block token usage with percentage
73
+ - **Burn Rate**: Token consumption rate per minute
23
74
 
24
- ## Installation
75
+ ## Emoji Guide
25
76
 
26
- Add the following to `~/.claude/settings.json`:
77
+ | Emoji | Description |
78
+ | ----- | ------------------------ |
79
+ | 📁 | Project folder name |
80
+ | 🌿 | Current Git branch |
81
+ | ⏱️ | Session elapsed time |
82
+ | 💰 | Session cost in USD |
83
+ | 🧠 | Context window usage |
84
+ | ⏳ | Limit reset countdown |
85
+ | 📊 | 5-hour block token usage |
86
+ | 🔥 | Token burn rate (per min)|
87
+ | ✏️ | Uncommitted changes |
88
+ | 📎 | Pull request link |
89
+
90
+ ## Usage Metrics
91
+
92
+ Shows usage information for the 5-hour billing block.
93
+
94
+ ### How It Works
95
+
96
+ Automatically parses JSONL files from `~/.claude/projects/` to detect:
97
+
98
+ 1. **Usage limit error messages** - Extracts exact reset time from "Claude AI usage limit reached" errors
99
+ 2. **5-hour billing blocks** - Calculates block end time based on latest activity (like [ccusage](https://github.com/ryoppippi/ccusage))
100
+ 3. **Token usage** - Sums input and output tokens within the current 5-hour block
101
+ 4. **Burn rate** - Calculates average token consumption per minute
102
+
103
+ No manual configuration required.
104
+
105
+ ### Plan Selection
106
+
107
+ Different Claude Code plans have different token limits. Use the `--plan` flag to set your plan:
27
108
 
28
109
  ```json
29
110
  {
30
111
  "statusLine": {
31
112
  "type": "command",
32
- "command": "bunx @say8425/cc-statusline",
113
+ "command": "bunx @say8425/cc-statusline --plan max5x",
114
+ "padding": 0
115
+ }
116
+ }
117
+ ```
118
+
119
+ | Plan | Token Limit | Command |
120
+ |------|-------------|---------|
121
+ | Pro (default) | 450K | `--plan pro` or omit |
122
+ | Max 5x | 2.25M | `--plan max5x` |
123
+ | Max 20x | 9M | `--plan max20x` |
124
+
125
+ ### Disable
126
+
127
+ To hide the usage metrics line (reset timer, block usage, burn rate), use the `--no-usage` flag:
128
+
129
+ ```json
130
+ {
131
+ "statusLine": {
132
+ "type": "command",
133
+ "command": "bunx @say8425/cc-statusline --no-usage",
33
134
  "padding": 0
34
135
  }
35
136
  }
@@ -42,9 +143,10 @@ Add the following to `~/.claude/settings.json`:
42
143
 
43
144
  ## Color Thresholds
44
145
 
45
- | Metric | Normal (white) | Warning (yellow) | Critical (red) |
46
- | --------- | -------------- | ---------------- | -------------- |
47
- | Context % | < 50% | 50-80% | > 80% |
146
+ | Metric | Normal (white) | Warning (yellow) | Critical (red) |
147
+ | ------------- | -------------- | ---------------- | -------------- |
148
+ | Context % | < 50% | 50-80% | > 80% |
149
+ | Block Usage % | < 50% | 50-80% | > 80% |
48
150
 
49
151
  ## License
50
152
 
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@say8425/cc-statusline",
3
- "version": "1.3.0",
4
- "type": "module",
5
- "description": "Custom statusline for Claude Code",
3
+ "version": "1.4.0",
6
4
  "repository": {
7
5
  "type": "git",
8
6
  "url": "https://github.com/say8425/cc-statusline.git"
9
7
  },
8
+ "devDependencies": {
9
+ "@biomejs/biome": "^2.3.9",
10
+ "@types/bun": "latest",
11
+ "typescript": "^5.9.3"
12
+ },
10
13
  "bin": {
11
14
  "cc-statusline": "src/index.ts"
12
15
  },
16
+ "description": "Custom statusline for Claude Code",
13
17
  "files": [
14
18
  "src/index.ts"
15
19
  ],
@@ -21,9 +25,8 @@
21
25
  "lint": "biome check src/",
22
26
  "typecheck": "tsc --noEmit"
23
27
  },
24
- "devDependencies": {
25
- "@biomejs/biome": "^2.3.9",
26
- "@types/bun": "latest",
27
- "typescript": "^5.9.3"
28
+ "type": "module",
29
+ "dependencies": {
30
+ "ccusage": "^18.0.5"
28
31
  }
29
32
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { $ } from "bun";
4
+ import { loadSessionBlockData } from "ccusage/data-loader";
4
5
 
5
6
  // 공식 Claude Code JSON input 타입 정의
6
7
  interface ClaudeStatusInput {
@@ -23,11 +24,22 @@ interface ClaudeStatusInput {
23
24
  };
24
25
  }
25
26
 
27
+ // 블록 사용량 정보 타입
28
+ interface BlockUsageInfo {
29
+ resetTime: Date | null;
30
+ blockTokens: number;
31
+ blockStartTime: number | null;
32
+ }
33
+
26
34
  // 캐시 구조
27
35
  const cache = {
28
36
  branch: { value: "", timestamp: 0 },
29
37
  gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
30
38
  prUrl: { value: null as string | null, timestamp: 0 },
39
+ blockUsage: {
40
+ value: null as BlockUsageInfo | null,
41
+ timestamp: 0,
42
+ },
31
43
  };
32
44
 
33
45
  // 캐시 TTL (ms)
@@ -35,8 +47,20 @@ const CACHE_TTL = {
35
47
  branch: 5000, // 5초
36
48
  gitChanges: 3000, // 3초
37
49
  prUrl: 30000, // 30초
50
+ blockUsage: 60000, // 60초 (JSONL 파싱은 비용이 크므로 긴 TTL)
38
51
  };
39
52
 
53
+ // Plan별 5시간 토큰 한도
54
+ const PLAN_LIMITS = {
55
+ pro: 450_000, // Pro plan
56
+ max5x: 2_250_000, // Max 5x (450K * 5)
57
+ max20x: 9_000_000, // Max 20x (450K * 20)
58
+ } as const;
59
+
60
+ type Plan = keyof typeof PLAN_LIMITS;
61
+
62
+ const DEFAULT_PLAN: Plan = "pro";
63
+
40
64
  // TrueColor 색상 정의
41
65
  const C = {
42
66
  RESET: "\x1b[0m",
@@ -50,8 +74,8 @@ const C = {
50
74
  UNDERLINE: "\x1b[4m",
51
75
  };
52
76
 
53
- // Context 사용률에 따른 색상
54
- function getContextColor(pct: number): string {
77
+ // 사용률에 따른 색상 (Context 및 Block Usage 공통)
78
+ function getUsageColor(pct: number): string {
55
79
  if (pct < 50) return C.WHITE;
56
80
  if (pct < 80) return C.YELLOW;
57
81
  return C.RED;
@@ -138,6 +162,114 @@ async function getPrUrlCached(): Promise<string | null> {
138
162
  }
139
163
  }
140
164
 
165
+ // CLI 인자 파싱
166
+ const args = process.argv.slice(2);
167
+ const noUsage = args.includes("--no-usage");
168
+
169
+ // --plan 옵션 파싱 (예: --plan max5x)
170
+ const planIndex = args.indexOf("--plan");
171
+ const planArg =
172
+ planIndex !== -1 && planIndex + 1 < args.length
173
+ ? args[planIndex + 1]
174
+ : DEFAULT_PLAN;
175
+ const BLOCK_TOKEN_LIMIT =
176
+ PLAN_LIMITS[planArg as Plan] ?? PLAN_LIMITS[DEFAULT_PLAN];
177
+
178
+ // ccusage를 사용하여 블록 사용량 정보 추출
179
+ async function getBlockUsageFromCcusage(): Promise<BlockUsageInfo> {
180
+ const result: BlockUsageInfo = {
181
+ resetTime: null,
182
+ blockTokens: 0,
183
+ blockStartTime: null,
184
+ };
185
+
186
+ try {
187
+ const blocks = await loadSessionBlockData({
188
+ sessionDurationHours: 5,
189
+ offline: true,
190
+ order: "desc",
191
+ });
192
+
193
+ if (blocks.length === 0) return result;
194
+
195
+ // 가장 최근의 활성 블록 찾기 (갭 블록 제외)
196
+ const activeBlock = blocks.find((b) => !b.isGap);
197
+ if (!activeBlock) return result;
198
+
199
+ // 블록 토큰 합산 (input + output만, 캐시 토큰 제외)
200
+ const { tokenCounts } = activeBlock;
201
+ result.blockTokens = tokenCounts.inputTokens + tokenCounts.outputTokens;
202
+
203
+ result.blockStartTime = activeBlock.startTime.getTime();
204
+
205
+ // 리셋 시간 설정 (ccusage가 제공하는 usageLimitResetTime 또는 블록 종료 시간)
206
+ if (
207
+ activeBlock.usageLimitResetTime &&
208
+ activeBlock.usageLimitResetTime > new Date()
209
+ ) {
210
+ result.resetTime = activeBlock.usageLimitResetTime;
211
+ } else if (activeBlock.endTime > new Date()) {
212
+ result.resetTime = activeBlock.endTime;
213
+ }
214
+
215
+ return result;
216
+ } catch {
217
+ return result;
218
+ }
219
+ }
220
+
221
+ // 블록 사용량 가져오기 (캐싱)
222
+ async function getBlockUsageCached(): Promise<BlockUsageInfo | null> {
223
+ if (Date.now() - cache.blockUsage.timestamp < CACHE_TTL.blockUsage) {
224
+ return cache.blockUsage.value;
225
+ }
226
+
227
+ const blockUsage = await getBlockUsageFromCcusage();
228
+ cache.blockUsage = { value: blockUsage, timestamp: Date.now() };
229
+ return blockUsage;
230
+ }
231
+
232
+ // 리셋까지 남은 시간 계산
233
+ function getTimeUntilReset(resetTime: Date): {
234
+ hours: number;
235
+ minutes: number;
236
+ } {
237
+ const now = new Date();
238
+ const diff = resetTime.getTime() - now.getTime();
239
+
240
+ if (diff <= 0) {
241
+ return { hours: 0, minutes: 0 };
242
+ }
243
+
244
+ const hours = Math.floor(diff / (1000 * 60 * 60));
245
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
246
+
247
+ return { hours, minutes };
248
+ }
249
+
250
+ // 토큰 수를 K 단위로 포맷팅
251
+ function formatTokensK(tokens: number): string {
252
+ if (tokens >= 1000) {
253
+ return `${Math.round(tokens / 1000)}K`;
254
+ }
255
+ return tokens.toString();
256
+ }
257
+
258
+ // 분당 번레이트 계산 (tokens/min)
259
+ function calculateBurnRate(
260
+ blockTokens: number,
261
+ blockStartTime: number | null,
262
+ ): number {
263
+ if (!blockStartTime || blockTokens === 0) return 0;
264
+
265
+ const now = Date.now();
266
+ const elapsedMinutes = (now - blockStartTime) / (1000 * 60);
267
+
268
+ if (elapsedMinutes < 1) return 0; // 1분 미만에는 변동성이 큰 값을 표시하지 않음
269
+
270
+ return Math.round(blockTokens / elapsedMinutes);
271
+ }
272
+
141
273
  // 메인 함수
142
274
  async function main() {
143
275
  // 1. stdin에서 Claude Code JSON 읽기
@@ -167,13 +299,14 @@ async function main() {
167
299
  : 0;
168
300
 
169
301
  const contextPct = Math.round((totalTokens / contextSize) * 100);
170
- const ctxColor = getContextColor(contextPct);
302
+ const ctxColor = getUsageColor(contextPct);
171
303
 
172
- // 6. Git 정보 (캐싱, 병렬 실행)
173
- const [branch, gitChanges, prUrl] = await Promise.all([
304
+ // 6. Git 정보 + 블록 사용량 (캐싱, 병렬 실행)
305
+ const [branch, gitChanges, prUrl, blockUsage] = await Promise.all([
174
306
  getBranchCached(),
175
307
  getGitChangesCached(),
176
308
  getPrUrlCached(),
309
+ noUsage ? Promise.resolve(null) : getBlockUsageCached(),
177
310
  ]);
178
311
 
179
312
  // 7. 출력
@@ -185,31 +318,67 @@ async function main() {
185
318
  console.log(line1);
186
319
 
187
320
  // 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
- );
321
+ const line2 =
322
+ `${C.WHITE}⏱️ ${formatTime(sessionHrs, sessionMins)}${C.RESET}` +
323
+ ` | ${C.WHITE}💰 $${costUsd.toFixed(2)}${C.RESET}` +
324
+ ` | ${ctxColor}🧠 ${formatNumber(totalTokens)} (${contextPct}%)${C.RESET}`;
325
+
326
+ console.log(line2);
327
+
328
+ // 3번째 줄: 리셋 타이머 | 사용량 | 번레이트 (--no-usage가 아닐 때)
329
+ if (!noUsage && blockUsage) {
330
+ const parts: string[] = [];
331
+
332
+ // 리셋 타이머
333
+ if (blockUsage.resetTime) {
334
+ const resetTime = getTimeUntilReset(blockUsage.resetTime);
335
+ parts.push(
336
+ `${C.WHITE}⏳ ${formatTime(resetTime.hours, resetTime.minutes)}${C.RESET}`,
337
+ );
338
+ }
339
+
340
+ // 블록 사용량
341
+ const usagePct = Math.round(
342
+ (blockUsage.blockTokens / BLOCK_TOKEN_LIMIT) * 100,
343
+ );
344
+ const usageColor = getUsageColor(usagePct);
345
+ parts.push(
346
+ `${usageColor}📊 ${formatTokensK(blockUsage.blockTokens)}/${formatTokensK(BLOCK_TOKEN_LIMIT)} (${usagePct}%)${C.RESET}`,
347
+ );
348
+
349
+ // 번레이트
350
+ const burnRate = calculateBurnRate(
351
+ blockUsage.blockTokens,
352
+ blockUsage.blockStartTime,
353
+ );
354
+ if (burnRate > 0) {
355
+ parts.push(`${C.WHITE}🔥 ${formatTokensK(burnRate)}/min${C.RESET}`);
356
+ }
357
+
358
+ if (parts.length > 0) {
359
+ console.log(parts.join(" | "));
360
+ }
361
+ }
193
362
 
194
- // 3번째 줄: git changes | PR URL
363
+ // 4번째 줄: git changes | PR URL
195
364
  const hasGitChanges =
196
365
  gitChanges.files > 0 ||
197
366
  gitChanges.insertions > 0 ||
198
367
  gitChanges.deletions > 0;
199
368
  if (hasGitChanges || prUrl) {
200
- let line3 = "";
369
+ let line4 = "";
201
370
  if (hasGitChanges) {
202
- line3 += `✏️ ${C.WHITE}${gitChanges.files} files${C.RESET} ${C.GREEN}+${gitChanges.insertions}${C.RESET} ${C.RED}-${gitChanges.deletions}${C.RESET}`;
371
+ line4 += `✏️ ${C.WHITE}${gitChanges.files} files${C.RESET} ${C.GREEN}+${gitChanges.insertions}${C.RESET} ${C.RED}-${gitChanges.deletions}${C.RESET}`;
203
372
  }
204
373
  if (prUrl) {
205
374
  const prLabel = prUrl
206
375
  .replace("https://github.com/", "")
207
376
  .replace("/pull/", "#");
208
- if (line3) line3 += " | ";
377
+ if (line4) line4 += " | ";
209
378
  // OSC 8 하이퍼링크
210
- line3 += `📎 ${C.WHITE}${C.UNDERLINE}\x1b]8;;${prUrl}\x07${prLabel}\x1b]8;;\x07${C.RESET}`;
379
+ line4 += `📎 ${C.WHITE}${C.UNDERLINE}\x1b]8;;${prUrl}\x07${prLabel}\x1b]8;;\x07${C.RESET}`;
211
380
  }
212
- console.log(line3);
381
+ console.log(line4);
213
382
  }
214
383
  }
215
384