rafcode 2.1.0 → 2.2.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 (130) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/CLAUDE.md +59 -11
  3. package/RAF/ahslfe-config-wizard/decisions.md +34 -0
  4. package/RAF/ahslfe-config-wizard/input.md +1 -0
  5. package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
  6. package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
  7. package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
  8. package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
  9. package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
  10. package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
  11. package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
  12. package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
  13. package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
  14. package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
  15. package/RAF/ahstvo-token-tracker/decisions.md +44 -0
  16. package/RAF/ahstvo-token-tracker/input.md +3 -0
  17. package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
  18. package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
  19. package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
  20. package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
  21. package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
  22. package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
  23. package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
  24. package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
  25. package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
  26. package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
  27. package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
  28. package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
  29. package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
  30. package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
  31. package/README.md +34 -0
  32. package/dist/commands/config.d.ts +3 -0
  33. package/dist/commands/config.d.ts.map +1 -0
  34. package/dist/commands/config.js +173 -0
  35. package/dist/commands/config.js.map +1 -0
  36. package/dist/commands/do.d.ts.map +1 -1
  37. package/dist/commands/do.js +50 -28
  38. package/dist/commands/do.js.map +1 -1
  39. package/dist/commands/plan.d.ts.map +1 -1
  40. package/dist/commands/plan.js +3 -2
  41. package/dist/commands/plan.js.map +1 -1
  42. package/dist/core/claude-runner.d.ts +17 -13
  43. package/dist/core/claude-runner.d.ts.map +1 -1
  44. package/dist/core/claude-runner.js +42 -257
  45. package/dist/core/claude-runner.js.map +1 -1
  46. package/dist/core/failure-analyzer.d.ts.map +1 -1
  47. package/dist/core/failure-analyzer.js +6 -3
  48. package/dist/core/failure-analyzer.js.map +1 -1
  49. package/dist/core/git.d.ts.map +1 -1
  50. package/dist/core/git.js +10 -3
  51. package/dist/core/git.js.map +1 -1
  52. package/dist/core/pull-request.d.ts +1 -1
  53. package/dist/core/pull-request.d.ts.map +1 -1
  54. package/dist/core/pull-request.js +7 -4
  55. package/dist/core/pull-request.js.map +1 -1
  56. package/dist/core/shutdown-handler.d.ts.map +1 -1
  57. package/dist/core/shutdown-handler.js +0 -4
  58. package/dist/core/shutdown-handler.js.map +1 -1
  59. package/dist/index.js +2 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/parsers/stream-renderer.d.ts +16 -4
  62. package/dist/parsers/stream-renderer.d.ts.map +1 -1
  63. package/dist/parsers/stream-renderer.js +35 -5
  64. package/dist/parsers/stream-renderer.js.map +1 -1
  65. package/dist/prompts/execution.d.ts.map +1 -1
  66. package/dist/prompts/execution.js +11 -1
  67. package/dist/prompts/execution.js.map +1 -1
  68. package/dist/types/config.d.ts +95 -5
  69. package/dist/types/config.d.ts.map +1 -1
  70. package/dist/types/config.js +63 -3
  71. package/dist/types/config.js.map +1 -1
  72. package/dist/utils/config.d.ts +59 -7
  73. package/dist/utils/config.d.ts.map +1 -1
  74. package/dist/utils/config.js +276 -21
  75. package/dist/utils/config.js.map +1 -1
  76. package/dist/utils/name-generator.d.ts +3 -7
  77. package/dist/utils/name-generator.d.ts.map +1 -1
  78. package/dist/utils/name-generator.js +75 -61
  79. package/dist/utils/name-generator.js.map +1 -1
  80. package/dist/utils/terminal-symbols.d.ts +21 -0
  81. package/dist/utils/terminal-symbols.d.ts.map +1 -1
  82. package/dist/utils/terminal-symbols.js +62 -0
  83. package/dist/utils/terminal-symbols.js.map +1 -1
  84. package/dist/utils/token-tracker.d.ts +45 -0
  85. package/dist/utils/token-tracker.d.ts.map +1 -0
  86. package/dist/utils/token-tracker.js +107 -0
  87. package/dist/utils/token-tracker.js.map +1 -0
  88. package/dist/utils/validation.d.ts +5 -5
  89. package/dist/utils/validation.d.ts.map +1 -1
  90. package/dist/utils/validation.js +10 -6
  91. package/dist/utils/validation.js.map +1 -1
  92. package/dist/utils/verbose-toggle.d.ts +33 -0
  93. package/dist/utils/verbose-toggle.d.ts.map +1 -0
  94. package/dist/utils/verbose-toggle.js +94 -0
  95. package/dist/utils/verbose-toggle.js.map +1 -0
  96. package/package.json +1 -1
  97. package/src/commands/config.ts +204 -0
  98. package/src/commands/do.ts +59 -27
  99. package/src/commands/plan.ts +3 -2
  100. package/src/core/claude-runner.ts +58 -311
  101. package/src/core/failure-analyzer.ts +6 -3
  102. package/src/core/git.ts +10 -3
  103. package/src/core/pull-request.ts +7 -4
  104. package/src/core/shutdown-handler.ts +0 -5
  105. package/src/index.ts +2 -0
  106. package/src/parsers/stream-renderer.ts +55 -8
  107. package/src/prompts/config-docs.md +331 -0
  108. package/src/prompts/execution.ts +13 -1
  109. package/src/types/config.ts +156 -8
  110. package/src/utils/config.ts +335 -21
  111. package/src/utils/name-generator.ts +84 -71
  112. package/src/utils/terminal-symbols.ts +68 -0
  113. package/src/utils/token-tracker.ts +135 -0
  114. package/src/utils/validation.ts +15 -10
  115. package/src/utils/verbose-toggle.ts +103 -0
  116. package/tests/unit/claude-runner.test.ts +216 -403
  117. package/tests/unit/config-command.test.ts +163 -0
  118. package/tests/unit/config.test.ts +608 -30
  119. package/tests/unit/name-generator.test.ts +99 -75
  120. package/tests/unit/pull-request.test.ts +2 -0
  121. package/tests/unit/stream-renderer.test.ts +83 -30
  122. package/tests/unit/terminal-symbols.test.ts +157 -0
  123. package/tests/unit/token-tracker.test.ts +352 -0
  124. package/tests/unit/verbose-toggle.test.ts +204 -0
  125. package/RAF/ahrtxf-session-sentinel/decisions.md +0 -19
  126. package/RAF/ahrtxf-session-sentinel/input.md +0 -1
  127. package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +0 -37
  128. package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +0 -45
  129. package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +0 -41
  130. package/RAF/ahrtxf-session-sentinel/plans/02-resume-flag.md +0 -51
