aiblueprint-cli 1.4.13 → 1.4.14

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 (25) hide show
  1. package/claude-code-config/scripts/CLAUDE.md +50 -0
  2. package/claude-code-config/scripts/biome.json +37 -0
  3. package/claude-code-config/scripts/bun.lockb +0 -0
  4. package/claude-code-config/scripts/package.json +39 -0
  5. package/claude-code-config/scripts/statusline/__tests__/context.test.ts +229 -0
  6. package/claude-code-config/scripts/statusline/__tests__/formatters.test.ts +108 -0
  7. package/claude-code-config/scripts/statusline/__tests__/statusline.test.ts +309 -0
  8. package/claude-code-config/scripts/statusline/data/.gitignore +8 -0
  9. package/claude-code-config/scripts/statusline/data/.gitkeep +0 -0
  10. package/claude-code-config/scripts/statusline/defaults.json +4 -0
  11. package/claude-code-config/scripts/statusline/docs/ARCHITECTURE.md +166 -0
  12. package/claude-code-config/scripts/statusline/fixtures/mock-transcript.jsonl +4 -0
  13. package/claude-code-config/scripts/statusline/fixtures/test-input.json +35 -0
  14. package/claude-code-config/scripts/statusline/src/index.ts +74 -0
  15. package/claude-code-config/scripts/statusline/src/lib/config-types.ts +4 -0
  16. package/claude-code-config/scripts/statusline/src/lib/menu-factories.ts +224 -0
  17. package/claude-code-config/scripts/statusline/src/lib/presets.ts +177 -0
  18. package/claude-code-config/scripts/statusline/src/lib/render-pure.ts +341 -21
  19. package/claude-code-config/scripts/statusline/src/lib/utils.ts +15 -0
  20. package/claude-code-config/scripts/statusline/src/tests/spend-v2.test.ts +306 -0
  21. package/claude-code-config/scripts/statusline/statusline.config.json +25 -39
  22. package/claude-code-config/scripts/statusline/test-with-fixtures.ts +37 -0
  23. package/claude-code-config/scripts/statusline/test.ts +20 -0
  24. package/claude-code-config/scripts/tsconfig.json +27 -0
  25. package/package.json +1 -1
@@ -3,8 +3,6 @@
3
3
  *
4
4
  * ARCHITECTURE: Raw data in, formatted string out.
5
5
  * ALL config decisions happen here, not in data preparation.
6
- *
7
- * FREE VERSION: Simplified - No limits, weekly, or daily tracking
8
6
  */
9
7
 
10
8
  import type { StatuslineConfig } from "./config-types";
@@ -14,13 +12,52 @@ import {
14
12
  formatDuration,
15
13
  formatPath,
16
14
  formatProgressBar,
15
+ formatResetTime,
17
16
  formatTokens,
18
17
  } from "./formatters";
19
18
 
19
+ const WEEKLY_HOURS = 168; // 7 days * 24 hours
20
+ const FIVE_HOUR_MINUTES = 300; // 5 hours * 60 minutes
21
+
20
22
  // ─────────────────────────────────────────────────────────────
21
- // DATA TYPES - Simplified for free version
23
+ // RAW DATA TYPES - No pre-formatting, just raw values
22
24
  // ─────────────────────────────────────────────────────────────
23
25
 
