Files
ai-setup/home-configs/claude/statusline-command.sh
vitaly 1cb4853dca refactor: нативный persistence effort, выпил мёртвого кэша effort_*
EFFORT_MAPPING, statusline и _apply_effort переведены на нативное
хранение уровня в settings.json лаунчера. Убран CLAUDE_CODE_EFFORT_LEVEL
(он блокировал /effort внутри сессии) и кэш ~/.cache/ai-setup/effort_*,
который никто не читал и который врал относительно реального уровня.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 05:21:12 +03:00

282 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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")
# Аккаунт 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
# Аккаунт/ключ Kimi — показываем email/имя из .meta, если оно есть, иначе alias.
if [ "${AI_LAUNCHER:-}" = "kimi" ]; then
kimi_account=$(cat "$HOME/.config/ai-setup/kimi_keys/current" 2>/dev/null)
if [ -n "$kimi_account" ]; then
kimi_display=$(cat "$HOME/.config/ai-setup/kimi_keys/${kimi_account}.meta" 2>/dev/null || echo "$kimi_account")
printf " %s[%s]\033[00m" "$brand_color" "$kimi_display"
fi
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