kollabor 0.4.9__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 (128) hide show
  1. core/__init__.py +18 -0
  2. core/application.py +578 -0
  3. core/cli.py +193 -0
  4. core/commands/__init__.py +43 -0
  5. core/commands/executor.py +277 -0
  6. core/commands/menu_renderer.py +319 -0
  7. core/commands/parser.py +186 -0
  8. core/commands/registry.py +331 -0
  9. core/commands/system_commands.py +479 -0
  10. core/config/__init__.py +7 -0
  11. core/config/llm_task_config.py +110 -0
  12. core/config/loader.py +501 -0
  13. core/config/manager.py +112 -0
  14. core/config/plugin_config_manager.py +346 -0
  15. core/config/plugin_schema.py +424 -0
  16. core/config/service.py +399 -0
  17. core/effects/__init__.py +1 -0
  18. core/events/__init__.py +12 -0
  19. core/events/bus.py +129 -0
  20. core/events/executor.py +154 -0
  21. core/events/models.py +258 -0
  22. core/events/processor.py +176 -0
  23. core/events/registry.py +289 -0
  24. core/fullscreen/__init__.py +19 -0
  25. core/fullscreen/command_integration.py +290 -0
  26. core/fullscreen/components/__init__.py +12 -0
  27. core/fullscreen/components/animation.py +258 -0
  28. core/fullscreen/components/drawing.py +160 -0
  29. core/fullscreen/components/matrix_components.py +177 -0
  30. core/fullscreen/manager.py +302 -0
  31. core/fullscreen/plugin.py +204 -0
  32. core/fullscreen/renderer.py +282 -0
  33. core/fullscreen/session.py +324 -0
  34. core/io/__init__.py +52 -0
  35. core/io/buffer_manager.py +362 -0
  36. core/io/config_status_view.py +272 -0
  37. core/io/core_status_views.py +410 -0
  38. core/io/input_errors.py +313 -0
  39. core/io/input_handler.py +2655 -0
  40. core/io/input_mode_manager.py +402 -0
  41. core/io/key_parser.py +344 -0
  42. core/io/layout.py +587 -0
  43. core/io/message_coordinator.py +204 -0
  44. core/io/message_renderer.py +601 -0
  45. core/io/modal_interaction_handler.py +315 -0
  46. core/io/raw_input_processor.py +946 -0
  47. core/io/status_renderer.py +845 -0
  48. core/io/terminal_renderer.py +586 -0
  49. core/io/terminal_state.py +551 -0
  50. core/io/visual_effects.py +734 -0
  51. core/llm/__init__.py +26 -0
  52. core/llm/api_communication_service.py +863 -0
  53. core/llm/conversation_logger.py +473 -0
  54. core/llm/conversation_manager.py +414 -0
  55. core/llm/file_operations_executor.py +1401 -0
  56. core/llm/hook_system.py +402 -0
  57. core/llm/llm_service.py +1629 -0
  58. core/llm/mcp_integration.py +386 -0
  59. core/llm/message_display_service.py +450 -0
  60. core/llm/model_router.py +214 -0
  61. core/llm/plugin_sdk.py +396 -0
  62. core/llm/response_parser.py +848 -0
  63. core/llm/response_processor.py +364 -0
  64. core/llm/tool_executor.py +520 -0
  65. core/logging/__init__.py +19 -0
  66. core/logging/setup.py +208 -0
  67. core/models/__init__.py +5 -0
  68. core/models/base.py +23 -0
  69. core/plugins/__init__.py +13 -0
  70. core/plugins/collector.py +212 -0
  71. core/plugins/discovery.py +386 -0
  72. core/plugins/factory.py +263 -0
  73. core/plugins/registry.py +152 -0
  74. core/storage/__init__.py +5 -0
  75. core/storage/state_manager.py +84 -0
  76. core/ui/__init__.py +6 -0
  77. core/ui/config_merger.py +176 -0
  78. core/ui/config_widgets.py +369 -0
  79. core/ui/live_modal_renderer.py +276 -0
  80. core/ui/modal_actions.py +162 -0
  81. core/ui/modal_overlay_renderer.py +373 -0
  82. core/ui/modal_renderer.py +591 -0
  83. core/ui/modal_state_manager.py +443 -0
  84. core/ui/widget_integration.py +222 -0
  85. core/ui/widgets/__init__.py +27 -0
  86. core/ui/widgets/base_widget.py +136 -0
  87. core/ui/widgets/checkbox.py +85 -0
  88. core/ui/widgets/dropdown.py +140 -0
  89. core/ui/widgets/label.py +78 -0
  90. core/ui/widgets/slider.py +185 -0
  91. core/ui/widgets/text_input.py +224 -0
  92. core/utils/__init__.py +11 -0
  93. core/utils/config_utils.py +656 -0
  94. core/utils/dict_utils.py +212 -0
  95. core/utils/error_utils.py +275 -0
  96. core/utils/key_reader.py +171 -0
  97. core/utils/plugin_utils.py +267 -0
  98. core/utils/prompt_renderer.py +151 -0
  99. kollabor-0.4.9.dist-info/METADATA +298 -0
  100. kollabor-0.4.9.dist-info/RECORD +128 -0
  101. kollabor-0.4.9.dist-info/WHEEL +5 -0
  102. kollabor-0.4.9.dist-info/entry_points.txt +2 -0
  103. kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
  104. kollabor-0.4.9.dist-info/top_level.txt +4 -0
  105. kollabor_cli_main.py +20 -0
  106. plugins/__init__.py +1 -0
  107. plugins/enhanced_input/__init__.py +18 -0
  108. plugins/enhanced_input/box_renderer.py +103 -0
  109. plugins/enhanced_input/box_styles.py +142 -0
  110. plugins/enhanced_input/color_engine.py +165 -0
  111. plugins/enhanced_input/config.py +150 -0
  112. plugins/enhanced_input/cursor_manager.py +72 -0
  113. plugins/enhanced_input/geometry.py +81 -0
  114. plugins/enhanced_input/state.py +130 -0
  115. plugins/enhanced_input/text_processor.py +115 -0
  116. plugins/enhanced_input_plugin.py +385 -0
  117. plugins/fullscreen/__init__.py +9 -0
  118. plugins/fullscreen/example_plugin.py +327 -0
  119. plugins/fullscreen/matrix_plugin.py +132 -0
  120. plugins/hook_monitoring_plugin.py +1299 -0
  121. plugins/query_enhancer_plugin.py +350 -0
  122. plugins/save_conversation_plugin.py +502 -0
  123. plugins/system_commands_plugin.py +93 -0
  124. plugins/tmux_plugin.py +795 -0
  125. plugins/workflow_enforcement_plugin.py +629 -0
  126. system_prompt/default.md +1286 -0
  127. system_prompt/default_win.md +265 -0
  128. system_prompt/example_with_trender.md +47 -0
