cc-context-stats 1.6.1 → 1.7.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/CHANGELOG.md +30 -0
- package/CLAUDE.md +12 -0
- package/README.md +35 -25
- package/docs/ARCHITECTURE.md +52 -25
- package/docs/CSV_FORMAT.md +2 -0
- package/docs/DEPLOYMENT.md +19 -8
- package/docs/DEVELOPMENT.md +48 -12
- package/docs/configuration.md +35 -0
- package/docs/context-stats.md +12 -1
- package/docs/installation.md +82 -22
- package/docs/scripts.md +47 -23
- package/docs/troubleshooting.md +93 -4
- package/images/v1.6.1.png +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/statusline.js +87 -15
- package/scripts/statusline.py +169 -51
- package/src/claude_statusline/__init__.py +1 -1
- package/src/claude_statusline/cli/context_stats.py +52 -10
- package/src/claude_statusline/cli/explain.py +228 -0
- package/src/claude_statusline/cli/statusline.py +16 -20
- package/src/claude_statusline/core/colors.py +78 -9
- package/src/claude_statusline/core/config.py +51 -9
- package/src/claude_statusline/core/git.py +16 -5
- package/tests/bash/test_delta_parity.bats +199 -0
- package/tests/python/test_colors.py +105 -0
- package/tests/python/test_config_colors.py +78 -0
- package/tests/python/test_explain.py +177 -0
package/scripts/statusline.py
CHANGED
|
@@ -21,7 +21,10 @@ Create/edit ~/.claude/statusline.conf and set:
|
|
|
21
21
|
When AC is enabled, 22.5% of context window is reserved for autocompact buffer.
|
|
22
22
|
|
|
23
23
|
State file format (CSV):
|
|
24
|
-
timestamp,total_input_tokens,total_output_tokens,current_usage_input_tokens,
|
|
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
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
import json
|
|
@@ -30,8 +33,41 @@ import re
|
|
|
30
33
|
import shutil
|
|
31
34
|
import subprocess
|
|
32
35
|
import sys
|
|
36
|
+
import tempfile
|
|
33
37
|
|
|
34
|
-
|
|
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)
|
|
35
71
|
BLUE = "\033[0;34m"
|
|
36
72
|
MAGENTA = "\033[0;35m"
|
|
37
73
|
CYAN = "\033[0;36m"
|
|
@@ -41,6 +77,48 @@ RED = "\033[0;31m"
|
|
|
41
77
|
DIM = "\033[2m"
|
|
42
78
|
RESET = "\033[0m"
|
|
43
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
|
+
|
|
44
122
|
# Pattern to strip ANSI escape sequences
|
|
45
123
|
_ANSI_RE = re.compile(r"\033\[[0-9;]*m")
|
|
46
124
|
|
|
@@ -93,8 +171,12 @@ def fit_to_width(parts, max_width):
|
|
|
93
171
|
return result
|
|
94
172
|
|
|
95
173
|
|
|
96
|
-
def get_git_info(project_dir):
|
|
174
|
+
def get_git_info(project_dir, magenta=None, cyan=None):
|
|
97
175
|
"""Get git branch and change count"""
|
|
176
|
+
if magenta is None:
|
|
177
|
+
magenta = MAGENTA
|
|
178
|
+
if cyan is None:
|
|
179
|
+
cyan = CYAN
|
|
98
180
|
git_dir = os.path.join(project_dir, ".git")
|
|
99
181
|
if not os.path.isdir(git_dir):
|
|
100
182
|
return ""
|
|
@@ -106,6 +188,7 @@ def get_git_info(project_dir):
|
|
|
106
188
|
cwd=project_dir,
|
|
107
189
|
capture_output=True,
|
|
108
190
|
text=True,
|
|
191
|
+
timeout=5,
|
|
109
192
|
)
|
|
110
193
|
branch = result.stdout.strip()
|
|
111
194
|
|
|
@@ -118,13 +201,14 @@ def get_git_info(project_dir):
|
|
|
118
201
|
cwd=project_dir,
|
|
119
202
|
capture_output=True,
|
|
120
203
|
text=True,
|
|
204
|
+
timeout=5,
|
|
121
205
|
)
|
|
122
206
|
changes = len([line for line in result.stdout.split("\n") if line.strip()])
|
|
123
207
|
|
|
124
208
|
if changes > 0:
|
|
125
|
-
return f" | {
|
|
126
|
-
return f" | {
|
|
127
|
-
except
|
|
209
|
+
return f" | {magenta}{branch}{RESET} {cyan}[{changes}]{RESET}"
|
|
210
|
+
return f" | {magenta}{branch}{RESET}"
|
|
211
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
128
212
|
return ""
|
|
129
213
|
|
|
130
214
|
|
|
@@ -136,6 +220,7 @@ def read_config():
|
|
|
136
220
|
"show_delta": True,
|
|
137
221
|
"show_session": True,
|
|
138
222
|
"show_io_tokens": True,
|
|
223
|
+
"colors": {},
|
|
139
224
|
}
|
|
140
225
|
config_path = os.path.expanduser("~/.claude/statusline.conf")
|
|
141
226
|
|
|
@@ -157,6 +242,12 @@ show_delta=true
|
|
|
157
242
|
|
|
158
243
|
# Show session_id in status line
|
|
159
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
|
|
160
251
|
"""
|
|
161
252
|
)
|
|
162
253
|
except Exception as e:
|
|
@@ -171,17 +262,22 @@ show_session=true
|
|
|
171
262
|
continue
|
|
172
263
|
key, value = line.split("=", 1)
|
|
173
264
|
key = key.strip()
|
|
174
|
-
|
|
265
|
+
raw_value = value.strip()
|
|
266
|
+
value_lower = raw_value.lower()
|
|
175
267
|
if key == "autocompact":
|
|
176
|
-
config["autocompact"] =
|
|
268
|
+
config["autocompact"] = value_lower != "false"
|
|
177
269
|
elif key == "token_detail":
|
|
178
|
-
config["token_detail"] =
|
|
270
|
+
config["token_detail"] = value_lower != "false"
|
|
179
271
|
elif key == "show_delta":
|
|
180
|
-
config["show_delta"] =
|
|
272
|
+
config["show_delta"] = value_lower != "false"
|
|
181
273
|
elif key == "show_session":
|
|
182
|
-
config["show_session"] =
|
|
274
|
+
config["show_session"] = value_lower != "false"
|
|
183
275
|
elif key == "show_io_tokens":
|
|
184
|
-
config["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
|
|
185
281
|
except (OSError, UnicodeDecodeError) as e:
|
|
186
282
|
sys.stderr.write(f"[statusline] warning: failed to read config: {e}\n")
|
|
187
283
|
return config
|
|
@@ -200,9 +296,6 @@ def main():
|
|
|
200
296
|
model = data.get("model", {}).get("display_name", "Claude")
|
|
201
297
|
dir_name = os.path.basename(cwd) or "~"
|
|
202
298
|
|
|
203
|
-
# Git info
|
|
204
|
-
git_info = get_git_info(project_dir)
|
|
205
|
-
|
|
206
299
|
# Read settings from config file
|
|
207
300
|
config = read_config()
|
|
208
301
|
autocompact_enabled = config["autocompact"]
|
|
@@ -211,6 +304,18 @@ def main():
|
|
|
211
304
|
show_session = config["show_session"]
|
|
212
305
|
# Note: show_io_tokens setting is read but not yet implemented
|
|
213
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
|
+
|
|
214
319
|
# Extract session_id once for reuse
|
|
215
320
|
session_id = data.get("session_id")
|
|
216
321
|
|
|
@@ -267,11 +372,11 @@ def main():
|
|
|
267
372
|
|
|
268
373
|
# Color based on free percentage
|
|
269
374
|
if free_pct_int > 50:
|
|
270
|
-
ctx_color =
|
|
375
|
+
ctx_color = c_green
|
|
271
376
|
elif free_pct_int > 25:
|
|
272
|
-
ctx_color =
|
|
377
|
+
ctx_color = c_yellow
|
|
273
378
|
else:
|
|
274
|
-
ctx_color =
|
|
379
|
+
ctx_color = c_red
|
|
275
380
|
|
|
276
381
|
context_info = f" | {ctx_color}{free_display} free ({free_pct:.1f}%){RESET}"
|
|
277
382
|
|
|
@@ -302,19 +407,27 @@ def main():
|
|
|
302
407
|
try:
|
|
303
408
|
if os.path.exists(state_file):
|
|
304
409
|
has_prev = True
|
|
305
|
-
# Read last line to get previous
|
|
410
|
+
# Read last line to get previous context usage
|
|
306
411
|
with open(state_file) as f:
|
|
307
412
|
lines = f.readlines()
|
|
308
413
|
if lines:
|
|
309
414
|
last_line = lines[-1].strip()
|
|
310
415
|
if "," in last_line:
|
|
311
|
-
|
|
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
|
|
312
424
|
else:
|
|
425
|
+
# Old format - single value
|
|
313
426
|
prev_tokens = int(last_line or 0)
|
|
314
|
-
except
|
|
427
|
+
except (OSError, ValueError) as e:
|
|
315
428
|
sys.stderr.write(f"[statusline] warning: failed to read state file: {e}\n")
|
|
316
429
|
prev_tokens = 0
|
|
317
|
-
# Calculate delta
|
|
430
|
+
# Calculate delta (difference in context window usage)
|
|
318
431
|
delta = used_tokens - prev_tokens
|
|
319
432
|
# Only show positive delta (and skip first run when no previous state)
|
|
320
433
|
if has_prev and delta > 0:
|
|
@@ -323,41 +436,46 @@ def main():
|
|
|
323
436
|
else:
|
|
324
437
|
delta_display = f"{delta / 1000:.1f}k"
|
|
325
438
|
delta_info = f" {DIM}[+{delta_display}]{RESET}"
|
|
326
|
-
#
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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")
|
|
354
472
|
|
|
355
473
|
# Display session_id if enabled
|
|
356
474
|
if show_session and session_id:
|
|
357
475
|
session_info = f" {DIM}{session_id}{RESET}"
|
|
358
476
|
|
|
359
477
|
# Output: [Model] directory | branch [changes] | XXk free (XX%) [+delta] [AC] [S:session_id]
|
|
360
|
-
base = f"{DIM}[{model}]{RESET} {
|
|
478
|
+
base = f"{DIM}[{model}]{RESET} {c_blue}{dir_name}{RESET}"
|
|
361
479
|
max_width = get_terminal_width()
|
|
362
480
|
parts = [base, git_info, context_info, delta_info, ac_info, session_info]
|
|
363
481
|
print(fit_to_width(parts, max_width))
|
|
@@ -46,10 +46,14 @@ def show_help() -> None:
|
|
|
46
46
|
|
|
47
47
|
USAGE:
|
|
48
48
|
context-stats [session_id] [options]
|
|
49
|
+
context-stats explain
|
|
49
50
|
|
|
50
51
|
ARGUMENTS:
|
|
51
52
|
session_id Optional session ID. If not provided, uses the latest session.
|
|
52
53
|
|
|
54
|
+
COMMANDS:
|
|
55
|
+
explain Diagnostic dump of Claude Code's JSON context (pipe JSON to stdin)
|
|
56
|
+
|
|
53
57
|
OPTIONS:
|
|
54
58
|
--type <type> Graph type to display:
|
|
55
59
|
- delta: Context growth per interaction (default)
|
|
@@ -89,6 +93,9 @@ EXAMPLES:
|
|
|
89
93
|
# Output to file (no colors, single run)
|
|
90
94
|
context-stats --no-watch --no-color > output.txt
|
|
91
95
|
|
|
96
|
+
# Diagnostic dump (pipe Claude Code JSON context)
|
|
97
|
+
echo '{"model":{"display_name":"Opus"},...}' | context-stats explain
|
|
98
|
+
|
|
92
99
|
DATA SOURCE:
|
|
93
100
|
Reads token history from ~/.claude/statusline/statusline.<session_id>.state
|
|
94
101
|
"""
|
|
@@ -345,16 +352,23 @@ def run_watch_mode(
|
|
|
345
352
|
# Show waiting message for new session
|
|
346
353
|
reduced_motion = config.reduced_motion if config else False
|
|
347
354
|
text = get_waiting_text(cycle_counter, reduced_motion)
|
|
348
|
-
buf_lines.append(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
355
|
+
buf_lines.append(
|
|
356
|
+
_format_waiting_message(
|
|
357
|
+
colors,
|
|
358
|
+
state_file.session_id,
|
|
359
|
+
text,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
353
362
|
else:
|
|
354
363
|
# Render graphs (returns buffered string in watch mode)
|
|
355
364
|
result = render_once(
|
|
356
|
-
state_file,
|
|
357
|
-
|
|
365
|
+
state_file,
|
|
366
|
+
graph_type,
|
|
367
|
+
renderer,
|
|
368
|
+
colors,
|
|
369
|
+
watch_mode=True,
|
|
370
|
+
config=config,
|
|
371
|
+
cycle_index=cycle_counter,
|
|
358
372
|
)
|
|
359
373
|
if isinstance(result, str):
|
|
360
374
|
buf_lines.append(result)
|
|
@@ -400,7 +414,9 @@ def _format_waiting_message(
|
|
|
400
414
|
lines.append(
|
|
401
415
|
f" {colors.dim}The session has just started and no data has been recorded yet.{colors.reset}"
|
|
402
416
|
)
|
|
403
|
-
lines.append(
|
|
417
|
+
lines.append(
|
|
418
|
+
f" {colors.dim}Data will appear after the first Claude interaction.{colors.reset}"
|
|
419
|
+
)
|
|
404
420
|
lines.append("")
|
|
405
421
|
return "\n".join(lines)
|
|
406
422
|
|
|
@@ -420,16 +436,42 @@ def show_waiting_message(
|
|
|
420
436
|
print(_format_waiting_message(colors, session_id, message))
|
|
421
437
|
|
|
422
438
|
|
|
439
|
+
def _ensure_utf8_stdout() -> None:
|
|
440
|
+
"""Reconfigure stdout/stderr to UTF-8 on Windows where cp1252 is the default."""
|
|
441
|
+
if sys.stdout.encoding and sys.stdout.encoding.lower().replace("-", "") != "utf8":
|
|
442
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
443
|
+
if sys.stderr.encoding and sys.stderr.encoding.lower().replace("-", "") != "utf8":
|
|
444
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
445
|
+
|
|
446
|
+
|
|
423
447
|
def main() -> None:
|
|
424
448
|
"""Main entry point for context-stats CLI."""
|
|
449
|
+
_ensure_utf8_stdout()
|
|
450
|
+
|
|
451
|
+
# Handle 'explain' subcommand before argparse (it expects stdin JSON, not flags)
|
|
452
|
+
if len(sys.argv) > 1 and sys.argv[1] == "explain":
|
|
453
|
+
import json
|
|
454
|
+
|
|
455
|
+
from claude_statusline.cli.explain import run_explain
|
|
456
|
+
|
|
457
|
+
no_color = "--no-color" in sys.argv
|
|
458
|
+
try:
|
|
459
|
+
data = json.load(sys.stdin)
|
|
460
|
+
except json.JSONDecodeError as e:
|
|
461
|
+
sys.stderr.write(f"Error: invalid JSON on stdin: {e}\n")
|
|
462
|
+
sys.stderr.write("Usage: echo '{...}' | context-stats explain\n")
|
|
463
|
+
sys.exit(1)
|
|
464
|
+
run_explain(data, no_color=no_color)
|
|
465
|
+
return
|
|
466
|
+
|
|
425
467
|
args = parse_args()
|
|
426
468
|
|
|
427
469
|
# Load config for token_detail setting
|
|
428
470
|
config = Config.load()
|
|
429
471
|
|
|
430
|
-
# Setup colors
|
|
472
|
+
# Setup colors with any user overrides from config
|
|
431
473
|
color_enabled = not args.no_color and sys.stdout.isatty()
|
|
432
|
-
colors = ColorManager(enabled=color_enabled)
|
|
474
|
+
colors = ColorManager(enabled=color_enabled, overrides=config.color_overrides)
|
|
433
475
|
|
|
434
476
|
# Setup state file
|
|
435
477
|
state_file = StateFile(args.session_id)
|