@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/VERSION +1 -0
  4. package/bin/attach.sh +98 -0
  5. package/bin/check-agents.sh +343 -0
  6. package/bin/clawforge +257 -0
  7. package/bin/clawforge-dashboard +0 -0
  8. package/bin/clean.sh +257 -0
  9. package/bin/config.sh +111 -0
  10. package/bin/conflicts.sh +224 -0
  11. package/bin/cost.sh +273 -0
  12. package/bin/dashboard.sh +557 -0
  13. package/bin/diff.sh +109 -0
  14. package/bin/doctor.sh +196 -0
  15. package/bin/eval.sh +217 -0
  16. package/bin/history.sh +91 -0
  17. package/bin/init.sh +182 -0
  18. package/bin/learn.sh +230 -0
  19. package/bin/logs.sh +126 -0
  20. package/bin/memory.sh +207 -0
  21. package/bin/merge-helper.sh +174 -0
  22. package/bin/multi-review.sh +215 -0
  23. package/bin/notify.sh +93 -0
  24. package/bin/on-complete.sh +149 -0
  25. package/bin/parse-cost.sh +205 -0
  26. package/bin/pr.sh +167 -0
  27. package/bin/resume.sh +183 -0
  28. package/bin/review-mode.sh +163 -0
  29. package/bin/review-pr.sh +145 -0
  30. package/bin/routing.sh +88 -0
  31. package/bin/scope-task.sh +169 -0
  32. package/bin/spawn-agent.sh +190 -0
  33. package/bin/sprint.sh +320 -0
  34. package/bin/steer.sh +107 -0
  35. package/bin/stop.sh +136 -0
  36. package/bin/summary.sh +182 -0
  37. package/bin/swarm.sh +525 -0
  38. package/bin/templates.sh +244 -0
  39. package/lib/common.sh +302 -0
  40. package/lib/templates/bugfix.json +6 -0
  41. package/lib/templates/migration.json +7 -0
  42. package/lib/templates/refactor.json +6 -0
  43. package/lib/templates/security-audit.json +5 -0
  44. package/lib/templates/test-coverage.json +6 -0
  45. package/package.json +31 -0
  46. package/registry/conflicts.jsonl +0 -0
  47. package/registry/costs.jsonl +0 -0
  48. package/tui/PRD.md +106 -0
  49. package/tui/agent.go +266 -0
  50. package/tui/animation.go +192 -0
  51. package/tui/dashboard.go +219 -0
  52. package/tui/filter.go +68 -0
  53. package/tui/go.mod +25 -0
  54. package/tui/go.sum +46 -0
  55. package/tui/keybindings.go +229 -0
  56. package/tui/main.go +61 -0
  57. package/tui/model.go +166 -0
  58. package/tui/steer.go +69 -0
  59. 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