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.
Files changed (102) hide show
  1. package/package.json +9 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/scripts/statusline.js +128 -18
  4. package/.editorconfig +0 -60
  5. package/.eslintrc.json +0 -35
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  8. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  9. package/.github/dependabot.yml +0 -44
  10. package/.github/workflows/ci.yml +0 -294
  11. package/.github/workflows/release.yml +0 -151
  12. package/.pre-commit-config.yaml +0 -74
  13. package/.prettierrc +0 -33
  14. package/.shellcheckrc +0 -10
  15. package/CHANGELOG.md +0 -163
  16. package/CLAUDE.md +0 -66
  17. package/CODE_OF_CONDUCT.md +0 -59
  18. package/CONTRIBUTING.md +0 -240
  19. package/RELEASE_NOTES.md +0 -19
  20. package/SECURITY.md +0 -44
  21. package/TODOS.md +0 -72
  22. package/assets/logo/favicon.svg +0 -19
  23. package/assets/logo/logo-black.svg +0 -24
  24. package/assets/logo/logo-full.svg +0 -40
  25. package/assets/logo/logo-icon.svg +0 -27
  26. package/assets/logo/logo-mark.svg +0 -28
  27. package/assets/logo/logo-white.svg +0 -24
  28. package/assets/logo/logo-wordmark.svg +0 -6
  29. package/config/settings-example.json +0 -7
  30. package/config/settings-node.json +0 -7
  31. package/config/settings-python.json +0 -7
  32. package/docs/ARCHITECTURE.md +0 -128
  33. package/docs/CSV_FORMAT.md +0 -42
  34. package/docs/DEPLOYMENT.md +0 -71
  35. package/docs/DEVELOPMENT.md +0 -161
  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 -304
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -485
  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 -512
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -169
  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 -148
  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/renderer.py +0 -366
  75. package/src/claude_statusline/graphs/statistics.py +0 -92
  76. package/src/claude_statusline/ui/__init__.py +0 -1
  77. package/src/claude_statusline/ui/icons.py +0 -93
  78. package/src/claude_statusline/ui/waiting.py +0 -62
  79. package/tests/bash/test_delta_parity.bats +0 -199
  80. package/tests/bash/test_install.bats +0 -29
  81. package/tests/bash/test_parity.bats +0 -315
  82. package/tests/bash/test_statusline_full.bats +0 -139
  83. package/tests/bash/test_statusline_git.bats +0 -42
  84. package/tests/bash/test_statusline_minimal.bats +0 -37
  85. package/tests/fixtures/json/comma_in_path.json +0 -31
  86. package/tests/fixtures/json/high_usage.json +0 -17
  87. package/tests/fixtures/json/low_usage.json +0 -17
  88. package/tests/fixtures/json/medium_usage.json +0 -17
  89. package/tests/fixtures/json/valid_full.json +0 -30
  90. package/tests/fixtures/json/valid_minimal.json +0 -9
  91. package/tests/node/rotation.test.js +0 -89
  92. package/tests/node/statusline.test.js +0 -240
  93. package/tests/python/conftest.py +0 -84
  94. package/tests/python/test_colors.py +0 -105
  95. package/tests/python/test_config_colors.py +0 -78
  96. package/tests/python/test_data_pipeline.py +0 -446
  97. package/tests/python/test_explain.py +0 -177
  98. package/tests/python/test_icons.py +0 -152
  99. package/tests/python/test_layout.py +0 -127
  100. package/tests/python/test_state_rotation_validation.py +0 -232
  101. package/tests/python/test_statusline.py +0 -215
  102. 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()