@say8425/cc-statusline 2.0.1 → 2.1.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.js +325 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -48,6 +48,7 @@ Add the following to `~/.claude/settings.json`:
48
48
  - **Context**: Token usage with percentage (color-coded)
49
49
  - **Git Diff**: File count, insertions, deletions
50
50
  - **PR URL**: Clickable OSC 8 hyperlink
51
+ - **Worktree Support**: Shows real project name when running in a `cc --worktree` session
51
52
  - **TrueColor**: Dynamic colors based on thresholds
52
53
  - **Limit Reset Time**: Reset time display (HH:MM)
53
54
  - **Block Usage**: 5-hour utilization percentage (from server API)
@@ -59,6 +60,7 @@ Add the following to `~/.claude/settings.json`:
59
60
  | Emoji | Description |
60
61
  | ----- | ------------------------ |
61
62
  | 📁 | Project folder name |
63
+ | 🌲 | Worktree name (shown in worktree sessions) |
62
64
  | 🌿 | Current Git branch |
63
65
  | ⏱️ | Session elapsed time |
64
66
  | 💰 | Session cost in USD |
package/dist/index.js ADDED
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/cli.ts
5
+ function parseCliArgs(args) {
6
+ const showUsage = args.includes("--show-usage");
7
+ return { showUsage };
8
+ }
9
+
10
+ // src/git/branch.ts
11
+ var {$ } = globalThis.Bun;
12
+
13
+ // src/cache.ts
14
+ var cache = {
15
+ branch: { value: "", timestamp: 0 },
16
+ gitChanges: { files: 0, insertions: 0, deletions: 0, timestamp: 0 },
17
+ prUrl: { value: null, timestamp: 0 },
18
+ blockUsage: {
19
+ value: null,
20
+ timestamp: 0
21
+ },
22
+ accessToken: { value: null, timestamp: 0 },
23
+ mainProjectName: { value: null, timestamp: 0 }
24
+ };
25
+ var CACHE_TTL = {
26
+ branch: 5000,
27
+ gitChanges: 0,
28
+ prUrl: 30000,
29
+ blockUsage: 120000,
30
+ accessToken: 300000,
31
+ mainProjectName: 300000
32
+ };
33
+
34
+ // src/git/branch.ts
35
+ async function getBranchCached() {
36
+ if (Date.now() - cache.branch.timestamp < CACHE_TTL.branch) {
37
+ return cache.branch.value;
38
+ }
39
+ try {
40
+ const result = await $`git branch --show-current 2>/dev/null`.text();
41
+ cache.branch = { value: result.trim(), timestamp: Date.now() };
42
+ return cache.branch.value;
43
+ } catch {
44
+ return cache.branch.value;
45
+ }
46
+ }
47
+ // src/git/changes.ts
48
+ var {$: $2 } = globalThis.Bun;
49
+ async function getGitChangesCached() {
50
+ if (Date.now() - cache.gitChanges.timestamp < CACHE_TTL.gitChanges) {
51
+ return cache.gitChanges;
52
+ }
53
+ try {
54
+ const [diff, staged] = await Promise.all([
55
+ $2`git diff --shortstat 2>/dev/null`.text(),
56
+ $2`git diff --cached --shortstat 2>/dev/null`.text()
57
+ ]);
58
+ const combined = `${diff}
59
+ ${staged}`;
60
+ const [files, insertions, deletions] = [
61
+ /(\d+) files?/g,
62
+ /(\d+) insertions?/g,
63
+ /(\d+) deletions?/g
64
+ ].map((regex) => (combined.match(regex) || []).reduce((sum, m) => sum + Number.parseInt(m, 10), 0));
65
+ cache.gitChanges = { files, insertions, deletions, timestamp: Date.now() };
66
+ return cache.gitChanges;
67
+ } catch {
68
+ return cache.gitChanges;
69
+ }
70
+ }
71
+ // src/git/pr.ts
72
+ var {$: $3 } = globalThis.Bun;
73
+ async function getPrUrlCached() {
74
+ if (Date.now() - cache.prUrl.timestamp < CACHE_TTL.prUrl) {
75
+ return cache.prUrl.value;
76
+ }
77
+ try {
78
+ const result = await $3`gh pr view --json url -q .url 2>/dev/null`.text();
79
+ cache.prUrl = { value: result.trim() || null, timestamp: Date.now() };
80
+ return cache.prUrl.value;
81
+ } catch {
82
+ return cache.prUrl.value;
83
+ }
84
+ }
85
+ // src/git/worktree.ts
86
+ var {$: $4 } = globalThis.Bun;
87
+ async function getMainProjectNameCached() {
88
+ if (Date.now() - cache.mainProjectName.timestamp < CACHE_TTL.mainProjectName) {
89
+ return cache.mainProjectName.value;
90
+ }
91
+ try {
92
+ const result = await $4`git rev-parse --git-common-dir 2>/dev/null`.text();
93
+ const gitCommonDir = result.trim();
94
+ if (gitCommonDir === ".git") {
95
+ cache.mainProjectName = { value: null, timestamp: Date.now() };
96
+ return null;
97
+ }
98
+ const projectRoot = gitCommonDir.replace(/\/.git$/, "");
99
+ const projectName = projectRoot.split("/").pop() || null;
100
+ cache.mainProjectName = { value: projectName, timestamp: Date.now() };
101
+ return projectName;
102
+ } catch {
103
+ return cache.mainProjectName.value;
104
+ }
105
+ }
106
+ // src/colors.ts
107
+ var C = {
108
+ RESET: "\x1B[0m",
109
+ CYAN: "\x1B[38;2;0;255;255m",
110
+ MAGENTA: "\x1B[38;2;255;100;200m",
111
+ GREEN: "\x1B[38;2;100;255;100m",
112
+ YELLOW: "\x1B[38;2;255;220;100m",
113
+ RED: "\x1B[38;2;255;100;100m",
114
+ BLUE: "\x1B[38;2;100;150;255m",
115
+ WHITE: "\x1B[38;2;200;200;200m",
116
+ UNDERLINE: "\x1B[4m"
117
+ };
118
+ function getUsageColor(pct) {
119
+ if (pct < 50)
120
+ return C.WHITE;
121
+ if (pct < 80)
122
+ return C.YELLOW;
123
+ return C.RED;
124
+ }
125
+
126
+ // src/format/formatNumber.ts
127
+ function formatNumber(n) {
128
+ return n.toLocaleString("en-US");
129
+ }
130
+ // src/format/formatResetDate.ts
131
+ function formatResetDate(date) {
132
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
133
+ const day = date.getDate().toString().padStart(2, "0");
134
+ const hours = date.getHours().toString().padStart(2, "0");
135
+ const minutes = date.getMinutes().toString().padStart(2, "0");
136
+ return `${month}/${day} ${hours}:${minutes}`;
137
+ }
138
+ // src/format/formatTime.ts
139
+ function formatTime(hours, mins) {
140
+ return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
141
+ }
142
+ // src/render.ts
143
+ function renderStatusLine(ctx) {
144
+ const lines = [];
145
+ const folder = ctx.claudeJson.workspace?.project_dir?.split("/").pop() || "";
146
+ const sessionMs = ctx.claudeJson.cost?.total_duration_ms || 0;
147
+ const sessionSec = Math.floor(sessionMs / 1000);
148
+ const sessionHrs = Math.floor(sessionSec / 3600);
149
+ const sessionMins = Math.floor(sessionSec % 3600 / 60);
150
+ const costUsd = ctx.claudeJson.cost?.total_cost_usd || 0;
151
+ const usage = ctx.claudeJson.context_window?.current_usage;
152
+ const contextSize = ctx.claudeJson.context_window?.context_window_size || 200000;
153
+ const totalTokens = usage ? usage.input_tokens + usage.output_tokens + usage.cache_creation_input_tokens + usage.cache_read_input_tokens : 0;
154
+ const contextPct = Math.round(totalTokens / contextSize * 100);
155
+ const ctxColor = getUsageColor(contextPct);
156
+ let line1;
157
+ if (ctx.mainProjectName) {
158
+ line1 = `${C.WHITE}\uD83D\uDCC1 ${ctx.mainProjectName}${C.RESET} | ${C.WHITE}\uD83C\uDF32 ${folder}${C.RESET}`;
159
+ } else {
160
+ line1 = `${C.WHITE}\uD83D\uDCC1 ${folder}${C.RESET}`;
161
+ }
162
+ if (ctx.branch) {
163
+ line1 += ` | ${C.WHITE}\uD83C\uDF3F ${ctx.branch}${C.RESET}`;
164
+ }
165
+ lines.push(line1);
166
+ const line2 = `${C.WHITE}\u23F1\uFE0F ${formatTime(sessionHrs, sessionMins)}${C.RESET}` + ` | ${C.WHITE}\uD83D\uDCB0 $${costUsd.toFixed(2)}${C.RESET}` + ` | ${ctxColor}\uD83E\uDDE0 ${formatNumber(totalTokens)} (${contextPct}%)${C.RESET}`;
167
+ lines.push(line2);
168
+ if (ctx.showUsage && ctx.blockUsage) {
169
+ const parts = [];
170
+ if (ctx.blockUsage.resetTime) {
171
+ const h = ctx.blockUsage.resetTime.getHours();
172
+ const m = ctx.blockUsage.resetTime.getMinutes();
173
+ parts.push(`${C.WHITE}\u23F3 ${formatTime(h, m)}${C.RESET}`);
174
+ }
175
+ const usageColor = getUsageColor(ctx.blockUsage.utilization);
176
+ parts.push(`${usageColor}\uD83D\uDCCA ${Math.round(ctx.blockUsage.utilization)}/100${C.RESET}`);
177
+ if (ctx.blockUsage.sevenDayResetTime) {
178
+ parts.push(`${C.WHITE}\u23F0 ${formatResetDate(ctx.blockUsage.sevenDayResetTime)}${C.RESET}`);
179
+ }
180
+ if (ctx.blockUsage.sevenDayUtilization !== null) {
181
+ const weekColor = getUsageColor(ctx.blockUsage.sevenDayUtilization);
182
+ parts.push(`${weekColor}\uD83D\uDCC5 ${Math.round(ctx.blockUsage.sevenDayUtilization)}/100${C.RESET}`);
183
+ }
184
+ if (parts.length > 0) {
185
+ lines.push(parts.join(" | "));
186
+ }
187
+ }
188
+ const hasGitChanges = ctx.gitChanges.files > 0 || ctx.gitChanges.insertions > 0 || ctx.gitChanges.deletions > 0;
189
+ if (hasGitChanges || ctx.prUrl) {
190
+ let line4 = "";
191
+ if (hasGitChanges) {
192
+ line4 += `\u270F\uFE0F ${C.WHITE}${ctx.gitChanges.files} files${C.RESET} ${C.GREEN}+${ctx.gitChanges.insertions}${C.RESET} ${C.RED}-${ctx.gitChanges.deletions}${C.RESET}`;
193
+ }
194
+ if (ctx.prUrl) {
195
+ const prLabel = ctx.prUrl.replace(/^https?:\/\/[^/]+\//, "").replace("/pull/", "#");
196
+ if (line4)
197
+ line4 += " | ";
198
+ line4 += `\uD83D\uDCCE ${C.WHITE}${C.UNDERLINE}\x1B]8;;${ctx.prUrl}\x07${prLabel}\x1B]8;;\x07${C.RESET}`;
199
+ }
200
+ lines.push(line4);
201
+ }
202
+ return lines;
203
+ }
204
+
205
+ // src/stdin.ts
206
+ async function readStdin() {
207
+ const chunks = [];
208
+ for await (const chunk of Bun.stdin.stream()) {
209
+ chunks.push(chunk);
210
+ }
211
+ return Buffer.concat(chunks).toString("utf-8");
212
+ }
213
+
214
+ // src/usage/token.ts
215
+ var {$: $5 } = globalThis.Bun;
216
+ async function getAccessToken() {
217
+ if (process.platform !== "darwin")
218
+ return null;
219
+ try {
220
+ const result = await $5`security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null`.text();
221
+ const creds = JSON.parse(result);
222
+ return creds?.claudeAiOauth?.accessToken ?? null;
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+ async function getAccessTokenCached() {
228
+ if (Date.now() - cache.accessToken.timestamp < CACHE_TTL.accessToken) {
229
+ return cache.accessToken.value;
230
+ }
231
+ const token = await getAccessToken();
232
+ cache.accessToken = { value: token, timestamp: Date.now() };
233
+ return token;
234
+ }
235
+
236
+ // src/usage/api.ts
237
+ async function fetchUsageFromAPI(accessToken) {
238
+ try {
239
+ const controller = new AbortController;
240
+ const timeout = setTimeout(() => controller.abort(), 5000);
241
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
242
+ headers: {
243
+ Authorization: `Bearer ${accessToken}`,
244
+ "anthropic-beta": "oauth-2025-04-20"
245
+ },
246
+ signal: controller.signal
247
+ });
248
+ clearTimeout(timeout);
249
+ if (!response.ok)
250
+ return null;
251
+ return await response.json();
252
+ } catch {
253
+ return null;
254
+ }
255
+ }
256
+ function usageResponseToBlockUsage(data) {
257
+ const result = {
258
+ resetTime: null,
259
+ utilization: 0,
260
+ sevenDayUtilization: null,
261
+ sevenDayResetTime: null
262
+ };
263
+ if (data.five_hour) {
264
+ result.utilization = data.five_hour.utilization;
265
+ if (data.five_hour.resets_at) {
266
+ result.resetTime = new Date(data.five_hour.resets_at);
267
+ }
268
+ }
269
+ if (data.seven_day) {
270
+ result.sevenDayUtilization = data.seven_day.utilization;
271
+ if (data.seven_day.resets_at) {
272
+ result.sevenDayResetTime = new Date(data.seven_day.resets_at);
273
+ }
274
+ }
275
+ return result;
276
+ }
277
+ async function getUsageCached() {
278
+ if (Date.now() - cache.blockUsage.timestamp < CACHE_TTL.blockUsage) {
279
+ return cache.blockUsage.value;
280
+ }
281
+ const token = await getAccessTokenCached();
282
+ if (!token) {
283
+ cache.blockUsage = { value: null, timestamp: Date.now() };
284
+ return null;
285
+ }
286
+ const data = await fetchUsageFromAPI(token);
287
+ if (!data) {
288
+ cache.blockUsage = { value: null, timestamp: Date.now() };
289
+ return null;
290
+ }
291
+ const blockUsage = usageResponseToBlockUsage(data);
292
+ cache.blockUsage = { value: blockUsage, timestamp: Date.now() };
293
+ return blockUsage;
294
+ }
295
+ // src/index.ts
296
+ async function main(cliArgs) {
297
+ const args = cliArgs ?? process.argv.slice(2);
298
+ const { showUsage } = parseCliArgs(args);
299
+ const claudeJson = JSON.parse(await readStdin() || "{}");
300
+ const [branch, gitChanges, prUrl, blockUsage, mainProjectName] = await Promise.all([
301
+ getBranchCached(),
302
+ getGitChangesCached(),
303
+ getPrUrlCached(),
304
+ showUsage ? getUsageCached() : Promise.resolve(null),
305
+ getMainProjectNameCached()
306
+ ]);
307
+ const lines = renderStatusLine({
308
+ claudeJson,
309
+ branch,
310
+ gitChanges,
311
+ prUrl,
312
+ blockUsage,
313
+ showUsage,
314
+ mainProjectName
315
+ });
316
+ for (const line of lines) {
317
+ console.log(line);
318
+ }
319
+ }
320
+ if (import.meta.main) {
321
+ main().catch(console.error);
322
+ }
323
+ export {
324
+ main
325
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@say8425/cc-statusline",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/say8425/cc-statusline.git"