aiblueprint-cli 1.4.12 → 1.4.13
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/claude-code-config/scripts/.claude/commands/fix-on-my-computer.md +87 -0
- package/claude-code-config/scripts/command-validator/CLAUDE.md +112 -0
- package/claude-code-config/scripts/command-validator/src/__tests__/validator.test.ts +62 -111
- package/claude-code-config/scripts/command-validator/src/cli.ts +5 -3
- package/claude-code-config/scripts/command-validator/src/lib/security-rules.ts +3 -4
- package/claude-code-config/scripts/command-validator/src/lib/types.ts +1 -0
- package/claude-code-config/scripts/command-validator/src/lib/validator.ts +47 -317
- package/claude-code-config/scripts/statusline/CLAUDE.md +29 -7
- package/claude-code-config/scripts/statusline/README.md +89 -1
- package/claude-code-config/scripts/statusline/defaults.json +75 -0
- package/claude-code-config/scripts/statusline/src/index.ts +101 -24
- package/claude-code-config/scripts/statusline/src/lib/config-types.ts +100 -0
- package/claude-code-config/scripts/statusline/src/lib/config.ts +21 -0
- package/claude-code-config/scripts/statusline/src/lib/context.ts +32 -11
- package/claude-code-config/scripts/statusline/src/lib/formatters.ts +360 -22
- package/claude-code-config/scripts/statusline/src/lib/git.ts +100 -0
- package/claude-code-config/scripts/statusline/src/lib/render-pure.ts +177 -0
- package/claude-code-config/scripts/statusline/src/lib/types.ts +11 -0
- package/claude-code-config/scripts/statusline/statusline.config.json +93 -0
- package/claude-code-config/skills/claude-memory/SKILL.md +689 -0
- package/claude-code-config/skills/claude-memory/references/comprehensive-example.md +175 -0
- package/claude-code-config/skills/claude-memory/references/project-patterns.md +334 -0
- package/claude-code-config/skills/claude-memory/references/prompting-techniques.md +411 -0
- package/claude-code-config/skills/claude-memory/references/section-templates.md +347 -0
- package/claude-code-config/skills/create-slash-commands/SKILL.md +1110 -0
- package/claude-code-config/skills/create-slash-commands/references/arguments.md +273 -0
- package/claude-code-config/skills/create-slash-commands/references/patterns.md +947 -0
- package/claude-code-config/skills/create-slash-commands/references/prompt-examples.md +656 -0
- package/claude-code-config/skills/create-slash-commands/references/tool-restrictions.md +389 -0
- package/claude-code-config/skills/create-subagents/SKILL.md +425 -0
- package/claude-code-config/skills/create-subagents/references/context-management.md +567 -0
- package/claude-code-config/skills/create-subagents/references/debugging-agents.md +714 -0
- package/claude-code-config/skills/create-subagents/references/error-handling-and-recovery.md +502 -0
- package/claude-code-config/skills/create-subagents/references/evaluation-and-testing.md +374 -0
- package/claude-code-config/skills/create-subagents/references/orchestration-patterns.md +591 -0
- package/claude-code-config/skills/create-subagents/references/subagents.md +599 -0
- package/claude-code-config/skills/create-subagents/references/writing-subagent-prompts.md +513 -0
- package/package.json +1 -1
- package/claude-code-config/commands/apex.md +0 -109
- package/claude-code-config/commands/tasks/run-task.md +0 -220
- package/claude-code-config/commands/utils/watch-ci.md +0 -47
- package/claude-code-config/scripts/command-validator/biome.json +0 -29
- package/claude-code-config/scripts/command-validator/bun.lockb +0 -0
- package/claude-code-config/scripts/command-validator/package.json +0 -27
- package/claude-code-config/scripts/command-validator/vitest.config.ts +0 -7
- package/claude-code-config/scripts/hook-post-file.ts +0 -162
- package/claude-code-config/scripts/statusline/biome.json +0 -34
- package/claude-code-config/scripts/statusline/bun.lockb +0 -0
- package/claude-code-config/scripts/statusline/fixtures/test-input.json +0 -25
- package/claude-code-config/scripts/statusline/package.json +0 -19
- package/claude-code-config/scripts/statusline/statusline.config.ts +0 -25
- package/claude-code-config/scripts/statusline/test.ts +0 -20
- package/claude-code-config/scripts/validate-command.js +0 -712
- package/claude-code-config/scripts/validate-command.readme.md +0 -283
|
@@ -1,48 +1,386 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { sep } from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import type {
|
|
5
|
+
CostFormat,
|
|
6
|
+
ProgressBarBackground,
|
|
7
|
+
ProgressBarColor,
|
|
8
|
+
ProgressBarStyle,
|
|
9
|
+
StatuslineConfig,
|
|
10
|
+
} from "./config-types";
|
|
11
|
+
import type { GitStatus } from "./git";
|
|
12
|
+
|
|
13
|
+
type ColorFunction = (text: string | number) => string;
|
|
14
|
+
|
|
15
|
+
const pico = pc.createColors(true);
|
|
2
16
|
|
|
3
17
|
export const colors = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
green: pico.green as ColorFunction,
|
|
19
|
+
red: pico.red as ColorFunction,
|
|
20
|
+
purple: pico.magenta as ColorFunction,
|
|
21
|
+
yellow: pico.yellow as ColorFunction,
|
|
22
|
+
orange: ((text: string | number) =>
|
|
23
|
+
`\x1b[38;5;208m${text}\x1b[0m`) as ColorFunction,
|
|
24
|
+
peach: ((text: string | number) =>
|
|
25
|
+
`\x1b[38;2;222;115;86m${text}\x1b[0m`) as ColorFunction,
|
|
26
|
+
bgPeach: ((text: string | number) =>
|
|
27
|
+
`\x1b[48;2;222;115;86m${text}\x1b[0m`) as ColorFunction,
|
|
28
|
+
black: ((text: string | number) =>
|
|
29
|
+
`\x1b[38;2;0;0;0m${text}\x1b[39m`) as ColorFunction,
|
|
30
|
+
white: ((text: string | number) =>
|
|
31
|
+
`\x1b[38;2;255;255;255m${text}\x1b[39m`) as ColorFunction,
|
|
32
|
+
gray: pico.gray as ColorFunction,
|
|
33
|
+
dimWhite: ((text: string | number) =>
|
|
34
|
+
`\x1b[37m${text}\x1b[39m`) as ColorFunction,
|
|
35
|
+
lightGray: pico.whiteBright as ColorFunction,
|
|
36
|
+
cyan: pico.cyan as ColorFunction,
|
|
37
|
+
blue: pico.blue as ColorFunction,
|
|
38
|
+
bgBlack: pico.bgBlack as ColorFunction,
|
|
39
|
+
bgBlackBright: pico.bgBlackBright as ColorFunction,
|
|
40
|
+
bgWhite: pico.bgWhite as ColorFunction,
|
|
41
|
+
bgBlue: pico.bgBlue as ColorFunction,
|
|
42
|
+
bgMagenta: pico.bgMagenta as ColorFunction,
|
|
43
|
+
bgCyan: pico.bgCyan as ColorFunction,
|
|
44
|
+
dim: pico.dim as ColorFunction,
|
|
45
|
+
bold: pico.bold as ColorFunction,
|
|
46
|
+
hidden: pico.hidden as ColorFunction,
|
|
47
|
+
italic: pico.italic as ColorFunction,
|
|
48
|
+
underline: pico.underline as ColorFunction,
|
|
49
|
+
strikethrough: pico.strikethrough as ColorFunction,
|
|
50
|
+
reset: pico.reset as ColorFunction,
|
|
51
|
+
inverse: pico.inverse as ColorFunction,
|
|
7
52
|
} as const;
|
|
8
53
|
|
|
9
|
-
export function
|
|
10
|
-
|
|
54
|
+
export function formatBranch(
|
|
55
|
+
git: GitStatus,
|
|
56
|
+
gitConfig: StatuslineConfig["git"],
|
|
57
|
+
): string {
|
|
58
|
+
let result = "";
|
|
59
|
+
|
|
60
|
+
if (gitConfig.showBranch) {
|
|
61
|
+
result = colors.lightGray(git.branch);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (git.hasChanges) {
|
|
65
|
+
const changes: string[] = [];
|
|
66
|
+
|
|
67
|
+
if (gitConfig.showDirtyIndicator) {
|
|
68
|
+
result += colors.purple("*");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (gitConfig.showChanges) {
|
|
72
|
+
const totalAdded = git.staged.added + git.unstaged.added;
|
|
73
|
+
const totalDeleted = git.staged.deleted + git.unstaged.deleted;
|
|
74
|
+
|
|
75
|
+
if (totalAdded > 0) {
|
|
76
|
+
changes.push(colors.green(`+${totalAdded}`));
|
|
77
|
+
}
|
|
78
|
+
if (totalDeleted > 0) {
|
|
79
|
+
changes.push(colors.red(`-${totalDeleted}`));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (gitConfig.showStaged && git.staged.files > 0) {
|
|
84
|
+
changes.push(colors.gray(`~${git.staged.files}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (gitConfig.showUnstaged && git.unstaged.files > 0) {
|
|
88
|
+
changes.push(colors.yellow(`~${git.unstaged.files}`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (changes.length > 0) {
|
|
92
|
+
result += ` ${changes.join(" ")}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatPath(
|
|
100
|
+
path: string,
|
|
101
|
+
mode: "full" | "truncated" | "basename" = "truncated",
|
|
102
|
+
): string {
|
|
103
|
+
const home = homedir();
|
|
104
|
+
let formattedPath = path;
|
|
105
|
+
|
|
11
106
|
if (home && path.startsWith(home)) {
|
|
12
|
-
|
|
107
|
+
formattedPath = `~${path.slice(home.length)}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (mode === "basename") {
|
|
111
|
+
const segments = path.split(/[/\\]/).filter((s) => s.length > 0);
|
|
112
|
+
return segments[segments.length - 1] || path;
|
|
13
113
|
}
|
|
14
|
-
|
|
114
|
+
|
|
115
|
+
if (mode === "truncated") {
|
|
116
|
+
const segments = formattedPath.split(/[/\\]/).filter((s) => s.length > 0);
|
|
117
|
+
if (segments.length > 2) {
|
|
118
|
+
return `…${sep}${segments.slice(-2).join(sep)}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return formattedPath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatCost(
|
|
126
|
+
cost: number,
|
|
127
|
+
format: CostFormat = "decimal1",
|
|
128
|
+
): string {
|
|
129
|
+
if (format === "integer") return Math.round(cost).toString();
|
|
130
|
+
if (format === "decimal1") return cost.toFixed(1);
|
|
131
|
+
return cost.toFixed(2);
|
|
15
132
|
}
|
|
16
133
|
|
|
17
|
-
function formatTokens(tokens: number): string {
|
|
134
|
+
export function formatTokens(tokens: number, showDecimals = true): string {
|
|
18
135
|
if (tokens >= 1000000) {
|
|
19
|
-
const value =
|
|
20
|
-
|
|
136
|
+
const value = tokens / 1000000;
|
|
137
|
+
const number = showDecimals
|
|
138
|
+
? value.toFixed(1)
|
|
139
|
+
: Math.round(value).toString();
|
|
140
|
+
return `${colors.lightGray(number)}${colors.gray("m")}`;
|
|
21
141
|
}
|
|
22
142
|
if (tokens >= 1000) {
|
|
23
|
-
const value =
|
|
24
|
-
|
|
143
|
+
const value = tokens / 1000;
|
|
144
|
+
const number = showDecimals
|
|
145
|
+
? value.toFixed(1)
|
|
146
|
+
: Math.round(value).toString();
|
|
147
|
+
return `${colors.lightGray(number)}${colors.gray("k")}`;
|
|
148
|
+
}
|
|
149
|
+
return colors.lightGray(tokens.toString());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function formatDuration(ms: number): string {
|
|
153
|
+
const minutes = Math.floor(ms / 60000);
|
|
154
|
+
const hours = Math.floor(minutes / 60);
|
|
155
|
+
const mins = minutes % 60;
|
|
156
|
+
|
|
157
|
+
if (hours > 0) {
|
|
158
|
+
return `${hours}h ${mins}m`;
|
|
159
|
+
}
|
|
160
|
+
return `${mins}m`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function formatResetTime(resetsAt: string): string {
|
|
164
|
+
try {
|
|
165
|
+
const resetDate = new Date(resetsAt);
|
|
166
|
+
if (Number.isNaN(resetDate.getTime())) {
|
|
167
|
+
return "N/A";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const now = new Date();
|
|
171
|
+
const diffMs = resetDate.getTime() - now.getTime();
|
|
172
|
+
|
|
173
|
+
if (diffMs <= 0) {
|
|
174
|
+
return "now";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const hours = Math.floor(diffMs / 3600000);
|
|
178
|
+
const minutes = Math.floor((diffMs % 3600000) / 60000);
|
|
179
|
+
|
|
180
|
+
if (hours > 0) {
|
|
181
|
+
return `${hours}h${minutes}m`;
|
|
182
|
+
}
|
|
183
|
+
return `${minutes}m`;
|
|
184
|
+
} catch {
|
|
185
|
+
return "N/A";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getProgressBarColor(
|
|
190
|
+
percentage: number,
|
|
191
|
+
colorMode: ProgressBarColor,
|
|
192
|
+
): ColorFunction {
|
|
193
|
+
if (colorMode === "progressive") {
|
|
194
|
+
if (percentage < 50) return colors.gray;
|
|
195
|
+
if (percentage < 70) return colors.yellow;
|
|
196
|
+
if (percentage < 90) return colors.orange;
|
|
197
|
+
return colors.red;
|
|
25
198
|
}
|
|
26
|
-
return
|
|
199
|
+
if (colorMode === "green") return colors.green;
|
|
200
|
+
if (colorMode === "yellow") return colors.yellow;
|
|
201
|
+
if (colorMode === "peach") return colors.peach;
|
|
202
|
+
if (colorMode === "black") return colors.black;
|
|
203
|
+
if (colorMode === "white") return colors.white;
|
|
204
|
+
return colors.red;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function getProgressBarBackground(
|
|
208
|
+
background: ProgressBarBackground,
|
|
209
|
+
): ColorFunction | null {
|
|
210
|
+
if (background === "none") return null;
|
|
211
|
+
if (background === "dark") return colors.bgBlack;
|
|
212
|
+
if (background === "gray") return colors.bgBlackBright;
|
|
213
|
+
if (background === "light") return colors.bgWhite;
|
|
214
|
+
if (background === "blue") return colors.bgBlue;
|
|
215
|
+
if (background === "purple") return colors.bgMagenta;
|
|
216
|
+
if (background === "cyan") return colors.bgCyan;
|
|
217
|
+
if (background === "peach") return colors.bgPeach;
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatProgressBarFilled(
|
|
222
|
+
percentage: number,
|
|
223
|
+
length: number,
|
|
224
|
+
colorMode: ProgressBarColor,
|
|
225
|
+
background: ProgressBarBackground,
|
|
226
|
+
): string {
|
|
227
|
+
const filled = Math.round((percentage / 100) * length);
|
|
228
|
+
const empty = length - filled;
|
|
229
|
+
|
|
230
|
+
const filledBar = "█".repeat(filled);
|
|
231
|
+
const emptyBar = "░".repeat(empty);
|
|
232
|
+
const colorFn = getProgressBarColor(percentage, colorMode);
|
|
233
|
+
const bgFn = getProgressBarBackground(background);
|
|
234
|
+
|
|
235
|
+
const coloredFilled = bgFn ? bgFn(colorFn(filledBar)) : colorFn(filledBar);
|
|
236
|
+
const coloredEmpty = bgFn ? bgFn(colorFn(emptyBar)) : colorFn(emptyBar);
|
|
237
|
+
|
|
238
|
+
return `${coloredFilled}${coloredEmpty}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function formatProgressBarRectangle(
|
|
242
|
+
percentage: number,
|
|
243
|
+
length: number,
|
|
244
|
+
colorMode: ProgressBarColor,
|
|
245
|
+
background: ProgressBarBackground,
|
|
246
|
+
): string {
|
|
247
|
+
const filled = Math.round((percentage / 100) * length);
|
|
248
|
+
const empty = length - filled;
|
|
249
|
+
|
|
250
|
+
const filledBar = "▰".repeat(filled);
|
|
251
|
+
const emptyBar = "▱".repeat(empty);
|
|
252
|
+
const colorFn = getProgressBarColor(percentage, colorMode);
|
|
253
|
+
const bgFn = getProgressBarBackground(background);
|
|
254
|
+
|
|
255
|
+
const coloredFilled = bgFn ? bgFn(colorFn(filledBar)) : colorFn(filledBar);
|
|
256
|
+
const coloredEmpty = bgFn ? bgFn(colorFn(emptyBar)) : colorFn(emptyBar);
|
|
257
|
+
|
|
258
|
+
return `${coloredFilled}${coloredEmpty}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function formatProgressBarBraille(
|
|
262
|
+
percentage: number,
|
|
263
|
+
length: number,
|
|
264
|
+
colorMode: ProgressBarColor,
|
|
265
|
+
background: ProgressBarBackground,
|
|
266
|
+
): string {
|
|
267
|
+
const brailleChars = ["⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"];
|
|
268
|
+
|
|
269
|
+
const totalSteps = length * (brailleChars.length - 1);
|
|
270
|
+
const currentStep = Math.round((percentage / 100) * totalSteps);
|
|
271
|
+
|
|
272
|
+
const fullBlocks = Math.floor(currentStep / (brailleChars.length - 1));
|
|
273
|
+
const partialIndex = currentStep % (brailleChars.length - 1);
|
|
274
|
+
const emptyBlocks = length - fullBlocks - (partialIndex > 0 ? 1 : 0);
|
|
275
|
+
|
|
276
|
+
const colorFn = getProgressBarColor(percentage, colorMode);
|
|
277
|
+
const bgFn = getProgressBarBackground(background);
|
|
278
|
+
|
|
279
|
+
const fullPart = bgFn
|
|
280
|
+
? bgFn(colorFn("⣿".repeat(fullBlocks)))
|
|
281
|
+
: colorFn("⣿".repeat(fullBlocks));
|
|
282
|
+
const partialPart =
|
|
283
|
+
partialIndex > 0
|
|
284
|
+
? bgFn
|
|
285
|
+
? bgFn(colorFn(brailleChars[partialIndex]))
|
|
286
|
+
: colorFn(brailleChars[partialIndex])
|
|
287
|
+
: "";
|
|
288
|
+
const emptyPart =
|
|
289
|
+
emptyBlocks > 0
|
|
290
|
+
? bgFn
|
|
291
|
+
? bgFn(colorFn("⣀".repeat(emptyBlocks)))
|
|
292
|
+
: colorFn("⣀".repeat(emptyBlocks))
|
|
293
|
+
: "";
|
|
294
|
+
|
|
295
|
+
return `${fullPart}${partialPart}${emptyPart}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function formatProgressBar({
|
|
299
|
+
percentage,
|
|
300
|
+
length,
|
|
301
|
+
style,
|
|
302
|
+
colorMode,
|
|
303
|
+
background,
|
|
304
|
+
}: {
|
|
305
|
+
percentage: number;
|
|
306
|
+
length: 5 | 10 | 15;
|
|
307
|
+
style: ProgressBarStyle;
|
|
308
|
+
colorMode: ProgressBarColor;
|
|
309
|
+
background: ProgressBarBackground;
|
|
310
|
+
}): string {
|
|
311
|
+
if (style === "rectangle") {
|
|
312
|
+
return formatProgressBarRectangle(
|
|
313
|
+
percentage,
|
|
314
|
+
length,
|
|
315
|
+
colorMode,
|
|
316
|
+
background,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (style === "braille") {
|
|
320
|
+
return formatProgressBarBraille(percentage, length, colorMode, background);
|
|
321
|
+
}
|
|
322
|
+
return formatProgressBarFilled(percentage, length, colorMode, background);
|
|
27
323
|
}
|
|
28
324
|
|
|
29
325
|
export function formatSession(
|
|
30
|
-
|
|
326
|
+
cost: string,
|
|
327
|
+
duration: string,
|
|
328
|
+
tokensUsed: number,
|
|
329
|
+
tokensMax: number,
|
|
31
330
|
percentage: number,
|
|
32
331
|
config: StatuslineConfig["session"],
|
|
33
332
|
): string {
|
|
34
|
-
const
|
|
333
|
+
const sessionItems: string[] = [];
|
|
334
|
+
|
|
335
|
+
if (config.cost.enabled) {
|
|
336
|
+
sessionItems.push(`${colors.gray("$")}${colors.dimWhite(cost)}`);
|
|
337
|
+
}
|
|
35
338
|
|
|
36
|
-
if (config.
|
|
37
|
-
|
|
339
|
+
if (config.tokens.enabled) {
|
|
340
|
+
const formattedUsed = formatTokens(tokensUsed, config.tokens.showDecimals);
|
|
341
|
+
if (config.tokens.showMax) {
|
|
342
|
+
const formattedMax = formatTokens(tokensMax, config.tokens.showDecimals);
|
|
343
|
+
sessionItems.push(`${formattedUsed}${colors.gray("/")}${formattedMax}`);
|
|
344
|
+
} else {
|
|
345
|
+
sessionItems.push(formattedUsed);
|
|
346
|
+
}
|
|
38
347
|
}
|
|
39
|
-
|
|
40
|
-
|
|
348
|
+
|
|
349
|
+
if (config.percentage.enabled) {
|
|
350
|
+
const parts: string[] = [];
|
|
351
|
+
|
|
352
|
+
if (config.percentage.progressBar.enabled) {
|
|
353
|
+
const bar = formatProgressBar({
|
|
354
|
+
percentage,
|
|
355
|
+
length: config.percentage.progressBar.length,
|
|
356
|
+
style: config.percentage.progressBar.style,
|
|
357
|
+
colorMode: config.percentage.progressBar.color,
|
|
358
|
+
background: config.percentage.progressBar.background,
|
|
359
|
+
});
|
|
360
|
+
parts.push(bar);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (config.percentage.showValue) {
|
|
364
|
+
parts.push(
|
|
365
|
+
`${colors.lightGray(percentage.toString())}${colors.gray("%")}`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (parts.length > 0) {
|
|
370
|
+
sessionItems.push(parts.join(" "));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (config.duration.enabled) {
|
|
375
|
+
sessionItems.push(colors.gray(`(${duration})`));
|
|
41
376
|
}
|
|
42
377
|
|
|
43
|
-
if (
|
|
378
|
+
if (sessionItems.length === 0) {
|
|
44
379
|
return "";
|
|
45
380
|
}
|
|
46
381
|
|
|
47
|
-
|
|
382
|
+
const infoSep = config.infoSeparator
|
|
383
|
+
? ` ${colors.gray(config.infoSeparator)} `
|
|
384
|
+
: " ";
|
|
385
|
+
return `${colors.gray("S:")} ${sessionItems.join(infoSep)}`;
|
|
48
386
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
|
|
3
|
+
export interface GitStatus {
|
|
4
|
+
branch: string;
|
|
5
|
+
hasChanges: boolean;
|
|
6
|
+
staged: {
|
|
7
|
+
added: number;
|
|
8
|
+
deleted: number;
|
|
9
|
+
files: number;
|
|
10
|
+
};
|
|
11
|
+
unstaged: {
|
|
12
|
+
added: number;
|
|
13
|
+
deleted: number;
|
|
14
|
+
files: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function getGitStatus(): Promise<GitStatus> {
|
|
19
|
+
try {
|
|
20
|
+
const isGitRepo = await $`git rev-parse --git-dir`.quiet().nothrow();
|
|
21
|
+
if (isGitRepo.exitCode !== 0) {
|
|
22
|
+
return {
|
|
23
|
+
branch: "no-git",
|
|
24
|
+
hasChanges: false,
|
|
25
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
26
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const branchResult = await $`git branch --show-current`.quiet().text();
|
|
31
|
+
const branch = branchResult.trim() || "detached";
|
|
32
|
+
|
|
33
|
+
const diffCheck = await $`git diff-index --quiet HEAD --`.quiet().nothrow();
|
|
34
|
+
const cachedCheck = await $`git diff-index --quiet --cached HEAD --`
|
|
35
|
+
.quiet()
|
|
36
|
+
.nothrow();
|
|
37
|
+
|
|
38
|
+
if (diffCheck.exitCode !== 0 || cachedCheck.exitCode !== 0) {
|
|
39
|
+
const unstagedDiff = await $`git diff --numstat`.quiet().text();
|
|
40
|
+
const stagedDiff = await $`git diff --cached --numstat`.quiet().text();
|
|
41
|
+
const stagedFilesResult = await $`git diff --cached --name-only`
|
|
42
|
+
.quiet()
|
|
43
|
+
.text();
|
|
44
|
+
const unstagedFilesResult = await $`git diff --name-only`.quiet().text();
|
|
45
|
+
|
|
46
|
+
const parseStats = (diff: string) => {
|
|
47
|
+
let added = 0;
|
|
48
|
+
let deleted = 0;
|
|
49
|
+
for (const line of diff.split("\n")) {
|
|
50
|
+
if (!line.trim()) continue;
|
|
51
|
+
const [a, d] = line
|
|
52
|
+
.split("\t")
|
|
53
|
+
.map((n) => Number.parseInt(n, 10) || 0);
|
|
54
|
+
added += a;
|
|
55
|
+
deleted += d;
|
|
56
|
+
}
|
|
57
|
+
return { added, deleted };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const unstagedStats = parseStats(unstagedDiff);
|
|
61
|
+
const stagedStats = parseStats(stagedDiff);
|
|
62
|
+
|
|
63
|
+
const stagedFilesCount = stagedFilesResult
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((f) => f.trim()).length;
|
|
66
|
+
const unstagedFilesCount = unstagedFilesResult
|
|
67
|
+
.split("\n")
|
|
68
|
+
.filter((f) => f.trim()).length;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
branch,
|
|
72
|
+
hasChanges: true,
|
|
73
|
+
staged: {
|
|
74
|
+
added: stagedStats.added,
|
|
75
|
+
deleted: stagedStats.deleted,
|
|
76
|
+
files: stagedFilesCount,
|
|
77
|
+
},
|
|
78
|
+
unstaged: {
|
|
79
|
+
added: unstagedStats.added,
|
|
80
|
+
deleted: unstagedStats.deleted,
|
|
81
|
+
files: unstagedFilesCount,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
branch,
|
|
88
|
+
hasChanges: false,
|
|
89
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
90
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
91
|
+
};
|
|
92
|
+
} catch {
|
|
93
|
+
return {
|
|
94
|
+
branch: "no-git",
|
|
95
|
+
hasChanges: false,
|
|
96
|
+
staged: { added: 0, deleted: 0, files: 0 },
|
|
97
|
+
unstaged: { added: 0, deleted: 0, files: 0 },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure statusline renderer - no I/O, no side effects
|
|
3
|
+
*
|
|
4
|
+
* ARCHITECTURE: Raw data in, formatted string out.
|
|
5
|
+
* ALL config decisions happen here, not in data preparation.
|
|
6
|
+
*
|
|
7
|
+
* FREE VERSION: Simplified - No limits, weekly, or daily tracking
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { StatuslineConfig } from "./config-types";
|
|
11
|
+
import {
|
|
12
|
+
colors,
|
|
13
|
+
formatCost,
|
|
14
|
+
formatDuration,
|
|
15
|
+
formatPath,
|
|
16
|
+
formatProgressBar,
|
|
17
|
+
formatTokens,
|
|
18
|
+
} from "./formatters";
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────
|
|
21
|
+
// DATA TYPES - Simplified for free version
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface StatuslineData {
|
|
25
|
+
branch: string;
|
|
26
|
+
dirPath: string;
|
|
27
|
+
modelName: string;
|
|
28
|
+
sessionCost: string;
|
|
29
|
+
sessionDuration: string;
|
|
30
|
+
contextTokens: number | null;
|
|
31
|
+
contextPercentage: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─────────────────────────────────────────────────────────────
|
|
35
|
+
// FORMATTING - All config-aware formatting in one place
|
|
36
|
+
// ─────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function formatSessionPart(
|
|
39
|
+
cost: number,
|
|
40
|
+
durationMs: number,
|
|
41
|
+
contextTokens: number | null,
|
|
42
|
+
contextPercentage: number | null,
|
|
43
|
+
maxTokens: number,
|
|
44
|
+
config: StatuslineConfig["session"],
|
|
45
|
+
): string {
|
|
46
|
+
// No context data yet - show placeholder
|
|
47
|
+
if (contextTokens === null || contextPercentage === null) {
|
|
48
|
+
return `${colors.gray("S:")} ${colors.gray("-")}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const items: string[] = [];
|
|
52
|
+
|
|
53
|
+
if (config.cost.enabled) {
|
|
54
|
+
const formattedCost = formatCost(cost, config.cost.format);
|
|
55
|
+
items.push(`${colors.gray("$")}${colors.dimWhite(formattedCost)}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (config.tokens.enabled) {
|
|
59
|
+
const formattedUsed = formatTokens(
|
|
60
|
+
contextTokens,
|
|
61
|
+
config.tokens.showDecimals,
|
|
62
|
+
);
|
|
63
|
+
if (config.tokens.showMax) {
|
|
64
|
+
const formattedMax = formatTokens(maxTokens, config.tokens.showDecimals);
|
|
65
|
+
items.push(`${formattedUsed}${colors.gray("/")}${formattedMax}`);
|
|
66
|
+
} else {
|
|
67
|
+
items.push(formattedUsed);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (config.percentage.enabled) {
|
|
72
|
+
const pctParts: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (config.percentage.progressBar.enabled) {
|
|
75
|
+
pctParts.push(
|
|
76
|
+
formatProgressBar({
|
|
77
|
+
percentage: contextPercentage,
|
|
78
|
+
length: config.percentage.progressBar.length,
|
|
79
|
+
style: config.percentage.progressBar.style,
|
|
80
|
+
colorMode: config.percentage.progressBar.color,
|
|
81
|
+
background: config.percentage.progressBar.background,
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.percentage.showValue) {
|
|
87
|
+
pctParts.push(
|
|
88
|
+
`${colors.lightGray(contextPercentage.toString())}${colors.gray("%")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (pctParts.length > 0) {
|
|
93
|
+
items.push(pctParts.join(" "));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.duration.enabled) {
|
|
98
|
+
items.push(colors.gray(`(${formatDuration(durationMs)})`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (items.length === 0) return "";
|
|
102
|
+
|
|
103
|
+
const sep = config.infoSeparator
|
|
104
|
+
? ` ${colors.gray(config.infoSeparator)} `
|
|
105
|
+
: " ";
|
|
106
|
+
return `${colors.gray("S:")} ${items.join(sep)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────────────────────────────────────
|
|
110
|
+
// MAIN RENDER FUNCTION - Simple version
|
|
111
|
+
// ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export function renderStatusline(
|
|
114
|
+
data: StatuslineData,
|
|
115
|
+
config: StatuslineConfig,
|
|
116
|
+
): string {
|
|
117
|
+
const sep = colors.gray(config.separator);
|
|
118
|
+
const sections: string[] = [];
|
|
119
|
+
|
|
120
|
+
// Line 1: Git + Path + Model
|
|
121
|
+
const line1Parts: string[] = [];
|
|
122
|
+
|
|
123
|
+
// Git branch
|
|
124
|
+
if (data.branch) {
|
|
125
|
+
line1Parts.push(colors.lightGray(data.branch));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Path
|
|
129
|
+
const pathPart = formatPath(data.dirPath, config.pathDisplayMode);
|
|
130
|
+
line1Parts.push(colors.gray(pathPart));
|
|
131
|
+
|
|
132
|
+
// Model name (hide Sonnet if configured)
|
|
133
|
+
const isSonnet = data.modelName.toLowerCase().includes("sonnet");
|
|
134
|
+
if (!isSonnet || config.showSonnetModel) {
|
|
135
|
+
line1Parts.push(colors.peach(data.modelName));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sections.push(line1Parts.join(` ${sep} `));
|
|
139
|
+
|
|
140
|
+
// Line 2: Session info (cost, tokens, percentage, duration)
|
|
141
|
+
const cost = parseFloat(data.sessionCost.replace(/[$,]/g, "")) || 0;
|
|
142
|
+
const durationMs = parseDurationToMs(data.sessionDuration);
|
|
143
|
+
|
|
144
|
+
const sessionPart = formatSessionPart(
|
|
145
|
+
cost,
|
|
146
|
+
durationMs,
|
|
147
|
+
data.contextTokens,
|
|
148
|
+
data.contextPercentage,
|
|
149
|
+
config.context.maxContextTokens,
|
|
150
|
+
config.session,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (sessionPart) sections.push(sessionPart);
|
|
154
|
+
|
|
155
|
+
const output = sections.join(` ${sep} `);
|
|
156
|
+
|
|
157
|
+
if (config.oneLine) return output;
|
|
158
|
+
|
|
159
|
+
// Two-line mode: break after line1
|
|
160
|
+
const line1 = sections[0];
|
|
161
|
+
const rest = sections.slice(1).join(` ${sep} `);
|
|
162
|
+
return rest ? `${line1}\n${rest}` : line1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────────────────────────────────────
|
|
166
|
+
// HELPERS
|
|
167
|
+
// ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
// Helper to parse "12m" or "1h 30m" back to ms
|
|
170
|
+
function parseDurationToMs(duration: string): number {
|
|
171
|
+
let ms = 0;
|
|
172
|
+
const hourMatch = duration.match(/(\d+)h/);
|
|
173
|
+
const minMatch = duration.match(/(\d+)m/);
|
|
174
|
+
if (hourMatch) ms += parseInt(hourMatch[1], 10) * 3600000;
|
|
175
|
+
if (minMatch) ms += parseInt(minMatch[1], 10) * 60000;
|
|
176
|
+
return ms || 720000; // Default 12 minutes
|
|
177
|
+
}
|
|
@@ -21,5 +21,16 @@ export interface HookInput {
|
|
|
21
21
|
total_lines_added: number;
|
|
22
22
|
total_lines_removed: number;
|
|
23
23
|
};
|
|
24
|
+
context_window?: {
|
|
25
|
+
total_input_tokens: number;
|
|
26
|
+
total_output_tokens: number;
|
|
27
|
+
context_window_size: number;
|
|
28
|
+
current_usage?: {
|
|
29
|
+
input_tokens: number;
|
|
30
|
+
output_tokens: number;
|
|
31
|
+
cache_creation_input_tokens?: number;
|
|
32
|
+
cache_read_input_tokens?: number;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
24
35
|
exceeds_200k_tokens?: boolean;
|
|
25
36
|
}
|