cc-context-stats 1.5.0 → 1.6.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.
Files changed (45) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +49 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +33 -0
  4. package/.github/workflows/ci.yml +39 -2
  5. package/.github/workflows/release.yml +3 -1
  6. package/CHANGELOG.md +16 -8
  7. package/CLAUDE.md +54 -0
  8. package/CODE_OF_CONDUCT.md +59 -0
  9. package/LICENSE +21 -0
  10. package/README.md +9 -0
  11. package/RELEASE_NOTES.md +16 -6
  12. package/SECURITY.md +44 -0
  13. package/TODOS.md +72 -0
  14. package/assets/logo/favicon.svg +17 -14
  15. package/assets/logo/logo-black.svg +19 -18
  16. package/assets/logo/logo-full.svg +39 -29
  17. package/assets/logo/logo-icon.svg +20 -19
  18. package/assets/logo/logo-mark.svg +21 -19
  19. package/assets/logo/logo-white.svg +19 -18
  20. package/assets/logo/logo-wordmark.svg +5 -6
  21. package/docs/ARCHITECTURE.md +101 -0
  22. package/docs/CSV_FORMAT.md +40 -0
  23. package/docs/DEPLOYMENT.md +60 -0
  24. package/docs/DEVELOPMENT.md +125 -0
  25. package/package.json +2 -2
  26. package/pyproject.toml +1 -1
  27. package/scripts/statusline-full.sh +11 -3
  28. package/scripts/statusline-git.sh +8 -1
  29. package/scripts/statusline-minimal.sh +8 -1
  30. package/scripts/statusline.js +62 -8
  31. package/scripts/statusline.py +24 -10
  32. package/src/claude_statusline/__init__.py +1 -1
  33. package/src/claude_statusline/cli/context_stats.py +20 -1
  34. package/src/claude_statusline/core/config.py +5 -4
  35. package/src/claude_statusline/core/state.py +64 -7
  36. package/src/claude_statusline/formatters/layout.py +17 -2
  37. package/tests/bash/test_parity.bats +315 -0
  38. package/tests/fixtures/json/comma_in_path.json +31 -0
  39. package/tests/node/rotation.test.js +89 -0
  40. package/tests/python/test_data_pipeline.py +446 -0
  41. package/tests/python/test_layout.py +19 -2
  42. package/tests/python/test_state_rotation_validation.py +232 -0
  43. package/tests/python/test_statusline.py +2 -0
  44. package/.claude/commands/context-stats.md +0 -17
  45. package/.claude/settings.local.json +0 -117
@@ -1,30 +1,40 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 64" width="320" height="64">
2
- <!-- Background -->
3
- <rect width="320" height="64" rx="12" fill="#000000"/>
4
-
5
- <!-- Mark: Gauge icon (left, inset) -->
6
- <!-- Gauge track arc -->
7
- <path d="M 12 50 A 22 22 0 1 1 52 50" stroke="#1a1a1a" stroke-width="5" fill="none" stroke-linecap="round"/>
8
- <!-- Green zone -->
9
- <path d="M 12 50 A 22 22 0 0 1 18.2 30.4" stroke="#22C55E" stroke-width="5" fill="none" stroke-linecap="round"/>
10
- <!-- Yellow zone -->
11
- <path d="M 18.2 30.4 A 22 22 0 0 1 45.8 30.4" stroke="#F59E0B" stroke-width="5" fill="none" stroke-linecap="round"/>
12
- <!-- Red zone -->
13
- <path d="M 45.8 30.4 A 22 22 0 0 1 52 50" stroke="#EF4444" stroke-width="5" fill="none" stroke-linecap="round"/>
14
- <!-- Needle -->
15
- <line x1="32" y1="38" x2="21" y2="27" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
16
- <!-- Pivot -->
17
- <circle cx="32" cy="38" r="3" fill="#22C55E"/>
18
-
19
- <!-- Divider line -->
20
- <line x1="68" y1="12" x2="68" y2="52" stroke="#1f1f1f" stroke-width="1"/>
21
-
22
- <!-- Wordmark: "cc-context" on top row, "stats" accent -->
23
- <!-- Primary text -->
24
- <text x="82" y="30" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="18" font-weight="700" fill="#FFFFFF" letter-spacing="-0.5">cc-context</text>
25
- <!-- Accent text with green highlight -->
26
- <text x="82" y="50" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="14" font-weight="500" fill="#22C55E" letter-spacing="2">stats</text>
27
-
28
- <!-- Subtle border accent on right -->
29
- <rect x="308" y="16" width="2" height="32" rx="1" fill="#22C55E" opacity="0.6"/>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 72" width="360" height="72">
2
+ <defs>
3
+ <linearGradient id="full-wave-grad" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" stop-color="#22C55E"/>
5
+ <stop offset="50%" stop-color="#E9AB34"/>
6
+ <stop offset="100%" stop-color="#EF4444"/>
7
+ </linearGradient>
8
+ </defs>
9
+
10
+ <!-- Background pill -->
11
+ <rect width="360" height="72" rx="14" fill="#1A1A1A"/>
12
+
13
+ <!-- Mark: Context wave in left area -->
14
+ <g transform="translate(8, 4)">
15
+ <!-- Context wave -->
16
+ <path d="M 10 48 C 16 48, 18 42, 22 40 C 26 38, 28 36, 32 32 C 36 28, 38 22, 42 18 C 46 14, 50 16, 54 14"
17
+ stroke="url(#full-wave-grad)" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
18
+
19
+ <!-- Echo line -->
20
+ <path d="M 10 52 C 16 52, 18 46, 22 44 C 26 42, 28 40, 32 36 C 36 32, 38 26, 42 22 C 46 18, 50 20, 54 18"
21
+ stroke="url(#full-wave-grad)" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.3"/>
22
+
23
+ <!-- Cursor dot -->
24
+ <circle cx="42" cy="18" r="3.5" fill="#E9AB34"/>
25
+ <circle cx="42" cy="18" r="6" fill="#E9AB34" opacity="0.15"/>
26
+
27
+ <!-- Baseline -->
28
+ <line x1="10" y1="54" x2="54" y2="54" stroke="#8B7D6B" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
29
+ </g>
30
+
31
+ <!-- Divider -->
32
+ <line x1="76" y1="16" x2="76" y2="56" stroke="#8B7D6B" stroke-width="1" opacity="0.25"/>
33
+
34
+ <!-- Wordmark -->
35
+ <text x="90" y="35" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="22" font-weight="700" fill="#FFFFFF" letter-spacing="-0.3">cc-context</text>
36
+ <text x="90" y="55" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="15" font-weight="500" fill="#E9AB34" letter-spacing="3">stats</text>
37
+
38
+ <!-- Right accent bar -->
39
+ <rect x="348" y="20" width="2" height="32" rx="1" fill="#E9AB34" opacity="0.5"/>
30
40
  </svg>
@@ -1,26 +1,27 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
2
- <!-- App icon — square with padding and rounded corners -->
3
- <rect width="512" height="512" rx="96" fill="#000000"/>
2
+ <defs>
3
+ <linearGradient id="icon-wave-grad" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" stop-color="#22C55E"/>
5
+ <stop offset="50%" stop-color="#E9AB34"/>
6
+ <stop offset="100%" stop-color="#EF4444"/>
7
+ </linearGradient>
8
+ </defs>
4
9
 
5
- <!-- Gauge track (background) -->
6
- <path d="M 96 352 A 176 176 0 1 1 416 352" stroke="#111111" stroke-width="38" fill="none" stroke-linecap="round"/>
10
+ <!-- Background -->
11
+ <rect width="512" height="512" rx="96" fill="#1A1A1A"/>
7
12
 
8
- <!-- Green zone (0-40%) -->
9
- <path d="M 96 352 A 176 176 0 0 1 145.6 194.8" stroke="#22C55E" stroke-width="38" fill="none" stroke-linecap="round"/>
13
+ <!-- Context wave (scaled up) -->
14
+ <path d="M 96 376 C 144 376, 160 328, 192 312 C 224 296, 240 280, 272 248 C 304 216, 320 168, 352 136 C 384 104, 400 120, 432 104"
15
+ stroke="url(#icon-wave-grad)" stroke-width="24" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
10
16
 
