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.
Files changed (72) hide show
  1. package/.claude/commands/context-stats.md +17 -0
  2. package/.claude/settings.local.json +85 -0
  3. package/.editorconfig +60 -0
  4. package/.eslintrc.json +35 -0
  5. package/.github/dependabot.yml +44 -0
  6. package/.github/workflows/ci.yml +255 -0
  7. package/.github/workflows/release.yml +149 -0
  8. package/.pre-commit-config.yaml +74 -0
  9. package/.prettierrc +33 -0
  10. package/.shellcheckrc +10 -0
  11. package/CHANGELOG.md +100 -0
  12. package/CONTRIBUTING.md +240 -0
  13. package/PUBLISHING_GUIDE.md +69 -0
  14. package/README.md +179 -0
  15. package/config/settings-example.json +7 -0
  16. package/config/settings-node.json +7 -0
  17. package/config/settings-python.json +7 -0
  18. package/docs/configuration.md +83 -0
  19. package/docs/context-stats.md +132 -0
  20. package/docs/installation.md +195 -0
  21. package/docs/scripts.md +116 -0
  22. package/docs/troubleshooting.md +189 -0
  23. package/images/claude-statusline-token-graph.gif +0 -0
  24. package/images/claude-statusline.png +0 -0
  25. package/images/context-status-dumbzone.png +0 -0
  26. package/images/context-status.png +0 -0
  27. package/images/statusline-detail.png +0 -0
  28. package/images/token-graph.jpeg +0 -0
  29. package/images/token-graph.png +0 -0
  30. package/install +344 -0
  31. package/install.sh +272 -0
  32. package/jest.config.js +11 -0
  33. package/npm-publish.sh +33 -0
  34. package/package.json +36 -0
  35. package/publish.sh +24 -0
  36. package/pyproject.toml +113 -0
  37. package/requirements-dev.txt +12 -0
  38. package/scripts/context-stats.sh +970 -0
  39. package/scripts/statusline-full.sh +241 -0
  40. package/scripts/statusline-git.sh +32 -0
  41. package/scripts/statusline-minimal.sh +11 -0
  42. package/scripts/statusline.js +350 -0
  43. package/scripts/statusline.py +312 -0
  44. package/show_raw_claude_code_api.js +11 -0
  45. package/src/claude_statusline/__init__.py +11 -0
  46. package/src/claude_statusline/__main__.py +6 -0
  47. package/src/claude_statusline/cli/__init__.py +1 -0
  48. package/src/claude_statusline/cli/context_stats.py +379 -0
  49. package/src/claude_statusline/cli/statusline.py +172 -0
  50. package/src/claude_statusline/core/__init__.py +1 -0
  51. package/src/claude_statusline/core/colors.py +55 -0
  52. package/src/claude_statusline/core/config.py +98 -0
  53. package/src/claude_statusline/core/git.py +67 -0
  54. package/src/claude_statusline/core/state.py +266 -0
  55. package/src/claude_statusline/formatters/__init__.py +1 -0
  56. package/src/claude_statusline/formatters/time.py +50 -0
  57. package/src/claude_statusline/formatters/tokens.py +70 -0
  58. package/src/claude_statusline/graphs/__init__.py +1 -0
  59. package/src/claude_statusline/graphs/renderer.py +346 -0
  60. package/src/claude_statusline/graphs/statistics.py +58 -0
  61. package/tests/bash/test_install.bats +29 -0
  62. package/tests/bash/test_statusline_full.bats +109 -0
  63. package/tests/bash/test_statusline_git.bats +42 -0
  64. package/tests/bash/test_statusline_minimal.bats +37 -0
  65. package/tests/fixtures/json/high_usage.json +17 -0
  66. package/tests/fixtures/json/low_usage.json +17 -0
  67. package/tests/fixtures/json/medium_usage.json +17 -0
  68. package/tests/fixtures/json/valid_full.json +30 -0
  69. package/tests/fixtures/json/valid_minimal.json +9 -0
  70. package/tests/node/statusline.test.js +199 -0
  71. package/tests/python/conftest.py +84 -0
  72. 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
+ });