cc-statusline-pro 1.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.
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const https = require('https');
7
+ const os = require('os');
8
+
9
+ // ANSI Color Codes
10
+ const colors = {
11
+ reset: '\x1b[0m',
12
+ bold: '\x1b[1m',
13
+
14
+ // Regular colors
15
+ black: '\x1b[30m',
16
+ red: '\x1b[31m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ blue: '\x1b[34m',
20
+ magenta: '\x1b[35m',
21
+ cyan: '\x1b[36m',
22
+ white: '\x1b[37m',
23
+
24
+ // Bright colors
25
+ brightRed: '\x1b[91m',
26
+ brightGreen: '\x1b[92m',
27
+ brightYellow: '\x1b[93m',
28
+ brightBlue: '\x1b[94m',
29
+ brightMagenta: '\x1b[95m',
30
+ brightCyan: '\x1b[96m',
31
+
32
+ // Combined styles
33
+ boldCyan: '\x1b[1;36m',
34
+ boldGreen: '\x1b[1;32m',
35
+ boldYellow: '\x1b[1;33m',
36
+ boldRed: '\x1b[1;31m',
37
+ boldMagenta: '\x1b[1;35m',
38
+ };
39
+
40
+ // Cache cho usage limits (5 phút)
41
+ const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in ms
42
+ let usageLimitsCache = {
43
+ data: null,
44
+ timestamp: 0
45
+ };
46
+
47
+ // Đọc input JSON từ stdin
48
+ let inputData = '';
49
+ process.stdin.on('data', chunk => {
50
+ inputData += chunk;
51
+ });
52
+
53
+ process.stdin.on('end', async () => {
54
+ try {
55
+ const data = JSON.parse(inputData);
56
+ const statusLine = await buildStatusLine(data);
57
+ process.stdout.write(statusLine);
58
+ } catch (error) {
59
+ // Fallback nếu có lỗi
60
+ process.stdout.write('Claude Code');
61
+ }
62
+ });
63
+
64
+ async function buildStatusLine(data) {
65
+ const parts = [];
66
+
67
+ // 1. Tên model
68
+ const modelName = getModelDisplayName(data);
69
+ parts.push(modelName);
70
+
71
+ // 2. Context usage với thanh tiến trình
72
+ const contextInfo = getContextUsage(data);
73
+ if (contextInfo) {
74
+ parts.push(contextInfo);
75
+ }
76
+
77
+ // 3. Thời gian session
78
+ const sessionTime = getSessionTime(data);
79
+ if (sessionTime) {
80
+ parts.push(sessionTime);
81
+ }
82
+
83
+ // 4. Usage limits từ Anthropic API
84
+ const usageLimits = await getUsageLimits();
85
+ if (usageLimits) {
86
+ parts.push(usageLimits);
87
+ }
88
+
89
+ // 5. Cost information
90
+ const costInfo = getCostInfo(data);
91
+ if (costInfo) {
92
+ parts.push(costInfo);
93
+ }
94
+
95
+ // 6. Code changes (lines added/removed)
96
+ const codeChanges = getCodeChanges(data);
97
+ if (codeChanges) {
98
+ parts.push(codeChanges);
99
+ }
100
+
101
+ // 7. Git branch (chỉ hiện nếu trong git repo)
102
+ const gitBranch = getGitBranch(data);
103
+ if (gitBranch) {
104
+ parts.push(gitBranch);
105
+ }
106
+
107
+ return parts.join(' | ');
108
+ }
109
+
110
+ function getModelDisplayName(data) {
111
+ const name = data.model?.display_name || 'Claude';
112
+ return `${colors.boldCyan}${name}${colors.reset}`;
113
+ }
114
+
115
+ function getContextUsage(data) {
116
+ if (!data.context_window || !data.context_window.current_usage) {
117
+ return null;
118
+ }
119
+
120
+ const usage = data.context_window.current_usage;
121
+ const windowSize = data.context_window.context_window_size;
122
+
123
+ // Tính tổng tokens đang sử dụng
124
+ const currentTokens = (usage.input_tokens || 0) +
125
+ (usage.cache_creation_input_tokens || 0) +
126
+ (usage.cache_read_input_tokens || 0);
127
+
128
+ const percentage = Math.round((currentTokens / windowSize) * 100);
129
+
130
+ // Chọn màu dựa trên percentage
131
+ let percentColor;
132
+ if (percentage < 40) {
133
+ percentColor = colors.green;
134
+ } else if (percentage < 70) {
135
+ percentColor = colors.yellow;
136
+ } else {
137
+ percentColor = colors.red;
138
+ }
139
+
140
+ // Tạo thanh tiến trình (10 ký tự) với màu
141
+ const barLength = 10;
142
+ const filled = Math.round((percentage / 100) * barLength);
143
+ const empty = barLength - filled;
144
+
145
+ const barColor = percentage < 40 ? colors.green :
146
+ percentage < 70 ? colors.yellow : colors.red;
147
+
148
+ const bar = `${barColor}${'▓'.repeat(filled)}${colors.reset}${'░'.repeat(empty)}`;
149
+
150
+ return `${bar} ${percentColor}${percentage}%${colors.reset}`;
151
+ }
152
+
153
+ function getSessionTime(data) {
154
+ if (!data.session_id || !data.transcript_path) {
155
+ return null;
156
+ }
157
+
158
+ try {
159
+ // Lấy thời gian tạo file transcript
160
+ const stats = fs.statSync(data.transcript_path);
161
+ const startTime = stats.birthtimeMs || stats.ctimeMs;
162
+ const now = Date.now();
163
+ const elapsed = Math.floor((now - startTime) / 1000); // seconds
164
+
165
+ // Tính thời gian đã trôi qua
166
+ const hours = Math.floor(elapsed / 3600);
167
+ const minutes = Math.floor((elapsed % 3600) / 60);
168
+ const seconds = elapsed % 60;
169
+
170
+ let timeStr;
171
+ if (hours > 0) {
172
+ timeStr = `⏱ ${hours}h ${minutes}m`;
173
+ } else if (minutes > 0) {
174
+ timeStr = `⏱ ${minutes}m ${seconds}s`;
175
+ } else {
176
+ timeStr = `⏱ ${seconds}s`;
177
+ }
178
+
179
+ return `${colors.blue}${timeStr}${colors.reset}`;
180
+ } catch (error) {
181
+ return null;
182
+ }
183
+ }
184
+
185
+ function getCostInfo(data) {
186
+ if (!data.cost) {
187
+ return null;
188
+ }
189
+
190
+ const cost = data.cost.total_cost_usd;
191
+
192
+ if (cost === undefined || cost === null) {
193
+ return null;
194
+ }
195
+
196
+ // Format chi phí với 4 chữ số thập phân
197
+ const costFormatted = cost.toFixed(4);
198
+
199
+ return `${colors.yellow}$${costFormatted}${colors.reset}`;
200
+ }
201
+
202
+ function getCodeChanges(data) {
203
+ if (!data.cost) {
204
+ return null;
205
+ }
206
+
207
+ const added = data.cost.total_lines_added || 0;
208
+ const removed = data.cost.total_lines_removed || 0;
209
+
210
+ // Chỉ hiển thị nếu có thay đổi code
211
+ if (added === 0 && removed === 0) {
212
+ return null;
213
+ }
214
+
215
+ const parts = [];
216
+
217
+ if (added > 0) {
218
+ parts.push(`${colors.green}+${added}${colors.reset}`);
219
+ }
220
+
221
+ if (removed > 0) {
222
+ parts.push(`${colors.red}-${removed}${colors.reset}`);
223
+ }
224
+
225
+ return parts.length > 0 ? parts.join(' ') : null;
226
+ }
227
+
228
+ // ============================================================================
229
+ // USAGE LIMITS FROM ANTHROPIC API
230
+ // ============================================================================
231
+
232
+ function getAccessToken() {
233
+ const platform = os.platform();
234
+
235
+ try {
236
+ if (platform === 'darwin') {
237
+ // macOS: Keychain
238
+ const token = execSync(
239
+ 'security find-generic-password -s "Claude Code-credentials" -w',
240
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
241
+ ).trim();
242
+ return token;
243
+ } else if (platform === 'win32') {
244
+ // Windows: Thử nhiều phương pháp
245
+
246
+ // Phương pháp 1: Windows Credential Manager via PowerShell
247
+ try {
248
+ const psScript = `
249
+ $cred = cmdkey /list | Select-String "Claude Code"
250
+ if ($cred) {
251
+ $target = ($cred -split ':')[1].Trim()
252
+ $password = cmdkey /list:$target | Select-String "Password"
253
+ if ($password) {
254
+ ($password -split ':')[1].Trim()
255
+ }
256
+ }
257
+ `;
258
+ const token = execSync(`powershell -Command "${psScript}"`, {
259
+ encoding: 'utf8',
260
+ stdio: ['pipe', 'pipe', 'pipe']
261
+ }).trim();
262
+ if (token) return token;
263
+ } catch (e) {
264
+ // Ignore and try next method
265
+ }
266
+
267
+ // Phương pháp 2: Tìm trong AppData
268
+ const appDataPaths = [
269
+ path.join(os.homedir(), '.claude', '.credentials.json'),
270
+ path.join(process.env.APPDATA || '', 'Claude Code', 'credentials'),
271
+ path.join(process.env.LOCALAPPDATA || '', 'Claude Code', 'credentials'),
272
+ path.join(os.homedir(), '.claude', 'credentials'),
273
+ ];
274
+
275
+ for (const credPath of appDataPaths) {
276
+ if (fs.existsSync(credPath)) {
277
+ const content = fs.readFileSync(credPath, 'utf8');
278
+ // Thử parse JSON
279
+ try {
280
+ const parsed = JSON.parse(content);
281
+
282
+ // Check for claudeAiOauth.accessToken (most common)
283
+ if (parsed.claudeAiOauth && parsed.claudeAiOauth.accessToken) {
284
+ return parsed.claudeAiOauth.accessToken;
285
+ }
286
+
287
+ // Fallback
288
+ if (parsed.access_token || parsed.token) {
289
+ return parsed.access_token || parsed.token;
290
+ }
291
+ } catch (e) {
292
+ // Có thể là plain text
293
+ return content.trim();
294
+ }
295
+ }
296
+ }
297
+ }
298
+ } catch (error) {
299
+ // Không tìm thấy access token
300
+ return null;
301
+ }
302
+
303
+ return null;
304
+ }
305
+
306
+ async function fetchUsageLimitsFromAPI() {
307
+ return new Promise((resolve) => {
308
+ const accessToken = getAccessToken();
309
+
310
+ if (!accessToken) {
311
+ resolve(null);
312
+ return;
313
+ }
314
+
315
+ const options = {
316
+ hostname: 'api.anthropic.com',
317
+ path: '/api/oauth/usage',
318
+ method: 'GET',
319
+ headers: {
320
+ 'Authorization': `Bearer ${accessToken}`,
321
+ 'anthropic-beta': 'oauth-2025-04-20',
322
+ 'User-Agent': `claude-code/${process.env.CLAUDE_CODE_VERSION || '2.0.76'}`
323
+ },
324
+ timeout: 2000 // 2 second timeout
325
+ };
326
+
327
+ const req = https.request(options, (res) => {
328
+ let data = '';
329
+
330
+ res.on('data', (chunk) => {
331
+ data += chunk;
332
+ });
333
+
334
+ res.on('end', () => {
335
+ try {
336
+ if (res.statusCode === 200) {
337
+ const usage = JSON.parse(data);
338
+ resolve(usage);
339
+ } else {
340
+ resolve(null);
341
+ }
342
+ } catch (error) {
343
+ resolve(null);
344
+ }
345
+ });
346
+ });
347
+
348
+ req.on('error', () => {
349
+ resolve(null);
350
+ });
351
+
352
+ req.on('timeout', () => {
353
+ req.destroy();
354
+ resolve(null);
355
+ });
356
+
357
+ req.end();
358
+ });
359
+ }
360
+
361
+ async function getUsageLimits() {
362
+ // Kiểm tra cache
363
+ const now = Date.now();
364
+ if (usageLimitsCache.data && (now - usageLimitsCache.timestamp) < CACHE_DURATION) {
365
+ return formatUsageLimits(usageLimitsCache.data);
366
+ }
367
+
368
+ // Fetch mới từ API
369
+ const usage = await fetchUsageLimitsFromAPI();
370
+
371
+ if (usage) {
372
+ usageLimitsCache.data = usage;
373
+ usageLimitsCache.timestamp = now;
374
+ return formatUsageLimits(usage);
375
+ }
376
+
377
+ return null;
378
+ }
379
+
380
+ function formatUsageLimits(usage) {
381
+ if (!usage || !usage.five_hour) {
382
+ return null;
383
+ }
384
+
385
+ const utilization = usage.five_hour.utilization;
386
+ const resetsAt = usage.five_hour.resets_at;
387
+
388
+ if (utilization === undefined || !resetsAt) {
389
+ return null;
390
+ }
391
+
392
+ // Dynamic color based on utilization
393
+ let usageColor;
394
+ if (utilization < 50) {
395
+ usageColor = colors.green;
396
+ } else if (utilization < 80) {
397
+ usageColor = colors.yellow;
398
+ } else {
399
+ usageColor = colors.red;
400
+ }
401
+
402
+ const parts = [];
403
+
404
+ // Format utilization with dynamic color
405
+ parts.push(`${usageColor}Plan: ${Math.round(utilization)}% used${colors.reset}`);
406
+
407
+ // Calculate time until reset
408
+ const resetTime = new Date(resetsAt);
409
+ const now = new Date();
410
+ const diff = resetTime - now;
411
+
412
+ if (diff > 0) {
413
+ const hours = Math.floor(diff / (1000 * 60 * 60));
414
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
415
+
416
+ let resetStr;
417
+ if (hours > 0) {
418
+ resetStr = `Resets in ${hours}h ${minutes}m`;
419
+ } else if (minutes > 0) {
420
+ resetStr = `Resets in ${minutes}m`;
421
+ } else {
422
+ resetStr = `Resets soon`;
423
+ }
424
+
425
+ parts.push(`${colors.magenta}${resetStr}${colors.reset}`);
426
+ }
427
+
428
+ return parts.join(' | ');
429
+ }
430
+
431
+ function getGitBranch(data) {
432
+ try {
433
+ const cwd = data.workspace?.project_dir || data.workspace?.current_dir || process.cwd();
434
+
435
+ // Kiểm tra xem có phải git repo không
436
+ const gitDir = path.join(cwd, '.git');
437
+ if (!fs.existsSync(gitDir)) {
438
+ return null;
439
+ }
440
+
441
+ // Lấy branch name (skip optional locks để tránh conflict)
442
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
443
+ cwd: cwd,
444
+ encoding: 'utf8',
445
+ stdio: ['pipe', 'pipe', 'ignore'],
446
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' }
447
+ }).trim();
448
+
449
+ if (branch && branch !== 'HEAD') {
450
+ return `${colors.cyan}⎇ ${branch}${colors.reset}`;
451
+ }
452
+
453
+ return null;
454
+ } catch (error) {
455
+ // Không phải git repo hoặc có lỗi
456
+ return null;
457
+ }
458
+ }