overcode 0.1.0__py3-none-any.whl

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 (43) hide show
  1. overcode/__init__.py +5 -0
  2. overcode/cli.py +812 -0
  3. overcode/config.py +72 -0
  4. overcode/daemon.py +1184 -0
  5. overcode/daemon_claude_skill.md +180 -0
  6. overcode/daemon_state.py +113 -0
  7. overcode/data_export.py +257 -0
  8. overcode/dependency_check.py +227 -0
  9. overcode/exceptions.py +219 -0
  10. overcode/history_reader.py +448 -0
  11. overcode/implementations.py +214 -0
  12. overcode/interfaces.py +49 -0
  13. overcode/launcher.py +434 -0
  14. overcode/logging_config.py +193 -0
  15. overcode/mocks.py +152 -0
  16. overcode/monitor_daemon.py +808 -0
  17. overcode/monitor_daemon_state.py +358 -0
  18. overcode/pid_utils.py +225 -0
  19. overcode/presence_logger.py +454 -0
  20. overcode/protocols.py +143 -0
  21. overcode/session_manager.py +606 -0
  22. overcode/settings.py +412 -0
  23. overcode/standing_instructions.py +276 -0
  24. overcode/status_constants.py +190 -0
  25. overcode/status_detector.py +339 -0
  26. overcode/status_history.py +164 -0
  27. overcode/status_patterns.py +264 -0
  28. overcode/summarizer_client.py +136 -0
  29. overcode/summarizer_component.py +312 -0
  30. overcode/supervisor_daemon.py +1000 -0
  31. overcode/supervisor_layout.sh +50 -0
  32. overcode/tmux_manager.py +228 -0
  33. overcode/tui.py +2549 -0
  34. overcode/tui_helpers.py +495 -0
  35. overcode/web_api.py +279 -0
  36. overcode/web_server.py +138 -0
  37. overcode/web_templates.py +563 -0
  38. overcode-0.1.0.dist-info/METADATA +87 -0
  39. overcode-0.1.0.dist-info/RECORD +43 -0
  40. overcode-0.1.0.dist-info/WHEEL +5 -0
  41. overcode-0.1.0.dist-info/entry_points.txt +2 -0
  42. overcode-0.1.0.dist-info/licenses/LICENSE +21 -0
  43. overcode-0.1.0.dist-info/top_level.txt +1 -0
