Skip to content

Instantly share code, notes, and snippets.

@dan-palmer
Created January 9, 2026 02:21
Show Gist options
  • Select an option

  • Save dan-palmer/3eb1bb47da8144f8c96c2feeac205998 to your computer and use it in GitHub Desktop.

Select an option

Save dan-palmer/3eb1bb47da8144f8c96c2feeac205998 to your computer and use it in GitHub Desktop.
slop-usage: Track AI spending across Claude Code, Factory Droid, and OpenAI Codex
#!/bin/bash
# AI Slop Usage Tracker
# Aggregates usage from Claude Code, Factory Droid, and OpenAI Codex
#
# Usage: slop-usage [--daily|--monthly] [--current]
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
# Parse arguments
PERIOD="daily"
CURRENT_ONLY=false
TODAY_ONLY=false
for arg in "$@"; do
case "$arg" in
--daily) PERIOD="daily" ;;
--weekly) PERIOD="weekly" ;;
--monthly) PERIOD="monthly" ;;
--current) CURRENT_ONLY=true ;;
--today) TODAY_ONLY=true; PERIOD="daily" ;;
--help|-h)
echo "Usage: slop-usage [--daily|--weekly|--monthly] [--current|--today]"
echo ""
echo "Options:"
echo " --daily Show usage grouped by day (default)"
echo " --weekly Show usage grouped by week (Monday start)"
echo " --monthly Show usage grouped by month"
echo " --current Only show current period (this week/month)"
echo " --today Only show today's usage (from midnight)"
exit 0
;;
*)
echo "Unknown option: $arg"
echo "Usage: slop-usage [--daily|--weekly|--monthly] [--current|--today]"
exit 1
;;
esac
done
# Prompt for confirmation
confirm() {
local msg="$1"
echo -en "${YELLOW}$msg [y/N] ${NC}"
read -r response
[[ "$response" =~ ^[Yy]$ ]]
}
# Check dependencies
check_deps() {
local missing=()
local install_cmds=()
if ! command -v npx &>/dev/null; then
echo -e "${RED}Error: npx not found. Please install Node.js first:${NC}"
echo " brew install node # macOS"
echo " apt install nodejs # Ubuntu/Debian"
exit 1
fi
if ! command -v jq &>/dev/null; then
missing+=("jq")
if command -v brew &>/dev/null; then
install_cmds+=("brew install jq")
elif command -v apt-get &>/dev/null; then
install_cmds+=("sudo apt-get install -y jq")
fi
fi
if ! command -v bc &>/dev/null; then
missing+=("bc")
if command -v brew &>/dev/null; then
install_cmds+=("brew install bc")
elif command -v apt-get &>/dev/null; then
install_cmds+=("sudo apt-get install -y bc")
fi
fi
if [[ ${#missing[@]} -gt 0 ]]; then
echo -e "${YELLOW}Missing dependencies: ${missing[*]}${NC}"
if [[ ${#install_cmds[@]} -gt 0 ]]; then
if confirm "Install missing dependencies?"; then
for cmd in "${install_cmds[@]}"; do
echo -e "${BLUE}Running: $cmd${NC}"
eval "$cmd"
done
else
echo -e "${RED}Cannot continue without dependencies.${NC}"
exit 1
fi
else
echo -e "${RED}Please install manually: ${missing[*]}${NC}"
exit 1
fi
fi
}
# Setup Droid sessions symlinks if needed
setup_droid_sessions() {
local FACTORY_DIR="${HOME}/.factory"
local SESSIONS_DIR="${FACTORY_DIR}/sessions"
local DATA_DIR="${FACTORY_DIR}/sessions-data"
[[ -d "$FACTORY_DIR" ]] || return 0
[[ -d "$SESSIONS_DIR" ]] || return 0
# If sessions-data doesn't exist, check if migration is needed
if [[ ! -d "$DATA_DIR" ]]; then
if find "$SESSIONS_DIR" -mindepth 2 -name "*.jsonl" -print -quit 2>/dev/null | grep -q .; then
echo -e "${YELLOW}Setting up Droid sessions for better-ccusage compatibility...${NC}"
mv "$SESSIONS_DIR" "$DATA_DIR"
mkdir "$SESSIONS_DIR"
for f in "$DATA_DIR"/*/*.jsonl "$DATA_DIR"/*/*.settings.json; do
[[ -e "$f" ]] || continue
rel_path="${f#$DATA_DIR/}"
ln -s "../sessions-data/$rel_path" "$SESSIONS_DIR/$(basename "$f")" 2>/dev/null || true
done
for dir in "$DATA_DIR"/*/; do
[[ -d "$dir" ]] || continue
dirname=$(basename "$dir")
ln -s "../sessions-data/$dirname" "$SESSIONS_DIR/$dirname" 2>/dev/null || true
done
echo -e "${GREEN}Droid sessions migrated.${NC}"
fi
else
# Sync any new sessions that don't have symlinks yet
for f in "$DATA_DIR"/*/*.jsonl "$DATA_DIR"/*/*.settings.json; do
[[ -e "$f" ]] || continue
base=$(basename "$f")
if [[ ! -L "$SESSIONS_DIR/$base" ]]; then
rel_path="${f#$DATA_DIR/}"
ln -s "../sessions-data/$rel_path" "$SESSIONS_DIR/$base" 2>/dev/null || true
fi
done
fi
}
# Main
check_deps
setup_droid_sessions
# Get today's date in YYYY-MM-DD format
TODAY_DATE=$(date +%Y-%m-%d)
THIS_MONTH=$(date +%Y-%m)
# Get Sunday of current week (week start - tools use Sunday-based weeks)
day_of_week=$(date +%u) # 1=Monday, 7=Sunday
if [[ "$day_of_week" -eq 7 ]]; then
THIS_WEEK="$TODAY_DATE"
else
THIS_WEEK=$(date -v-${day_of_week}d +%Y-%m-%d 2>/dev/null || date -d "$day_of_week days ago" +%Y-%m-%d 2>/dev/null || echo "$TODAY_DATE")
fi
if $TODAY_ONLY; then
echo -e "\n${BOLD}AI Slop Usage (today - ${TODAY_DATE})${NC}\n"
elif $CURRENT_ONLY; then
if [[ "$PERIOD" == "weekly" ]]; then
echo -e "\n${BOLD}AI Slop Usage (this week - ${THIS_WEEK})${NC}\n"
else
echo -e "\n${BOLD}AI Slop Usage (this month - ${THIS_MONTH})${NC}\n"
fi
else
echo -e "\n${BOLD}AI Slop Usage (${PERIOD})${NC}\n"
fi
echo "Fetching usage data..."
# Temp files for parallel execution
tmp_claude=$(mktemp)
tmp_droid=$(mktemp)
tmp_codex=$(mktemp)
trap "rm -f $tmp_claude $tmp_droid $tmp_codex" EXIT
# Get system timezone
TZ_NAME=$(readlink /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||' || echo "UTC")
# Check which tools have data available
HAS_CLAUDE=false
HAS_DROID=false
HAS_CODEX=false
[[ -d "${HOME}/.claude" ]] && HAS_CLAUDE=true
[[ -d "${HOME}/.factory/sessions" ]] && HAS_DROID=true
[[ -d "${HOME}/.codex" ]] && HAS_CODEX=true
# Run available tools in parallel with timezone
if $HAS_CLAUDE; then
npx --yes ccusage@latest "$PERIOD" --json --timezone "$TZ_NAME" 2>/dev/null > "$tmp_claude" &
pid1=$!
else
echo '{}' > "$tmp_claude"
pid1=""
fi
if $HAS_DROID; then
npx --yes better-ccusage@latest "$PERIOD" --json --timezone "$TZ_NAME" 2>/dev/null > "$tmp_droid" &
pid2=$!
else
echo '{}' > "$tmp_droid"
pid2=""
fi
if $HAS_CODEX && [[ "$PERIOD" != "weekly" ]]; then
# Codex doesn't support weekly
npx --yes @ccusage/codex "$PERIOD" --json --timezone "$TZ_NAME" 2>/dev/null > "$tmp_codex" &
pid3=$!
else
echo '{}' > "$tmp_codex"
pid3=""
fi
# Wait for running processes
[[ -n "$pid1" ]] && wait $pid1
[[ -n "$pid2" ]] && wait $pid2
[[ -n "$pid3" ]] && wait $pid3
# Show which sources are available
sources=()
$HAS_CLAUDE && sources+=("Claude Code")
$HAS_DROID && sources+=("Factory Droid")
$HAS_CODEX && [[ "$PERIOD" != "weekly" ]] && sources+=("OpenAI Codex")
if [[ ${#sources[@]} -eq 0 ]]; then
echo -e "${RED}No usage data found. Install Claude Code, Factory Droid, or OpenAI Codex first.${NC}"
exit 1
fi
echo -e "${BLUE}Sources: ${sources[*]}${NC}"
# Get date field name based on period
case "$PERIOD" in
monthly) DATE_FIELD="month" ;;
weekly) DATE_FIELD="week" ;;
*) DATE_FIELD="date" ;;
esac
# Build combined data with jq
# Normalize codex dates (e.g., "Jan 2026" -> "2026-01") and merge all sources
if $TODAY_ONLY || $CURRENT_ONLY; then
# For --today: filter to today's date specifically
# For --current: get last entry (this month)
if $TODAY_ONLY; then
filter_date="$TODAY_DATE"
# Get entry matching today's date, or null
claude_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.date == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_claude" 2>/dev/null || echo 0)
droid_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.date == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_droid" 2>/dev/null || echo 0)
# Codex uses different date format, need to check both
codex_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.date == \"$filter_date\" or .date == \"$(date +'%b %d, %Y')\")] | .[0].costUSD // 0" "$tmp_codex" 2>/dev/null || echo 0)
period_label="$TODAY_DATE"
elif [[ "$PERIOD" == "weekly" ]]; then
filter_date="$THIS_WEEK"
# Get entry for this week
claude_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.week == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_claude" 2>/dev/null || echo 0)
droid_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.week == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_droid" 2>/dev/null || echo 0)
codex_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.week == \"$filter_date\")] | .[0].costUSD // 0" "$tmp_codex" 2>/dev/null || echo 0)
period_label="$THIS_WEEK"
else
filter_date="$THIS_MONTH"
# Get last entry for this month
claude_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.month == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_claude" 2>/dev/null || echo 0)
droid_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.month == \"$filter_date\")] | .[0].totalCost // 0" "$tmp_droid" 2>/dev/null || echo 0)
# Codex uses different month format like "Jan 2026"
codex_month=$(date +'%b %Y')
codex_cost=$(jq -r "[.[\"$PERIOD\"] // [] | .[] | select(.month == \"$filter_date\" or .month == \"$codex_month\")] | .[0].costUSD // 0" "$tmp_codex" 2>/dev/null || echo 0)
period_label="$THIS_MONTH"
fi
total=$(echo "$claude_cost + $droid_cost + $codex_cost" | bc -l 2>/dev/null || echo "0")
echo ""
printf "${BOLD}%-14s %12s %14s %13s %12s${NC}\n" "Period" "Claude Code" "Factory Droid" "OpenAI Codex" "Total"
printf "%-14s %12s %14s %13s %12s\n" "--------------" "------------" "--------------" "-------------" "------------"
printf "%-14s %11.2f %13.2f %12.2f %11.2f\n" "$period_label" "$claude_cost" "$droid_cost" "$codex_cost" "$total"
echo ""
else
# Normalize dates to YYYY-MM-DD format for merging
# Claude/Droid use: 2025-12-08 (daily) or 2025-12 (monthly)
# Codex uses: Dec 08, 2025 (daily) or Jan 2026 (monthly)
normalize_jq='
def normalize_date:
if test("^[0-9]{4}-[0-9]{2}-[0-9]{2}$") then .
elif test("^[0-9]{4}-[0-9]{2}$") then .
elif test("^[A-Z][a-z]{2} [0-9]{2}, [0-9]{4}$") then
capture("^(?<m>[A-Z][a-z]{2}) (?<d>[0-9]{2}), (?<y>[0-9]{4})$") |
{"Jan":"01","Feb":"02","Mar":"03","Apr":"04","May":"05","Jun":"06",
"Jul":"07","Aug":"08","Sep":"09","Oct":"10","Nov":"11","Dec":"12"}[.m] as $mm |
"\(.y)-\($mm)-\(.d)"
elif test("^[A-Z][a-z]{2} [0-9]{4}$") then
capture("^(?<m>[A-Z][a-z]{2}) (?<y>[0-9]{4})$") |
{"Jan":"01","Feb":"02","Mar":"03","Apr":"04","May":"05","Jun":"06",
"Jul":"07","Aug":"08","Sep":"09","Oct":"10","Nov":"11","Dec":"12"}[.m] as $mm |
"\(.y)-\($mm)"
else .
end;
[.[] | {key: (.'$DATE_FIELD' | normalize_date), value: .COST_FIELD}] | from_entries
'
claude_json=$(jq -c "${normalize_jq/COST_FIELD/totalCost}" <<< "$(jq ".[\"$PERIOD\"] // []" "$tmp_claude")" 2>/dev/null || echo '{}')
droid_json=$(jq -c "${normalize_jq/COST_FIELD/totalCost}" <<< "$(jq ".[\"$PERIOD\"] // []" "$tmp_droid")" 2>/dev/null || echo '{}')
codex_json=$(jq -c "${normalize_jq/COST_FIELD/costUSD}" <<< "$(jq ".[\"$PERIOD\"] // []" "$tmp_codex")" 2>/dev/null || echo '{}')
# Get all unique normalized dates, sorted
all_dates=$(echo "$claude_json $droid_json $codex_json" | jq -rs '[.[] | keys] | flatten | unique | sort[]' 2>/dev/null)
echo ""
printf "${BOLD}%-14s %12s %14s %13s %12s${NC}\n" "Period" "Claude Code" "Factory Droid" "OpenAI Codex" "Total"
printf "%-14s %12s %14s %13s %12s\n" "--------------" "------------" "--------------" "-------------" "------------"
grand_claude=0
grand_droid=0
grand_codex=0
while IFS= read -r date; do
[[ -z "$date" ]] && continue
claude_cost=$(echo "$claude_json" | jq -r ".[\"$date\"] // 0")
droid_cost=$(echo "$droid_json" | jq -r ".[\"$date\"] // 0")
codex_cost=$(echo "$codex_json" | jq -r ".[\"$date\"] // 0")
row_total=$(echo "$claude_cost + $droid_cost + $codex_cost" | bc -l 2>/dev/null || echo "0")
grand_claude=$(echo "$grand_claude + $claude_cost" | bc -l)
grand_droid=$(echo "$grand_droid + $droid_cost" | bc -l)
grand_codex=$(echo "$grand_codex + $codex_cost" | bc -l)
printf "%-14s %11.2f %13.2f %12.2f %11.2f\n" "$date" "$claude_cost" "$droid_cost" "$codex_cost" "$row_total"
done <<< "$all_dates"
grand_total=$(echo "$grand_claude + $grand_droid + $grand_codex" | bc -l)
printf "%-14s %12s %14s %13s %12s\n" "--------------" "------------" "--------------" "-------------" "------------"
printf "${BOLD}%-14s %11.2f %13.2f %12.2f %11.2f${NC}\n" "TOTAL" "$grand_claude" "$grand_droid" "$grand_codex" "$grand_total"
echo ""
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment