@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.
- package/README.md +113 -11
- package/package.json +10 -7
- 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
|
[](https://code.claude.com/docs/en/statusline)
|
|
@@ -7,29 +9,128 @@ 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
|
+
## 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
|
+

|
|
38
|
+
|
|
39
|
+
### PR only
|
|
40
|
+
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
### Git diff + PR
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+
### Context Normal (< 50%)
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
### Context Warning (50-80%)
|
|
11
52
|
|
|
12
|
-

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

|
|
58
|
+
|
|
59
|
+
### Limit Reset Timer
|
|
60
|
+
|
|
61
|
+

|
|
15
62
|
|
|
16
63
|
## Features
|
|
17
64
|
|
|
18
65
|
- **Session Time**: Current session elapsed time
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
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
|
-
##
|
|
75
|
+
## Emoji Guide
|
|
25
76
|
|
|
26
|
-
|
|
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
|
|
46
|
-
|
|
|
47
|
-
| Context %
|
|
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.
|
|
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
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
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
|
-
//
|
|
54
|
-
function
|
|
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 =
|
|
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
|
-
|
|
189
|
-
`${C.WHITE}⏱️ ${formatTime(sessionHrs, sessionMins)}${C.RESET}
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
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
|
|
369
|
+
let line4 = "";
|
|
201
370
|
if (hasGitChanges) {
|
|
202
|
-
|
|
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 (
|
|
377
|
+
if (line4) line4 += " | ";
|
|
209
378
|
// OSC 8 하이퍼링크
|
|
210
|
-
|
|
379
|
+
line4 += `📎 ${C.WHITE}${C.UNDERLINE}\x1b]8;;${prUrl}\x07${prLabel}\x1b]8;;\x07${C.RESET}`;
|
|
211
380
|
}
|
|
212
|
-
console.log(
|
|
381
|
+
console.log(line4);
|
|
213
382
|
}
|
|
214
383
|
}
|
|
215
384
|
|