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.
- package/.claude/settings.local.json +4 -1
- package/CLAUDE.md +59 -11
- package/RAF/ahslfe-config-wizard/decisions.md +34 -0
- package/RAF/ahslfe-config-wizard/input.md +1 -0
- package/RAF/ahslfe-config-wizard/outcomes/01-define-config-schema.md +38 -0
- package/RAF/ahslfe-config-wizard/outcomes/02-refactor-codebase-to-use-config.md +67 -0
- package/RAF/ahslfe-config-wizard/outcomes/03-create-config-documentation.md +37 -0
- package/RAF/ahslfe-config-wizard/outcomes/04-implement-raf-config-command.md +47 -0
- package/RAF/ahslfe-config-wizard/outcomes/05-update-claude-md.md +26 -0
- package/RAF/ahslfe-config-wizard/plans/01-define-config-schema.md +73 -0
- package/RAF/ahslfe-config-wizard/plans/02-refactor-codebase-to-use-config.md +74 -0
- package/RAF/ahslfe-config-wizard/plans/03-create-config-documentation.md +57 -0
- package/RAF/ahslfe-config-wizard/plans/04-implement-raf-config-command.md +66 -0
- package/RAF/ahslfe-config-wizard/plans/05-update-claude-md.md +60 -0
- package/RAF/ahstvo-token-tracker/decisions.md +44 -0
- package/RAF/ahstvo-token-tracker/input.md +3 -0
- package/RAF/ahstvo-token-tracker/outcomes/01-full-model-id-support.md +43 -0
- package/RAF/ahstvo-token-tracker/outcomes/02-name-generation-no-session.md +33 -0
- package/RAF/ahstvo-token-tracker/outcomes/03-unify-stream-json-execution.md +48 -0
- package/RAF/ahstvo-token-tracker/outcomes/04-token-tracking-cost-calculation.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/05-token-cost-console-reporting.md +57 -0
- package/RAF/ahstvo-token-tracker/outcomes/06-runtime-verbose-toggle.md +53 -0
- package/RAF/ahstvo-token-tracker/outcomes/07-readme-config-docs.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/01-full-model-id-support.md +35 -0
- package/RAF/ahstvo-token-tracker/plans/02-name-generation-no-session.md +36 -0
- package/RAF/ahstvo-token-tracker/plans/03-unify-stream-json-execution.md +44 -0
- package/RAF/ahstvo-token-tracker/plans/04-token-tracking-cost-calculation.md +56 -0
- package/RAF/ahstvo-token-tracker/plans/05-token-cost-console-reporting.md +55 -0
- package/RAF/ahstvo-token-tracker/plans/06-runtime-verbose-toggle.md +48 -0
- package/RAF/ahstvo-token-tracker/plans/07-readme-config-docs.md +44 -0
- package/README.md +34 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +173 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +50 -28
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +3 -2
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +17 -13
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +42 -257
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/failure-analyzer.d.ts.map +1 -1
- package/dist/core/failure-analyzer.js +6 -3
- package/dist/core/failure-analyzer.js.map +1 -1
- package/dist/core/git.d.ts.map +1 -1
- package/dist/core/git.js +10 -3
- package/dist/core/git.js.map +1 -1
- package/dist/core/pull-request.d.ts +1 -1
- package/dist/core/pull-request.d.ts.map +1 -1
- package/dist/core/pull-request.js +7 -4
- package/dist/core/pull-request.js.map +1 -1
- package/dist/core/shutdown-handler.d.ts.map +1 -1
- package/dist/core/shutdown-handler.js +0 -4
- package/dist/core/shutdown-handler.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +16 -4
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +35 -5
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/execution.d.ts.map +1 -1
- package/dist/prompts/execution.js +11 -1
- package/dist/prompts/execution.js.map +1 -1
- package/dist/types/config.d.ts +95 -5
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +63 -3
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +59 -7
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +276 -21
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/name-generator.d.ts +3 -7
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +75 -61
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +21 -0
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +62 -0
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +45 -0
- package/dist/utils/token-tracker.d.ts.map +1 -0
- package/dist/utils/token-tracker.js +107 -0
- package/dist/utils/token-tracker.js.map +1 -0
- package/dist/utils/validation.d.ts +5 -5
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +10 -6
- package/dist/utils/validation.js.map +1 -1
- package/dist/utils/verbose-toggle.d.ts +33 -0
- package/dist/utils/verbose-toggle.d.ts.map +1 -0
- package/dist/utils/verbose-toggle.js +94 -0
- package/dist/utils/verbose-toggle.js.map +1 -0
- package/package.json +1 -1
- package/src/commands/config.ts +204 -0
- package/src/commands/do.ts +59 -27
- package/src/commands/plan.ts +3 -2
- package/src/core/claude-runner.ts +58 -311
- package/src/core/failure-analyzer.ts +6 -3
- package/src/core/git.ts +10 -3
- package/src/core/pull-request.ts +7 -4
- package/src/core/shutdown-handler.ts +0 -5
- package/src/index.ts +2 -0
- package/src/parsers/stream-renderer.ts +55 -8
- package/src/prompts/config-docs.md +331 -0
- package/src/prompts/execution.ts +13 -1
- package/src/types/config.ts +156 -8
- package/src/utils/config.ts +335 -21
- package/src/utils/name-generator.ts +84 -71
- package/src/utils/terminal-symbols.ts +68 -0
- package/src/utils/token-tracker.ts +135 -0
- package/src/utils/validation.ts +15 -10
- package/src/utils/verbose-toggle.ts +103 -0
- package/tests/unit/claude-runner.test.ts +216 -403
- package/tests/unit/config-command.test.ts +163 -0
- package/tests/unit/config.test.ts +608 -30
- package/tests/unit/name-generator.test.ts +99 -75
- package/tests/unit/pull-request.test.ts +2 -0
- package/tests/unit/stream-renderer.test.ts +83 -30
- package/tests/unit/terminal-symbols.test.ts +157 -0
- package/tests/unit/token-tracker.test.ts +352 -0
- package/tests/unit/verbose-toggle.test.ts +204 -0
- package/RAF/ahrtxf-session-sentinel/decisions.md +0 -19
- package/RAF/ahrtxf-session-sentinel/input.md +0 -1
- package/RAF/ahrtxf-session-sentinel/outcomes/01-capture-session-id.md +0 -37
- package/RAF/ahrtxf-session-sentinel/outcomes/02-resume-flag.md +0 -45
- package/RAF/ahrtxf-session-sentinel/plans/01-capture-session-id.md +0 -41
- 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>
|