@owloops/claude-powerline 1.0.1 → 1.1.1
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 +131 -125
- package/dist/index.js +894 -237
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,263 +1,899 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { execSync as
|
|
8
|
-
import
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
import path2 from "path";
|
|
6
|
+
import fs2 from "fs";
|
|
7
|
+
import { execSync as execSync3 } from "child_process";
|
|
8
|
+
import os2 from "os";
|
|
9
9
|
import getStdin from "get-stdin";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
11
|
+
// src/lib/colors.ts
|
|
12
|
+
function hexToAnsi(hex, isBackground) {
|
|
13
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
14
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
15
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
16
|
+
return `\x1B[${isBackground ? "48" : "38"};2;${r};${g};${b}m`;
|
|
17
|
+
}
|
|
18
|
+
function extractBgToFg(ansiCode) {
|
|
19
|
+
const match = ansiCode.match(/48;2;(\d+);(\d+);(\d+)/);
|
|
20
|
+
if (match) {
|
|
21
|
+
return `\x1B[38;2;${match[1]};${match[2]};${match[3]}m`;
|
|
22
|
+
}
|
|
23
|
+
return ansiCode.replace("48", "38");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/lib/usage-provider.ts
|
|
13
27
|
import {
|
|
14
28
|
loadSessionUsageById,
|
|
15
29
|
loadDailyUsageData,
|
|
30
|
+
loadSessionBlockData,
|
|
16
31
|
getClaudePaths
|
|
17
32
|
} from "ccusage/data-loader";
|
|
18
|
-
import { calculateTotals } from "ccusage/calculate-cost";
|
|
33
|
+
import { calculateTotals, getTotalTokens } from "ccusage/calculate-cost";
|
|
19
34
|
import { logger } from "ccusage/logger";
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
dailyBg: "\x1B[48;2;135;206;235m",
|
|
33
|
-
dailyFg: "\x1B[30m",
|
|
34
|
-
blockBg: "\x1B[48;2;218;112;214m",
|
|
35
|
-
blockFg: "\x1B[97m",
|
|
36
|
-
burnLowBg: "\x1B[48;2;144;238;144m",
|
|
37
|
-
burnFg: "\x1B[97m"
|
|
38
|
-
},
|
|
39
|
-
dark: {
|
|
40
|
-
reset: "\x1B[0m",
|
|
41
|
-
modeBg: "\x1B[48;2;139;69;19m",
|
|
42
|
-
modeFg: "\x1B[97m",
|
|
43
|
-
sessionBg: "\x1B[48;2;64;64;64m",
|
|
44
|
-
sessionFg: "\x1B[97m",
|
|
45
|
-
dailyBg: "\x1B[48;2;45;45;45m",
|
|
46
|
-
dailyFg: "\x1B[97m",
|
|
47
|
-
blockBg: "\x1B[48;2;32;32;32m",
|
|
48
|
-
blockFg: "\x1B[96m",
|
|
49
|
-
burnLowBg: "\x1B[48;2;28;28;28m",
|
|
50
|
-
burnFg: "\x1B[97m"
|
|
35
|
+
var UsageProvider = class {
|
|
36
|
+
async getSessionBlockInfo() {
|
|
37
|
+
const originalLevel = logger.level;
|
|
38
|
+
logger.level = 0;
|
|
39
|
+
try {
|
|
40
|
+
const blocks = await loadSessionBlockData({
|
|
41
|
+
mode: "auto",
|
|
42
|
+
sessionDurationHours: 5
|
|
43
|
+
});
|
|
44
|
+
const activeBlock = blocks.find((block) => block.isActive);
|
|
45
|
+
if (!activeBlock) {
|
|
46
|
+
return null;
|
|
51
47
|
}
|
|
52
|
-
|
|
48
|
+
const now = /* @__PURE__ */ new Date();
|
|
49
|
+
const timeRemaining = Math.round(
|
|
50
|
+
(activeBlock.endTime.getTime() - now.getTime()) / (1e3 * 60)
|
|
51
|
+
);
|
|
52
|
+
const elapsed = Math.round(
|
|
53
|
+
(now.getTime() - activeBlock.startTime.getTime()) / (1e3 * 60)
|
|
54
|
+
);
|
|
55
|
+
const totalTokens = (activeBlock.tokenCounts?.inputTokens || 0) + (activeBlock.tokenCounts?.outputTokens || 0) + (activeBlock.tokenCounts?.cacheCreationInputTokens || 0) + (activeBlock.tokenCounts?.cacheReadInputTokens || 0);
|
|
56
|
+
const burnRate = elapsed > 0 ? activeBlock.costUSD / elapsed * 60 : null;
|
|
57
|
+
const tokenBurnRate = elapsed > 0 ? totalTokens / elapsed * 60 : null;
|
|
58
|
+
return {
|
|
59
|
+
cost: activeBlock.costUSD,
|
|
60
|
+
tokens: totalTokens,
|
|
61
|
+
timeRemaining: Math.max(0, timeRemaining),
|
|
62
|
+
burnRate,
|
|
63
|
+
tokenBurnRate,
|
|
64
|
+
isActive: true
|
|
65
|
+
};
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
} finally {
|
|
69
|
+
logger.level = originalLevel;
|
|
70
|
+
}
|
|
53
71
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
async getUsageInfo(sessionId) {
|
|
73
|
+
const originalLevel = logger.level;
|
|
74
|
+
logger.level = 0;
|
|
75
|
+
try {
|
|
76
|
+
const claudePaths = getClaudePaths();
|
|
77
|
+
if (claudePaths.length === 0) {
|
|
78
|
+
return {
|
|
79
|
+
session: { cost: null, tokens: null, tokenBreakdown: null },
|
|
80
|
+
daily: { cost: 0, tokens: 0, tokenBreakdown: null }
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const [sessionData, dailyData] = await Promise.all([
|
|
84
|
+
this.getSessionData(sessionId),
|
|
85
|
+
this.getDailyData()
|
|
86
|
+
]);
|
|
87
|
+
return {
|
|
88
|
+
session: sessionData,
|
|
89
|
+
daily: dailyData
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return {
|
|
93
|
+
session: { cost: null, tokens: null, tokenBreakdown: null },
|
|
94
|
+
daily: { cost: 0, tokens: 0, tokenBreakdown: null }
|
|
95
|
+
};
|
|
96
|
+
} finally {
|
|
97
|
+
logger.level = originalLevel;
|
|
98
|
+
}
|
|
67
99
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
async getSessionData(sessionId) {
|
|
101
|
+
try {
|
|
102
|
+
const sessionData = await loadSessionUsageById(sessionId, {
|
|
103
|
+
mode: "auto"
|
|
104
|
+
});
|
|
105
|
+
if (!sessionData) {
|
|
106
|
+
return { cost: null, tokens: null, tokenBreakdown: null };
|
|
107
|
+
}
|
|
108
|
+
const breakdown = sessionData.entries.reduce(
|
|
109
|
+
(acc, entry) => {
|
|
110
|
+
const usage = entry.message.usage;
|
|
111
|
+
return {
|
|
112
|
+
input: acc.input + usage.input_tokens,
|
|
113
|
+
output: acc.output + usage.output_tokens,
|
|
114
|
+
cacheCreation: acc.cacheCreation + (usage.cache_creation_input_tokens || 0),
|
|
115
|
+
cacheRead: acc.cacheRead + (usage.cache_read_input_tokens || 0)
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
{ input: 0, output: 0, cacheCreation: 0, cacheRead: 0 }
|
|
119
|
+
);
|
|
120
|
+
const totalTokens = breakdown.input + breakdown.output + breakdown.cacheCreation + breakdown.cacheRead;
|
|
121
|
+
return {
|
|
122
|
+
cost: sessionData.totalCost,
|
|
123
|
+
tokens: totalTokens,
|
|
124
|
+
tokenBreakdown: breakdown
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return { cost: null, tokens: null, tokenBreakdown: null };
|
|
72
128
|
}
|
|
73
|
-
return ansiCode.replace("48", "38");
|
|
74
129
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
130
|
+
async getDailyData() {
|
|
131
|
+
try {
|
|
132
|
+
const today = /* @__PURE__ */ new Date();
|
|
133
|
+
const todayStr = today.toISOString().split("T")[0]?.replace(/-/g, "") ?? "";
|
|
134
|
+
const dailyData = await loadDailyUsageData({
|
|
135
|
+
since: todayStr,
|
|
136
|
+
until: todayStr,
|
|
137
|
+
mode: "auto"
|
|
138
|
+
});
|
|
139
|
+
if (dailyData.length === 0) {
|
|
140
|
+
return { cost: 0, tokens: 0, tokenBreakdown: null };
|
|
141
|
+
}
|
|
142
|
+
const totals = calculateTotals(dailyData);
|
|
143
|
+
const breakdown = dailyData.reduce(
|
|
144
|
+
(acc, entry) => {
|
|
145
|
+
return {
|
|
146
|
+
input: acc.input + (entry.inputTokens || 0),
|
|
147
|
+
output: acc.output + (entry.outputTokens || 0),
|
|
148
|
+
cacheCreation: acc.cacheCreation + (entry.cacheCreationTokens || 0),
|
|
149
|
+
cacheRead: acc.cacheRead + (entry.cacheReadTokens || 0)
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
{ input: 0, output: 0, cacheCreation: 0, cacheRead: 0 }
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
cost: totals.totalCost,
|
|
156
|
+
tokens: getTotalTokens(totals),
|
|
157
|
+
tokenBreakdown: breakdown
|
|
158
|
+
};
|
|
159
|
+
} catch {
|
|
160
|
+
return { cost: 0, tokens: 0, tokenBreakdown: null };
|
|
83
161
|
}
|
|
84
|
-
return output;
|
|
85
162
|
}
|
|
86
|
-
|
|
87
|
-
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/lib/git-service.ts
|
|
166
|
+
import { execSync } from "child_process";
|
|
167
|
+
var GitService = class {
|
|
168
|
+
sanitizePath(path3) {
|
|
169
|
+
return path3.replace(/[;&|`$(){}[\]<>'"\\]/g, "");
|
|
88
170
|
}
|
|
89
|
-
getGitInfo(workingDir) {
|
|
171
|
+
getGitInfo(workingDir, showSha = false) {
|
|
90
172
|
try {
|
|
91
173
|
const sanitizedDir = this.sanitizePath(workingDir);
|
|
92
|
-
const branch =
|
|
93
|
-
|
|
174
|
+
const branch = this.getBranch(sanitizedDir);
|
|
175
|
+
const status = this.getStatus(sanitizedDir);
|
|
176
|
+
const { ahead, behind } = this.getAheadBehind(sanitizedDir);
|
|
177
|
+
const sha = showSha ? this.getSha(sanitizedDir) || void 0 : void 0;
|
|
178
|
+
return {
|
|
179
|
+
branch: branch || "detached",
|
|
180
|
+
status,
|
|
181
|
+
ahead,
|
|
182
|
+
behind,
|
|
183
|
+
sha
|
|
184
|
+
};
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
getBranch(workingDir) {
|
|
190
|
+
try {
|
|
191
|
+
return execSync("git branch --show-current 2>/dev/null", {
|
|
192
|
+
cwd: workingDir,
|
|
94
193
|
encoding: "utf8",
|
|
95
194
|
timeout: 1e3
|
|
96
|
-
}).trim();
|
|
195
|
+
}).trim() || null;
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
getStatus(workingDir) {
|
|
201
|
+
try {
|
|
97
202
|
const gitStatus = execSync("git status --porcelain 2>/dev/null", {
|
|
98
|
-
cwd:
|
|
203
|
+
cwd: workingDir,
|
|
99
204
|
encoding: "utf8",
|
|
100
205
|
timeout: 1e3
|
|
101
206
|
}).trim();
|
|
102
|
-
|
|
103
|
-
if (gitStatus) {
|
|
104
|
-
|
|
105
|
-
status = "conflicts";
|
|
106
|
-
} else {
|
|
107
|
-
status = "dirty";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
let ahead = 0, behind = 0;
|
|
111
|
-
try {
|
|
112
|
-
const aheadResult = execSync(
|
|
113
|
-
"git rev-list --count @{u}..HEAD 2>/dev/null",
|
|
114
|
-
{
|
|
115
|
-
cwd: sanitizedDir,
|
|
116
|
-
encoding: "utf8",
|
|
117
|
-
timeout: 1e3
|
|
118
|
-
}
|
|
119
|
-
).trim();
|
|
120
|
-
ahead = parseInt(aheadResult) || 0;
|
|
121
|
-
const behindResult = execSync(
|
|
122
|
-
"git rev-list --count HEAD..@{u} 2>/dev/null",
|
|
123
|
-
{
|
|
124
|
-
cwd: sanitizedDir,
|
|
125
|
-
encoding: "utf8",
|
|
126
|
-
timeout: 1e3
|
|
127
|
-
}
|
|
128
|
-
).trim();
|
|
129
|
-
behind = parseInt(behindResult) || 0;
|
|
130
|
-
} catch {
|
|
207
|
+
if (!gitStatus) return "clean";
|
|
208
|
+
if (gitStatus.includes("UU") || gitStatus.includes("AA") || gitStatus.includes("DD")) {
|
|
209
|
+
return "conflicts";
|
|
131
210
|
}
|
|
132
|
-
return
|
|
211
|
+
return "dirty";
|
|
133
212
|
} catch {
|
|
134
|
-
return
|
|
213
|
+
return "clean";
|
|
135
214
|
}
|
|
136
215
|
}
|
|
137
|
-
|
|
138
|
-
const originalLevel = logger.level;
|
|
139
|
-
logger.level = 0;
|
|
216
|
+
getAheadBehind(workingDir) {
|
|
140
217
|
try {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
const sessionData = await loadSessionUsageById(sessionId, {
|
|
149
|
-
mode: "auto"
|
|
150
|
-
});
|
|
151
|
-
if (sessionData != null) {
|
|
152
|
-
sessionCost = sessionData.totalCost;
|
|
218
|
+
const aheadResult = execSync(
|
|
219
|
+
"git rev-list --count @{u}..HEAD 2>/dev/null",
|
|
220
|
+
{
|
|
221
|
+
cwd: workingDir,
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
timeout: 1e3
|
|
153
224
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
since: todayStr,
|
|
162
|
-
until: todayStr,
|
|
163
|
-
mode: "auto"
|
|
164
|
-
});
|
|
165
|
-
if (dailyData.length > 0) {
|
|
166
|
-
const totals = calculateTotals(dailyData);
|
|
167
|
-
dailyCost = totals.totalCost;
|
|
225
|
+
).trim();
|
|
226
|
+
const behindResult = execSync(
|
|
227
|
+
"git rev-list --count HEAD..@{u} 2>/dev/null",
|
|
228
|
+
{
|
|
229
|
+
cwd: workingDir,
|
|
230
|
+
encoding: "utf8",
|
|
231
|
+
timeout: 1e3
|
|
168
232
|
}
|
|
169
|
-
|
|
233
|
+
).trim();
|
|
234
|
+
return {
|
|
235
|
+
ahead: parseInt(aheadResult) || 0,
|
|
236
|
+
behind: parseInt(behindResult) || 0
|
|
237
|
+
};
|
|
238
|
+
} catch {
|
|
239
|
+
return { ahead: 0, behind: 0 };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
getSha(workingDir) {
|
|
243
|
+
try {
|
|
244
|
+
const sha = execSync("git rev-parse --short=7 HEAD 2>/dev/null", {
|
|
245
|
+
cwd: workingDir,
|
|
246
|
+
encoding: "utf8",
|
|
247
|
+
timeout: 1e3
|
|
248
|
+
}).trim();
|
|
249
|
+
return sha || null;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/lib/tmux-service.ts
|
|
257
|
+
import { execSync as execSync2 } from "child_process";
|
|
258
|
+
var TmuxService = class {
|
|
259
|
+
getSessionId() {
|
|
260
|
+
try {
|
|
261
|
+
if (!process.env.TMUX_PANE) {
|
|
262
|
+
return null;
|
|
170
263
|
}
|
|
171
|
-
|
|
172
|
-
|
|
264
|
+
const sessionId = execSync2("tmux display-message -p '#S'", {
|
|
265
|
+
encoding: "utf8",
|
|
266
|
+
timeout: 1e3
|
|
267
|
+
}).trim();
|
|
268
|
+
return sessionId || null;
|
|
173
269
|
} catch {
|
|
174
|
-
|
|
175
|
-
return { sessionCost: null, dailyCost: 0 };
|
|
270
|
+
return null;
|
|
176
271
|
}
|
|
177
272
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (cost < 0.01) return "<$0.01";
|
|
181
|
-
return `$${cost.toFixed(2)}`;
|
|
273
|
+
isInTmux() {
|
|
274
|
+
return !!process.env.TMUX_PANE;
|
|
182
275
|
}
|
|
183
|
-
|
|
184
|
-
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/lib/formatters.ts
|
|
279
|
+
function formatCost(cost) {
|
|
280
|
+
if (cost === null) return "N/A";
|
|
281
|
+
if (cost < 0.01) return "<$0.01";
|
|
282
|
+
return `$${cost.toFixed(2)}`;
|
|
283
|
+
}
|
|
284
|
+
function formatTokens(tokens) {
|
|
285
|
+
if (tokens === null) return "N/A";
|
|
286
|
+
if (tokens === 0) return "0 tokens";
|
|
287
|
+
if (tokens >= 1e6) {
|
|
288
|
+
return `${(tokens / 1e6).toFixed(1)}M tokens`;
|
|
289
|
+
} else if (tokens >= 1e3) {
|
|
290
|
+
return `${(tokens / 1e3).toFixed(1)}K tokens`;
|
|
291
|
+
}
|
|
292
|
+
return `${tokens} tokens`;
|
|
293
|
+
}
|
|
294
|
+
function formatTokenBreakdown(breakdown) {
|
|
295
|
+
if (!breakdown) return "N/A";
|
|
296
|
+
const parts = [];
|
|
297
|
+
if (breakdown.input > 0) {
|
|
298
|
+
parts.push(`${formatTokens(breakdown.input).replace(" tokens", "")}in`);
|
|
299
|
+
}
|
|
300
|
+
if (breakdown.output > 0) {
|
|
301
|
+
parts.push(`${formatTokens(breakdown.output).replace(" tokens", "")}out`);
|
|
302
|
+
}
|
|
303
|
+
if (breakdown.cacheCreation > 0 || breakdown.cacheRead > 0) {
|
|
304
|
+
const totalCached = breakdown.cacheCreation + breakdown.cacheRead;
|
|
305
|
+
parts.push(`${formatTokens(totalCached).replace(" tokens", "")}cached`);
|
|
306
|
+
}
|
|
307
|
+
return parts.length > 0 ? parts.join(" + ") : "0 tokens";
|
|
308
|
+
}
|
|
309
|
+
function formatTimeRemaining(minutes) {
|
|
310
|
+
if (minutes <= 0) return "0m";
|
|
311
|
+
const hours = Math.floor(minutes / 60);
|
|
312
|
+
const mins = minutes % 60;
|
|
313
|
+
if (hours > 0) {
|
|
314
|
+
return `${hours}h${mins > 0 ? ` ${mins}m` : ""}`;
|
|
315
|
+
}
|
|
316
|
+
return `${mins}m`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/lib/budget.ts
|
|
320
|
+
function calculateBudgetPercentage(cost, budget) {
|
|
321
|
+
if (!budget || budget <= 0 || cost < 0) return null;
|
|
322
|
+
return Math.min(100, cost / budget * 100);
|
|
323
|
+
}
|
|
324
|
+
function getBudgetStatus(cost, budget, warningThreshold = 80) {
|
|
325
|
+
const percentage = calculateBudgetPercentage(cost, budget);
|
|
326
|
+
if (percentage === null) {
|
|
327
|
+
return {
|
|
328
|
+
percentage: null,
|
|
329
|
+
isWarning: false,
|
|
330
|
+
displayText: ""
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const percentStr = `${percentage.toFixed(0)}%`;
|
|
334
|
+
const isWarning = percentage >= warningThreshold;
|
|
335
|
+
let displayText = "";
|
|
336
|
+
if (isWarning) {
|
|
337
|
+
displayText = ` !${percentStr}`;
|
|
338
|
+
} else if (percentage >= 50) {
|
|
339
|
+
displayText = ` +${percentStr}`;
|
|
340
|
+
} else {
|
|
341
|
+
displayText = ` ${percentStr}`;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
percentage,
|
|
345
|
+
isWarning,
|
|
346
|
+
displayText
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/lib/segment-renderer.ts
|
|
351
|
+
var SegmentRenderer = class {
|
|
352
|
+
constructor(config, symbols) {
|
|
353
|
+
this.config = config;
|
|
354
|
+
this.symbols = symbols;
|
|
355
|
+
}
|
|
356
|
+
renderDirectory(hookData, colors) {
|
|
185
357
|
const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
|
|
186
358
|
const projectDir = hookData.workspace?.project_dir;
|
|
187
|
-
|
|
359
|
+
const dirName = this.getDisplayDirectoryName(currentDir, projectDir);
|
|
360
|
+
return {
|
|
361
|
+
text: dirName,
|
|
362
|
+
bgColor: colors.modeBg,
|
|
363
|
+
fgColor: colors.modeFg
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
renderGit(gitInfo, colors, showSha = false) {
|
|
367
|
+
if (!gitInfo) return null;
|
|
368
|
+
let gitStatusIcon = this.symbols.git_clean;
|
|
369
|
+
if (gitInfo.status === "conflicts") {
|
|
370
|
+
gitStatusIcon = this.symbols.git_conflicts;
|
|
371
|
+
} else if (gitInfo.status === "dirty") {
|
|
372
|
+
gitStatusIcon = this.symbols.git_dirty;
|
|
373
|
+
}
|
|
374
|
+
let text = `${this.symbols.branch} ${gitInfo.branch} ${gitStatusIcon}`;
|
|
375
|
+
if (gitInfo.sha && showSha) {
|
|
376
|
+
text += ` ${gitInfo.sha}`;
|
|
377
|
+
}
|
|
378
|
+
if (gitInfo.ahead > 0 && gitInfo.behind > 0) {
|
|
379
|
+
text += ` ${this.symbols.git_ahead}${gitInfo.ahead}${this.symbols.git_behind}${gitInfo.behind}`;
|
|
380
|
+
} else if (gitInfo.ahead > 0) {
|
|
381
|
+
text += ` ${this.symbols.git_ahead}${gitInfo.ahead}`;
|
|
382
|
+
} else if (gitInfo.behind > 0) {
|
|
383
|
+
text += ` ${this.symbols.git_behind}${gitInfo.behind}`;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
text,
|
|
387
|
+
bgColor: colors.gitBg,
|
|
388
|
+
fgColor: colors.gitFg
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
renderModel(hookData, colors) {
|
|
392
|
+
const modelName = hookData.model?.display_name || "Claude";
|
|
393
|
+
return {
|
|
394
|
+
text: `${this.symbols.model} ${modelName}`,
|
|
395
|
+
bgColor: colors.todayBg,
|
|
396
|
+
fgColor: colors.todayFg
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
renderSession(usageInfo, colors, type = "cost") {
|
|
400
|
+
const sessionBudget = this.config.budget?.session;
|
|
401
|
+
const text = `${this.symbols.session_cost} ${this.formatUsageWithBudget(
|
|
402
|
+
usageInfo.session.cost,
|
|
403
|
+
usageInfo.session.tokens,
|
|
404
|
+
usageInfo.session.tokenBreakdown,
|
|
405
|
+
type,
|
|
406
|
+
sessionBudget?.amount,
|
|
407
|
+
sessionBudget?.warningThreshold || 80
|
|
408
|
+
)}`;
|
|
409
|
+
return {
|
|
410
|
+
text,
|
|
411
|
+
bgColor: colors.sessionBg,
|
|
412
|
+
fgColor: colors.sessionFg
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
renderToday(usageInfo, colors, type = "cost") {
|
|
416
|
+
const todayBudget = this.config.budget?.today;
|
|
417
|
+
const text = `Today ${this.formatUsageWithBudget(
|
|
418
|
+
usageInfo.daily.cost,
|
|
419
|
+
usageInfo.daily.tokens,
|
|
420
|
+
usageInfo.daily.tokenBreakdown,
|
|
421
|
+
type,
|
|
422
|
+
todayBudget?.amount,
|
|
423
|
+
todayBudget?.warningThreshold || 80
|
|
424
|
+
)}`;
|
|
425
|
+
return {
|
|
426
|
+
text,
|
|
427
|
+
bgColor: colors.burnLowBg,
|
|
428
|
+
fgColor: colors.burnFg
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
renderBlock(blockInfo, colors, type = "cost") {
|
|
432
|
+
if (!blockInfo) return null;
|
|
433
|
+
const text = `${this.symbols.block_cost} ${this.formatSessionBlockInfo(blockInfo, type)}`;
|
|
434
|
+
return {
|
|
435
|
+
text,
|
|
436
|
+
bgColor: colors.blockBg,
|
|
437
|
+
fgColor: colors.blockFg
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
renderTmux(sessionId, colors) {
|
|
441
|
+
if (!sessionId) return null;
|
|
442
|
+
return {
|
|
443
|
+
text: `tmux:${sessionId}`,
|
|
444
|
+
bgColor: colors.tmuxBg,
|
|
445
|
+
fgColor: colors.tmuxFg
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
getDisplayDirectoryName(currentDir, projectDir) {
|
|
188
449
|
if (projectDir && projectDir !== currentDir) {
|
|
189
450
|
const projectName = projectDir.split("/").pop() || "project";
|
|
190
451
|
const currentDirName = currentDir.split("/").pop() || "root";
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
452
|
+
if (currentDir.includes(projectDir)) {
|
|
453
|
+
return `${projectName}/${currentDirName}`;
|
|
454
|
+
}
|
|
455
|
+
return currentDirName;
|
|
456
|
+
}
|
|
457
|
+
return currentDir.split("/").pop() || "root";
|
|
458
|
+
}
|
|
459
|
+
formatUsageDisplay(cost, tokens, tokenBreakdown, type) {
|
|
460
|
+
switch (type) {
|
|
461
|
+
case "cost":
|
|
462
|
+
return formatCost(cost);
|
|
463
|
+
case "tokens":
|
|
464
|
+
return formatTokens(tokens);
|
|
465
|
+
case "both":
|
|
466
|
+
return `${formatCost(cost)} (${formatTokens(tokens)})`;
|
|
467
|
+
case "breakdown":
|
|
468
|
+
return formatTokenBreakdown(tokenBreakdown);
|
|
469
|
+
default:
|
|
470
|
+
return formatCost(cost);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
formatUsageWithBudget(cost, tokens, tokenBreakdown, type, budget, warningThreshold = 80) {
|
|
474
|
+
const baseDisplay = this.formatUsageDisplay(
|
|
475
|
+
cost,
|
|
476
|
+
tokens,
|
|
477
|
+
tokenBreakdown,
|
|
478
|
+
type
|
|
205
479
|
);
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
480
|
+
if (budget && budget > 0 && cost !== null) {
|
|
481
|
+
const budgetStatus = getBudgetStatus(cost, budget, warningThreshold);
|
|
482
|
+
return baseDisplay + budgetStatus.displayText;
|
|
483
|
+
}
|
|
484
|
+
return baseDisplay;
|
|
485
|
+
}
|
|
486
|
+
formatSessionBlockInfo(blockInfo, type = "cost") {
|
|
487
|
+
if (!blockInfo.isActive) {
|
|
488
|
+
return "No active block";
|
|
489
|
+
}
|
|
490
|
+
const timeStr = formatTimeRemaining(blockInfo.timeRemaining);
|
|
491
|
+
if (type === "tokens") {
|
|
492
|
+
const tokensStr = formatTokens(blockInfo.tokens);
|
|
493
|
+
let result = `${tokensStr} (${timeStr} left)`;
|
|
494
|
+
if (blockInfo.tokenBurnRate !== null && blockInfo.tokenBurnRate > 0) {
|
|
495
|
+
const burnRateStr = `${formatTokens(blockInfo.tokenBurnRate)}/hr`;
|
|
496
|
+
result += ` ${burnRateStr}`;
|
|
212
497
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
498
|
+
return result;
|
|
499
|
+
} else {
|
|
500
|
+
const costStr = formatCost(blockInfo.cost);
|
|
501
|
+
let result = `${costStr} (${timeStr} left)`;
|
|
502
|
+
if (blockInfo.burnRate !== null && blockInfo.burnRate > 0) {
|
|
503
|
+
const burnRateStr = `${formatCost(blockInfo.burnRate)}/hr`;
|
|
504
|
+
result += ` ${burnRateStr}`;
|
|
220
505
|
}
|
|
221
|
-
|
|
222
|
-
colors.sessionBg,
|
|
223
|
-
colors.sessionFg,
|
|
224
|
-
branchText,
|
|
225
|
-
colors.dailyBg
|
|
226
|
-
);
|
|
506
|
+
return result;
|
|
227
507
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/powerline.ts
|
|
512
|
+
var PowerlineRenderer = class {
|
|
513
|
+
constructor(config) {
|
|
514
|
+
this.config = config;
|
|
515
|
+
this.symbols = this.initializeSymbols();
|
|
516
|
+
this.usageProvider = new UsageProvider();
|
|
517
|
+
this.gitService = new GitService();
|
|
518
|
+
this.tmuxService = new TmuxService();
|
|
519
|
+
this.segmentRenderer = new SegmentRenderer(config, this.symbols);
|
|
520
|
+
}
|
|
521
|
+
symbols;
|
|
522
|
+
usageProvider;
|
|
523
|
+
gitService;
|
|
524
|
+
tmuxService;
|
|
525
|
+
segmentRenderer;
|
|
526
|
+
async generateStatusline(hookData) {
|
|
527
|
+
const usageInfo = await this.usageProvider.getUsageInfo(
|
|
528
|
+
hookData.session_id
|
|
239
529
|
);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
530
|
+
let sessionBlockInfo = null;
|
|
531
|
+
if (this.needsSessionBlock()) {
|
|
532
|
+
sessionBlockInfo = await this.usageProvider.getSessionBlockInfo();
|
|
533
|
+
}
|
|
534
|
+
const lines = this.config.display.lines.map(
|
|
535
|
+
(lineConfig) => this.renderLine(lineConfig, hookData, usageInfo, sessionBlockInfo)
|
|
536
|
+
).filter((line) => line.length > 0);
|
|
537
|
+
return lines.join("\n");
|
|
538
|
+
}
|
|
539
|
+
needsSessionBlock() {
|
|
540
|
+
return this.config.display.lines.some(
|
|
541
|
+
(line) => line.segments.block?.enabled
|
|
244
542
|
);
|
|
245
|
-
|
|
543
|
+
}
|
|
544
|
+
renderLine(lineConfig, hookData, usageInfo, sessionBlockInfo) {
|
|
545
|
+
const colors = this.getThemeColors();
|
|
546
|
+
const currentDir = hookData.workspace?.current_dir || hookData.cwd || "/";
|
|
547
|
+
const segments = Object.entries(lineConfig.segments).filter(([_, config]) => config?.enabled).map(([type, config]) => ({ type, config }));
|
|
548
|
+
let line = "";
|
|
549
|
+
for (let i = 0; i < segments.length; i++) {
|
|
550
|
+
const segment = segments[i];
|
|
551
|
+
if (!segment) continue;
|
|
552
|
+
const isLast = i === segments.length - 1;
|
|
553
|
+
const nextSegment = !isLast ? segments[i + 1] : null;
|
|
554
|
+
const nextBgColor = nextSegment ? this.getSegmentBgColor(nextSegment.type, colors) : "";
|
|
555
|
+
const segmentData = this.renderSegment(
|
|
556
|
+
segment,
|
|
557
|
+
hookData,
|
|
558
|
+
usageInfo,
|
|
559
|
+
sessionBlockInfo,
|
|
560
|
+
colors,
|
|
561
|
+
currentDir
|
|
562
|
+
);
|
|
563
|
+
if (segmentData) {
|
|
564
|
+
line += this.formatSegment(
|
|
565
|
+
segmentData.bgColor,
|
|
566
|
+
segmentData.fgColor,
|
|
567
|
+
segmentData.text,
|
|
568
|
+
isLast ? void 0 : nextBgColor
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return line;
|
|
573
|
+
}
|
|
574
|
+
renderSegment(segment, hookData, usageInfo, sessionBlockInfo, colors, currentDir) {
|
|
575
|
+
switch (segment.type) {
|
|
576
|
+
case "directory":
|
|
577
|
+
return this.segmentRenderer.renderDirectory(hookData, colors);
|
|
578
|
+
case "git":
|
|
579
|
+
const showSha = segment.config?.showSha || false;
|
|
580
|
+
const gitInfo = this.gitService.getGitInfo(currentDir, showSha);
|
|
581
|
+
return gitInfo ? this.segmentRenderer.renderGit(gitInfo, colors, showSha) : null;
|
|
582
|
+
case "model":
|
|
583
|
+
return this.segmentRenderer.renderModel(hookData, colors);
|
|
584
|
+
case "session":
|
|
585
|
+
const usageType = segment.config?.type || "cost";
|
|
586
|
+
return this.segmentRenderer.renderSession(usageInfo, colors, usageType);
|
|
587
|
+
case "today":
|
|
588
|
+
const todayType = segment.config?.type || "cost";
|
|
589
|
+
return this.segmentRenderer.renderToday(usageInfo, colors, todayType);
|
|
590
|
+
case "block":
|
|
591
|
+
const blockType = segment.config?.type || "cost";
|
|
592
|
+
return this.segmentRenderer.renderBlock(
|
|
593
|
+
sessionBlockInfo,
|
|
594
|
+
colors,
|
|
595
|
+
blockType
|
|
596
|
+
);
|
|
597
|
+
case "tmux":
|
|
598
|
+
const tmuxSessionId = this.tmuxService.getSessionId();
|
|
599
|
+
return this.segmentRenderer.renderTmux(tmuxSessionId, colors);
|
|
600
|
+
default:
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
initializeSymbols() {
|
|
605
|
+
return {
|
|
606
|
+
right: "\uE0B0",
|
|
607
|
+
branch: "\uE0A0",
|
|
608
|
+
model: "\u26A1",
|
|
609
|
+
git_clean: "\u2713",
|
|
610
|
+
git_dirty: "\u25CF",
|
|
611
|
+
git_conflicts: "\u26A0",
|
|
612
|
+
git_ahead: "\u2191",
|
|
613
|
+
git_behind: "\u2193",
|
|
614
|
+
session_cost: "Session",
|
|
615
|
+
daily_cost: "Today",
|
|
616
|
+
block_cost: "Block"
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
getThemeColors() {
|
|
620
|
+
const theme = this.config.theme;
|
|
621
|
+
const colorTheme = this.config.colors[theme];
|
|
622
|
+
if (!colorTheme) {
|
|
623
|
+
throw new Error(`Theme '${theme}' not found in configuration`);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
reset: "\x1B[0m",
|
|
627
|
+
modeBg: hexToAnsi(colorTheme.directory.bg, true),
|
|
628
|
+
modeFg: hexToAnsi(colorTheme.directory.fg, false),
|
|
629
|
+
gitBg: hexToAnsi(colorTheme.git.bg, true),
|
|
630
|
+
gitFg: hexToAnsi(colorTheme.git.fg, false),
|
|
631
|
+
sessionBg: hexToAnsi(colorTheme.session.bg, true),
|
|
632
|
+
sessionFg: hexToAnsi(colorTheme.session.fg, false),
|
|
633
|
+
todayBg: hexToAnsi(colorTheme.today.bg, true),
|
|
634
|
+
todayFg: hexToAnsi(colorTheme.today.fg, false),
|
|
635
|
+
blockBg: hexToAnsi(colorTheme.block.bg, true),
|
|
636
|
+
blockFg: hexToAnsi(colorTheme.block.fg, false),
|
|
637
|
+
burnLowBg: hexToAnsi(colorTheme.today.bg, true),
|
|
638
|
+
burnFg: hexToAnsi(colorTheme.today.fg, false),
|
|
639
|
+
tmuxBg: hexToAnsi(colorTheme.tmux.bg, true),
|
|
640
|
+
tmuxFg: hexToAnsi(colorTheme.tmux.fg, false)
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
getSegmentBgColor(segmentType, colors) {
|
|
644
|
+
switch (segmentType) {
|
|
645
|
+
case "directory":
|
|
646
|
+
return colors.modeBg;
|
|
647
|
+
case "git":
|
|
648
|
+
return colors.gitBg;
|
|
649
|
+
case "model":
|
|
650
|
+
return colors.todayBg;
|
|
651
|
+
case "session":
|
|
652
|
+
return colors.sessionBg;
|
|
653
|
+
case "today":
|
|
654
|
+
return colors.burnLowBg;
|
|
655
|
+
case "block":
|
|
656
|
+
return colors.blockBg;
|
|
657
|
+
case "tmux":
|
|
658
|
+
return colors.tmuxBg;
|
|
659
|
+
default:
|
|
660
|
+
return colors.modeBg;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
formatSegment(bgColor, fgColor, text, nextBgColor) {
|
|
664
|
+
let output = `${bgColor}${fgColor} ${text} `;
|
|
665
|
+
if (nextBgColor) {
|
|
666
|
+
const arrowFgColor = extractBgToFg(bgColor);
|
|
667
|
+
output += `${nextBgColor}${arrowFgColor}${this.symbols.right}`;
|
|
668
|
+
} else {
|
|
669
|
+
const arrowFgColor = extractBgToFg(bgColor);
|
|
670
|
+
output += `\x1B[0m${arrowFgColor}${this.symbols.right}\x1B[0m`;
|
|
671
|
+
}
|
|
672
|
+
return output;
|
|
246
673
|
}
|
|
247
674
|
};
|
|
248
675
|
|
|
676
|
+
// src/config/loader.ts
|
|
677
|
+
import fs from "fs";
|
|
678
|
+
import path from "path";
|
|
679
|
+
import os from "os";
|
|
680
|
+
|
|
681
|
+
// src/config/defaults.ts
|
|
682
|
+
var DEFAULT_CONFIG = {
|
|
683
|
+
theme: "dark",
|
|
684
|
+
display: {
|
|
685
|
+
lines: [
|
|
686
|
+
{
|
|
687
|
+
segments: {
|
|
688
|
+
directory: { enabled: true },
|
|
689
|
+
git: {
|
|
690
|
+
enabled: true,
|
|
691
|
+
showSha: false
|
|
692
|
+
},
|
|
693
|
+
model: { enabled: true },
|
|
694
|
+
session: { enabled: true, type: "tokens" },
|
|
695
|
+
today: { enabled: true, type: "both" },
|
|
696
|
+
block: { enabled: false, type: "cost" },
|
|
697
|
+
tmux: { enabled: false }
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
]
|
|
701
|
+
},
|
|
702
|
+
colors: {
|
|
703
|
+
light: {
|
|
704
|
+
directory: { bg: "#ff6b47", fg: "#ffffff" },
|
|
705
|
+
git: { bg: "#4fb3d9", fg: "#ffffff" },
|
|
706
|
+
model: { bg: "#87ceeb", fg: "#000000" },
|
|
707
|
+
session: { bg: "#da70d6", fg: "#ffffff" },
|
|
708
|
+
today: { bg: "#90ee90", fg: "#ffffff" },
|
|
709
|
+
block: { bg: "#ff8c00", fg: "#ffffff" },
|
|
710
|
+
tmux: { bg: "#32cd32", fg: "#ffffff" }
|
|
711
|
+
},
|
|
712
|
+
dark: {
|
|
713
|
+
directory: { bg: "#8b4513", fg: "#ffffff" },
|
|
714
|
+
git: { bg: "#404040", fg: "#ffffff" },
|
|
715
|
+
model: { bg: "#2d2d2d", fg: "#ffffff" },
|
|
716
|
+
session: { bg: "#202020", fg: "#00ffff" },
|
|
717
|
+
today: { bg: "#1c1c1c", fg: "#ffffff" },
|
|
718
|
+
block: { bg: "#8b4500", fg: "#ffffff" },
|
|
719
|
+
tmux: { bg: "#2f4f2f", fg: "#90ee90" }
|
|
720
|
+
},
|
|
721
|
+
custom: {
|
|
722
|
+
directory: { bg: "#ff6600", fg: "#ffffff" },
|
|
723
|
+
git: { bg: "#0066cc", fg: "#ffffff" },
|
|
724
|
+
model: { bg: "#9900cc", fg: "#ffffff" },
|
|
725
|
+
session: { bg: "#cc0099", fg: "#ffffff" },
|
|
726
|
+
today: { bg: "#00cc66", fg: "#000000" },
|
|
727
|
+
block: { bg: "#cc6600", fg: "#ffffff" },
|
|
728
|
+
tmux: { bg: "#228b22", fg: "#ffffff" }
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
budget: {
|
|
732
|
+
session: {
|
|
733
|
+
warningThreshold: 80
|
|
734
|
+
},
|
|
735
|
+
today: {
|
|
736
|
+
amount: 50,
|
|
737
|
+
warningThreshold: 80
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/config/loader.ts
|
|
743
|
+
function deepMerge(target, source) {
|
|
744
|
+
const result = { ...target };
|
|
745
|
+
for (const key in source) {
|
|
746
|
+
if (source[key] !== void 0) {
|
|
747
|
+
if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
|
|
748
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
749
|
+
} else {
|
|
750
|
+
result[key] = source[key];
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
function findConfigFile(customPath, projectDir) {
|
|
757
|
+
if (customPath) {
|
|
758
|
+
return fs.existsSync(customPath) ? customPath : null;
|
|
759
|
+
}
|
|
760
|
+
const locations = [
|
|
761
|
+
...projectDir ? [path.join(projectDir, ".claude-powerline.json")] : [],
|
|
762
|
+
path.join(process.cwd(), ".claude-powerline.json"),
|
|
763
|
+
path.join(os.homedir(), ".claude", "claude-powerline.json"),
|
|
764
|
+
path.join(os.homedir(), ".config", "claude-powerline", "config.json")
|
|
765
|
+
];
|
|
766
|
+
return locations.find(fs.existsSync) || null;
|
|
767
|
+
}
|
|
768
|
+
function loadConfigFile(filePath) {
|
|
769
|
+
try {
|
|
770
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
771
|
+
return JSON.parse(content);
|
|
772
|
+
} catch (error) {
|
|
773
|
+
throw new Error(
|
|
774
|
+
`Failed to load config file ${filePath}: ${error instanceof Error ? error.message : String(error)}`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function loadEnvConfig() {
|
|
779
|
+
const config = {};
|
|
780
|
+
if (process.env.CLAUDE_POWERLINE_THEME) {
|
|
781
|
+
config.theme = process.env.CLAUDE_POWERLINE_THEME;
|
|
782
|
+
}
|
|
783
|
+
if (process.env.CLAUDE_POWERLINE_USAGE_TYPE) {
|
|
784
|
+
const usageType = process.env.CLAUDE_POWERLINE_USAGE_TYPE;
|
|
785
|
+
config.display = config.display || { lines: [] };
|
|
786
|
+
if (config.display.lines.length === 0) {
|
|
787
|
+
config.display.lines = [{ segments: {} }];
|
|
788
|
+
}
|
|
789
|
+
config.display.lines.forEach((line) => {
|
|
790
|
+
if (line.segments.session) {
|
|
791
|
+
line.segments.session.type = usageType;
|
|
792
|
+
}
|
|
793
|
+
if (line.segments.today) {
|
|
794
|
+
line.segments.today.type = usageType;
|
|
795
|
+
}
|
|
796
|
+
if (line.segments.block) {
|
|
797
|
+
line.segments.block.type = usageType === "breakdown" || usageType === "both" ? "cost" : usageType;
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
return config;
|
|
802
|
+
}
|
|
803
|
+
function getConfigPathFromEnv() {
|
|
804
|
+
return process.env.CLAUDE_POWERLINE_CONFIG;
|
|
805
|
+
}
|
|
806
|
+
function parseCLIOverrides(args) {
|
|
807
|
+
const config = {};
|
|
808
|
+
const themeIndex = args.findIndex((arg) => arg.startsWith("--theme="));
|
|
809
|
+
if (themeIndex !== -1) {
|
|
810
|
+
const theme = args[themeIndex]?.split("=")[1];
|
|
811
|
+
if (theme) {
|
|
812
|
+
config.theme = theme;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const dailyBudgetIndex = args.findIndex(
|
|
816
|
+
(arg) => arg.startsWith("--daily-budget=")
|
|
817
|
+
);
|
|
818
|
+
if (dailyBudgetIndex !== -1) {
|
|
819
|
+
const dailyBudget = parseFloat(args[dailyBudgetIndex]?.split("=")[1] || "");
|
|
820
|
+
if (!isNaN(dailyBudget) && dailyBudget > 0) {
|
|
821
|
+
config.budget = {
|
|
822
|
+
...config.budget,
|
|
823
|
+
today: {
|
|
824
|
+
...DEFAULT_CONFIG.budget?.today,
|
|
825
|
+
amount: dailyBudget
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const sessionBudgetIndex = args.findIndex(
|
|
831
|
+
(arg) => arg.startsWith("--session-budget=")
|
|
832
|
+
);
|
|
833
|
+
if (sessionBudgetIndex !== -1) {
|
|
834
|
+
const sessionBudget = parseFloat(
|
|
835
|
+
args[sessionBudgetIndex]?.split("=")[1] || ""
|
|
836
|
+
);
|
|
837
|
+
if (!isNaN(sessionBudget) && sessionBudget > 0) {
|
|
838
|
+
config.budget = {
|
|
839
|
+
...config.budget,
|
|
840
|
+
session: {
|
|
841
|
+
...DEFAULT_CONFIG.budget?.session,
|
|
842
|
+
amount: sessionBudget
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return config;
|
|
848
|
+
}
|
|
849
|
+
function loadConfig(options = {}) {
|
|
850
|
+
const {
|
|
851
|
+
configPath,
|
|
852
|
+
ignoreEnvVars = false,
|
|
853
|
+
cliOverrides = {},
|
|
854
|
+
projectDir
|
|
855
|
+
} = options;
|
|
856
|
+
let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
857
|
+
const configFile = findConfigFile(configPath, projectDir);
|
|
858
|
+
if (configFile) {
|
|
859
|
+
try {
|
|
860
|
+
const fileConfig = loadConfigFile(configFile);
|
|
861
|
+
config = deepMerge(config, fileConfig);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
console.warn(
|
|
864
|
+
`Warning: ${err instanceof Error ? err.message : String(err)}`
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (!ignoreEnvVars) {
|
|
869
|
+
const envConfig = loadEnvConfig();
|
|
870
|
+
config = deepMerge(config, envConfig);
|
|
871
|
+
}
|
|
872
|
+
config = deepMerge(config, cliOverrides);
|
|
873
|
+
return config;
|
|
874
|
+
}
|
|
875
|
+
function loadConfigFromCLI(args = process.argv, projectDir) {
|
|
876
|
+
const configPathIndex = args.findIndex((arg) => arg.startsWith("--config="));
|
|
877
|
+
const configPath = configPathIndex !== -1 ? args[configPathIndex]?.split("=")[1] : getConfigPathFromEnv();
|
|
878
|
+
const cliOverrides = parseCLIOverrides(args);
|
|
879
|
+
return loadConfig({ configPath, cliOverrides, projectDir });
|
|
880
|
+
}
|
|
881
|
+
function getDefaultConfigJSON() {
|
|
882
|
+
return JSON.stringify(DEFAULT_CONFIG, null, 2);
|
|
883
|
+
}
|
|
884
|
+
|
|
249
885
|
// src/index.ts
|
|
250
886
|
async function installFonts() {
|
|
251
887
|
try {
|
|
252
|
-
const platform =
|
|
888
|
+
const platform = os2.platform();
|
|
253
889
|
let fontDir;
|
|
254
890
|
if (platform === "darwin") {
|
|
255
|
-
fontDir =
|
|
891
|
+
fontDir = path2.join(os2.homedir(), "Library", "Fonts");
|
|
256
892
|
} else if (platform === "linux") {
|
|
257
|
-
fontDir =
|
|
893
|
+
fontDir = path2.join(os2.homedir(), ".local", "share", "fonts");
|
|
258
894
|
} else if (platform === "win32") {
|
|
259
|
-
fontDir =
|
|
260
|
-
|
|
895
|
+
fontDir = path2.join(
|
|
896
|
+
os2.homedir(),
|
|
261
897
|
"AppData",
|
|
262
898
|
"Local",
|
|
263
899
|
"Microsoft",
|
|
@@ -268,29 +904,29 @@ async function installFonts() {
|
|
|
268
904
|
console.log("Unsupported platform for font installation");
|
|
269
905
|
return;
|
|
270
906
|
}
|
|
271
|
-
if (!
|
|
272
|
-
|
|
907
|
+
if (!fs2.existsSync(fontDir)) {
|
|
908
|
+
fs2.mkdirSync(fontDir, { recursive: true });
|
|
273
909
|
}
|
|
274
910
|
console.log("\u{1F4E6} Installing Powerline Fonts...");
|
|
275
911
|
console.log("Downloading from https://github.com/powerline/fonts");
|
|
276
|
-
const tempDir =
|
|
912
|
+
const tempDir = path2.join(os2.tmpdir(), "powerline-fonts");
|
|
277
913
|
try {
|
|
278
|
-
if (
|
|
279
|
-
|
|
914
|
+
if (fs2.existsSync(tempDir)) {
|
|
915
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
280
916
|
}
|
|
281
917
|
console.log("Cloning powerline fonts repository...");
|
|
282
|
-
|
|
918
|
+
execSync3(
|
|
283
919
|
"git clone --depth=1 https://github.com/powerline/fonts.git powerline-fonts",
|
|
284
920
|
{
|
|
285
921
|
stdio: "inherit",
|
|
286
|
-
cwd:
|
|
922
|
+
cwd: os2.tmpdir()
|
|
287
923
|
}
|
|
288
924
|
);
|
|
289
925
|
console.log("Installing fonts...");
|
|
290
|
-
const installScript =
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
926
|
+
const installScript = path2.join(tempDir, "install.sh");
|
|
927
|
+
if (fs2.existsSync(installScript)) {
|
|
928
|
+
fs2.chmodSync(installScript, 493);
|
|
929
|
+
execSync3("./install.sh", { stdio: "inherit", cwd: tempDir });
|
|
294
930
|
} else {
|
|
295
931
|
throw new Error(
|
|
296
932
|
"Install script not found in powerline fonts repository"
|
|
@@ -304,8 +940,8 @@ async function installFonts() {
|
|
|
304
940
|
"Popular choices: Source Code Pro Powerline, DejaVu Sans Mono Powerline, Ubuntu Mono Powerline"
|
|
305
941
|
);
|
|
306
942
|
} finally {
|
|
307
|
-
if (
|
|
308
|
-
|
|
943
|
+
if (fs2.existsSync(tempDir)) {
|
|
944
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
309
945
|
}
|
|
310
946
|
}
|
|
311
947
|
} catch (error) {
|
|
@@ -320,12 +956,16 @@ async function installFonts() {
|
|
|
320
956
|
}
|
|
321
957
|
async function main() {
|
|
322
958
|
try {
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
const
|
|
959
|
+
const showHelp = process2.argv.includes("--help") || process2.argv.includes("-h");
|
|
960
|
+
const installFontsFlag = process2.argv.includes("--install-fonts");
|
|
961
|
+
const printDefaultConfig = process2.argv.includes("--print-default-config");
|
|
326
962
|
if (installFontsFlag) {
|
|
327
963
|
await installFonts();
|
|
328
|
-
|
|
964
|
+
process2.exit(0);
|
|
965
|
+
}
|
|
966
|
+
if (printDefaultConfig) {
|
|
967
|
+
console.log(getDefaultConfigJSON());
|
|
968
|
+
process2.exit(0);
|
|
329
969
|
}
|
|
330
970
|
if (showHelp) {
|
|
331
971
|
console.log(`
|
|
@@ -334,9 +974,26 @@ claude-powerline - Beautiful powerline statusline for Claude Code
|
|
|
334
974
|
Usage: claude-powerline [options]
|
|
335
975
|
|
|
336
976
|
Options:
|
|
337
|
-
--
|
|
338
|
-
--
|
|
339
|
-
-
|
|
977
|
+
--theme=THEME Set theme: light, dark, custom
|
|
978
|
+
--usage=TYPE Usage display: cost, tokens, both, breakdown
|
|
979
|
+
--session-budget=AMOUNT Set session budget for percentage tracking
|
|
980
|
+
--daily-budget=AMOUNT Set daily budget for percentage tracking
|
|
981
|
+
--config=PATH Use custom config file path
|
|
982
|
+
--install-fonts Install powerline fonts to system
|
|
983
|
+
--print-default-config Print default configuration template
|
|
984
|
+
-h, --help Show this help
|
|
985
|
+
|
|
986
|
+
Configuration:
|
|
987
|
+
Config files are loaded in this order (highest priority first):
|
|
988
|
+
1. CLI arguments (--theme, --usage, --config)
|
|
989
|
+
2. Environment variables (CLAUDE_POWERLINE_THEME, CLAUDE_POWERLINE_USAGE_TYPE, CLAUDE_POWERLINE_CONFIG)
|
|
990
|
+
3. ./.claude-powerline.json (project)
|
|
991
|
+
4. ~/.claude/claude-powerline.json (user)
|
|
992
|
+
5. ~/.config/claude-powerline/config.json (XDG)
|
|
993
|
+
|
|
994
|
+
Creating a config file:
|
|
995
|
+
claude-powerline --print-default-config > ~/.claude/claude-powerline.json
|
|
996
|
+
claude-powerline --print-default-config > .claude-powerline.json
|
|
340
997
|
|
|
341
998
|
Usage in Claude Code settings.json:
|
|
342
999
|
{
|
|
@@ -347,7 +1004,7 @@ Usage in Claude Code settings.json:
|
|
|
347
1004
|
}
|
|
348
1005
|
}
|
|
349
1006
|
`);
|
|
350
|
-
|
|
1007
|
+
process2.exit(0);
|
|
351
1008
|
}
|
|
352
1009
|
const stdin = await getStdin();
|
|
353
1010
|
if (stdin.length === 0) {
|
|
@@ -358,20 +1015,18 @@ claude-powerline - Beautiful powerline statusline for Claude Code
|
|
|
358
1015
|
Usage: claude-powerline [options]
|
|
359
1016
|
|
|
360
1017
|
Options:
|
|
361
|
-
--
|
|
362
|
-
--
|
|
363
|
-
-
|
|
1018
|
+
--theme=THEME Set theme: light, dark, custom
|
|
1019
|
+
--usage=TYPE Usage display: cost, tokens, both, breakdown
|
|
1020
|
+
--session-budget=AMOUNT Set session budget for percentage tracking
|
|
1021
|
+
--daily-budget=AMOUNT Set daily budget for percentage tracking
|
|
1022
|
+
--config=PATH Use custom config file path
|
|
1023
|
+
--install-fonts Install powerline fonts to system
|
|
1024
|
+
--print-default-config Print default configuration template
|
|
1025
|
+
-h, --help Show this help
|
|
364
1026
|
|
|
365
|
-
|
|
366
|
-
{
|
|
367
|
-
"statusLine": {
|
|
368
|
-
"type": "command",
|
|
369
|
-
"command": "claude-powerline",
|
|
370
|
-
"padding": 0
|
|
371
|
-
}
|
|
372
|
-
}
|
|
1027
|
+
Run 'claude-powerline --print-default-config' to see configuration options.
|
|
373
1028
|
`);
|
|
374
|
-
|
|
1029
|
+
process2.exit(1);
|
|
375
1030
|
}
|
|
376
1031
|
let hookData;
|
|
377
1032
|
try {
|
|
@@ -381,15 +1036,17 @@ Usage in Claude Code settings.json:
|
|
|
381
1036
|
"Error: Invalid JSON input:",
|
|
382
1037
|
error instanceof Error ? error.message : String(error)
|
|
383
1038
|
);
|
|
384
|
-
|
|
1039
|
+
process2.exit(1);
|
|
385
1040
|
}
|
|
386
|
-
const
|
|
387
|
-
const
|
|
1041
|
+
const projectDir = hookData.workspace?.project_dir;
|
|
1042
|
+
const config = loadConfigFromCLI(process2.argv, projectDir);
|
|
1043
|
+
const renderer = new PowerlineRenderer(config);
|
|
1044
|
+
const statusline = await renderer.generateStatusline(hookData);
|
|
388
1045
|
console.log(statusline);
|
|
389
1046
|
} catch (error) {
|
|
390
1047
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
391
1048
|
console.error("Error generating statusline:", errorMessage);
|
|
392
|
-
|
|
1049
|
+
process2.exit(1);
|
|
393
1050
|
}
|
|
394
1051
|
}
|
|
395
1052
|
main();
|