@say8425/cc-statusline 1.4.0 → 2.0.1
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 +43 -57
- package/package.json +30 -30
- package/src/index.ts +0 -385
package/README.md
CHANGED
|
@@ -23,13 +23,6 @@ Add the following to `~/.claude/settings.json`:
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
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
26
|
## Screenshots
|
|
34
27
|
|
|
35
28
|
### Git diff only
|
|
@@ -44,21 +37,9 @@ Add the following to `~/.claude/settings.json`:
|
|
|
44
37
|
|
|
45
38
|

|
|
46
39
|
|
|
47
|
-
###
|
|
48
|
-
|
|
49
|
-

|
|
50
|
-
|
|
51
|
-
### Context Warning (50-80%)
|
|
52
|
-
|
|
53
|
-

|
|
54
|
-
|
|
55
|
-
### Context Critical (> 80%)
|
|
56
|
-
|
|
57
|
-

|
|
40
|
+
### Usage Metrics
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-

|
|
42
|
+

|
|
62
43
|
|
|
63
44
|
## Features
|
|
64
45
|
|
|
@@ -68,9 +49,10 @@ Add the following to `~/.claude/settings.json`:
|
|
|
68
49
|
- **Git Diff**: File count, insertions, deletions
|
|
69
50
|
- **PR URL**: Clickable OSC 8 hyperlink
|
|
70
51
|
- **TrueColor**: Dynamic colors based on thresholds
|
|
71
|
-
- **Limit Reset
|
|
72
|
-
- **Block Usage**: 5-hour
|
|
73
|
-
- **
|
|
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)
|
|
74
56
|
|
|
75
57
|
## Emoji Guide
|
|
76
58
|
|
|
@@ -81,56 +63,41 @@ Add the following to `~/.claude/settings.json`:
|
|
|
81
63
|
| ⏱️ | Session elapsed time |
|
|
82
64
|
| 💰 | Session cost in USD |
|
|
83
65
|
| 🧠 | Context window usage |
|
|
84
|
-
| ⏳ | Limit reset
|
|
85
|
-
| 📊 | 5-hour
|
|
86
|
-
|
|
|
66
|
+
| ⏳ | Limit reset time |
|
|
67
|
+
| 📊 | 5-hour utilization % |
|
|
68
|
+
| ⏰ | Weekly limit reset time |
|
|
69
|
+
| 📅 | 7-day utilization % |
|
|
87
70
|
| ✏️ | Uncommitted changes |
|
|
88
71
|
| 📎 | Pull request link |
|
|
89
72
|
|
|
90
73
|
## Usage Metrics
|
|
91
74
|
|
|
92
|
-
Shows usage information
|
|
93
|
-
|
|
94
|
-
### How It Works
|
|
95
|
-
|
|
96
|
-
Automatically parses JSONL files from `~/.claude/projects/` to detect:
|
|
75
|
+
Shows usage information from the Anthropic Usage API.
|
|
97
76
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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.
|
|
102
79
|
|
|
103
|
-
|
|
80
|
+
> [!NOTE]
|
|
81
|
+
> This feature is **macOS only** as it reads the OAuth token from macOS Keychain (`Claude Code-credentials`).
|
|
104
82
|
|
|
105
|
-
###
|
|
106
|
-
|
|
107
|
-
Different Claude Code plans have different token limits. Use the `--plan` flag to set your plan:
|
|
83
|
+
### How It Works
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
{
|
|
111
|
-
"statusLine": {
|
|
112
|
-
"type": "command",
|
|
113
|
-
"command": "bunx @say8425/cc-statusline --plan max5x",
|
|
114
|
-
"padding": 0
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
```
|
|
85
|
+
Calls the Anthropic Usage API (`/api/oauth/usage`) using the OAuth access token from macOS Keychain to retrieve:
|
|
118
86
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| Max 20x | 9M | `--plan max20x` |
|
|
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`)
|
|
124
91
|
|
|
125
|
-
###
|
|
92
|
+
### Enable
|
|
126
93
|
|
|
127
|
-
|
|
94
|
+
Usage metrics are **hidden by default**. To enable, use the `--show-usage` flag:
|
|
128
95
|
|
|
129
96
|
```json
|
|
130
97
|
{
|
|
131
98
|
"statusLine": {
|
|
132
99
|
"type": "command",
|
|
133
|
-
"command": "bunx @say8425/cc-statusline --
|
|
100
|
+
"command": "bunx @say8425/cc-statusline --show-usage",
|
|
134
101
|
"padding": 0
|
|
135
102
|
}
|
|
136
103
|
}
|
|
@@ -141,6 +108,25 @@ To hide the usage metrics line (reset timer, block usage, burn rate), use the `-
|
|
|
141
108
|
- [Bun](https://bun.sh) - JavaScript runtime
|
|
142
109
|
- [gh](https://cli.github.com) - GitHub CLI (optional, for PR URL)
|
|
143
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
|
+
|
|
144
130
|
## Color Thresholds
|
|
145
131
|
|
|
146
132
|
| Metric | Normal (white) | Warning (yellow) | Critical (red) |
|
package/package.json
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
2
|
+
"name": "@say8425/cc-statusline",
|
|
3
|
+
"version": "2.0.1",
|
|
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": "dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"description": "Custom statusline for Claude Code",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/index.js"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "bun build.ts",
|
|
25
|
+
"start": "bun src/index.ts",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"test:coverage": "bun test --coverage",
|
|
28
|
+
"lint": "biome check src/",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"type": "module"
|
|
32
32
|
}
|
package/src/index.ts
DELETED
|
@@ -1,385 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { $ } from "bun";
|
|
4
|
-
import { loadSessionBlockData } from "ccusage/data-loader";
|
|
5
|
-
|
|
6
|
-
// 공식 Claude Code JSON input 타입 정의
|
|
7
|
-
interface ClaudeStatusInput {
|
|
8
|
-
cost: {
|
|
9
|
-
total_duration_ms: number;
|
|
10
|
-
total_cost_usd: number;
|
|
11
|
-
};
|
|
12
|
-
context_window: {
|
|
13
|
-
context_window_size: number;
|
|
14
|
-
current_usage: {
|
|
15
|
-
input_tokens: number;
|
|
16
|
-
output_tokens: number;
|
|
17
|
-
cache_creation_input_tokens: number;
|
|
18
|
-
cache_read_input_tokens: number;
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
workspace: {
|
|
22
|
-
current_dir: string;
|
|
23
|
-
project_dir: string;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 블록 사용량 정보 타입
|
|
28
|
-
interface BlockUsageInfo {
|
|
29
|
-
resetTime: Date | null;
|
|
30
|
-
blockTokens: number;
|
|
31
|
-
blockStartTime: number | null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 캐시 구조
|
|
35
|
-
const cache = {
|
|
36
|
-
branch: { value: "", timestamp: 0 },
|
|
37
|
-
gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
|
|
38
|
-
prUrl: { value: null as string | null, timestamp: 0 },
|
|
39
|
-
blockUsage: {
|
|
40
|
-
value: null as BlockUsageInfo | null,
|
|
41
|
-
timestamp: 0,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// 캐시 TTL (ms)
|
|
46
|
-
const CACHE_TTL = {
|
|
47
|
-
branch: 5000, // 5초
|
|
48
|
-
gitChanges: 3000, // 3초
|
|
49
|
-
prUrl: 30000, // 30초
|
|
50
|
-
blockUsage: 60000, // 60초 (JSONL 파싱은 비용이 크므로 긴 TTL)
|
|
51
|
-
};
|
|
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
|
-
|
|
64
|
-
// TrueColor 색상 정의
|
|
65
|
-
const C = {
|
|
66
|
-
RESET: "\x1b[0m",
|
|
67
|
-
CYAN: "\x1b[38;2;0;255;255m",
|
|
68
|
-
MAGENTA: "\x1b[38;2;255;100;200m",
|
|
69
|
-
GREEN: "\x1b[38;2;100;255;100m",
|
|
70
|
-
YELLOW: "\x1b[38;2;255;220;100m",
|
|
71
|
-
RED: "\x1b[38;2;255;100;100m",
|
|
72
|
-
BLUE: "\x1b[38;2;100;150;255m",
|
|
73
|
-
WHITE: "\x1b[38;2;200;200;200m",
|
|
74
|
-
UNDERLINE: "\x1b[4m",
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// 사용률에 따른 색상 (Context 및 Block Usage 공통)
|
|
78
|
-
function getUsageColor(pct: number): string {
|
|
79
|
-
if (pct < 50) return C.WHITE;
|
|
80
|
-
if (pct < 80) return C.YELLOW;
|
|
81
|
-
return C.RED;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 숫자 포맷팅 (천 단위 콤마)
|
|
85
|
-
function formatNumber(n: number): string {
|
|
86
|
-
return n.toLocaleString("en-US");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 시간 포맷팅 (HH:MM)
|
|
90
|
-
function formatTime(hours: number, mins: number): string {
|
|
91
|
-
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// stdin에서 JSON 읽기
|
|
95
|
-
async function readStdin(): Promise<string> {
|
|
96
|
-
const chunks: Uint8Array[] = [];
|
|
97
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
98
|
-
chunks.push(chunk);
|
|
99
|
-
}
|
|
100
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Git 브랜치 가져오기 (캐싱)
|
|
104
|
-
async function getBranchCached(): Promise<string> {
|
|
105
|
-
if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
|
|
106
|
-
return cache.branch.value;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
const result = await $`git branch --show-current 2>/dev/null`.text();
|
|
110
|
-
cache.branch = { value: result.trim(), timestamp: Date.now() };
|
|
111
|
-
return cache.branch.value;
|
|
112
|
-
} catch {
|
|
113
|
-
return cache.branch.value;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Git 변경사항 가져오기 (캐싱)
|
|
118
|
-
async function getGitChangesCached(): Promise<{
|
|
119
|
-
files: number;
|
|
120
|
-
insertions: number;
|
|
121
|
-
deletions: number;
|
|
122
|
-
}> {
|
|
123
|
-
if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
|
|
124
|
-
return cache.gitChanges;
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
const [diff, staged] = await Promise.all([
|
|
128
|
-
$`git diff --shortstat 2>/dev/null`.text(),
|
|
129
|
-
$`git diff --cached --shortstat 2>/dev/null`.text(),
|
|
130
|
-
]);
|
|
131
|
-
const combined = `${diff}\n${staged}`;
|
|
132
|
-
|
|
133
|
-
// 파일 수, insertions, deletions 추출
|
|
134
|
-
const [files, insertions, deletions] = [
|
|
135
|
-
/(\d+) file/g,
|
|
136
|
-
/(\d+) insertion/g,
|
|
137
|
-
/(\d+) deletion/g,
|
|
138
|
-
].map((regex) =>
|
|
139
|
-
(combined.match(regex) || []).reduce(
|
|
140
|
-
(sum, m) => sum + Number.parseInt(m, 10),
|
|
141
|
-
0,
|
|
142
|
-
),
|
|
143
|
-
);
|
|
144
|
-
cache.gitChanges = { files, insertions, deletions, timestamp: Date.now() };
|
|
145
|
-
return cache.gitChanges;
|
|
146
|
-
} catch {
|
|
147
|
-
return cache.gitChanges;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// PR URL 가져오기 (캐싱)
|
|
152
|
-
async function getPrUrlCached(): Promise<string | null> {
|
|
153
|
-
if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
|
|
154
|
-
return cache.prUrl.value;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const result = await $`gh pr view --json url -q .url 2>/dev/null`.text();
|
|
158
|
-
cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
|
|
159
|
-
return cache.prUrl.value;
|
|
160
|
-
} catch {
|
|
161
|
-
return cache.prUrl.value;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
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
|
-
|
|
273
|
-
// 메인 함수
|
|
274
|
-
async function main() {
|
|
275
|
-
// 1. stdin에서 Claude Code JSON 읽기
|
|
276
|
-
const claudeJson: ClaudeStatusInput = JSON.parse(await readStdin());
|
|
277
|
-
|
|
278
|
-
// 2. 폴더 이름 추출 (프로젝트 루트 디렉토리)
|
|
279
|
-
const folder = claudeJson.workspace?.project_dir?.split("/").pop() || "";
|
|
280
|
-
|
|
281
|
-
// 3. 세션 시간 계산
|
|
282
|
-
const sessionMs = claudeJson.cost?.total_duration_ms || 0;
|
|
283
|
-
const sessionSec = Math.floor(sessionMs / 1000);
|
|
284
|
-
const sessionHrs = Math.floor(sessionSec / 3600);
|
|
285
|
-
const sessionMins = Math.floor((sessionSec % 3600) / 60);
|
|
286
|
-
|
|
287
|
-
// 4. 비용
|
|
288
|
-
const costUsd = claudeJson.cost?.total_cost_usd || 0;
|
|
289
|
-
|
|
290
|
-
// 5. Context 토큰 계산
|
|
291
|
-
const usage = claudeJson.context_window?.current_usage;
|
|
292
|
-
const contextSize = claudeJson.context_window?.context_window_size || 200000;
|
|
293
|
-
|
|
294
|
-
const totalTokens = usage
|
|
295
|
-
? usage.input_tokens +
|
|
296
|
-
usage.output_tokens +
|
|
297
|
-
usage.cache_creation_input_tokens +
|
|
298
|
-
usage.cache_read_input_tokens
|
|
299
|
-
: 0;
|
|
300
|
-
|
|
301
|
-
const contextPct = Math.round((totalTokens / contextSize) * 100);
|
|
302
|
-
const ctxColor = getUsageColor(contextPct);
|
|
303
|
-
|
|
304
|
-
// 6. Git 정보 + 블록 사용량 (캐싱, 병렬 실행)
|
|
305
|
-
const [branch, gitChanges, prUrl, blockUsage] = await Promise.all([
|
|
306
|
-
getBranchCached(),
|
|
307
|
-
getGitChangesCached(),
|
|
308
|
-
getPrUrlCached(),
|
|
309
|
-
noUsage ? Promise.resolve(null) : getBlockUsageCached(),
|
|
310
|
-
]);
|
|
311
|
-
|
|
312
|
-
// 7. 출력
|
|
313
|
-
// 1번째 줄: 폴더 | 브랜치
|
|
314
|
-
let line1 = `${C.WHITE}📁 ${folder}${C.RESET}`;
|
|
315
|
-
if (branch) {
|
|
316
|
-
line1 += ` | ${C.WHITE}🌿 ${branch}${C.RESET}`;
|
|
317
|
-
}
|
|
318
|
-
console.log(line1);
|
|
319
|
-
|
|
320
|
-
// 2번째 줄: 세션 시간 | 비용 | 컨텍스트
|
|
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
|
-
}
|
|
362
|
-
|
|
363
|
-
// 4번째 줄: git changes | PR URL
|
|
364
|
-
const hasGitChanges =
|
|
365
|
-
gitChanges.files > 0 ||
|
|
366
|
-
gitChanges.insertions > 0 ||
|
|
367
|
-
gitChanges.deletions > 0;
|
|
368
|
-
if (hasGitChanges || prUrl) {
|
|
369
|
-
let line4 = "";
|
|
370
|
-
if (hasGitChanges) {
|
|
371
|
-
line4 += `✏️ ${C.WHITE}${gitChanges.files} files${C.RESET} ${C.GREEN}+${gitChanges.insertions}${C.RESET} ${C.RED}-${gitChanges.deletions}${C.RESET}`;
|
|
372
|
-
}
|
|
373
|
-
if (prUrl) {
|
|
374
|
-
const prLabel = prUrl
|
|
375
|
-
.replace("https://github.com/", "")
|
|
376
|
-
.replace("/pull/", "#");
|
|
377
|
-
if (line4) line4 += " | ";
|
|
378
|
-
// OSC 8 하이퍼링크
|
|
379
|
-
line4 += `📎 ${C.WHITE}${C.UNDERLINE}\x1b]8;;${prUrl}\x07${prLabel}\x1b]8;;\x07${C.RESET}`;
|
|
380
|
-
}
|
|
381
|
-
console.log(line4);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
main().catch(console.error);
|