一个 Claude Code 自定义状态栏脚本,显示模型名称、Git 分支、Context 使用进度条,以及会话累计 token 统计。
V1不带计费版本
效果预览
LongCat-2.0[1m] | main | ██░░░░░░░░ 21% | [输出:128K 新增输入:630K 缓存:35M 命中率:98%]
LongCat-2.0[1m] | main | ████████░░ 85% | [输出:50K 新增输入:800K 缓存:720M 命中率:99%]
-
进度条 ≤50%:灰色,51-80%:绿色,>80%:红色预警
-
无
used_percentage时(部分 API 提供商不返回)显示--%,仍可显示 token 统计
安装
1. 复制脚本
cp statusline-command.sh ~/.claude/statusline-command.sh
chmod +x ~/.claude/statusline-command.sh
2. 配置 settings.json
编辑 ~/.claude/settings.json,添加:
{
"statusLine": {
"type": "command",
"command": "bash ~/.claude/statusline-command.sh"
}
}
3. 依赖
-
jq(推荐,性能更好):用于解析 JSON
-
Windows:
scoop install jq或从 jqlang.github.io 下载放到 PATH 中 -
macOS:
brew install jq -
Linux:
apt install jq/pacman -S jq
-
-
git(可选):显示当前分支名
-
无 jq 时自动 fallback 到 grep/awk 解析(功能相同,transcript 解析稍慢)
4. 重启 Claude Code
重启会话后生效。
显示内容
| 区域 | 内容 | 示例 |
|---|---|---|
| 模型 | 当前模型名称 | LongCat-2.0[1m] |
| 分支 | Git 当前分支 | main / no-git |
| 进度条 | 10 格 Context 使用率 | ██░░░░░░░░ 21% |
| 输出 | 会话累计输出 token | 输出:128K |
| 新增输入 | 会话累计非缓存输入 token | 新增输入:630K |
| 缓存 | 会话累计缓存读取 token | 缓存:35M |
| 命中率 | 缓存读取占总上下文的比例 | 命中率:98% |
Token 说明
-
新增输入 (
input_tokens):每轮 API 调用中,未命中缓存、需要新处理的 token -
缓存 (
cache_read_input_tokens):每轮 API 调用中,从缓存读取的 token -
命中率 = 缓存 / (新增输入 + 缓存) × 100%
注意:缓存是每轮 API 调用的累计值。由于上下文每轮都在增长,同一段历史内容会在后续每轮都被"读取",因此缓存累计值会远大于新增输入,这是正常的。
工作原理
-
Claude Code 每次事件(消息、工具调用等)触发时,通过 stdin 传入 JSON
-
脚本解析 JSON 提取模型、目录、context 等信息
-
从
transcript_path指向的 JSONL 文件增量解析 token 用量(只读新增行) -
缓存文件
~/.claude/statusline-cache/statusline-cache-{session_id}.json记录已解析行数,避免重复解析 -
7 天未更新的缓存文件自动清理
自定义
编辑 statusline-command.sh 顶部的配置:
CACHE_DIR="${HOME}/.claude/statusline-cache" # 缓存目录
CACHE_TTL_DAYS=7 # 缓存保留天数
颜色阈值(约第 190 行):
if [ "$pct_int" -gt 80 ]; then # >80% 红色
elif [ "$pct_int" -gt 50 ]; then # 51-80% 绿色
else # ≤50% 灰色
fi
bash
#!/bin/bash
# Claude Code status line script
# Shows: model name, git branch, context bar, session token breakdown
input=$(cat)
# --- Config ---
CACHE_DIR="${HOME}/.claude/statusline-cache"
CACHE_TTL_DAYS=7
# --- jq resolver ---
# Windows paths (C:\Users) break jq because \U \v are invalid escapes.
# We must test jq against the REAL input, not a simple test string.
JQ=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
if printf '%s' "$input" | "$candidate" -r '.model.id' >/dev/null 2>&1; then
JQ="$candidate"
break
fi
fi
done
# --- Extract fields from stdin JSON ---
if [ -n "$JQ" ]; then
model=$(printf '%s' "$input" | "$JQ" -r '.model.display_name // .model.id // "unknown"')
cwd=$(printf '%s' "$input" | "$JQ" -r '.workspace.current_dir // empty')
used_pct=$(printf '%s' "$input" | "$JQ" -r '.context_window.used_percentage // empty')
total_size=$(printf '%s' "$input" | "$JQ" -r '.context_window.context_window_size // empty')
total_input=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_input_tokens // empty')
total_output=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_output_tokens // empty')
cache_read=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_read_input_tokens // empty')
cache_create=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_creation_input_tokens // empty')
session_id=$(printf '%s' "$input" | "$JQ" -r '.session_id // empty')
transcript_path=$(printf '%s' "$input" | "$JQ" -r '.transcript_path // empty')
else
_extract() { printf '%s' "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'; }
model=$(_extract display_name)
[ -z "$model" ] && model=$(_extract id)
[ -z "$model" ] && model="unknown"
cwd=$(grep -o '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
used_pct=$(grep -o '"used_percentage"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$input" | head -1 | grep -o '[0-9.]*$')
total_size=$(grep -o '"context_window_size"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
total_input=$(grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
total_output=$(grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
cache_read=$(grep -o '"cache_read_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
cache_create=$(grep -o '"cache_creation_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
session_id=$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
transcript_path=$(grep -o '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
fi
# --- Session token accumulation (incremental, via cache file) ---
mkdir -p "$CACHE_DIR"
session_input=0
session_output=0
session_cache=0
session_context=0 # input + cache_read (total context per API call)
if [ -n "$session_id" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
CACHE_FILE="${CACHE_DIR}/statusline-cache-${session_id}.json"
# Read previous cache
cached_lines=0
cached_input=0
cached_output=0
cached_cache=0
cached_context=0
if [ -f "$CACHE_FILE" ]; then
_cache_json=$(cat "$CACHE_FILE" 2>/dev/null)
if [ -n "$JQ" ]; then
cached_lines=$(printf '%s' "$_cache_json" | "$JQ" -r '.lines // 0')
cached_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.input // 0')
cached_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.output // 0')
cached_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.cache // 0')
cached_context=$(printf '%s' "$_cache_json" | "$JQ" -r '.context // 0')
else
cached_lines=$(grep -o '"lines"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_input=$(grep -o '"input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_output=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_cache=$(grep -o '"cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_context=$(grep -o '"context"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
fi
fi
# Count current lines in transcript
current_lines=$(wc -l < "$transcript_path" 2>/dev/null || echo 0)
# Only parse new lines
if [ "$current_lines" -gt "$cached_lines" ] 2>/dev/null; then
start_line=$(( cached_lines + 1 ))
# Extract new lines to a temp file for batch processing
_new_lines=$(sed -n "${start_line},\$p" "$transcript_path" 2>/dev/null)
if [ -n "$JQ" ]; then
# Fast path: jq single-pass aggregation over all new lines at once
_agg=$(printf '%s\n' "$_new_lines" | "$JQ" -s '
[.[] | select(.message.usage != null) | .message.usage] |
{ input: ([.[].input_tokens // 0] | add),
output: ([.[].output_tokens // 0] | add),
cache: ([.[].cache_read_input_tokens // 0] | add)
}
' 2>/dev/null)
new_input=$(printf '%s' "$_agg" | "$JQ" -r '.input // 0')
new_output=$(printf '%s' "$_agg" | "$JQ" -r '.output // 0')
new_cache=$(printf '%s' "$_agg" | "$JQ" -r '.cache // 0')
else
# Slow fallback: awk single-pass (no jq available)
_agg=$(printf '%s\n' "$_new_lines" | awk '
/"input_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); i+=a[1] }
/"output_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"output_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); o+=a[1] }
/"cache_read_input_tokens"/ { match($0, /"cache_read_input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); c+=a[1] }
END { printf "%d %d %d", i, o, c }
')
new_input=$(echo "$_agg" | awk '{print $1}')
new_output=$(echo "$_agg" | awk '{print $2}')
new_cache=$(echo "$_agg" | awk '{print $3}')
fi
new_context=$(( new_input + new_cache ))
session_input=$(( cached_input + new_input ))
session_output=$(( cached_output + new_output ))
session_cache=$(( cached_cache + new_cache ))
session_context=$(( cached_context + new_context ))
# Update cache (always write, regardless of jq or grep)
printf '{"lines":%d,"input":%d,"output":%d,"cache":%d,"context":%d}' \
"$current_lines" "$session_input" "$session_output" "$session_cache" "$session_context" > "$CACHE_FILE"
else
session_input=$cached_input
session_output=$cached_output
session_cache=$cached_cache
session_context=$cached_context
fi
# Cleanup old cache files (older than CACHE_TTL_DAYS)
find "$CACHE_DIR" -name "statusline-cache-*.json" -mtime +${CACHE_TTL_DAYS} -delete 2>/dev/null
fi
# --- Extract git branch ---
branch=""
if [ -n "$cwd" ]; then
branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
fi
[ -z "$branch" ] && branch="no-git"
# --- Format token counts ---
_fmt_tokens() {
local t=$1
if [ -z "$t" ] || [ "$t" -eq 0 ] 2>/dev/null; then echo ""; return; fi
if [ "$t" -ge 1000000 ]; then
printf '%dM' $(( t / 1000000 ))
elif [ "$t" -ge 1000 ]; then
printf '%dK' $(( t / 1000 ))
else
printf '%d' "$t"
fi
}
# --- Session cumulative values ---
sess_in_str=$(_fmt_tokens "$session_input")
sess_out_str=$(_fmt_tokens "$session_output")
sess_cache_str=$(_fmt_tokens "$session_cache")
# Cache hit rate = cache_read / (input + cache_read) per session
sess_hit_rate=""
if [ "$session_context" -gt 0 ] 2>/dev/null; then
sess_hit_rate=$(( session_cache * 100 / session_context ))
fi
# Build detail: session cumulative
detail=""
[ -n "$sess_out_str" ] && detail="输出:${sess_out_str}"
[ -n "$sess_in_str" ] && detail="${detail:+$detail }新增输入:${sess_in_str}"
[ -n "$sess_cache_str" ] && detail="${detail:+$detail }缓存:${sess_cache_str}"
[ -n "$sess_hit_rate" ] && detail="${detail:+$detail }命中率:${sess_hit_rate}%"
# --- Build the 10-cell progress bar ---
bar=""
if [ -n "$used_pct" ] && [ "$used_pct" != "null" ]; then
pct_int=$(printf "%.0f" "$used_pct")
filled=$(( pct_int / 10 ))
[ $filled -gt 10 ] && filled=10
empty=$(( 10 - filled ))
blocks=$(printf '█%.0s' $(seq 1 $filled); printf '░%.0s' $(seq 1 $empty))
info="${pct_int}%"
if [ "$pct_int" -gt 80 ] 2>/dev/null; then
bar=$(printf '\033[31m%s\033[0m %s' "$blocks" "$info")
elif [ "$pct_int" -gt 50 ] 2>/dev/null; then
bar=$(printf '\033[32m%s\033[0m %s' "$blocks" "$info")
else
bar=$(printf '\033[90m%s\033[0m %s' "$blocks" "$info")
fi
else
bar="--%"
fi
# Append session detail
[ -n "$detail" ] && bar="${bar} | [${detail}]"
# --- Output ---
printf '%b\n' "${model} | ${branch} | ${bar}"
V1带计费版本
效果预览
no-git | ██░░░░░░░░ 23% | [输出:204.79K 新增输入:1.68M 缓存:114.90M 命中率:98%] | LongCat-2.0[1m]:¥9.13
-
进度条颜色:≤50% 灰色 | 51-80% 绿色 | >80% 红色预警
-
费用根据 API 调用时间自动分段(高峰/非高峰)计价
-
无价格配置时提示
(未配置价格)
价格配置
价格文件存放在 ~/.claude/price/,每个模型一个 JSON 文件。
配置格式
{
"name": "显示名",
"currency": "¥",
"unit": "per_million_tokens",
"peak": {
"hours": [[9,12],[14,18]],
"cache_hit": 0.025,
"cache_miss": 3.0,
"output": 6.0
},
"offpeak": {
"hours": [[0,9],[12,14],[18,24]],
"cache_hit": 0.02,
"cache_miss": 1.0,
"output": 2.0
}
}
| 字段 | 说明 |
|---|---|
| peak.hours | 高峰时段(北京时间),支持多段 |
| peak.cache_hit | 高峰缓存命中价格(元/百万 token) |
| peak.cache_miss | 高峰缓存未命中价格 |
| peak.output | 高峰输出价格 |
| offpeak.* | 空闲时段价格 |
添加新模型
cat > ~/.claude/price/新模型.json << 'EOF'
{
"name": "新模型",
"currency": "¥",
"peak": {"hours": [[0,24]], "cache_hit": 0.1, "cache_miss": 2.0, "output": 5.0},
"offpeak": {"hours": [[0,24]], "cache_hit": 0.1, "cache_miss": 2.0, "output": 5.0}
}
EOF
自动识别
脚本从 Claude Code 的 model.id 自动匹配价格文件:
-
精确匹配:模型名 == 价格文件名(忽略大小写和连字符)
-
模糊匹配:模型名包含文件名,或反之(取最长匹配)
-
兜底 :使用
~/.claude/price/active文件指定的模型
示例:mimo-v2.5-pro → 自动匹配 v2.5pro.json
费用计算原理
Token 分类
-
新增输入 (
input_tokens):未命中缓存、需要新处理的 token -
缓存 (
cache_read_input_tokens):从缓存读取的 token -
命中率 = 缓存 / (新增输入 + 缓存) × 100%
高峰/非高峰分段
每条 API 调用记录包含 UTC 时间戳,脚本:
-
将 UTC 转换为北京时间(UTC+8)
-
根据价格配置判断该调用属于高峰/非高峰
-
分别累加各时段的 token 用量
计算公式
费用 = 高峰(缓存×cache_hit + 输入×cache_miss + 输出×output) / 1,000,000
+ 空闲(缓存×cache_hit + 输入×cache_miss + 输出×output) / 1,000,000
增量缓存
-
首次解析:读取整个 transcript,按时间分段累加
-
后续刷新:只处理新增行,合并到缓存
-
缓存文件:
~/.claude/statusline-cache/statusline-cache-{session_id}.json -
7 天未更新的缓存自动清理
常见问题
Q: 会话开始显示 N/A? A: 需要发第一条消息后才有数据。
Q: 为什么缓存数值很大? A: 缓存是每轮 API 调用的累计值。上下文每轮增长,同一段历史在后续每轮都会被"读取"一次。
Q: 费用和实际账单不一致? A: 脚本使用配置的价格估算,实际价格以服务商为准。确保价格配置与实际一致。
Q: 支持多模型切换吗? A: 支持。脚本按每条 API 调用的实际时间分段计价,但所有 token 使用当前模型的价格文件。
bash
#!/bin/bash
# Claude Code status line script
# Shows: model name, git branch, context bar, session token breakdown
input=$(cat)
# --- Config ---
CACHE_DIR="${HOME}/.claude/statusline-cache"
CACHE_TTL_DAYS=7
# --- jq resolver ---
# Windows paths (C:\Users) break jq because \U \v are invalid escapes.
# We must test jq against the REAL input, not a simple test string.
JQ=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
if printf '%s' "$input" | "$candidate" -r '.model.id' >/dev/null 2>&1; then
JQ="$candidate"
break
fi
fi
done
# --- Extract fields from stdin JSON ---
if [ -n "$JQ" ]; then
model=$(printf '%s' "$input" | "$JQ" -r '.model.display_name // .model.id // "unknown"')
cwd=$(printf '%s' "$input" | "$JQ" -r '.workspace.current_dir // empty')
used_pct=$(printf '%s' "$input" | "$JQ" -r '.context_window.used_percentage // empty')
total_size=$(printf '%s' "$input" | "$JQ" -r '.context_window.context_window_size // empty')
total_input=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_input_tokens // empty')
total_output=$(printf '%s' "$input" | "$JQ" -r '.context_window.total_output_tokens // empty')
cache_read=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_read_input_tokens // empty')
cache_create=$(printf '%s' "$input" | "$JQ" -r '.context_window.current_usage.cache_creation_input_tokens // empty')
session_id=$(printf '%s' "$input" | "$JQ" -r '.session_id // empty')
transcript_path=$(printf '%s' "$input" | "$JQ" -r '.transcript_path // empty')
else
_extract() { printf '%s' "$input" | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'; }
model=$(_extract display_name)
[ -z "$model" ] && model=$(_extract id)
[ -z "$model" ] && model="unknown"
cwd=$(grep -o '"current_dir"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
used_pct=$(grep -o '"used_percentage"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$input" | head -1 | grep -o '[0-9.]*$')
total_size=$(grep -o '"context_window_size"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
total_input=$(grep -o '"total_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
total_output=$(grep -o '"total_output_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
cache_read=$(grep -o '"cache_read_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
cache_create=$(grep -o '"cache_creation_input_tokens"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$input" | head -1 | grep -o '[0-9]*$')
session_id=$(grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
transcript_path=$(grep -o '"transcript_path"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$input" | head -1 | sed 's/.*"transcript_path"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
fi
# --- Detect price file and extract peak hours (before transcript parsing) ---
PRICE_DIR_EARLY="$(dirname "$0")/price"
peak_hours_str="0,24" # default: all peak
if [ -d "$PRICE_DIR_EARLY" ]; then
# Find jq for price parsing
PJQ_EARLY=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
if echo '{"a":1}' | "$candidate" -r '.a' >/dev/null 2>&1; then
PJQ_EARLY="$candidate"
break
fi
fi
done
# Auto-detect price file (same logic as cost section)
_norm() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]' | tr -d ' '; }
_early_price=""
price_matched=""
model_norm=$(_norm "$model")
for pf in "$PRICE_DIR_EARLY"/*.json; do
[ -f "$pf" ] || continue
pf_name=$(basename "$pf" .json)
if [ "$model_norm" = "$(_norm "$pf_name")" ]; then _early_price="$pf"; price_matched=1; break; fi
done
if [ -z "$_early_price" ]; then
best_len=0
for pf in "$PRICE_DIR_EARLY"/*.json; do
[ -f "$pf" ] || continue
pf_name=$(basename "$pf" .json)
pf_norm=$(_norm "$pf_name")
if [[ "$model_norm" == *"$pf_norm"* ]] || [[ "$pf_norm" == *"$model_norm"* ]]; then
if [ ${#pf_name} -gt $best_len ]; then best_len=${#pf_name}; _early_price="$pf"; price_matched=1; fi
fi
done
fi
if [ -z "$_early_price" ]; then
_af="${PRICE_DIR_EARLY}/active"
if [ -f "$_af" ]; then
_am=$(cat "$_af" 2>/dev/null | tr -d '\r\n\t ')
[ -f "${PRICE_DIR_EARLY}/${_am}.json" ] && _early_price="${PRICE_DIR_EARLY}/${_am}.json"
fi
fi
# Extract peak hours from price config: [[9,12],[14,18]] → "9,12,14,18"
if [ -n "$_early_price" ] && [ -f "$_early_price" ]; then
if [ -n "$PJQ_EARLY" ]; then
_ph=$("$PJQ_EARLY" -r '.peak.hours // [[0,24]]' "$_early_price" 2>/dev/null)
peak_hours_str=$(printf '%s' "$_ph" | tr -d '[]\r\n' | tr -s ',' ' ' | sed 's/^ //;s/ $//;s/ /,/g')
else
_compact=$(tr -d '\n\t\r ' < "$_early_price")
_ph_section=$(grep -o '"peak"[^}]*' <<<"$_compact" | head -1)
peak_hours_str=$(grep -o '"hours"\[[^]]*\]' <<<"$_ph_section" | grep -o '[0-9]*' | tr '\n' ' ' | sed 's/^ //;s/ $//;s/ /,/g')
fi
fi
fi
# --- Session token accumulation (incremental, via cache file) ---
mkdir -p "$CACHE_DIR"
session_input=0
session_output=0
session_cache=0
session_context=0 # input + cache_read (total context per API call)
session_peak_input=0; session_peak_output=0; session_peak_cache=0
session_off_input=0; session_off_output=0; session_off_cache=0
if [ -n "$session_id" ] && [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
CACHE_FILE="${CACHE_DIR}/statusline-cache-${session_id}.json"
# Read previous cache
cached_lines=0
cached_input=0
cached_output=0
cached_cache=0
cached_context=0
cached_peak_input=0; cached_peak_output=0; cached_peak_cache=0
cached_off_input=0; cached_off_output=0; cached_off_cache=0
if [ -f "$CACHE_FILE" ]; then
_cache_json=$(cat "$CACHE_FILE" 2>/dev/null)
if [ -n "$JQ" ]; then
cached_lines=$(printf '%s' "$_cache_json" | "$JQ" -r '.lines // 0')
cached_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.input // 0')
cached_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.output // 0')
cached_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.cache // 0')
cached_context=$(printf '%s' "$_cache_json" | "$JQ" -r '.context // 0')
cached_peak_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_input // 0')
cached_peak_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_output // 0')
cached_peak_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.peak_cache // 0')
cached_off_input=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_input // 0')
cached_off_output=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_output // 0')
cached_off_cache=$(printf '%s' "$_cache_json" | "$JQ" -r '.off_cache // 0')
else
cached_lines=$(grep -o '"lines"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_input=$(grep -o '"input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_output=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_cache=$(grep -o '"cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_context=$(grep -o '"context"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_peak_input=$(grep -o '"peak_input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_peak_output=$(grep -o '"peak_output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_peak_cache=$(grep -o '"peak_cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_off_input=$(grep -o '"off_input"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_off_output=$(grep -o '"off_output"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
cached_off_cache=$(grep -o '"off_cache"[[:space:]]*:[[:space:]]*[0-9]*' <<<"$_cache_json" | head -1 | grep -o '[0-9]*$')
fi
fi
# Count current lines in transcript
current_lines=$(wc -l < "$transcript_path" 2>/dev/null || echo 0)
# Only parse new lines
if [ "$current_lines" -gt "$cached_lines" ] 2>/dev/null; then
start_line=$(( cached_lines + 1 ))
# Extract new lines to a temp file for batch processing
_new_lines=$(sed -n "${start_line},\$p" "$transcript_path" 2>/dev/null)
# Parse new lines: jq extracts per-line data, awk aggregates peak/offpeak
# Peak hours come from the price config (e.g. [[9,12],[14,18]])
# Pass as awk variable: "9,12,14,18" meaning ranges [9,12) and [14,18)
# Fallback: if no peak_hours configured, treat all as peak
if [ -z "$peak_hours_str" ]; then peak_hours_str="0,24"; fi
if [ -n "$JQ" ]; then
_agg=$(printf '%s\n' "$_new_lines" | "$JQ" -r '
select(.message.usage != null) |
[(.timestamp // "" | split("T")[1] | split(":")[0] | tonumber // 0),
.message.usage.input_tokens // 0,
.message.usage.output_tokens // 0,
.message.usage.cache_read_input_tokens // 0] | @tsv
' 2>/dev/null | awk -F'\t' -v ph="$peak_hours_str" '
function ispeak(h, s,n,a,i) {
n=split(ph,a,",")
for(i=1;i<=n;i+=2) if(h>=a[i] && h<a[i+1]) return 1
return 0
}
{ h=($1+8)%24; i+=$2; o+=$3; c+=int($4)
if(ispeak(h)){pi+=$2;po+=$3;pc+=int($4)} else {oi+=$2;oo+=$3;oc+=int($4)}
} END { printf "%d %d %d %d %d %d %d %d %d", i, o, c, pi, po, pc, oi, oo, oc }')
else
_agg=$(printf '%s\n' "$_new_lines" | awk -v ph="$peak_hours_str" '
function ispeak(h, s,n,a,i) {
n=split(ph,a,",")
for(i=1;i<=n;i+=2) if(h>=a[i] && h<a[i+1]) return 1
return 0
}
/"timestamp"/ { match($0, /"timestamp":"[^T]*T([0-9]+):/, a); h=(a[1]+8)%24 }
/"input_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); i+=a[1]; if(ispeak(h)) pi+=a[1]; else oi+=a[1] }
/"output_tokens"[[:space:]]*:[[:space:]]*[0-9]/ { match($0, /"output_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); o+=a[1]; if(ispeak(h)) po+=a[1]; else oo+=a[1] }
/"cache_read_input_tokens"/ { match($0, /"cache_read_input_tokens"[[:space:]]*:[[:space:]]*([0-9]+)/, a); c+=a[1]; if(ispeak(h)) pc+=a[1]; else oc+=a[1] }
END { printf "%d %d %d %d %d %d %d %d %d", i, o, c, pi, po, pc, oi, oo, oc }
')
fi
new_input=$(echo "$_agg" | awk '{print $1}')
new_output=$(echo "$_agg" | awk '{print $2}')
new_cache=$(echo "$_agg" | awk '{print $3}')
new_peak_input=$(echo "$_agg" | awk '{print $4}')
new_peak_output=$(echo "$_agg" | awk '{print $5}')
new_peak_cache=$(echo "$_agg" | awk '{print $6}')
new_off_input=$(echo "$_agg" | awk '{print $7}')
new_off_output=$(echo "$_agg" | awk '{print $8}')
new_off_cache=$(echo "$_agg" | awk '{print $9}')
new_context=$(( new_input + new_cache ))
session_input=$(( cached_input + new_input ))
session_output=$(( cached_output + new_output ))
session_cache=$(( cached_cache + new_cache ))
session_context=$(( cached_context + new_context ))
session_peak_input=$(( cached_peak_input + new_peak_input ))
session_peak_output=$(( cached_peak_output + new_peak_output ))
session_peak_cache=$(( cached_peak_cache + new_peak_cache ))
session_off_input=$(( cached_off_input + new_off_input ))
session_off_output=$(( cached_off_output + new_off_output ))
session_off_cache=$(( cached_off_cache + new_off_cache ))
# Update cache
printf '{"lines":%d,"input":%d,"output":%d,"cache":%d,"context":%d,"peak_input":%d,"peak_output":%d,"peak_cache":%d,"off_input":%d,"off_output":%d,"off_cache":%d}' \
"$current_lines" "$session_input" "$session_output" "$session_cache" "$session_context" \
"$session_peak_input" "$session_peak_output" "$session_peak_cache" \
"$session_off_input" "$session_off_output" "$session_off_cache" > "$CACHE_FILE"
else
session_input=$cached_input
session_output=$cached_output
session_cache=$cached_cache
session_context=$cached_context
session_peak_input=$cached_peak_input
session_peak_output=$cached_peak_output
session_peak_cache=$cached_peak_cache
session_off_input=$cached_off_input
session_off_output=$cached_off_output
session_off_cache=$cached_off_cache
fi
# Cleanup old cache files (older than CACHE_TTL_DAYS)
find "$CACHE_DIR" -name "statusline-cache-*.json" -mtime +${CACHE_TTL_DAYS} -delete 2>/dev/null
fi
# --- Extract git branch ---
branch=""
if [ -n "$cwd" ]; then
branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
fi
[ -z "$branch" ] && branch="no-git"
# --- Format token counts (2 decimal places) ---
_fmt_tokens() {
local t=$1
if [ -z "$t" ] || [ "$t" -eq 0 ] 2>/dev/null; then echo ""; return; fi
if [ "$t" -ge 1000000 ]; then
awk "BEGIN { printf \"%.2fM\", $t/1000000 }"
elif [ "$t" -ge 1000 ]; then
awk "BEGIN { printf \"%.2fK\", $t/1000 }"
else
printf '%d' "$t"
fi
}
# --- Session cumulative values ---
sess_in_str=$(_fmt_tokens "$session_input")
sess_out_str=$(_fmt_tokens "$session_output")
sess_cache_str=$(_fmt_tokens "$session_cache")
# Cache hit rate = cache_read / (input + cache_read) per session
sess_hit_rate=""
if [ "$session_context" -gt 0 ] 2>/dev/null; then
sess_hit_rate=$(( session_cache * 100 / session_context ))
fi
# Build detail: session cumulative
detail=""
[ -n "$sess_out_str" ] && detail="输出:${sess_out_str}"
[ -n "$sess_in_str" ] && detail="${detail:+$detail }新增输入:${sess_in_str}"
[ -n "$sess_cache_str" ] && detail="${detail:+$detail }缓存:${sess_cache_str}"
[ -n "$sess_hit_rate" ] && detail="${detail:+$detail }命中率:${sess_hit_rate}%"
# --- Cost calculation from price configs ---
PRICE_DIR="$(dirname "$0")/price"
cost_detail=""
if [ -d "$PRICE_DIR" ] && [ "$session_context" -gt 0 ] 2>/dev/null; then
# Get current hour in Asia/Shanghai (Beijing time)
# On Windows Git Bash, TZ override often fails. Try multiple methods.
bj_hour=$(TZ='Asia/Shanghai' date +%H 2>/dev/null)
# If TZ returned 00 but system hour is different, system is likely already in Beijing time
sys_hour=$(date +%H)
if [ "$bj_hour" = "00" ] && [ "$sys_hour" != "00" ]; then
bj_hour="$sys_hour"
fi
# Validate: if still empty, fallback to system hour
[ -z "$bj_hour" ] && bj_hour="$sys_hour"
# Find a working jq for price files (price files have no Windows paths, so jq should always work)
PJQ=""
for candidate in "/c/Program Files/jq/jq.exe" "/c/Program Files/jq/jq" "$(command -v jq 2>/dev/null)"; do
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
if echo '{"a":1}' | "$candidate" -r '.a' >/dev/null 2>&1; then
PJQ="$candidate"
break
fi
fi
done
# Auto-detect price file from current model name
# Try: exact match > partial match > active file > first file
# Normalize: lowercase, strip hyphens/dots/spaces for fuzzy matching
_norm() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]' | tr -d ' '; }
price_file=""
model_norm=$(_norm "$model")
# 1. Try exact match: normalized model name == normalized price file name
for pf in "$PRICE_DIR"/*.json; do
[ -f "$pf" ] || continue
pf_name=$(basename "$pf" .json)
if [ "$model_norm" = "$(_norm "$pf_name")" ]; then
price_file="$pf"
break
fi
done
# 2. Try partial match: normalized model contains price name, or vice versa
if [ -z "$price_file" ]; then
best_len=0
for pf in "$PRICE_DIR"/*.json; do
[ -f "$pf" ] || continue
pf_name=$(basename "$pf" .json)
pf_norm=$(_norm "$pf_name")
if [[ "$model_norm" == *"$pf_norm"* ]] || [[ "$pf_norm" == *"$model_norm"* ]]; then
# Prefer longer match (more specific)
if [ ${#pf_name} -gt $best_len ]; then
best_len=${#pf_name}
price_file="$pf"
fi
fi
done
fi
# 3. Fall back to active file
if [ -z "$price_file" ]; then
ACTIVE_FILE="${PRICE_DIR}/active"
if [ -f "$ACTIVE_FILE" ]; then
active_model=$(cat "$ACTIVE_FILE" 2>/dev/null | tr -d '\r\n\t ')
[ -f "${PRICE_DIR}/${active_model}.json" ] && price_file="${PRICE_DIR}/${active_model}.json"
fi
fi
# 4. Last resort: first json file
if [ -z "$price_file" ]; then
for pf in "$PRICE_DIR"/*.json; do
[ -f "$pf" ] && { price_file="$pf"; break; }
done
fi
# Use only the matched price file
if [ -n "$price_file" ] && [ -f "$price_file" ]; then
price_files=("$price_file")
else
price_files=()
fi
for price_file in "${price_files[@]}"; do
[ -f "$price_file" ] || continue
# Read price file and compact (remove newlines/tabs for grep fallback)
_pjson=$(cat "$price_file" 2>/dev/null)
_pjson_compact=$(printf '%s' "$_pjson" | tr -d '\n\t\r')
if [ -n "$PJQ" ]; then
pname=$(printf '%s' "$_pjson" | "$PJQ" -r '.name // empty')
currency=$(printf '%s' "$_pjson" | "$PJQ" -r '.currency // "¥"')
# Extract both peak and offpeak rates
pr_hit=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.cache_hit // 0')
pr_miss=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.cache_miss // 0')
pr_out=$(printf '%s' "$_pjson" | "$PJQ" -r '.peak.output // 0')
or_hit=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.cache_hit // 0')
or_miss=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.cache_miss // 0')
or_out=$(printf '%s' "$_pjson" | "$PJQ" -r '.offpeak.output // 0')
else
# grep fallback: compact JSON is single-line, patterns work
pname=$(grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$_pjson_compact" | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
currency=$(grep -o '"currency"[[:space:]]*:[[:space:]]*"[^"]*"' <<<"$_pjson_compact" | head -1 | sed 's/.*"currency"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
[ -z "$currency" ] && currency="¥"
# Extract both peak and offpeak rates
peak_section=$(grep -o '"peak"[^}]*}' <<<"$_pjson_compact" | head -1)
off_section=$(grep -o '"offpeak"[^}]*}' <<<"$_pjson_compact" | head -1)
pr_hit=$(grep -o '"cache_hit"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
pr_miss=$(grep -o '"cache_miss"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
pr_out=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$peak_section" | grep -o '[0-9.]*$')
or_hit=$(grep -o '"cache_hit"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
or_miss=$(grep -o '"cache_miss"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
or_out=$(grep -o '"output"[[:space:]]*:[[:space:]]*[0-9.]*' <<<"$off_section" | grep -o '[0-9.]*$')
fi
# Calculate cost: peak tokens × peak rate + offpeak tokens × offpeak rate
if [ -n "$pname" ] && [ -n "$pr_hit" ] && [ -n "$pr_miss" ] && [ -n "$pr_out" ]; then
total_cost=$(awk "BEGIN {
peak = ($session_peak_cache * $pr_hit + $session_peak_input * $pr_miss + $session_peak_output * $pr_out) / 1000000
off = ($session_off_cache * $or_hit + $session_off_input * $or_miss + $session_off_output * $or_out) / 1000000
printf \"%.2f\", peak + off
}")
# Show original model name, with warning if using fallback (active) price
if [ -n "$price_matched" ]; then
cost_detail="${cost_detail:+$cost_detail }${model}:${currency}${total_cost}"
else
cost_detail="${cost_detail:+$cost_detail }${model}(未配置价格):${currency}${total_cost}"
fi
fi
done
fi
# --- Build the 10-cell progress bar ---
bar=""
if [ -n "$used_pct" ] && [ "$used_pct" != "null" ]; then
pct_int=$(printf "%.0f" "$used_pct")
filled=$(( pct_int / 10 ))
[ $filled -gt 10 ] && filled=10
empty=$(( 10 - filled ))
blocks=$(printf '█%.0s' $(seq 1 $filled); printf '░%.0s' $(seq 1 $empty))
info="${pct_int}%"
if [ "$pct_int" -gt 80 ] 2>/dev/null; then
bar=$(printf '\033[31m%s\033[0m %s' "$blocks" "$info")
elif [ "$pct_int" -gt 50 ] 2>/dev/null; then
bar=$(printf '\033[32m%s\033[0m %s' "$blocks" "$info")
else
bar=$(printf '\033[90m%s\033[0m %s' "$blocks" "$info")
fi
else
bar="--%"
fi
# Append session detail
[ -n "$detail" ] && bar="${bar} | [${detail}]"
# Append cost detail
[ -n "$cost_detail" ] && bar="${bar} | ${cost_detail}"
# --- Output ---
printf '%b\n' "${branch} | ${bar}"