#!/usr/bin/env bash # burn — a token-burning harness around Claude Code or Codex CLI. # Usage: burn TOKENS [--model MODEL] [--backend claude|codex] set -euo pipefail # Resolve this script's directory so we can find sibling files (codex_pricing.py). SCRIPT_PATH="${BASH_SOURCE[0]}" while [[ -L "$SCRIPT_PATH" ]]; do SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" SCRIPT_PATH="$(readlink "$SCRIPT_PATH")" [[ "$SCRIPT_PATH" != /* ]] && SCRIPT_PATH="$SCRIPT_DIR/$SCRIPT_PATH" done SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" && pwd)" FILLER_TOKENS=1000 # Backend-specific system overhead, measured empirically per CLI. CLAUDE_SYSTEM_OVERHEAD_TOKENS=9000 # claude -p with --tools "" CODEX_SYSTEM_OVERHEAD_TOKENS=12000 # codex exec --sandbox read-only FILLER_LINE="Burn baby burn, this is a disco inferno, burning tokens is for great engineers. " usage() { cat <&2 exit 2 fi model="$2" shift 2 ;; --model=*) model="${1#*=}" shift ;; --backend) if [[ $# -lt 2 ]]; then echo "error: --backend requires a value" >&2 exit 2 fi backend="$2" shift 2 ;; --backend=*) backend="${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 # Resolve backend: explicit flag > auto-detect (claude preferred, codex fallback). if [[ -z "$backend" ]]; then if command -v claude >/dev/null 2>&1; then backend=claude elif command -v codex >/dev/null 2>&1; then backend=codex else echo "error: neither 'claude' nor 'codex' found on PATH." >&2 echo " install Claude Code: https://docs.claude.com/claude-code" >&2 echo " or Codex CLI: https://github.com/openai/codex" >&2 exit 1 fi fi case "$backend" in claude) SYSTEM_OVERHEAD_TOKENS=$CLAUDE_SYSTEM_OVERHEAD_TOKENS ;; codex) SYSTEM_OVERHEAD_TOKENS=$CODEX_SYSTEM_OVERHEAD_TOKENS ;; *) echo "error: unknown --backend '$backend' (must be 'claude' or 'codex')" >&2 exit 2 ;; esac if ! command -v "$backend" >/dev/null 2>&1; then echo "error: '$backend' CLI not found on PATH." >&2 case "$backend" in claude) echo " install Claude Code: https://docs.claude.com/claude-code" >&2 ;; codex) echo " install Codex CLI: https://github.com/openai/codex" >&2 ;; esac exit 1 fi if ! command -v jq >/dev/null 2>&1; then echo "error: 'jq' not found on PATH (used to parse CLI JSON output)." >&2 echo " install with: brew install jq" >&2 exit 1 fi TOKENS_PER_CALL=$((SYSTEM_OVERHEAD_TOKENS + FILLER_TOKENS)) MIN_TOKENS=$TOKENS_PER_CALL if ((tokens < MIN_TOKENS)); then echo "error: tokens must be at least ${MIN_TOKENS} for backend '${backend}' (got ${tokens})" >&2 exit 2 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' MAGENTA=$'\033[1;35m' CYAN=$'\033[1;36m' RED=$'\033[1;31m' YELLOW=$'\033[1;93m' DIM=$'\033[2m' RESET=$'\033[0m' HIDE_CURSOR=$'\033[?25l' SHOW_CURSOR=$'\033[?25h' else ORANGE='' MAGENTA='' CYAN='' RED='' YELLOW='' DIM='' RESET='' HIDE_CURSOR='' SHOW_CURSOR='' fi PAD=' ' # Four disco frames cycled to make the dancer sway across the floor while # the ball glitters on the off-beat. Each row is exactly 23 characters wide # so it sits cleanly inside the booth opening. disco_frame() { case "$1_$2" in # frame 0 — centered Travolta: left hand on hip, right arm pointing up-right 0_0) printf '%s' ' | ' ;; 0_1) printf '%s' ' 🪩 ' ;; 0_2) printf '%s' ' /|\ ' ;; 0_3) printf '%s' ' / ' ;; 0_4) printf '%s' ' o/ ' ;; 0_5) printf '%s' ' <| ' ;; 0_6) printf '%s' ' | ' ;; 0_7) printf '%s' ' / \ ' ;; 0_8) printf '%s' ' ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ ' ;; # frame 1 — lean left, same Travolta pose; sparkles on the chain 1_0) printf '%s' ' * | * ' ;; 1_1) printf '%s' ' 🪩 ' ;; 1_2) printf '%s' ' \|/ ' ;; 1_3) printf '%s' ' / ' ;; 1_4) printf '%s' ' o/ ' ;; 1_5) printf '%s' ' <| ' ;; 1_6) printf '%s' ' | ' ;; 1_7) printf '%s' ' / \ ' ;; 1_8) printf '%s' ' ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ ' ;; # frame 2 — centered mirror: right hand on hip, left arm pointing up-left 2_0) printf '%s' ' | ' ;; 2_1) printf '%s' ' 🪩 ' ;; 2_2) printf '%s' ' /|\ ' ;; 2_3) printf '%s' ' \ ' ;; 2_4) printf '%s' ' \o ' ;; 2_5) printf '%s' ' |> ' ;; 2_6) printf '%s' ' | ' ;; 2_7) printf '%s' ' / \ ' ;; 2_8) printf '%s' ' ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ ' ;; # frame 3 — lean right, mirror Travolta; sparkles on the chain 3_0) printf '%s' ' * | * ' ;; 3_1) printf '%s' ' 🪩 ' ;; 3_2) printf '%s' ' \|/ ' ;; 3_3) printf '%s' ' \ ' ;; 3_4) printf '%s' ' \o ' ;; 3_5) printf '%s' ' |> ' ;; 3_6) printf '%s' ' | ' ;; 3_7) printf '%s' ' / \ ' ;; 3_8) printf '%s' ' ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ā–“ā–’ ' ;; esac } disco_color_for_row() { case "$1" in 0 | 2) printf '%s' "$ORANGE" ;; # chain sparkles + light rays 1 | 8) printf '%s' "$MAGENTA" ;; # disco ball + dance floor *) printf '%s' "$CYAN" ;; # dancer (and blank rows) esac } # Render `count` copies of `char` colored across a cycling fire palette # (red → orange → yellow). `offset` rotates the cycle so the strip flickers. fire_strip() { local count=$1 char=$2 offset=${3:-0} local palette=("$RED" "$ORANGE" "$YELLOW") local n=${#palette[@]} local i out="" for ((i = 0; i < count; i++)); do out+="${palette[$(((i + offset) % n))]}${char}" done printf '%s' "$out" } print_disco_row() { local frame=$1 row=$2 content color fl content=$(disco_frame "$frame" "$row") color=$(disco_color_for_row "$row") fl=$(fire_strip 3 'ā–‘' "$frame") printf '%s%s│%s%s│%s%s%s%s%s│%s%s│%s\n' \ "$PAD" "$RED" "$fl" "$YELLOW" "$RESET" \ "$color" "$content" "$RESET" \ "$YELLOW" "$fl" "$RED" "$RESET" } draw_disco() { local frame=${1:-0} local flames fr3 flames=$(fire_strip 31 'ā–²') fr3=$(fire_strip 3 'ā–‘') printf '%s%sā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”%s\n' "$PAD" "$RED" "$RESET" printf '%s%s│%s%s│%s\n' "$PAD" "$RED" "$flames" "$RED" "$RESET" printf '%s%s│%s%sā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”%s%s│%s\n' \ "$PAD" "$RED" "$fr3" "$YELLOW" "$fr3" "$RED" "$RESET" for row in 0 1 2 3 4 5 6 7 8; do print_disco_row "$frame" "$row" done printf '%s%s│%s%sā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜%s%s│%s\n' \ "$PAD" "$RED" "$fr3" "$YELLOW" "$fr3" "$RED" "$RESET" printf '%s%sā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜%s\n' "$PAD" "$RED" "$RESET" } # Distance (in lines) from the progress-bar cursor up to the top inner row: # blank line above bar + outer-bottom + inner-bottom + 9 inner rows = 12. DISCO_OFFSET=12 redraw_disco() { local frame=$1 printf '\033[s' # save cursor printf '\033[%dA\r' "$DISCO_OFFSET" # move up to top inner row for row in 0 1 2 3 4 5 6 7 8; do print_disco_row "$frame" "$row" done printf '\033[u' # restore cursor } burned=0 iteration=0 total_cost=0.000000 BAR_WIDTH=30 cli_pid="" last_output="" # Build CLI command for the chosen backend. case "$backend" in claude) cli_cmd=(claude -p --output-format json --no-session-persistence --tools "") [[ -n "$model" ]] && cli_cmd+=(--model "$model") # --tools is variadic, so terminate flag parsing before the prompt. cli_cmd+=(--) ;; codex) cli_cmd=(codex exec --json --skip-git-repo-check --sandbox read-only) [[ -n "$model" ]] && cli_cmd+=(--model "$model") ;; esac # Resolve whether we can report dollar cost for this run: # - claude reports it directly in JSON. # - codex doesn't, so we estimate from token counts using codex_rates.sh. # If the rates file is missing or the chosen model isn't in the table, # we silently fall back to tokens-only display. cost_reported=0 cost_model="" if [[ "$backend" == claude ]]; then cost_reported=1 elif [[ "$backend" == codex ]]; then if [[ -f "$SCRIPT_DIR/codex_rates.sh" ]]; then # shellcheck source=codex_rates.sh source "$SCRIPT_DIR/codex_rates.sh" # Assume gpt-5.5 unless the user passed --model. cost_model="${model:-gpt-5.5}" if codex_has_rates "$cost_model"; then cost_reported=1 fi fi fi format_cost_field() { if ((cost_reported)); then printf '$%s' "$total_cost" else printf 'cost n/a' fi } 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" "$(format_cost_field)" "$iteration" "$suffix" } restore_cursor() { printf '%s' "$SHOW_CURSOR" } trap restore_cursor EXIT CELEBRATIONS=( "šŸ”§ You are an true AI-native engineer!" "šŸŖ„ You are an AI wizard!" "šŸ•ŗ Tokens burned. Groove restored." "✨ You are a master of vibe-coding." "šŸš€ You shipped faster than the flame could spread." "šŸ’ƒ The code is hot and the vibes are immaculate." "🪩 Groovy job. I've never seen someone so productive!" "šŸ† Another glorious victory for human-machine collaboration." "⚔ You turned tokens into pure developer energy." "šŸŽ© Behold: a masterclass in responsible token combustion." "šŸ› ļø Great work. The token furnace is pleased." ) # Round a dollar amount to the nearest cent. round_cents() { awk -v c="$1" 'BEGIN{printf "%.2f", c}' } # Format a non-negative integer with thousands-grouping commas (21826 -> 21,826). fmt_num() { local n=$1 result="" while ((n >= 1000)); do result=$(printf ",%03d%s" $((n % 1000)) "$result") n=$((n / 1000)) done printf "%d%s" "$n" "$result" } on_interrupt() { if [[ -n "$cli_pid" ]]; then kill "$cli_pid" 2>/dev/null || true wait "$cli_pid" 2>/dev/null || true fi local burned_fmt cost_msg burned_fmt=$(fmt_num "$burned") if ((cost_reported)); then cost_msg=$(printf "šŸ’° spent \$%s" "$(round_cents "$total_cost")") else cost_msg="šŸ’° cost not reported by codex" fi printf "\n\nšŸ›‘ stopped early.\n\n" printf " šŸ”„ torched %s tokens across %s call(s) • %s\n\n" "$burned_fmt" "$iteration" "$cost_msg" exit 130 } trap on_interrupt INT # Run one CLI call in the background while animating flames. On success # leaves the parsed stdout in $last_output. Returns the CLI exit status. run_cli() { local output_file output_file=$(mktemp) # "$output_file" 2>/dev/null /dev/null; do redraw_disco $((frame % 4)) frame=$((frame + 1)) sleep 0.4 done if wait "$cli_pid" 2>/dev/null; then last_output=$(<"$output_file") rm -f "$output_file" cli_pid="" return 0 else rm -f "$output_file" cli_pid="" return 1 fi } # Parse one CLI call's output into " " (tab-separated). parse_usage() { case "$backend" in claude) 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" ;; codex) # codex emits JSONL; sum usage across any turn.completed events. # input_tokens already includes cached_input_tokens, so don't double-count # for the token total. Cost is computed locally from per-model rates. local inp cached out cost read -r inp cached out < <(jq -s -r ' [ .[] | select(.type? == "turn.completed") | .usage ] | [ ([.[] | .input_tokens // 0] | add // 0), ([.[] | .cached_input_tokens // 0] | add // 0), ([.[] | .output_tokens // 0] | add // 0) ] | @tsv' <<<"$last_output") if ((cost_reported)); then cost=$(codex_cost "$cost_model" "$inp" "$cached" "$out") else cost=0 fi printf '%d\t%s\n' "$((inp + out))" "$cost" ;; esac } printf '%s' "$HIDE_CURSOR" echo draw_disco 0 echo draw_bar while ((burned < tokens)); do iteration=$((iteration + 1)) if ! run_cli; then draw_bar " (retrying)" if ! run_cli; then printf "\nerror: two consecutive %s calls failed; aborting.\n" "$backend" >&2 exit 1 fi fi read -r this_call this_cost < <(parse_usage) if ! [[ "$this_call" =~ ^[0-9]+$ ]]; then printf "\nerror: could not parse token usage from %s output\n" "$backend" >&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 msg="${CELEBRATIONS[RANDOM % ${#CELEBRATIONS[@]}]}" burned_fmt=$(fmt_num "$burned") tokens_fmt=$(fmt_num "$tokens") if ((cost_reported)); then cost_rounded=$(round_cents "$total_cost") printf "\n\n šŸ”„ torched %s tokens across %s call(s) (target: %s) • šŸ’° spent \$%s šŸ’µšŸ’µšŸ’µ\n\n" \ "$burned_fmt" "$iteration" "$tokens_fmt" "$cost_rounded" else printf "\n\n šŸ”„ torched %s tokens across %s call(s) (target: %s) • šŸ’° cost not reported by codex\n\n" \ "$burned_fmt" "$iteration" "$tokens_fmt" fi printf " %s\n\n" "$msg"