cc-context-stats 1.8.0 → 1.8.2

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 (106) hide show
  1. package/package.json +8 -1
  2. package/scripts/context-stats.sh +1 -1
  3. package/.editorconfig +0 -60
  4. package/.eslintrc.json +0 -35
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -49
  6. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -31
  7. package/.github/PULL_REQUEST_TEMPLATE.md +0 -33
  8. package/.github/dependabot.yml +0 -44
  9. package/.github/workflows/ci.yml +0 -294
  10. package/.github/workflows/release.yml +0 -151
  11. package/.pre-commit-config.yaml +0 -74
  12. package/.prettierrc +0 -33
  13. package/.shellcheckrc +0 -10
  14. package/CHANGELOG.md +0 -187
  15. package/CLAUDE.md +0 -66
  16. package/CODE_OF_CONDUCT.md +0 -59
  17. package/CONTRIBUTING.md +0 -240
  18. package/RELEASE_NOTES.md +0 -19
  19. package/SECURITY.md +0 -44
  20. package/TODOS.md +0 -72
  21. package/assets/logo/favicon.svg +0 -19
  22. package/assets/logo/logo-black.svg +0 -24
  23. package/assets/logo/logo-full.svg +0 -40
  24. package/assets/logo/logo-icon.svg +0 -27
  25. package/assets/logo/logo-mark.svg +0 -28
  26. package/assets/logo/logo-white.svg +0 -24
  27. package/assets/logo/logo-wordmark.svg +0 -6
  28. package/config/settings-example.json +0 -7
  29. package/config/settings-node.json +0 -7
  30. package/config/settings-python.json +0 -7
  31. package/docs/ARCHITECTURE.md +0 -128
  32. package/docs/CSV_FORMAT.md +0 -42
  33. package/docs/DEPLOYMENT.md +0 -71
  34. package/docs/DEVELOPMENT.md +0 -161
  35. package/docs/MODEL_INTELLIGENCE.md +0 -396
  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 -438
  55. package/scripts/statusline-git.sh +0 -88
  56. package/scripts/statusline-minimal.sh +0 -67
  57. package/scripts/statusline.py +0 -569
  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 -542
  62. package/src/claude_statusline/cli/explain.py +0 -228
  63. package/src/claude_statusline/cli/statusline.py +0 -184
  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 -165
  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/intelligence.py +0 -162
  75. package/src/claude_statusline/graphs/renderer.py +0 -401
  76. package/src/claude_statusline/graphs/statistics.py +0 -92
  77. package/src/claude_statusline/ui/__init__.py +0 -1
  78. package/src/claude_statusline/ui/icons.py +0 -93
  79. package/src/claude_statusline/ui/waiting.py +0 -62
  80. package/tests/bash/test_delta_parity.bats +0 -199
  81. package/tests/bash/test_install.bats +0 -29
  82. package/tests/bash/test_parity.bats +0 -315
  83. package/tests/bash/test_statusline_full.bats +0 -139
  84. package/tests/bash/test_statusline_git.bats +0 -42
  85. package/tests/bash/test_statusline_minimal.bats +0 -37
  86. package/tests/fixtures/json/comma_in_path.json +0 -31
  87. package/tests/fixtures/json/high_usage.json +0 -17
  88. package/tests/fixtures/json/low_usage.json +0 -17
  89. package/tests/fixtures/json/medium_usage.json +0 -17
  90. package/tests/fixtures/json/valid_full.json +0 -30
  91. package/tests/fixtures/json/valid_minimal.json +0 -9
  92. package/tests/fixtures/mi_test_vectors.json +0 -140
  93. package/tests/node/intelligence.test.js +0 -98
  94. package/tests/node/rotation.test.js +0 -89
  95. package/tests/node/statusline.test.js +0 -240
  96. package/tests/python/conftest.py +0 -84
  97. package/tests/python/test_colors.py +0 -105
  98. package/tests/python/test_config_colors.py +0 -78
  99. package/tests/python/test_data_pipeline.py +0 -446
  100. package/tests/python/test_explain.py +0 -177
  101. package/tests/python/test_icons.py +0 -152
  102. package/tests/python/test_intelligence.py +0 -314
  103. package/tests/python/test_layout.py +0 -127
  104. package/tests/python/test_state_rotation_validation.py +0 -232
  105. package/tests/python/test_statusline.py +0 -215
  106. package/tests/python/test_waiting.py +0 -127
