rafcode 2.4.0 → 2.5.0-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 +3 -1
- package/CLAUDE.md +7 -5
- package/RAF/ahwidh-quick-fix-gremlin/decisions.md +37 -0
- package/RAF/ahwidh-quick-fix-gremlin/input.md +35 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/01-fix-name-generation-prompt.md +33 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/02-fix-amend-commit-scope.md +43 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/03-fix-diverged-main-branch-sync.md +32 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/04-wire-rate-limit-to-do-command.md +61 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/05-add-config-get-set-flags.md +125 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/06-sync-worktree-branch-before-execution.md +96 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/07-update-frontmatter-format.md +107 -0
- package/RAF/ahwidh-quick-fix-gremlin/outcomes/08-remove-plan-token-report.md +76 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/01-fix-name-generation-prompt.md +52 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/02-fix-amend-commit-scope.md +48 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/03-fix-diverged-main-branch-sync.md +49 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/04-wire-rate-limit-to-do-command.md +78 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/05-add-config-get-set-flags.md +101 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/06-sync-worktree-branch-before-execution.md +92 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/07-update-frontmatter-format.md +105 -0
- package/RAF/ahwidh-quick-fix-gremlin/plans/08-remove-plan-token-report.md +50 -0
- package/RAF/ahwqwq-model-whisperer/decisions.md +22 -0
- package/RAF/ahwqwq-model-whisperer/input.md +5 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/01-show-model-on-task-line.md +49 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/02-use-claude-cost-estimation.md +107 -0
- package/RAF/ahwqwq-model-whisperer/outcomes/03-add-plan-resume-flag.md +87 -0
- package/RAF/ahwqwq-model-whisperer/plans/01-show-model-on-task-line.md +45 -0
- package/RAF/ahwqwq-model-whisperer/plans/02-use-claude-cost-estimation.md +115 -0
- package/RAF/ahwqwq-model-whisperer/plans/03-add-plan-resume-flag.md +70 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +209 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/do.d.ts.map +1 -1
- package/dist/commands/do.js +37 -8
- package/dist/commands/do.js.map +1 -1
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +92 -54
- package/dist/commands/plan.js.map +1 -1
- package/dist/core/claude-runner.d.ts +8 -6
- package/dist/core/claude-runner.d.ts.map +1 -1
- package/dist/core/claude-runner.js +73 -5
- package/dist/core/claude-runner.js.map +1 -1
- package/dist/core/worktree.d.ts +12 -0
- package/dist/core/worktree.d.ts.map +1 -1
- package/dist/core/worktree.js +33 -1
- package/dist/core/worktree.js.map +1 -1
- package/dist/parsers/stream-renderer.d.ts +2 -0
- package/dist/parsers/stream-renderer.d.ts.map +1 -1
- package/dist/parsers/stream-renderer.js +2 -0
- package/dist/parsers/stream-renderer.js.map +1 -1
- package/dist/prompts/amend.d.ts.map +1 -1
- package/dist/prompts/amend.js +3 -1
- package/dist/prompts/amend.js.map +1 -1
- package/dist/prompts/planning.d.ts.map +1 -1
- package/dist/prompts/planning.js +3 -1
- package/dist/prompts/planning.js.map +1 -1
- package/dist/types/config.d.ts +4 -24
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +0 -24
- package/dist/types/config.js.map +1 -1
- package/dist/utils/config.d.ts +1 -26
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +2 -98
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +13 -3
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +40 -10
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/name-generator.d.ts.map +1 -1
- package/dist/utils/name-generator.js +7 -16
- package/dist/utils/name-generator.js.map +1 -1
- package/dist/utils/terminal-symbols.d.ts +7 -16
- package/dist/utils/terminal-symbols.d.ts.map +1 -1
- package/dist/utils/terminal-symbols.js +16 -42
- package/dist/utils/terminal-symbols.js.map +1 -1
- package/dist/utils/token-tracker.d.ts +4 -30
- package/dist/utils/token-tracker.d.ts.map +1 -1
- package/dist/utils/token-tracker.js +17 -98
- package/dist/utils/token-tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/config.ts +242 -0
- package/src/commands/do.ts +39 -7
- package/src/commands/plan.ts +101 -58
- package/src/core/claude-runner.ts +82 -12
- package/src/core/worktree.ts +37 -1
- package/src/parsers/stream-renderer.ts +4 -0
- package/src/prompts/amend.ts +3 -1
- package/src/prompts/config-docs.md +1 -72
- package/src/prompts/planning.ts +3 -1
- package/src/types/config.ts +4 -52
- package/src/utils/config.ts +2 -112
- package/src/utils/frontmatter.ts +41 -11
- package/src/utils/name-generator.ts +7 -16
- package/src/utils/terminal-symbols.ts +16 -46
- package/src/utils/token-tracker.ts +19 -113
- package/tests/unit/claude-runner.test.ts +1 -0
- package/tests/unit/commit-planning-artifacts-worktree.test.ts +6 -14
- package/tests/unit/commit-planning-artifacts.test.ts +4 -12
- package/tests/unit/config-command.test.ts +161 -0
- package/tests/unit/config.test.ts +6 -148
- package/tests/unit/frontmatter.test.ts +95 -1
- package/tests/unit/name-generator.test.ts +1 -1
- package/tests/unit/post-execution-picker.test.ts +1 -0
- package/tests/unit/stream-renderer.test.ts +82 -0
- package/tests/unit/terminal-symbols.test.ts +86 -124
- package/tests/unit/token-tracker.test.ts +159 -679
- package/tests/unit/worktree.test.ts +68 -1
- package/src/utils/session-parser.ts +0 -161
- package/tests/unit/session-parser.test.ts +0 -301
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import { resolveModelPricingCategory, getPricingConfig, getRateLimitWindowConfig } from './config.js';
|
|
2
1
|
/**
|
|
3
2
|
* Sum multiple CostBreakdown objects into a single total.
|
|
4
3
|
*/
|
|
5
4
|
export function sumCostBreakdowns(costs) {
|
|
6
|
-
|
|
7
|
-
inputCost: 0,
|
|
8
|
-
outputCost: 0,
|
|
9
|
-
cacheReadCost: 0,
|
|
10
|
-
cacheCreateCost: 0,
|
|
11
|
-
totalCost: 0,
|
|
12
|
-
};
|
|
5
|
+
let totalCost = 0;
|
|
13
6
|
for (const cost of costs) {
|
|
14
|
-
|
|
15
|
-
result.outputCost += cost.outputCost;
|
|
16
|
-
result.cacheReadCost += cost.cacheReadCost;
|
|
17
|
-
result.cacheCreateCost += cost.cacheCreateCost;
|
|
18
|
-
result.totalCost += cost.totalCost;
|
|
7
|
+
totalCost += cost.totalCost;
|
|
19
8
|
}
|
|
20
|
-
return
|
|
9
|
+
return { totalCost };
|
|
21
10
|
}
|
|
22
11
|
/**
|
|
23
12
|
* Merge multiple UsageData objects into a single accumulated UsageData.
|
|
@@ -30,6 +19,7 @@ export function accumulateUsage(attempts) {
|
|
|
30
19
|
cacheReadInputTokens: 0,
|
|
31
20
|
cacheCreationInputTokens: 0,
|
|
32
21
|
modelUsage: {},
|
|
22
|
+
totalCostUsd: 0,
|
|
33
23
|
};
|
|
34
24
|
for (const attempt of attempts) {
|
|
35
25
|
result.inputTokens += attempt.inputTokens;
|
|
@@ -44,36 +34,35 @@ export function accumulateUsage(attempts) {
|
|
|
44
34
|
existing.outputTokens += modelUsage.outputTokens;
|
|
45
35
|
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
46
36
|
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
37
|
+
existing.costUsd += modelUsage.costUsd;
|
|
47
38
|
}
|
|
48
39
|
else {
|
|
49
40
|
result.modelUsage[modelId] = { ...modelUsage };
|
|
50
41
|
}
|
|
51
42
|
}
|
|
43
|
+
// Sum totalCostUsd across attempts
|
|
44
|
+
result.totalCostUsd += attempt.totalCostUsd;
|
|
52
45
|
}
|
|
53
46
|
return result;
|
|
54
47
|
}
|
|
55
48
|
/**
|
|
56
|
-
* Accumulates token usage across multiple task executions
|
|
57
|
-
* using configurable per-model pricing.
|
|
49
|
+
* Accumulates token usage across multiple task executions using Claude-provided cost data.
|
|
58
50
|
*/
|
|
59
51
|
export class TokenTracker {
|
|
60
52
|
entries = [];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.pricingConfig = pricingConfig ?? getPricingConfig();
|
|
53
|
+
constructor() {
|
|
54
|
+
// No pricing config needed - costs come from Claude
|
|
64
55
|
}
|
|
65
56
|
/**
|
|
66
57
|
* Record usage data from a completed task.
|
|
67
58
|
* Accepts an array of UsageData (one per attempt) and accumulates them.
|
|
68
|
-
*
|
|
69
|
-
* have modelUsage and others only have aggregate fields.
|
|
59
|
+
* Costs are summed from Claude-provided totalCostUsd values.
|
|
70
60
|
*/
|
|
71
61
|
addTask(taskId, attempts) {
|
|
72
62
|
const usage = accumulateUsage(attempts);
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
const cost = sumCostBreakdowns(perAttemptCosts);
|
|
63
|
+
// Sum costs from Claude-provided totalCostUsd
|
|
64
|
+
const totalCost = attempts.reduce((sum, attempt) => sum + attempt.totalCostUsd, 0);
|
|
65
|
+
const cost = { totalCost };
|
|
77
66
|
const entry = { taskId, usage, cost, attempts };
|
|
78
67
|
this.entries.push(entry);
|
|
79
68
|
return entry;
|
|
@@ -94,12 +83,9 @@ export class TokenTracker {
|
|
|
94
83
|
cacheReadInputTokens: 0,
|
|
95
84
|
cacheCreationInputTokens: 0,
|
|
96
85
|
modelUsage: {},
|
|
86
|
+
totalCostUsd: 0,
|
|
97
87
|
};
|
|
98
88
|
const totalCost = {
|
|
99
|
-
inputCost: 0,
|
|
100
|
-
outputCost: 0,
|
|
101
|
-
cacheReadCost: 0,
|
|
102
|
-
cacheCreateCost: 0,
|
|
103
89
|
totalCost: 0,
|
|
104
90
|
};
|
|
105
91
|
for (const entry of this.entries) {
|
|
@@ -107,6 +93,7 @@ export class TokenTracker {
|
|
|
107
93
|
totalUsage.outputTokens += entry.usage.outputTokens;
|
|
108
94
|
totalUsage.cacheReadInputTokens += entry.usage.cacheReadInputTokens;
|
|
109
95
|
totalUsage.cacheCreationInputTokens += entry.usage.cacheCreationInputTokens;
|
|
96
|
+
totalUsage.totalCostUsd += entry.usage.totalCostUsd;
|
|
110
97
|
// Merge per-model usage
|
|
111
98
|
for (const [modelId, modelUsage] of Object.entries(entry.usage.modelUsage)) {
|
|
112
99
|
const existing = totalUsage.modelUsage[modelId];
|
|
@@ -115,83 +102,15 @@ export class TokenTracker {
|
|
|
115
102
|
existing.outputTokens += modelUsage.outputTokens;
|
|
116
103
|
existing.cacheReadInputTokens += modelUsage.cacheReadInputTokens;
|
|
117
104
|
existing.cacheCreationInputTokens += modelUsage.cacheCreationInputTokens;
|
|
105
|
+
existing.costUsd += modelUsage.costUsd;
|
|
118
106
|
}
|
|
119
107
|
else {
|
|
120
108
|
totalUsage.modelUsage[modelId] = { ...modelUsage };
|
|
121
109
|
}
|
|
122
110
|
}
|
|
123
|
-
totalCost.inputCost += entry.cost.inputCost;
|
|
124
|
-
totalCost.outputCost += entry.cost.outputCost;
|
|
125
|
-
totalCost.cacheReadCost += entry.cost.cacheReadCost;
|
|
126
|
-
totalCost.cacheCreateCost += entry.cost.cacheCreateCost;
|
|
127
111
|
totalCost.totalCost += entry.cost.totalCost;
|
|
128
112
|
}
|
|
129
113
|
return { usage: totalUsage, cost: totalCost };
|
|
130
114
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Calculate cost for a given UsageData using per-model pricing.
|
|
133
|
-
* Uses per-model breakdown when available, falls back to aggregate with sonnet pricing.
|
|
134
|
-
*/
|
|
135
|
-
calculateCost(usage) {
|
|
136
|
-
const result = {
|
|
137
|
-
inputCost: 0,
|
|
138
|
-
outputCost: 0,
|
|
139
|
-
cacheReadCost: 0,
|
|
140
|
-
cacheCreateCost: 0,
|
|
141
|
-
totalCost: 0,
|
|
142
|
-
};
|
|
143
|
-
const modelEntries = Object.entries(usage.modelUsage);
|
|
144
|
-
if (modelEntries.length > 0) {
|
|
145
|
-
// Use per-model breakdown for accurate pricing
|
|
146
|
-
for (const [modelId, modelUsage] of modelEntries) {
|
|
147
|
-
const category = resolveModelPricingCategory(modelId);
|
|
148
|
-
const pricing = this.pricingConfig[category ?? 'sonnet'];
|
|
149
|
-
result.inputCost += (modelUsage.inputTokens / 1_000_000) * pricing.inputPerMTok;
|
|
150
|
-
result.outputCost += (modelUsage.outputTokens / 1_000_000) * pricing.outputPerMTok;
|
|
151
|
-
result.cacheReadCost += (modelUsage.cacheReadInputTokens / 1_000_000) * pricing.cacheReadPerMTok;
|
|
152
|
-
result.cacheCreateCost += (modelUsage.cacheCreationInputTokens / 1_000_000) * pricing.cacheCreatePerMTok;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
// Fallback: use aggregate totals with sonnet pricing
|
|
157
|
-
const pricing = this.pricingConfig.sonnet;
|
|
158
|
-
result.inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPerMTok;
|
|
159
|
-
result.outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPerMTok;
|
|
160
|
-
result.cacheReadCost = (usage.cacheReadInputTokens / 1_000_000) * pricing.cacheReadPerMTok;
|
|
161
|
-
result.cacheCreateCost = (usage.cacheCreationInputTokens / 1_000_000) * pricing.cacheCreatePerMTok;
|
|
162
|
-
}
|
|
163
|
-
result.totalCost = result.inputCost + result.outputCost + result.cacheReadCost + result.cacheCreateCost;
|
|
164
|
-
return result;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Calculate the 5h rate limit window percentage for a given cost.
|
|
168
|
-
* Converts cost to Sonnet-equivalent tokens using the configured Sonnet pricing,
|
|
169
|
-
* then divides by the configured cap.
|
|
170
|
-
*
|
|
171
|
-
* @param totalCost - The total cost in dollars
|
|
172
|
-
* @param sonnetTokenCap - Optional override for the Sonnet-equivalent token cap (defaults to config value)
|
|
173
|
-
* @returns The percentage of the 5h window consumed (0-100+)
|
|
174
|
-
*/
|
|
175
|
-
calculateRateLimitPercentage(totalCost, sonnetTokenCap) {
|
|
176
|
-
if (totalCost === 0)
|
|
177
|
-
return 0;
|
|
178
|
-
// Get the configured cap or use the provided override
|
|
179
|
-
const cap = sonnetTokenCap ?? getRateLimitWindowConfig().sonnetTokenCap;
|
|
180
|
-
// Calculate the average Sonnet cost per token
|
|
181
|
-
// Using the average of input and output pricing (simplified approach)
|
|
182
|
-
const sonnetPricing = this.pricingConfig.sonnet;
|
|
183
|
-
const avgSonnetCostPerToken = (sonnetPricing.inputPerMTok + sonnetPricing.outputPerMTok) / 2 / 1_000_000;
|
|
184
|
-
// Convert cost to Sonnet-equivalent tokens
|
|
185
|
-
const sonnetEquivalentTokens = totalCost / avgSonnetCostPerToken;
|
|
186
|
-
// Calculate percentage
|
|
187
|
-
return (sonnetEquivalentTokens / cap) * 100;
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Get the cumulative 5h window percentage across all recorded tasks.
|
|
191
|
-
*/
|
|
192
|
-
getCumulativeRateLimitPercentage(sonnetTokenCap) {
|
|
193
|
-
const totals = this.getTotals();
|
|
194
|
-
return this.calculateRateLimitPercentage(totals.cost.totalCost, sonnetTokenCap);
|
|
195
|
-
}
|
|
196
115
|
}
|
|
197
116
|
//# sourceMappingURL=token-tracker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-tracker.js","sourceRoot":"","sources":["../../src/utils/token-tracker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token-tracker.js","sourceRoot":"","sources":["../../src/utils/token-tracker.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/config.ts
CHANGED
|
@@ -13,11 +13,16 @@ import {
|
|
|
13
13
|
validateConfig,
|
|
14
14
|
ConfigValidationError,
|
|
15
15
|
resetConfigCache,
|
|
16
|
+
resolveConfig,
|
|
17
|
+
saveConfig,
|
|
16
18
|
} from '../utils/config.js';
|
|
17
19
|
import { DEFAULT_CONFIG } from '../types/config.js';
|
|
20
|
+
import type { UserConfig } from '../types/config.js';
|
|
18
21
|
|
|
19
22
|
interface ConfigCommandOptions {
|
|
20
23
|
reset?: boolean;
|
|
24
|
+
get?: true | string; // true when --get with no key, string when --get <key>
|
|
25
|
+
set?: string[]; // [key, value]
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
/**
|
|
@@ -114,17 +119,254 @@ async function confirm(message: string): Promise<boolean> {
|
|
|
114
119
|
});
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
// ---- Helper functions for nested config access ----
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get a nested value from an object using dot notation.
|
|
126
|
+
* Returns undefined if the path doesn't exist.
|
|
127
|
+
*/
|
|
128
|
+
function getNestedValue(obj: unknown, dotPath: string): unknown {
|
|
129
|
+
const keys = dotPath.split('.');
|
|
130
|
+
let current: unknown = obj;
|
|
131
|
+
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
if (current === null || typeof current !== 'object') {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
current = (current as Record<string, unknown>)[key];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set a nested value in an object using dot notation.
|
|
144
|
+
* Creates intermediate objects as needed.
|
|
145
|
+
*/
|
|
146
|
+
function setNestedValue(obj: Record<string, unknown>, dotPath: string, value: unknown): void {
|
|
147
|
+
const keys = dotPath.split('.');
|
|
148
|
+
let current: Record<string, unknown> = obj;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
151
|
+
const key = keys[i]!;
|
|
152
|
+
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
153
|
+
current[key] = {};
|
|
154
|
+
}
|
|
155
|
+
current = current[key] as Record<string, unknown>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const lastKey = keys[keys.length - 1]!;
|
|
159
|
+
current[lastKey] = value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete a nested value from an object using dot notation.
|
|
164
|
+
* Cleans up empty parent objects after deletion.
|
|
165
|
+
*/
|
|
166
|
+
function deleteNestedValue(obj: Record<string, unknown>, dotPath: string): void {
|
|
167
|
+
const keys = dotPath.split('.');
|
|
168
|
+
|
|
169
|
+
// Navigate to parent and delete the key
|
|
170
|
+
if (keys.length === 1) {
|
|
171
|
+
delete obj[keys[0]!];
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Build path to parent
|
|
176
|
+
let current: Record<string, unknown> = obj;
|
|
177
|
+
const path: Array<{ obj: Record<string, unknown>; key: string }> = [];
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
180
|
+
const key = keys[i]!;
|
|
181
|
+
path.push({ obj: current, key });
|
|
182
|
+
|
|
183
|
+
if (typeof current[key] !== 'object' || current[key] === null) {
|
|
184
|
+
return; // Path doesn't exist
|
|
185
|
+
}
|
|
186
|
+
current = current[key] as Record<string, unknown>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Delete the leaf value
|
|
190
|
+
const lastKey = keys[keys.length - 1]!;
|
|
191
|
+
delete current[lastKey];
|
|
192
|
+
|
|
193
|
+
// Clean up empty parents
|
|
194
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
195
|
+
const { obj, key } = path[i]!;
|
|
196
|
+
const child = obj[key] as Record<string, unknown>;
|
|
197
|
+
if (Object.keys(child).length === 0) {
|
|
198
|
+
delete obj[key];
|
|
199
|
+
} else {
|
|
200
|
+
break; // Stop if we find a non-empty parent
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get the default value at a dot-notation path from DEFAULT_CONFIG.
|
|
207
|
+
*/
|
|
208
|
+
function getDefaultValue(dotPath: string): unknown {
|
|
209
|
+
return getNestedValue(DEFAULT_CONFIG, dotPath);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Parse a string value, attempting JSON.parse for numbers/booleans, falling back to string.
|
|
214
|
+
*/
|
|
215
|
+
function parseValue(value: string): unknown {
|
|
216
|
+
// Try JSON.parse for numbers, booleans, null
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(value);
|
|
219
|
+
} catch {
|
|
220
|
+
// Fall back to string
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Format a value for console output.
|
|
227
|
+
* Strings are printed plain, objects/arrays as JSON.
|
|
228
|
+
*/
|
|
229
|
+
function formatValue(value: unknown): string {
|
|
230
|
+
if (typeof value === 'string') {
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
234
|
+
return String(value);
|
|
235
|
+
}
|
|
236
|
+
if (value === null || value === undefined) {
|
|
237
|
+
return String(value);
|
|
238
|
+
}
|
|
239
|
+
return JSON.stringify(value, null, 2);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- Config get/set handlers ----
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Handle --get flag: print config value(s).
|
|
246
|
+
*/
|
|
247
|
+
function handleGet(key: true | string): void {
|
|
248
|
+
const config = resolveConfig();
|
|
249
|
+
|
|
250
|
+
if (key === true) {
|
|
251
|
+
// No key specified: print full config
|
|
252
|
+
console.log(JSON.stringify(config, null, 2));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Specific key requested
|
|
257
|
+
const value = getNestedValue(config, key);
|
|
258
|
+
|
|
259
|
+
if (value === undefined) {
|
|
260
|
+
logger.error(`Config key not found: ${key}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(formatValue(value));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle --set flag: update config file with a new value.
|
|
269
|
+
*/
|
|
270
|
+
function handleSet(args: string[]): void {
|
|
271
|
+
if (args.length !== 2) {
|
|
272
|
+
logger.error('--set requires exactly 2 arguments: key and value');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const [key, rawValue] = args as [string, string];
|
|
277
|
+
const value = parseValue(rawValue);
|
|
278
|
+
const configPath = getConfigPath();
|
|
279
|
+
|
|
280
|
+
// Read current user config (or start with empty)
|
|
281
|
+
let userConfig: UserConfig = {};
|
|
282
|
+
if (fs.existsSync(configPath)) {
|
|
283
|
+
try {
|
|
284
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
285
|
+
userConfig = JSON.parse(content) as UserConfig;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logger.error(`Failed to read config file: ${error}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check if value matches default
|
|
293
|
+
const defaultValue = getDefaultValue(key);
|
|
294
|
+
|
|
295
|
+
if (defaultValue === undefined) {
|
|
296
|
+
logger.error(`Config key not found in schema: ${key}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Deep equality check for objects
|
|
301
|
+
const valuesMatch = JSON.stringify(value) === JSON.stringify(defaultValue);
|
|
302
|
+
|
|
303
|
+
if (valuesMatch) {
|
|
304
|
+
// Remove from config file (keep config minimal)
|
|
305
|
+
deleteNestedValue(userConfig as Record<string, unknown>, key);
|
|
306
|
+
logger.info(`Value matches default, removing ${key} from config`);
|
|
307
|
+
} else {
|
|
308
|
+
// Set the value
|
|
309
|
+
setNestedValue(userConfig as Record<string, unknown>, key, value);
|
|
310
|
+
logger.info(`Set ${key} = ${formatValue(value)}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validate the resulting config
|
|
314
|
+
try {
|
|
315
|
+
validateConfig(userConfig);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof ConfigValidationError) {
|
|
318
|
+
logger.error(`Validation error: ${error.message}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Save or delete config file
|
|
325
|
+
if (Object.keys(userConfig).length === 0) {
|
|
326
|
+
// Config is empty, delete the file
|
|
327
|
+
if (fs.existsSync(configPath)) {
|
|
328
|
+
fs.unlinkSync(configPath);
|
|
329
|
+
logger.info('Config is empty, removed file');
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
saveConfig(configPath, userConfig);
|
|
333
|
+
logger.success('Config updated successfully');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
117
337
|
export function createConfigCommand(): Command {
|
|
118
338
|
const command = new Command('config')
|
|
119
339
|
.description('View and edit RAF configuration with Claude')
|
|
120
340
|
.argument('[prompt...]', 'Optional initial prompt for the config session')
|
|
121
341
|
.option('--reset', 'Delete config file and restore all defaults')
|
|
342
|
+
.option('--get [key]', 'Show config value (all config if no key, or specific dot-notation key)')
|
|
343
|
+
.option('--set <items...>', 'Set a config value using dot-notation key and value')
|
|
122
344
|
.action(async (promptParts: string[], options: ConfigCommandOptions) => {
|
|
345
|
+
// --reset takes precedence
|
|
123
346
|
if (options.reset) {
|
|
124
347
|
await handleReset();
|
|
125
348
|
return;
|
|
126
349
|
}
|
|
127
350
|
|
|
351
|
+
// --get and --set are mutually exclusive
|
|
352
|
+
if (options.get !== undefined && options.set !== undefined) {
|
|
353
|
+
logger.error('Cannot use --get and --set together');
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle --get
|
|
358
|
+
if (options.get !== undefined) {
|
|
359
|
+
handleGet(options.get);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Handle --set
|
|
364
|
+
if (options.set !== undefined) {
|
|
365
|
+
handleSet(options.set);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Default: run interactive session
|
|
128
370
|
const initialPrompt = promptParts.length > 0 ? promptParts.join(' ') : undefined;
|
|
129
371
|
await runConfigSession(initialPrompt);
|
|
130
372
|
});
|
package/src/commands/do.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { getRafDir, extractProjectNumber, extractProjectName, extractTaskNameFro
|
|
|
13
13
|
import { pickPendingProject, getPendingProjects, getPendingWorktreeProjects } from '../ui/project-picker.js';
|
|
14
14
|
import type { PendingProjectInfo } from '../ui/project-picker.js';
|
|
15
15
|
import { logger } from '../utils/logger.js';
|
|
16
|
-
import { getConfig, getWorktreeDefault, getModel, getModelShortName, resolveFullModelId, getSyncMainBranch, resolveEffortToModel, applyModelCeiling } from '../utils/config.js';
|
|
16
|
+
import { getConfig, getWorktreeDefault, getModel, getModelShortName, resolveFullModelId, getSyncMainBranch, resolveEffortToModel, applyModelCeiling, getShowCacheTokens } from '../utils/config.js';
|
|
17
17
|
import type { PlanFrontmatter } from '../utils/frontmatter.js';
|
|
18
18
|
import { getVersion } from '../utils/version.js';
|
|
19
19
|
import { createTaskTimer, formatElapsedTime } from '../utils/timer.js';
|
|
@@ -53,6 +53,8 @@ import {
|
|
|
53
53
|
resolveWorktreeProjectByIdentifier,
|
|
54
54
|
pushMainBranch,
|
|
55
55
|
pullMainBranch,
|
|
56
|
+
detectMainBranch,
|
|
57
|
+
rebaseOntoMain,
|
|
56
58
|
} from '../core/worktree.js';
|
|
57
59
|
import { createPullRequest, prPreflight } from '../core/pull-request.js';
|
|
58
60
|
import type { DoCommandOptions } from '../types/config.js';
|
|
@@ -224,6 +226,7 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
|
|
|
224
226
|
// Variables for worktree context (set when --worktree is used)
|
|
225
227
|
let worktreeRoot: string | undefined;
|
|
226
228
|
let originalBranch: string | undefined;
|
|
229
|
+
let mainBranchName: string | null = null;
|
|
227
230
|
|
|
228
231
|
if (worktreeMode) {
|
|
229
232
|
// Validate git repo
|
|
@@ -241,6 +244,7 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
|
|
|
241
244
|
// Sync main branch before worktree operations (if enabled)
|
|
242
245
|
if (getSyncMainBranch()) {
|
|
243
246
|
const syncResult = pullMainBranch();
|
|
247
|
+
mainBranchName = syncResult.mainBranch;
|
|
244
248
|
if (syncResult.success) {
|
|
245
249
|
if (syncResult.hadChanges) {
|
|
246
250
|
logger.info(`Synced ${syncResult.mainBranch} from remote`);
|
|
@@ -462,6 +466,20 @@ async function runDoCommand(projectIdentifierArg: string | undefined, options: D
|
|
|
462
466
|
}
|
|
463
467
|
throw error;
|
|
464
468
|
}
|
|
469
|
+
|
|
470
|
+
// Rebase worktree branch onto main before execution (if sync is enabled)
|
|
471
|
+
if (getSyncMainBranch()) {
|
|
472
|
+
const mainBranch = mainBranchName ?? detectMainBranch();
|
|
473
|
+
if (mainBranch) {
|
|
474
|
+
const rebaseResult = rebaseOntoMain(mainBranch, worktreeRoot);
|
|
475
|
+
if (rebaseResult.success) {
|
|
476
|
+
logger.info(`Rebased onto ${mainBranch}`);
|
|
477
|
+
} else {
|
|
478
|
+
logger.warn(`Could not rebase onto ${mainBranch}: ${rebaseResult.error}`);
|
|
479
|
+
logger.warn('Continuing with current branch state.');
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
465
483
|
}
|
|
466
484
|
|
|
467
485
|
// Execute project
|
|
@@ -1003,6 +1021,8 @@ async function executeSingleProject(
|
|
|
1003
1021
|
const attemptUsageData: import('../types/config.js').UsageData[] = [];
|
|
1004
1022
|
// Track failure history for each attempt (attempt number -> reason)
|
|
1005
1023
|
const failureHistory: Array<{ attempt: number; reason: string }> = [];
|
|
1024
|
+
// Track current model for display in status line (updated in retry loop)
|
|
1025
|
+
let currentModel: string | undefined;
|
|
1006
1026
|
|
|
1007
1027
|
// Set up timer for elapsed time tracking
|
|
1008
1028
|
const statusLine = createStatusLine();
|
|
@@ -1013,7 +1033,8 @@ async function executeSingleProject(
|
|
|
1013
1033
|
return;
|
|
1014
1034
|
}
|
|
1015
1035
|
// Show running status with task name and timer (updates in place)
|
|
1016
|
-
|
|
1036
|
+
const modelShortName = currentModel ? getModelShortName(currentModel) : undefined;
|
|
1037
|
+
statusLine.update(formatTaskProgress(taskNumber, totalTasks, 'running', displayName, elapsed, taskId, modelShortName));
|
|
1017
1038
|
});
|
|
1018
1039
|
timer.start();
|
|
1019
1040
|
|
|
@@ -1036,6 +1057,9 @@ async function executeSingleProject(
|
|
|
1036
1057
|
isRetry,
|
|
1037
1058
|
);
|
|
1038
1059
|
|
|
1060
|
+
// Update current model for timer callback display
|
|
1061
|
+
currentModel = modelResolution.model;
|
|
1062
|
+
|
|
1039
1063
|
// Log missing frontmatter warning on first attempt only
|
|
1040
1064
|
if (!isRetry && modelResolution.missingFrontmatter) {
|
|
1041
1065
|
logger.warn(` No effort frontmatter found — using ceiling model`);
|
|
@@ -1211,13 +1235,16 @@ Task completed. No detailed report provided.
|
|
|
1211
1235
|
logger.success(` Task ${taskLabel} completed (${elapsedFormatted})`);
|
|
1212
1236
|
} else {
|
|
1213
1237
|
// Minimal mode: show completed task line
|
|
1214
|
-
|
|
1238
|
+
const modelShortName = currentModel ? getModelShortName(currentModel) : undefined;
|
|
1239
|
+
logger.info(formatTaskProgress(taskNumber, totalTasks, 'completed', displayName, elapsedMs, task.id, modelShortName));
|
|
1215
1240
|
}
|
|
1216
1241
|
|
|
1217
1242
|
// Track and display token usage for this task
|
|
1218
1243
|
if (attemptUsageData.length > 0) {
|
|
1219
1244
|
const entry = tokenTracker.addTask(task.id, attemptUsageData);
|
|
1220
|
-
logger.dim(formatTaskTokenSummary(entry,
|
|
1245
|
+
logger.dim(formatTaskTokenSummary(entry, {
|
|
1246
|
+
showCacheTokens: getShowCacheTokens(),
|
|
1247
|
+
}));
|
|
1221
1248
|
}
|
|
1222
1249
|
|
|
1223
1250
|
completedInSession.add(task.id);
|
|
@@ -1239,13 +1266,16 @@ Task completed. No detailed report provided.
|
|
|
1239
1266
|
logger.info(` Analyzing failure with ${analysisModel}...`);
|
|
1240
1267
|
} else {
|
|
1241
1268
|
// Minimal mode: show failed task line
|
|
1242
|
-
|
|
1269
|
+
const modelShortName = currentModel ? getModelShortName(currentModel) : undefined;
|
|
1270
|
+
logger.info(formatTaskProgress(taskNumber, totalTasks, 'failed', displayName, elapsedMs, task.id, modelShortName));
|
|
1243
1271
|
}
|
|
1244
1272
|
|
|
1245
1273
|
// Track token usage even for failed tasks (partial data still useful for totals)
|
|
1246
1274
|
if (attemptUsageData.length > 0) {
|
|
1247
1275
|
const entry = tokenTracker.addTask(task.id, attemptUsageData);
|
|
1248
|
-
logger.dim(formatTaskTokenSummary(entry,
|
|
1276
|
+
logger.dim(formatTaskTokenSummary(entry, {
|
|
1277
|
+
showCacheTokens: getShowCacheTokens(),
|
|
1278
|
+
}));
|
|
1249
1279
|
}
|
|
1250
1280
|
|
|
1251
1281
|
// Analyze failure and generate structured report
|
|
@@ -1358,7 +1388,9 @@ ${stashName ? `- Stash: ${stashName}` : ''}
|
|
|
1358
1388
|
if (trackerEntries.length > 0) {
|
|
1359
1389
|
logger.newline();
|
|
1360
1390
|
const totals = tokenTracker.getTotals();
|
|
1361
|
-
logger.dim(formatTokenTotalSummary(totals.usage, totals.cost
|
|
1391
|
+
logger.dim(formatTokenTotalSummary(totals.usage, totals.cost, {
|
|
1392
|
+
showCacheTokens: getShowCacheTokens(),
|
|
1393
|
+
}));
|
|
1362
1394
|
}
|
|
1363
1395
|
|
|
1364
1396
|
// Show retry history for tasks that had failures (even if eventually successful)
|