@@ -0,0 +1,352 @@
1
+ import { TokenTracker, CostBreakdown } from '../../src/utils/token-tracker.js';
2
+ import { UsageData, PricingConfig, DEFAULT_CONFIG } from '../../src/types/config.js';
3
+
4
+ function makeUsage(overrides: Partial<UsageData> = {}): UsageData {
5
+ return {
6
+ inputTokens: 0,
7
+ outputTokens: 0,
8
+ cacheReadInputTokens: 0,
9
+ cacheCreationInputTokens: 0,
10
+ modelUsage: {},
11
+ ...overrides,
12
+ };
13
+ }
14
+
15
+ const testPricing: PricingConfig = DEFAULT_CONFIG.pricing;
16
+
17
+ describe('TokenTracker', () => {
18
+ describe('calculateCost', () => {
19
+ it('should calculate cost for opus model usage', () => {
20
+ const tracker = new TokenTracker(testPricing);
21
+ const usage = makeUsage({
22
+ inputTokens: 1_000_000,
23
+ outputTokens: 500_000,
24
+ cacheReadInputTokens: 200_000,
25
+ cacheCreationInputTokens: 100_000,
26
+ modelUsage: {
27
+ 'claude-opus-4-6': {
28
+ inputTokens: 1_000_000,
29
+ outputTokens: 500_000,
30
+ cacheReadInputTokens: 200_000,
31
+ cacheCreationInputTokens: 100_000,
32
+ },
33
+ },
34
+ });
35
+
36
+ const cost = tracker.calculateCost(usage);
37
+ expect(cost.inputCost).toBeCloseTo(15); // 1M * $15/MTok
38
+ expect(cost.outputCost).toBeCloseTo(37.5); // 0.5M * $75/MTok
39
+ expect(cost.cacheReadCost).toBeCloseTo(0.3); // 0.2M * $1.5/MTok
40
+ expect(cost.cacheCreateCost).toBeCloseTo(1.875); // 0.1M * $18.75/MTok
41
+ expect(cost.totalCost).toBeCloseTo(15 + 37.5 + 0.3 + 1.875);
42
+ });
43
+
44
+ it('should calculate cost for sonnet model usage', () => {
45
+ const tracker = new TokenTracker(testPricing);
46
+ const usage = makeUsage({
47
+ inputTokens: 1_000_000,
48
+ outputTokens: 1_000_000,
49
+ modelUsage: {
50
+ 'claude-sonnet-4-5-20250929': {
51
+ inputTokens: 1_000_000,
52
+ outputTokens: 1_000_000,
53
+ cacheReadInputTokens: 0,
54
+ cacheCreationInputTokens: 0,
55
+ },
56
+ },
57
+ });
58
+
59
+ const cost = tracker.calculateCost(usage);
60
+ expect(cost.inputCost).toBeCloseTo(3); // 1M * $3/MTok
61
+ expect(cost.outputCost).toBeCloseTo(15); // 1M * $15/MTok
62
+ expect(cost.totalCost).toBeCloseTo(18);
63
+ });
64
+
65
+ it('should calculate cost for haiku model usage', () => {
66
+ const tracker = new TokenTracker(testPricing);
67
+ const usage = makeUsage({
68
+ inputTokens: 2_000_000,
69
+ outputTokens: 1_000_000,
70
+ modelUsage: {
71
+ 'claude-haiku-4-5-20251001': {
72
+ inputTokens: 2_000_000,
73
+ outputTokens: 1_000_000,
74
+ cacheReadInputTokens: 0,
75
+ cacheCreationInputTokens: 0,
76
+ },
77
+ },
78
+ });
79
+
80
+ const cost = tracker.calculateCost(usage);
81
+ expect(cost.inputCost).toBeCloseTo(2); // 2M * $1/MTok
82
+ expect(cost.outputCost).toBeCloseTo(5); // 1M * $5/MTok
83
+ expect(cost.totalCost).toBeCloseTo(7);
84
+ });
85
+
86
+ it('should handle multi-model usage in a single task', () => {
87
+ const tracker = new TokenTracker(testPricing);
88
+ const usage = makeUsage({
89
+ inputTokens: 2_000_000,
90
+ outputTokens: 1_500_000,
91
+ modelUsage: {
92
+ 'claude-opus-4-6': {
93
+ inputTokens: 1_000_000,
94
+ outputTokens: 500_000,
95
+ cacheReadInputTokens: 0,
96
+ cacheCreationInputTokens: 0,
97
+ },
98
+ 'claude-haiku-4-5-20251001': {
99
+ inputTokens: 1_000_000,
100
+ outputTokens: 1_000_000,
101
+ cacheReadInputTokens: 0,
102
+ cacheCreationInputTokens: 0,
103
+ },
104
+ },
105
+ });
106
+
107
+ const cost = tracker.calculateCost(usage);
108
+ // Opus: 1M*$15 + 0.5M*$75 = $15 + $37.5
109
+ // Haiku: 1M*$1 + 1M*$5 = $1 + $5
110
+ expect(cost.inputCost).toBeCloseTo(16); // 15 + 1
111
+ expect(cost.outputCost).toBeCloseTo(42.5); // 37.5 + 5
112
+ expect(cost.totalCost).toBeCloseTo(58.5);
113
+ });
114
+
115
+ it('should fallback to sonnet pricing when no model breakdown', () => {
116
+ const tracker = new TokenTracker(testPricing);
117
+ const usage = makeUsage({
118
+ inputTokens: 1_000_000,
119
+ outputTokens: 1_000_000,
120
+ modelUsage: {},
121
+ });
122
+
123
+ const cost = tracker.calculateCost(usage);
124
+ expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
125
+ expect(cost.outputCost).toBeCloseTo(15);
126
+ expect(cost.totalCost).toBeCloseTo(18);
127
+ });
128
+
129
+ it('should fallback to sonnet pricing for unknown model families', () => {
130
+ const tracker = new TokenTracker(testPricing);
131
+ const usage = makeUsage({
132
+ inputTokens: 1_000_000,
133
+ outputTokens: 1_000_000,
134
+ modelUsage: {
135
+ 'claude-unknown-3-0': {
136
+ inputTokens: 1_000_000,
137
+ outputTokens: 1_000_000,
138
+ cacheReadInputTokens: 0,
139
+ cacheCreationInputTokens: 0,
140
+ },
141
+ },
142
+ });
143
+
144
+ const cost = tracker.calculateCost(usage);
145
+ expect(cost.inputCost).toBeCloseTo(3); // sonnet fallback
146
+ expect(cost.outputCost).toBeCloseTo(15);
147
+ });
148
+
149
+ it('should return zero cost for zero tokens', () => {
150
+ const tracker = new TokenTracker(testPricing);
151
+ const usage = makeUsage();
152
+ const cost = tracker.calculateCost(usage);
153
+ expect(cost.totalCost).toBe(0);
154
+ });
155
+
156
+ it('should apply cache read discount correctly', () => {
157
+ const tracker = new TokenTracker(testPricing);
158
+ const usage = makeUsage({
159
+ cacheReadInputTokens: 1_000_000,
160
+ modelUsage: {
161
+ 'claude-sonnet-4-5': {
162
+ inputTokens: 0,
163
+ outputTokens: 0,
164
+ cacheReadInputTokens: 1_000_000,
165
+ cacheCreationInputTokens: 0,
166
+ },
167
+ },
168
+ });
169
+
170
+ const cost = tracker.calculateCost(usage);
171
+ // Cache read: 1M * $0.30/MTok = $0.30 (90% off $3 input price)
172
+ expect(cost.cacheReadCost).toBeCloseTo(0.3);
173
+ expect(cost.totalCost).toBeCloseTo(0.3);
174
+ });
175
+ });
176
+
177
+ describe('addTask and accumulation', () => {
178
+ it('should accumulate usage across multiple tasks', () => {
179
+ const tracker = new TokenTracker(testPricing);
180
+
181
+ tracker.addTask('01', makeUsage({
182
+ inputTokens: 500_000,
183
+ outputTokens: 200_000,
184
+ modelUsage: {
185
+ 'claude-opus-4-6': {
186
+ inputTokens: 500_000,
187
+ outputTokens: 200_000,
188
+ cacheReadInputTokens: 0,
189
+ cacheCreationInputTokens: 0,
190
+ },
191
+ },
192
+ }));
193
+
194
+ tracker.addTask('02', makeUsage({
195
+ inputTokens: 300_000,
196
+ outputTokens: 100_000,
197
+ modelUsage: {
198
+ 'claude-opus-4-6': {
199
+ inputTokens: 300_000,
200
+ outputTokens: 100_000,
201
+ cacheReadInputTokens: 0,
202
+ cacheCreationInputTokens: 0,
203
+ },
204
+ },
205
+ }));
206
+
207
+ const totals = tracker.getTotals();
208
+ expect(totals.usage.inputTokens).toBe(800_000);
209
+ expect(totals.usage.outputTokens).toBe(300_000);
210
+ expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(800_000);
211
+ expect(totals.usage.modelUsage['claude-opus-4-6']?.outputTokens).toBe(300_000);
212
+ });
213
+
214
+ it('should accumulate costs across multiple tasks', () => {
215
+ const tracker = new TokenTracker(testPricing);
216
+
217
+ const entry1 = tracker.addTask('01', makeUsage({
218
+ inputTokens: 1_000_000,
219
+ outputTokens: 1_000_000,
220
+ modelUsage: {
221
+ 'claude-sonnet-4-5': {
222
+ inputTokens: 1_000_000,
223
+ outputTokens: 1_000_000,
224
+ cacheReadInputTokens: 0,
225
+ cacheCreationInputTokens: 0,
226
+ },
227
+ },
228
+ }));
229
+
230
+ const entry2 = tracker.addTask('02', makeUsage({
231
+ inputTokens: 1_000_000,
232
+ outputTokens: 1_000_000,
233
+ modelUsage: {
234
+ 'claude-sonnet-4-5': {
235
+ inputTokens: 1_000_000,
236
+ outputTokens: 1_000_000,
237
+ cacheReadInputTokens: 0,
238
+ cacheCreationInputTokens: 0,
239
+ },
240
+ },
241
+ }));
242
+
243
+ const totals = tracker.getTotals();
244
+ // Each task: $3 input + $15 output = $18
245
+ expect(entry1.cost.totalCost).toBeCloseTo(18);
246
+ expect(entry2.cost.totalCost).toBeCloseTo(18);
247
+ expect(totals.cost.totalCost).toBeCloseTo(36);
248
+ });
249
+
250
+ it('should accumulate multi-model usage across tasks', () => {
251
+ const tracker = new TokenTracker(testPricing);
252
+
253
+ tracker.addTask('01', makeUsage({
254
+ inputTokens: 1_000_000,
255
+ outputTokens: 500_000,
256
+ modelUsage: {
257
+ 'claude-opus-4-6': {
258
+ inputTokens: 1_000_000,
259
+ outputTokens: 500_000,
260
+ cacheReadInputTokens: 0,
261
+ cacheCreationInputTokens: 0,
262
+ },
263
+ },
264
+ }));
265
+
266
+ tracker.addTask('02', makeUsage({
267
+ inputTokens: 500_000,
268
+ outputTokens: 200_000,
269
+ modelUsage: {
270
+ 'claude-haiku-4-5-20251001': {
271
+ inputTokens: 500_000,
272
+ outputTokens: 200_000,
273
+ cacheReadInputTokens: 0,
274
+ cacheCreationInputTokens: 0,
275
+ },
276
+ },
277
+ }));
278
+
279
+ const totals = tracker.getTotals();
280
+ expect(totals.usage.modelUsage['claude-opus-4-6']?.inputTokens).toBe(1_000_000);
281
+ expect(totals.usage.modelUsage['claude-haiku-4-5-20251001']?.inputTokens).toBe(500_000);
282
+ });
283
+
284
+ it('should return empty totals when no tasks added', () => {
285
+ const tracker = new TokenTracker(testPricing);
286
+ const totals = tracker.getTotals();
287
+ expect(totals.usage.inputTokens).toBe(0);
288
+ expect(totals.usage.outputTokens).toBe(0);
289
+ expect(totals.cost.totalCost).toBe(0);
290
+ expect(Object.keys(totals.usage.modelUsage)).toHaveLength(0);
291
+ });
292
+
293
+ it('should return per-task entries', () => {
294
+ const tracker = new TokenTracker(testPricing);
295
+ tracker.addTask('01', makeUsage({ inputTokens: 100 }));
296
+ tracker.addTask('02', makeUsage({ inputTokens: 200 }));
297
+
298
+ const entries = tracker.getEntries();
299
+ expect(entries).toHaveLength(2);
300
+ expect(entries[0].taskId).toBe('01');
301
+ expect(entries[1].taskId).toBe('02');
302
+ });
303
+
304
+ it('addTask returns the entry with cost', () => {
305
+ const tracker = new TokenTracker(testPricing);
306
+ const entry = tracker.addTask('01', makeUsage({
307
+ inputTokens: 1_000_000,
308
+ modelUsage: {
309
+ 'claude-opus-4-6': {
310
+ inputTokens: 1_000_000,
311
+ outputTokens: 0,
312
+ cacheReadInputTokens: 0,
313
+ cacheCreationInputTokens: 0,
314
+ },
315
+ },
316
+ }));
317
+
318
+ expect(entry.taskId).toBe('01');
319
+ expect(entry.cost.inputCost).toBeCloseTo(15);
320
+ expect(entry.cost.totalCost).toBeCloseTo(15);
321
+ });
322
+ });
323
+
324
+ describe('custom pricing', () => {
325
+ it('should use custom pricing config', () => {
326
+ const customPricing: PricingConfig = {
327
+ opus: { inputPerMTok: 10, outputPerMTok: 50, cacheReadPerMTok: 1, cacheCreatePerMTok: 12.5 },
328
+ sonnet: { inputPerMTok: 2, outputPerMTok: 10, cacheReadPerMTok: 0.2, cacheCreatePerMTok: 2.5 },
329
+ haiku: { inputPerMTok: 0.5, outputPerMTok: 2.5, cacheReadPerMTok: 0.05, cacheCreatePerMTok: 0.625 },
330
+ };
331
+
332
+ const tracker = new TokenTracker(customPricing);
333
+ const usage = makeUsage({
334
+ inputTokens: 1_000_000,
335
+ outputTokens: 1_000_000,
336
+ modelUsage: {
337
+ 'claude-opus-4-6': {
338
+ inputTokens: 1_000_000,
339
+ outputTokens: 1_000_000,
340
+ cacheReadInputTokens: 0,
341
+ cacheCreationInputTokens: 0,
342
+ },
343
+ },
344
+ });
345
+
346
+ const cost = tracker.calculateCost(usage);
347
+ expect(cost.inputCost).toBeCloseTo(10); // 1M * $10/MTok
348
+ expect(cost.outputCost).toBeCloseTo(50); // 1M * $50/MTok
349
+ expect(cost.totalCost).toBeCloseTo(60);
350
+ });
351
+ });
352
+ });
@@ -0,0 +1,204 @@
1
+ import { jest } from '@jest/globals';
2
+ import { EventEmitter } from 'events';
3
+
4
+ // Mock logger to capture output
5
+ const mockDim = jest.fn();
6
+ jest.unstable_mockModule('../../src/utils/logger.js', () => ({
7
+ logger: {
8
+ dim: mockDim,
9
+ info: jest.fn(),
10
+ debug: jest.fn(),
11
+ warn: jest.fn(),
12
+ error: jest.fn(),
13
+ },
14
+ }));
15
+
16
+ const { VerboseToggle } = await import('../../src/utils/verbose-toggle.js');
17
+
18
+ describe('VerboseToggle', () => {
19
+ // Save original stdin properties
20
+ const originalIsTTY = process.stdin.isTTY;
21
+ const originalSetRawMode = process.stdin.setRawMode;
22
+ const originalResume = process.stdin.resume;
23
+ const originalPause = process.stdin.pause;
24
+
25
+ let mockSetRawMode: jest.Mock;
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ mockSetRawMode = jest.fn();
30
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, writable: true, configurable: true });
31
+ (process.stdin as any).setRawMode = mockSetRawMode;
32
+ (process.stdin as any).resume = jest.fn();
33
+ (process.stdin as any).pause = jest.fn();
34
+ });
35
+
36
+ afterEach(() => {
37
+ // Restore original stdin
38
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, writable: true, configurable: true });
39
+ if (originalSetRawMode) {
40
+ (process.stdin as any).setRawMode = originalSetRawMode;
41
+ }
42
+ (process.stdin as any).resume = originalResume;
43
+ (process.stdin as any).pause = originalPause;
44
+ });
45
+
46
+ it('initializes with the provided verbose state', () => {
47
+ const toggle = new VerboseToggle(true);
48
+ expect(toggle.isVerbose).toBe(true);
49
+
50
+ const toggle2 = new VerboseToggle(false);
51
+ expect(toggle2.isVerbose).toBe(false);
52
+ });
53
+
54
+ it('is not active before start()', () => {
55
+ const toggle = new VerboseToggle(false);
56
+ expect(toggle.isActive).toBe(false);
57
+ });
58
+
59
+ it('becomes active after start() on a TTY', () => {
60
+ const toggle = new VerboseToggle(false);
61
+ toggle.start();
62
+ expect(toggle.isActive).toBe(true);
63
+ expect(mockSetRawMode).toHaveBeenCalledWith(true);
64
+ toggle.stop();
65
+ });
66
+
67
+ it('shows toggle hint on start', () => {
68
+ const toggle = new VerboseToggle(false);
69
+ toggle.start();
70
+ expect(mockDim).toHaveBeenCalledWith(' Press Tab to toggle verbose mode');
71
+ toggle.stop();
72
+ });
73
+
74
+ it('skips start when stdin is not a TTY', () => {
75
+ Object.defineProperty(process.stdin, 'isTTY', { value: false, writable: true, configurable: true });
76
+ const toggle = new VerboseToggle(false);
77
+ toggle.start();
78
+ expect(toggle.isActive).toBe(false);
79
+ expect(mockSetRawMode).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('toggles verbose state on Tab keypress', () => {
83
+ const toggle = new VerboseToggle(false);
84
+ toggle.start();
85
+
86
+ // Simulate Tab key (0x09)
87
+ process.stdin.emit('data', Buffer.from([0x09]));
88
+ expect(toggle.isVerbose).toBe(true);
89
+ expect(mockDim).toHaveBeenCalledWith(' [verbose: on]');
90
+
91
+ // Toggle back
92
+ process.stdin.emit('data', Buffer.from([0x09]));
93
+ expect(toggle.isVerbose).toBe(false);
94
+ expect(mockDim).toHaveBeenCalledWith(' [verbose: off]');
95
+
96
+ toggle.stop();
97
+ });
98
+
99
+ it('emits SIGINT on Ctrl+C keypress', () => {
100
+ const toggle = new VerboseToggle(false);
101
+ toggle.start();
102
+
103
+ const sigintHandler = jest.fn();
104
+ process.once('SIGINT', sigintHandler);
105
+
106
+ // Simulate Ctrl+C (0x03)
107
+ process.stdin.emit('data', Buffer.from([0x03]));
108
+ expect(sigintHandler).toHaveBeenCalled();
109
+
110
+ toggle.stop();
111
+ });
112
+
113
+ it('ignores non-Tab, non-Ctrl+C keypresses', () => {
114
+ const toggle = new VerboseToggle(false);
115
+ toggle.start();
116
+
117
+ // Simulate 'a' keypress
118
+ process.stdin.emit('data', Buffer.from([0x61]));
119
+ expect(toggle.isVerbose).toBe(false);
120
+
121
+ toggle.stop();
122
+ });
123
+
124
+ it('handles multiple bytes in a single data event', () => {
125
+ const toggle = new VerboseToggle(false);
126
+ toggle.start();
127
+
128
+ // Two Tab keys in one buffer
129
+ process.stdin.emit('data', Buffer.from([0x09, 0x09]));
130
+ // Should toggle twice → back to false
131
+ expect(toggle.isVerbose).toBe(false);
132
+
133
+ toggle.stop();
134
+ });
135
+
136
+ it('restores stdin on stop()', () => {
137
+ const toggle = new VerboseToggle(false);
138
+ toggle.start();
139
+ toggle.stop();
140
+
141
+ expect(toggle.isActive).toBe(false);
142
+ expect(mockSetRawMode).toHaveBeenCalledWith(false);
143
+ expect(process.stdin.pause).toHaveBeenCalled();
144
+ });
145
+
146
+ it('is safe to call stop() multiple times', () => {
147
+ const toggle = new VerboseToggle(false);
148
+ toggle.start();
149
+ toggle.stop();
150
+ toggle.stop(); // should not throw
151
+ expect(toggle.isActive).toBe(false);
152
+ });
153
+
154
+ it('is safe to call start() multiple times', () => {
155
+ const toggle = new VerboseToggle(false);
156
+ toggle.start();
157
+ toggle.start(); // should not start again
158
+ expect(mockSetRawMode).toHaveBeenCalledTimes(1);
159
+ toggle.stop();
160
+ });
161
+
162
+ it('does not respond to keypress after stop()', () => {
163
+ const toggle = new VerboseToggle(false);
164
+ toggle.start();
165
+ toggle.stop();
166
+
167
+ // Clear mocks to check no new calls
168
+ mockDim.mockClear();
169
+
170
+ // This should not trigger any toggle
171
+ process.stdin.emit('data', Buffer.from([0x09]));
172
+ expect(toggle.isVerbose).toBe(false);
173
+ // Only the hint message was logged (already cleared by mockClear), no toggle messages
174
+ expect(mockDim).not.toHaveBeenCalled();
175
+ });
176
+
177
+ it('works correctly across multiple tasks (stop and restart)', () => {
178
+ const toggle = new VerboseToggle(false);
179
+
180
+ // First task
181
+ toggle.start();
182
+ process.stdin.emit('data', Buffer.from([0x09]));
183
+ expect(toggle.isVerbose).toBe(true);
184
+ toggle.stop();
185
+
186
+ // State persists after stop
187
+ expect(toggle.isVerbose).toBe(true);
188
+
189
+ // Restart for second task
190
+ toggle.start();
191
+ expect(toggle.isVerbose).toBe(true); // Still true from previous toggle
192
+ process.stdin.emit('data', Buffer.from([0x09]));
193
+ expect(toggle.isVerbose).toBe(false);
194
+ toggle.stop();
195
+ });
196
+
197
+ it('handles setRawMode throwing an error', () => {
198
+ mockSetRawMode.mockImplementation(() => { throw new Error('Cannot set raw mode'); });
199
+ const toggle = new VerboseToggle(false);
200
+ toggle.start();
201
+ // Should gracefully skip activation
202
+ expect(toggle.isActive).toBe(false);
203
+ });
204
+ });
@@ -1,19 +0,0 @@
1
- # Project Decisions
2
-
3
- ## What's the main use case for logging the session ID?
4
- Resume interrupted sessions. Capture session ID so RAF can attempt to resume interrupted Claude sessions using `claude --resume <id>`, and also allow manual inspection. Add `raf do <project> --resume <session-id>` flag for resuming after Ctrl+C interruption.
5
-
6
- ## Should the session ID be captured in all execution modes?
7
- Verbose + non-interactive. Both modes that run tasks should capture session ID. Interactive planning mode can be skipped.
8
-
9
- ## When a session is interrupted, should the session ID be displayed to the user?
10
- Print to terminal. Display session ID in terminal output on interruption so user can copy it for `--resume`.
11
-
12
- ## For `raf do --resume`, should it resume the exact interrupted task or restart from scratch?
13
- Resume exact task. Pass `--resume` to Claude CLI for the specific interrupted task, continuing from where Claude left off mid-task. Add metadata (task ID) to support this. Format could be `--resume <task-id>:<session-id>` or assume it's the last unfinished task.
14
-
15
- ## How should non-interactive mode get access to the session ID?
16
- Always use `--output-format stream-json` for both verbose and non-interactive modes. Parse the init event to capture session_id. Only render/display the stream output when `--verbose` flag is passed. This gives us session IDs universally without changing user-visible behavior.
17
-
18
- ## When resuming, should RAF pass the original task's system prompt again?
19
- Rely on session state. Trust that Claude's `--resume` restores the full context including the original prompt. Don't re-send system prompt or task context.
@@ -1 +0,0 @@
1
- check if it's possible to log claude session id if session got interrupted
@@ -1,37 +0,0 @@
1
- # Outcome: Capture Session ID from Claude CLI Output
2
-
3
- ## Summary
4
-
5
- Implemented session ID extraction from Claude CLI's `system.init` NDJSON event in both `run()` and `runVerbose()` methods. The session ID is now captured, returned in `RunResult`, and printed to the terminal on interruption.
6
-
7
- ## Key Changes
8
-
9
- ### `src/parsers/stream-renderer.ts`
10
- - Added `session_id` field to `StreamEvent` interface
11
- - Added `sessionId` field to `RenderResult` interface
12
- - Modified `renderStreamEvent()` to extract and return `session_id` from system init events
13
-
14
- ### `src/core/claude-runner.ts`
15
- - Added `sessionId?: string` field to `RunResult` interface
16
- - Added `_sessionId` private field and public `sessionId` getter to `ClaudeRunner` class
17
- - Refactored `run()` to use `--output-format stream-json --verbose` with silent NDJSON parsing (no stdout display), enabling session ID extraction
18
- - Updated `runVerbose()` to capture `sessionId` from stream events
19
- - Both methods return `sessionId` in `RunResult`
20
- - Session ID is printed via `logger.info()` on timeout and context overflow in both methods
21
-
22
- ### `src/core/shutdown-handler.ts`
23
- - Added session ID logging in `handleShutdown()` — prints `Session ID: <id>` when a Claude session is interrupted via Ctrl+C/SIGTERM
24
-
25
- ### `tests/unit/stream-renderer.test.ts`
26
- - Added 3 new tests: session_id extraction, undefined for missing session_id, undefined for non-system events
27
-
28
- ### `tests/unit/claude-runner.test.ts`
29
- - Updated existing `run()` tests to emit NDJSON events (since `run()` now uses stream-json format)
30
- - Updated flag assertion: `run()` now includes `--output-format stream-json --verbose`
31
- - Added 5 new tests: sessionId extraction in run(), runVerbose(), undefined when missing, getter exposure, deduplication
32
-
33
- ## Test Results
34
-
35
- All 986 tests pass (45 test suites). No regressions introduced.
36
-
37
- <promise>COMPLETE</promise>
@@ -1,45 +0,0 @@
1
- # Outcome: Add --resume Flag to raf do Command
2
-
3
- ## Summary
4
-
5
- Added `--resume <session-id>` option to `raf do` that resumes an interrupted Claude session for a specific task. When used, Claude is spawned with `--resume` flag only (no prompt/model/system-prompt flags), and completion monitoring works identically to normal execution.
6
-
7
- ## Key Changes
8
-
9
- ### `src/types/config.ts`
10
- - Added `resume?: string` field to `DoCommandOptions` interface
11
-
12
- ### `src/commands/do.ts`
13
- - Added `-r, --resume <session-id>` option to the `do` command definition
14
- - Added `resumeSessionId` to `SingleProjectOptions` interface
15
- - In the task execution loop: when `activeResumeSessionId` is set, calls `runResume()` instead of `run()`/`runVerbose()` for the first attempt
16
- - Clears `activeResumeSessionId` after the first task completes, so subsequent tasks use normal execution
17
- - Prints clear user-facing message: `Resuming task <id> with session <session-id>` in both verbose and minimal modes
18
-
19
- ### `src/core/claude-runner.ts`
20
- - Added `runResume(sessionId, options)` method that spawns Claude with:
21
- - `--resume <session-id>` — restores the interrupted session
22
- - `--dangerously-skip-permissions` — required for non-interactive operation
23
- - `--output-format stream-json --verbose` — enables NDJSON event parsing
24
- - Does NOT pass `--model`, `--append-system-prompt`, or `-p` (Claude restores these from session state)
25
- - Same completion detection, timeout handling, context overflow detection, and session ID extraction as existing methods
26
-
27
- ### `tests/unit/claude-runner.test.ts`
28
- - Added 11 new tests in `runResume()` describe block:
29
- - Spawns with `--resume` flag and session ID
30
- - Does NOT include `--model`, `--append-system-prompt`, or `-p` flags
31
- - Includes `--dangerously-skip-permissions` flag
32
- - Includes `--output-format stream-json` and `--verbose` flags
33
- - Collects output from NDJSON events
34
- - Handles timeout correctly
35
- - Detects completion markers
36
- - Extracts session ID from resumed session
37
- - Passes cwd to spawn (worktree support)
38
- - Detects context overflow
39
- - Sets CLAUDE_CODE_EFFORT_LEVEL env var when provided
40
-
41
- ## Test Results
42
-
43
- All 997 tests pass (45 test suites). No regressions introduced.
44
-
45
- <promise>COMPLETE</promise>