77
88from .quiet_console import console
99
10- from .claude_log_utils import (
11- get_claude_session_log_path ,
12- parse_jsonl_line ,
13- read_existing_log_lines ,
14- validate_log_entry ,
15- format_log_for_api
16- )
10+ from .claude_log_utils import get_claude_session_log_path , parse_jsonl_line , read_existing_log_lines , validate_log_entry , format_log_for_api
1711from .claude_session_api import send_claude_session_log
1812
1913
2014class ClaudeLogWatcher :
2115 """Watches Claude Code session log files for new entries and sends them to the API."""
22-
23- def __init__ (
24- self ,
25- session_id : str ,
26- org_id : Optional [int ] = None ,
27- poll_interval : float = 1.0 ,
28- on_log_entry : Optional [Callable [[Dict [str , Any ]], None ]] = None
29- ):
16+
17+ def __init__ (self , session_id : str , org_id : Optional [int ] = None , poll_interval : float = 1.0 , on_log_entry : Optional [Callable [[Dict [str , Any ]], None ]] = None ):
3018 """Initialize the log watcher.
31-
19+
3220 Args:
3321 session_id: The Claude session ID to watch
3422 org_id: Organization ID for API calls
@@ -39,56 +27,56 @@ def __init__(
3927 self .org_id = org_id
4028 self .poll_interval = poll_interval
4129 self .on_log_entry = on_log_entry
42-
30+
4331 self .log_path = get_claude_session_log_path (session_id )
4432 self .last_line_count = 0
4533 self .is_running = False
4634 self .watcher_thread : Optional [threading .Thread ] = None
47-
35+
4836 # Stats
4937 self .total_entries_processed = 0
5038 self .total_entries_sent = 0
5139 self .total_send_failures = 0
52-
40+
5341 def start (self ) -> bool :
5442 """Start the log watcher in a background thread.
55-
43+
5644 Returns:
5745 True if started successfully, False otherwise
5846 """
5947 if self .is_running :
6048 console .print (f"⚠️ Log watcher for session { self .session_id [:8 ]} ... is already running" , style = "yellow" )
6149 return False
62-
50+
6351 # Initialize line count
6452 self .last_line_count = read_existing_log_lines (self .log_path )
65-
53+
6654 self .is_running = True
6755 self .watcher_thread = threading .Thread (target = self ._watch_loop , daemon = True )
6856 self .watcher_thread .start ()
69-
57+
7058 console .print (f"📋 Started log watcher for session { self .session_id [:8 ]} ..." , style = "green" )
7159 console .print (f" Log file: { self .log_path } " , style = "dim" )
7260 console .print (f" Starting from line: { self .last_line_count + 1 } " , style = "dim" )
73-
61+
7462 return True
75-
63+
7664 def stop (self ) -> None :
7765 """Stop the log watcher."""
7866 if not self .is_running :
7967 return
80-
68+
8169 self .is_running = False
82-
70+
8371 if self .watcher_thread and self .watcher_thread .is_alive ():
8472 self .watcher_thread .join (timeout = 2.0 )
85-
73+
8674 console .print (f"📋 Stopped log watcher for session { self .session_id [:8 ]} ..." , style = "dim" )
8775 console .print (f" Processed: { self .total_entries_processed } entries" , style = "dim" )
8876 console .print (f" Sent: { self .total_entries_sent } entries" , style = "dim" )
8977 if self .total_send_failures > 0 :
9078 console .print (f" Failures: { self .total_send_failures } entries" , style = "yellow" )
91-
79+
9280 def _watch_loop (self ) -> None :
9381 """Main watching loop that runs in a background thread."""
9482 while self .is_running :
@@ -98,104 +86,104 @@ def _watch_loop(self) -> None:
9886 except Exception as e :
9987 console .print (f"⚠️ Error in log watcher: { e } " , style = "yellow" )
10088 time .sleep (self .poll_interval * 2 ) # Back off on errors
101-
89+
10290 def _check_for_new_entries (self ) -> None :
10391 """Check for new log entries and process them."""
10492 if not self .log_path .exists ():
10593 return
106-
94+
10795 try :
10896 current_line_count = read_existing_log_lines (self .log_path )
109-
97+
11098 if current_line_count > self .last_line_count :
11199 new_entries = self ._read_new_lines (self .last_line_count , current_line_count )
112-
100+
113101 for entry in new_entries :
114102 self ._process_log_entry (entry )
115-
103+
116104 self .last_line_count = current_line_count
117-
105+
118106 except Exception as e :
119107 console .print (f"⚠️ Error reading log file: { e } " , style = "yellow" )
120-
108+
121109 def _read_new_lines (self , start_line : int , end_line : int ) -> list [Dict [str , Any ]]:
122110 """Read new lines from the log file.
123-
111+
124112 Args:
125113 start_line: Line number to start from (0-indexed)
126114 end_line: Line number to end at (0-indexed, exclusive)
127-
115+
128116 Returns:
129117 List of parsed log entries
130118 """
131119 entries = []
132-
120+
133121 try :
134- with open (self .log_path , 'r' , encoding = ' utf-8' ) as f :
122+ with open (self .log_path , "r" , encoding = " utf-8" ) as f :
135123 lines = f .readlines ()
136-
124+
137125 # Read only the new lines
138126 for i in range (start_line , min (end_line , len (lines ))):
139127 line = lines [i ]
140128 entry = parse_jsonl_line (line )
141-
129+
142130 if entry is not None :
143131 entries .append (entry )
144-
132+
145133 except (OSError , UnicodeDecodeError ) as e :
146134 console .print (f"⚠️ Error reading log file: { e } " , style = "yellow" )
147-
135+
148136 return entries
149-
137+
150138 def _process_log_entry (self , log_entry : Dict [str , Any ]) -> None :
151139 """Process a single log entry.
152-
140+
153141 Args:
154142 log_entry: The parsed log entry
155143 """
156144 self .total_entries_processed += 1
157-
145+
158146 # Validate the entry
159147 if not validate_log_entry (log_entry ):
160148 console .print (f"⚠️ Invalid log entry skipped: { log_entry } " , style = "yellow" )
161149 return
162-
150+
163151 # Format for API
164152 formatted_entry = format_log_for_api (log_entry )
165-
153+
166154 # Call optional callback
167155 if self .on_log_entry :
168156 try :
169157 self .on_log_entry (formatted_entry )
170158 except Exception as e :
171159 console .print (f"⚠️ Error in log entry callback: { e } " , style = "yellow" )
172-
160+
173161 # Send to API
174162 self ._send_log_entry (formatted_entry )
175-
163+
176164 def _send_log_entry (self , log_entry : Dict [str , Any ]) -> None :
177165 """Send a log entry to the API.
178-
166+
179167 Args:
180168 log_entry: The formatted log entry
181169 """
182170 try :
183171 success = send_claude_session_log (self .session_id , log_entry , self .org_id )
184-
172+
185173 if success :
186174 self .total_entries_sent += 1
187175 # Only show verbose output in debug mode
188176 console .print (f"📤 Sent log entry: { log_entry .get ('type' , 'unknown' )} " , style = "dim" )
189177 else :
190178 self .total_send_failures += 1
191-
179+
192180 except Exception as e :
193181 self .total_send_failures += 1
194182 console .print (f"⚠️ Failed to send log entry: { e } " , style = "yellow" )
195-
183+
196184 def get_stats (self ) -> Dict [str , Any ]:
197185 """Get watcher statistics.
198-
186+
199187 Returns:
200188 Dictionary with watcher stats
201189 """
@@ -208,96 +196,79 @@ def get_stats(self) -> Dict[str, Any]:
208196 "total_entries_processed" : self .total_entries_processed ,
209197 "total_entries_sent" : self .total_entries_sent ,
210198 "total_send_failures" : self .total_send_failures ,
211- "success_rate" : (
212- self .total_entries_sent / max (1 , self .total_entries_processed ) * 100
213- if self .total_entries_processed > 0 else 0
214- )
199+ "success_rate" : (self .total_entries_sent / max (1 , self .total_entries_processed ) * 100 if self .total_entries_processed > 0 else 0 ),
215200 }
216201
217202
218203class ClaudeLogWatcherManager :
219204 """Manages multiple log watchers for different sessions."""
220-
205+
221206 def __init__ (self ):
222207 self .watchers : Dict [str , ClaudeLogWatcher ] = {}
223-
224- def start_watcher (
225- self ,
226- session_id : str ,
227- org_id : Optional [int ] = None ,
228- poll_interval : float = 1.0 ,
229- on_log_entry : Optional [Callable [[Dict [str , Any ]], None ]] = None
230- ) -> bool :
208+
209+ def start_watcher (self , session_id : str , org_id : Optional [int ] = None , poll_interval : float = 1.0 , on_log_entry : Optional [Callable [[Dict [str , Any ]], None ]] = None ) -> bool :
231210 """Start a log watcher for a session.
232-
211+
233212 Args:
234213 session_id: The Claude session ID
235214 org_id: Organization ID for API calls
236215 poll_interval: How often to check for new entries (seconds)
237216 on_log_entry: Optional callback for each new log entry
238-
217+
239218 Returns:
240219 True if started successfully, False otherwise
241220 """
242221 if session_id in self .watchers :
243222 console .print (f"⚠️ Watcher for session { session_id [:8 ]} ... already exists" , style = "yellow" )
244223 return False
245-
246- watcher = ClaudeLogWatcher (
247- session_id = session_id ,
248- org_id = org_id ,
249- poll_interval = poll_interval ,
250- on_log_entry = on_log_entry
251- )
252-
224+
225+ watcher = ClaudeLogWatcher (session_id = session_id , org_id = org_id , poll_interval = poll_interval , on_log_entry = on_log_entry )
226+
253227 if watcher .start ():
254228 self .watchers [session_id ] = watcher
255229 return True
256230 return False
257-
231+
258232 def stop_watcher (self , session_id : str ) -> None :
259233 """Stop a log watcher for a session.
260-
234+
261235 Args:
262236 session_id: The Claude session ID
263237 """
264238 if session_id in self .watchers :
265239 self .watchers [session_id ].stop ()
266240 del self .watchers [session_id ]
267-
241+
268242 def stop_all_watchers (self ) -> None :
269243 """Stop all active watchers."""
270244 for session_id in list (self .watchers .keys ()):
271245 self .stop_watcher (session_id )
272-
246+
273247 def get_active_sessions (self ) -> list [str ]:
274248 """Get list of active session IDs being watched.
275-
249+
276250 Returns:
277251 List of session IDs
278252 """
279253 return list (self .watchers .keys ())
280-
254+
281255 def get_watcher_stats (self , session_id : str ) -> Optional [Dict [str , Any ]]:
282256 """Get stats for a specific watcher.
283-
257+
284258 Args:
285259 session_id: The Claude session ID
286-
260+
287261 Returns:
288262 Watcher stats or None if not found
289263 """
290264 if session_id in self .watchers :
291265 return self .watchers [session_id ].get_stats ()
292266 return None
293-
267+
294268 def get_all_stats (self ) -> Dict [str , Dict [str , Any ]]:
295269 """Get stats for all active watchers.
296-
270+
297271 Returns:
298272 Dictionary mapping session IDs to their stats
299273 """
300- return {
301- session_id : watcher .get_stats ()
302- for session_id , watcher in self .watchers .items ()
303- }
274+ return {session_id : watcher .get_stats () for session_id , watcher in self .watchers .items ()}
0 commit comments