11
- <!-- Yellow zone (40-80%) -->
12
- <path d="M 145.6 194.8 A 176 176 0 0 1 366.4 194.8" stroke="#F59E0B" stroke-width="38" fill="none" stroke-linecap="round"/>
17
+ <!-- Echo line -->
18
+ <path d="M 96 408 C 144 408, 160 360, 192 344 C 224 328, 240 312, 272 280 C 304 248, 320 200, 352 168 C 384 136, 400 152, 432 136"
19
+ stroke="url(#icon-wave-grad)" stroke-width="10" fill="none" stroke-linecap="round" opacity="0.25"/>
13
20
 
14
- <!-- Red zone (80-100%) -->
15
- <path d="M 366.4 194.8 A 176 176 0 0 1 416 352" stroke="#EF4444" stroke-width="38" fill="none" stroke-linecap="round"/>
21
+ <!-- Cursor dot -->
22
+ <circle cx="352" cy="136" r="28" fill="#E9AB34"/>
23
+ <circle cx="352" cy="136" r="48" fill="#E9AB34" opacity="0.12"/>
16
24
 
17
- <!-- Needle at ~55% -->
18
- <line x1="256" y1="256" x2="168" y2="168" stroke="#FFFFFF" stroke-width="20" stroke-linecap="round"/>
19
-
20
- <!-- Pivot dot -->
21
- <circle cx="256" cy="256" r="24" fill="#22C55E"/>
22
-
23
- <!-- Label dots at zone boundaries -->
24
- <circle cx="96" cy="352" r="8" fill="#6B7280"/>
25
- <circle cx="416" cy="352" r="8" fill="#6B7280"/>
25
+ <!-- Baseline -->
26
+ <line x1="96" y1="424" x2="432" y2="424" stroke="#8B7D6B" stroke-width="6" stroke-linecap="round" opacity="0.35"/>
26
27
  </svg>
@@ -1,26 +1,28 @@
1
1
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
- <!-- Background square with rounded corners -->
3
- <rect width="64" height="64" rx="12" fill="#000000"/>
2
+ <defs>
3
+ <linearGradient id="wave-grad" x1="0%" y1="0%" x2="100%" y2="0%">
4
+ <stop offset="0%" stop-color="#22C55E"/>
5
+ <stop offset="50%" stop-color="#E9AB34"/>
6
+ <stop offset="100%" stop-color="#EF4444"/>
7
+ </linearGradient>
8
+ </defs>
4
9
 
5
- <!-- Gauge track arc (background) -->
6
- <path d="M 12 44 A 22 22 0 1 1 52 44" stroke="#1a1a1a" stroke-width="5" fill="none" stroke-linecap="round"/>
10
+ <!-- Background -->
11
+ <rect width="64" height="64" rx="14" fill="#1A1A1A"/>
7
12
 
8
- <!-- Gauge fill: green zone (0-40%) -->
9
- <path d="M 12 44 A 22 22 0 0 1 18.2 24.4" stroke="#22C55E" stroke-width="5" fill="none" stroke-linecap="round"/>
13
+ <!-- Context wave: a flowing curve rising from left (low usage) to right (high usage) -->
14
+ <!-- This represents the context growth graph - the core concept of the tool -->
15
+ <path d="M 10 48 C 16 48, 18 42, 22 40 C 26 38, 28 36, 32 32 C 36 28, 38 22, 42 18 C 46 14, 50 16, 54 14"
16
+ stroke="url(#wave-grad)" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
10
17
 
11
- <!-- Gauge fill: yellow zone (40-80%) -->
12
- <path d="M 18.2 24.4 A 22 22 0 0 1 45.8 24.4" stroke="#F59E0B" stroke-width="5" fill="none" stroke-linecap="round"/>
18
+ <!-- Secondary pulse line (echo effect for depth) -->
19
+ <path d="M 10 52 C 16 52, 18 46, 22 44 C 26 42, 28 40, 32 36 C 36 32, 38 26, 42 22 C 46 18, 50 20, 54 18"
20
+ stroke="url(#wave-grad)" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.3"/>
13
21
 
