@cyperx/clawforge 1.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/LICENSE +21 -0
- package/README.md +312 -0
- package/VERSION +1 -0
- package/bin/attach.sh +98 -0
- package/bin/check-agents.sh +343 -0
- package/bin/clawforge +257 -0
- package/bin/clawforge-dashboard +0 -0
- package/bin/clean.sh +257 -0
- package/bin/config.sh +111 -0
- package/bin/conflicts.sh +224 -0
- package/bin/cost.sh +273 -0
- package/bin/dashboard.sh +557 -0
- package/bin/diff.sh +109 -0
- package/bin/doctor.sh +196 -0
- package/bin/eval.sh +217 -0
- package/bin/history.sh +91 -0
- package/bin/init.sh +182 -0
- package/bin/learn.sh +230 -0
- package/bin/logs.sh +126 -0
- package/bin/memory.sh +207 -0
- package/bin/merge-helper.sh +174 -0
- package/bin/multi-review.sh +215 -0
- package/bin/notify.sh +93 -0
- package/bin/on-complete.sh +149 -0
- package/bin/parse-cost.sh +205 -0
- package/bin/pr.sh +167 -0
- package/bin/resume.sh +183 -0
- package/bin/review-mode.sh +163 -0
- package/bin/review-pr.sh +145 -0
- package/bin/routing.sh +88 -0
- package/bin/scope-task.sh +169 -0
- package/bin/spawn-agent.sh +190 -0
- package/bin/sprint.sh +320 -0
- package/bin/steer.sh +107 -0
- package/bin/stop.sh +136 -0
- package/bin/summary.sh +182 -0
- package/bin/swarm.sh +525 -0
- package/bin/templates.sh +244 -0
- package/lib/common.sh +302 -0
- package/lib/templates/bugfix.json +6 -0
- package/lib/templates/migration.json +7 -0
- package/lib/templates/refactor.json +6 -0
- package/lib/templates/security-audit.json +5 -0
- package/lib/templates/test-coverage.json +6 -0
- package/package.json +31 -0
- package/registry/conflicts.jsonl +0 -0
- package/registry/costs.jsonl +0 -0
- package/tui/PRD.md +106 -0
- package/tui/agent.go +266 -0
- package/tui/animation.go +192 -0
- package/tui/dashboard.go +219 -0
- package/tui/filter.go +68 -0
- package/tui/go.mod +25 -0
- package/tui/go.sum +46 -0
- package/tui/keybindings.go +229 -0
- package/tui/main.go +61 -0
- package/tui/model.go +166 -0
- package/tui/steer.go +69 -0
- package/tui/styles.go +69 -0
package/bin/cost.sh
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cost.sh — Cost tracking module: capture, store, and query token usage
|
|
3
|
+
# Usage: clawforge cost [task-id] [--summary] [--json]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/../lib/common.sh"
|
|
8
|
+
|
|
9
|
+
COSTS_FILE="${CLAWFORGE_DIR}/registry/costs.jsonl"
|
|
10
|
+
|
|
11
|
+
# ── Help ───────────────────────────────────────────────────────────────
|
|
12
|
+
usage() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: clawforge cost [task-id] [flags]
|
|
15
|
+
|
|
16
|
+
Cost tracking for ClawForge agent runs.
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
clawforge cost <task-id> Show cost breakdown for a task
|
|
20
|
+
clawforge cost --summary All-time cost summary grouped by mode
|
|
21
|
+
clawforge cost --capture <id> Capture cost from running agent tmux pane
|
|
22
|
+
|
|
23
|
+
Flags:
|
|
24
|
+
--summary Show all-time cost summary
|
|
25
|
+
--capture <id> Capture cost from agent's tmux pane
|
|
26
|
+
--json Output as JSON
|
|
27
|
+
--help Show this help
|
|
28
|
+
EOF
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# ── Parse args ─────────────────────────────────────────────────────────
|
|
32
|
+
TASK_ID="" SUMMARY=false CAPTURE="" JSON_OUTPUT=false
|
|
33
|
+
POSITIONAL=()
|
|
34
|
+
|
|
35
|
+
while [[ $# -gt 0 ]]; do
|
|
36
|
+
case "$1" in
|
|
37
|
+
--summary) SUMMARY=true; shift ;;
|
|
38
|
+
--capture) CAPTURE="$2"; shift 2 ;;
|
|
39
|
+
--json) JSON_OUTPUT=true; shift ;;
|
|
40
|
+
--help|-h) usage; exit 0 ;;
|
|
41
|
+
--*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
|
42
|
+
*) POSITIONAL+=("$1"); shift ;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [[ ${#POSITIONAL[@]} -gt 0 ]]; then
|
|
47
|
+
TASK_ID="${POSITIONAL[0]}"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
mkdir -p "$(dirname "$COSTS_FILE")"
|
|
51
|
+
touch "$COSTS_FILE"
|
|
52
|
+
|
|
53
|
+
# ── Capture cost from tmux pane ────────────────────────────────────────
|
|
54
|
+
_capture_cost() {
|
|
55
|
+
local task_id="$1"
|
|
56
|
+
local resolved_id
|
|
57
|
+
resolved_id=$(resolve_task_id "$task_id")
|
|
58
|
+
|
|
59
|
+
local task_data
|
|
60
|
+
task_data=$(registry_get "$resolved_id")
|
|
61
|
+
if [[ -z "$task_data" ]]; then
|
|
62
|
+
log_error "Task '$task_id' not found"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
local tmux_session agent_type
|
|
67
|
+
tmux_session=$(echo "$task_data" | jq -r '.tmuxSession // ""')
|
|
68
|
+
agent_type=$(echo "$task_data" | jq -r '.agent // "claude"')
|
|
69
|
+
local model
|
|
70
|
+
model=$(echo "$task_data" | jq -r '.model // "unknown"')
|
|
71
|
+
|
|
72
|
+
# Scrape tmux pane for cost info
|
|
73
|
+
local pane_content=""
|
|
74
|
+
if [[ -n "$tmux_session" ]] && tmux has-session -t "$tmux_session" 2>/dev/null; then
|
|
75
|
+
pane_content=$(tmux capture-pane -t "$tmux_session" -p -S -100 2>/dev/null || true)
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Parse cost data from pane output
|
|
79
|
+
local input_tokens=0 output_tokens=0 cache_hits=0 total_cost="0.00"
|
|
80
|
+
|
|
81
|
+
if [[ -n "$pane_content" ]]; then
|
|
82
|
+
# Claude Code /cost output format: "Input tokens: X Output tokens: Y Total cost: $Z"
|
|
83
|
+
input_tokens=$(echo "$pane_content" | grep -ioE 'input[_ ]tokens?:?\s*[0-9,]+' | tail -1 | grep -oE '[0-9,]+' | tr -d ',' || echo "0")
|
|
84
|
+
output_tokens=$(echo "$pane_content" | grep -ioE 'output[_ ]tokens?:?\s*[0-9,]+' | tail -1 | grep -oE '[0-9,]+' | tr -d ',' || echo "0")
|
|
85
|
+
cache_hits=$(echo "$pane_content" | grep -ioE 'cache[_ ]hits?:?\s*[0-9,]+' | tail -1 | grep -oE '[0-9,]+' | tr -d ',' || echo "0")
|
|
86
|
+
total_cost=$(echo "$pane_content" | grep -ioE 'total[_ ]cost:?\s*\$?[0-9]+\.?[0-9]*' | tail -1 | grep -oE '[0-9]+\.?[0-9]*' || echo "0.00")
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
[[ -z "$input_tokens" ]] && input_tokens=0
|
|
90
|
+
[[ -z "$output_tokens" ]] && output_tokens=0
|
|
91
|
+
[[ -z "$cache_hits" ]] && cache_hits=0
|
|
92
|
+
[[ -z "$total_cost" ]] && total_cost="0.00"
|
|
93
|
+
|
|
94
|
+
local now
|
|
95
|
+
now=$(epoch_ms)
|
|
96
|
+
|
|
97
|
+
local cost_entry
|
|
98
|
+
cost_entry=$(jq -cn \
|
|
99
|
+
--arg taskId "$resolved_id" \
|
|
100
|
+
--arg agentId "${tmux_session}" \
|
|
101
|
+
--arg model "$model" \
|
|
102
|
+
--argjson inputTokens "${input_tokens:-0}" \
|
|
103
|
+
--argjson outputTokens "${output_tokens:-0}" \
|
|
104
|
+
--argjson cacheHits "${cache_hits:-0}" \
|
|
105
|
+
--arg totalCost "$total_cost" \
|
|
106
|
+
--argjson timestamp "$now" \
|
|
107
|
+
'{
|
|
108
|
+
taskId: $taskId,
|
|
109
|
+
agentId: $agentId,
|
|
110
|
+
model: $model,
|
|
111
|
+
inputTokens: $inputTokens,
|
|
112
|
+
outputTokens: $outputTokens,
|
|
113
|
+
cacheHits: $cacheHits,
|
|
114
|
+
totalCost: ($totalCost | tonumber),
|
|
115
|
+
timestamp: $timestamp
|
|
116
|
+
}')
|
|
117
|
+
|
|
118
|
+
echo "$cost_entry" >> "$COSTS_FILE"
|
|
119
|
+
log_info "Cost captured for $resolved_id: \$$total_cost"
|
|
120
|
+
|
|
121
|
+
if $JSON_OUTPUT; then
|
|
122
|
+
echo "$cost_entry"
|
|
123
|
+
else
|
|
124
|
+
echo "Cost captured for $resolved_id:"
|
|
125
|
+
echo " Input tokens: $input_tokens"
|
|
126
|
+
echo " Output tokens: $output_tokens"
|
|
127
|
+
echo " Cache hits: $cache_hits"
|
|
128
|
+
echo " Total cost: \$$total_cost"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ── Show cost for a task ───────────────────────────────────────────────
|
|
133
|
+
_show_task_cost() {
|
|
134
|
+
local task_id="$1"
|
|
135
|
+
local resolved_id
|
|
136
|
+
resolved_id=$(resolve_task_id "$task_id")
|
|
137
|
+
|
|
138
|
+
local entries
|
|
139
|
+
entries=$(grep "\"taskId\":\"${resolved_id}\"" "$COSTS_FILE" 2>/dev/null || true)
|
|
140
|
+
|
|
141
|
+
if [[ -z "$entries" ]]; then
|
|
142
|
+
# Check if it's a swarm parent — sum sub-agent costs
|
|
143
|
+
local sub_costs
|
|
144
|
+
sub_costs=$(grep "\"taskId\":\"${resolved_id}" "$COSTS_FILE" 2>/dev/null || true)
|
|
145
|
+
if [[ -z "$sub_costs" ]]; then
|
|
146
|
+
if $JSON_OUTPUT; then
|
|
147
|
+
echo '{"taskId":"'"$resolved_id"'","totalCost":0,"entries":[]}'
|
|
148
|
+
else
|
|
149
|
+
echo "No cost data for task: $resolved_id"
|
|
150
|
+
fi
|
|
151
|
+
return
|
|
152
|
+
fi
|
|
153
|
+
entries="$sub_costs"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if $JSON_OUTPUT; then
|
|
157
|
+
echo "$entries" | jq -s --arg tid "$resolved_id" '{
|
|
158
|
+
taskId: $tid,
|
|
159
|
+
entries: .,
|
|
160
|
+
totalCost: ([.[].totalCost] | add // 0),
|
|
161
|
+
totalInputTokens: ([.[].inputTokens] | add // 0),
|
|
162
|
+
totalOutputTokens: ([.[].outputTokens] | add // 0)
|
|
163
|
+
}'
|
|
164
|
+
else
|
|
165
|
+
local total_cost total_input total_output
|
|
166
|
+
total_cost=$(echo "$entries" | jq -s '[.[].totalCost] | add // 0' 2>/dev/null || echo "0")
|
|
167
|
+
total_input=$(echo "$entries" | jq -s '[.[].inputTokens] | add // 0' 2>/dev/null || echo "0")
|
|
168
|
+
total_output=$(echo "$entries" | jq -s '[.[].outputTokens] | add // 0' 2>/dev/null || echo "0")
|
|
169
|
+
|
|
170
|
+
echo "=== Cost Breakdown: $resolved_id ==="
|
|
171
|
+
echo ""
|
|
172
|
+
echo " Total cost: \$${total_cost}"
|
|
173
|
+
echo " Input tokens: ${total_input}"
|
|
174
|
+
echo " Output tokens: ${total_output}"
|
|
175
|
+
echo ""
|
|
176
|
+
|
|
177
|
+
local entry_count
|
|
178
|
+
entry_count=$(echo "$entries" | wc -l | tr -d ' ')
|
|
179
|
+
echo " Entries: ${entry_count}"
|
|
180
|
+
|
|
181
|
+
echo ""
|
|
182
|
+
echo " History:"
|
|
183
|
+
echo "$entries" | jq -r '" [\(.timestamp | . / 1000 | strftime("%H:%M:%S"))] \(.model) — $\(.totalCost) (\(.inputTokens) in / \(.outputTokens) out)"' 2>/dev/null || true
|
|
184
|
+
fi
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ── Summary ────────────────────────────────────────────────────────────
|
|
188
|
+
_show_summary() {
|
|
189
|
+
if [[ ! -s "$COSTS_FILE" ]]; then
|
|
190
|
+
if $JSON_OUTPUT; then
|
|
191
|
+
echo '{"totalCost":0,"entries":0,"byMode":{}}'
|
|
192
|
+
else
|
|
193
|
+
echo "No cost data recorded yet."
|
|
194
|
+
fi
|
|
195
|
+
return
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
local all_entries
|
|
199
|
+
all_entries=$(cat "$COSTS_FILE" | jq -s '.' 2>/dev/null)
|
|
200
|
+
|
|
201
|
+
if $JSON_OUTPUT; then
|
|
202
|
+
echo "$all_entries" | jq '{
|
|
203
|
+
totalCost: ([.[].totalCost] | add // 0),
|
|
204
|
+
totalInputTokens: ([.[].inputTokens] | add // 0),
|
|
205
|
+
totalOutputTokens: ([.[].outputTokens] | add // 0),
|
|
206
|
+
entries: length,
|
|
207
|
+
byModel: (group_by(.model) | map({key: .[0].model, value: {cost: ([.[].totalCost] | add // 0), entries: length}}) | from_entries)
|
|
208
|
+
}'
|
|
209
|
+
return
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
local total_cost total_input total_output entry_count
|
|
213
|
+
total_cost=$(echo "$all_entries" | jq '[.[].totalCost] | add // 0' 2>/dev/null || echo "0")
|
|
214
|
+
total_input=$(echo "$all_entries" | jq '[.[].inputTokens] | add // 0' 2>/dev/null || echo "0")
|
|
215
|
+
total_output=$(echo "$all_entries" | jq '[.[].outputTokens] | add // 0' 2>/dev/null || echo "0")
|
|
216
|
+
entry_count=$(echo "$all_entries" | jq 'length' 2>/dev/null || echo "0")
|
|
217
|
+
|
|
218
|
+
echo "=== Cost Summary ==="
|
|
219
|
+
echo ""
|
|
220
|
+
echo " Total cost: \$${total_cost}"
|
|
221
|
+
echo " Input tokens: ${total_input}"
|
|
222
|
+
echo " Output tokens: ${total_output}"
|
|
223
|
+
echo " Entries: ${entry_count}"
|
|
224
|
+
echo ""
|
|
225
|
+
|
|
226
|
+
# By model
|
|
227
|
+
echo " By Model:"
|
|
228
|
+
echo "$all_entries" | jq -r 'group_by(.model) | .[] | " \(.[0].model): $\([.[].totalCost] | add // 0) (\(length) runs)"' 2>/dev/null || true
|
|
229
|
+
echo ""
|
|
230
|
+
|
|
231
|
+
# By task (top 5 most expensive)
|
|
232
|
+
echo " Top 5 Most Expensive Tasks:"
|
|
233
|
+
echo "$all_entries" | jq -r 'group_by(.taskId) | map({taskId: .[0].taskId, cost: ([.[].totalCost] | add // 0)}) | sort_by(-.cost) | .[0:5] | .[] | " \(.taskId): $\(.cost)"' 2>/dev/null || true
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# ── Budget check (called from sprint/swarm) ───────────────────────────
|
|
237
|
+
# Usage: source cost.sh && check_budget <task-id> <budget>
|
|
238
|
+
check_budget() {
|
|
239
|
+
local task_id="$1" budget="$2"
|
|
240
|
+
if [[ ! -f "$COSTS_FILE" ]]; then
|
|
241
|
+
return 0 # No data, budget not exceeded
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
local spent
|
|
245
|
+
spent=$(grep "\"taskId\":\"${task_id}\"" "$COSTS_FILE" 2>/dev/null | jq -s '[.[].totalCost] | add // 0' 2>/dev/null || echo "0")
|
|
246
|
+
|
|
247
|
+
local exceeded
|
|
248
|
+
exceeded=$(python3 -c "print(1 if $spent >= $budget else 0)" 2>/dev/null || echo "0")
|
|
249
|
+
if [[ "$exceeded" == "1" ]]; then
|
|
250
|
+
return 1 # Budget exceeded
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# Warn at 80%
|
|
254
|
+
local warning
|
|
255
|
+
warning=$(python3 -c "print(1 if $spent >= $budget * 0.8 else 0)" 2>/dev/null || echo "0")
|
|
256
|
+
if [[ "$warning" == "1" ]]; then
|
|
257
|
+
log_warn "Budget warning: \$$spent / \$$budget (80% threshold)"
|
|
258
|
+
fi
|
|
259
|
+
|
|
260
|
+
return 0
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
# ── Route ──────────────────────────────────────────────────────────────
|
|
264
|
+
if [[ -n "$CAPTURE" ]]; then
|
|
265
|
+
_capture_cost "$CAPTURE"
|
|
266
|
+
elif $SUMMARY; then
|
|
267
|
+
_show_summary
|
|
268
|
+
elif [[ -n "$TASK_ID" ]]; then
|
|
269
|
+
_show_task_cost "$TASK_ID"
|
|
270
|
+
else
|
|
271
|
+
usage
|
|
272
|
+
exit 0
|
|
273
|
+
fi
|