cc-context-stats 1.8.0 → 1.8.2

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 (106) hide show
  1. package/package.json +8 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/.editorconfig +0 -60
  4. package/.eslintrc.json +0 -35
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  7. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  8. package/.github/dependabot.yml +0 -44
  9. package/.github/workflows/ci.yml +0 -294
  10. package/.github/workflows/release.yml +0 -151
  11. package/.pre-commit-config.yaml +0 -74
  12. package/.prettierrc +0 -33
  13. package/.shellcheckrc +0 -10
  14. package/CHANGELOG.md +0 -187
  15. package/CLAUDE.md +0 -66
  16. package/CODE_OF_CONDUCT.md +0 -59
  17. package/CONTRIBUTING.md +0 -240
  18. package/RELEASE_NOTES.md +0 -19
  19. package/SECURITY.md +0 -44
  20. package/TODOS.md +0 -72
  21. package/assets/logo/favicon.svg +0 -19
  22. package/assets/logo/logo-black.svg +0 -24
  23. package/assets/logo/logo-full.svg +0 -40
  24. package/assets/logo/logo-icon.svg +0 -27
  25. package/assets/logo/logo-mark.svg +0 -28
  26. package/assets/logo/logo-white.svg +0 -24
  27. package/assets/logo/logo-wordmark.svg +0 -6
  28. package/config/settings-example.json +0 -7
  29. package/config/settings-node.json +0 -7
  30. package/config/settings-python.json +0 -7
  31. package/docs/ARCHITECTURE.md +0 -128
  32. package/docs/CSV_FORMAT.md +0 -42
  33. package/docs/DEPLOYMENT.md +0 -71
  34. package/docs/DEVELOPMENT.md +0 -161
  35. package/docs/MODEL_INTELLIGENCE.md +0 -396
  36. package/docs/configuration.md +0 -118
  37. package/docs/context-stats.md +0 -143
  38. package/docs/installation.md +0 -255
  39. package/docs/scripts.md +0 -140
  40. package/docs/troubleshooting.md +0 -278
  41. package/images/claude-statusline-token-graph.gif +0 -0
  42. package/images/claude-statusline.png +0 -0
  43. package/images/context-status-dumbzone.png +0 -0
  44. package/images/context-status.png +0 -0
  45. package/images/statusline-detail.png +0 -0
  46. package/images/token-graph.jpeg +0 -0
  47. package/images/token-graph.png +0 -0
  48. package/images/v1.6.1.png +0 -0
  49. package/install +0 -351
  50. package/install.sh +0 -298
  51. package/jest.config.js +0 -11
  52. package/pyproject.toml +0 -115
  53. package/requirements-dev.txt +0 -12
  54. package/scripts/statusline-full.sh +0 -438
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -569
  58. package/src/claude_statusline/__init__.py +0 -11
  59. package/src/claude_statusline/__main__.py +0 -6
  60. package/src/claude_statusline/cli/__init__.py +0 -1
  61. package/src/claude_statusline/cli/context_stats.py +0 -542
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -184
  64. package/src/claude_statusline/core/__init__.py +0 -1
  65. package/src/claude_statusline/core/colors.py +0 -124
  66. package/src/claude_statusline/core/config.py +0 -165
  67. package/src/claude_statusline/core/git.py +0 -78
  68. package/src/claude_statusline/core/state.py +0 -323
  69. package/src/claude_statusline/formatters/__init__.py +0 -1
  70. package/src/claude_statusline/formatters/layout.py +0 -67
  71. package/src/claude_statusline/formatters/time.py +0 -50
  72. package/src/claude_statusline/formatters/tokens.py +0 -70
  73. package/src/claude_statusline/graphs/__init__.py +0 -1
  74. package/src/claude_statusline/graphs/intelligence.py +0 -162
  75. package/src/claude_statusline/graphs/renderer.py +0 -401
  76. package/src/claude_statusline/graphs/statistics.py +0 -92
  77. package/src/claude_statusline/ui/__init__.py +0 -1
  78. package/src/claude_statusline/ui/icons.py +0 -93
  79. package/src/claude_statusline/ui/waiting.py +0 -62
  80. package/tests/bash/test_delta_parity.bats +0 -199
  81. package/tests/bash/test_install.bats +0 -29
  82. package/tests/bash/test_parity.bats +0 -315
  83. package/tests/bash/test_statusline_full.bats +0 -139
  84. package/tests/bash/test_statusline_git.bats +0 -42
  85. package/tests/bash/test_statusline_minimal.bats +0 -37
  86. package/tests/fixtures/json/comma_in_path.json +0 -31
  87. package/tests/fixtures/json/high_usage.json +0 -17
  88. package/tests/fixtures/json/low_usage.json +0 -17
  89. package/tests/fixtures/json/medium_usage.json +0 -17
  90. package/tests/fixtures/json/valid_full.json +0 -30
  91. package/tests/fixtures/json/valid_minimal.json +0 -9
  92. package/tests/fixtures/mi_test_vectors.json +0 -140
  93. package/tests/node/intelligence.test.js +0 -98
  94. package/tests/node/rotation.test.js +0 -89
  95. package/tests/node/statusline.test.js +0 -240
  96. package/tests/python/conftest.py +0 -84
  97. package/tests/python/test_colors.py +0 -105
  98. package/tests/python/test_config_colors.py +0 -78
  99. package/tests/python/test_data_pipeline.py +0 -446
  100. package/tests/python/test_explain.py +0 -177
  101. package/tests/python/test_icons.py +0 -152
  102. package/tests/python/test_intelligence.py +0 -314
  103. package/tests/python/test_layout.py +0 -127
  104. package/tests/python/test_state_rotation_validation.py +0 -232
  105. package/tests/python/test_statusline.py +0 -215
  106. package/tests/python/test_waiting.py +0 -127
