aiblueprint-cli 1.4.12 → 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 (69) hide show
  1. package/claude-code-config/scripts/.claude/commands/fix-on-my-computer.md +87 -0
  2. package/claude-code-config/scripts/CLAUDE.md +50 -0
  3. package/claude-code-config/scripts/{statusline/biome.json → biome.json} +5 -2
  4. package/claude-code-config/scripts/bun.lockb +0 -0
  5. package/claude-code-config/scripts/command-validator/CLAUDE.md +112 -0
  6. package/claude-code-config/scripts/command-validator/src/__tests__/validator.test.ts +62 -111
  7. package/claude-code-config/scripts/command-validator/src/cli.ts +5 -3
  8. package/claude-code-config/scripts/command-validator/src/lib/security-rules.ts +3 -4
  9. package/claude-code-config/scripts/command-validator/src/lib/types.ts +1 -0
  10. package/claude-code-config/scripts/command-validator/src/lib/validator.ts +47 -317
  11. package/claude-code-config/scripts/package.json +39 -0
  12. package/claude-code-config/scripts/statusline/CLAUDE.md +29 -7
  13. package/claude-code-config/scripts/statusline/README.md +89 -1
  14. package/claude-code-config/scripts/statusline/__tests__/context.test.ts +229 -0
  15. package/claude-code-config/scripts/statusline/__tests__/formatters.test.ts +108 -0
  16. package/claude-code-config/scripts/statusline/__tests__/statusline.test.ts +309 -0
  17. package/claude-code-config/scripts/statusline/data/.gitignore +8 -0
  18. package/claude-code-config/scripts/statusline/data/.gitkeep +0 -0
  19. package/claude-code-config/scripts/statusline/defaults.json +79 -0
  20. package/claude-code-config/scripts/statusline/docs/ARCHITECTURE.md +166 -0
  21. package/claude-code-config/scripts/statusline/fixtures/mock-transcript.jsonl +4 -0
  22. package/claude-code-config/scripts/statusline/fixtures/test-input.json +12 -2
  23. package/claude-code-config/scripts/statusline/src/index.ts +175 -24
  24. package/claude-code-config/scripts/statusline/src/lib/config-types.ts +104 -0
  25. package/claude-code-config/scripts/statusline/src/lib/config.ts +21 -0
  26. package/claude-code-config/scripts/statusline/src/lib/context.ts +32 -11
  27. package/claude-code-config/scripts/statusline/src/lib/formatters.ts +360 -22
  28. package/claude-code-config/scripts/statusline/src/lib/git.ts +100 -0
  29. package/claude-code-config/scripts/statusline/src/lib/menu-factories.ts +224 -0
  30. package/claude-code-config/scripts/statusline/src/lib/presets.ts +177 -0
  31. package/claude-code-config/scripts/statusline/src/lib/render-pure.ts +497 -0
  32. package/claude-code-config/scripts/statusline/src/lib/types.ts +11 -0
  33. package/claude-code-config/scripts/statusline/src/lib/utils.ts +15 -0
  34. package/claude-code-config/scripts/statusline/src/tests/spend-v2.test.ts +306 -0
  35. package/claude-code-config/scripts/statusline/statusline.config.json +79 -0
  36. package/claude-code-config/scripts/statusline/test-with-fixtures.ts +37 -0
  37. package/claude-code-config/scripts/tsconfig.json +27 -0
  38. package/claude-code-config/skills/claude-memory/SKILL.md +689 -0
  39. package/claude-code-config/skills/claude-memory/references/comprehensive-example.md +175 -0
  40. package/claude-code-config/skills/claude-memory/references/project-patterns.md +334 -0
  41. package/claude-code-config/skills/claude-memory/references/prompting-techniques.md +411 -0
  42. package/claude-code-config/skills/claude-memory/references/section-templates.md +347 -0
  43. package/claude-code-config/skills/create-slash-commands/SKILL.md +1110 -0
  44. package/claude-code-config/skills/create-slash-commands/references/arguments.md +273 -0
  45. package/claude-code-config/skills/create-slash-commands/references/patterns.md +947 -0
  46. package/claude-code-config/skills/create-slash-commands/references/prompt-examples.md +656 -0
  47. package/claude-code-config/skills/create-slash-commands/references/tool-restrictions.md +389 -0
  48. package/claude-code-config/skills/create-subagents/SKILL.md +425 -0
  49. package/claude-code-config/skills/create-subagents/references/context-management.md +567 -0
  50. package/claude-code-config/skills/create-subagents/references/debugging-agents.md +714 -0
  51. package/claude-code-config/skills/create-subagents/references/error-handling-and-recovery.md +502 -0
  52. package/claude-code-config/skills/create-subagents/references/evaluation-and-testing.md +374 -0
  53. package/claude-code-config/skills/create-subagents/references/orchestration-patterns.md +591 -0
  54. package/claude-code-config/skills/create-subagents/references/subagents.md +599 -0
  55. package/claude-code-config/skills/create-subagents/references/writing-subagent-prompts.md +513 -0
  56. package/package.json +1 -1
  57. package/claude-code-config/commands/apex.md +0 -109
  58. package/claude-code-config/commands/tasks/run-task.md +0 -220
  59. package/claude-code-config/commands/utils/watch-ci.md +0 -47
  60. package/claude-code-config/scripts/command-validator/biome.json +0 -29
  61. package/claude-code-config/scripts/command-validator/bun.lockb +0 -0
  62. package/claude-code-config/scripts/command-validator/package.json +0 -27
  63. package/claude-code-config/scripts/command-validator/vitest.config.ts +0 -7
  64. package/claude-code-config/scripts/hook-post-file.ts +0 -162
  65. package/claude-code-config/scripts/statusline/bun.lockb +0 -0
  66. package/claude-code-config/scripts/statusline/package.json +0 -19
  67. package/claude-code-config/scripts/statusline/statusline.config.ts +0 -25
  68. package/claude-code-config/scripts/validate-command.js +0 -712
  69. package/claude-code-config/scripts/validate-command.readme.md +0 -283
