aiblueprint-cli 1.4.13 → 1.4.15

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 (26) 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/dist/cli.js +16 -11
  26. package/package.json +1 -1
@@ -0,0 +1,50 @@
1
+ # Scripts - Project Memory
2
+
3
+ Monorepo containing Claude Code utilities and extensions.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ scripts/
9
+ ├── auto-rename-session/ # Auto-generates session titles via AI
10
+ ├── claude-code-ai/ # Shared Claude API helpers
11
+ ├── command-validator/ # Security validation for bash commands
12
+ ├── statusline/ # Custom statusline for Claude Code
13
+ └── package.json # Root package with all scripts
14
+ ```
15
+
16
+ ## Commands
17
+
18
+ ```bash
19
+ bun run test # Run ALL tests (186 tests)
20
+ bun run lint # Lint all packages
21
+ ```
22
+
23
+ ### Per-Package Commands
24
+
25
+ | Package | Test | Start |
26
+ |---------|------|-------|
27
+ | auto-rename-session | `bun run auto-rename:test` | `bun run auto-rename:start` |
28
+ | claude-code-ai | `bun run ai:test` | - |
29
+ | command-validator | `bun run validator:test` | `bun run validator:cli` |
30
+ | statusline | `bun run statusline:test` | `bun run statusline:start` |
31
+
32
+ ## Cross-Platform Support
33
+
34
+ All packages support macOS, Linux, and Windows (via WSL):
35
+ - Use `path.join()` instead of string concatenation
36
+ - Use `os.homedir()` instead of `process.env.HOME`
37
+ - Use `path.sep` or regex `[/\\]` for path splitting
38
+
39
+ ## Shared Dependencies
40
+
41
+ - `@ai-sdk/anthropic` + `ai` - Claude API access
42
+ - `picocolors` - Terminal colors
43
+ - `@biomejs/biome` - Linting/formatting
44
+ - `bun:test` - Testing
45
+
46
+ ## Credentials
47
+
48
+ Claude Code OAuth tokens are retrieved via `claude-code-ai/helper/credentials.ts`:
49
+ - macOS: Keychain (`security find-generic-password`)
50
+ - Linux/Windows: `~/.claude/.credentials.json`
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "indentStyle": "tab"
14
+ },
15
+ "linter": {
16
+ "enabled": true,
17
+ "rules": {
18
+ "recommended": true,
19
+ "suspicious": {
20
+ "noControlCharactersInRegex": "off"
21
+ }
22
+ }
23
+ },
24
+ "javascript": {
25
+ "formatter": {
26
+ "quoteStyle": "double"
27
+ }
28
+ },
29
+ "assist": {
30
+ "enabled": true,
31
+ "actions": {
32
+ "source": {
33
+ "organizeImports": "on"
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "scripts",
3
+ "type": "module",
4
+ "scripts": {
5
+ "test": "bun test auto-rename-session claude-code-ai statusline command-validator",
6
+ "auto-rename:start": "bun auto-rename-session/src/index.ts",
7
+ "auto-rename:rename-all": "bun auto-rename-session/src/rename-all.ts",
8
+ "auto-rename:test": "bun test auto-rename-session",
9
+ "validator:cli": "bun command-validator/src/cli.ts",
10
+ "validator:test": "bun test command-validator",
11
+ "validator:lint": "biome check --write command-validator",
12
+ "ai:test": "bun test claude-code-ai",
13
+ "statusline:start": "bun statusline/src/index.ts",
14
+ "statusline:test-fixtures": "bun statusline/test-with-fixtures.ts",
15
+ "statusline:test": "bun test statusline",
16
+ "statusline:spend:today": "bun statusline/src/lib/features/spend/commands/spend-today.ts",
17
+ "statusline:spend:month": "bun statusline/src/lib/features/spend/commands/spend-month.ts",
18
+ "statusline:spend:project": "bun statusline/src/lib/features/spend/commands/spend-project.ts",
19
+ "statusline:migrate": "bun statusline/src/lib/features/spend/commands/migrate-to-sqlite.ts",
20
+ "statusline:weekly": "bun statusline/src/lib/features/limits/commands/weekly-analysis.ts",
21
+ "statusline:lint": "biome check --write statusline",
22
+ "lint": "biome check --write .",
23
+ "format": "biome format --write ."
24
+ },
25
+ "dependencies": {
26
+ "@ai-sdk/anthropic": "^3.0.6",
27
+ "ai": "^6.0.11",
28
+ "picocolors": "^1.1.1",
29
+ "table": "^6.9.0",
30
+ "zod": "^4.3.5"
31
+ },
32
+ "devDependencies": {
33
+ "@biomejs/biome": "^2.3.2",
34
+ "@types/bun": "latest"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": "^5.0.0"
38
+ }
39
+ }
@@ -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
+ });