1+ #!/usr/bin/env python3
2+ """
3+ Claude Code Context Monitor
4+ Real-time context usage monitoring with visual indicators and session analytics
5+ """
6+
7+ import json
8+ import sys
9+ import os
10+ import re
11+
12+ def parse_context_from_transcript (transcript_path ):
13+ """Parse context usage from transcript file."""
14+ if not transcript_path or not os .path .exists (transcript_path ):
15+ return None
16+
17+ try :
18+ with open (transcript_path , 'r' ) as f :
19+ lines = f .readlines ()
20+
21+ # Check last 15 lines for context information
22+ recent_lines = lines [- 15 :] if len (lines ) > 15 else lines
23+
24+ for line in reversed (recent_lines ):
25+ try :
26+ data = json .loads (line .strip ())
27+
28+ # Method 1: Parse usage tokens from assistant messages
29+ if data .get ('type' ) == 'assistant' :
30+ message = data .get ('message' , {})
31+ usage = message .get ('usage' , {})
32+
33+ if usage :
34+ input_tokens = usage .get ('input_tokens' , 0 )
35+ cache_read = usage .get ('cache_read_input_tokens' , 0 )
36+ cache_creation = usage .get ('cache_creation_input_tokens' , 0 )
37+
38+ # Estimate context usage (assume 200k context for Claude Sonnet)
39+ total_tokens = input_tokens + cache_read + cache_creation
40+ if total_tokens > 0 :
41+ percent_used = min (100 , (total_tokens / 200000 ) * 100 )
42+ return {
43+ 'percent' : percent_used ,
44+ 'tokens' : total_tokens ,
45+ 'method' : 'usage'
46+ }
47+
48+ # Method 2: Parse system context warnings
49+ elif data .get ('type' ) == 'system_message' :
50+ content = data .get ('content' , '' )
51+
52+ # "Context left until auto-compact: X%"
53+ match = re .search (r'Context left until auto-compact: (\d+)%' , content )
54+ if match :
55+ percent_left = int (match .group (1 ))
56+ return {
57+ 'percent' : 100 - percent_left ,
58+ 'warning' : 'auto-compact' ,
59+ 'method' : 'system'
60+ }
61+
62+ # "Context low (X% remaining)"
63+ match = re .search (r'Context low \((\d+)% remaining\)' , content )
64+ if match :
65+ percent_left = int (match .group (1 ))
66+ return {
67+ 'percent' : 100 - percent_left ,
68+ 'warning' : 'low' ,
69+ 'method' : 'system'
70+ }
71+
72+ except (json .JSONDecodeError , KeyError , ValueError ):
73+ continue
74+
75+ return None
76+
77+ except (FileNotFoundError , PermissionError ):
78+ return None
79+
80+ def get_context_display (context_info ):
81+ """Generate context display with visual indicators."""
82+ if not context_info :
83+ return "🔵 ???"
84+
85+ percent = context_info .get ('percent' , 0 )
86+ warning = context_info .get ('warning' )
87+
88+ # Color and icon based on usage level
89+ if percent >= 95 :
90+ icon , color = "🚨" , "\033 [31;1m" # Blinking red
91+ alert = "CRIT"
92+ elif percent >= 90 :
93+ icon , color = "🔴" , "\033 [31m" # Red
94+ alert = "HIGH"
95+ elif percent >= 75 :
96+ icon , color = "🟠" , "\033 [91m" # Light red
97+ alert = ""
98+ elif percent >= 50 :
99+ icon , color = "🟡" , "\033 [33m" # Yellow
100+ alert = ""
101+ else :
102+ icon , color = "🟢" , "\033 [32m" # Green
103+ alert = ""
104+
105+ # Create progress bar
106+ segments = 8
107+ filled = int ((percent / 100 ) * segments )
108+ bar = "█" * filled + "▁" * (segments - filled )
109+
110+ # Special warnings
111+ if warning == 'auto-compact' :
112+ alert = "AUTO-COMPACT!"
113+ elif warning == 'low' :
114+ alert = "LOW!"
115+
116+ reset = "\033 [0m"
117+ alert_str = f" { alert } " if alert else ""
118+
119+ return f"{ icon } { color } { bar } { reset } { percent :.0f} %{ alert_str } "
120+
121+ def get_directory_display (workspace_data ):
122+ """Get directory display name."""
123+ current_dir = workspace_data .get ('current_dir' , '' )
124+ project_dir = workspace_data .get ('project_dir' , '' )
125+
126+ if current_dir and project_dir :
127+ if current_dir .startswith (project_dir ):
128+ rel_path = current_dir [len (project_dir ):].lstrip ('/' )
129+ return rel_path or os .path .basename (project_dir )
130+ else :
131+ return os .path .basename (current_dir )
132+ elif project_dir :
133+ return os .path .basename (project_dir )
134+ elif current_dir :
135+ return os .path .basename (current_dir )
136+ else :
137+ return "unknown"
138+
139+ def get_session_metrics (cost_data ):
140+ """Get session metrics display."""
141+ if not cost_data :
142+ return ""
143+
144+ metrics = []
145+
146+ # Cost
147+ cost_usd = cost_data .get ('total_cost_usd' , 0 )
148+ if cost_usd > 0 :
149+ if cost_usd >= 0.10 :
150+ cost_color = "\033 [31m" # Red for expensive
151+ elif cost_usd >= 0.05 :
152+ cost_color = "\033 [33m" # Yellow for moderate
153+ else :
154+ cost_color = "\033 [32m" # Green for cheap
155+
156+ cost_str = f"{ cost_usd * 100 :.0f} ¢" if cost_usd < 0.01 else f"${ cost_usd :.3f} "
157+ metrics .append (f"{ cost_color } 💰 { cost_str } \033 [0m" )
158+
159+ # Duration
160+ duration_ms = cost_data .get ('total_duration_ms' , 0 )
161+ if duration_ms > 0 :
162+ minutes = duration_ms / 60000
163+ if minutes >= 30 :
164+ duration_color = "\033 [33m" # Yellow for long sessions
165+ else :
166+ duration_color = "\033 [32m" # Green
167+
168+ if minutes < 1 :
169+ duration_str = f"{ duration_ms // 1000 } s"
170+ else :
171+ duration_str = f"{ minutes :.0f} m"
172+
173+ metrics .append (f"{ duration_color } ⏱ { duration_str } \033 [0m" )
174+
175+ # Lines changed
176+ lines_added = cost_data .get ('total_lines_added' , 0 )
177+ lines_removed = cost_data .get ('total_lines_removed' , 0 )
178+ if lines_added > 0 or lines_removed > 0 :
179+ net_lines = lines_added - lines_removed
180+
181+ if net_lines > 0 :
182+ lines_color = "\033 [32m" # Green for additions
183+ elif net_lines < 0 :
184+ lines_color = "\033 [31m" # Red for deletions
185+ else :
186+ lines_color = "\033 [33m" # Yellow for neutral
187+
188+ sign = "+" if net_lines >= 0 else ""
189+ metrics .append (f"{ lines_color } 📝 { sign } { net_lines } \033 [0m" )
190+
191+ return f" \033 [90m|\033 [0m { ' ' .join (metrics )} " if metrics else ""
192+
193+ def main ():
194+ try :
195+ # Read JSON input from Claude Code
196+ data = json .load (sys .stdin )
197+
198+ # Extract information
199+ model_name = data .get ('model' , {}).get ('display_name' , 'Claude' )
200+ workspace = data .get ('workspace' , {})
201+ transcript_path = data .get ('transcript_path' , '' )
202+ cost_data = data .get ('cost' , {})
203+
204+ # Parse context usage
205+ context_info = parse_context_from_transcript (transcript_path )
206+
207+ # Build status components
208+ context_display = get_context_display (context_info )
209+ directory = get_directory_display (workspace )
210+ session_metrics = get_session_metrics (cost_data )
211+
212+ # Model display with context-aware coloring
213+ if context_info :
214+ percent = context_info .get ('percent' , 0 )
215+ if percent >= 90 :
216+ model_color = "\033 [31m" # Red
217+ elif percent >= 75 :
218+ model_color = "\033 [33m" # Yellow
219+ else :
220+ model_color = "\033 [32m" # Green
221+
222+ model_display = f"{ model_color } [{ model_name } ]\033 [0m"
223+ else :
224+ model_display = f"\033 [94m[{ model_name } ]\033 [0m"
225+
226+ # Combine all components
227+ status_line = f"{ model_display } \033 [93m📁 { directory } \033 [0m 🧠 { context_display } { session_metrics } "
228+
229+ print (status_line )
230+
231+ except Exception as e :
232+ # Fallback display on any error
233+ print (f"\033 [94m[Claude]\033 [0m \033 [93m📁 { os .path .basename (os .getcwd ())} \033 [0m 🧠 \033 [31m[Error: { str (e )[:20 ]} ]\033 [0m" )
234+
235+ if __name__ == "__main__" :
236+ main ()
0 commit comments