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.
- package/QUICK_START.md +190 -0
- package/README.md +118 -0
- package/cli.js +476 -0
- package/install.ps1 +92 -0
- package/install.sh +106 -0
- package/package.json +23 -0
- package/statusline-template.js +458 -0
|
@@ -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
|
+
}
|