14
- <!-- Gauge fill: red zone (80-100%) -->
15
- <path d="M 45.8 24.4 A 22 22 0 0 1 52 44" stroke="#EF4444" stroke-width="5" fill="none" stroke-linecap="round"/>
22
+ <!-- Cursor dot at the "current position" on the wave -->
23
+ <circle cx="42" cy="18" r="3.5" fill="#E9AB34"/>
24
+ <circle cx="42" cy="18" r="6" fill="#E9AB34" opacity="0.15"/>
16
25
 
17
- <!-- Needle at ~55% (in dumb zone) -->
18
- <line x1="32" y1="32" x2="21" y2="21" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
19
-
20
- <!-- Needle pivot center dot -->
21
- <circle cx="32" cy="32" r="3" fill="#22C55E"/>
22
-
23
- <!-- Tick marks at zone boundaries -->
24
- <line x1="12" y1="44" x2="14.5" y2="41.5" stroke="#6B7280" stroke-width="1.5" stroke-linecap="round"/>
25
- <line x1="52" y1="44" x2="49.5" y2="41.5" stroke="#6B7280" stroke-width="1.5" stroke-linecap="round"/>
26
+ <!-- Baseline -->
27
+ <line x1="10" y1="54" x2="54" y2="54" stroke="#8B7D6B" stroke-width="1" stroke-linecap="round" opacity="0.4"/>
26
28
  </svg>
@@ -1,23 +1,24 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 64" width="320" height="64">
2
- <!-- White version for dark backgrounds — no bg rect -->
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 72" width="360" height="72">
2
+ <!-- White version for dark backgrounds — no background rectangle -->
3
3
 
4
- <!-- Gauge track arc (dim white) -->
5
- <path d="M 12 50 A 22 22 0 1 1 52 50" stroke="rgba(255,255,255,0.15)" stroke-width="5" fill="none" stroke-linecap="round"/>
6
- <!-- Green zone -->
7
- <path d="M 12 50 A 22 22 0 0 1 18.2 30.4" stroke="#22C55E" stroke-width="5" fill="none" stroke-linecap="round"/>
8
- <!-- Yellow zone -->
9
- <path d="M 18.2 30.4 A 22 22 0 0 1 45.8 30.4" stroke="#F59E0B" stroke-width="5" fill="none" stroke-linecap="round"/>
10
- <!-- Red zone -->
11
- <path d="M 45.8 30.4 A 22 22 0 0 1 52 50" stroke="#EF4444" stroke-width="5" fill="none" stroke-linecap="round"/>
12
- <!-- Needle -->
13
- <line x1="32" y1="38" x2="21" y2="27" stroke="#FFFFFF" stroke-width="2.5" stroke-linecap="round"/>
14
- <!-- Pivot -->
15
- <circle cx="32" cy="38" r="3" fill="#22C55E"/>
4
+ <!-- Mark: Context wave -->
5
+ <g transform="translate(8, 4)">
6
+ <path d="M 10 48 C 16 48, 18 42, 22 40 C 26 38, 28 36, 32 32 C 36 28, 38 22, 42 18 C 46 14, 50 16, 54 14"
7
+ stroke="#FFFFFF" stroke-width="3.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
8
+ <path d="M 10 52 C 16 52, 18 46, 22 44 C 26 42, 28 40, 32 36 C 36 32, 38 26, 42 22 C 46 18, 50 20, 54 18"
9
+ stroke="#FFFFFF" stroke-width="1.5" fill="none" stroke-linecap="round" opacity="0.25"/>
10
+ <circle cx="42" cy="18" r="3.5" fill="#FFFFFF"/>
11
+ <circle cx="42" cy="18" r="6" fill="#FFFFFF" opacity="0.12"/>
12
+ <line x1="10" y1="54" x2="54" y2="54" stroke="#FFFFFF" stroke-width="1" stroke-linecap="round" opacity="0.3"/>
13
+ </g>
16
14
 
17
15
  <!-- Divider -->
18
- <line x1="68" y1="12" x2="68" y2="52" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
16
+ <line x1="76" y1="16" x2="76" y2="56" stroke="#FFFFFF" stroke-width="1" opacity="0.2"/>
19
17
 
