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
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Context Stats Visualizer for Claude Code.
|
|
3
|
-
|
|
4
|
-
Displays ASCII graphs of token consumption over time.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
context-stats [session_id] [options]
|
|
8
|
-
|
|
9
|
-
Options:
|
|
10
|
-
--type <cumulative|delta|io|both|all> Graph type to display (default: both)
|
|
11
|
-
--watch, -w [interval] Real-time monitoring mode (default: 2s)
|
|
12
|
-
--no-color Disable color output
|
|
13
|
-
--version, -V Show version and exit
|
|
14
|
-
--help Show this help
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import argparse
|
|
20
|
-
import signal
|
|
21
|
-
import sys
|
|
22
|
-
import time
|
|
23
|
-
from pathlib import Path
|
|
24
|
-
|
|
25
|
-
from claude_statusline import __version__
|
|
26
|
-
from claude_statusline.core.colors import ColorManager
|
|
27
|
-
from claude_statusline.core.config import Config
|
|
28
|
-
from claude_statusline.core.state import StateFile, _validate_session_id
|
|
29
|
-
from claude_statusline.graphs.renderer import GraphDimensions, GraphRenderer
|
|
30
|
-
from claude_statusline.graphs.statistics import calculate_deltas
|
|
31
|
-
from claude_statusline.ui.icons import get_activity_tier, get_tier_label
|
|
32
|
-
from claude_statusline.ui.waiting import get_waiting_text, is_active
|
|
33
|
-
|
|
34
|
-
# Cursor control sequences
|
|
35
|
-
CURSOR_HOME = "\033[H"
|
|
36
|
-
CLEAR_SCREEN = "\033[2J"
|
|
37
|
-
HIDE_CURSOR = "\033[?25l"
|
|
38
|
-
SHOW_CURSOR = "\033[?25h"
|
|
39
|
-
CLEAR_TO_END = "\033[J"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def show_help() -> None:
|
|
43
|
-
"""Show help message."""
|
|
44
|
-
print(
|
|
45
|
-
"""Context Stats Visualizer for Claude Code
|
|
46
|
-
|
|
47
|
-
USAGE:
|
|
48
|
-
context-stats [session_id] [options]
|
|
49
|
-
context-stats explain
|
|
50
|
-
|
|
51
|
-
ARGUMENTS:
|
|
52
|
-
session_id Optional session ID. If not provided, uses the latest session.
|
|
53
|
-
|
|
54
|
-
COMMANDS:
|
|
55
|
-
explain Diagnostic dump of Claude Code's JSON context (pipe JSON to stdin)
|
|
56
|
-
|
|
57
|
-
OPTIONS:
|
|
58
|
-
--type <type> Graph type to display:
|
|
59
|
-
- delta: Context growth per interaction (default)
|
|
60
|
-
- cumulative: Total context usage over time
|
|
61
|
-
- io: Input/output tokens over time
|
|
62
|
-
- both: Show cumulative and delta graphs
|
|
63
|
-
- all: Show all graphs including I/O
|
|
64
|
-
-w [interval] Set refresh interval in seconds (default: 2)
|
|
65
|
-
--no-watch Show graphs once and exit (disable live monitoring)
|
|
66
|
-
--no-color Disable color output
|
|
67
|
-
--version, -V Show version and exit
|
|
68
|
-
--help Show this help message
|
|
69
|
-
|
|
70
|
-
NOTE:
|
|
71
|
-
By default, context-stats runs in live monitoring mode, refreshing every 2 seconds.
|
|
72
|
-
Press Ctrl+C to exit. Use --no-watch to display graphs once and exit.
|
|
73
|
-
|
|
74
|
-
EXAMPLES:
|
|
75
|
-
# Live monitoring (default, refreshes every 2s)
|
|
76
|
-
context-stats
|
|
77
|
-
|
|
78
|
-
# Live monitoring with custom interval
|
|
79
|
-
context-stats -w 5
|
|
80
|
-
|
|
81
|
-
# Show graphs once and exit
|
|
82
|
-
context-stats --no-watch
|
|
83
|
-
|
|
84
|
-
# Show graphs for specific session
|
|
85
|
-
context-stats abc123def
|
|
86
|
-
|
|
87
|
-
# Show only cumulative graph
|
|
88
|
-
context-stats --type cumulative
|
|
89
|
-
|
|
90
|
-
# Combine options
|
|
91
|
-
context-stats abc123 --type cumulative -w 3
|
|
92
|
-
|
|
93
|
-
# Output to file (no colors, single run)
|
|
94
|
-
context-stats --no-watch --no-color > output.txt
|
|
95
|
-
|
|
96
|
-
# Diagnostic dump (pipe Claude Code JSON context)
|
|
97
|
-
echo '{"model":{"display_name":"Opus"},...}' | context-stats explain
|
|
98
|
-
|
|
99
|
-
DATA SOURCE:
|
|
100
|
-
Reads token history from ~/.claude/statusline/statusline.<session_id>.state
|
|
101
|
-
"""
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def parse_args() -> argparse.Namespace:
|
|
106
|
-
"""Parse command-line arguments."""
|
|
107
|
-
parser = argparse.ArgumentParser(
|
|
108
|
-
description="Context Stats Visualizer for Claude Code",
|
|
109
|
-
add_help=False,
|
|
110
|
-
)
|
|
111
|
-
parser.add_argument("session_id", nargs="?", default=None, help="Session ID")
|
|
112
|
-
parser.add_argument(
|
|
113
|
-
"--type",
|
|
114
|
-
choices=["cumulative", "delta", "io", "both", "all"],
|
|
115
|
-
default="delta",
|
|
116
|
-
help="Graph type to display (default: delta)",
|
|
117
|
-
)
|
|
118
|
-
parser.add_argument(
|
|
119
|
-
"--watch",
|
|
120
|
-
"-w",
|
|
121
|
-
nargs="?",
|
|
122
|
-
const=2,
|
|
123
|
-
type=int,
|
|
124
|
-
default=2,
|
|
125
|
-
help="Watch mode interval in seconds (default: 2, use --no-watch to disable)",
|
|
126
|
-
)
|
|
127
|
-
parser.add_argument(
|
|
128
|
-
"--no-watch",
|
|
129
|
-
action="store_true",
|
|
130
|
-
help="Disable watch mode (show graphs once and exit)",
|
|
131
|
-
)
|
|
132
|
-
parser.add_argument(
|
|
133
|
-
"--no-color",
|
|
134
|
-
action="store_true",
|
|
135
|
-
help="Disable color output",
|
|
136
|
-
)
|
|
137
|
-
parser.add_argument(
|
|
138
|
-
"--help",
|
|
139
|
-
"-h",
|
|
140
|
-
action="store_true",
|
|
141
|
-
help="Show help message",
|
|
142
|
-
)
|
|
143
|
-
parser.add_argument(
|
|
144
|
-
"--version",
|
|
145
|
-
"-V",
|
|
146
|
-
action="store_true",
|
|
147
|
-
help="Show version and exit",
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
args = parser.parse_args()
|
|
151
|
-
|
|
152
|
-
if args.version:
|
|
153
|
-
print(f"cc-context-stats {__version__}")
|
|
154
|
-
sys.exit(0)
|
|
155
|
-
|
|
156
|
-
if args.help:
|
|
157
|
-
show_help()
|
|
158
|
-
sys.exit(0)
|
|
159
|
-
|
|
160
|
-
if args.session_id is not None:
|
|
161
|
-
try:
|
|
162
|
-
_validate_session_id(args.session_id)
|
|
163
|
-
except ValueError as e:
|
|
164
|
-
sys.stderr.write(f"Error: {e}\n")
|
|
165
|
-
sys.exit(1)
|
|
166
|
-
|
|
167
|
-
return args
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def render_once(
|
|
171
|
-
state_file: StateFile,
|
|
172
|
-
graph_type: str,
|
|
173
|
-
renderer: GraphRenderer,
|
|
174
|
-
colors: ColorManager,
|
|
175
|
-
watch_mode: bool = False,
|
|
176
|
-
config: Config | None = None,
|
|
177
|
-
cycle_index: int = 0,
|
|
178
|
-
) -> bool | str:
|
|
179
|
-
"""Render graphs once.
|
|
180
|
-
|
|
181
|
-
Args:
|
|
182
|
-
state_file: StateFile instance
|
|
183
|
-
graph_type: Type of graphs to render
|
|
184
|
-
renderer: GraphRenderer instance
|
|
185
|
-
colors: ColorManager instance
|
|
186
|
-
watch_mode: Whether running in watch mode
|
|
187
|
-
config: Config instance for motion settings
|
|
188
|
-
cycle_index: Watch mode refresh counter for rotating text
|
|
189
|
-
|
|
190
|
-
Returns:
|
|
191
|
-
True if rendering was successful (non-watch mode),
|
|
192
|
-
buffered string if watch_mode is True,
|
|
193
|
-
False if not enough data
|
|
194
|
-
"""
|
|
195
|
-
entries = state_file.read_history()
|
|
196
|
-
|
|
197
|
-
if len(entries) < 2:
|
|
198
|
-
msg = (
|
|
199
|
-
f"\n{colors.yellow}Need at least 2 data points to generate graphs.{colors.reset}\n"
|
|
200
|
-
f"{colors.dim}Found: {len(entries)} entry. Use Claude Code to accumulate more data.{colors.reset}"
|
|
201
|
-
)
|
|
202
|
-
if watch_mode:
|
|
203
|
-
return msg
|
|
204
|
-
print(msg)
|
|
205
|
-
return False
|
|
206
|
-
|
|
207
|
-
# In watch mode, buffer all output
|
|
208
|
-
lines: list[str] = []
|
|
209
|
-
|
|
210
|
-
def emit(line: str = "") -> None:
|
|
211
|
-
if watch_mode:
|
|
212
|
-
lines.append(line)
|
|
213
|
-
else:
|
|
214
|
-
print(line)
|
|
215
|
-
|
|
216
|
-
# Extract data for graphs
|
|
217
|
-
timestamps = [e.timestamp for e in entries]
|
|
218
|
-
# Current context window usage (what's actually in the context)
|
|
219
|
-
# This is: cache_read + cache_creation + current_input_tokens
|
|
220
|
-
context_used = [e.current_used_tokens for e in entries]
|
|
221
|
-
# Per-request I/O tokens from current_usage
|
|
222
|
-
current_input = [e.current_input_tokens for e in entries]
|
|
223
|
-
current_output = [e.current_output_tokens for e in entries]
|
|
224
|
-
deltas = calculate_deltas(context_used)
|
|
225
|
-
delta_times = timestamps[1:] # Deltas start from second entry
|
|
226
|
-
|
|
227
|
-
# Get session name and project from entries
|
|
228
|
-
file_path = state_file.find_latest_state_file()
|
|
229
|
-
session_name = file_path.stem.replace("statusline.", "") if file_path else "unknown"
|
|
230
|
-
|
|
231
|
-
# Get project name from the last entry (most recent)
|
|
232
|
-
last_entry = entries[-1]
|
|
233
|
-
project_name = ""
|
|
234
|
-
if last_entry.workspace_project_dir:
|
|
235
|
-
# Extract just the project folder name from the path
|
|
236
|
-
project_name = Path(last_entry.workspace_project_dir).name
|
|
237
|
-
|
|
238
|
-
# Header
|
|
239
|
-
if not watch_mode:
|
|
240
|
-
emit()
|
|
241
|
-
if project_name:
|
|
242
|
-
emit(
|
|
243
|
-
f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
|
|
244
|
-
f"{colors.dim}({colors.cyan}{project_name}{colors.dim} • {session_name}){colors.reset}"
|
|
245
|
-
)
|
|
246
|
-
else:
|
|
247
|
-
emit(
|
|
248
|
-
f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
|
|
249
|
-
f"{colors.dim}(Session: {session_name}){colors.reset}"
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
# Activity indicator (waiting text + label)
|
|
253
|
-
reduced_motion = config.reduced_motion if config else False
|
|
254
|
-
tier = get_activity_tier(entries, last_entry.context_window_size)
|
|
255
|
-
label = get_tier_label(tier)
|
|
256
|
-
active = is_active(entries)
|
|
257
|
-
|
|
258
|
-
if active:
|
|
259
|
-
text = get_waiting_text(cycle_index, reduced_motion)
|
|
260
|
-
emit(f" {colors.dim}{text} [{label}]{colors.reset}")
|
|
261
|
-
else:
|
|
262
|
-
emit(f" {colors.dim}{label}{colors.reset}")
|
|
263
|
-
|
|
264
|
-
# In watch mode, enable renderer buffering
|
|
265
|
-
if watch_mode:
|
|
266
|
-
renderer.begin_buffering()
|
|
267
|
-
|
|
268
|
-
# Render requested graphs
|
|
269
|
-
if graph_type in ("cumulative", "both", "all"):
|
|
270
|
-
renderer.render_timeseries(
|
|
271
|
-
context_used, timestamps, "Context Usage Over Time", colors.green
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
if graph_type in ("delta", "both", "all"):
|
|
275
|
-
renderer.render_timeseries(
|
|
276
|
-
deltas, delta_times, "Context Growth Per Interaction", colors.cyan
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
if graph_type in ("io", "all"):
|
|
280
|
-
renderer.render_timeseries(
|
|
281
|
-
current_input, timestamps, "Input Tokens (per request)", colors.blue
|
|
282
|
-
)
|
|
283
|
-
renderer.render_timeseries(
|
|
284
|
-
current_output, timestamps, "Output Tokens (per request)", colors.magenta
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Summary and footer
|
|
288
|
-
renderer.render_summary(entries, deltas)
|
|
289
|
-
renderer.render_footer(__version__)
|
|
290
|
-
|
|
291
|
-
if watch_mode:
|
|
292
|
-
# Collect renderer buffer and combine with header lines
|
|
293
|
-
renderer_output = renderer.get_buffer()
|
|
294
|
-
lines.append(renderer_output)
|
|
295
|
-
return "\n".join(lines)
|
|
296
|
-
|
|
297
|
-
return True
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def run_watch_mode(
|
|
301
|
-
state_file: StateFile,
|
|
302
|
-
graph_type: str,
|
|
303
|
-
interval: int,
|
|
304
|
-
renderer: GraphRenderer,
|
|
305
|
-
colors: ColorManager,
|
|
306
|
-
config: Config | None = None,
|
|
307
|
-
) -> None:
|
|
308
|
-
"""Run in watch mode with continuous refresh.
|
|
309
|
-
|
|
310
|
-
Args:
|
|
311
|
-
state_file: StateFile instance
|
|
312
|
-
graph_type: Type of graphs to render
|
|
313
|
-
interval: Refresh interval in seconds
|
|
314
|
-
renderer: GraphRenderer instance
|
|
315
|
-
colors: ColorManager instance
|
|
316
|
-
config: Config instance for motion settings
|
|
317
|
-
"""
|
|
318
|
-
|
|
319
|
-
# Signal handler for clean exit
|
|
320
|
-
def handle_signal(_signum: int, _frame: object) -> None:
|
|
321
|
-
sys.stdout.write(SHOW_CURSOR)
|
|
322
|
-
sys.stdout.flush()
|
|
323
|
-
print(f"\n{colors.dim}Watch mode stopped.{colors.reset}")
|
|
324
|
-
sys.exit(0)
|
|
325
|
-
|
|
326
|
-
signal.signal(signal.SIGINT, handle_signal)
|
|
327
|
-
signal.signal(signal.SIGTERM, handle_signal)
|
|
328
|
-
|
|
329
|
-
# Hide cursor and initial clear in one write
|
|
330
|
-
sys.stdout.write(f"{HIDE_CURSOR}{CLEAR_SCREEN}{CURSOR_HOME}")
|
|
331
|
-
sys.stdout.flush()
|
|
332
|
-
|
|
333
|
-
cycle_counter = 0
|
|
334
|
-
|
|
335
|
-
try:
|
|
336
|
-
while True:
|
|
337
|
-
# Update dimensions in case of terminal resize
|
|
338
|
-
renderer.dimensions = GraphDimensions.detect()
|
|
339
|
-
|
|
340
|
-
# Build all output into a buffer
|
|
341
|
-
buf_lines: list[str] = []
|
|
342
|
-
|
|
343
|
-
# Watch mode indicator
|
|
344
|
-
current_time = time.strftime("%H:%M:%S")
|
|
345
|
-
buf_lines.append(
|
|
346
|
-
f"{colors.dim}[LIVE {current_time}] Refresh: {interval}s | Ctrl+C to exit{colors.reset}"
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Check if state file exists now (may have been created since start)
|
|
350
|
-
file_path = state_file.find_latest_state_file()
|
|
351
|
-
if not file_path or not file_path.exists():
|
|
352
|
-
# Show waiting message for new session
|
|
353
|
-
reduced_motion = config.reduced_motion if config else False
|
|
354
|
-
text = get_waiting_text(cycle_counter, reduced_motion)
|
|
355
|
-
buf_lines.append(
|
|
356
|
-
_format_waiting_message(
|
|
357
|
-
colors,
|
|
358
|
-
state_file.session_id,
|
|
359
|
-
text,
|
|
360
|
-
)
|
|
361
|
-
)
|
|
362
|
-
else:
|
|
363
|
-
# Render graphs (returns buffered string in watch mode)
|
|
364
|
-
result = render_once(
|
|
365
|
-
state_file,
|
|
366
|
-
graph_type,
|
|
367
|
-
renderer,
|
|
368
|
-
colors,
|
|
369
|
-
watch_mode=True,
|
|
370
|
-
config=config,
|
|
371
|
-
cycle_index=cycle_counter,
|
|
372
|
-
)
|
|
373
|
-
if isinstance(result, str):
|
|
374
|
-
buf_lines.append(result)
|
|
375
|
-
|
|
376
|
-
# Atomic write: CURSOR_HOME + content + CLEAR_TO_END (clean up stale trailing lines)
|
|
377
|
-
buffered_content = "\n".join(buf_lines)
|
|
378
|
-
sys.stdout.write(f"{CURSOR_HOME}{buffered_content}\n{CLEAR_TO_END}")
|
|
379
|
-
sys.stdout.flush()
|
|
380
|
-
|
|
381
|
-
cycle_counter += 1
|
|
382
|
-
time.sleep(interval)
|
|
383
|
-
finally:
|
|
384
|
-
sys.stdout.write(SHOW_CURSOR)
|
|
385
|
-
sys.stdout.flush()
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def _format_waiting_message(
|
|
389
|
-
colors: ColorManager,
|
|
390
|
-
session_id: str | None,
|
|
391
|
-
message: str = "Waiting for session data...",
|
|
392
|
-
) -> str:
|
|
393
|
-
"""Format a waiting message as a string.
|
|
394
|
-
|
|
395
|
-
Args:
|
|
396
|
-
colors: ColorManager instance
|
|
397
|
-
session_id: Session ID if specified
|
|
398
|
-
message: Message to display
|
|
399
|
-
|
|
400
|
-
Returns:
|
|
401
|
-
Formatted waiting message string
|
|
402
|
-
"""
|
|
403
|
-
lines = [""]
|
|
404
|
-
if session_id:
|
|
405
|
-
lines.append(
|
|
406
|
-
f"{colors.bold}{colors.magenta}Context Stats{colors.reset} "
|
|
407
|
-
f"{colors.dim}(Session: {session_id}){colors.reset}"
|
|
408
|
-
)
|
|
409
|
-
else:
|
|
410
|
-
lines.append(f"{colors.bold}{colors.magenta}Context Stats{colors.reset}")
|
|
411
|
-
lines.append("")
|
|
412
|
-
lines.append(f" {colors.cyan}⏳ {message}{colors.reset}")
|
|
413
|
-
lines.append("")
|
|
414
|
-
lines.append(
|
|
415
|
-
f" {colors.dim}The session has just started and no data has been recorded yet.{colors.reset}"
|
|
416
|
-
)
|
|
417
|
-
lines.append(
|
|
418
|
-
f" {colors.dim}Data will appear after the first Claude interaction.{colors.reset}"
|
|
419
|
-
)
|
|
420
|
-
lines.append("")
|
|
421
|
-
return "\n".join(lines)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
def show_waiting_message(
|
|
425
|
-
colors: ColorManager,
|
|
426
|
-
session_id: str | None,
|
|
427
|
-
message: str = "Waiting for session data...",
|
|
428
|
-
) -> None:
|
|
429
|
-
"""Show a friendly waiting message for new sessions.
|
|
430
|
-
|
|
431
|
-
Args:
|
|
432
|
-
colors: ColorManager instance
|
|
433
|
-
session_id: Session ID if specified
|
|
434
|
-
message: Message to display
|
|
435
|
-
"""
|
|
436
|
-
print(_format_waiting_message(colors, session_id, message))
|
|
437
|
-
|
|
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
|
-
|
|
447
|
-
def main() -> None:
|
|
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
|
-
|
|
467
|
-
args = parse_args()
|
|
468
|
-
|
|
469
|
-
# Load config for token_detail setting
|
|
470
|
-
config = Config.load()
|
|
471
|
-
|
|
472
|
-
# Setup colors with any user overrides from config
|
|
473
|
-
color_enabled = not args.no_color and sys.stdout.isatty()
|
|
474
|
-
colors = ColorManager(enabled=color_enabled, overrides=config.color_overrides)
|
|
475
|
-
|
|
476
|
-
# Setup state file
|
|
477
|
-
state_file = StateFile(args.session_id)
|
|
478
|
-
|
|
479
|
-
# Find state file
|
|
480
|
-
file_path = state_file.find_latest_state_file()
|
|
481
|
-
|
|
482
|
-
# Handle case where no state file exists yet
|
|
483
|
-
if not file_path or not file_path.exists():
|
|
484
|
-
if args.no_watch:
|
|
485
|
-
# Single run mode - show friendly message and exit
|
|
486
|
-
if args.session_id:
|
|
487
|
-
show_waiting_message(colors, args.session_id)
|
|
488
|
-
else:
|
|
489
|
-
print(f"{colors.yellow}No session data found.{colors.reset}")
|
|
490
|
-
print(f"{colors.dim}Run Claude Code to generate token usage data.{colors.reset}")
|
|
491
|
-
sys.exit(0)
|
|
492
|
-
else:
|
|
493
|
-
# Watch mode - continue and wait for data
|
|
494
|
-
pass
|
|
495
|
-
|
|
496
|
-
# Setup renderer
|
|
497
|
-
renderer = GraphRenderer(
|
|
498
|
-
colors=colors,
|
|
499
|
-
token_detail=config.token_detail,
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
# Run
|
|
503
|
-
if args.no_watch:
|
|
504
|
-
success = render_once(state_file, args.type, renderer, colors, config=config)
|
|
505
|
-
if not success:
|
|
506
|
-
sys.exit(1)
|
|
507
|
-
else:
|
|
508
|
-
run_watch_mode(state_file, args.type, args.watch, renderer, colors, config=config)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if __name__ == "__main__":
|
|
512
|
-
main()
|