@@ -1,92 +0,0 @@
1
- """Statistical calculations for token data."""
2
-
3
- from __future__ import annotations
4
-
5
- from dataclasses import dataclass
6
-
7
-
8
- @dataclass
9
- class Stats:
10
- """Statistical summary of a data series."""
11
-
12
- min_val: int
13
- max_val: int
14
- avg_val: int
15
- total: int
16
- count: int
17
-
18
-
19
- def calculate_stats(data: list[int]) -> Stats:
20
- """Calculate basic statistics for a list of integers.
21
-
22
- Args:
23
- data: List of integer values
24
-
25
- Returns:
26
- Stats object with min, max, avg, total, and count
27
- """
28
- if not data:
29
- return Stats(min_val=0, max_val=0, avg_val=0, total=0, count=0)
30
-
31
- min_val = min(data)
32
- max_val = max(data)
33
- total = sum(data)
34
- count = len(data)
35
- avg_val = total // count if count > 0 else 0
36
-
37
- return Stats(min_val=min_val, max_val=max_val, avg_val=avg_val, total=total, count=count)
38
-
39
-
40
- def detect_spike(deltas: list[int], context_window_size: int, window: int = 5) -> bool:
41
- """Check if the latest delta is a spike.
42
-
43
- A spike is defined as:
44
- - Latest delta > 15% of context window size, OR
45
- - Latest delta > 3x the rolling average of the last `window` deltas
46
-
47
- Args:
48
- deltas: List of token deltas
49
- context_window_size: Total context window size in tokens
50
- window: Number of recent deltas for rolling average (default: 5)
51
-
52
- Returns:
53
- True if the latest delta qualifies as a spike
54
- """
55
- if not deltas:
56
- return False
57
-
58
- latest = deltas[-1]
59
-
60
- # Check absolute threshold: > 15% of context window
61
- if context_window_size > 0 and latest > context_window_size * 0.15:
62
- return True
63
-
64
- # Check relative threshold: > 3x rolling average of previous deltas
65
- previous = deltas[-(window + 1):-1] if len(deltas) > window else deltas[:-1]
66
- if previous:
67
- avg = sum(previous) / len(previous)
68
- if avg > 0 and latest > avg * 3:
69
- return True
70
-
71
- return False
72
-
73
-
74
- def calculate_deltas(values: list[int]) -> list[int]:
75
- """Calculate deltas between consecutive values.
76
-
77
- Args:
78
- values: List of values (e.g., cumulative token counts)
79
-
80
- Returns:
81
- List of deltas (length = len(values) - 1)
82
- """
83
- if len(values) < 2:
84
- return []
85
-
86
- deltas = []
87
- for i in range(1, len(values)):
88
- delta = values[i] - values[i - 1]
89
- # Handle negative deltas (session reset) by showing 0
90
- deltas.append(max(0, delta))
91
-
92
- return deltas
@@ -1 +0,0 @@
1
- """UI components for context-stats display."""
@@ -1,93 +0,0 @@
1
- """Activity tier detection for token usage visualization."""
2
-
3
- from __future__ import annotations
4
-
5
- from enum import Enum
6
-
7
- from claude_statusline.core.state import StateEntry
8
- from claude_statusline.graphs.statistics import calculate_deltas, detect_spike
9
-
10
-
11
- class ActivityTier(Enum):
12
- """Token activity intensity tiers."""
13
-
14
- IDLE = "idle"
15
- LOW = "low"
16
- MEDIUM = "medium"
17
- HIGH = "high"
18
- SPIKE = "spike"
19
-
20
-
21
- # Tier labels for accessibility (understandable without color)
22
- TIER_LABELS: dict[ActivityTier, str] = {
23
- ActivityTier.IDLE: "Idle",
24
- ActivityTier.LOW: "Low activity",
25
- ActivityTier.MEDIUM: "Active",
26
- ActivityTier.HIGH: "High activity",
27
- ActivityTier.SPIKE: "Spike!",
28
- }
29
-
30
-
31
- def get_activity_tier(
32
- entries: list[StateEntry],
33
- context_window_size: int,
34
- ) -> ActivityTier:
35
- """Determine the current activity tier based on recent token deltas.
36
-
37
- Args:
38
- entries: List of StateEntry objects (chronological order)
39
- context_window_size: Total context window size in tokens
40
-
41
- Returns:
42
- The current ActivityTier
43
- """
44
- if len(entries) < 2:
45
- return ActivityTier.IDLE
46
-
47
- # Check if session is idle (>30s since last entry)
48
- import time
49
-
50
- now = int(time.time())
51
- last_timestamp = entries[-1].timestamp
52
- if now - last_timestamp > 30:
53
- return ActivityTier.IDLE
54
-
55
- # Calculate deltas from context usage
56
- context_used = [e.current_used_tokens for e in entries]
57
- deltas = calculate_deltas(context_used)
58
-
59
- if not deltas:
60
- return ActivityTier.IDLE
61
-
62
- latest_delta = deltas[-1]
63
-
64
- if context_window_size <= 0:
65
- return ActivityTier.LOW if latest_delta > 0 else ActivityTier.IDLE
66
-
67
- # Check for spike first (highest priority)
68
- if detect_spike(deltas, context_window_size):
69
- return ActivityTier.SPIKE
70
-
71
- # Calculate delta as percentage of context window
72
- delta_pct = (latest_delta / context_window_size) * 100
73
-
74
- if delta_pct > 5:
75
- return ActivityTier.HIGH
76
- elif delta_pct > 2:
77
- return ActivityTier.MEDIUM
78
- elif latest_delta > 0:
79
- return ActivityTier.LOW
80
- else:
81
- return ActivityTier.IDLE
82
-
83
-
84
- def get_tier_label(tier: ActivityTier) -> str:
85
- """Get an accessible text label for a tier.
86
-
87
- Args:
88
- tier: The activity tier
89
-
90
- Returns:
91
- Human-readable label string
92
- """
93
- return TIER_LABELS.get(tier, "")
@@ -1,62 +0,0 @@
1
- """Rotating waiting text for active sessions."""
2
-
3
- from __future__ import annotations
4
-
5
- import time
6
-
7
- from claude_statusline.core.state import StateEntry
8
-
9
- WAITING_MESSAGES = [
10
- "Thinking...",
11
- "Cooking...",
12
- "Crunching tokens...",
13
- "Compiling plan...",
14
- "Running steps...",
15
- "Processing...",
16
- "Working on it...",
17
- "Analyzing...",
18
- ]
19
-
20
- # Static message for reduced-motion mode
21
- STATIC_MESSAGE = "Working..."
22
-
23
-
24
- def get_waiting_text(cycle_index: int, reduced_motion: bool = False) -> str:
25
- """Get the current waiting text based on the refresh cycle.
26
-
27
- Messages rotate every 2 cycles (approximately every 4 seconds at 2s refresh).
28
-
29
- Args:
30
- cycle_index: The current watch-mode refresh counter
31
- reduced_motion: If True, return a static message instead of rotating
32
-
33
- Returns:
34
- A waiting message string
35
- """
36
- if reduced_motion:
37
- return STATIC_MESSAGE
38
-
39
- # Rotate every 2 cycles to keep it readable
40
- message_index = (cycle_index // 2) % len(WAITING_MESSAGES)
41
- return WAITING_MESSAGES[message_index]
42
-
43
-
44
- def is_active(entries: list[StateEntry], timeout: int = 30) -> bool:
45
- """Determine if the session is currently active.
46
-
47
- A session is considered active if the most recent state entry
48
- was recorded within `timeout` seconds of the current time.
49
-
50
- Args:
51
- entries: List of StateEntry objects (chronological order)
52
- timeout: Seconds since last entry to consider session active (default: 30)
53
-
54
- Returns:
55
- True if the session appears to be actively running
56
- """
57
- if not entries:
58
- return False
59
-
60
- now = int(time.time())
61
- last_timestamp = entries[-1].timestamp
62
- return (now - last_timestamp) <= timeout
@@ -1,199 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # Delta calculation parity tests: Python vs Node.js statusline scripts
4
- # Verifies both implementations compute identical deltas from identical state.
5
-
6
- strip_ansi() {
7
- printf '%s' "$1" | sed -e $'s/\033\[[0-9;]*m//g' -e 's/\\033\[[0-9;]*m//g'
8
- }
9
-
10
- setup() {
11
- PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
12
- PYTHON_SCRIPT="$PROJECT_ROOT/scripts/statusline.py"
13
- NODE_SCRIPT="$PROJECT_ROOT/scripts/statusline.js"
14
-
15
- # Create isolated temp HOME so state files don't pollute real ~/.claude/
16
- TEST_HOME=$(mktemp -d)
17
- export HOME="$TEST_HOME"
18
-
19
- # Normalize terminal width for deterministic output
20
- export COLUMNS=200
21
-
22
- # Enable delta display
23
- mkdir -p "$TEST_HOME/.claude"
24
- echo "show_delta=true" > "$TEST_HOME/.claude/statusline.conf"
25
-
26
- # Create a non-git temp working directory so both scripts skip git info
27
- TEST_WORKDIR=$(mktemp -d)
28
- cd "$TEST_WORKDIR"
29
- }
30
-
31
- teardown() {
32
- rm -rf "$TEST_HOME"
33
- rm -rf "$TEST_WORKDIR"
34
- }
35
-
36
- # Helper: create a JSON payload with specific token values and session_id
37
- make_payload() {
38
- local session="$1" input_tokens="$2" cache_creation="$3" cache_read="$4"
39
- cat <<EOF
40
- {
41
- "model": {"display_name": "Opus 4.5"},
42
- "workspace": {"current_dir": "$TEST_WORKDIR", "project_dir": "$TEST_WORKDIR"},
43
- "session_id": "$session",
44
- "context_window": {
45
- "context_window_size": 200000,
46
- "current_usage": {
47
- "input_tokens": $input_tokens,
48
- "cache_creation_input_tokens": $cache_creation,
49
- "cache_read_input_tokens": $cache_read
50
- }
51
- }
52
- }
53
- EOF
54
- }
55
-
56
- # ============================================
57
- # Delta Calculation Parity Tests
58
- # ============================================
59
-
60
- @test "delta parity: both scripts show identical delta after two sequential payloads" {
61
- # First payload: 30k context usage (10k input + 10k cache_create + 10k cache_read)
62
- local py_session="delta-parity-py"
63
- local node_session="delta-parity-node"
64
-
65
- local payload1_py=$(make_payload "$py_session" 10000 10000 10000)
66
- local payload1_node=$(make_payload "$node_session" 10000 10000 10000)
67
-
68
- # Run first payload (seeds state file, no delta shown)
69
- echo "$payload1_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
70
- echo "$payload1_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
71
-
72
- # Second payload: 80k context usage (40k input + 20k cache_create + 20k cache_read)
73
- # Expected delta = 80k - 30k = 50k
74
- local payload2_py=$(make_payload "$py_session" 40000 20000 20000)
75
- local payload2_node=$(make_payload "$node_session" 40000 20000 20000)
76
-
77
- local py_output=$(echo "$payload2_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
78
- local node_output=$(echo "$payload2_node" | node "$NODE_SCRIPT" 2>/dev/null)
79
-
80
- local py_clean=$(strip_ansi "$py_output")
81
- local node_clean=$(strip_ansi "$node_output")
82
-
83
- # Both should contain [+50,000] delta
84
- if [[ "$py_clean" != *"[+50,000]"* ]]; then
85
- echo "Python output missing expected delta [+50,000]"
86
- echo "Python output: $py_clean"
87
- return 1
88
- fi
89
- if [[ "$node_clean" != *"[+50,000]"* ]]; then
90
- echo "Node.js output missing expected delta [+50,000]"
91
- echo "Node.js output: $node_clean"
92
- return 1
93
- fi
94
-
95
- # Compare outputs ignoring session_id suffix (which intentionally differs)
96
- # Strip the trailing session ID from both outputs for comparison
97
- local py_no_session=$(echo "$py_clean" | sed 's/ delta-parity-py$//')
98
- local node_no_session=$(echo "$node_clean" | sed 's/ delta-parity-node$//')
99
-
100
- if [ "$py_no_session" != "$node_no_session" ]; then
101
- echo "DELTA PARITY MISMATCH (ignoring session_id)"
102
- echo "Python: $py_no_session"
103
- echo "Node.js: $node_no_session"
104
- return 1
105
- fi
106
- }
107
-
108
- @test "delta parity: no delta shown on first run (no previous state)" {
109
- local py_session="delta-first-py"
110
- local node_session="delta-first-node"
111
-
112
- local payload_py=$(make_payload "$py_session" 50000 10000 5000)
113
- local payload_node=$(make_payload "$node_session" 50000 10000 5000)
114
-
115
- local py_output=$(echo "$payload_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
116
- local node_output=$(echo "$payload_node" | node "$NODE_SCRIPT" 2>/dev/null)
117
-
118
- local py_clean=$(strip_ansi "$py_output")
119
- local node_clean=$(strip_ansi "$node_output")
120
-
121
- # Neither should show a delta on first run
122
- if [[ "$py_clean" == *"[+"* ]]; then
123
- echo "Python should not show delta on first run"
124
- echo "Python output: $py_clean"
125
- return 1
126
- fi
127
- if [[ "$node_clean" == *"[+"* ]]; then
128
- echo "Node.js should not show delta on first run"
129
- echo "Node.js output: $node_clean"
130
- return 1
131
- fi
132
- }
133
-
134
- @test "delta parity: no delta shown when tokens decrease (context reset)" {
135
- local py_session="delta-decrease-py"
136
- local node_session="delta-decrease-node"
137
-
138
- # First payload: high usage
139
- local payload1_py=$(make_payload "$py_session" 80000 20000 10000)
140
- local payload1_node=$(make_payload "$node_session" 80000 20000 10000)
141
-
142
- echo "$payload1_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
143
- echo "$payload1_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
144
-
145
- # Second payload: lower usage (context was reset/compacted)
146
- local payload2_py=$(make_payload "$py_session" 20000 5000 5000)
147
- local payload2_node=$(make_payload "$node_session" 20000 5000 5000)
148
-
149
- local py_output=$(echo "$payload2_py" | python3 "$PYTHON_SCRIPT" 2>/dev/null)
150
- local node_output=$(echo "$payload2_node" | node "$NODE_SCRIPT" 2>/dev/null)
151
-
152
- local py_clean=$(strip_ansi "$py_output")
153
- local node_clean=$(strip_ansi "$node_output")
154
-
155
- # Neither should show delta when tokens decrease
156
- if [[ "$py_clean" == *"[+"* ]]; then
157
- echo "Python should not show delta when tokens decrease"
158
- echo "Python output: $py_clean"
159
- return 1
160
- fi
161
- if [[ "$node_clean" == *"[+"* ]]; then
162
- echo "Node.js should not show delta when tokens decrease"
163
- echo "Node.js output: $node_clean"
164
- return 1
165
- fi
166
- }
167
-
168
- @test "delta parity: duplicate guard prevents writing when tokens unchanged" {
169
- local py_session="delta-dedup-py"
170
- local node_session="delta-dedup-node"
171
-
172
- local payload_py=$(make_payload "$py_session" 50000 10000 5000)
173
- local payload_node=$(make_payload "$node_session" 50000 10000 5000)
174
-
175
- # Run same payload three times
176
- echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
177
- echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
178
- echo "$payload_py" | python3 "$PYTHON_SCRIPT" > /dev/null 2>&1
179
-
180
- echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
181
- echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
182
- echo "$payload_node" | node "$NODE_SCRIPT" > /dev/null 2>&1
183
-
184
- local py_state="$TEST_HOME/.claude/statusline/statusline.${py_session}.state"
185
- local node_state="$TEST_HOME/.claude/statusline/statusline.${node_session}.state"
186
-
187
- # Both should have written only 1 line (duplicate guard)
188
- local py_lines=$(wc -l < "$py_state" | tr -d ' ')
189
- local node_lines=$(wc -l < "$node_state" | tr -d ' ')
190
-
191
- if [ "$py_lines" -ne 1 ]; then
192
- echo "Python wrote $py_lines lines (expected 1 — duplicate guard failed)"
193
- return 1
194
- fi
195
- if [ "$node_lines" -ne 1 ]; then
196
- echo "Node.js wrote $node_lines lines (expected 1 — duplicate guard failed)"
197
- return 1
198
- fi
199
- }
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # Test suite for install.sh
4
-
5
- setup() {
6
- PROJECT_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)"
7
- SCRIPT="$PROJECT_ROOT/install.sh"
8
- }
9
-
10
- @test "install.sh exists and is executable" {
11
- [ -f "$SCRIPT" ]
12
- [ -x "$SCRIPT" ]
13
- }
14
-
15
- @test "install.sh contains expected functions" {
16
- grep -q "check_jq" "$SCRIPT"
17
- grep -q "select_script" "$SCRIPT"
18
- grep -q "ensure_claude_dir" "$SCRIPT"
19
- grep -q "install_script" "$SCRIPT"
20
- grep -q "update_settings" "$SCRIPT"
21
- }
22
-
23
- @test "install.sh has correct shebang" {
24
- head -1 "$SCRIPT" | grep -q "#!/bin/bash"
25
- }
26
-
27
- @test "install.sh uses set -e for error handling" {
28
- grep -q "set -e" "$SCRIPT"
29
- }