20
- <!-- Wordmark white -->
21
- <text x="82" y="30" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="18" font-weight="700" fill="#FFFFFF" letter-spacing="-0.5">cc-context</text>
22
- <text x="82" y="50" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="14" font-weight="500" fill="#22C55E" letter-spacing="2">stats</text>
18
+ <!-- Wordmark -->
19
+ <text x="90" y="35" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="22" font-weight="700" fill="#FFFFFF" letter-spacing="-0.3">cc-context</text>
20
+ <text x="90" y="55" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="15" font-weight="500" fill="#FFFFFF" letter-spacing="3" opacity="0.7">stats</text>
21
+
22
+ <!-- Right accent bar -->
23
+ <rect x="348" y="20" width="2" height="32" rx="1" fill="#FFFFFF" opacity="0.4"/>
23
24
  </svg>
@@ -1,7 +1,6 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 48" width="220" height="48">
2
- <!-- Background transparent -->
3
- <!-- Primary text -->
4
- <text x="0" y="28" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="22" font-weight="700" fill="#000000" letter-spacing="-0.5">cc-context</text>
5
- <!-- Accent -->
6
- <text x="0" y="44" font-family="'SF Mono', 'Fira Mono', 'Courier New', monospace" font-size="14" font-weight="500" fill="#22C55E" letter-spacing="2">stats</text>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 52" width="260" height="52">
2
+ <!-- Wordmark only — transparent background -->
3
+ <!-- "cc-context" bold, "stats" in golden amber below -->
4
+ <text x="0" y="30" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="24" font-weight="700" fill="#1A1A1A" letter-spacing="-0.3">cc-context</text>
5
+ <text x="0" y="48" font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace" font-size="15" font-weight="500" fill="#E9AB34" letter-spacing="3">stats</text>
7
6
  </svg>