@@ -0,0 +1,229 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getContextData, getContextLength } from "../src/lib/context";
5
+
6
+ const TEST_DIR = join(import.meta.dir, "..", "fixtures", "test-transcripts");
7
+
8
+ beforeAll(() => {
9
+ mkdirSync(TEST_DIR, { recursive: true });
10
+ });
11
+
12
+ afterAll(() => {
13
+ rmSync(TEST_DIR, { recursive: true, force: true });
14
+ });
15
+
16
+ function createTranscript(lines: object[]): string {
17
+ return lines.map((l) => JSON.stringify(l)).join("\n");
18
+ }
19
+
20
+ describe("getContextLength", () => {
21
+ it("should return 0 for empty transcript", async () => {
22
+ const path = join(TEST_DIR, "empty.jsonl");
23
+ writeFileSync(path, "");
24
+ expect(await getContextLength(path)).toBe(0);
25
+ });
26
+
27
+ it("should return 0 for transcript with no usage data", async () => {
28
+ const path = join(TEST_DIR, "no-usage.jsonl");
29
+ const content = createTranscript([
30
+ { type: "user", message: { role: "user", content: "hello" } },
31
+ ]);
32
+ writeFileSync(path, content);
33
+ expect(await getContextLength(path)).toBe(0);
34
+ });
35
+
36
+ it("should calculate tokens from most recent entry", async () => {
37
+ const path = join(TEST_DIR, "with-usage.jsonl");
38
+ const content = createTranscript([
39
+ {
40
+ timestamp: "2025-01-01T10:00:00Z",
41
+ message: {
42
+ role: "assistant",
43
+ usage: { input_tokens: 1000, output_tokens: 100 },
44
+ },
45
+ },
46
+ {
47
+ timestamp: "2025-01-01T10:05:00Z",
48
+ message: {
49
+ role: "assistant",
50
+ usage: { input_tokens: 5000, output_tokens: 200 },
51
+ },
52
+ },
53
+ ]);
54
+ writeFileSync(path, content);
55
+ expect(await getContextLength(path)).toBe(5000);
56
+ });
57
+
58
+ it("should include cache tokens in calculation", async () => {
59
+ const path = join(TEST_DIR, "with-cache.jsonl");
60
+ const content = createTranscript([
61
+ {
62
+ timestamp: "2025-01-01T10:00:00Z",
63
+ message: {
64
+ role: "assistant",
65
+ usage: {
66
+ input_tokens: 1000,
67
+ cache_read_input_tokens: 2000,
68
+ cache_creation_input_tokens: 3000,
69
+ output_tokens: 100,
70
+ },
71
+ },
72
+ },
73
+ ]);
74
+ writeFileSync(path, content);
75
+ expect(await getContextLength(path)).toBe(6000);
76
+ });
77
+
78
+ it("should skip sidechain entries", async () => {
79
+ const path = join(TEST_DIR, "with-sidechain.jsonl");
80
+ const content = createTranscript([
81
+ {
82
+ timestamp: "2025-01-01T10:00:00Z",
83
+ message: {
84
+ role: "assistant",
85
+ usage: { input_tokens: 1000, output_tokens: 100 },
86
+ },
87
+ },
88
+ {
89
+ timestamp: "2025-01-01T10:05:00Z",
90
+ isSidechain: true,
91
+ message: {
92
+ role: "assistant",
93
+ usage: { input_tokens: 99999, output_tokens: 200 },
94
+ },
95
+ },
96
+ ]);
97
+ writeFileSync(path, content);
98
+ expect(await getContextLength(path)).toBe(1000);
99
+ });
100
+
101
+ it("should skip API error messages", async () => {
102
+ const path = join(TEST_DIR, "with-error.jsonl");
103
+ const content = createTranscript([
104
+ {
105
+ timestamp: "2025-01-01T10:00:00Z",
106
+ message: {
107
+ role: "assistant",
108
+ usage: { input_tokens: 1000, output_tokens: 100 },
109
+ },
110
+ },
111
+ {
112
+ timestamp: "2025-01-01T10:05:00Z",
113
+ isApiErrorMessage: true,
114
+ message: {
115
+ role: "assistant",
116
+ usage: { input_tokens: 99999, output_tokens: 200 },
117
+ },
118
+ },
119
+ ]);
120
+ writeFileSync(path, content);
121
+ expect(await getContextLength(path)).toBe(1000);
122
+ });
123
+
124
+ it("should return 0 for non-existent file", async () => {
125
+ expect(await getContextLength("/non/existent/path.jsonl")).toBe(0);
126
+ });
127
+ });
128
+
129
+ describe("getContextData", () => {
130
+ it("should return zeros for non-existent file", async () => {
131
+ const result = await getContextData({
132
+ transcriptPath: "/non/existent/path.jsonl",
133
+ maxContextTokens: 200000,
134
+ autocompactBufferTokens: 45000,
135
+ });
136
+ expect(result).toEqual({ tokens: 0, percentage: 0 });
137
+ });
138
+
139
+ it("should calculate percentage correctly", async () => {
140
+ const path = join(TEST_DIR, "percentage.jsonl");
141
+ const content = createTranscript([
142
+ {
143
+ timestamp: "2025-01-01T10:00:00Z",
144
+ message: {
145
+ role: "assistant",
146
+ usage: { input_tokens: 100000, output_tokens: 100 },
147
+ },
148
+ },
149
+ ]);
150
+ writeFileSync(path, content);
151
+
152
+ const result = await getContextData({
153
+ transcriptPath: path,
154
+ maxContextTokens: 200000,
155
+ autocompactBufferTokens: 45000,
156
+ });
157
+
158
+ expect(result.tokens).toBe(100000);
159
+ expect(result.percentage).toBe(50);
160
+ });
161
+
162
+ it("should add autocompact buffer when useUsableContextOnly is true", async () => {
163
+ const path = join(TEST_DIR, "usable.jsonl");
164
+ const content = createTranscript([
165
+ {
166
+ timestamp: "2025-01-01T10:00:00Z",
167
+ message: {
168
+ role: "assistant",
169
+ usage: { input_tokens: 50000, output_tokens: 100 },
170
+ },
171
+ },
172
+ ]);
173
+ writeFileSync(path, content);
174
+
175
+ const result = await getContextData({
176
+ transcriptPath: path,
177
+ maxContextTokens: 200000,
178
+ autocompactBufferTokens: 45000,
179
+ useUsableContextOnly: true,
180
+ });
181
+
182
+ expect(result.tokens).toBe(95000);
183
+ });
184
+
185
+ it("should add overhead tokens", async () => {
186
+ const path = join(TEST_DIR, "overhead.jsonl");
187
+ const content = createTranscript([
188
+ {
189
+ timestamp: "2025-01-01T10:00:00Z",
190
+ message: {
191
+ role: "assistant",
192
+ usage: { input_tokens: 50000, output_tokens: 100 },
193
+ },
194
+ },
195
+ ]);
196
+ writeFileSync(path, content);
197
+
198
+ const result = await getContextData({
199
+ transcriptPath: path,
200
+ maxContextTokens: 200000,
201
+ autocompactBufferTokens: 45000,
202
+ overheadTokens: 20000,
203
+ });
204
+
205
+ expect(result.tokens).toBe(70000);
206
+ });
207
+
208
+ it("should cap percentage at 100", async () => {
209
+ const path = join(TEST_DIR, "over100.jsonl");
210
+ const content = createTranscript([
211
+ {
212
+ timestamp: "2025-01-01T10:00:00Z",
213
+ message: {
214
+ role: "assistant",
215
+ usage: { input_tokens: 250000, output_tokens: 100 },
216
+ },
217
+ },
218
+ ]);
219
+ writeFileSync(path, content);
220
+
221
+ const result = await getContextData({
222
+ transcriptPath: path,
223
+ maxContextTokens: 200000,
224
+ autocompactBufferTokens: 45000,
225
+ });
226
+
227
+ expect(result.percentage).toBe(100);
228
+ });
229
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ formatCost,
4
+ formatDuration,
5
+ formatPath,
6
+ formatResetTime,
7
+ formatTokens,
8
+ } from "../src/lib/formatters";
9
+
10
+ describe("formatPath", () => {
11
+ it("should return basename for basename mode", () => {
12
+ expect(formatPath("/Users/test/project/src/file.ts", "basename")).toBe(
13
+ "file.ts",
14
+ );
15
+ });
16
+
17
+ it("should truncate long paths in truncated mode", () => {
18
+ const result = formatPath("/Users/test/deep/nested/path/file.ts");
19
+ expect(result).toContain("path");
20
+ expect(result).toContain("file.ts");
21
+ });
22
+
23
+ it("should handle Windows-style paths", () => {
24
+ expect(formatPath("C:\\Users\\test\\project\\file.ts", "basename")).toBe(
25
+ "file.ts",
26
+ );
27
+ });
28
+
29
+ it("should handle mixed separators", () => {
30
+ expect(formatPath("/Users/test\\mixed/path", "basename")).toBe("path");
31
+ });
32
+ });
33
+
34
+ describe("formatCost", () => {
35
+ it("should format with 1 decimal by default", () => {
36
+ expect(formatCost(1.234)).toBe("1.2");
37
+ });
38
+
39
+ it("should format as integer when specified", () => {
40
+ expect(formatCost(1.789, "integer")).toBe("2");
41
+ });
42
+
43
+ it("should format with 2 decimals when specified", () => {
44
+ expect(formatCost(1.789, "decimal2")).toBe("1.79");
45
+ });
46
+ });
47
+
48
+ describe("formatTokens", () => {
49
+ it("should format thousands with k suffix", () => {
50
+ const result = formatTokens(5000);
51
+ expect(result).toContain("5.0");
52
+ expect(result).toContain("k");
53
+ });
54
+
55
+ it("should format millions with m suffix", () => {
56
+ const result = formatTokens(1500000);
57
+ expect(result).toContain("1.5");
58
+ expect(result).toContain("m");
59
+ });
60
+
61
+ it("should return raw number for small values", () => {
62
+ const result = formatTokens(500);
63
+ expect(result).toContain("500");
64
+ });
65
+
66
+ it("should hide decimals when showDecimals is false", () => {
67
+ const result = formatTokens(5500, false);
68
+ expect(result).toContain("6");
69
+ });
70
+ });
71
+
72
+ describe("formatDuration", () => {
73
+ it("should format minutes only for short durations", () => {
74
+ expect(formatDuration(300000)).toBe("5m");
75
+ });
76
+
77
+ it("should format hours and minutes for long durations", () => {
78
+ expect(formatDuration(5400000)).toBe("1h 30m");
79
+ });
80
+
81
+ it("should handle zero duration", () => {
82
+ expect(formatDuration(0)).toBe("0m");
83
+ });
84
+ });
85
+
86
+ describe("formatResetTime", () => {
87
+ it("should return 'now' for past times", () => {
88
+ const pastTime = new Date(Date.now() - 60000).toISOString();
89
+ expect(formatResetTime(pastTime)).toBe("now");
90
+ });
91
+
92
+ it("should format future times with hours and minutes", () => {
93
+ const futureTime = new Date(Date.now() + 3700000).toISOString();
94
+ const result = formatResetTime(futureTime);
95
+ expect(result).toContain("h");
96
+ expect(result).toContain("m");
97
+ });
98
+
99
+ it("should format minutes only for short durations", () => {
100
+ const futureTime = new Date(Date.now() + 1800000).toISOString();
101
+ const result = formatResetTime(futureTime);
102
+ expect(result).toMatch(/^\d+m$/);
103
+ });
104
+
105
+ it("should return N/A for invalid dates", () => {
106
+ expect(formatResetTime("invalid-date")).toBe("N/A");
107
+ });
108
+ });
@@ -0,0 +1,309 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { renderStatusline, type StatuslineData } from "../src/index";
3
+ import { defaultConfig, type StatuslineConfig } from "../src/lib/config";
4
+
5
+ function createMockData(
6
+ overrides: Partial<StatuslineData> = {},
7
+ ): StatuslineData {
8
+ return {
9
+ branch: "main",
10
+ dirPath: "~/project",
11
+ modelName: "Sonnet 4.5",
12
+ sessionCost: "0.5",
13
+ sessionDuration: "10m",
14
+ contextTokens: 50000,
15
+ contextPercentage: 25,
16
+ usageLimits: {
17
+ five_hour: { utilization: 30, resets_at: "2025-01-01T15:00:00Z" },
18
+ seven_day: { utilization: 10, resets_at: "2025-01-07T00:00:00Z" },
19
+ },
20
+ periodCost: 1.5,
21
+ todayCost: 2.0,
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function createConfig(
27
+ overrides: Partial<StatuslineConfig> = {},
28
+ ): StatuslineConfig {
29
+ return { ...defaultConfig, ...overrides };
30
+ }
31
+
32
+ describe("renderStatusline", () => {
33
+ describe("basic rendering", () => {
34
+ it("should render branch and directory", () => {
35
+ const data = createMockData();
36
+ const config = createConfig();
37
+ const output = renderStatusline(data, config);
38
+
39
+ expect(output).toContain("main");
40
+ expect(output).toContain("~/project");
41
+ });
42
+
43
+ it("should hide Sonnet model name when showSonnetModel is false", () => {
44
+ const data = createMockData({ modelName: "Sonnet 4.5" });
45
+ const config = createConfig({ showSonnetModel: false });
46
+ const output = renderStatusline(data, config);
47
+
48
+ expect(output).not.toContain("Sonnet");
49
+ });
50
+
51
+ it("should show Sonnet model name when showSonnetModel is true", () => {
52
+ const data = createMockData({ modelName: "Sonnet 4.5" });
53
+ const config = createConfig({ showSonnetModel: true });
54
+ const output = renderStatusline(data, config);
55
+
56
+ expect(output).toContain("Sonnet");
57
+ });
58
+
59
+ it("should always show non-Sonnet models", () => {
60
+ const data = createMockData({ modelName: "Opus 4.5" });
61
+ const config = createConfig({ showSonnetModel: false });
62
+ const output = renderStatusline(data, config);
63
+
64
+ expect(output).toContain("Opus");
65
+ });
66
+ });
67
+
68
+ describe("session info", () => {
69
+ it("should show cost when enabled", () => {
70
+ const data = createMockData({ sessionCost: "1.5" });
71
+ const config = createConfig({
72
+ session: {
73
+ ...defaultConfig.session,
74
+ cost: { enabled: true, format: "decimal1" },
75
+ },
76
+ });
77
+ const output = renderStatusline(data, config);
78
+
79
+ expect(output).toContain("$");
80
+ expect(output).toContain("1.5");
81
+ });
82
+
83
+ it("should hide cost when disabled", () => {
84
+ const data = createMockData({ sessionCost: "1.5" });
85
+ const config = createConfig({
86
+ session: {
87
+ ...defaultConfig.session,
88
+ cost: { enabled: false, format: "decimal1" },
89
+ },
90
+ });
91
+ const output = renderStatusline(data, config);
92
+
93
+ expect(output).toContain("S:");
94
+ });
95
+
96
+ it("should show duration when enabled", () => {
97
+ const data = createMockData({ sessionDuration: "15m" });
98
+ const config = createConfig({
99
+ session: {
100
+ ...defaultConfig.session,
101
+ duration: { enabled: true },
102
+ },
103
+ });
104
+ const output = renderStatusline(data, config);
105
+
106
+ expect(output).toContain("15m");
107
+ });
108
+
109
+ it("should show percentage when enabled", () => {
110
+ const data = createMockData({ contextPercentage: 45 });
111
+ const config = createConfig({
112
+ session: {
113
+ ...defaultConfig.session,
114
+ percentage: {
115
+ enabled: true,
116
+ showValue: true,
117
+ progressBar: {
118
+ ...defaultConfig.session.percentage.progressBar,
119
+ enabled: false,
120
+ },
121
+ },
122
+ },
123
+ });
124
+ const output = renderStatusline(data, config);
125
+
126
+ expect(output).toContain("45");
127
+ expect(output).toContain("%");
128
+ });
129
+ });
130
+
131
+ describe("limits section", () => {
132
+ it("should show limits when enabled", () => {
133
+ const data = createMockData({
134
+ usageLimits: {
135
+ five_hour: { utilization: 50, resets_at: "2025-01-01T15:00:00Z" },
136
+ seven_day: null,
137
+ },
138
+ });
139
+ const config = createConfig({
140
+ limits: {
141
+ ...defaultConfig.limits,
142
+ enabled: true,
143
+ percentage: {
144
+ enabled: true,
145
+ showValue: true,
146
+ progressBar: {
147
+ ...defaultConfig.limits.percentage.progressBar,
148
+ enabled: false,
149
+ },
150
+ },
151
+ },
152
+ });
153
+ const output = renderStatusline(data, config);
154
+
155
+ expect(output).toContain("L:");
156
+ expect(output).toContain("50");
157
+ });
158
+
159
+ it("should hide limits when disabled", () => {
160
+ const data = createMockData();
161
+ const config = createConfig({
162
+ limits: { ...defaultConfig.limits, enabled: false },
163
+ });
164
+ const output = renderStatusline(data, config);
165
+
166
+ expect(output).not.toContain("L:");
167
+ });
168
+
169
+ it("should show reset time when enabled", () => {
170
+ const futureTime = new Date(Date.now() + 3600000).toISOString();
171
+ const data = createMockData({
172
+ usageLimits: {
173
+ five_hour: { utilization: 50, resets_at: futureTime },
174
+ seven_day: null,
175
+ },
176
+ });
177
+ const config = createConfig({
178
+ limits: {
179
+ ...defaultConfig.limits,
180
+ enabled: true,
181
+ showTimeLeft: true,
182
+ },
183
+ });
184
+ const output = renderStatusline(data, config);
185
+
186
+ expect(output).toMatch(/\(\d+[hm]/);
187
+ });
188
+ });
189
+
190
+ describe("weekly usage", () => {
191
+ it("should show weekly when enabled=true", () => {
192
+ const data = createMockData({
193
+ usageLimits: {
194
+ five_hour: { utilization: 30, resets_at: null },
195
+ seven_day: { utilization: 20, resets_at: null },
196
+ },
197
+ });
198
+ const config = createConfig({
199
+ weeklyUsage: {
200
+ ...defaultConfig.weeklyUsage,
201
+ enabled: true,
202
+ percentage: {
203
+ enabled: true,
204
+ showValue: true,
205
+ progressBar: {
206
+ ...defaultConfig.weeklyUsage.percentage.progressBar,
207
+ enabled: false,
208
+ },
209
+ },
210
+ },
211
+ });
212
+ const output = renderStatusline(data, config);
213
+
214
+ expect(output).toContain("W:");
215
+ });
216
+
217
+ it("should show weekly when enabled=90% and five_hour >= 90", () => {
218
+ const data = createMockData({
219
+ usageLimits: {
220
+ five_hour: { utilization: 95, resets_at: null },
221
+ seven_day: { utilization: 20, resets_at: null },
222
+ },
223
+ });
224
+ const config = createConfig({
225
+ weeklyUsage: {
226
+ ...defaultConfig.weeklyUsage,
227
+ enabled: "90%",
228
+ percentage: {
229
+ enabled: true,
230
+ showValue: true,
231
+ progressBar: {
232
+ ...defaultConfig.weeklyUsage.percentage.progressBar,
233
+ enabled: false,
234
+ },
235
+ },
236
+ },
237
+ });
238
+ const output = renderStatusline(data, config);
239
+
240
+ expect(output).toContain("W:");
241
+ });
242
+
243
+ it("should hide weekly when enabled=90% and five_hour < 90", () => {
244
+ const data = createMockData({
245
+ usageLimits: {
246
+ five_hour: { utilization: 50, resets_at: null },
247
+ seven_day: { utilization: 20, resets_at: null },
248
+ },
249
+ });
250
+ const config = createConfig({
251
+ weeklyUsage: { ...defaultConfig.weeklyUsage, enabled: "90%" },
252
+ });
253
+ const output = renderStatusline(data, config);
254
+
255
+ expect(output).not.toContain("W:");
256
+ });
257
+ });
258
+
259
+ describe("daily spend", () => {
260
+ it("should show daily cost when enabled and > 0", () => {
261
+ const data = createMockData({ todayCost: 5.5 });
262
+ const config = createConfig({
263
+ dailySpend: { cost: { enabled: true, format: "decimal1" } },
264
+ });
265
+ const output = renderStatusline(data, config);
266
+
267
+ expect(output).toContain("D:");
268
+ expect(output).toContain("5.5");
269
+ });
270
+
271
+ it("should hide daily cost when todayCost is 0", () => {
272
+ const data = createMockData({ todayCost: 0 });
273
+ const config = createConfig({
274
+ dailySpend: { cost: { enabled: true, format: "decimal1" } },
275
+ });
276
+ const output = renderStatusline(data, config);
277
+
278
+ expect(output).not.toContain("D:");
279
+ });
280
+ });
281
+
282
+ describe("separators", () => {
283
+ it("should use configured separator", () => {
284
+ const data = createMockData();
285
+ const config = createConfig({ separator: "|" });
286
+ const output = renderStatusline(data, config);
287
+
288
+ expect(output).toContain("|");
289
+ });
290
+ });
291
+
292
+ describe("one line vs two lines", () => {
293
+ it("should render on one line when oneLine is true", () => {
294
+ const data = createMockData();
295
+ const config = createConfig({ oneLine: true });
296
+ const output = renderStatusline(data, config);
297
+
298
+ expect(output.split("\n").length).toBe(1);
299
+ });
300
+
301
+ it("should render on two lines when oneLine is false", () => {
302
+ const data = createMockData();
303
+ const config = createConfig({ oneLine: false });
304
+ const output = renderStatusline(data, config);
305
+
306
+ expect(output).toContain("\n");
307
+ });
308
+ });
309
+ });
@@ -0,0 +1,8 @@
1
+ # Ignore all data files
2
+ *.db
3
+ *.json
4
+
5
+ # But keep this .gitignore
6
+ !.gitignore
7
+
8
+ last_payload.txt