ctxline-claude 0.0.1 → 0.0.2
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/LICENSE +21 -21
- package/README.md +154 -167
- package/bin/install.js +135 -135
- package/package.json +39 -39
- package/statusline.js +394 -345
- package/preview.png +0 -0
package/statusline.js
CHANGED
|
@@ -1,345 +1,394 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Claude Code Enhanced Statusline
|
|
3
|
-
// Shows: directory | model | context usage |
|
|
4
|
-
// Auto-detects API key vs subscription usage
|
|
5
|
-
// https://github.com/MithunWijayasiri/claudecode-statusline
|
|
6
|
-
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
const https = require('https');
|
|
11
|
-
const { execSync } = require('child_process');
|
|
12
|
-
|
|
13
|
-
const IS_API_KEY = !!process.env.ANTHROPIC_API_KEY;
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return colors.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (!
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code Enhanced Statusline
|
|
3
|
+
// Shows: directory | model | context usage | current (5-hour) + weekly usage | current task
|
|
4
|
+
// Auto-detects API key vs subscription usage
|
|
5
|
+
// https://github.com/MithunWijayasiri/claudecode-statusline
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const https = require('https');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const IS_API_KEY = !!process.env.ANTHROPIC_API_KEY;
|
|
14
|
+
|
|
15
|
+
// Shared width (cells) for all progress bars: context, current, weekly.
|
|
16
|
+
const BAR_WIDTH = 6;
|
|
17
|
+
|
|
18
|
+
// Cache configuration
|
|
19
|
+
const CACHE_DIR = path.join(os.homedir(), '.claude', 'cache');
|
|
20
|
+
const USAGE_CACHE_FILE = path.join(CACHE_DIR, 'usage-cache.json');
|
|
21
|
+
// Fresh: trust the cache and skip the API call entirely (fewer calls, faster render).
|
|
22
|
+
const FRESH_TTL_MS = 30000; // 30 seconds
|
|
23
|
+
// Stale: used only as a fallback when a live API call fails, so the usage bar stays
|
|
24
|
+
// visible through transient timeouts/errors instead of disappearing.
|
|
25
|
+
const STALE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
26
|
+
|
|
27
|
+
// ANSI color codes
|
|
28
|
+
const colors = {
|
|
29
|
+
reset: '\x1b[0m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
bold: '\x1b[1m',
|
|
32
|
+
green: '\x1b[32m',
|
|
33
|
+
yellow: '\x1b[33m',
|
|
34
|
+
orange: '\x1b[38;5;208m',
|
|
35
|
+
red: '\x1b[31m',
|
|
36
|
+
blink: '\x1b[5m'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function getUsageColor(percentage) {
|
|
40
|
+
if (percentage < 50) return colors.green;
|
|
41
|
+
if (percentage < 75) return colors.yellow;
|
|
42
|
+
if (percentage < 90) return colors.orange;
|
|
43
|
+
return colors.red;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Shorten verbose model names for the statusline: "Opus 4.8 (1M context)" -> "Opus 4.8 (1M)".
|
|
47
|
+
function shortenModel(name) {
|
|
48
|
+
return name.replace(/\s+context\)/i, ')');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getContextBar(remaining) {
|
|
52
|
+
const effectiveRemaining = remaining ?? 100;
|
|
53
|
+
const used = Math.max(0, Math.min(100, 100 - Math.round(effectiveRemaining)));
|
|
54
|
+
|
|
55
|
+
const filled = Math.floor((used / 100) * BAR_WIDTH);
|
|
56
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(BAR_WIDTH - filled);
|
|
57
|
+
|
|
58
|
+
let coloredBar;
|
|
59
|
+
if (used < 50) {
|
|
60
|
+
coloredBar = `${colors.green}${bar} ${used}%${colors.reset}`;
|
|
61
|
+
} else if (used < 65) {
|
|
62
|
+
coloredBar = `${colors.yellow}${bar} ${used}%${colors.reset}`;
|
|
63
|
+
} else if (used < 80) {
|
|
64
|
+
coloredBar = `${colors.orange}${bar} ${used}%${colors.reset}`;
|
|
65
|
+
} else {
|
|
66
|
+
coloredBar = `${colors.blink}${colors.red}${bar} ${used}%${colors.reset}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return coloredBar;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Render the usage bar from raw data. Called on every read (live or cached) so the
|
|
73
|
+
// reset countdown is always recomputed from resetsAt rather than frozen at fetch time.
|
|
74
|
+
function buildUsageBar(percentage, resetsAt) {
|
|
75
|
+
let timeStr = '';
|
|
76
|
+
if (resetsAt) {
|
|
77
|
+
const diffMins = Math.max(0, Math.floor((new Date(resetsAt) - new Date()) / 60000));
|
|
78
|
+
const days = Math.floor(diffMins / 1440);
|
|
79
|
+
const hours = Math.floor((diffMins % 1440) / 60);
|
|
80
|
+
const mins = diffMins % 60;
|
|
81
|
+
if (days > 0) timeStr = `${days}d${hours}h`;
|
|
82
|
+
else if (hours > 0) timeStr = `${hours}h${mins}m`;
|
|
83
|
+
else timeStr = `${mins}m`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const filledWidth = Math.max(0, Math.min(BAR_WIDTH, Math.round((percentage / 100) * BAR_WIDTH)));
|
|
87
|
+
const filled = '█'.repeat(filledWidth);
|
|
88
|
+
const empty = '░'.repeat(BAR_WIDTH - filledWidth);
|
|
89
|
+
const color = getUsageColor(percentage);
|
|
90
|
+
const timePart = timeStr ? `${colors.dim} (${timeStr})${colors.reset}` : '';
|
|
91
|
+
|
|
92
|
+
return `${color}${filled}${empty} ${percentage}%${colors.reset}${timePart}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build both usage bars from raw entries. Each entry is { percentage, resetsAt } or
|
|
96
|
+
// null/absent. Returns { current, weekly } where each is a rendered bar string or null.
|
|
97
|
+
function buildUsageBars(fiveHour, weekly) {
|
|
98
|
+
return {
|
|
99
|
+
current: fiveHour ? buildUsageBar(fiveHour.percentage, fiveHour.resetsAt) : null,
|
|
100
|
+
weekly: weekly ? buildUsageBar(weekly.percentage, weekly.resetsAt) : null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Normalize a raw API utilization into the 0-100 integer that the rest of the
|
|
105
|
+
// pipeline (cache validation + bar rendering) expects. Returns null when the value
|
|
106
|
+
// isn't a finite number, so callers can omit that bar instead of rendering "NaN%".
|
|
107
|
+
function normalizePercentage(value) {
|
|
108
|
+
if (!Number.isFinite(value)) return null;
|
|
109
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate a single usage entry ({ percentage, resetsAt }). Returns true only for a
|
|
113
|
+
// finite 0-100 percentage and a parseable (or absent) resetsAt.
|
|
114
|
+
function isValidUsageEntry(entry) {
|
|
115
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
116
|
+
if (!Number.isFinite(entry.percentage) || entry.percentage < 0 || entry.percentage > 100) return false;
|
|
117
|
+
if (entry.resetsAt != null && Number.isNaN(new Date(entry.resetsAt).getTime())) return false;
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Read the raw cached usage data
|
|
122
|
+
// ({ timestamp, data: { fiveHour: {percentage,resetsAt}, weekly: {...}|null } }).
|
|
123
|
+
// Returns { age, data } or null. Age-vs-TTL decisions are made by the caller.
|
|
124
|
+
function readCachedUsage() {
|
|
125
|
+
try {
|
|
126
|
+
if (!fs.existsSync(USAGE_CACHE_FILE)) return null;
|
|
127
|
+
|
|
128
|
+
const cache = JSON.parse(fs.readFileSync(USAGE_CACHE_FILE, 'utf8'));
|
|
129
|
+
if (!cache || !Number.isFinite(cache.timestamp) || cache.timestamp <= 0) return null;
|
|
130
|
+
|
|
131
|
+
// Validate data. fiveHour is required; weekly is optional (the API may omit it).
|
|
132
|
+
// This also rejects the legacy single-{percentage,resetsAt} format from older
|
|
133
|
+
// versions, which had no fiveHour key, so stale caches are ignored on read.
|
|
134
|
+
const data = cache.data;
|
|
135
|
+
if (!data || typeof data !== 'object') return null;
|
|
136
|
+
if (!isValidUsageEntry(data.fiveHour)) return null;
|
|
137
|
+
if (data.weekly != null && !isValidUsageEntry(data.weekly)) return null;
|
|
138
|
+
|
|
139
|
+
return { age: Date.now() - cache.timestamp, data };
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Write usage data to cache (shared across all sessions)
|
|
146
|
+
function setCachedUsage(data) {
|
|
147
|
+
try {
|
|
148
|
+
if (!fs.existsSync(CACHE_DIR)) {
|
|
149
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cache = {
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
data: data
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
fs.writeFileSync(USAGE_CACHE_FILE, JSON.stringify(cache), 'utf8');
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Silently fail
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getCredentials() {
|
|
164
|
+
// Try file first (legacy / Linux / Windows)
|
|
165
|
+
const credsPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
166
|
+
if (fs.existsSync(credsPath)) {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(fs.readFileSync(credsPath, 'utf8'));
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fallback: macOS keychain
|
|
173
|
+
if (os.platform() === 'darwin') {
|
|
174
|
+
try {
|
|
175
|
+
const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null', { encoding: 'utf8', timeout: 1000 });
|
|
176
|
+
return JSON.parse(raw.trim());
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getApiUsage(callback) {
|
|
184
|
+
try {
|
|
185
|
+
// Read credentials (file or macOS keychain)
|
|
186
|
+
const creds = getCredentials();
|
|
187
|
+
if (!creds) {
|
|
188
|
+
return callback(null);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const accessToken = creds.claudeAiOauth?.accessToken;
|
|
192
|
+
|
|
193
|
+
if (!accessToken) {
|
|
194
|
+
return callback(null);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Adaptive timeout: if cache exists, be faster (1200ms); if not, be patient (1500ms)
|
|
198
|
+
// API typically takes ~850ms, so 1200ms gives reasonable headroom
|
|
199
|
+
const hasCache = fs.existsSync(USAGE_CACHE_FILE);
|
|
200
|
+
const timeout = hasCache ? 1200 : 1500;
|
|
201
|
+
|
|
202
|
+
// Make API call with adaptive timeout
|
|
203
|
+
const req = https.request({
|
|
204
|
+
hostname: 'api.anthropic.com',
|
|
205
|
+
path: '/api/oauth/usage',
|
|
206
|
+
method: 'GET',
|
|
207
|
+
headers: {
|
|
208
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
'anthropic-beta': 'oauth-2025-04-20'
|
|
211
|
+
},
|
|
212
|
+
timeout: timeout
|
|
213
|
+
}, (res) => {
|
|
214
|
+
let data = '';
|
|
215
|
+
|
|
216
|
+
res.on('data', chunk => data += chunk);
|
|
217
|
+
res.on('end', () => {
|
|
218
|
+
try {
|
|
219
|
+
const usage = JSON.parse(data);
|
|
220
|
+
|
|
221
|
+
// 5-hour session usage is required; weekly (seven_day) is rendered when present.
|
|
222
|
+
// Normalize utilization first so a missing/non-finite value omits the bar
|
|
223
|
+
// instead of rendering "NaN%" or an out-of-range percentage.
|
|
224
|
+
const fivePct = usage.five_hour ? normalizePercentage(usage.five_hour.utilization) : null;
|
|
225
|
+
if (fivePct != null) {
|
|
226
|
+
const fiveHour = {
|
|
227
|
+
percentage: fivePct,
|
|
228
|
+
resetsAt: usage.five_hour.resets_at || null
|
|
229
|
+
};
|
|
230
|
+
const weeklyPct = usage.seven_day ? normalizePercentage(usage.seven_day.utilization) : null;
|
|
231
|
+
const weekly = weeklyPct != null ? {
|
|
232
|
+
percentage: weeklyPct,
|
|
233
|
+
resetsAt: usage.seven_day.resets_at || null
|
|
234
|
+
} : null;
|
|
235
|
+
|
|
236
|
+
// Cache the raw data (shared across sessions); render the bars from it.
|
|
237
|
+
setCachedUsage({ fiveHour, weekly });
|
|
238
|
+
callback(buildUsageBars(fiveHour, weekly));
|
|
239
|
+
} else {
|
|
240
|
+
callback(null);
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
callback(null);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
req.on('error', () => callback(null));
|
|
249
|
+
req.on('timeout', () => {
|
|
250
|
+
req.destroy();
|
|
251
|
+
callback(null);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
req.end();
|
|
255
|
+
} catch (e) {
|
|
256
|
+
callback(null);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Get usage, cache-first.
|
|
261
|
+
function getUsageWithCache(callback) {
|
|
262
|
+
const cached = readCachedUsage();
|
|
263
|
+
|
|
264
|
+
// Cache is fresh -> render it and skip the API entirely (fewer calls, faster).
|
|
265
|
+
if (cached && cached.age < FRESH_TTL_MS) {
|
|
266
|
+
return callback(buildUsageBars(cached.data.fiveHour, cached.data.weekly));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Cache is stale or missing -> refresh from the API.
|
|
270
|
+
getApiUsage((freshBars) => {
|
|
271
|
+
if (freshBars) {
|
|
272
|
+
callback(freshBars);
|
|
273
|
+
} else if (cached && cached.age < STALE_TTL_MS) {
|
|
274
|
+
// API failed/timed out, but recent cache exists -> show it instead of nothing.
|
|
275
|
+
callback(buildUsageBars(cached.data.fiveHour, cached.data.weekly));
|
|
276
|
+
} else {
|
|
277
|
+
callback(null);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getCurrentTask(sessionId) {
|
|
283
|
+
if (!sessionId) return '';
|
|
284
|
+
|
|
285
|
+
const homeDir = os.homedir();
|
|
286
|
+
const todosDir = path.join(homeDir, '.claude', 'todos');
|
|
287
|
+
|
|
288
|
+
if (!fs.existsSync(todosDir)) return '';
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const files = fs.readdirSync(todosDir)
|
|
292
|
+
.filter(f => f.startsWith(sessionId) && f.includes('-agent-') && f.endsWith('.json'))
|
|
293
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
|
|
294
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
295
|
+
|
|
296
|
+
if (files.length > 0) {
|
|
297
|
+
const todos = JSON.parse(fs.readFileSync(path.join(todosDir, files[0].name), 'utf8'));
|
|
298
|
+
const inProgress = todos.find(t => t.status === 'in_progress');
|
|
299
|
+
if (inProgress) return inProgress.activeForm || '';
|
|
300
|
+
}
|
|
301
|
+
} catch (e) {}
|
|
302
|
+
|
|
303
|
+
return '';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Main
|
|
307
|
+
function outputStatus(data, usage) {
|
|
308
|
+
try {
|
|
309
|
+
const model = shortenModel(data?.model?.display_name || 'Claude');
|
|
310
|
+
const dir = data?.workspace?.current_dir || process.cwd();
|
|
311
|
+
const dirname = path.basename(dir);
|
|
312
|
+
const sessionId = data?.session_id || '';
|
|
313
|
+
const remaining = data?.context_window?.remaining_percentage;
|
|
314
|
+
|
|
315
|
+
const contextBar = getContextBar(remaining);
|
|
316
|
+
const task = getCurrentTask(sessionId);
|
|
317
|
+
const parts = [];
|
|
318
|
+
parts.push(dirname);
|
|
319
|
+
parts.push(model);
|
|
320
|
+
parts.push(`CTX ${contextBar}`);
|
|
321
|
+
|
|
322
|
+
if (usage?.current) parts.push(`5h ${usage.current}`);
|
|
323
|
+
if (usage?.weekly) parts.push(`7d ${usage.weekly}`);
|
|
324
|
+
|
|
325
|
+
if (task) parts.push(`${colors.dim}${task}${colors.reset}`);
|
|
326
|
+
process.stdout.write(parts.join(' \u2502 '));
|
|
327
|
+
} catch (e) {
|
|
328
|
+
process.stdout.write('Status unavailable');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function outputFallback(usage) {
|
|
333
|
+
const contextBar = getContextBar(undefined);
|
|
334
|
+
const parts = ['~', 'Claude', `CTX ${contextBar}`];
|
|
335
|
+
if (usage?.current) parts.push(`5h ${usage.current}`);
|
|
336
|
+
if (usage?.weekly) parts.push(`7d ${usage.weekly}`);
|
|
337
|
+
process.stdout.write(parts.join(' \u2502 '));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Wrapper that skips usage fetch for API key users
|
|
341
|
+
function getUsage(callback) {
|
|
342
|
+
if (IS_API_KEY) {
|
|
343
|
+
callback(null);
|
|
344
|
+
} else {
|
|
345
|
+
getUsageWithCache(callback);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Process with timeout
|
|
350
|
+
if (process.stdin.isTTY) {
|
|
351
|
+
getUsage((usage) => {
|
|
352
|
+
outputFallback(usage);
|
|
353
|
+
process.exit(0);
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
let input = '';
|
|
357
|
+
let timeoutReached = false;
|
|
358
|
+
|
|
359
|
+
const overallTimeout = IS_API_KEY ? 500 : (fs.existsSync(USAGE_CACHE_FILE) ? 1300 : 1600);
|
|
360
|
+
|
|
361
|
+
const timeout = setTimeout(() => {
|
|
362
|
+
timeoutReached = true;
|
|
363
|
+
getUsage((usage) => {
|
|
364
|
+
if (input.length > 0) {
|
|
365
|
+
try {
|
|
366
|
+
const data = JSON.parse(input);
|
|
367
|
+
outputStatus(data, usage);
|
|
368
|
+
} catch (e) {
|
|
369
|
+
outputFallback(usage);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
outputFallback(usage);
|
|
373
|
+
}
|
|
374
|
+
process.exit(0);
|
|
375
|
+
});
|
|
376
|
+
}, overallTimeout);
|
|
377
|
+
|
|
378
|
+
process.stdin.setEncoding('utf8');
|
|
379
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
380
|
+
process.stdin.on('end', () => {
|
|
381
|
+
if (timeoutReached) return;
|
|
382
|
+
clearTimeout(timeout);
|
|
383
|
+
|
|
384
|
+
getUsage((usage) => {
|
|
385
|
+
try {
|
|
386
|
+
const data = JSON.parse(input);
|
|
387
|
+
outputStatus(data, usage);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
outputFallback(usage);
|
|
390
|
+
}
|
|
391
|
+
process.exit(0);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
}
|