Раньше все ai-* лаунчеры делили один ~/.claude и общий settings.json, из-за чего кастомная модель (openai/gpt-5.5) из ai-openrouter протекала в пикер ai-claude. Теперь каждый сторонний провайдер изолирован в своём CLAUDE_CONFIG_DIR (~/.config/ai-setup/cfg/<launcher>) - свои settings.json и .claude.json, ноль протечек. ai-claude остаётся на ~/.claude (нативный логин). Пикеры /model приведены к требуемому виду: - ai-deepseek: только DeepSeek V4 Pro (opus) и DeepSeek V4 Flash (haiku), дефолт Pro; через availableModels + ANTHROPIC_DEFAULT_*_MODEL_NAME - ai-kimi: только Kimi K2.6 (opus) - ai-claude: только нативные модели Claude Общие skills и CLAUDE.md шарятся симлинком из ~/.claude. Persistence effort - гибрид: - low/medium/high/xhigh живут нативно в settings.json лаунчера, /effort внутри сессии работает свободно и уровень сохраняется - max нельзя сохранить в settings.json (session-only), поэтому он восстанавливается через CLAUDE_CODE_EFFORT_LEVEL; в такой max-сессии /effort залочен (ограничение Claude Code), выход - AI_EFFORT=<lvl> ai-* Текущий уровень ловит статусбар в ~/.cache/ai-setup/effort_<launcher>. Удалён устаревший effort-save-hook (заменён нативным persistence + гибридом), почищен из ~/.claude/settings.json и осиротевший кэш model_*. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
243 lines
10 KiB
Bash
Executable File
243 lines
10 KiB
Bash
Executable File
#!/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
|
||
else
|
||
# Рейт-лимиты для НЕ-DeepSeek провайдеров
|
||
# Кеш специфичен для провайдера (по model_id) чтобы не смешивать claude/kimi/openrouter
|
||
_cache_key=$(echo "${model_id:-unknown}" | 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
|