@@ -0,0 +1,101 @@
1
+ # Architecture
2
+
3
+ ## Overview
4
+
5
+ cc-context-stats provides real-time context monitoring for Claude Code sessions. It consists of two main components:
6
+
7
+ 1. **Status Line** - A compact one-line display integrated into Claude Code's UI
8
+ 2. **Context Stats CLI** - A live terminal dashboard with ASCII graphs
9
+
10
+ ## System Architecture
11
+
12
+ ```
13
+ ┌─────────────┐ JSON stdin ┌──────────────────┐
14
+ │ Claude Code │ ──────────────────> │ Statusline Script │
15
+ │ (host) │ <────────────────── │ (sh/py/js) │
16
+ └─────────────┘ stdout text └──────┬───────────┘
17
+ │ writes
18
+
19
+ ┌──────────────────┐
20
+ │ State Files │
21
+ │ ~/.claude/ │
22
+ │ statusline/ │
23
+ └──────┬───────────┘
24
+ │ reads
25
+
26
+ ┌──────────────────┐
27
+ │ Context Stats CLI │
28
+ │ (Python) │
29
+ └──────────────────┘
30
+ ```
31
+
32
+ ## Component Details
33
+
34
+ ### Status Line Scripts
35
+
36
+ Three implementation languages with identical output:
37
+
38
+ | Script | Language | Dependencies |
39
+ | --------------------- | ---------- | ------------ |
40
+ | `statusline-full.sh` | Bash | `jq` |
41
+ | `statusline-git.sh` | Bash | `jq` |
42
+ | `statusline-minimal.sh` | Bash | `jq` |
43
+ | `statusline.py` | Python 3 | None |
44
+ | `statusline.js` | Node.js 18+| None |
45
+
46
+ **Data flow:**
47
+ 1. Claude Code pipes JSON state via stdin on each refresh
48
+ 2. Script parses model info, context tokens, session data
49
+ 3. Script reads `~/.claude/statusline.conf` for user preferences
50
+ 4. Script checks git status for branch/changes info
51
+ 5. Script writes state to `~/.claude/statusline/<session_id>.state`
52
+ 6. Script outputs formatted ANSI text to stdout
53
+
54
+ ### Python Package (`src/claude_statusline/`)
55
+
56
+ The pip-installable package provides both the statusline and context-stats CLI:
57
+
58
+ ```
59
+ src/claude_statusline/
60
+ ├── __init__.py
61
+ ├── __main__.py
62
+ ├── cli/
63
+ │ ├── statusline.py # claude-statusline entry point
64
+ │ └── context_stats.py # context-stats entry point
65
+ ├── core/
66
+ │ ├── colors.py # ANSI color management
67
+ │ ├── config.py # Configuration loading
68
+ │ ├── git.py # Git status detection
69
+ │ └── state.py # State file reading/writing
70
+ ├── formatters/
71
+ │ ├── layout.py # Output width/layout management
72
+ │ ├── time.py # Duration formatting
73
+ │ └── tokens.py # Token count formatting
74
+ ├── graphs/
75
+ │ ├── renderer.py # ASCII graph rendering
76
+ │ └── statistics.py # Data statistics
77
+ └── ui/
78
+ ├── icons.py # Unicode icons
79
+ └── waiting.py # Waiting animation
80
+ ```
81
+
82
+ ### State Files
83
+
84
+ State files persist token history between statusline refreshes:
85
+
86
+ ```
87
+ ~/.claude/statusline/statusline.<session_id>.state
88
+ ```
89
+
90
+ Each line is a CSV record with 14 comma-separated fields (timestamp, token counts, cost, session metadata, and context metrics). See [CSV_FORMAT.md](CSV_FORMAT.md) for the full field specification. The context-stats CLI reads these files to render graphs.
91
+
92
+ ## Data Privacy
93
+
94
+ All data stays local:
95
+ - State files are written to `~/.claude/statusline/`
96
+ - No network requests are made
97
+ - No telemetry or analytics
98
+
99
+ ## Configuration
100
+
101
+ User preferences are stored in `~/.claude/statusline.conf` as simple `key=value` pairs. See [Configuration](configuration.md) for details.
@@ -0,0 +1,40 @@
1
+ # CSV State File Format
2
+
3
+ State files are stored at `~/.claude/statusline/statusline.<session_id>.state`. Each line is a CSV record with 14 comma-separated fields.
4
+
5
+ ## Field Specification
6
+
7
+ | Index | Field | Type | Description |
8
+ |-------|-------|------|-------------|
9
+ | 0 | `timestamp` | integer | Unix timestamp in seconds |
10
+ | 1 | `total_input_tokens` | integer | Cumulative input tokens for the session |
11
+ | 2 | `total_output_tokens` | integer | Cumulative output tokens for the session |
12
+ | 3 | `current_input_tokens` | integer | Input tokens for the current request |
13
+ | 4 | `current_output_tokens` | integer | Output tokens for the current request |
14
+ | 5 | `cache_creation` | integer | Cache creation input tokens |
15
+ | 6 | `cache_read` | integer | Cache read input tokens |
16
+ | 7 | `cost_usd` | float | Total session cost in USD |
17
+ | 8 | `lines_added` | integer | Total lines added in session |
18
+ | 9 | `lines_removed` | integer | Total lines removed in session |
19
+ | 10 | `session_id` | string | Session identifier (UUID) |
20
+ | 11 | `model_id` | string | Model identifier (e.g., `claude-opus-4-5`) |
21
+ | 12 | `workspace_project_dir` | string | Project directory path (commas replaced with underscores) |
22
+ | 13 | `context_window_size` | integer | Context window size in tokens |
23
+
24
+ ## Constraints
25
+
26
+ - Fields are separated by commas with no quoting or escaping.
27
+ - The `workspace_project_dir` field (index 12) is sanitized before writing: all comma characters (`,`) are replaced with underscores (`_`) to prevent CSV corruption.
28
+ - Numeric fields default to `0` when absent. String fields default to empty string.
29
+ - Lines are newline-terminated (`\n`).
30
+ - Files are append-only.
31
+
32
+ ## Legacy Format
33
+
34
+ Older state files may contain 2-field lines: `timestamp,total_input_tokens`. The reader defaults all other fields to zero/empty for these lines.
35
+
36
+ ## Example
37
+
38
+ ```
39
+ 1710288000,75000,8500,50000,5000,10000,20000,0.05234,250,45,abc-123-def,claude-opus-4-5,/home/user/my-project,200000
40
+ ```
@@ -0,0 +1,60 @@
1
+ # Deployment
2
+
3
+ ## Distribution Channels
4
+
5
+ cc-context-stats is distributed through three channels:
6
+
7
+ | Channel | Package Name | Command |
8
+ | ------------ | ----------------- | ------------------------------------ |
9
+ | Shell script | N/A | `curl -fsSL .../install.sh \| bash` |
10
+ | PyPI | `cc-context-stats`| `pip install cc-context-stats` |
11
+ | npm | `cc-context-stats`| `npm install -g cc-context-stats` |
12
+
13
+ ## Publishing to PyPI
14
+
15
+ ```bash
16
+ # Ensure clean build
17
+ rm -rf dist/ build/
18
+
19
+ # Build
20
+ python -m build
21
+
22
+ # Check package
23
+ twine check dist/*
24
+
25
+ # Upload to PyPI
26
+ twine upload dist/*
27
+ ```
28
+
29
+ ## Publishing to npm
30
+
31
+ ```bash
32
+ # Verify package.json
33
+ npm pack --dry-run
34
+
35
+ # Publish
36
+ npm publish
37
+ ```
38
+
39
+ ## Release Workflow
40
+
41
+ The project uses GitHub Actions for automated releases (`.github/workflows/release.yml`):
42
+
43
+ 1. Create and push a version tag: `git tag v1.x.x && git push --tags`
44
+ 2. The release workflow automatically:
45
+ - Runs the full test suite
46
+ - Builds Python and npm packages
47
+ - Creates a GitHub Release with release notes
48
+
49
+ ## Version Management
50
+
51
+ Versions must be updated in sync across:
52
+
53
+ - `pyproject.toml` - `[project] version`
54
+ - `package.json` - `version`
55
+ - `CHANGELOG.md` - New version entry
56
+ - `RELEASE_NOTES.md` - Current release notes
57
+
58
+ ## Install Script
59
+
60
+ The `install.sh` script is fetched directly from the `main` branch on GitHub. Changes to the installer take effect immediately for new users running the curl one-liner.
@@ -0,0 +1,125 @@
1
+ # Development Guide
2
+
3
+ ## Prerequisites
4
+
5
+ - **Git** - Version control
6
+ - **jq** - JSON processor (for bash scripts)
7
+ - **Python 3.9+** - For Python package and testing
8
+ - **Node.js 18+** - For Node.js script and testing
9
+ - **Bats** - Bash Automated Testing System (optional, for bash tests)
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ # Clone the repository
15
+ git clone https://github.com/luongnv89/cc-context-stats.git
16
+ cd cc-context-stats
17
+
18
+ # Python setup
19
+ python3 -m venv venv
20
+ source venv/bin/activate
21
+ pip install -r requirements-dev.txt
22
+ pip install -e ".[dev]"
23
+
24
+ # Node.js setup
25
+ npm install
26
+
27
+ # Install pre-commit hooks
28
+ pre-commit install
29
+ ```
30
+
31
+ ## Project Layout
32
+
33
+ ```
34
+ cc-context-stats/
35
+ ├── src/claude_statusline/ # Python package source
36
+ ├── scripts/ # Standalone scripts (sh/py/js)
37
+ ├── tests/
38
+ │ ├── bash/ # Bats tests
39
+ │ ├── python/ # Pytest tests
40
+ │ └── node/ # Jest tests
41
+ ├── config/ # Configuration examples
42
+ ├── docs/ # Documentation
43
+ ├── .github/workflows/ # CI/CD
44
+ ├── pyproject.toml # Python build config
45
+ └── package.json # Node.js config
46
+ ```
47
+
48
+ ## Running Tests
49
+
50
+ ```bash
51
+ # All tests
52
+ npm test && pytest && bats tests/bash/*.bats
53
+
54
+ # Individual suites
55
+ pytest tests/python/ -v # Python
56
+ pytest tests/python/ -v --cov=scripts --cov-report=html # Python + coverage
57
+ npm test # Node.js (Jest)
58
+ npm run test:coverage # Node.js + coverage
59
+ bats tests/bash/*.bats # Bash
60
+ ```
61
+
62
+ ## Linting & Formatting
63
+
64
+ ```bash
65
+ # Run all checks via pre-commit
66
+ pre-commit run --all-files
67
+
68
+ # Individual tools
69
+ ruff check src/ scripts/statusline.py # Python lint
70
+ ruff format src/ scripts/statusline.py # Python format
71
+ npx eslint scripts/statusline.js # JavaScript lint
72
+ npx prettier --write scripts/statusline.js # JavaScript format
73
+ shellcheck scripts/*.sh install.sh # Bash lint
74
+ ```
75
+
76
+ ## Manual Testing
77
+
78
+ ```bash
79
+ # Test statusline scripts with mock input
80
+ echo '{"model":{"display_name":"Test"},"cwd":"/test","session_id":"abc123","context":{"tokens_remaining":64000,"context_window":200000}}' | python3 scripts/statusline.py
81
+
82
+ echo '{"model":{"display_name":"Test"}}' | node scripts/statusline.js
83
+
84
+ echo '{"model":{"display_name":"Test"}}' | bash scripts/statusline-full.sh
85
+ ```
86
+
87
+ ## Building
88
+
89
+ ```bash
90
+ # Python package
91
+ python -m build
92
+
93
+ # Verify package
94
+ twine check dist/*
95
+ ```
96
+
97
+ ## Cross-Script Consistency
98
+
99
+ All three implementations (bash, Python, Node.js) must produce identical output for the same input. When modifying status line behavior:
100
+
101
+ 1. Update all three script variants
102
+ 2. Run integration tests to verify parity
103
+ 3. Test on multiple platforms if possible
104
+
105
+ ## Debugging
106
+
107
+ ### State files
108
+
109
+ ```bash
110
+ # View current state files
111
+ ls -la ~/.claude/statusline/statusline.*.state
112
+
113
+ # Inspect state content
114
+ cat ~/.claude/statusline/statusline.<session_id>.state
115
+ ```
116
+
117
+ ### Verbose testing
118
+
119
+ ```bash
120
+ # Python with verbose output
121
+ pytest tests/python/ -v -s
122
+
123
+ # Node.js with verbose output
124
+ npx jest --verbose
125
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-context-stats",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Monitor your Claude Code session context in real-time - track token usage and never run out of context",
5
5
  "main": "scripts/statusline.js",
6
6
  "scripts": {
@@ -32,7 +32,7 @@
32
32
  "devDependencies": {
33
33
  "eslint": "^8.56.0",
34
34
  "jest": "^29.7.0",
35
- "prettier": "^3.2.0"
35
+ "prettier": "^3.8.1"
36
36
  },
37
37
  "engines": {
38
38
  "node": ">=18"
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cc-context-stats"
7
- version = "1.5.0"
7
+ version = "1.6.0"
8
8
  description = "Monitor your Claude Code session context in real-time - track token usage and never run out of context"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -116,13 +116,21 @@ visible_width() {
116
116
  }
117
117
 
118
118
  get_terminal_width() {
119
- # Return terminal width, fallback to 80
119
+ # Return terminal width for fit_to_width truncation.
120
+ # When running inside Claude Code's statusline subprocess, neither $COLUMNS
121
+ # nor tput can detect the real terminal width (they always return 80).
122
+ # If COLUMNS is explicitly set, trust it. Otherwise use 200 as default
123
+ # so no parts are unnecessarily dropped; Claude Code handles overflow.
120
124
  if [[ -n "$COLUMNS" ]]; then
121
125
  echo "$COLUMNS"
122
126
  else
123
127
  local cols
124
128
  cols=$(tput cols 2>/dev/null || echo 80)
125
- echo "$cols"
129
+ if [[ "$cols" -eq 80 ]]; then
130
+ echo 200
131
+ else
132
+ echo "$cols"
133
+ fi
126
134
  fi
127
135
  }
128
136
 
@@ -170,7 +178,7 @@ cost_usd=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
170
178
  lines_added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
171
179
  lines_removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
172
180
  model_id=$(echo "$input" | jq -r '.model.id // ""')
173
- workspace_project_dir=$(echo "$input" | jq -r '.workspace.project_dir // ""')
181
+ workspace_project_dir=$(echo "$input" | jq -r '.workspace.project_dir // ""' | tr ',' '_')
174
182
 
175
183
  if [[ "$total_size" -gt 0 && "$current_usage" != "null" ]]; then
176
184
  # Get tokens from current_usage (includes cache)