rafcode 2.3.0 → 2.4.0

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 (109) hide show
  1. package/CLAUDE.md +19 -4
  2. package/RAF/ahvrih-rate-forge/decisions.md +70 -0
  3. package/RAF/ahvrih-rate-forge/input.md +44 -0
  4. package/RAF/ahvrih-rate-forge/outcomes/01-remove-claude-command-config.md +58 -0
  5. package/RAF/ahvrih-rate-forge/outcomes/02-fix-mixed-attempt-cost.md +46 -0
  6. package/RAF/ahvrih-rate-forge/outcomes/03-rate-limit-estimation.md +82 -0
  7. package/RAF/ahvrih-rate-forge/outcomes/04-show-version-in-do-logs.md +45 -0
  8. package/RAF/ahvrih-rate-forge/outcomes/05-sync-main-before-worktree.md +96 -0
  9. package/RAF/ahvrih-rate-forge/outcomes/06-sync-readme-with-codebase.md +45 -0
  10. package/RAF/ahvrih-rate-forge/outcomes/07-no-session-persistence.md +26 -0
  11. package/RAF/ahvrih-rate-forge/outcomes/08-plan-execution-metadata.md +130 -0
  12. package/RAF/ahvrih-rate-forge/plans/01-remove-claude-command-config.md +36 -0
  13. package/RAF/ahvrih-rate-forge/plans/02-fix-mixed-attempt-cost.md +33 -0
  14. package/RAF/ahvrih-rate-forge/plans/03-rate-limit-estimation.md +82 -0
  15. package/RAF/ahvrih-rate-forge/plans/04-show-version-in-do-logs.md +32 -0
  16. package/RAF/ahvrih-rate-forge/plans/05-sync-main-before-worktree.md +40 -0
  17. package/RAF/ahvrih-rate-forge/plans/06-sync-readme-with-codebase.md +61 -0
  18. package/RAF/ahvrih-rate-forge/plans/07-no-session-persistence.md +28 -0
  19. package/RAF/ahvrih-rate-forge/plans/08-plan-execution-metadata.md +123 -0
  20. package/README.md +27 -7
  21. package/dist/commands/config.d.ts.map +1 -1
  22. package/dist/commands/config.js +1 -6
  23. package/dist/commands/config.js.map +1 -1
  24. package/dist/commands/do.d.ts.map +1 -1
  25. package/dist/commands/do.js +106 -18
  26. package/dist/commands/do.js.map +1 -1
  27. package/dist/commands/plan.d.ts.map +1 -1
  28. package/dist/commands/plan.js +77 -2
  29. package/dist/commands/plan.js.map +1 -1
  30. package/dist/core/claude-runner.d.ts +6 -6
  31. package/dist/core/claude-runner.d.ts.map +1 -1
  32. package/dist/core/claude-runner.js +9 -10
  33. package/dist/core/claude-runner.js.map +1 -1
  34. package/dist/core/failure-analyzer.d.ts.map +1 -1
  35. package/dist/core/failure-analyzer.js +3 -3
  36. package/dist/core/failure-analyzer.js.map +1 -1
  37. package/dist/core/pull-request.js +3 -3
  38. package/dist/core/pull-request.js.map +1 -1
  39. package/dist/core/state-derivation.d.ts +5 -0
  40. package/dist/core/state-derivation.d.ts.map +1 -1
  41. package/dist/core/state-derivation.js +14 -4
  42. package/dist/core/state-derivation.js.map +1 -1
  43. package/dist/core/worktree.d.ts +32 -0
  44. package/dist/core/worktree.d.ts.map +1 -1
  45. package/dist/core/worktree.js +215 -0
  46. package/dist/core/worktree.js.map +1 -1
  47. package/dist/prompts/amend.d.ts.map +1 -1
  48. package/dist/prompts/amend.js +26 -11
  49. package/dist/prompts/amend.js.map +1 -1
  50. package/dist/prompts/planning.d.ts.map +1 -1
  51. package/dist/prompts/planning.js +26 -11
  52. package/dist/prompts/planning.js.map +1 -1
  53. package/dist/types/config.d.ts +30 -13
  54. package/dist/types/config.d.ts.map +1 -1
  55. package/dist/types/config.js +14 -10
  56. package/dist/types/config.js.map +1 -1
  57. package/dist/utils/config.d.ts +47 -4
  58. package/dist/utils/config.d.ts.map +1 -1
  59. package/dist/utils/config.js +176 -30
  60. package/dist/utils/config.js.map +1 -1
  61. package/dist/utils/frontmatter.d.ts +43 -0
  62. package/dist/utils/frontmatter.d.ts.map +1 -0
  63. package/dist/utils/frontmatter.js +85 -0
  64. package/dist/utils/frontmatter.js.map +1 -0
  65. package/dist/utils/name-generator.d.ts.map +1 -1
  66. package/dist/utils/name-generator.js +2 -3
  67. package/dist/utils/name-generator.js.map +1 -1
  68. package/dist/utils/session-parser.d.ts +44 -0
  69. package/dist/utils/session-parser.d.ts.map +1 -0
  70. package/dist/utils/session-parser.js +122 -0
  71. package/dist/utils/session-parser.js.map +1 -0
  72. package/dist/utils/terminal-symbols.d.ts +22 -3
  73. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  74. package/dist/utils/terminal-symbols.js +52 -18
  75. package/dist/utils/terminal-symbols.js.map +1 -1
  76. package/dist/utils/token-tracker.d.ts +20 -0
  77. package/dist/utils/token-tracker.d.ts.map +1 -1
  78. package/dist/utils/token-tracker.js +57 -2
  79. package/dist/utils/token-tracker.js.map +1 -1
  80. package/package.json +1 -1
  81. package/src/commands/config.ts +0 -7
  82. package/src/commands/do.ts +141 -20
  83. package/src/commands/plan.ts +87 -1
  84. package/src/core/claude-runner.ts +16 -17
  85. package/src/core/failure-analyzer.ts +3 -3
  86. package/src/core/pull-request.ts +3 -3
  87. package/src/core/state-derivation.ts +20 -4
  88. package/src/core/worktree.ts +230 -0
  89. package/src/prompts/amend.ts +26 -11
  90. package/src/prompts/config-docs.md +91 -29
  91. package/src/prompts/planning.ts +26 -11
  92. package/src/types/config.ts +46 -21
  93. package/src/utils/config.ts +200 -33
  94. package/src/utils/frontmatter.ts +110 -0
  95. package/src/utils/name-generator.ts +2 -3
  96. package/src/utils/session-parser.ts +161 -0
  97. package/src/utils/terminal-symbols.ts +68 -16
  98. package/src/utils/token-tracker.ts +65 -2
  99. package/tests/unit/claude-runner-interactive.test.ts +8 -6
  100. package/tests/unit/claude-runner.test.ts +5 -66
  101. package/tests/unit/config-command.test.ts +6 -6
  102. package/tests/unit/config.test.ts +268 -45
  103. package/tests/unit/frontmatter.test.ts +182 -0
  104. package/tests/unit/post-execution-picker.test.ts +5 -0
  105. package/tests/unit/session-parser.test.ts +301 -0
  106. package/tests/unit/terminal-symbols.test.ts +142 -0
  107. package/tests/unit/token-tracker.test.ts +304 -1
  108. package/tests/unit/validation.test.ts +6 -4
  109. package/tests/unit/worktree.test.ts +242 -0