26
+ export interface GitChanges {
27
+ files: number;
28
+ added: number;
29
+ deleted: number;
30
+ }
31
+
32
+ export interface RawGitData {
33
+ branch: string;
34
+ dirty: boolean;
35
+ staged: GitChanges;
36
+ unstaged: GitChanges;
37
+ }
38
+
39
+ export interface UsageLimit {
40
+ utilization: number;
41
+ resets_at: string | null;
42
+ }
43
+
44
+ export interface RawStatuslineData {
45
+ git: RawGitData | null;
46
+ path: string;
47
+ modelName: string;
48
+ cost: number;
49
+ durationMs: number;
50
+ contextTokens: number | null;
51
+ contextPercentage: number | null;
52
+ usageLimits?: {
53
+ five_hour: UsageLimit | null;
54
+ seven_day: UsageLimit | null;
55
+ };
56
+ periodCost?: number;
57
+ todayCost?: number;
58
+ }
59
+
60
+ // Legacy interface for backwards compatibility
24
61
  export interface StatuslineData {
25
62
  branch: string;
26
63
  dirPath: string;
@@ -29,12 +66,63 @@ export interface StatuslineData {
29
66
  sessionDuration: string;
30
67
  contextTokens: number | null;
31
68
  contextPercentage: number | null;
69
+ usageLimits?: {
70
+ five_hour: UsageLimit | null;
71
+ seven_day: UsageLimit | null;
72
+ };
73
+ periodCost?: number;
74
+ todayCost?: number;
32
75
  }
33
76
 
34
77
  // ─────────────────────────────────────────────────────────────
35
78
  // FORMATTING - All config-aware formatting in one place
36
79
  // ─────────────────────────────────────────────────────────────
37
80
 
81
+ function formatGitPart(
82
+ git: RawGitData | null,
83
+ config: StatuslineConfig["git"],
84
+ ): string {
85
+ if (!git || !config.enabled) return "";
86
+
87
+ const parts: string[] = [];
88
+
89
+ if (config.showBranch) {
90
+ parts.push(colors.lightGray(git.branch));
91
+ }
92
+
93
+ if (git.dirty && config.showDirtyIndicator) {
94
+ // Append to branch name without space
95
+ if (parts.length > 0) {
96
+ parts[parts.length - 1] += colors.purple("*");
97
+ } else {
98
+ parts.push(colors.purple("*"));
99
+ }
100
+ }
101
+
102
+ const changeParts: string[] = [];
103
+
104
+ if (config.showChanges) {
105
+ const totalAdded = git.staged.added + git.unstaged.added;
106
+ const totalDeleted = git.staged.deleted + git.unstaged.deleted;
107
+ if (totalAdded > 0) changeParts.push(colors.green(`+${totalAdded}`));
108
+ if (totalDeleted > 0) changeParts.push(colors.red(`-${totalDeleted}`));
109
+ }
110
+
111
+ if (config.showStaged && git.staged.files > 0) {
112
+ changeParts.push(colors.gray(`~${git.staged.files}`));
113
+ }
114
+
115
+ if (config.showUnstaged && git.unstaged.files > 0) {
116
+ changeParts.push(colors.yellow(`~${git.unstaged.files}`));
117
+ }
118
+
119
+ if (changeParts.length > 0) {
120
+ parts.push(changeParts.join(" "));
121
+ }
122
+
123
+ return parts.join(" ");
124
+ }
125
+
38
126
  function formatSessionPart(
39
127
  cost: number,
40
128
  durationMs: number,
@@ -106,12 +194,179 @@ function formatSessionPart(
106
194
  return `${colors.gray("S:")} ${items.join(sep)}`;
107
195
  }
108
196
 
197
+ function formatLimitsPart(
198
+ fiveHour: UsageLimit | null,
199
+ periodCost: number,
200
+ config: StatuslineConfig["limits"],
201
+ ): string {
202
+ if (!config.enabled || !fiveHour) return "";
203
+
204
+ const parts: string[] = [];
205
+
206
+ if (config.cost.enabled && periodCost > 0) {
207
+ parts.push(
208
+ `${colors.gray("$")}${colors.dimWhite(formatCost(periodCost, config.cost.format))}`,
209
+ );
210
+ }
211
+
212
+ if (config.percentage.enabled) {
213
+ if (config.percentage.progressBar.enabled) {
214
+ parts.push(
215
+ formatProgressBar({
216
+ percentage: fiveHour.utilization,
217
+ length: config.percentage.progressBar.length,
218
+ style: config.percentage.progressBar.style,
219
+ colorMode: config.percentage.progressBar.color,
220
+ background: config.percentage.progressBar.background,
221
+ }),
222
+ );
223
+ }
224
+
225
+ if (config.percentage.showValue) {
226
+ parts.push(
227
+ `${colors.lightGray(fiveHour.utilization.toString())}${colors.gray("%")}`,
228
+ );
229
+ }
230
+ }
231
+
232
+ if (config.showPacingDelta && fiveHour.resets_at) {
233
+ const delta = calculateFiveHourDelta(
234
+ fiveHour.utilization,
235
+ fiveHour.resets_at,
236
+ );
237
+ parts.push(
238
+ `${colors.gray("(")}${formatPacingDelta(delta)}${colors.gray(")")}`,
239
+ );
240
+ }
241
+
242
+ if (config.showTimeLeft && fiveHour.resets_at) {
243
+ parts.push(colors.gray(`(${formatResetTime(fiveHour.resets_at)})`));
244
+ }
245
+
246
+ return parts.length > 0 ? `${colors.gray("L:")} ${parts.join(" ")}` : "";
247
+ }
248
+
249
+ function shouldShowWeekly(
250
+ config: StatuslineConfig["weeklyUsage"],
251
+ fiveHourUtilization: number | null,
252
+ ): boolean {
253
+ if (config.enabled === true) return true;
254
+ if (config.enabled === false) return false;
255
+ if (config.enabled === "90%" && fiveHourUtilization !== null) {
256
+ return fiveHourUtilization >= 90;
257
+ }
258
+ return false;
259
+ }
260
+
261
+ function calculateWeeklyDelta(
262
+ utilization: number,
263
+ resetsAt: string | null,
264
+ ): number {
265
+ if (!resetsAt) return 0;
266
+
267
+ const resetDate = new Date(resetsAt);
268
+ const now = new Date();
269
+ const diffMs = resetDate.getTime() - now.getTime();
270
+ const hoursRemaining = Math.max(0, diffMs / 3600000);
271
+ const timeElapsedPercent =
272
+ ((WEEKLY_HOURS - hoursRemaining) / WEEKLY_HOURS) * 100;
273
+
274
+ return utilization - timeElapsedPercent;
275
+ }
276
+
277
+ function formatPacingDelta(delta: number): string {
278
+ const sign = delta >= 0 ? "+" : "";
279
+ const value = `${sign}${delta.toFixed(1)}%`;
280
+
281
+ if (delta > 5) return colors.green(value);
282
+ if (delta > 0) return colors.lightGray(value);
283
+ if (delta > -10) return colors.yellow(value);
284
+ return colors.red(value);
285
+ }
286
+
287
+ function calculateFiveHourDelta(
288
+ utilization: number,
289
+ resetsAt: string | null,
290
+ ): number {
291
+ if (!resetsAt) return 0;
292
+
293
+ const resetDate = new Date(resetsAt);
294
+ const now = new Date();
295
+ const diffMs = resetDate.getTime() - now.getTime();
296
+ const minutesRemaining = Math.max(0, diffMs / 60000);
297
+ const timeElapsedPercent =
298
+ ((FIVE_HOUR_MINUTES - minutesRemaining) / FIVE_HOUR_MINUTES) * 100;
299
+
300
+ return utilization - timeElapsedPercent;
301
+ }
302
+
303
+ function formatWeeklyPart(
304
+ sevenDay: UsageLimit | null,
305
+ fiveHourUtilization: number | null,
306
+ periodCost: number,
307
+ config: StatuslineConfig["weeklyUsage"],
308
+ ): string {
309
+ if (!shouldShowWeekly(config, fiveHourUtilization) || !sevenDay) return "";
310
+
311
+ const parts: string[] = [];
312
+
313
+ if (config.cost.enabled && periodCost > 0) {
314
+ parts.push(
315
+ `${colors.gray("$")}${colors.dimWhite(formatCost(periodCost, config.cost.format))}`,
316
+ );
317
+ }
318
+
319
+ if (config.percentage.enabled) {
320
+ if (config.percentage.progressBar.enabled) {
321
+ parts.push(
322
+ formatProgressBar({
323
+ percentage: sevenDay.utilization,
324
+ length: config.percentage.progressBar.length,
325
+ style: config.percentage.progressBar.style,
326
+ colorMode: config.percentage.progressBar.color,
327
+ background: config.percentage.progressBar.background,
328
+ }),
329
+ );
330
+ }
331
+
332
+ if (config.percentage.showValue) {
333
+ parts.push(
334
+ `${colors.lightGray(sevenDay.utilization.toString())}${colors.gray("%")}`,
335
+ );
336
+ }
337
+ }
338
+
339
+ if (config.showPacingDelta && sevenDay.resets_at) {
340
+ const delta = calculateWeeklyDelta(
341
+ sevenDay.utilization,
342
+ sevenDay.resets_at,
343
+ );
344
+ parts.push(
345
+ `${colors.gray("(")}${formatPacingDelta(delta)}${colors.gray(")")}`,
346
+ );
347
+ }
348
+
349
+ if (config.showTimeLeft && sevenDay.resets_at) {
350
+ parts.push(colors.gray(`(${formatResetTime(sevenDay.resets_at)})`));
351
+ }
352
+
353
+ return parts.length > 0 ? `${colors.gray("W:")} ${parts.join(" ")}` : "";
354
+ }
355
+
356
+ function formatDailyPart(
357
+ todayCost: number,
358
+ config: StatuslineConfig["dailySpend"],
359
+ ): string {
360
+ if (!config.cost.enabled || todayCost <= 0) return "";
361
+ return `${colors.gray("D:")} ${colors.gray("$")}${colors.dimWhite(formatCost(todayCost, config.cost.format))}`;
362
+ }
363
+
109
364
  // ─────────────────────────────────────────────────────────────
110
- // MAIN RENDER FUNCTION - Simple version
365
+ // MAIN RENDER FUNCTION - Raw data + config = output
111
366
  // ─────────────────────────────────────────────────────────────
112
367
 
113
- export function renderStatusline(
114
- data: StatuslineData,
368
+ export function renderStatuslineRaw(
369
+ data: RawStatuslineData,
115
370
  config: StatuslineConfig,
116
371
  ): string {
117
372
  const sep = colors.gray(config.separator);
@@ -120,16 +375,12 @@ export function renderStatusline(
120
375
  // Line 1: Git + Path + Model
121
376
  const line1Parts: string[] = [];
122
377
 
123
- // Git branch
124
- if (data.branch) {
125
- line1Parts.push(colors.lightGray(data.branch));
126
- }
378
+ const gitPart = formatGitPart(data.git, config.git);
379
+ if (gitPart) line1Parts.push(gitPart);
127
380
 
128
- // Path
129
- const pathPart = formatPath(data.dirPath, config.pathDisplayMode);
381
+ const pathPart = formatPath(data.path, config.pathDisplayMode);
130
382
  line1Parts.push(colors.gray(pathPart));
131
383
 
132
- // Model name (hide Sonnet if configured)
133
384
  const isSonnet = data.modelName.toLowerCase().includes("sonnet");
134
385
  if (!isSonnet || config.showSonnetModel) {
135
386
  line1Parts.push(colors.peach(data.modelName));
@@ -137,21 +388,38 @@ export function renderStatusline(
137
388
 
138
389
  sections.push(line1Parts.join(` ${sep} `));
139
390
 
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
-
391
+ // Line 2: Session info
144
392
  const sessionPart = formatSessionPart(
145
- cost,
146
- durationMs,
393
+ data.cost,
394
+ data.durationMs,
147
395
  data.contextTokens,
148
396
  data.contextPercentage,
149
397
  config.context.maxContextTokens,
150
398
  config.session,
151
399
  );
152
-
153
400
  if (sessionPart) sections.push(sessionPart);
154
401
 
402
+ // Limits
403
+ const limitsPart = formatLimitsPart(
404
+ data.usageLimits?.five_hour ?? null,
405
+ data.periodCost ?? 0,
406
+ config.limits,
407
+ );
408
+ if (limitsPart) sections.push(limitsPart);
409
+
410
+ // Weekly
411
+ const weeklyPart = formatWeeklyPart(
412
+ data.usageLimits?.seven_day ?? null,
413
+ data.usageLimits?.five_hour?.utilization ?? null,
414
+ data.periodCost ?? 0,
415
+ config.weeklyUsage,
416
+ );
417
+ if (weeklyPart) sections.push(weeklyPart);
418
+
419
+ // Daily
420
+ const dailyPart = formatDailyPart(data.todayCost ?? 0, config.dailySpend);
421
+ if (dailyPart) sections.push(dailyPart);
422
+
155
423
  const output = sections.join(` ${sep} `);
156
424
 
157
425
  if (config.oneLine) return output;
@@ -163,9 +431,61 @@ export function renderStatusline(
163
431
  }
164
432
 
165
433
  // ─────────────────────────────────────────────────────────────
166
- // HELPERS
434
+ // LEGACY SUPPORT - For backwards compatibility with old data format
167
435
  // ─────────────────────────────────────────────────────────────
168
436
 
437
+ export function renderStatusline(
438
+ data: StatuslineData,
439
+ config: StatuslineConfig,
440
+ ): string {
441
+ // Convert legacy format to raw format
442
+ // Parse pre-formatted values back to raw (best effort)
443
+ const rawData: RawStatuslineData = {
444
+ git: parseGitFromBranch(data.branch),
445
+ path: data.dirPath.startsWith("~") ? data.dirPath : data.dirPath,
446
+ modelName: data.modelName,
447
+ cost: parseFloat(data.sessionCost.replace(/[$,]/g, "")) || 0,
448
+ durationMs: parseDurationToMs(data.sessionDuration),
449
+ contextTokens: data.contextTokens,
450
+ contextPercentage: data.contextPercentage,
451
+ usageLimits: data.usageLimits,
452
+ periodCost: data.periodCost,
453
+ todayCost: data.todayCost,
454
+ };
455
+
456
+ return renderStatuslineRaw(rawData, config);
457
+ }
458
+
459
+ // Helper to parse legacy branch string back to git data
460
+ function parseGitFromBranch(branch: string): RawGitData | null {
461
+ if (!branch) return null;
462
+
463
+ // Parse "main* +10 -5" format
464
+ const dirty = branch.includes("*");
465
+ const branchName =
466
+ branch.replace(/\*.*$/, "").replace(/\*/, "").trim() || "main";
467
+
468
+ const addMatch = branch.match(/\+(\d+)/);
469
+ const delMatch = branch.match(/-(\d+)/);
470
+ const added = addMatch ? parseInt(addMatch[1], 10) : 0;
471
+ const deleted = delMatch ? parseInt(delMatch[1], 10) : 0;
472
+
473
+ return {
474
+ branch: branchName,
475
+ dirty,
476
+ staged: {
477
+ files: 0,
478
+ added: Math.floor(added / 2),
479
+ deleted: Math.floor(deleted / 2),
480
+ },
481
+ unstaged: {
482
+ files: 0,
483
+ added: Math.ceil(added / 2),
484
+ deleted: Math.ceil(deleted / 2),
485
+ },
486
+ };
487
+ }
488
+
169
489
  // Helper to parse "12m" or "1h 30m" back to ms
170
490
  function parseDurationToMs(duration: string): number {
171
491
  let ms = 0;
@@ -0,0 +1,15 @@
1
+ export function normalizeResetsAt(resetsAt: string): string {
2
+ try {
3
+ const date = new Date(resetsAt);
4
+ const minutes = date.getMinutes();
5
+ const roundedMinutes = Math.round(minutes / 5) * 5;
6
+
7
+ date.setMinutes(roundedMinutes);
8
+ date.setSeconds(0);
9
+ date.setMilliseconds(0);
10
+
11
+ return date.toISOString();
12
+ } catch {
13
+ return resetsAt;
14
+ }
15
+ }