overcode/launcher.py ADDED
@@ -0,0 +1,434 @@
1
+ """
2
+ Launcher for interactive Claude Code sessions in tmux windows.
3
+
4
+ All Claude sessions launched by overcode are interactive - users can
5
+ take over at any time. Initial prompts are sent as keystrokes after
6
+ Claude starts, not as CLI arguments.
7
+ """
8
+
9
+ import time
10
+ import subprocess
11
+ import tempfile
12
+ import os
13
+ from typing import List, Optional, TYPE_CHECKING
14
+ from pathlib import Path
15
+
16
+ import re
17
+
18
+ from .tmux_manager import TmuxManager
19
+ from .session_manager import SessionManager, Session
20
+ from .config import get_default_standing_instructions
21
+ from .dependency_check import require_tmux, require_claude
22
+ from .exceptions import TmuxNotFoundError, ClaudeNotFoundError, InvalidSessionNameError
23
+
24
+
25
+ # Valid session name pattern
26
+ SESSION_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
27
+
28
+
29
+ def validate_session_name(name: str) -> None:
30
+ """Validate session name format.
31
+
32
+ Args:
33
+ name: Session name to validate
34
+
35
+ Raises:
36
+ InvalidSessionNameError: If name is invalid
37
+ """
38
+ if not name:
39
+ raise InvalidSessionNameError(name, "name cannot be empty")
40
+ if not SESSION_NAME_PATTERN.match(name):
41
+ raise InvalidSessionNameError(name)
42
+
43
+ if TYPE_CHECKING:
44
+ pass # For future type hints
45
+
46
+
47
+ class ClaudeLauncher:
48
+ """Launches interactive Claude Code sessions in tmux windows.
49
+
50
+ All sessions are interactive - this is the only supported mode.
51
+ Users can take over any session at any time via tmux.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ tmux_session: str = "agents",
57
+ tmux_manager: TmuxManager = None,
58
+ session_manager: SessionManager = None,
59
+ ):
60
+ """Initialize the launcher.
61
+
62
+ Args:
63
+ tmux_session: Name of the tmux session to use
64
+ tmux_manager: Optional TmuxManager for dependency injection (testing)
65
+ session_manager: Optional SessionManager for dependency injection (testing)
66
+ """
67
+ self.tmux = tmux_manager if tmux_manager else TmuxManager(tmux_session)
68
+ self.sessions = session_manager if session_manager else SessionManager()
69
+
70
+ def launch(
71
+ self,
72
+ name: str,
73
+ start_directory: Optional[str] = None,
74
+ initial_prompt: Optional[str] = None,
75
+ skip_permissions: bool = False,
76
+ dangerously_skip_permissions: bool = False,
77
+ ) -> Optional[Session]:
78
+ """
79
+ Launch an interactive Claude Code session in a tmux window.
80
+
81
+ Args:
82
+ name: Name for this Claude session
83
+ start_directory: Starting directory for the session
84
+ initial_prompt: Optional initial prompt to send after Claude starts
85
+ skip_permissions: If True, use --permission-mode dontAsk
86
+ dangerously_skip_permissions: If True, use --dangerously-skip-permissions
87
+ (for testing only - bypasses folder trust dialog)
88
+
89
+ Returns:
90
+ Session object if successful, None otherwise
91
+ """
92
+ # Validate session name
93
+ try:
94
+ validate_session_name(name)
95
+ except InvalidSessionNameError as e:
96
+ print(f"Cannot launch: {e}")
97
+ return None
98
+
99
+ # Check dependencies before attempting to launch
100
+ try:
101
+ require_tmux()
102
+ require_claude()
103
+ except (TmuxNotFoundError, ClaudeNotFoundError) as e:
104
+ print(f"Cannot launch: {e}")
105
+ return None
106
+
107
+ # Check if a session with this name already exists
108
+ existing = self.sessions.get_session_by_name(name)
109
+ if existing:
110
+ # Check if its tmux window still exists
111
+ if self.tmux.window_exists(existing.tmux_window):
112
+ print(f"Session '{name}' already exists in window {existing.tmux_window}")
113
+ return existing
114
+ else:
115
+ # Window is gone, clean up the stale session
116
+ self.sessions.delete_session(existing.id)
117
+
118
+ # Ensure tmux session exists
119
+ if not self.tmux.ensure_session():
120
+ print(f"Failed to create tmux session '{self.tmux.session_name}'")
121
+ return None
122
+
123
+ # Create window
124
+ window_index = self.tmux.create_window(name, start_directory)
125
+ if window_index is None:
126
+ print(f"Failed to create tmux window '{name}'")
127
+ return None
128
+
129
+ # Build the claude command - always interactive
130
+ # Support CLAUDE_COMMAND env var for testing with mock
131
+ claude_command = os.environ.get("CLAUDE_COMMAND", "claude")
132
+ claude_cmd = [claude_command, "code"] if claude_command == "claude" else [claude_command]
133
+ if dangerously_skip_permissions:
134
+ claude_cmd.append("--dangerously-skip-permissions")
135
+ elif skip_permissions:
136
+ claude_cmd.extend(["--permission-mode", "dontAsk"])
137
+
138
+ # If MOCK_SCENARIO is set, prepend it to the command for testing
139
+ mock_scenario = os.environ.get("MOCK_SCENARIO")
140
+ if mock_scenario:
141
+ cmd_str = f"MOCK_SCENARIO={mock_scenario} python {' '.join(claude_cmd)}"
142
+ else:
143
+ cmd_str = " ".join(claude_cmd)
144
+
145
+ # Send command to window to start interactive Claude
146
+ if not self.tmux.send_keys(window_index, cmd_str, enter=True):
147
+ print(f"Failed to send command to window {window_index}")
148
+ return None
149
+
150
+ # Determine permissiveness mode based on flags
151
+ if dangerously_skip_permissions:
152
+ perm_mode = "bypass"
153
+ elif skip_permissions:
154
+ perm_mode = "permissive"
155
+ else:
156
+ perm_mode = "normal"
157
+
158
+ # Register session with default standing instructions from config
159
+ default_instructions = get_default_standing_instructions()
160
+ session = self.sessions.create_session(
161
+ name=name,
162
+ tmux_session=self.tmux.session_name,
163
+ tmux_window=window_index,
164
+ command=claude_cmd,
165
+ start_directory=start_directory,
166
+ standing_instructions=default_instructions,
167
+ permissiveness_mode=perm_mode
168
+ )
169
+
170
+ print(f"✓ Launched '{name}' in tmux window {window_index}")
171
+
172
+ # Send initial prompt if provided (after Claude starts)
173
+ if initial_prompt:
174
+ self._send_prompt_to_window(window_index, initial_prompt)
175
+
176
+ return session
177
+
178
+ def _send_prompt_to_window(
179
+ self,
180
+ window_index: int,
181
+ prompt: str,
182
+ startup_delay: float = 3.0,
183
+ ) -> bool:
184
+ """
185
+ Send a prompt to a Claude session via tmux keystrokes.
186
+
187
+ This sends the prompt as if the user typed it, so the session
188
+ remains fully interactive - the user can take over at any time.
189
+
190
+ Args:
191
+ window_index: The tmux window index
192
+ prompt: The prompt text to send
193
+ startup_delay: Seconds to wait for Claude to start (default: 3)
194
+
195
+ Returns:
196
+ True if successful, False otherwise
197
+ """
198
+ # Wait for Claude to start up
199
+ time.sleep(startup_delay)
200
+
201
+ # For large prompts, use tmux load-buffer/paste-buffer
202
+ # to avoid escaping issues and line length limits
203
+ lines = prompt.split('\n')
204
+ batch_size = 10
205
+
206
+ for i in range(0, len(lines), batch_size):
207
+ batch = lines[i:i + batch_size]
208
+ text = '\n'.join(batch)
209
+ if i + batch_size < len(lines):
210
+ text += '\n' # Add newline between batches
211
+
212
+ # Use tempfile for the buffer
213
+ temp_path = None
214
+ try:
215
+ with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt') as f:
216
+ temp_path = f.name
217
+ f.write(text)
218
+
219
+ subprocess.run(['tmux', 'load-buffer', temp_path], timeout=5, check=True)
220
+ subprocess.run([
221
+ 'tmux', 'paste-buffer', '-t',
222
+ f"{self.tmux.session_name}:{window_index}"
223
+ ], timeout=5, check=True)
224
+ except subprocess.SubprocessError as e:
225
+ print(f"Failed to send prompt batch: {e}")
226
+ return False
227
+ finally:
228
+ if temp_path:
229
+ try:
230
+ os.unlink(temp_path)
231
+ except OSError:
232
+ pass
233
+
234
+ time.sleep(0.1)
235
+
236
+ # Send Enter to submit the prompt
237
+ subprocess.run([
238
+ 'tmux', 'send-keys', '-t',
239
+ f"{self.tmux.session_name}:{window_index}",
240
+ '', 'Enter'
241
+ ])
242
+
243
+ return True
244
+
245
+ def attach(self):
246
+ """Attach to the tmux session"""
247
+ if not self.tmux.session_exists():
248
+ print(f"Error: tmux session '{self.tmux.session_name}' does not exist")
249
+ print("No active sessions to attach to. Launch a session first with 'overcode launch'")
250
+ return
251
+ self.tmux.attach_session()
252
+
253
+ def list_sessions(self, detect_terminated: bool = True, kill_untracked: bool = False) -> List[Session]:
254
+ """
255
+ List all registered sessions, detecting terminated ones.
256
+
257
+ Args:
258
+ detect_terminated: If True (default), check tmux and mark sessions as
259
+ "terminated" if their window no longer exists
260
+ kill_untracked: If True, kill tmux windows that aren't tracked in sessions.json
261
+
262
+ Returns:
263
+ List of all Session objects (including terminated ones)
264
+ """
265
+ all_sessions = self.sessions.list_sessions()
266
+
267
+ # Filter to only sessions belonging to this tmux session
268
+ my_sessions = [s for s in all_sessions if s.tmux_session == self.tmux.session_name]
269
+ other_sessions = [s for s in all_sessions if s.tmux_session != self.tmux.session_name]
270
+
271
+ # Detect terminated sessions (tmux window gone but session still tracked)
272
+ if detect_terminated:
273
+ newly_terminated = []
274
+ for session in my_sessions:
275
+ # Only check non-terminated sessions
276
+ if session.status != "terminated":
277
+ if not self.tmux.window_exists(session.tmux_window):
278
+ # Mark as terminated in state file
279
+ self.sessions.update_session_status(session.id, "terminated")
280
+ session.status = "terminated" # Update local object too
281
+ newly_terminated.append(session.name)
282
+
283
+ if newly_terminated:
284
+ print(f"Detected {len(newly_terminated)} terminated session(s): {', '.join(newly_terminated)}")
285
+
286
+ # Kill untracked windows (tmux windows exist but not tracked)
287
+ if kill_untracked and self.tmux.session_exists():
288
+ active_sessions = [s for s in my_sessions if s.status != "terminated"]
289
+ tracked_windows = {s.tmux_window for s in active_sessions}
290
+ tmux_windows = self.tmux.list_windows()
291
+
292
+ untracked_count = 0
293
+ for window_info in tmux_windows:
294
+ window_idx = int(window_info['index'])
295
+ # Don't kill window 0 (default shell) or tracked windows
296
+ if window_idx != 0 and window_idx not in tracked_windows:
297
+ window_name = window_info['name']
298
+ print(f"Killing untracked window {window_idx}: {window_name}")
299
+ self.tmux.kill_window(window_idx)
300
+ untracked_count += 1
301
+
302
+ if untracked_count > 0:
303
+ print(f"Killed {untracked_count} untracked window(s)")
304
+
305
+ return my_sessions + other_sessions
306
+
307
+ def cleanup_terminated_sessions(self) -> int:
308
+ """Remove all terminated sessions from state.
309
+
310
+ Returns:
311
+ Number of sessions cleaned up
312
+ """
313
+ all_sessions = self.sessions.list_sessions()
314
+ terminated = [s for s in all_sessions if s.status == "terminated"]
315
+
316
+ for session in terminated:
317
+ self.sessions.delete_session(session.id)
318
+
319
+ return len(terminated)
320
+
321
+ def kill_session(self, name: str) -> bool:
322
+ """Kill a session by name.
323
+
324
+ Handles both active sessions and stale sessions (where tmux window/session
325
+ no longer exists, e.g., after a machine reboot).
326
+ """
327
+ session = self.sessions.get_session_by_name(name)
328
+ if session is None:
329
+ print(f"Session '{name}' not found")
330
+ return False
331
+
332
+ # Check if the tmux window/session still exists
333
+ window_exists = self.tmux.window_exists(session.tmux_window)
334
+
335
+ if window_exists:
336
+ # Active session - try to kill the tmux window
337
+ if self.tmux.kill_window(session.tmux_window):
338
+ self.sessions.delete_session(session.id)
339
+ print(f"✓ Killed session '{name}'")
340
+ return True
341
+ else:
342
+ print(f"Failed to kill tmux window for '{name}'")
343
+ return False
344
+ else:
345
+ # Stale session - tmux window/session is already gone (e.g., after reboot)
346
+ # Just clean up the state file
347
+ self.sessions.delete_session(session.id)
348
+ print(f"✓ Cleaned up stale session '{name}' (tmux window no longer exists)")
349
+ return True
350
+
351
+ def send_to_session(self, name: str, text: str, enter: bool = True) -> bool:
352
+ """Send text/keys to a session by name.
353
+
354
+ Args:
355
+ name: Name of the session
356
+ text: Text to send (or special key like "Enter", "Escape")
357
+ enter: Whether to press Enter after the text (default: True)
358
+
359
+ Returns:
360
+ True if successful, False otherwise
361
+ """
362
+ session = self.sessions.get_session_by_name(name)
363
+ if session is None:
364
+ print(f"Session '{name}' not found")
365
+ return False
366
+
367
+ # Handle special keys
368
+ special_keys = {
369
+ "enter": "", # Empty string + Enter = just press Enter
370
+ "escape": "Escape",
371
+ "esc": "Escape",
372
+ "tab": "Tab",
373
+ "up": "Up",
374
+ "down": "Down",
375
+ "left": "Left",
376
+ "right": "Right",
377
+ }
378
+
379
+ # Check if it's a special key
380
+ text_lower = text.lower().strip()
381
+ success = False
382
+ if text_lower in special_keys:
383
+ key = special_keys[text_lower]
384
+ if key == "":
385
+ # Just press Enter
386
+ success = self.tmux.send_keys(session.tmux_window, "", enter=True)
387
+ else:
388
+ # Send special key without Enter
389
+ success = self.tmux.send_keys(session.tmux_window, key, enter=False)
390
+ else:
391
+ # Regular text
392
+ success = self.tmux.send_keys(session.tmux_window, text, enter=enter)
393
+
394
+ # Update last activity on success (steers_count is tracked via supervisor log parsing)
395
+ if success:
396
+ self.sessions.update_stats(
397
+ session.id,
398
+ last_activity=time.strftime("%Y-%m-%dT%H:%M:%S")
399
+ )
400
+
401
+ return success
402
+
403
+ def get_session_output(self, name: str, lines: int = 50) -> Optional[str]:
404
+ """Get recent output from a session.
405
+
406
+ Args:
407
+ name: Name of the session
408
+ lines: Number of lines to capture (default: 50)
409
+
410
+ Returns:
411
+ The captured output, or None if session not found
412
+ """
413
+ session = self.sessions.get_session_by_name(name)
414
+ if session is None:
415
+ print(f"Session '{name}' not found")
416
+ return None
417
+
418
+ try:
419
+ result = subprocess.run(
420
+ [
421
+ "tmux", "capture-pane",
422
+ "-t", f"{self.tmux.session_name}:{session.tmux_window}",
423
+ "-p", # Print to stdout
424
+ "-S", f"-{lines}", # Capture last N lines
425
+ ],
426
+ capture_output=True,
427
+ text=True,
428
+ timeout=5
429
+ )
430
+ if result.returncode == 0:
431
+ return result.stdout.rstrip()
432
+ return None
433
+ except subprocess.SubprocessError:
434
+ return None
@@ -0,0 +1,193 @@
1
+ """
2
+ Structured logging configuration for Overcode.
3
+
4
+ Provides centralized logging configuration with support for:
5
+ - Console output with Rich formatting (optional)
6
+ - File output for persistent logs
7
+ - Different log levels per component
8
+ - Structured log messages
9
+ """
10
+
11
+ import logging
12
+ import sys
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+
18
+ # Default log directory
19
+ DEFAULT_LOG_DIR = Path.home() / ".overcode" / "logs"
20
+
21
+
22
+ def get_logger(name: str) -> logging.Logger:
23
+ """Get a logger for the specified component.
24
+
25
+ Args:
26
+ name: Component name (e.g., 'daemon', 'launcher', 'tui')
27
+
28
+ Returns:
29
+ Configured logger instance
30
+ """
31
+ return logging.getLogger(f"overcode.{name}")
32
+
33
+
34
+ def setup_logging(
35
+ level: int = logging.INFO,
36
+ log_file: Optional[Path] = None,
37
+ console: bool = True,
38
+ rich_console: bool = False,
39
+ ) -> None:
40
+ """Configure logging for the application.
41
+
42
+ Args:
43
+ level: Logging level (default: INFO)
44
+ log_file: Optional path to log file
45
+ console: Whether to log to console (default: True)
46
+ rich_console: Whether to use Rich for console output (default: False)
47
+ """
48
+ root_logger = logging.getLogger("overcode")
49
+ root_logger.setLevel(level)
50
+
51
+ # Clear existing handlers
52
+ root_logger.handlers.clear()
53
+
54
+ # Log format
55
+ fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
56
+ date_fmt = "%Y-%m-%d %H:%M:%S"
57
+
58
+ # Console handler
59
+ if console:
60
+ if rich_console:
61
+ try:
62
+ from rich.logging import RichHandler
63
+
64
+ console_handler = RichHandler(
65
+ show_time=True,
66
+ show_path=False,
67
+ markup=True,
68
+ rich_tracebacks=True,
69
+ )
70
+ console_handler.setLevel(level)
71
+ root_logger.addHandler(console_handler)
72
+ except ImportError:
73
+ # Fall back to standard console handler
74
+ console_handler = logging.StreamHandler(sys.stderr)
75
+ console_handler.setLevel(level)
76
+ console_handler.setFormatter(logging.Formatter(fmt, date_fmt))
77
+ root_logger.addHandler(console_handler)
78
+ else:
79
+ console_handler = logging.StreamHandler(sys.stderr)
80
+ console_handler.setLevel(level)
81
+ console_handler.setFormatter(logging.Formatter(fmt, date_fmt))
82
+ root_logger.addHandler(console_handler)
83
+
84
+ # File handler
85
+ if log_file:
86
+ log_file.parent.mkdir(parents=True, exist_ok=True)
87
+ file_handler = logging.FileHandler(log_file)
88
+ file_handler.setLevel(level)
89
+ file_handler.setFormatter(logging.Formatter(fmt, date_fmt))
90
+ root_logger.addHandler(file_handler)
91
+
92
+
93
+ def setup_daemon_logging(log_file: Optional[Path] = None) -> logging.Logger:
94
+ """Configure logging specifically for the daemon.
95
+
96
+ Uses file logging by default to the daemon log directory.
97
+
98
+ Args:
99
+ log_file: Optional custom log file path
100
+
101
+ Returns:
102
+ Configured daemon logger
103
+ """
104
+ if log_file is None:
105
+ DEFAULT_LOG_DIR.mkdir(parents=True, exist_ok=True)
106
+ log_file = DEFAULT_LOG_DIR / "daemon.log"
107
+
108
+ setup_logging(
109
+ level=logging.INFO,
110
+ log_file=log_file,
111
+ console=True,
112
+ rich_console=True,
113
+ )
114
+
115
+ return get_logger("daemon")
116
+
117
+
118
+ def setup_cli_logging() -> logging.Logger:
119
+ """Configure logging for CLI commands.
120
+
121
+ Uses minimal console output since CLI uses Rich for user feedback.
122
+
123
+ Returns:
124
+ Configured CLI logger
125
+ """
126
+ setup_logging(
127
+ level=logging.WARNING, # Only warnings and errors
128
+ console=True,
129
+ rich_console=False,
130
+ )
131
+
132
+ return get_logger("cli")
133
+
134
+
135
+ class StructuredLogger:
136
+ """Logger that supports structured log messages with context."""
137
+
138
+ def __init__(self, logger: logging.Logger):
139
+ self._logger = logger
140
+ self._context: dict = {}
141
+
142
+ def with_context(self, **kwargs) -> "StructuredLogger":
143
+ """Create a new logger with additional context.
144
+
145
+ Args:
146
+ **kwargs: Key-value pairs to add to log context
147
+
148
+ Returns:
149
+ New StructuredLogger with merged context
150
+ """
151
+ new_logger = StructuredLogger(self._logger)
152
+ new_logger._context = {**self._context, **kwargs}
153
+ return new_logger
154
+
155
+ def _format_message(self, message: str, **kwargs) -> str:
156
+ """Format message with context."""
157
+ context = {**self._context, **kwargs}
158
+ if context:
159
+ context_str = " ".join(f"{k}={v}" for k, v in context.items())
160
+ return f"{message} [{context_str}]"
161
+ return message
162
+
163
+ def debug(self, message: str, **kwargs) -> None:
164
+ """Log debug message."""
165
+ self._logger.debug(self._format_message(message, **kwargs))
166
+
167
+ def info(self, message: str, **kwargs) -> None:
168
+ """Log info message."""
169
+ self._logger.info(self._format_message(message, **kwargs))
170
+
171
+ def warning(self, message: str, **kwargs) -> None:
172
+ """Log warning message."""
173
+ self._logger.warning(self._format_message(message, **kwargs))
174
+
175
+ def error(self, message: str, **kwargs) -> None:
176
+ """Log error message."""
177
+ self._logger.error(self._format_message(message, **kwargs))
178
+
179
+ def exception(self, message: str, **kwargs) -> None:
180
+ """Log exception with traceback."""
181
+ self._logger.exception(self._format_message(message, **kwargs))
182
+
183
+
184
+ def get_structured_logger(name: str) -> StructuredLogger:
185
+ """Get a structured logger for the specified component.
186
+
187
+ Args:
188
+ name: Component name
189
+
190
+ Returns:
191
+ StructuredLogger instance
192
+ """
193
+ return StructuredLogger(get_logger(name))