@@ -1,67 +0,0 @@
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
- # Width-fitting helpers
12
- visible_width() {
13
- local stripped
14
- stripped=$(printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g')
15
- printf '%s' "$stripped" | wc -m | tr -d ' '
16
- }
17
-
18
- get_terminal_width() {
19
- # When running inside Claude Code's statusline subprocess, $COLUMNS is not set
20
- # and tput falls back to 80. If COLUMNS is set, trust it. Otherwise use 200
21
- # so no parts are dropped; Claude Code handles overflow.
22
- if [[ -n "$COLUMNS" ]]; then
23
- echo "$COLUMNS"
24
- else
25
- local cols
26
- cols=$(tput cols 2>/dev/null || echo 80)
27
- if [[ "$cols" -eq 80 ]]; then
28
- echo 200
29
- else
30
- echo "$cols"
31
- fi
32
- fi
33
- }
34
-
35
- fit_to_width() {
36
- local max_width=$1
37
- shift
38
- local parts=("$@")
39
-
40
- if [[ ${#parts[@]} -eq 0 ]]; then
41
- echo ""
42
- return
43
- fi
44
-
45
- local result="${parts[0]}"
46
- local current_width
47
- current_width=$(visible_width "$result")
48
-
49
- for ((i = 1; i < ${#parts[@]}; i++)); do
50
- local part="${parts[$i]}"
51
- if [[ -z "$part" ]]; then
52
- continue
53
- fi
54
- local part_width
55
- part_width=$(visible_width "$part")
56
- if (( current_width + part_width <= max_width )); then
57
- result+="$part"
58
- (( current_width += part_width ))
59
- fi
60
- done
61
-
62
- echo -e "$result"
63
- }
64
-
65
- base="[$MODEL_DISPLAY] $DIR_NAME"
66
- max_width=$(get_terminal_width)
67
- fit_to_width "$max_width" "$base"
@@ -1,569 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Python status line script for Claude Code
4
- Usage: Copy to ~/.claude/statusline.py 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,context_window_size
28
- """
29
-
30
- import json
31
- import os
32
- import re
33
- import shutil
34
- import subprocess
35
- import sys
36
- import tempfile
37
-
38
- ROTATION_THRESHOLD = 10_000
39
- ROTATION_KEEP = 5_000
40
-
41
- # Model Intelligence constants (hardcoded, not configurable)
42
- MI_WEIGHT_CPS = 0.60
43
- MI_WEIGHT_ES = 0.25
44
- MI_WEIGHT_PS = 0.15
45
- MI_GREEN_THRESHOLD = 0.65
46
- MI_YELLOW_THRESHOLD = 0.35
47
- MI_PRODUCTIVITY_TARGET = 0.2
48
-
49
-
50
- def compute_mi(used_tokens, context_window_size, cache_read, total_context,
51
- delta_lines, delta_output, beta=1.5):
52
- """Compute Model Intelligence score. Returns (mi, cps, es, ps)."""
53
- # Guard clause
54
- if context_window_size == 0:
55
- return (1.0, 1.0, 1.0, 0.5)
56
-
57
- # CPS
58
- u = used_tokens / context_window_size
59
- cps = max(0.0, 1.0 - u ** beta) if u > 0 else 1.0
60
-
61
- # ES
62
- if total_context == 0:
63
- es = 1.0
64
- else:
65
- cache_hit_ratio = cache_read / total_context
66
- es = 0.3 + 0.7 * cache_hit_ratio
67
-
68
- # PS
69
- if delta_output is None or delta_output <= 0:
70
- ps = 0.5
71
- else:
72
- ratio = delta_lines / delta_output
73
- normalized = min(1.0, ratio / MI_PRODUCTIVITY_TARGET)
74
- ps = 0.2 + 0.8 * normalized
75
-
76
- mi = MI_WEIGHT_CPS * cps + MI_WEIGHT_ES * es + MI_WEIGHT_PS * ps
77
- return (mi, cps, es, ps)
78
-
79
-
80
- def get_mi_color(mi):
81
- """Return ANSI color code for MI score."""
82
- if mi > MI_GREEN_THRESHOLD:
83
- return GREEN
84
- if mi > MI_YELLOW_THRESHOLD:
85
- return YELLOW
86
- return RED
87
-
88
-
89
- def maybe_rotate_state_file(state_file):
90
- """Rotate a state file if it exceeds ROTATION_THRESHOLD lines.
91
-
92
- Keeps the most recent ROTATION_KEEP lines via atomic temp-file + rename.
93
- """
94
- try:
95
- if not os.path.exists(state_file):
96
- return
97
- with open(state_file) as f:
98
- lines = f.readlines()
99
- if len(lines) <= ROTATION_THRESHOLD:
100
- return
101
- keep = lines[-ROTATION_KEEP:]
102
- fd, tmp_path = tempfile.mkstemp(dir=os.path.dirname(state_file), suffix=".tmp")
103
- try:
104
- with os.fdopen(fd, "w") as tmp_f:
105
- tmp_f.writelines(keep)
106
- os.replace(tmp_path, state_file)
107
- except BaseException:
108
- try:
109
- os.unlink(tmp_path)
110
- except OSError:
111
- pass
112
- raise
113
- except OSError as e:
114
- sys.stderr.write(f"[statusline] warning: failed to rotate state file: {e}\n")
115
-
116
-
117
- # ANSI Colors (defaults, overridable via config)
118
- BLUE = "\033[0;34m"
119
- MAGENTA = "\033[0;35m"
120
- CYAN = "\033[0;36m"
121
- GREEN = "\033[0;32m"
122
- YELLOW = "\033[0;33m"
123
- RED = "\033[0;31m"
124
- DIM = "\033[2m"
125
- RESET = "\033[0m"
126
-
127
- # Named colors for config parsing
128
- _COLOR_NAMES = {
129
- "black": "\033[0;30m",
130
- "red": "\033[0;31m",
131
- "green": "\033[0;32m",
132
- "yellow": "\033[0;33m",
133
- "blue": "\033[0;34m",
134
- "magenta": "\033[0;35m",
135
- "cyan": "\033[0;36m",
136
- "white": "\033[0;37m",
137
- "bright_black": "\033[0;90m",
138
- "bright_red": "\033[0;91m",
139
- "bright_green": "\033[0;92m",
140
- "bright_yellow": "\033[0;93m",
141
- "bright_blue": "\033[0;94m",
142
- "bright_magenta": "\033[0;95m",
143
- "bright_cyan": "\033[0;96m",
144
- "bright_white": "\033[0;97m",
145
- }
146
-
147
-
148
- def _parse_color(value):
149
- """Parse a color name or #rrggbb hex into an ANSI escape code."""
150
- value = value.strip().lower()
151
- if value in _COLOR_NAMES:
152
- return _COLOR_NAMES[value]
153
- if re.match(r"^#[0-9a-f]{6}$", value):
154
- r, g, b = int(value[1:3], 16), int(value[3:5], 16), int(value[5:7], 16)
155
- return f"\033[38;2;{r};{g};{b}m"
156
- return None
157
-
158
-
159
- # Color config keys and which color slot they map to
160
- _COLOR_KEYS = {
161
- "color_green": "green",
162
- "color_yellow": "yellow",
163
- "color_red": "red",
164
- "color_blue": "blue",
165
- "color_magenta": "magenta",
166
- "color_cyan": "cyan",
167
- }
168
-
169
- # Pattern to strip ANSI escape sequences
170
- _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
171
-
172
-
173
- def visible_width(s):
174
- """Return the visible width of a string after stripping ANSI escape sequences."""
175
- return len(_ANSI_RE.sub("", s))
176
-
177
-
178
- def get_terminal_width():
179
- """Return the terminal width in columns.
180
-
181
- When running inside Claude Code's statusline subprocess, neither $COLUMNS
182
- nor tput/shutil can detect the real terminal width (they always return 80).
183
- If COLUMNS is not explicitly set and shutil falls back to 80, we use a
184
- generous default of 200 so that no parts are unnecessarily dropped;
185
- Claude Code's own UI handles any overflow/truncation.
186
- """
187
- # If COLUMNS is explicitly set, trust it (real terminal or test override)
188
- if os.environ.get("COLUMNS"):
189
- return shutil.get_terminal_size().columns
190
- # No COLUMNS env var — likely a Claude Code subprocess with no real TTY.
191
- # shutil will fall back to 80, which is too narrow. Use 200 instead.
192
- cols = shutil.get_terminal_size(fallback=(200, 24)).columns
193
- return 200 if cols == 80 else cols
194
-
195
-
196
- def fit_to_width(parts, max_width):
197
- """Assemble parts into a single line that fits within max_width.
198
-
199
- Parts are added in priority order (first = highest priority).
200
- The first part (base) is always included. Subsequent parts are
201
- included only if adding them does not exceed max_width.
202
- Empty parts are skipped.
203
- """
204
- if not parts:
205
- return ""
206
-
207
- result = parts[0]
208
- current_width = visible_width(result)
209
-
210
- for part in parts[1:]:
211
- if not part:
212
- continue
213
- part_width = visible_width(part)
214
- if current_width + part_width <= max_width:
215
- result += part
216
- current_width += part_width
217
-
218
- return result
219
-
220
-
221
- def get_git_info(project_dir, magenta=None, cyan=None):
222
- """Get git branch and change count"""
223
- if magenta is None:
224
- magenta = MAGENTA
225
- if cyan is None:
226
- cyan = CYAN
227
- git_dir = os.path.join(project_dir, ".git")
228
- if not os.path.isdir(git_dir):
229
- return ""
230
-
231
- try:
232
- # Get branch name (skip optional locks for performance)
233
- result = subprocess.run(
234
- ["git", "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD"],
235
- cwd=project_dir,
236
- capture_output=True,
237
- text=True,
238
- timeout=5,
239
- )
240
- branch = result.stdout.strip()
241
-
242
- if not branch:
243
- return ""
244
-
245
- # Count changes
246
- result = subprocess.run(
247
- ["git", "--no-optional-locks", "status", "--porcelain"],
248
- cwd=project_dir,
249
- capture_output=True,
250
- text=True,
251
- timeout=5,
252
- )
253
- changes = len([line for line in result.stdout.split("\n") if line.strip()])
254
-
255
- if changes > 0:
256
- return f" | {magenta}{branch}{RESET} {cyan}[{changes}]{RESET}"
257
- return f" | {magenta}{branch}{RESET}"
258
- except (subprocess.TimeoutExpired, OSError):
259
- return ""
260
-
261
-
262
- def read_config():
263
- """Read settings from config file"""
264
- config = {
265
- "autocompact": True,
266
- "token_detail": True,
267
- "show_delta": True,
268
- "show_session": True,
269
- "show_io_tokens": True,
270
- "reduced_motion": False,
271
- "show_mi": True,
272
- "mi_curve_beta": 1.5,
273
- "colors": {},
274
- }
275
- config_path = os.path.expanduser("~/.claude/statusline.conf")
276
-
277
- # Create config file with defaults if it doesn't exist
278
- if not os.path.exists(config_path):
279
- try:
280
- os.makedirs(os.path.dirname(config_path), exist_ok=True)
281
- with open(config_path, "w") as f:
282
- f.write(
283
- """# Autocompact setting - sync with Claude Code's /config
284
- autocompact=true
285
-
286
- # Token display format
287
- token_detail=true
288
-
289
- # Show token delta since last refresh (adds file I/O on every refresh)
290
- # Disable if you don't need it to reduce overhead
291
- show_delta=true
292
-
293
- # Show session_id in status line
294
- show_session=true
295
-
296
- # Custom colors - use named colors or hex (#rrggbb)
297
- # Available: color_green, color_yellow, color_red, color_blue, color_magenta, color_cyan
298
- # Examples:
299
- # color_green=#7dcfff
300
- # color_red=#f7768e
301
- """
302
- )
303
- except Exception as e:
304
- sys.stderr.write(f"[statusline] warning: failed to create config: {e}\n")
305
- return config
306
-
307
- try:
308
- with open(config_path) as f:
309
- for line in f:
310
- line = line.strip()
311
- if line.startswith("#") or "=" not in line:
312
- continue
313
- key, value = line.split("=", 1)
314
- key = key.strip()
315
- raw_value = value.strip()
316
- value_lower = raw_value.lower()
317
- if key == "autocompact":
318
- config["autocompact"] = value_lower != "false"
319
- elif key == "token_detail":
320
- config["token_detail"] = value_lower != "false"
321
- elif key == "show_delta":
322
- config["show_delta"] = value_lower != "false"
323
- elif key == "show_session":
324
- config["show_session"] = value_lower != "false"
325
- elif key == "show_io_tokens":
326
- config["show_io_tokens"] = value_lower != "false"
327
- elif key == "reduced_motion":
328
- config["reduced_motion"] = value_lower != "false"
329
- elif key == "show_mi":
330
- config["show_mi"] = value_lower != "false"
331
- elif key == "mi_curve_beta":
332
- try:
333
- config["mi_curve_beta"] = float(raw_value)
334
- except ValueError:
335
- pass
336
- elif key in _COLOR_KEYS:
337
- ansi = _parse_color(raw_value)
338
- if ansi:
339
- config["colors"][_COLOR_KEYS[key]] = ansi
340
- except (OSError, UnicodeDecodeError) as e:
341
- sys.stderr.write(f"[statusline] warning: failed to read config: {e}\n")
342
- return config
343
-
344
-
345
- def main():
346
- try:
347
- data = json.load(sys.stdin)
348
- except json.JSONDecodeError:
349
- print("[Claude] ~")
350
- return
351
-
352
- # Extract data
353
- cwd = data.get("workspace", {}).get("current_dir", "~")
354
- project_dir = data.get("workspace", {}).get("project_dir", cwd)
355
- model = data.get("model", {}).get("display_name", "Claude")
356
- dir_name = os.path.basename(cwd) or "~"
357
-
358
- # Read settings from config file
359
- config = read_config()
360
- autocompact_enabled = config["autocompact"]
361
- token_detail = config["token_detail"]
362
- show_delta = config["show_delta"]
363
- show_session = config["show_session"]
364
- show_mi = config["show_mi"]
365
- mi_curve_beta = config["mi_curve_beta"]
366
- # Note: show_io_tokens setting is read but not yet implemented
367
-
368
- # Apply color overrides from config
369
- c = config.get("colors", {})
370
- c_green = c.get("green", GREEN)
371
- c_yellow = c.get("yellow", YELLOW)
372
- c_red = c.get("red", RED)
373
- c_blue = c.get("blue", BLUE)
374
- c_magenta = c.get("magenta", MAGENTA)
375
- c_cyan = c.get("cyan", CYAN)
376
-
377
- # Git info (pass configurable colors)
378
- git_info = get_git_info(project_dir, magenta=c_magenta, cyan=c_cyan)
379
-
380
- # Extract session_id once for reuse
381
- session_id = data.get("session_id")
382
-
383
- # Context window calculation
384
- context_info = ""
385
- ac_info = ""
386
- delta_info = ""
387
- mi_info = ""
388
- session_info = ""
389
- total_size = data.get("context_window", {}).get("context_window_size", 0)
390
- current_usage = data.get("context_window", {}).get("current_usage")
391
- total_input_tokens = data.get("context_window", {}).get("total_input_tokens", 0)
392
- total_output_tokens = data.get("context_window", {}).get("total_output_tokens", 0)
393
- cost_usd = data.get("cost", {}).get("total_cost_usd", 0)
394
- lines_added = data.get("cost", {}).get("total_lines_added", 0)
395
- lines_removed = data.get("cost", {}).get("total_lines_removed", 0)
396
- model_id = data.get("model", {}).get("id", "")
397
- workspace_project_dir = data.get("workspace", {}).get("project_dir", "")
398
-
399
- if total_size > 0 and current_usage:
400
- # Get tokens from current_usage (includes cache)
401
- input_tokens = current_usage.get("input_tokens", 0)
402
- cache_creation = current_usage.get("cache_creation_input_tokens", 0)
403
- cache_read = current_usage.get("cache_read_input_tokens", 0)
404
-
405
- # Total used from current request
406
- used_tokens = input_tokens + cache_creation + cache_read
407
-
408
- # Calculate autocompact buffer (22.5% of context window = 45k for 200k)
409
- autocompact_buffer = int(total_size * 0.225)
410
-
411
- # Free tokens calculation depends on autocompact setting
412
- if autocompact_enabled:
413
- # When AC enabled: subtract buffer to show actual usable space
414
- free_tokens = total_size - used_tokens - autocompact_buffer
415
- buffer_k = autocompact_buffer // 1000
416
- ac_info = f" {DIM}[AC:{buffer_k}k]{RESET}"
417
- else:
418
- # When AC disabled: show full free space
419
- free_tokens = total_size - used_tokens
420
- ac_info = f" {DIM}[AC:off]{RESET}"
421
-
422
- if free_tokens < 0:
423
- free_tokens = 0
424
-
425
- # Calculate percentage with one decimal (relative to total size)
426
- free_pct = (free_tokens * 100.0) / total_size
427
- free_pct_int = int(free_pct)
428
-
429
- # Format tokens based on token_detail setting
430
- if token_detail:
431
- free_display = f"{free_tokens:,}"
432
- else:
433
- free_display = f"{free_tokens / 1000:.1f}k"
434
-
435
- # Color based on free percentage
436
- if free_pct_int > 50:
437
- ctx_color = c_green
438
- elif free_pct_int > 25:
439
- ctx_color = c_yellow
440
- else:
441
- ctx_color = c_red
442
-
443
- context_info = f" | {ctx_color}{free_display} ({free_pct:.1f}%){RESET}"
444
-
445
- # Read previous entry if needed for delta OR MI
446
- if show_delta or show_mi:
447
- import glob
448
- import shutil
449
- import time
450
-
451
- state_dir = os.path.expanduser("~/.claude/statusline")
452
- os.makedirs(state_dir, exist_ok=True)
453
-
454
- old_state_dir = os.path.expanduser("~/.claude")
455
- for old_file in glob.glob(os.path.join(old_state_dir, "statusline*.state")):
456
- if os.path.isfile(old_file):
457
- new_file = os.path.join(state_dir, os.path.basename(old_file))
458
- if not os.path.exists(new_file):
459
- shutil.move(old_file, new_file)
460
- else:
461
- os.remove(old_file)
462
-
463
- if session_id:
464
- state_file = os.path.join(state_dir, f"statusline.{session_id}.state")
465
- else:
466
- state_file = os.path.join(state_dir, "statusline.state")
467
- has_prev = False
468
- prev_tokens = 0
469
- prev_lines_added = 0
470
- prev_lines_removed = 0
471
- prev_output_tokens = 0
472
- try:
473
- if os.path.exists(state_file):
474
- has_prev = True
475
- # Read last line to get previous state
476
- with open(state_file) as f:
477
- file_lines = f.readlines()
478
- if file_lines:
479
- last_line = file_lines[-1].strip()
480
- if "," in last_line:
481
- csv_parts = last_line.split(",")
482
- # Calculate previous context usage:
483
- # cur_input + cache_creation + cache_read
484
- # CSV indices: cur_in[3], cache_create[5], cache_read[6]
485
- prev_cur_input = int(csv_parts[3]) if len(csv_parts) > 3 else 0
486
- prev_cache_creation = int(csv_parts[5]) if len(csv_parts) > 5 else 0
487
- prev_cache_read = int(csv_parts[6]) if len(csv_parts) > 6 else 0
488
- prev_tokens = prev_cur_input + prev_cache_creation + prev_cache_read
489
- # For MI productivity score
490
- prev_output_tokens = int(csv_parts[2]) if len(csv_parts) > 2 else 0
491
- prev_lines_added = int(csv_parts[8]) if len(csv_parts) > 8 else 0
492
- prev_lines_removed = int(csv_parts[9]) if len(csv_parts) > 9 else 0
493
- else:
494
- # Old format - single value
495
- prev_tokens = int(last_line or 0)
496
- except (OSError, ValueError) as e:
497
- sys.stderr.write(f"[statusline] warning: failed to read state file: {e}\n")
498
- prev_tokens = 0
499
-
500
- # Calculate and display token delta if enabled
501
- if show_delta:
502
- delta = used_tokens - prev_tokens
503
- if has_prev and delta > 0:
504
- if token_detail:
505
- delta_display = f"{delta:,}"
506
- else:
507
- delta_display = f"{delta / 1000:.1f}k"
508
- delta_info = f" {DIM}[+{delta_display}]{RESET}"
509
-
510
- # Calculate and display MI score if enabled
511
- if show_mi:
512
- if has_prev:
513
- delta_la = lines_added - prev_lines_added
514
- delta_lr = lines_removed - prev_lines_removed
515
- delta_lines = delta_la + delta_lr
516
- delta_output = total_output_tokens - prev_output_tokens
517
- else:
518
- delta_lines = 0
519
- delta_output = None
520
- mi_val, _, _, _ = compute_mi(
521
- used_tokens, total_size, cache_read, used_tokens,
522
- delta_lines, delta_output, mi_curve_beta,
523
- )
524
- mi_color = get_mi_color(mi_val)
525
- mi_info = f" {mi_color}MI:{mi_val:.2f}{RESET}"
526
-
527
- # Only append if context usage changed (avoid duplicates from multiple refreshes)
528
- if not has_prev or used_tokens != prev_tokens:
529
- try:
530
- cur_input_tokens = current_usage.get("input_tokens", 0)
531
- cur_output_tokens = current_usage.get("output_tokens", 0)
532
- state_data = ",".join(
533
- str(x)
534
- for x in [
535
- int(time.time()),
536
- total_input_tokens,
537
- total_output_tokens,
538
- cur_input_tokens,
539
- cur_output_tokens,
540
- cache_creation,
541
- cache_read,
542
- cost_usd,
543
- lines_added,
544
- lines_removed,
545
- session_id or "",
546
- model_id,
547
- workspace_project_dir.replace(",", "_"),
548
- total_size,
549
- ]
550
- )
551
- with open(state_file, "a") as f:
552
- f.write(f"{state_data}\n")
553
- maybe_rotate_state_file(state_file)
554
- except OSError as e:
555
- sys.stderr.write(f"[statusline] warning: failed to write state file: {e}\n")
556
-
557
- # Display session_id if enabled
558
- if show_session and session_id:
559
- session_info = f" {DIM}{session_id}{RESET}"
560
-
561
- # Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
562
- base = f"{DIM}[{model}]{RESET} {c_blue}{dir_name}{RESET}"
563
- max_width = get_terminal_width()
564
- parts = [base, git_info, context_info, delta_info, mi_info, ac_info, session_info]
565
- print(fit_to_width(parts, max_width))
566
-
567
-
568
- if __name__ == "__main__":
569
- main()
@@ -1,11 +0,0 @@
1
- """Claude Code Context Stats.
2
-
3
- Never run out of context unexpectedly - monitor your session context in real-time.
4
- """
5
-
6
- __version__ = "1.8.0"
7
-
8
- from claude_statusline.core.config import Config
9
- from claude_statusline.core.state import StateFile
10
-
11
- __all__ = ["__version__", "Config", "StateFile"]
@@ -1,6 +0,0 @@
1
- """Enable running as python -m claude_statusline."""
2
-
3
- from claude_statusline.cli.statusline import main
4
-
5
- if __name__ == "__main__":
6
- main()
@@ -1 +0,0 @@
1
- """CLI entry points for claude-statusline."""