mirror of
https://github.com/FBukovina/burn-baby-burn.git
synced 2026-10-27 15:14:32 +00:00
first commit
This commit is contained in:
commit
7478aef719
5 changed files with 516 additions and 0 deletions
275
bin/burn
Executable file
275
bin/burn
Executable file
|
|
@ -0,0 +1,275 @@
|
|||
#!/usr/bin/env bash
|
||||
# burn — a token-burning harness around Claude Code.
|
||||
# Usage: burn TOKENS [--model MODEL]
|
||||
set -euo pipefail
|
||||
|
||||
FILLER_TOKENS=1000
|
||||
SYSTEM_OVERHEAD_TOKENS=9000 # measured: claude -p system prompt + tool defs
|
||||
TOKENS_PER_CALL=$(( SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS ))
|
||||
MIN_TOKENS=$TOKENS_PER_CALL # asking for less than one call can't be honored
|
||||
FILLER_LINE="Burn baby burn, this is a disco inferno, burning tokens is for great engineers. "
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
burn — spin up Claude Code in a loop to burn approximately N tokens.
|
||||
|
||||
Usage:
|
||||
burn TOKENS [--model MODEL]
|
||||
|
||||
Arguments:
|
||||
TOKENS Target tokens to burn (minimum ${MIN_TOKENS}).
|
||||
|
||||
Options:
|
||||
--model NAME Pass through to 'claude --model' (e.g. haiku, sonnet, opus).
|
||||
-h, --help Show this help and exit.
|
||||
EOF
|
||||
}
|
||||
|
||||
model=""
|
||||
tokens=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--model)
|
||||
if [[ $# -lt 2 ]]; then echo "error: --model requires a value" >&2; exit 2; fi
|
||||
model="$2"; shift 2 ;;
|
||||
--model=*) model="${1#*=}"; shift ;;
|
||||
--) shift; break ;;
|
||||
-*) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;;
|
||||
*)
|
||||
if [[ -n "$tokens" ]]; then
|
||||
echo "error: unexpected extra argument: $1" >&2
|
||||
exit 2
|
||||
fi
|
||||
tokens="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$tokens" ]]; then
|
||||
usage >&2; exit 2
|
||||
fi
|
||||
if ! [[ "$tokens" =~ ^[0-9]+$ ]]; then
|
||||
echo "error: TOKENS must be a positive integer (got '$tokens')" >&2; exit 2
|
||||
fi
|
||||
if (( tokens < MIN_TOKENS )); then
|
||||
echo "error: tokens must be at least ${MIN_TOKENS} (got ${tokens})" >&2; exit 2
|
||||
fi
|
||||
|
||||
if ! command -v claude >/dev/null 2>&1; then
|
||||
echo "error: 'claude' CLI not found on PATH." >&2
|
||||
echo " install Claude Code first: https://docs.claude.com/claude-code" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "error: 'jq' not found on PATH (used to parse Claude Code's JSON output)." >&2
|
||||
echo " install with: brew install jq" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ~4 chars/token is the standard rule of thumb for English text.
|
||||
target_chars=$(( FILLER_TOKENS * 4 ))
|
||||
filler_len=${#FILLER_LINE}
|
||||
repeats=$(( target_chars / filler_len ))
|
||||
(( repeats < 1 )) && repeats=1
|
||||
filler=""
|
||||
for ((i=0; i<repeats; i++)); do
|
||||
filler+="$FILLER_LINE"
|
||||
done
|
||||
prompt=$(printf "Reply with the single word 'burned' and nothing else. Ignore the filler text below; it exists only to consume tokens.\n\n%s" "$filler")
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
ORANGE=$'\033[1;33m'
|
||||
RED=$'\033[0;31m'
|
||||
DIM=$'\033[2m'
|
||||
RESET=$'\033[0m'
|
||||
HIDE_CURSOR=$'\033[?25l'
|
||||
SHOW_CURSOR=$'\033[?25h'
|
||||
else
|
||||
ORANGE=''
|
||||
RED=''
|
||||
DIM=''
|
||||
RESET=''
|
||||
HIDE_CURSOR=''
|
||||
SHOW_CURSOR=''
|
||||
fi
|
||||
|
||||
PAD=' '
|
||||
|
||||
# Three flame frames cycled to make the fire dance. Each row is exactly
|
||||
# 23 characters wide so it sits cleanly inside the fireplace opening.
|
||||
flame_frame() {
|
||||
case "$1_$2" in
|
||||
0_0) printf '%s' ' ) ) ) ' ;;
|
||||
0_1) printf '%s' ' )\ )\ )\ ' ;;
|
||||
0_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||
0_3) printf '%s' ' ) ) ) ) ) ) ) ) ) ' ;;
|
||||
0_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||
1_0) printf '%s' ' ( ( ( ' ;;
|
||||
1_1) printf '%s' ' /( /( /( ' ;;
|
||||
1_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||
1_3) printf '%s' ' ( ) ( ) ( ) ( ) ( ' ;;
|
||||
1_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||
2_0) printf '%s' ' ) ( ) ' ;;
|
||||
2_1) printf '%s' ' (( )( (( ' ;;
|
||||
2_2) printf '%s' ' ( ( ( ( ( ( ( ( ( ' ;;
|
||||
2_3) printf '%s' ' ) ( ) ( ) ( ) ( ) ' ;;
|
||||
2_4) printf '%s' ' \_/ \_/ \_/ ' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
flame_color_for_row() {
|
||||
case "$1" in
|
||||
0|1) printf '%s' "$ORANGE" ;;
|
||||
*) printf '%s' "$RED" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
print_flame_row() {
|
||||
local frame=$1 row=$2 content color
|
||||
content=$(flame_frame "$frame" "$row")
|
||||
color=$(flame_color_for_row "$row")
|
||||
printf '%s%s│░░░│%s%s%s%s%s│░░░│%s\n' \
|
||||
"$PAD" "$DIM" "$RESET" "$color" "$content" "$RESET" "$DIM" "$RESET"
|
||||
}
|
||||
|
||||
draw_fireplace() {
|
||||
local frame=${1:-0}
|
||||
printf '%s%s┌───────────────────────────────┐%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░┌───────────────────────┐░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
for row in 0 1 2 3 4; do
|
||||
print_flame_row "$frame" "$row"
|
||||
done
|
||||
printf '%s%s│░░░│ │░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░│%s %s▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓%s %s│░░░│%s\n' \
|
||||
"$PAD" "$DIM" "$RESET" "$RED" "$RESET" "$DIM" "$RESET"
|
||||
printf '%s%s│░░░└───────────────────────┘░░░│%s\n' "$PAD" "$DIM" "$RESET"
|
||||
printf '%s%s└───────────────────────────────┘%s\n' "$PAD" "$DIM" "$RESET"
|
||||
}
|
||||
|
||||
# Distance (in lines) from the progress-bar cursor up to the top flame row:
|
||||
# blank line above bar + outer-bottom + inner-bottom + log + blank-below-flames
|
||||
# + 5 flame rows = 10.
|
||||
FLAME_OFFSET=10
|
||||
|
||||
redraw_flames() {
|
||||
local frame=$1
|
||||
printf '\033[s' # save cursor
|
||||
printf '\033[%dA\r' "$FLAME_OFFSET" # move up to top flame row
|
||||
for row in 0 1 2 3 4; do
|
||||
print_flame_row "$frame" "$row"
|
||||
done
|
||||
printf '\033[u' # restore cursor
|
||||
}
|
||||
|
||||
burned=0
|
||||
iteration=0
|
||||
total_cost=0.000000
|
||||
BAR_WIDTH=30
|
||||
claude_pid=""
|
||||
last_output=""
|
||||
|
||||
claude_cmd=(claude -p --output-format json --no-session-persistence --tools "")
|
||||
if [[ -n "$model" ]]; then
|
||||
claude_cmd+=(--model "$model")
|
||||
fi
|
||||
# `--tools` is variadic, so terminate flag parsing before the prompt.
|
||||
claude_cmd+=(--)
|
||||
|
||||
draw_bar() {
|
||||
local pct filled empty bar="" i suffix="${1:-}"
|
||||
pct=$(( burned * 100 / tokens ))
|
||||
(( pct > 100 )) && pct=100
|
||||
filled=$(( pct * BAR_WIDTH / 100 ))
|
||||
empty=$(( BAR_WIDTH - filled ))
|
||||
for ((i=0; i<filled; i++)); do bar+='█'; done
|
||||
for ((i=0; i<empty; i++)); do bar+='░'; done
|
||||
# \033[K clears any leftover characters from a longer previous draw.
|
||||
printf "\r[%s] %3d%% • %d/%d tokens • \$%s • %d call(s)%s\033[K" \
|
||||
"$bar" "$pct" "$burned" "$tokens" "$total_cost" "$iteration" "$suffix"
|
||||
}
|
||||
|
||||
restore_cursor() {
|
||||
printf '%s' "$SHOW_CURSOR"
|
||||
}
|
||||
trap restore_cursor EXIT
|
||||
|
||||
on_interrupt() {
|
||||
if [[ -n "$claude_pid" ]]; then
|
||||
kill "$claude_pid" 2>/dev/null || true
|
||||
wait "$claude_pid" 2>/dev/null || true
|
||||
fi
|
||||
echo
|
||||
printf "stopped. burned %s tokens across %s call(s), spent \$%s.\n" \
|
||||
"$burned" "$iteration" "$total_cost"
|
||||
exit 130
|
||||
}
|
||||
trap on_interrupt INT
|
||||
|
||||
# Run one claude call in the background while animating flames. On success
|
||||
# leaves the parsed stdout in $last_output. Returns the claude exit status.
|
||||
run_claude() {
|
||||
local output_file
|
||||
output_file=$(mktemp)
|
||||
"${claude_cmd[@]}" "$prompt" > "$output_file" 2>/dev/null &
|
||||
claude_pid=$!
|
||||
|
||||
local frame=0
|
||||
while kill -0 "$claude_pid" 2>/dev/null; do
|
||||
redraw_flames $(( frame % 3 ))
|
||||
frame=$(( frame + 1 ))
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
if wait "$claude_pid" 2>/dev/null; then
|
||||
last_output=$(<"$output_file")
|
||||
rm -f "$output_file"
|
||||
claude_pid=""
|
||||
return 0
|
||||
else
|
||||
rm -f "$output_file"
|
||||
claude_pid=""
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
printf '%s' "$HIDE_CURSOR"
|
||||
echo
|
||||
draw_fireplace 0
|
||||
echo
|
||||
draw_bar
|
||||
|
||||
while (( burned < tokens )); do
|
||||
iteration=$(( iteration + 1 ))
|
||||
|
||||
if ! run_claude; then
|
||||
draw_bar " (retrying)"
|
||||
if ! run_claude; then
|
||||
printf "\nerror: two consecutive claude calls failed; aborting.\n" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
read -r this_call this_cost < <(jq -r '
|
||||
[ ((.usage.input_tokens // 0) +
|
||||
(.usage.cache_creation_input_tokens // 0) +
|
||||
(.usage.cache_read_input_tokens // 0) +
|
||||
(.usage.output_tokens // 0)),
|
||||
(.total_cost_usd // 0)
|
||||
] | @tsv' <<<"$last_output")
|
||||
|
||||
if ! [[ "$this_call" =~ ^[0-9]+$ ]]; then
|
||||
printf "\nerror: could not parse token usage from claude output\n" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
burned=$(( burned + this_call ))
|
||||
total_cost=$(awk -v a="$total_cost" -v b="$this_cost" 'BEGIN{printf "%.6f", a+b}')
|
||||
draw_bar
|
||||
done
|
||||
|
||||
echo
|
||||
printf "done. burned %s tokens across %s call(s) (target was %s), spent \$%s.\n" \
|
||||
"$burned" "$iteration" "$tokens" "$total_cost"
|
||||
Loading…
Add table
Add a link
Reference in a new issue