Files
ai-setup/home-configs/claude/statusline-command.sh
Виталий Никитенко e6fe58eb37 feat(mimo): add ai-mimo launcher for Xiaomi MiMo V2.6 Pro
ai-mimo runs Claude Code through Xiaomi's Anthropic-compatible API
(Token Plan or pay-as-you-go). The key's endpoint is found by probing
token-plan-sgp, token-plan-ams and api.xiaomimimo.com (a key only works
in its own region) and is saved to mimo_base_url; MIMO_BASE_URL
overrides. Unreachable endpoints are skipped, token-plan-cn is left out
of the probe since it times out from our network, and a 402 on the
pay-as-you-go endpoint explains that a tp- Token Plan key is needed.

Always the smartest model: mimo-v2.6-pro in every slot and for
subagents, --model opus and --effort max on each start, 128000 output
tokens (Claude Code's ceiling), real 1M context window. Measured on
13.10: MiMo ignores output_config.effort (even "bogus" is accepted, no
length trend from low to max) and treats thinking adaptive like
enabled, so reasoning is always on; EFFORT_MAPPING.md records this.

/logout-mimo is registered as a command and handled by the logout hook
(removes mimo_key and mimo_base_url). setup.sh status, statusline brand
color, README, QUICK_START and AGENTS.md list the new launcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:43:25 +07:00

311 lines
13 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
# Copyright (c) 2026 Виталий Никитенко. MIT License.
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')
# Суффикс [1m] в отображении не нужен: окно контекста задаётся
# CLAUDE_CODE_MAX_CONTEXT_TOKENS, а не суффиксом модели
model="${model/\[1m\]/}"
model="${model% \[1M\]}"
model="${model% (1M context)}"
model_id="${model_id/\[1m\]/}"
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')
# Разделитель сегментов: жирная серая средняя точка ·
_sep() { printf " \033[1m\033[38;5;240m·\033[00m "; }
# Цвет effort: насыщенные, глубже чем пастельные (jewel tones)
_effort_color() {
case "$1" in
low) printf '\033[38;5;220mlow\033[00m' ;; # золотой
medium) printf '\033[38;5;50mmedium\033[00m' ;; # насыщенный teal
high) printf '\033[38;5;38mhigh\033[00m' ;; # глубокий голубой
xhigh) printf '\033[38;5;206mxhigh\033[00m' ;; # насыщенный фиолетовый
max) printf '\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 голубой
glm) printf '\033[38;5;223m' ;; # Z.ai кремовый (бренд-фон z.ai, #FFE4B5)
mimo) printf '\033[38;5;202m' ;; # Xiaomi оранжевый (#FF6900)
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" ] && { _sep; printf "\033[38;5;111m%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
ACCOUNTS_DIR="$HOME/.claude/accounts"
account=$(cat "$ACCOUNTS_DIR/current" 2>/dev/null)
current_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json" 2>/dev/null)
# current надёжен, только если его файл существует И токен совпадает с активной сессией.
account_token=""
if [ -n "$account" ] && [ -f "$ACCOUNTS_DIR/${account}.credentials.json" ]; then
account_token=$(jq -r '.claudeAiOauth.accessToken // empty' "$ACCOUNTS_DIR/${account}.credentials.json" 2>/dev/null)
fi
# Рассинхрон (пусто или чужой токен) — определяем аккаунт по активному токену.
if [ -z "$account" ] || [ "$account_token" != "$current_token" ]; then
account=""
if [ -n "$current_token" ]; then
# 1) Локальный матч по токену среди сохранённых аккаунтов (мгновенно)
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
# 2) Не нашли локально — резолвим email через account-email.sh (один раз на токен)
if [ -z "$account" ]; then
sentinel="$HOME/.cache/ai-setup/email_fetch_token"
if [ "$(cat "$sentinel" 2>/dev/null)" != "$current_token" ]; then
mkdir -p "$HOME/.cache/ai-setup"
echo "$current_token" > "$sentinel"
email=$(bash "$HOME/.claude/hooks/account-email.sh" "$HOME/.claude/.credentials.json" 2>/dev/null)
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" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$account"; }
fi
# Аккаунт/ключ Kimi — показываем alias (account1/account2/...).
if [ "${AI_LAUNCHER:-}" = "kimi" ]; then
kimi_account=$(cat "$HOME/.config/ai-setup/kimi_keys/current" 2>/dev/null)
[ -n "$kimi_account" ] && { _sep; printf "%s%s\033[00m" "$brand_color" "$kimi_account"; }
fi
if [ -n "$effort" ]; then
_sep
printf "%s%s " "$brand_color" "$model"
_effort_color "$effort"
else
_sep
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" ] && { _sep; 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', [])
parts = []
for info in infos:
if info.get('currency', '').upper() != 'USD':
continue
total = info.get('total_balance', '0')
parts.append(f'\${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" ] && { _sep; 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")
_sep; 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")
_sep; printf "%s%s:%s%%\033[00m" "$color" "$remaining" "$week_int"
fi
fi
# Z.ai (ai-glm, ai-eltex) отдаёт usage только в конце ответа, в начале потока
# input_tokens=0, поэтому пока идёт запрос, Claude Code присылает used_percentage=0.
# Держим последнее ненулевое значение этой сессии.
session_id=$(echo "$input" | jq -r '.session_id // empty')
if [ -n "$session_id" ]; then
ctx_cache="$HOME/.cache/ai-setup/ctx_last_${session_id}"
if [ -n "$ctx_pct" ] && [ "$(printf '%.0f' "$ctx_pct")" -gt 0 ]; then
if [ ! -f "$ctx_cache" ]; then
mkdir -p "$HOME/.cache/ai-setup"
# Раз за сессию чистим кэш старых сессий
find "$HOME/.cache/ai-setup" -name 'ctx_last_*' -mtime +7 -delete 2>/dev/null
fi
echo "$ctx_pct" > "$ctx_cache"
elif [ -f "$ctx_cache" ]; then
ctx_pct=$(cat "$ctx_cache")
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")
_sep; 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