@@ -0,0 +1,301 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import {
5
+ escapeProjectPath,
6
+ getSessionFilePath,
7
+ parseSessionFile,
8
+ parseSessionById,
9
+ } from '../../src/utils/session-parser.js';
10
+
11
+ describe('Session Parser', () => {
12
+ let tempDir: string;
13
+
14
+ beforeEach(() => {
15
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'raf-session-parser-test-'));
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tempDir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('escapeProjectPath', () => {
23
+ it('should replace slashes with dashes', () => {
24
+ expect(escapeProjectPath('/Users/test/project')).toBe('Users-test-project');
25
+ });
26
+
27
+ it('should handle single leading slash', () => {
28
+ expect(escapeProjectPath('/project')).toBe('project');
29
+ });
30
+
31
+ it('should handle no leading slash', () => {
32
+ expect(escapeProjectPath('Users/test/project')).toBe('Users-test-project');
33
+ });
34
+
35
+ it('should handle multiple slashes', () => {
36
+ expect(escapeProjectPath('/a/b/c/d')).toBe('a-b-c-d');
37
+ });
38
+ });
39
+
40
+ describe('getSessionFilePath', () => {
41
+ it('should construct correct session file path', () => {
42
+ const sessionId = '550e8400-e29b-41d4-a716-446655440000';
43
+ const cwd = '/Users/test/myproject';
44
+
45
+ const result = getSessionFilePath(sessionId, cwd);
46
+
47
+ expect(result).toBe(
48
+ path.join(os.homedir(), '.claude', 'projects', 'Users-test-myproject', '550e8400-e29b-41d4-a716-446655440000.jsonl')
49
+ );
50
+ });
51
+ });
52
+
53
+ describe('parseSessionFile', () => {
54
+ it('should return error when file does not exist', () => {
55
+ const result = parseSessionFile(path.join(tempDir, 'nonexistent.jsonl'));
56
+
57
+ expect(result.success).toBe(false);
58
+ expect(result.error).toContain('not found');
59
+ expect(result.usage.inputTokens).toBe(0);
60
+ expect(result.usage.outputTokens).toBe(0);
61
+ });
62
+
63
+ it('should parse empty session file', () => {
64
+ const filePath = path.join(tempDir, 'empty.jsonl');
65
+ fs.writeFileSync(filePath, '');
66
+
67
+ const result = parseSessionFile(filePath);
68
+
69
+ expect(result.success).toBe(true);
70
+ expect(result.usage.inputTokens).toBe(0);
71
+ expect(result.usage.outputTokens).toBe(0);
72
+ });
73
+
74
+ it('should parse single assistant message entry', () => {
75
+ const filePath = path.join(tempDir, 'single.jsonl');
76
+ const entry = {
77
+ type: 'assistant',
78
+ message: {
79
+ model: 'claude-sonnet-4-5',
80
+ usage: {
81
+ input_tokens: 1000,
82
+ output_tokens: 500,
83
+ cache_read_input_tokens: 100,
84
+ cache_creation_input_tokens: 50,
85
+ },
86
+ },
87
+ };
88
+ fs.writeFileSync(filePath, JSON.stringify(entry) + '\n');
89
+
90
+ const result = parseSessionFile(filePath);
91
+
92
+ expect(result.success).toBe(true);
93
+ expect(result.usage.inputTokens).toBe(1000);
94
+ expect(result.usage.outputTokens).toBe(500);
95
+ expect(result.usage.cacheReadInputTokens).toBe(100);
96
+ expect(result.usage.cacheCreationInputTokens).toBe(50);
97
+ expect(result.usage.modelUsage['claude-sonnet-4-5']?.inputTokens).toBe(1000);
98
+ });
99
+
100
+ it('should accumulate multiple assistant messages', () => {
101
+ const filePath = path.join(tempDir, 'multiple.jsonl');
102
+ const entry1 = {
103
+ type: 'assistant',
104
+ message: {
105
+ model: 'claude-sonnet-4-5',
106
+ usage: {
107
+ input_tokens: 1000,
108
+ output_tokens: 500,
109
+ },
110
+ },
111
+ };
112
+ const entry2 = {
113
+ type: 'assistant',
114
+ message: {
115
+ model: 'claude-sonnet-4-5',
116
+ usage: {
117
+ input_tokens: 2000,
118
+ output_tokens: 1000,
119
+ },
120
+ },
121
+ };
122
+ fs.writeFileSync(filePath, JSON.stringify(entry1) + '\n' + JSON.stringify(entry2) + '\n');
123
+
124
+ const result = parseSessionFile(filePath);
125
+
126
+ expect(result.success).toBe(true);
127
+ expect(result.usage.inputTokens).toBe(3000);
128
+ expect(result.usage.outputTokens).toBe(1500);
129
+ expect(result.usage.modelUsage['claude-sonnet-4-5']?.inputTokens).toBe(3000);
130
+ });
131
+
132
+ it('should handle different models in same session', () => {
133
+ const filePath = path.join(tempDir, 'multi-model.jsonl');
134
+ const entry1 = {
135
+ type: 'assistant',
136
+ message: {
137
+ model: 'claude-sonnet-4-5',
138
+ usage: {
139
+ input_tokens: 1000,
140
+ output_tokens: 500,
141
+ },
142
+ },
143
+ };
144
+ const entry2 = {
145
+ type: 'assistant',
146
+ message: {
147
+ model: 'claude-haiku-4-5',
148
+ usage: {
149
+ input_tokens: 500,
150
+ output_tokens: 200,
151
+ },
152
+ },
153
+ };
154
+ fs.writeFileSync(filePath, JSON.stringify(entry1) + '\n' + JSON.stringify(entry2) + '\n');
155
+
156
+ const result = parseSessionFile(filePath);
157
+
158
+ expect(result.success).toBe(true);
159
+ expect(result.usage.inputTokens).toBe(1500);
160
+ expect(result.usage.outputTokens).toBe(700);
161
+ expect(result.usage.modelUsage['claude-sonnet-4-5']?.inputTokens).toBe(1000);
162
+ expect(result.usage.modelUsage['claude-haiku-4-5']?.inputTokens).toBe(500);
163
+ });
164
+
165
+ it('should skip non-assistant entries', () => {
166
+ const filePath = path.join(tempDir, 'mixed.jsonl');
167
+ const userEntry = { type: 'user', message: { content: 'hello' } };
168
+ const assistantEntry = {
169
+ type: 'assistant',
170
+ message: {
171
+ model: 'claude-sonnet-4-5',
172
+ usage: {
173
+ input_tokens: 1000,
174
+ output_tokens: 500,
175
+ },
176
+ },
177
+ };
178
+ const systemEntry = { type: 'system', data: {} };
179
+ fs.writeFileSync(
180
+ filePath,
181
+ JSON.stringify(userEntry) + '\n' + JSON.stringify(assistantEntry) + '\n' + JSON.stringify(systemEntry) + '\n'
182
+ );
183
+
184
+ const result = parseSessionFile(filePath);
185
+
186
+ expect(result.success).toBe(true);
187
+ expect(result.usage.inputTokens).toBe(1000);
188
+ expect(result.usage.outputTokens).toBe(500);
189
+ });
190
+
191
+ it('should skip malformed JSON lines', () => {
192
+ const filePath = path.join(tempDir, 'malformed.jsonl');
193
+ const goodEntry = {
194
+ type: 'assistant',
195
+ message: {
196
+ model: 'claude-sonnet-4-5',
197
+ usage: {
198
+ input_tokens: 1000,
199
+ output_tokens: 500,
200
+ },
201
+ },
202
+ };
203
+ fs.writeFileSync(
204
+ filePath,
205
+ 'not valid json\n' + JSON.stringify(goodEntry) + '\n' + '{ broken }\n'
206
+ );
207
+
208
+ const result = parseSessionFile(filePath);
209
+
210
+ expect(result.success).toBe(true);
211
+ expect(result.usage.inputTokens).toBe(1000);
212
+ expect(result.usage.outputTokens).toBe(500);
213
+ });
214
+
215
+ it('should handle entries without usage data', () => {
216
+ const filePath = path.join(tempDir, 'no-usage.jsonl');
217
+ const entryWithUsage = {
218
+ type: 'assistant',
219
+ message: {
220
+ model: 'claude-sonnet-4-5',
221
+ usage: {
222
+ input_tokens: 1000,
223
+ output_tokens: 500,
224
+ },
225
+ },
226
+ };
227
+ const entryWithoutUsage = {
228
+ type: 'assistant',
229
+ message: {
230
+ model: 'claude-sonnet-4-5',
231
+ },
232
+ };
233
+ fs.writeFileSync(
234
+ filePath,
235
+ JSON.stringify(entryWithUsage) + '\n' + JSON.stringify(entryWithoutUsage) + '\n'
236
+ );
237
+
238
+ const result = parseSessionFile(filePath);
239
+
240
+ expect(result.success).toBe(true);
241
+ expect(result.usage.inputTokens).toBe(1000);
242
+ });
243
+
244
+ it('should handle entries without model', () => {
245
+ const filePath = path.join(tempDir, 'no-model.jsonl');
246
+ const entry = {
247
+ type: 'assistant',
248
+ message: {
249
+ usage: {
250
+ input_tokens: 1000,
251
+ output_tokens: 500,
252
+ },
253
+ },
254
+ };
255
+ fs.writeFileSync(filePath, JSON.stringify(entry) + '\n');
256
+
257
+ const result = parseSessionFile(filePath);
258
+
259
+ expect(result.success).toBe(true);
260
+ expect(result.usage.inputTokens).toBe(1000);
261
+ expect(result.usage.outputTokens).toBe(500);
262
+ // modelUsage should be empty since no model specified
263
+ expect(Object.keys(result.usage.modelUsage)).toHaveLength(0);
264
+ });
265
+ });
266
+
267
+ describe('parseSessionById', () => {
268
+ it('should combine getSessionFilePath and parseSessionFile', () => {
269
+ // Create the expected directory structure
270
+ const sessionId = 'test-session-id';
271
+ const cwd = tempDir;
272
+ const escapedPath = cwd.replace(/^\//, '').replace(/\//g, '-');
273
+ const projectsDir = path.join(tempDir, '.claude', 'projects', escapedPath);
274
+ fs.mkdirSync(projectsDir, { recursive: true });
275
+
276
+ // Create a session file
277
+ const entry = {
278
+ type: 'assistant',
279
+ message: {
280
+ model: 'claude-opus-4-6',
281
+ usage: {
282
+ input_tokens: 5000,
283
+ output_tokens: 2000,
284
+ },
285
+ },
286
+ };
287
+ fs.writeFileSync(path.join(projectsDir, `${sessionId}.jsonl`), JSON.stringify(entry) + '\n');
288
+
289
+ // Mock os.homedir to return tempDir
290
+ // Since we can't easily mock, we'll test parseSessionFile directly
291
+ // and just verify parseSessionById calls it correctly by testing with the expected path
292
+ const expectedPath = getSessionFilePath(sessionId, cwd);
293
+
294
+ // This will fail because the path is relative to actual homedir, but demonstrates the logic
295
+ const result = parseSessionById(sessionId, cwd);
296
+
297
+ // Will return error since file doesn't exist at real homedir path
298
+ expect(result.success).toBe(false);
299
+ });
300
+ });
301
+ });
@@ -8,6 +8,8 @@ import {
8
8
  formatCost,
9
9
  formatTaskTokenSummary,
10
10
  formatTokenTotalSummary,
11
+ formatRateLimitPercentage,
12
+ TokenSummaryOptions,
11
13
  TaskStatus,
12
14
  } from '../../src/utils/terminal-symbols.js';
13
15
  import type { UsageData } from '../../src/types/config.js';
@@ -475,5 +477,145 @@ describe('Terminal Symbols', () => {
475
477
  expect(lines[0]).toContain('──');
476
478
  expect(lines[lines.length - 1]).toContain('──');
477
479
  });
480
+
481
+ it('should include rate limit percentage when option enabled', () => {
482
+ const options: TokenSummaryOptions = {
483
+ showRateLimitEstimate: true,
484
+ rateLimitPercentage: 42.5,
485
+ };
486
+ const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75), options);
487
+ expect(result).toContain('~43% of 5h window');
488
+ });
489
+
490
+ it('should not include rate limit when option disabled', () => {
491
+ const options: TokenSummaryOptions = {
492
+ showRateLimitEstimate: false,
493
+ rateLimitPercentage: 42.5,
494
+ };
495
+ const result = formatTokenTotalSummary(makeUsage(), makeCost(3.75), options);
496
+ expect(result).not.toContain('5h window');
497
+ });
498
+
499
+ it('should hide cache tokens when option disabled', () => {
500
+ const options: TokenSummaryOptions = {
501
+ showCacheTokens: false,
502
+ };
503
+ const result = formatTokenTotalSummary(
504
+ makeUsage({ cacheReadInputTokens: 125000 }),
505
+ makeCost(3.75),
506
+ options
507
+ );
508
+ expect(result).not.toContain('Cache:');
509
+ });
510
+ });
511
+
512
+ describe('formatRateLimitPercentage', () => {
513
+ it('should format zero percentage', () => {
514
+ expect(formatRateLimitPercentage(0)).toBe('~0% of 5h window');
515
+ });
516
+
517
+ it('should format very small percentages with 2 decimals', () => {
518
+ expect(formatRateLimitPercentage(0.05)).toBe('~0.05% of 5h window');
519
+ });
520
+
521
+ it('should format small percentages with 1 decimal', () => {
522
+ expect(formatRateLimitPercentage(0.5)).toBe('~0.5% of 5h window');
523
+ });
524
+
525
+ it('should round percentages >= 1', () => {
526
+ expect(formatRateLimitPercentage(1.5)).toBe('~2% of 5h window');
527
+ expect(formatRateLimitPercentage(42.3)).toBe('~42% of 5h window');
528
+ expect(formatRateLimitPercentage(100)).toBe('~100% of 5h window');
529
+ });
530
+
531
+ it('should handle percentages over 100%', () => {
532
+ expect(formatRateLimitPercentage(150.7)).toBe('~151% of 5h window');
533
+ });
534
+ });
535
+
536
+ describe('formatTaskTokenSummary with options', () => {
537
+ const makeUsage = (overrides: Partial<UsageData> = {}): UsageData => ({
538
+ inputTokens: 5234,
539
+ outputTokens: 1023,
540
+ cacheReadInputTokens: 0,
541
+ cacheCreationInputTokens: 0,
542
+ modelUsage: {},
543
+ ...overrides,
544
+ });
545
+
546
+ const makeCost = (total: number): CostBreakdown => ({
547
+ inputCost: 0,
548
+ outputCost: 0,
549
+ cacheReadCost: 0,
550
+ cacheCreateCost: 0,
551
+ totalCost: total,
552
+ });
553
+
554
+ const makeEntry = (usage: UsageData, cost: CostBreakdown, attempts?: UsageData[]): TaskUsageEntry => ({
555
+ taskId: '01',
556
+ usage,
557
+ cost,
558
+ attempts: attempts ?? [usage],
559
+ });
560
+
561
+ it('should include rate limit percentage in single-attempt summary', () => {
562
+ const usage = makeUsage({ cacheReadInputTokens: 18500 });
563
+ const entry = makeEntry(usage, makeCost(0.42));
564
+ const options: TokenSummaryOptions = {
565
+ showRateLimitEstimate: true,
566
+ rateLimitPercentage: 2.5,
567
+ };
568
+
569
+ const result = formatTaskTokenSummary(entry, undefined, options);
570
+ expect(result).toContain('~3% of 5h window');
571
+ });
572
+
573
+ it('should hide cache tokens in single-attempt summary when disabled', () => {
574
+ const usage = makeUsage({ cacheReadInputTokens: 18500 });
575
+ const entry = makeEntry(usage, makeCost(0.42));
576
+ const options: TokenSummaryOptions = {
577
+ showCacheTokens: false,
578
+ };
579
+
580
+ const result = formatTaskTokenSummary(entry, undefined, options);
581
+ expect(result).not.toContain('Cache:');
582
+ });
583
+
584
+ it('should only show rate limit on total line in multi-attempt summary', () => {
585
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200 });
586
+ const attempt2 = makeUsage({ inputTokens: 2000, outputTokens: 400 });
587
+ const totalUsage = makeUsage({ inputTokens: 3000, outputTokens: 600 });
588
+ const entry = makeEntry(totalUsage, makeCost(0.05), [attempt1, attempt2]);
589
+ const options: TokenSummaryOptions = {
590
+ showRateLimitEstimate: true,
591
+ rateLimitPercentage: 1.5,
592
+ };
593
+
594
+ const result = formatTaskTokenSummary(entry, undefined, options);
595
+ const lines = result.split('\n');
596
+
597
+ // Rate limit should only appear on the Total line
598
+ expect(lines[0]).not.toContain('5h window'); // Attempt 1
599
+ expect(lines[1]).not.toContain('5h window'); // Attempt 2
600
+ expect(lines[2]).toContain('~2% of 5h window'); // Total
601
+ });
602
+
603
+ it('should respect showCacheTokens in multi-attempt summary', () => {
604
+ const attempt1 = makeUsage({ inputTokens: 1000, outputTokens: 200, cacheReadInputTokens: 5000 });
605
+ const attempt2 = makeUsage({ inputTokens: 1500, outputTokens: 300, cacheCreationInputTokens: 2000 });
606
+ const totalUsage = makeUsage({
607
+ inputTokens: 2500,
608
+ outputTokens: 500,
609
+ cacheReadInputTokens: 5000,
610
+ cacheCreationInputTokens: 2000,
611
+ });
612
+ const entry = makeEntry(totalUsage, makeCost(0.08), [attempt1, attempt2]);
613
+ const options: TokenSummaryOptions = {
614
+ showCacheTokens: false,
615
+ };
616
+
617
+ const result = formatTaskTokenSummary(entry, undefined, options);
618
+ expect(result).not.toContain('Cache:');
619
+ });
478
620
  });
479
621
  });