#!/bin/bash input=$(cat) cwd=$(echo "$input" | jq -r '.cwd') model=$(echo "$input" | jq -r '.model.display_name // empty') model_id=$(echo "$input" | jq -r '.model.id // empty') five_pct=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty') five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty') week_pct=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty') week_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty') ctx_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty') # Цвет effort: насыщенные, глубже чем пастельные (jewel tones) _effort_color() { case "$1" in low) printf '\033[38;5;220m[low]\033[00m' ;; # золотой medium) printf '\033[38;5;50m[medium]\033[00m' ;; # насыщенный teal high) printf '\033[38;5;38m[high]\033[00m' ;; # глубокий голубой xhigh) printf '\033[38;5;206m[xhigh]\033[00m' ;; # насыщенный фиолетовый max) printf '\033[38;5;210m[\033[38;5;220mm\033[38;5;114ma\033[38;5;50mx\033[38;5;206m]\033[00m' ;; # радуга *) printf '\033[38;5;252m[%s]\033[00m' "$1" ;; esac } # Брендовый цвет имени модели по лаунчеру _brand_color() { case "${1:-}" in deepseek) printf '\033[38;5;69m' ;; # DeepSeek фирменный синий claude) printf '\033[38;5;173m' ;; # Anthropic оранжевый kimi) printf '\033[38;5;81m' ;; # Kimi голубой openrouter) printf '\033[38;5;135m' ;; # OpenRouter фиолетовый *) printf '\033[38;5;223m' ;; # кремовый (fallback) esac } branch=$(git -C "$cwd" --no-optional-locks symbolic-ref --short HEAD 2>/dev/null) short_cwd="${cwd/#$HOME/\~}" printf "\033[38;5;252m%s\033[00m" "$short_cwd" [ -n "$branch" ] && printf " \033[38;5;252m[%s]\033[00m" "$branch" if [ -n "$model" ]; then brand_color=$(_brand_color "${AI_LAUNCHER:-}") effort=$(echo "$input" | jq -r ".effort.level // empty") # Ловим выбранный уровень в кэш лаунчера (чтобы запомнить max между сессиями). # Когда CLAUDE_CODE_EFFORT_LEVEL выставлена (восстановленная max-сессия) - уровень # форсится env, кэш НЕ трогаем, чтобы дисплей-баг (.effort.level=xhigh) не затёр max. if [ -n "${AI_LAUNCHER:-}" ] && [ -z "${CLAUDE_CODE_EFFORT_LEVEL:-}" ] && [ -n "$effort" ]; then effort_file="$HOME/.cache/ai-setup/effort_${AI_LAUNCHER}" if [ "$effort" != "$(cat "$effort_file" 2>/dev/null)" ]; then mkdir -p "$HOME/.cache/ai-setup" echo "$effort" > "$effort_file" fi fi # Аккаунт Claude.ai актуален только для нативных моделей Claude if [[ "$model_id" == claude-* ]]; then account=$(cat ~/.claude/accounts/current 2>/dev/null) ACCOUNTS_DIR="$HOME/.claude/accounts" # Автоопределение: если current пуст или файл не существует — # ищем аккаунт по access-токену в .credentials.json if [ -z "$account" ] || [ ! -f "$ACCOUNTS_DIR/${account}.credentials.json" ]; then current_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null) if [ -n "$current_token" ]; then for f in "$ACCOUNTS_DIR"/*.credentials.json; do [ ! -f "$f" ] && continue token=$(jq -r '.claudeAiOauth.accessToken // empty' "$f" 2>/dev/null) if [ "$token" = "$current_token" ]; then account=$(basename "$f" .credentials.json) echo "$account" > "$ACCOUNTS_DIR/current" break fi done # Если токен не найден — спрашиваем haiku (один раз на аккаунт) if [ -z "$account" ]; then sentinel="$HOME/.cache/ai-setup/email_fetch_token" prev_token=$(cat "$sentinel" 2>/dev/null) if [ "$prev_token" != "$current_token" ]; then mkdir -p "$HOME/.cache/ai-setup" echo "$current_token" > "$sentinel" email=$(unset ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN; \ echo 'какой имейл этого аккаунта? напиши только имейл без других слов.' | \ claude --print --model claude-haiku-4-5 --dangerously-skip-permissions \ --output-format text --max-turns 1 --tools "" --effort low 2>/dev/null) email=$(echo "$email" | grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | head -1) if [ -n "$email" ]; then cp "$HOME/.claude/.credentials.json" "$ACCOUNTS_DIR/${email}.credentials.json" chmod 600 "$ACCOUNTS_DIR/${email}.credentials.json" account="$email" echo "$account" > "$ACCOUNTS_DIR/current" fi fi fi fi fi [ -n "$account" ] && printf " %s[%s]\033[00m" "$brand_color" "$account" fi if [ -n "$effort" ]; then printf " %s%s " "$brand_color" "$model" _effort_color "$effort" else printf " %s%s\033[00m" "$brand_color" "$model" fi fi # Форматирует оставшееся время до сброса лимита fmt_remaining() { local reset_ts="$1" local now now=$(date +%s) local diff=$(( reset_ts - now )) [ "$diff" -le 0 ] && echo "скоро" && return local d=$(( diff / 86400 )) local h=$(( (diff % 86400) / 3600 )) local m=$(( (diff % 3600) / 60 )) if [ "$d" -gt 0 ]; then echo "${d}д${h}ч" elif [ "$h" -gt 0 ]; then echo "${h}ч${m}м" else echo "${m}м" fi } # Возвращает ANSI-цвет по проценту: зелёный <40%, жёлтый 40-60%, красный >=60% pct_color() { local pct="$1" if [ "$pct" -lt 40 ]; then printf '\033[38;5;114m' # мягкий зелёный elif [ "$pct" -lt 60 ]; then printf '\033[38;5;220m' # золотой (как effort low) else printf '\033[38;5;210m' # мягкий красный fi } # --- Баланс DeepSeek --- # Моментально показываем кэшированный баланс, в фоне обновляем через API. if [[ "$model_id" == *deepseek* ]]; then cache_file="$HOME/.cache/ai-setup/deepseek_balance" if [ -f "$cache_file" ]; then balance=$(head -1 "$cache_file") [ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance" fi # Фоновое обновление баланса (не чаще раза в 30 секунд) refresh_ts="$HOME/.cache/ai-setup/deepseek_balance_refresh_ts" now=$(date +%s) last=$(cat "$refresh_ts" 2>/dev/null || echo 0) if [ $(( now - last )) -gt 30 ]; then key_file="$HOME/.config/ai-setup/deepseek_key" if [ -f "$key_file" ]; then echo "$now" > "$refresh_ts" 2>/dev/null ( api_key=$(cat "$key_file") resp=$(curl -s --max-time 10 "https://api.deepseek.com/user/balance" \ -H "Authorization: Bearer $api_key" \ -H "Accept: application/json" 2>/dev/null) if [ -n "$resp" ]; then new_balance=$(echo "$resp" | python3 -c " import sys, json d = json.load(sys.stdin) infos = d.get('balance_infos', []) symbols = {'USD': '\$', 'CNY': '\u00a5'} parts = [] for info in infos: curr = info.get('currency', '') total = info.get('total_balance', '0') sym = symbols.get(curr, curr) parts.append(f'{sym}{total}') if parts: print(' '.join(parts)) " 2>/dev/null) if [ -n "$new_balance" ]; then echo "$new_balance" > "$cache_file" fi fi ) & fi fi elif [ "${AI_LAUNCHER:-}" = "openrouter" ]; then # --- Баланс OpenRouter --- # Моментально показываем кэшированный остаток, в фоне обновляем через API. cache_file="$HOME/.cache/ai-setup/openrouter_balance" if [ -f "$cache_file" ]; then balance=$(head -1 "$cache_file") [ -n "$balance" ] && printf " \033[38;5;78m%s\033[00m" "$balance" fi # Фоновое обновление баланса (не чаще раза в 30 секунд) refresh_ts="$HOME/.cache/ai-setup/openrouter_balance_refresh_ts" now=$(date +%s) last=$(cat "$refresh_ts" 2>/dev/null || echo 0) if [ $(( now - last )) -gt 30 ]; then key_file="$HOME/.config/ai-setup/openrouter_key" if [ -f "$key_file" ]; then echo "$now" > "$refresh_ts" 2>/dev/null ( api_key=$(cat "$key_file") resp=$(curl -s --max-time 10 "https://openrouter.ai/api/v1/credits" \ -H "Authorization: Bearer $api_key" \ -H "Accept: application/json" 2>/dev/null) if [ -n "$resp" ]; then new_balance=$(echo "$resp" | python3 -c " import sys, json d = json.load(sys.stdin) data = d.get('data', {}) total = data.get('total_credits', 0) or 0 usage = data.get('total_usage', 0) or 0 remaining = total - usage print(f'${remaining:.2f}') " 2>/dev/null) if [ -n "$new_balance" ]; then echo "$new_balance" > "$cache_file" fi fi ) & fi fi else # Рейт-лимиты для НЕ-DeepSeek/OpenRouter провайдеров # Кеш специфичен для провайдера (model_id) И аккаунта (account): лимиты привязаны # к аккаунту, поэтому при переключении /switch-account проценты не должны смешиваться. _cache_key=$(echo "${model_id:-unknown}_${account:-}" | sed 's/[^a-zA-Z0-9._-]/_/g') RATE_CACHE="$HOME/.cache/ai-setup/rate_limits_${_cache_key}.cache" mkdir -p "$HOME/.cache/ai-setup" # Если есть свежие данные - сохранить в кеш if [ -n "$five_pct" ] || [ -n "$week_pct" ]; then { echo "FIVE_PCT=${five_pct}" echo "FIVE_RESET=${five_reset}" echo "WEEK_PCT=${week_pct}" echo "WEEK_RESET=${week_reset}" } > "$RATE_CACHE" fi # Если нет данных - читать из кеша (старт сессии до первого запроса) if [ -z "$five_pct" ] && [ -z "$week_pct" ] && [ -f "$RATE_CACHE" ]; then # shellcheck source=/dev/null source "$RATE_CACHE" 2>/dev/null five_pct="${FIVE_PCT:-}" five_reset="${FIVE_RESET:-}" week_pct="${WEEK_PCT:-}" week_reset="${WEEK_RESET:-}" fi if [ -n "$five_pct" ] && [ -n "$five_reset" ]; then five_int=$(printf '%.0f' "$five_pct") remaining=$(fmt_remaining "$five_reset") color=$(pct_color "$five_int") printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$five_int" fi if [ -n "$week_pct" ] && [ -n "$week_reset" ]; then week_int=$(printf '%.0f' "$week_pct") remaining=$(fmt_remaining "$week_reset") color=$(pct_color "$week_int") printf " %s%s:%s%%\033[00m" "$color" "$remaining" "$week_int" fi fi # ctx:0% при старте новой сессии (нет данных от API) [ -z "$ctx_pct" ] && ctx_pct="0" if [ -n "$ctx_pct" ]; then ctx_int=$(printf '%.0f' "$ctx_pct") color=$(pct_color "$ctx_int") printf " %sctx:%s%%\033[00m" "$color" "$ctx_int" # Звуковой сигнал при первом достижении 60% alert_file="$HOME/.cache/ai-setup/ctx_alert_state" if [ "$ctx_int" -ge 60 ]; then if [ ! -f "$alert_file" ] || [ "$(cat "$alert_file")" != "alerted" ]; then mkdir -p "$HOME/.cache/ai-setup" echo "alerted" > "$alert_file" (timeout 1s paplay /usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga 2>/dev/null; true) & fi elif [ "$ctx_int" -lt 50 ]; then rm -f "$alert_file" 2>/dev/null fi fi exit 0