cc-context-stats 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/context-stats.md +17 -0
- package/.claude/settings.local.json +85 -0
- package/.editorconfig +60 -0
- package/.eslintrc.json +35 -0
- package/.github/dependabot.yml +44 -0
- package/.github/workflows/ci.yml +255 -0
- package/.github/workflows/release.yml +149 -0
- package/.pre-commit-config.yaml +74 -0
- package/.prettierrc +33 -0
- package/.shellcheckrc +10 -0
- package/CHANGELOG.md +100 -0
- package/CONTRIBUTING.md +240 -0
- package/PUBLISHING_GUIDE.md +69 -0
- package/README.md +179 -0
- package/config/settings-example.json +7 -0
- package/config/settings-node.json +7 -0
- package/config/settings-python.json +7 -0
- package/docs/configuration.md +83 -0
- package/docs/context-stats.md +132 -0
- package/docs/installation.md +195 -0
- package/docs/scripts.md +116 -0
- package/docs/troubleshooting.md +189 -0
- package/images/claude-statusline-token-graph.gif +0 -0
- package/images/claude-statusline.png +0 -0
- package/images/context-status-dumbzone.png +0 -0
- package/images/context-status.png +0 -0
- package/images/statusline-detail.png +0 -0
- package/images/token-graph.jpeg +0 -0
- package/images/token-graph.png +0 -0
- package/install +344 -0
- package/install.sh +272 -0
- package/jest.config.js +11 -0
- package/npm-publish.sh +33 -0
- package/package.json +36 -0
- package/publish.sh +24 -0
- package/pyproject.toml +113 -0
- package/requirements-dev.txt +12 -0
- package/scripts/context-stats.sh +970 -0
- package/scripts/statusline-full.sh +241 -0
- package/scripts/statusline-git.sh +32 -0
- package/scripts/statusline-minimal.sh +11 -0
- package/scripts/statusline.js +350 -0
- package/scripts/statusline.py +312 -0
- package/show_raw_claude_code_api.js +11 -0
- package/src/claude_statusline/__init__.py +11 -0
- package/src/claude_statusline/__main__.py +6 -0
- package/src/claude_statusline/cli/__init__.py +1 -0
- package/src/claude_statusline/cli/context_stats.py +379 -0
- package/src/claude_statusline/cli/statusline.py +172 -0
- package/src/claude_statusline/core/__init__.py +1 -0
- package/src/claude_statusline/core/colors.py +55 -0
- package/src/claude_statusline/core/config.py +98 -0
- package/src/claude_statusline/core/git.py +67 -0
- package/src/claude_statusline/core/state.py +266 -0
- package/src/claude_statusline/formatters/__init__.py +1 -0
- package/src/claude_statusline/formatters/time.py +50 -0
- package/src/claude_statusline/formatters/tokens.py +70 -0
- package/src/claude_statusline/graphs/__init__.py +1 -0
- package/src/claude_statusline/graphs/renderer.py +346 -0
- package/src/claude_statusline/graphs/statistics.py +58 -0
- package/tests/bash/test_install.bats +29 -0
- package/tests/bash/test_statusline_full.bats +109 -0
- package/tests/bash/test_statusline_git.bats +42 -0
- package/tests/bash/test_statusline_minimal.bats +37 -0
- package/tests/fixtures/json/high_usage.json +17 -0
- package/tests/fixtures/json/low_usage.json +17 -0
- package/tests/fixtures/json/medium_usage.json +17 -0
- package/tests/fixtures/json/valid_full.json +30 -0
- package/tests/fixtures/json/valid_minimal.json +9 -0
- package/tests/node/statusline.test.js +199 -0
- package/tests/python/conftest.py +84 -0
- package/tests/python/test_statusline.py +154 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Full-featured status line with context window usage
|
|
3
|
+
# Usage: Copy to ~/.claude/statusline.sh and make executable
|
|
4
|
+
#
|
|
5
|
+
# Configuration:
|
|
6
|
+
# Create/edit ~/.claude/statusline.conf and set:
|
|
7
|
+
#
|
|
8
|
+
# autocompact=true (when autocompact is enabled in Claude Code - default)
|
|
9
|
+
# autocompact=false (when you disable autocompact via /config in Claude Code)
|
|
10
|
+
#
|
|
11
|
+
# token_detail=true (show exact token count like 64,000 - default)
|
|
12
|
+
# token_detail=false (show abbreviated tokens like 64.0k)
|
|
13
|
+
#
|
|
14
|
+
# show_delta=true (show token delta since last refresh like [+2,500] - default)
|
|
15
|
+
# show_delta=false (disable delta display - saves file I/O on every refresh)
|
|
16
|
+
#
|
|
17
|
+
# show_session=true (show session_id in status line - default)
|
|
18
|
+
# show_session=false (hide session_id from status line)
|
|
19
|
+
#
|
|
20
|
+
# When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
|
|
21
|
+
#
|
|
22
|
+
# State file format (CSV):
|
|
23
|
+
# timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,workspace_project_dir
|
|
24
|
+
|
|
25
|
+
# Colors
|
|
26
|
+
BLUE='\033[0;34m'
|
|
27
|
+
MAGENTA='\033[0;35m'
|
|
28
|
+
CYAN='\033[0;36m'
|
|
29
|
+
GREEN='\033[0;32m'
|
|
30
|
+
YELLOW='\033[0;33m'
|
|
31
|
+
RED='\033[0;31m'
|
|
32
|
+
DIM='\033[2m'
|
|
33
|
+
RESET='\033[0m'
|
|
34
|
+
|
|
35
|
+
# Read JSON input from stdin
|
|
36
|
+
input=$(cat)
|
|
37
|
+
|
|
38
|
+
# Extract information from JSON
|
|
39
|
+
cwd=$(echo "$input" | jq -r '.workspace.current_dir')
|
|
40
|
+
project_dir=$(echo "$input" | jq -r '.workspace.project_dir')
|
|
41
|
+
model=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
42
|
+
session_id=$(echo "$input" | jq -r '.session_id // empty')
|
|
43
|
+
dir_name=$(basename "$cwd")
|
|
44
|
+
|
|
45
|
+
# Git information (skip optional locks for performance)
|
|
46
|
+
git_info=""
|
|
47
|
+
if [[ -d "$project_dir/.git" ]]; then
|
|
48
|
+
git_branch=$(cd "$project_dir" 2>/dev/null && git --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
49
|
+
git_status_count=$(cd "$project_dir" 2>/dev/null && git --no-optional-locks status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
50
|
+
|
|
51
|
+
if [[ -n "$git_branch" ]]; then
|
|
52
|
+
if [[ "$git_status_count" != "0" ]]; then
|
|
53
|
+
git_info=" | ${MAGENTA}${git_branch}${RESET} ${CYAN}[${git_status_count}]${RESET}"
|
|
54
|
+
else
|
|
55
|
+
git_info=" | ${MAGENTA}${git_branch}${RESET}"
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Read settings from ~/.claude/statusline.conf
|
|
61
|
+
# Sync this manually when you change settings in Claude Code via /config
|
|
62
|
+
autocompact_enabled=true
|
|
63
|
+
token_detail_enabled=true
|
|
64
|
+
show_delta_enabled=true
|
|
65
|
+
show_session_enabled=true
|
|
66
|
+
autocompact="" # Will be set by sourced config
|
|
67
|
+
token_detail="" # Will be set by sourced config
|
|
68
|
+
show_delta="" # Will be set by sourced config
|
|
69
|
+
show_session="" # Will be set by sourced config
|
|
70
|
+
ac_info=""
|
|
71
|
+
delta_info=""
|
|
72
|
+
session_info=""
|
|
73
|
+
|
|
74
|
+
# Create config file with defaults if it doesn't exist
|
|
75
|
+
if [[ ! -f ~/.claude/statusline.conf ]]; then
|
|
76
|
+
mkdir -p ~/.claude
|
|
77
|
+
cat >~/.claude/statusline.conf <<'EOF'
|
|
78
|
+
# Autocompact setting - sync with Claude Code's /config
|
|
79
|
+
autocompact=true
|
|
80
|
+
|
|
81
|
+
# Token display format
|
|
82
|
+
token_detail=true
|
|
83
|
+
|
|
84
|
+
# Show token delta since last refresh (adds file I/O on every refresh)
|
|
85
|
+
# Disable if you don't need it to reduce overhead
|
|
86
|
+
show_delta=true
|
|
87
|
+
|
|
88
|
+
# Show session_id in status line
|
|
89
|
+
show_session=true
|
|
90
|
+
EOF
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
if [[ -f ~/.claude/statusline.conf ]]; then
|
|
94
|
+
# shellcheck source=/dev/null
|
|
95
|
+
source ~/.claude/statusline.conf
|
|
96
|
+
if [[ "$autocompact" == "false" ]]; then
|
|
97
|
+
autocompact_enabled=false
|
|
98
|
+
fi
|
|
99
|
+
if [[ "$token_detail" == "false" ]]; then
|
|
100
|
+
token_detail_enabled=false
|
|
101
|
+
fi
|
|
102
|
+
if [[ "$show_delta" == "false" ]]; then
|
|
103
|
+
show_delta_enabled=false
|
|
104
|
+
fi
|
|
105
|
+
if [[ "$show_session" == "false" ]]; then
|
|
106
|
+
show_session_enabled=false
|
|
107
|
+
fi
|
|
108
|
+
# Note: show_io_tokens setting is read but not yet implemented
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Calculate context window - show remaining free space
|
|
112
|
+
context_info=""
|
|
113
|
+
total_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
|
|
114
|
+
current_usage=$(echo "$input" | jq '.context_window.current_usage')
|
|
115
|
+
total_input_tokens=$(echo "$input" | jq -r '.context_window.total_input_tokens // 0')
|
|
116
|
+
total_output_tokens=$(echo "$input" | jq -r '.context_window.total_output_tokens // 0')
|
|
117
|
+
cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
|
|
118
|
+
lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
|
|
119
|
+
lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
|
|
120
|
+
model_id=$(echo "$input" | jq -r '.model.id // ""')
|
|
121
|
+
workspace_project_dir=$(echo "$input" | jq -r '.workspace.project_dir // ""')
|
|
122
|
+
|
|
123
|
+
if [[ "$total_size" -gt 0 && "$current_usage" != "null" ]]; then
|
|
124
|
+
# Get tokens from current_usage (includes cache)
|
|
125
|
+
input_tokens=$(echo "$current_usage" | jq -r '.input_tokens // 0')
|
|
126
|
+
cache_creation=$(echo "$current_usage" | jq -r '.cache_creation_input_tokens // 0')
|
|
127
|
+
cache_read=$(echo "$current_usage" | jq -r '.cache_read_input_tokens // 0')
|
|
128
|
+
|
|
129
|
+
# Total used from current request
|
|
130
|
+
used_tokens=$((input_tokens + cache_creation + cache_read))
|
|
131
|
+
|
|
132
|
+
# Calculate autocompact buffer (22.5% of context window = 45k for 200k)
|
|
133
|
+
autocompact_buffer=$((total_size * 225 / 1000))
|
|
134
|
+
|
|
135
|
+
# Free tokens calculation depends on autocompact setting
|
|
136
|
+
if [[ "$autocompact_enabled" == "true" ]]; then
|
|
137
|
+
# When AC enabled: subtract buffer to show actual usable space
|
|
138
|
+
free_tokens=$((total_size - used_tokens - autocompact_buffer))
|
|
139
|
+
buffer_k=$(awk "BEGIN {printf \"%.0f\", $autocompact_buffer / 1000}")
|
|
140
|
+
ac_info=" ${DIM}[AC:${buffer_k}k]${RESET}"
|
|
141
|
+
else
|
|
142
|
+
# When AC disabled: show full free space
|
|
143
|
+
free_tokens=$((total_size - used_tokens))
|
|
144
|
+
ac_info=" ${DIM}[AC:off]${RESET}"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
if [[ "$free_tokens" -lt 0 ]]; then
|
|
148
|
+
free_tokens=0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Calculate percentage with one decimal (relative to total size)
|
|
152
|
+
free_pct=$(awk "BEGIN {printf \"%.1f\", ($free_tokens * 100.0 / $total_size)}")
|
|
153
|
+
free_pct_int=${free_pct%.*}
|
|
154
|
+
|
|
155
|
+
# Format tokens based on token_detail setting
|
|
156
|
+
if [[ "$token_detail_enabled" == "true" ]]; then
|
|
157
|
+
# Use awk for portable comma formatting (works regardless of locale)
|
|
158
|
+
free_display=$(awk -v n="$free_tokens" 'BEGIN { printf "%\047d", n }')
|
|
159
|
+
else
|
|
160
|
+
free_display=$(awk "BEGIN {printf \"%.1fk\", $free_tokens / 1000}")
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# Color based on free percentage
|
|
164
|
+
if [[ "$free_pct_int" -gt 50 ]]; then
|
|
165
|
+
ctx_color="$GREEN"
|
|
166
|
+
elif [[ "$free_pct_int" -gt 25 ]]; then
|
|
167
|
+
ctx_color="$YELLOW"
|
|
168
|
+
else
|
|
169
|
+
ctx_color="$RED"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
context_info=" | ${ctx_color}${free_display} free (${free_pct}%)${RESET}"
|
|
173
|
+
|
|
174
|
+
# Calculate and display token delta if enabled
|
|
175
|
+
if [[ "$show_delta_enabled" == "true" ]]; then
|
|
176
|
+
# Use session_id for per-session state (avoids conflicts with parallel sessions)
|
|
177
|
+
state_dir=~/.claude/statusline
|
|
178
|
+
mkdir -p "$state_dir"
|
|
179
|
+
|
|
180
|
+
# Migrate old state files from ~/.claude/ to ~/.claude/statusline/ (one-time migration)
|
|
181
|
+
old_state_dir=~/.claude
|
|
182
|
+
for old_file in "$old_state_dir"/statusline*.state; do
|
|
183
|
+
if [[ -f "$old_file" ]]; then
|
|
184
|
+
new_file="${state_dir}/$(basename "$old_file")"
|
|
185
|
+
if [[ ! -f "$new_file" ]]; then
|
|
186
|
+
mv "$old_file" "$new_file" 2>/dev/null || true
|
|
187
|
+
else
|
|
188
|
+
# New file exists, just remove old one
|
|
189
|
+
rm -f "$old_file" 2>/dev/null || true
|
|
190
|
+
fi
|
|
191
|
+
fi
|
|
192
|
+
done
|
|
193
|
+
|
|
194
|
+
if [[ -n "$session_id" ]]; then
|
|
195
|
+
state_file=${state_dir}/statusline.${session_id}.state
|
|
196
|
+
else
|
|
197
|
+
state_file=${state_dir}/statusline.state
|
|
198
|
+
fi
|
|
199
|
+
has_prev=false
|
|
200
|
+
prev_tokens=0
|
|
201
|
+
if [[ -f "$state_file" ]]; then
|
|
202
|
+
has_prev=true
|
|
203
|
+
# Read last line and calculate previous context usage
|
|
204
|
+
# CSV: ts[0],in[1],out[2],cur_in[3],cur_out[4],cache_create[5],cache_read[6]
|
|
205
|
+
last_line=$(tail -1 "$state_file" 2>/dev/null)
|
|
206
|
+
if [[ -n "$last_line" ]]; then
|
|
207
|
+
prev_cur_in=$(echo "$last_line" | cut -d',' -f4)
|
|
208
|
+
prev_cache_create=$(echo "$last_line" | cut -d',' -f6)
|
|
209
|
+
prev_cache_read=$(echo "$last_line" | cut -d',' -f7)
|
|
210
|
+
prev_tokens=$(( ${prev_cur_in:-0} + ${prev_cache_create:-0} + ${prev_cache_read:-0} ))
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
# Calculate delta
|
|
214
|
+
delta=$((used_tokens - prev_tokens))
|
|
215
|
+
# Only show positive delta (and skip first run when no previous state)
|
|
216
|
+
if [[ "$has_prev" == "true" && "$delta" -gt 0 ]]; then
|
|
217
|
+
if [[ "$token_detail_enabled" == "true" ]]; then
|
|
218
|
+
delta_display=$(awk -v n="$delta" 'BEGIN { printf "%\047d", n }')
|
|
219
|
+
else
|
|
220
|
+
delta_display=$(awk "BEGIN {printf \"%.1fk\", $delta / 1000}")
|
|
221
|
+
fi
|
|
222
|
+
delta_info=" ${DIM}[+${delta_display}]${RESET}"
|
|
223
|
+
fi
|
|
224
|
+
# Only append if context usage changed (avoid duplicates from multiple refreshes)
|
|
225
|
+
cur_input_tokens=$(echo "$current_usage" | jq -r '.input_tokens // 0')
|
|
226
|
+
cur_output_tokens=$(echo "$current_usage" | jq -r '.output_tokens // 0')
|
|
227
|
+
if [[ "$has_prev" != "true" || "$used_tokens" != "$prev_tokens" ]]; then
|
|
228
|
+
# Append current usage with comprehensive format
|
|
229
|
+
# Format: ts,in,out,cur_in,cur_out,cache_create,cache_read,cost,+lines,-lines,session,model,dir,size
|
|
230
|
+
echo "$(date +%s),$total_input_tokens,$total_output_tokens,$cur_input_tokens,$cur_output_tokens,$cache_creation,$cache_read,$cost_usd,$lines_added,$lines_removed,$session_id,$model_id,$workspace_project_dir,$total_size" >>"$state_file"
|
|
231
|
+
fi
|
|
232
|
+
fi
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# Display session_id if enabled
|
|
236
|
+
if [[ "$show_session_enabled" == "true" && -n "$session_id" ]]; then
|
|
237
|
+
session_info=" ${DIM}${session_id}${RESET}"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
|
|
241
|
+
echo -e "${DIM}[${model}]${RESET} ${BLUE}${dir_name}${RESET}${git_info}${context_info}${delta_info}${ac_info}${session_info}"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Git-aware status line - shows model, directory, and git branch
|
|
3
|
+
# Usage: Copy to ~/.claude/statusline.sh and make executable
|
|
4
|
+
|
|
5
|
+
# Colors
|
|
6
|
+
BLUE='\033[0;34m'
|
|
7
|
+
MAGENTA='\033[0;35m'
|
|
8
|
+
CYAN='\033[0;36m'
|
|
9
|
+
RESET='\033[0m'
|
|
10
|
+
|
|
11
|
+
input=$(cat)
|
|
12
|
+
|
|
13
|
+
MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
14
|
+
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
|
|
15
|
+
DIR_NAME="${CURRENT_DIR##*/}"
|
|
16
|
+
|
|
17
|
+
# Git branch detection
|
|
18
|
+
GIT_INFO=""
|
|
19
|
+
if git -C "$CURRENT_DIR" rev-parse --git-dir > /dev/null 2>&1; then
|
|
20
|
+
BRANCH=$(git -C "$CURRENT_DIR" branch --show-current 2>/dev/null)
|
|
21
|
+
if [ -n "$BRANCH" ]; then
|
|
22
|
+
# Count uncommitted changes
|
|
23
|
+
CHANGES=$(git -C "$CURRENT_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
24
|
+
if [ "$CHANGES" -gt 0 ]; then
|
|
25
|
+
GIT_INFO=" | ${MAGENTA}${BRANCH}${RESET} ${CYAN}[${CHANGES}]${RESET}"
|
|
26
|
+
else
|
|
27
|
+
GIT_INFO=" | ${MAGENTA}${BRANCH}${RESET}"
|
|
28
|
+
fi
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo -e "[${MODEL_DISPLAY}] ${BLUE}${DIR_NAME}${RESET}${GIT_INFO}"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Minimal status line - shows model and current directory
|
|
3
|
+
# Usage: Copy to ~/.claude/statusline.sh and make executable
|
|
4
|
+
|
|
5
|
+
input=$(cat)
|
|
6
|
+
|
|
7
|
+
MODEL_DISPLAY=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
8
|
+
CURRENT_DIR=$(echo "$input" | jq -r '.workspace.current_dir // "~"')
|
|
9
|
+
DIR_NAME="${CURRENT_DIR##*/}"
|
|
10
|
+
|
|
11
|
+
echo "[$MODEL_DISPLAY] $DIR_NAME"
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Node.js status line script for Claude Code
|
|
4
|
+
* Usage: Copy to ~/.claude/statusline.js and make executable
|
|
5
|
+
*
|
|
6
|
+
* Configuration:
|
|
7
|
+
* Create/edit ~/.claude/statusline.conf and set:
|
|
8
|
+
*
|
|
9
|
+
* autocompact=true (when autocompact is enabled in Claude Code - default)
|
|
10
|
+
* autocompact=false (when you disable autocompact via /config in Claude Code)
|
|
11
|
+
*
|
|
12
|
+
* token_detail=true (show exact token count like 64,000 - default)
|
|
13
|
+
* token_detail=false (show abbreviated tokens like 64.0k)
|
|
14
|
+
*
|
|
15
|
+
* show_delta=true (show token delta since last refresh like [+2,500] - default)
|
|
16
|
+
* show_delta=false (disable delta display - saves file I/O on every refresh)
|
|
17
|
+
*
|
|
18
|
+
* show_session=true (show session_id in status line - default)
|
|
19
|
+
* show_session=false (hide session_id from status line)
|
|
20
|
+
*
|
|
21
|
+
* When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
|
|
22
|
+
*
|
|
23
|
+
* State file format (CSV):
|
|
24
|
+
* timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,
|
|
25
|
+
* current_usage_output_tokens,current_usage_cache_creation,current_usage_cache_read,
|
|
26
|
+
* total_cost_usd,total_lines_added,total_lines_removed,session_id,model_id,
|
|
27
|
+
* workspace_project_dir
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const { execSync } = require('child_process');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
const os = require('os');
|
|
34
|
+
|
|
35
|
+
// ANSI Colors
|
|
36
|
+
const BLUE = '\x1b[0;34m';
|
|
37
|
+
const MAGENTA = '\x1b[0;35m';
|
|
38
|
+
const CYAN = '\x1b[0;36m';
|
|
39
|
+
const GREEN = '\x1b[0;32m';
|
|
40
|
+
const YELLOW = '\x1b[0;33m';
|
|
41
|
+
const RED = '\x1b[0;31m';
|
|
42
|
+
const DIM = '\x1b[2m';
|
|
43
|
+
const RESET = '\x1b[0m';
|
|
44
|
+
|
|
45
|
+
function getGitInfo(projectDir) {
|
|
46
|
+
const gitDir = path.join(projectDir, '.git');
|
|
47
|
+
if (!fs.existsSync(gitDir) || !fs.statSync(gitDir).isDirectory()) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Get branch name (skip optional locks for performance)
|
|
53
|
+
const branch = execSync('git --no-optional-locks rev-parse --abbrev-ref HEAD', {
|
|
54
|
+
cwd: projectDir,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
57
|
+
}).trim();
|
|
58
|
+
|
|
59
|
+
if (!branch) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Count changes
|
|
64
|
+
const status = execSync('git --no-optional-locks status --porcelain', {
|
|
65
|
+
cwd: projectDir,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
const changes = status.split('\n').filter(l => l.trim()).length;
|
|
70
|
+
|
|
71
|
+
if (changes > 0) {
|
|
72
|
+
return ` | ${MAGENTA}${branch}${RESET} ${CYAN}[${changes}]${RESET}`;
|
|
73
|
+
}
|
|
74
|
+
return ` | ${MAGENTA}${branch}${RESET}`;
|
|
75
|
+
} catch {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readConfig() {
|
|
81
|
+
const config = {
|
|
82
|
+
autocompact: true,
|
|
83
|
+
tokenDetail: true,
|
|
84
|
+
showDelta: true,
|
|
85
|
+
showSession: true,
|
|
86
|
+
showIoTokens: true,
|
|
87
|
+
};
|
|
88
|
+
const configPath = path.join(os.homedir(), '.claude', 'statusline.conf');
|
|
89
|
+
|
|
90
|
+
// Create config file with defaults if it doesn't exist
|
|
91
|
+
if (!fs.existsSync(configPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const configDir = path.dirname(configPath);
|
|
94
|
+
if (!fs.existsSync(configDir)) {
|
|
95
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
const defaultConfig = `# Autocompact setting - sync with Claude Code's /config
|
|
98
|
+
autocompact=true
|
|
99
|
+
|
|
100
|
+
# Token display format
|
|
101
|
+
token_detail=true
|
|
102
|
+
|
|
103
|
+
# Show token delta since last refresh (adds file I/O on every refresh)
|
|
104
|
+
# Disable if you don't need it to reduce overhead
|
|
105
|
+
show_delta=true
|
|
106
|
+
|
|
107
|
+
# Show session_id in status line
|
|
108
|
+
show_session=true
|
|
109
|
+
`;
|
|
110
|
+
fs.writeFileSync(configPath, defaultConfig);
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore errors creating config
|
|
113
|
+
}
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
119
|
+
for (const line of content.split('\n')) {
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
if (trimmed.startsWith('#') || !trimmed.includes('=')) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const [key, value] = trimmed.split('=', 2);
|
|
125
|
+
const keyTrimmed = key.trim();
|
|
126
|
+
const valueTrimmed = value.trim().toLowerCase();
|
|
127
|
+
if (keyTrimmed === 'autocompact') {
|
|
128
|
+
config.autocompact = valueTrimmed !== 'false';
|
|
129
|
+
} else if (keyTrimmed === 'token_detail') {
|
|
130
|
+
config.tokenDetail = valueTrimmed !== 'false';
|
|
131
|
+
} else if (keyTrimmed === 'show_delta') {
|
|
132
|
+
config.showDelta = valueTrimmed !== 'false';
|
|
133
|
+
} else if (keyTrimmed === 'show_session') {
|
|
134
|
+
config.showSession = valueTrimmed !== 'false';
|
|
135
|
+
} else if (keyTrimmed === 'show_io_tokens') {
|
|
136
|
+
config.showIoTokens = valueTrimmed !== 'false';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore errors
|
|
141
|
+
}
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let input = '';
|
|
146
|
+
|
|
147
|
+
process.stdin.setEncoding('utf8');
|
|
148
|
+
process.stdin.on('data', chunk => (input += chunk));
|
|
149
|
+
|
|
150
|
+
process.stdin.on('end', () => {
|
|
151
|
+
let data;
|
|
152
|
+
try {
|
|
153
|
+
data = JSON.parse(input);
|
|
154
|
+
} catch {
|
|
155
|
+
console.log('[Claude] ~');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Extract data
|
|
160
|
+
const cwd = data.workspace?.current_dir || '~';
|
|
161
|
+
const projectDir = data.workspace?.project_dir || cwd;
|
|
162
|
+
const model = data.model?.display_name || 'Claude';
|
|
163
|
+
const dirName = path.basename(cwd) || '~';
|
|
164
|
+
|
|
165
|
+
// Git info
|
|
166
|
+
const gitInfo = getGitInfo(projectDir);
|
|
167
|
+
|
|
168
|
+
// Read settings from config file
|
|
169
|
+
const config = readConfig();
|
|
170
|
+
const autocompactEnabled = config.autocompact;
|
|
171
|
+
const tokenDetail = config.tokenDetail;
|
|
172
|
+
const showDelta = config.showDelta;
|
|
173
|
+
const showSession = config.showSession;
|
|
174
|
+
// Note: showIoTokens setting is read but not yet implemented
|
|
175
|
+
|
|
176
|
+
// Extract session_id once for reuse
|
|
177
|
+
const sessionId = data.session_id;
|
|
178
|
+
|
|
179
|
+
// Context window calculation
|
|
180
|
+
let contextInfo = '';
|
|
181
|
+
let acInfo = '';
|
|
182
|
+
let deltaInfo = '';
|
|
183
|
+
let sessionInfo = '';
|
|
184
|
+
const totalSize = data.context_window?.context_window_size || 0;
|
|
185
|
+
const currentUsage = data.context_window?.current_usage;
|
|
186
|
+
const totalInputTokens = data.context_window?.total_input_tokens || 0;
|
|
187
|
+
const totalOutputTokens = data.context_window?.total_output_tokens || 0;
|
|
188
|
+
const costUsd = data.cost?.total_cost_usd || 0;
|
|
189
|
+
const linesAdded = data.cost?.total_lines_added || 0;
|
|
190
|
+
const linesRemoved = data.cost?.total_lines_removed || 0;
|
|
191
|
+
const modelId = data.model?.id || '';
|
|
192
|
+
const workspaceProjectDir = data.workspace?.project_dir || '';
|
|
193
|
+
|
|
194
|
+
if (totalSize > 0 && currentUsage) {
|
|
195
|
+
// Get tokens from current_usage (includes cache)
|
|
196
|
+
const inputTokens = currentUsage.input_tokens || 0;
|
|
197
|
+
const cacheCreation = currentUsage.cache_creation_input_tokens || 0;
|
|
198
|
+
const cacheRead = currentUsage.cache_read_input_tokens || 0;
|
|
199
|
+
|
|
200
|
+
// Total used from current request
|
|
201
|
+
const usedTokens = inputTokens + cacheCreation + cacheRead;
|
|
202
|
+
|
|
203
|
+
// Calculate autocompact buffer (22.5% of context window = 45k for 200k)
|
|
204
|
+
const autocompactBuffer = Math.floor(totalSize * 0.225);
|
|
205
|
+
|
|
206
|
+
// Free tokens calculation depends on autocompact setting
|
|
207
|
+
let freeTokens;
|
|
208
|
+
if (autocompactEnabled) {
|
|
209
|
+
// When AC enabled: subtract buffer to show actual usable space
|
|
210
|
+
freeTokens = totalSize - usedTokens - autocompactBuffer;
|
|
211
|
+
const bufferK = Math.floor(autocompactBuffer / 1000);
|
|
212
|
+
acInfo = ` ${DIM}[AC:${bufferK}k]${RESET}`;
|
|
213
|
+
} else {
|
|
214
|
+
// When AC disabled: show full free space
|
|
215
|
+
freeTokens = totalSize - usedTokens;
|
|
216
|
+
acInfo = ` ${DIM}[AC:off]${RESET}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (freeTokens < 0) {
|
|
220
|
+
freeTokens = 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Calculate percentage with one decimal (relative to total size)
|
|
224
|
+
const freePct = (freeTokens * 100.0) / totalSize;
|
|
225
|
+
const freePctInt = Math.floor(freePct);
|
|
226
|
+
|
|
227
|
+
// Format tokens based on token_detail setting
|
|
228
|
+
const freeDisplay = tokenDetail
|
|
229
|
+
? freeTokens.toLocaleString('en-US')
|
|
230
|
+
: `${(freeTokens / 1000).toFixed(1)}k`;
|
|
231
|
+
|
|
232
|
+
// Color based on free percentage
|
|
233
|
+
let ctxColor;
|
|
234
|
+
if (freePctInt > 50) {
|
|
235
|
+
ctxColor = GREEN;
|
|
236
|
+
} else if (freePctInt > 25) {
|
|
237
|
+
ctxColor = YELLOW;
|
|
238
|
+
} else {
|
|
239
|
+
ctxColor = RED;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
contextInfo = ` | ${ctxColor}${freeDisplay} free (${freePct.toFixed(1)}%)${RESET}`;
|
|
243
|
+
|
|
244
|
+
// Calculate and display token delta if enabled
|
|
245
|
+
if (showDelta) {
|
|
246
|
+
const stateDir = path.join(os.homedir(), '.claude', 'statusline');
|
|
247
|
+
if (!fs.existsSync(stateDir)) {
|
|
248
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const oldStateDir = path.join(os.homedir(), '.claude');
|
|
252
|
+
try {
|
|
253
|
+
const oldFiles = fs
|
|
254
|
+
.readdirSync(oldStateDir)
|
|
255
|
+
.filter(f => f.match(/^statusline.*\.state$/));
|
|
256
|
+
for (const fileName of oldFiles) {
|
|
257
|
+
const oldFile = path.join(oldStateDir, fileName);
|
|
258
|
+
const newFile = path.join(stateDir, fileName);
|
|
259
|
+
if (fs.statSync(oldFile).isFile()) {
|
|
260
|
+
if (!fs.existsSync(newFile)) {
|
|
261
|
+
fs.renameSync(oldFile, newFile);
|
|
262
|
+
} else {
|
|
263
|
+
fs.unlinkSync(oldFile);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
/* migration errors are non-fatal */
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const stateFileName = sessionId ? `statusline.${sessionId}.state` : 'statusline.state';
|
|
272
|
+
const stateFile = path.join(stateDir, stateFileName);
|
|
273
|
+
let hasPrev = false;
|
|
274
|
+
let prevTokens = 0;
|
|
275
|
+
try {
|
|
276
|
+
if (fs.existsSync(stateFile)) {
|
|
277
|
+
hasPrev = true;
|
|
278
|
+
// Read last line to get previous context usage
|
|
279
|
+
const content = fs.readFileSync(stateFile, 'utf8').trim();
|
|
280
|
+
const lines = content.split('\n');
|
|
281
|
+
const lastLine = lines[lines.length - 1];
|
|
282
|
+
if (lastLine.includes(',')) {
|
|
283
|
+
const parts = lastLine.split(',');
|
|
284
|
+
// Calculate previous context usage:
|
|
285
|
+
// cur_input + cache_creation + cache_read
|
|
286
|
+
// CSV indices: cur_in[3], cache_create[5], cache_read[6]
|
|
287
|
+
const prevCurInput = parseInt(parts[3], 10) || 0;
|
|
288
|
+
const prevCacheCreation = parseInt(parts[5], 10) || 0;
|
|
289
|
+
const prevCacheRead = parseInt(parts[6], 10) || 0;
|
|
290
|
+
prevTokens = prevCurInput + prevCacheCreation + prevCacheRead;
|
|
291
|
+
} else {
|
|
292
|
+
// Old format - single value
|
|
293
|
+
prevTokens = parseInt(lastLine, 10) || 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
prevTokens = 0;
|
|
298
|
+
}
|
|
299
|
+
// Calculate delta (difference in context window usage)
|
|
300
|
+
const delta = usedTokens - prevTokens;
|
|
301
|
+
// Only show positive delta (and skip first run when no previous state)
|
|
302
|
+
if (hasPrev && delta > 0) {
|
|
303
|
+
const deltaDisplay = tokenDetail
|
|
304
|
+
? delta.toLocaleString('en-US')
|
|
305
|
+
: `${(delta / 1000).toFixed(1)}k`;
|
|
306
|
+
deltaInfo = ` ${DIM}[+${deltaDisplay}]${RESET}`;
|
|
307
|
+
}
|
|
308
|
+
// Only append if context usage changed (avoid duplicates from multiple refreshes)
|
|
309
|
+
if (!hasPrev || usedTokens !== prevTokens) {
|
|
310
|
+
// Append current usage with comprehensive format
|
|
311
|
+
// Format: ts,total_in,total_out,cur_in,cur_out,cache_create,cache_read,
|
|
312
|
+
// cost_usd,lines_added,lines_removed,session_id,model_id,project_dir
|
|
313
|
+
try {
|
|
314
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
315
|
+
const curInputTokens = currentUsage.input_tokens || 0;
|
|
316
|
+
const curOutputTokens = currentUsage.output_tokens || 0;
|
|
317
|
+
const stateData = [
|
|
318
|
+
timestamp,
|
|
319
|
+
totalInputTokens,
|
|
320
|
+
totalOutputTokens,
|
|
321
|
+
curInputTokens,
|
|
322
|
+
curOutputTokens,
|
|
323
|
+
cacheCreation,
|
|
324
|
+
cacheRead,
|
|
325
|
+
costUsd,
|
|
326
|
+
linesAdded,
|
|
327
|
+
linesRemoved,
|
|
328
|
+
sessionId || '',
|
|
329
|
+
modelId,
|
|
330
|
+
workspaceProjectDir,
|
|
331
|
+
totalSize,
|
|
332
|
+
].join(',');
|
|
333
|
+
fs.appendFileSync(stateFile, `${stateData}\n`);
|
|
334
|
+
} catch {
|
|
335
|
+
// Ignore errors
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Display session_id if enabled
|
|
342
|
+
if (showSession && sessionId) {
|
|
343
|
+
sessionInfo = ` ${DIM}${sessionId}${RESET}`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
|
|
347
|
+
console.log(
|
|
348
|
+
`${DIM}[${model}]${RESET} ${BLUE}${dirName}${RESET}${gitInfo}${contextInfo}${deltaInfo}${acInfo}${sessionInfo}`
|
|
349
|
+
);
|
|
350
|
+
});
|