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