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,215 +0,0 @@
1
- """Tests for statusline.py script."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import re
8
- import subprocess
9
- import sys
10
- from pathlib import Path
11
-
12
- SCRIPT_PATH = Path(__file__).parent.parent.parent / "scripts" / "statusline.py"
13
-
14
- _ANSI_RE = re.compile(r"\033\[[0-9;]*m")
15
-
16
-
17
- def strip_ansi(s: str) -> str:
18
- """Strip ANSI escape sequences from a string."""
19
- return _ANSI_RE.sub("", s)
20
-
21
-
22
- def run_script(input_data: dict, env_overrides: dict | None = None) -> tuple[str, int]:
23
- """Run the statusline.py script with the given input.
24
-
25
- Args:
26
- input_data: JSON-serializable dict to pass as stdin.
27
- env_overrides: Optional dict of environment variable overrides.
28
-
29
- Returns:
30
- Tuple of (stdout, return_code)
31
- """
32
- env = os.environ.copy()
33
- env["PYTHONUTF8"] = "1"
34
- if env_overrides:
35
- env.update(env_overrides)
36
- result = subprocess.run(
37
- [sys.executable, str(SCRIPT_PATH)],
38
- input=json.dumps(input_data),
39
- capture_output=True,
40
- text=True,
41
- env=env,
42
- )
43
- return result.stdout.strip(), result.returncode
44
-
45
-
46
- class TestStatuslineScript:
47
- """Tests for the statusline.py script execution."""
48
-
49
- def test_script_exists(self):
50
- """Script file should exist."""
51
- assert SCRIPT_PATH.exists()
52
-
53
- def test_script_is_python(self):
54
- """Script should have Python shebang."""
55
- content = SCRIPT_PATH.read_text(encoding="utf-8")
56
- assert content.startswith("#!/usr/bin/env python3")
57
-
58
- def test_outputs_model_name(self, sample_input):
59
- """Should output the model name."""
60
- output, code = run_script(sample_input)
61
- assert code == 0
62
- assert "Claude 3.5 Sonnet" in output
63
-
64
- def test_outputs_directory_name(self, sample_input):
65
- """Should output the directory name."""
66
- output, code = run_script(sample_input)
67
- assert code == 0
68
- assert "myproject" in output
69
-
70
- def test_shows_free_tokens(self, sample_input):
71
- """Should show free tokens indicator."""
72
- output, code = run_script(sample_input)
73
- assert code == 0
74
- assert "free" in output
75
-
76
- def test_shows_ac_indicator(self, sample_input):
77
- """Should show autocompact indicator."""
78
- output, code = run_script(sample_input)
79
- assert code == 0
80
- assert "[AC:" in output
81
-
82
- def test_handles_missing_model(self):
83
- """Should handle missing model gracefully."""
84
- input_data = {"workspace": {"current_dir": "/tmp/test", "project_dir": "/tmp/test"}}
85
- output, code = run_script(input_data)
86
- assert code == 0
87
- assert "Claude" in output # Default fallback
88
-
89
- def test_handles_invalid_json(self):
90
- """Should handle invalid JSON gracefully."""
91
- env = os.environ.copy()
92
- env["PYTHONUTF8"] = "1"
93
- result = subprocess.run(
94
- [sys.executable, str(SCRIPT_PATH)],
95
- input="invalid json",
96
- capture_output=True,
97
- text=True,
98
- env=env,
99
- )
100
- assert result.returncode == 0
101
- assert "Claude" in result.stdout
102
-
103
- def test_handles_empty_input(self):
104
- """Should handle empty input gracefully."""
105
- env = os.environ.copy()
106
- env["PYTHONUTF8"] = "1"
107
- result = subprocess.run(
108
- [sys.executable, str(SCRIPT_PATH)],
109
- input="",
110
- capture_output=True,
111
- text=True,
112
- env=env,
113
- )
114
- assert result.returncode == 0
115
-
116
-
117
- class TestContextWindowColors:
118
- """Tests for context window color coding."""
119
-
120
- def test_low_usage_has_output(self, low_usage_input):
121
- """Low usage (>50% free) should produce output with 'free'."""
122
- output, code = run_script(low_usage_input)
123
- assert code == 0
124
- assert "free" in output
125
-
126
- def test_medium_usage_has_output(self, medium_usage_input):
127
- """Medium usage (25-50% free) should produce output with 'free'."""
128
- output, code = run_script(medium_usage_input)
129
- assert code == 0
130
- assert "free" in output
131
-
132
- def test_high_usage_has_output(self, high_usage_input):
133
- """High usage (<25% free) should produce output with 'free'."""
134
- output, code = run_script(high_usage_input)
135
- assert code == 0
136
- assert "free" in output
137
-
138
-
139
- class TestFixtures:
140
- """Tests using fixture files."""
141
-
142
- def test_valid_full_fixture(self, valid_full_input):
143
- """Should handle valid_full.json fixture."""
144
- output, code = run_script(valid_full_input)
145
- assert code == 0
146
- assert "Opus 4.5" in output
147
- assert "my-project" in output
148
-
149
- def test_valid_minimal_fixture(self, valid_minimal_input):
150
- """Should handle valid_minimal.json fixture."""
151
- output, code = run_script(valid_minimal_input)
152
- assert code == 0
153
- assert "Claude" in output
154
-
155
- def test_all_fixtures_succeed(self, fixtures_dir):
156
- """All JSON fixtures should be processed without errors."""
157
- for fixture_file in fixtures_dir.glob("*.json"):
158
- with open(fixture_file) as f:
159
- input_data = json.load(f)
160
- output, code = run_script(input_data)
161
- assert code == 0, f"Failed on fixture: {fixture_file.name}"
162
-
163
-
164
- class TestSessionDisplay:
165
- """Tests for session_id display feature."""
166
-
167
- def test_shows_session_id_by_default(self, sample_input):
168
- """Should show session_id by default (show_session=true)."""
169
- sample_input["session_id"] = "test-session-12345"
170
- output, code = run_script(sample_input, {"COLUMNS": "200"})
171
- assert code == 0
172
- assert "test-session-12345" in output
173
-
174
- def test_handles_missing_session_id(self, sample_input):
175
- """Should handle missing session_id gracefully."""
176
- # Ensure no session_id in input
177
- if "session_id" in sample_input:
178
- del sample_input["session_id"]
179
- output, code = run_script(sample_input)
180
- assert code == 0
181
-
182
-
183
- class TestWidthTruncation:
184
- """Tests for width truncation to fit terminal width."""
185
-
186
- def test_output_fits_80_columns(self, sample_input):
187
- """Output should fit within 80 columns."""
188
- sample_input["session_id"] = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
189
- output, code = run_script(sample_input, {"COLUMNS": "80"})
190
- assert code == 0
191
- visible = strip_ansi(output)
192
- assert len(visible) <= 80
193
-
194
- def test_output_fits_narrow_terminal(self, sample_input):
195
- """Output should fit within 40 columns and still show model+dir."""
196
- output, code = run_script(sample_input, {"COLUMNS": "40"})
197
- assert code == 0
198
- visible = strip_ansi(output)
199
- assert len(visible) <= 40
200
- assert "Claude 3.5 Sonnet" in visible
201
- assert "myproject" in visible
202
-
203
- def test_wide_terminal_shows_all(self, sample_input):
204
- """Wide terminal should show session_id."""
205
- sample_input["session_id"] = "test-wide-session-uuid"
206
- output, code = run_script(sample_input, {"COLUMNS": "200"})
207
- assert code == 0
208
- assert "test-wide-session-uuid" in output
209
-
210
- def test_full_input_truncated(self, valid_full_input):
211
- """Full input with all features should fit within 80 columns."""
212
- output, code = run_script(valid_full_input, {"COLUMNS": "80"})
213
- assert code == 0
214
- visible = strip_ansi(output)
215
- assert len(visible) <= 80
@@ -1,127 +0,0 @@
1
- """Tests for rotating waiting text and activity detection."""
2
-
3
- import time
4
-
5
- from claude_statusline.core.state import StateEntry
6
- from claude_statusline.ui.waiting import (
7
- STATIC_MESSAGE,
8
- WAITING_MESSAGES,
9
- get_waiting_text,
10
- is_active,
11
- )
12
-
13
-
14
- def _make_entry(timestamp: int = 0) -> StateEntry:
15
- """Helper to create a StateEntry for testing."""
16
- return StateEntry(
17
- timestamp=timestamp,
18
- total_input_tokens=1000,
19
- total_output_tokens=0,
20
- current_input_tokens=1000,
21
- current_output_tokens=0,
22
- cache_creation=0,
23
- cache_read=0,
24
- cost_usd=0.0,
25
- lines_added=0,
26
- lines_removed=0,
27
- session_id="test",
28
- model_id="test-model",
29
- workspace_project_dir="/tmp/test",
30
- context_window_size=200_000,
31
- )
32
-
33
-
34
- class TestGetWaitingText:
35
- """Tests for rotating waiting text."""
36
-
37
- def test_returns_string(self):
38
- text = get_waiting_text(0)
39
- assert isinstance(text, str)
40
- assert len(text) > 0
41
-
42
- def test_rotates_every_two_cycles(self):
43
- """Messages should change every 2 cycles."""
44
- text_0 = get_waiting_text(0)
45
- text_1 = get_waiting_text(1)
46
- text_2 = get_waiting_text(2)
47
-
48
- # Cycle 0 and 1 should be the same (same message index)
49
- assert text_0 == text_1
50
- # Cycle 2 should be different (next message)
51
- assert text_0 != text_2
52
-
53
- def test_wraps_around(self):
54
- """Should cycle back to the first message."""
55
- total_messages = len(WAITING_MESSAGES)
56
- # After going through all messages (2 cycles each), should wrap
57
- first = get_waiting_text(0)
58
- wrapped = get_waiting_text(total_messages * 2)
59
- assert first == wrapped
60
-
61
- def test_all_messages_reachable(self):
62
- """Every message should appear at some cycle."""
63
- seen = set()
64
- for i in range(len(WAITING_MESSAGES) * 2):
65
- seen.add(get_waiting_text(i))
66
- assert seen == set(WAITING_MESSAGES)
67
-
68
- def test_reduced_motion_returns_static(self):
69
- """With reduced_motion=True, always return static message."""
70
- for i in range(10):
71
- text = get_waiting_text(i, reduced_motion=True)
72
- assert text == STATIC_MESSAGE
73
-
74
- def test_reduced_motion_consistent(self):
75
- """Static message should be the same regardless of cycle."""
76
- texts = {get_waiting_text(i, reduced_motion=True) for i in range(20)}
77
- assert len(texts) == 1
78
-
79
-
80
- class TestIsActive:
81
- """Tests for session activity detection."""
82
-
83
- def test_empty_entries(self):
84
- assert is_active([]) is False
85
-
86
- def test_recent_entry_is_active(self):
87
- """Entry within timeout is active."""
88
- entry = _make_entry(timestamp=int(time.time()) - 5)
89
- assert is_active([entry]) is True
90
-
91
- def test_old_entry_is_not_active(self):
92
- """Entry older than timeout is not active."""
93
- entry = _make_entry(timestamp=int(time.time()) - 60)
94
- assert is_active([entry]) is False
95
-
96
- def test_exactly_at_timeout_is_active(self):
97
- """Entry exactly at timeout boundary is still active."""
98
- entry = _make_entry(timestamp=int(time.time()) - 30)
99
- assert is_active([entry], timeout=30) is True
100
-
101
- def test_just_past_timeout_is_not_active(self):
102
- """Entry just past timeout boundary is not active."""
103
- entry = _make_entry(timestamp=int(time.time()) - 31)
104
- assert is_active([entry], timeout=30) is False
105
-
106
- def test_custom_timeout(self):
107
- """Custom timeout should be respected."""
108
- entry = _make_entry(timestamp=int(time.time()) - 10)
109
- assert is_active([entry], timeout=5) is False
110
- assert is_active([entry], timeout=15) is True
111
-
112
- def test_uses_last_entry(self):
113
- """Should check the most recent (last) entry, not the first."""
114
- old_entry = _make_entry(timestamp=int(time.time()) - 120)
115
- recent_entry = _make_entry(timestamp=int(time.time()) - 5)
116
- assert is_active([old_entry, recent_entry]) is True
117
-
118
- def test_old_entries_with_recent_last(self):
119
- """Multiple old entries don't matter if last one is recent."""
120
- now = int(time.time())
121
- entries = [
122
- _make_entry(timestamp=now - 300),
123
- _make_entry(timestamp=now - 200),
124
- _make_entry(timestamp=now - 100),
125
- _make_entry(timestamp=now - 2),
126
- ]
127
- assert is_active(entries) is True