@@ -0,0 +1,386 @@
1
+ """Plugin discovery for file system scanning and module loading."""
2
+
3
+ import importlib
4
+ import inspect
5
+ import logging
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Type, Optional
10
+
11
+ from ..utils.plugin_utils import has_method, get_plugin_config_safely
12
+ from ..utils.error_utils import safe_execute
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Platform check
17
+ IS_WINDOWS = sys.platform == "win32"
18
+
19
+
20
+ class PluginDiscovery:
21
+ """Handles plugin discovery and module loading from the file system.
22
+
23
+ This class is responsible for scanning directories for plugin files,
24
+ loading Python modules, and extracting plugin classes and configurations.
25
+ """
26
+
27
+ def __init__(self, plugins_dir: Path):
28
+ """Initialize plugin discovery.
29
+
30
+ Args:
31
+ plugins_dir: Directory containing plugin modules.
32
+ """
33
+ self.plugins_dir = plugins_dir
34
+ self.discovered_modules: List[str] = []
35
+ self.loaded_classes: Dict[str, Type] = {}
36
+ self.plugin_configs: Dict[str, Dict[str, Any]] = {}
37
+
38
+ # Security validation patterns
39
+ self.valid_plugin_name_pattern = re.compile(r'^[a-zA-Z][a-zA-Z0-9_]*$')
40
+ self.max_plugin_name_length = 50
41
+ self.blocked_names = {
42
+ '__init__', '__pycache__', 'system', 'os', 'sys', 'subprocess',
43
+ 'eval', 'exec', 'compile', 'open', 'file', 'input', 'raw_input'
44
+ }
45
+
46
+ logger.info(f"PluginDiscovery initialized with directory: {plugins_dir}")
47
+
48
+ def _sanitize_plugin_name(self, plugin_name: str) -> Optional[str]:
49
+ """Sanitize and validate plugin name for security."""
50
+ if not plugin_name:
51
+ logger.warning("Empty plugin name rejected")
52
+ return None
53
+
54
+ # Length check
55
+ if len(plugin_name) > self.max_plugin_name_length:
56
+ logger.warning(f"Plugin name too long: {plugin_name}")
57
+ return None
58
+
59
+ # Pattern validation (letters, numbers, underscores only)
60
+ if not self.valid_plugin_name_pattern.match(plugin_name):
61
+ logger.warning(f"Invalid plugin name pattern: {plugin_name}")
62
+ return None
63
+
64
+ # Block dangerous names
65
+ if plugin_name.lower() in self.blocked_names:
66
+ logger.warning(f"Blocked plugin name: {plugin_name}")
67
+ return None
68
+
69
+ # Block path traversal attempts
70
+ if '..' in plugin_name or '/' in plugin_name or '\\' in plugin_name:
71
+ logger.warning(f"Path traversal attempt in plugin name: {plugin_name}")
72
+ return None
73
+
74
+ # Block shell metacharacters
75
+ if any(char in plugin_name for char in [';', '&', '|', '`', '$', '"', "'"]):
76
+ logger.warning(f"Shell metacharacters in plugin name: {plugin_name}")
77
+ return None
78
+
79
+ return plugin_name
80
+
81
+ def _verify_plugin_location(self, plugin_name: str) -> bool:
82
+ """Verify plugin file exists in expected location."""
83
+ try:
84
+ # Construct expected file path (plugin_name already includes _plugin suffix)
85
+ plugin_file = self.plugins_dir / f"{plugin_name}.py"
86
+
87
+ # Resolve to absolute path to prevent symlink attacks
88
+ plugin_file = plugin_file.resolve()
89
+
90
+ # Verify it's within the plugins directory
91
+ plugins_dir = self.plugins_dir.resolve()
92
+ if not str(plugin_file).startswith(str(plugins_dir)):
93
+ logger.error(f"Plugin file outside plugins directory: {plugin_file}")
94
+ return False
95
+
96
+ # Verify file exists and is a regular file
97
+ if not plugin_file.is_file():
98
+ logger.error(f"Plugin file not found: {plugin_file}")
99
+ return False
100
+
101
+ # Additional security: check file permissions (Unix only)
102
+ if not IS_WINDOWS:
103
+ if plugin_file.stat().st_mode & 0o777 != 0o644:
104
+ logger.warning(f"Plugin file has unusual permissions: {plugin_file}")
105
+
106
+ return True
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error verifying plugin location: {e}")
110
+ return False
111
+
112
+ def _verify_loaded_module(self, module, plugin_name: str) -> bool:
113
+ """Verify the loaded module is actually our plugin."""
114
+ try:
115
+ # Check module name matches
116
+ expected_module_name = f"plugins.{plugin_name}"
117
+ if module.__name__ != expected_module_name:
118
+ logger.error(f"Module name mismatch: {module.__name__} != {expected_module_name}")
119
+ return False
120
+
121
+ # Check module file location
122
+ if hasattr(module, '__file__'):
123
+ module_file = Path(module.__file__).resolve()
124
+ plugins_dir = self.plugins_dir.resolve()
125
+
126
+ if not str(module_file).startswith(str(plugins_dir)):
127
+ logger.error(f"Module file outside plugins directory: {module_file}")
128
+ return False
129
+
130
+ # Verify module has expected plugin attributes
131
+ if not hasattr(module, '__dict__'):
132
+ logger.error(f"Module {plugin_name} has no __dict__ attribute")
133
+ return False
134
+
135
+ return True
136
+
137
+ except Exception as e:
138
+ logger.error(f"Error verifying loaded module {plugin_name}: {e}")
139
+ return False
140
+
141
+ def scan_plugin_files(self) -> List[str]:
142
+ """Scan the plugins directory for plugin files with security validation.
143
+
144
+ Returns:
145
+ List of discovered plugin module names.
146
+ """
147
+ discovered = []
148
+
149
+ if not self.plugins_dir.exists():
150
+ logger.warning(f"Plugins directory does not exist: {self.plugins_dir}")
151
+ return discovered
152
+
153
+ # Resolve plugins directory to prevent symlink attacks
154
+ try:
155
+ plugins_dir = self.plugins_dir.resolve()
156
+
157
+ # Verify directory permissions (Unix only - Windows doesn't use these bits)
158
+ if not IS_WINDOWS:
159
+ if plugins_dir.stat().st_mode & 0o002:
160
+ logger.error(f"Plugins directory is world-writable: {plugins_dir}")
161
+ return discovered
162
+
163
+ except Exception as e:
164
+ logger.error(f"Error resolving plugins directory: {e}")
165
+ return discovered
166
+
167
+ for plugin_file in plugins_dir.glob("*_plugin.py"):
168
+ try:
169
+ # Extract module name from file (KEEP _plugin suffix for import)
170
+ module_name = plugin_file.stem # e.g., "enhanced_input_plugin"
171
+
172
+ # Apply security validation
173
+ safe_name = self._sanitize_plugin_name(module_name)
174
+ if not safe_name:
175
+ logger.warning(f"Skipping invalid plugin: {module_name}")
176
+ continue
177
+
178
+ # Verify plugin location
179
+ if not self._verify_plugin_location(safe_name):
180
+ logger.warning(f"Plugin location verification failed: {safe_name}")
181
+ continue
182
+
183
+ discovered.append(safe_name)
184
+ logger.debug(f"Discovered valid plugin: {safe_name}")
185
+
186
+ except Exception as e:
187
+ logger.error(f"Error processing plugin file {plugin_file}: {e}")
188
+ continue
189
+
190
+ self.discovered_modules = discovered
191
+ logger.info(f"Discovered {len(discovered)} validated plugin modules")
192
+ return discovered
193
+
194
+ def load_module(self, module_name: str) -> bool:
195
+ """Load a single plugin module and extract plugin classes.
196
+
197
+ Args:
198
+ module_name: Name of the plugin module to load.
199
+
200
+ Returns:
201
+ True if module loaded successfully, False otherwise.
202
+ """
203
+ # Validate module name before loading
204
+ safe_name = self._sanitize_plugin_name(module_name)
205
+ if not safe_name:
206
+ logger.error(f"Invalid plugin name rejected: {module_name}")
207
+ return False
208
+
209
+ # Verify plugin location again for safety
210
+ if not self._verify_plugin_location(safe_name):
211
+ logger.error(f"Plugin location verification failed during loading: {safe_name}")
212
+ return False
213
+
214
+ def _import_and_extract():
215
+ # Import the plugin module with security validation
216
+ module_path = f"plugins.{safe_name}"
217
+
218
+ try:
219
+ module = importlib.import_module(module_path)
220
+ except ImportError as e:
221
+ logger.error(f"Failed to import plugin module {safe_name}: {e}")
222
+ raise
223
+ except Exception as e:
224
+ logger.error(f"Unexpected error importing plugin module {safe_name}: {e}")
225
+ raise
226
+
227
+ # Verify the loaded module is actually our plugin
228
+ if not self._verify_loaded_module(module, safe_name):
229
+ raise ValueError(f"Module verification failed: {safe_name}")
230
+
231
+ # Find classes that look like plugins (end with 'Plugin')
232
+ found_plugins = False
233
+ for name, obj in inspect.getmembers(module, inspect.isclass):
234
+ if name.endswith('Plugin') and has_method(obj, 'get_default_config'):
235
+ # Store the plugin class
236
+ self.loaded_classes[name] = obj
237
+
238
+ # Get and store plugin configuration
239
+ config = get_plugin_config_safely(obj)
240
+ self.plugin_configs[name] = config
241
+
242
+ if config:
243
+ logger.info(f"Loaded plugin class: {name} with config keys: {list(config.keys())}")
244
+ else:
245
+ logger.info(f"Loaded plugin class: {name} with no configuration")
246
+
247
+ found_plugins = True
248
+
249
+ return found_plugins
250
+
251
+ result = safe_execute(
252
+ _import_and_extract,
253
+ f"loading plugin module {module_name}",
254
+ default=False,
255
+ logger_instance=logger
256
+ )
257
+
258
+ return result
259
+
260
+ def load_all_modules(self) -> int:
261
+ """Load all discovered plugin modules.
262
+
263
+ Returns:
264
+ Number of successfully loaded plugin classes.
265
+ """
266
+ initial_count = len(self.loaded_classes)
267
+
268
+ for module_name in self.discovered_modules:
269
+ self.load_module(module_name)
270
+
271
+ loaded_count = len(self.loaded_classes) - initial_count
272
+ logger.info(f"Loaded {loaded_count} plugin classes from {len(self.discovered_modules)} modules")
273
+
274
+ return loaded_count
275
+
276
+ def discover_and_load(self) -> Dict[str, Type]:
277
+ """Perform complete discovery and loading process.
278
+
279
+ Returns:
280
+ Dictionary mapping plugin names to their classes.
281
+ """
282
+ # Scan for plugin files
283
+ self.scan_plugin_files()
284
+
285
+ # Load all discovered modules
286
+ self.load_all_modules()
287
+
288
+ logger.info(f"Discovery complete: {len(self.loaded_classes)} plugins loaded")
289
+ return self.loaded_classes
290
+
291
+ def get_plugin_class(self, plugin_name: str) -> Type:
292
+ """Get a loaded plugin class by name.
293
+
294
+ Args:
295
+ plugin_name: Name of the plugin class.
296
+
297
+ Returns:
298
+ Plugin class if found.
299
+
300
+ Raises:
301
+ KeyError: If plugin class not found.
302
+ """
303
+ if plugin_name not in self.loaded_classes:
304
+ raise KeyError(f"Plugin class '{plugin_name}' not found")
305
+
306
+ return self.loaded_classes[plugin_name]
307
+
308
+ def get_plugin_config(self, plugin_name: str) -> Dict[str, Any]:
309
+ """Get configuration for a specific plugin.
310
+
311
+ Args:
312
+ plugin_name: Name of the plugin.
313
+
314
+ Returns:
315
+ Plugin configuration dictionary, or empty dict if not found.
316
+ """
317
+ return self.plugin_configs.get(plugin_name, {})
318
+
319
+ def get_all_configs(self) -> Dict[str, Dict[str, Any]]:
320
+ """Get configurations for all loaded plugins.
321
+
322
+ Returns:
323
+ Dictionary mapping plugin names to their configurations.
324
+ """
325
+ return self.plugin_configs.copy()
326
+
327
+ def get_discovery_stats(self) -> Dict[str, Any]:
328
+ """Get statistics about the discovery process.
329
+
330
+ Returns:
331
+ Dictionary with discovery statistics.
332
+ """
333
+ return {
334
+ "plugins_directory": str(self.plugins_dir),
335
+ "directory_exists": self.plugins_dir.exists(),
336
+ "discovered_modules": len(self.discovered_modules),
337
+ "loaded_classes": len(self.loaded_classes),
338
+ "plugins_with_config": sum(1 for c in self.plugin_configs.values() if c),
339
+ "module_names": self.discovered_modules,
340
+ "class_names": list(self.loaded_classes.keys())
341
+ }
342
+
343
+ def has_plugin_method(self, plugin_name: str, method_name: str) -> bool:
344
+ """Check if a loaded plugin has a specific method.
345
+
346
+ Args:
347
+ plugin_name: Name of the plugin class.
348
+ method_name: Name of the method to check for.
349
+
350
+ Returns:
351
+ True if plugin has the method, False otherwise.
352
+ """
353
+ if plugin_name not in self.loaded_classes:
354
+ return False
355
+
356
+ plugin_class = self.loaded_classes[plugin_name]
357
+ return has_method(plugin_class, method_name)
358
+
359
+ def call_plugin_method(self, plugin_name: str, method_name: str, *args, **kwargs) -> Any:
360
+ """Safely call a method on a loaded plugin class.
361
+
362
+ Args:
363
+ plugin_name: Name of the plugin class.
364
+ method_name: Name of the method to call.
365
+ *args: Positional arguments to pass.
366
+ **kwargs: Keyword arguments to pass.
367
+
368
+ Returns:
369
+ Method result or None if method doesn't exist or call failed.
370
+ """
371
+ if plugin_name not in self.loaded_classes:
372
+ logger.warning(f"Plugin {plugin_name} not found for method call: {method_name}")
373
+ return None
374
+
375
+ plugin_class = self.loaded_classes[plugin_name]
376
+
377
+ try:
378
+ if has_method(plugin_class, method_name):
379
+ method = getattr(plugin_class, method_name)
380
+ return method(*args, **kwargs)
381
+ else:
382
+ logger.debug(f"Plugin {plugin_name} has no method: {method_name}")
383
+ return None
384
+ except Exception as e:
385
+ logger.error(f"Failed to call {plugin_name}.{method_name}: {e}")
386
+ return None
@@ -0,0 +1,263 @@
1
+ """Plugin factory for instantiating plugin classes with dependencies."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Type
5
+
6
+ from ..utils.plugin_utils import has_method, instantiate_plugin_safely
7
+ from ..utils.error_utils import ErrorAccumulator, safe_execute
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PluginFactory:
13
+ """Handles plugin instantiation with dependency injection.
14
+
15
+ This class is responsible for creating instances of plugin classes,
16
+ managing their dependencies, and handling instantiation errors.
17
+ """
18
+
19
+ def __init__(self):
20
+ """Initialize the plugin factory."""
21
+ self.plugin_instances: Dict[str, Any] = {}
22
+ self.instantiation_errors: Dict[str, str] = {}
23
+ logger.info("PluginFactory initialized")
24
+
25
+ def instantiate_plugin(
26
+ self,
27
+ plugin_class: Type,
28
+ plugin_name: str,
29
+ state_manager: Any,
30
+ event_bus: Any,
31
+ renderer: Any,
32
+ config: Any
33
+ ) -> Any:
34
+ """Instantiate a single plugin with dependencies.
35
+
36
+ Args:
37
+ plugin_class: The plugin class to instantiate.
38
+ plugin_name: Name of the plugin.
39
+ state_manager: State management system.
40
+ event_bus: Event bus for hook registration.
41
+ renderer: Terminal renderer.
42
+ config: Configuration manager.
43
+
44
+ Returns:
45
+ Plugin instance if successful, None otherwise.
46
+ """
47
+ # Check if the plugin class has an __init__ method
48
+ if not has_method(plugin_class, '__init__'):
49
+ logger.debug(f"Plugin {plugin_name} is not instantiable (no __init__ method)")
50
+ return None
51
+
52
+ # Try to instantiate the plugin
53
+ # Clean plugin name: remove 'Plugin' suffix if present and use as name
54
+ clean_name = plugin_name
55
+ if plugin_name.endswith('Plugin'):
56
+ clean_name = plugin_name[:-6].lower()
57
+
58
+ instance = instantiate_plugin_safely(
59
+ plugin_class,
60
+ name=clean_name,
61
+ state_manager=state_manager,
62
+ event_bus=event_bus,
63
+ renderer=renderer,
64
+ config=config
65
+ )
66
+
67
+ if instance:
68
+ # Store with both the class name and the clean name for compatibility
69
+ self.plugin_instances[plugin_name] = instance
70
+ self.plugin_instances[clean_name] = instance
71
+ logger.info(f"Successfully instantiated plugin: {plugin_name} (as '{clean_name}')")
72
+ else:
73
+ self.instantiation_errors[plugin_name] = "Failed to instantiate"
74
+ logger.warning(f"Failed to instantiate plugin: {plugin_name}")
75
+
76
+ return instance
77
+
78
+ def instantiate_all(
79
+ self,
80
+ plugin_classes: Dict[str, Type],
81
+ state_manager: Any,
82
+ event_bus: Any,
83
+ renderer: Any,
84
+ config: Any
85
+ ) -> Dict[str, Any]:
86
+ """Instantiate all provided plugin classes.
87
+
88
+ Args:
89
+ plugin_classes: Dictionary mapping plugin names to classes.
90
+ state_manager: State management system.
91
+ event_bus: Event bus for hook registration.
92
+ renderer: Terminal renderer.
93
+ config: Configuration manager.
94
+
95
+ Returns:
96
+ Dictionary mapping plugin names to their instances.
97
+ """
98
+ error_accumulator = ErrorAccumulator(logger)
99
+
100
+ for plugin_name, plugin_class in plugin_classes.items():
101
+ instance = self.instantiate_plugin(
102
+ plugin_class,
103
+ plugin_name,
104
+ state_manager,
105
+ event_bus,
106
+ renderer,
107
+ config
108
+ )
109
+
110
+ if not instance:
111
+ error_accumulator.add_warning(
112
+ f"instantiating plugin {plugin_name}",
113
+ "Plugin instantiation failed"
114
+ )
115
+
116
+ error_accumulator.report_summary()
117
+ logger.info(f"Instantiated {len(self.plugin_instances)} plugins out of {len(plugin_classes)}")
118
+
119
+ return self.plugin_instances
120
+
121
+ def get_instance(self, plugin_name: str) -> Any:
122
+ """Get a plugin instance by name.
123
+
124
+ Args:
125
+ plugin_name: Name of the plugin.
126
+
127
+ Returns:
128
+ Plugin instance if found, None otherwise.
129
+ """
130
+ return self.plugin_instances.get(plugin_name)
131
+
132
+ def get_all_instances(self) -> Dict[str, Any]:
133
+ """Get all plugin instances.
134
+
135
+ Returns:
136
+ Dictionary mapping plugin names to instances.
137
+ """
138
+ return self.plugin_instances.copy()
139
+
140
+ def get_instantiation_errors(self) -> Dict[str, str]:
141
+ """Get errors from failed instantiations.
142
+
143
+ Returns:
144
+ Dictionary mapping plugin names to error messages.
145
+ """
146
+ return self.instantiation_errors.copy()
147
+
148
+ def initialize_plugin(self, plugin_name: str) -> bool:
149
+ """Initialize a plugin instance if it has an initialize method.
150
+
151
+ Args:
152
+ plugin_name: Name of the plugin to initialize.
153
+
154
+ Returns:
155
+ True if initialization successful, False otherwise.
156
+ """
157
+ instance = self.plugin_instances.get(plugin_name)
158
+ if not instance:
159
+ logger.warning(f"Cannot initialize non-existent plugin: {plugin_name}")
160
+ return False
161
+
162
+ if not has_method(instance, 'initialize'):
163
+ logger.debug(f"Plugin {plugin_name} has no initialize method")
164
+ return True
165
+
166
+ def _initialize():
167
+ return instance.initialize()
168
+
169
+ result = safe_execute(
170
+ _initialize,
171
+ f"initializing plugin {plugin_name}",
172
+ default=False,
173
+ logger_instance=logger
174
+ )
175
+
176
+ return result is not False
177
+
178
+ def initialize_all_plugins(self) -> Dict[str, bool]:
179
+ """Initialize all plugin instances.
180
+
181
+ Returns:
182
+ Dictionary mapping plugin names to initialization success status.
183
+ """
184
+ initialization_results = {}
185
+
186
+ for plugin_name in self.plugin_instances:
187
+ success = self.initialize_plugin(plugin_name)
188
+ initialization_results[plugin_name] = success
189
+
190
+ if not success:
191
+ logger.warning(f"Failed to initialize plugin: {plugin_name}")
192
+
193
+ successful = sum(1 for s in initialization_results.values() if s)
194
+ logger.info(f"Initialized {successful}/{len(initialization_results)} plugins")
195
+
196
+ return initialization_results
197
+
198
+ def shutdown_plugin(self, plugin_name: str) -> bool:
199
+ """Shutdown a plugin instance if it has a shutdown method.
200
+
201
+ Args:
202
+ plugin_name: Name of the plugin to shutdown.
203
+
204
+ Returns:
205
+ True if shutdown successful, False otherwise.
206
+ """
207
+ instance = self.plugin_instances.get(plugin_name)
208
+ if not instance:
209
+ logger.warning(f"Cannot shutdown non-existent plugin: {plugin_name}")
210
+ return False
211
+
212
+ if not has_method(instance, 'shutdown'):
213
+ logger.debug(f"Plugin {plugin_name} has no shutdown method")
214
+ return True
215
+
216
+ def _shutdown():
217
+ return instance.shutdown()
218
+
219
+ result = safe_execute(
220
+ _shutdown,
221
+ f"shutting down plugin {plugin_name}",
222
+ default=False,
223
+ logger_instance=logger
224
+ )
225
+
226
+ return result is not False
227
+
228
+ def shutdown_all_plugins(self) -> Dict[str, bool]:
229
+ """Shutdown all plugin instances.
230
+
231
+ Returns:
232
+ Dictionary mapping plugin names to shutdown success status.
233
+ """
234
+ shutdown_results = {}
235
+
236
+ for plugin_name in self.plugin_instances:
237
+ success = self.shutdown_plugin(plugin_name)
238
+ shutdown_results[plugin_name] = success
239
+
240
+ if not success:
241
+ logger.warning(f"Failed to shutdown plugin: {plugin_name}")
242
+
243
+ successful = sum(1 for s in shutdown_results.values() if s)
244
+ logger.info(f"Shutdown {successful}/{len(shutdown_results)} plugins")
245
+
246
+ return shutdown_results
247
+
248
+ def get_factory_stats(self) -> Dict[str, Any]:
249
+ """Get statistics about the factory's operations.
250
+
251
+ Returns:
252
+ Dictionary with factory statistics.
253
+ """
254
+ return {
255
+ "total_instances": len(self.plugin_instances),
256
+ "instantiation_errors": len(self.instantiation_errors),
257
+ "plugin_names": list(self.plugin_instances.keys()),
258
+ "error_plugins": list(self.instantiation_errors.keys()),
259
+ "instance_types": {
260
+ name: type(instance).__name__
261
+ for name, instance in self.plugin_instances.items()
262
+ }
263
+ }