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
core/config/loader.py ADDED
@@ -0,0 +1,501 @@
1
+ """Configuration loading and plugin integration logic."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List
6
+ from importlib.metadata import version as get_version, PackageNotFoundError
7
+
8
+ from ..utils import deep_merge
9
+ from ..utils.error_utils import safe_execute, log_and_continue
10
+ from ..utils.config_utils import get_system_prompt_content
11
+ from ..utils.prompt_renderer import render_system_prompt
12
+ from .manager import ConfigManager
13
+ from .plugin_config_manager import PluginConfigManager
14
+
15
+ # Get version from package metadata
16
+ try:
17
+ _package_version = get_version("kollabor")
18
+ except PackageNotFoundError:
19
+ _package_version = "0.4.7" # Fallback for development mode
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ConfigLoader:
25
+ """Handles complex configuration loading with plugin integration.
26
+
27
+ This class manages the coordination between file-based configuration
28
+ and plugin-provided configurations, implementing the complex merging
29
+ logic that was previously in ConfigManager.
30
+ """
31
+
32
+ def __init__(self, config_manager: ConfigManager, plugin_registry=None):
33
+ """Initialize the config loader.
34
+
35
+ Args:
36
+ config_manager: Basic config manager for file operations.
37
+ plugin_registry: Optional plugin registry for plugin configs.
38
+ """
39
+ self.config_manager = config_manager
40
+ self.plugin_registry = plugin_registry
41
+ self.plugin_config_manager = None
42
+
43
+ # Initialize plugin config manager if registry is available
44
+ if plugin_registry and hasattr(plugin_registry, 'discovery'):
45
+ self.plugin_config_manager = PluginConfigManager(plugin_registry.discovery)
46
+ logger.debug("PluginConfigManager initialized")
47
+
48
+ logger.debug("ConfigLoader initialized")
49
+
50
+ def _load_system_prompt(self) -> str:
51
+ """Load system prompt from env vars or file and render dynamic content.
52
+
53
+ Processes <trender>command</trender> tags by executing commands
54
+ and replacing tags with their output.
55
+
56
+ Priority:
57
+ 1. KOLLABOR_SYSTEM_PROMPT environment variable (direct string)
58
+ 2. KOLLABOR_SYSTEM_PROMPT_FILE environment variable (custom file path)
59
+ 3. Local/global system_prompt/default.md files
60
+ 4. Fallback default
61
+
62
+ Returns:
63
+ System prompt content with rendered commands or fallback message.
64
+ """
65
+ try:
66
+ # Use the new unified function that checks env vars and files
67
+ content = get_system_prompt_content()
68
+
69
+ # Render dynamic <trender> tags
70
+ rendered_content = render_system_prompt(content, timeout=5)
71
+
72
+ return rendered_content
73
+ except Exception as e:
74
+ logger.error(f"Failed to load system prompt: {e}")
75
+ return "You are Kollabor, an intelligent coding assistant."
76
+
77
+ def get_base_config(self) -> Dict[str, Any]:
78
+ """Get the base application configuration with defaults.
79
+
80
+ Returns:
81
+ Base configuration dictionary with application defaults.
82
+ """
83
+ # Load system prompt from file
84
+ system_prompt = self._load_system_prompt()
85
+
86
+ return {
87
+ "terminal": {
88
+ "render_fps": 20,
89
+ "spinner_frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"],
90
+ "status_lines": 4,
91
+ "thinking_message_limit": 25,
92
+ "thinking_effect": "shimmer",
93
+ "shimmer_speed": 3,
94
+ "shimmer_wave_width": 4,
95
+ "render_error_delay": 0.1,
96
+ "render_cache_enabled": True
97
+ },
98
+ "input": {
99
+ "ctrl_c_exit": True,
100
+ "backspace_enabled": True,
101
+ "input_buffer_limit": 100000,
102
+ "polling_delay": 0.01,
103
+ "error_delay": 0.1,
104
+ "history_limit": 100,
105
+ "error_threshold": 10,
106
+ "error_window_minutes": 5,
107
+ "max_errors": 100,
108
+ "paste_detection_enabled": True,
109
+ "paste_threshold_ms": 50,
110
+ "paste_min_chars": 3,
111
+ "paste_max_chars": 10000,
112
+ "bracketed_paste_enabled": True
113
+ },
114
+ "logging": {
115
+ "level": "INFO",
116
+ "file": ".kollabor-cli/logs/kollabor.log",
117
+ "format_type": "compact",
118
+ "format": "%(asctime)s - %(levelname)-4s - %(message)-100s - %(filename)s:%(lineno)04d"
119
+ },
120
+ "hooks": {
121
+ "default_timeout": 30,
122
+ "default_retries": 3,
123
+ "default_error_action": "continue"
124
+ },
125
+ "application": {
126
+ "name": "Kollabor CLI",
127
+ "version": _package_version,
128
+ "description": "AI Edition"
129
+ },
130
+ "core": {
131
+ "llm": {
132
+ "api_url": "http://localhost:1234",
133
+ "api_token": "",
134
+ "model": "qwen/qwen3-4b",
135
+ "temperature": 0.7,
136
+ "timeout": 0,
137
+ "max_history": 90,
138
+ "save_conversations": True,
139
+ "conversation_format": "jsonl",
140
+ "show_status": True,
141
+ "http_connector_limit": 10,
142
+ "message_history_limit": 20,
143
+ "thinking_phase_delay": 0.5,
144
+ "log_message_truncate": 50,
145
+ "enable_streaming": False,
146
+ "processing_delay": 0.1,
147
+ "thinking_delay": 0.3,
148
+ "api_poll_delay": 0.01,
149
+ "terminal_timeout": 30,
150
+ "mcp_timeout": 60,
151
+ "system_prompt": {
152
+ "base_prompt": system_prompt,
153
+ "include_project_structure": True,
154
+ "attachment_files": [],
155
+ "custom_prompt_files": []
156
+ },
157
+ "task_management": {
158
+ "background_tasks": {
159
+ "max_concurrent": 10000,
160
+ "default_timeout": 0,
161
+ "cleanup_interval": 60,
162
+ "enable_monitoring": True,
163
+ "log_task_events": True,
164
+ "log_task_errors": True,
165
+ "enable_metrics": True,
166
+ "task_retry_attempts": 0,
167
+ "task_retry_delay": 1.0,
168
+ "enable_task_circuit_breaker": False,
169
+ "circuit_breaker_threshold": 5,
170
+ "circuit_breaker_timeout": 60.0
171
+ },
172
+ "queue": {
173
+ "max_size": 1000,
174
+ "overflow_strategy": "drop_oldest",
175
+ "block_timeout": 1.0,
176
+ "enable_queue_metrics": True,
177
+ "log_queue_events": True
178
+ }
179
+ }
180
+ }
181
+ },
182
+ "performance": {
183
+ "failure_rate_warning": 0.05,
184
+ "failure_rate_critical": 0.15,
185
+ "degradation_threshold": 0.15
186
+ },
187
+ "plugins": {
188
+ "enhanced_input": {
189
+ "enabled": True,
190
+ "style": "rounded",
191
+ "width": "auto",
192
+ "placeholder": "Type your message here...",
193
+ "show_placeholder": True,
194
+ "min_width": 60,
195
+ "max_width": 120,
196
+ "randomize_style": False,
197
+ "randomize_interval": 5.0,
198
+ "dynamic_sizing": True,
199
+ "min_height": 3,
200
+ "max_height": 10,
201
+ "wrap_text": True,
202
+ "colors": {
203
+ "border": "cyan",
204
+ "text": "white",
205
+ "placeholder": "dim",
206
+ "gradient_mode": True,
207
+ "gradient_colors": [
208
+ "#333333",
209
+ "#999999",
210
+ "#222222"
211
+ ],
212
+ "border_gradient": True,
213
+ "text_gradient": True
214
+ },
215
+ "cursor_blink_rate": 0.5,
216
+ "show_status": True
217
+ },
218
+ "system_commands": {
219
+ "enabled": True
220
+ },
221
+ "hook_monitoring": {
222
+ "enabled": False,
223
+ "debug_logging": True,
224
+ "show_status": True,
225
+ "hook_timeout": 5,
226
+ "log_all_events": True,
227
+ "log_event_data": False,
228
+ "log_performance": True,
229
+ "log_failures_only": False,
230
+ "performance_threshold_ms": 100,
231
+ "max_error_log_size": 50,
232
+ "enable_plugin_discovery": True,
233
+ "discovery_interval": 30,
234
+ "auto_analyze_capabilities": True,
235
+ "enable_service_registration": True,
236
+ "register_performance_service": True,
237
+ "register_health_service": True,
238
+ "register_metrics_service": True,
239
+ "enable_cross_plugin_communication": True,
240
+ "message_history_limit": 20,
241
+ "auto_respond_to_health_checks": True,
242
+ "health_check_interval": 30,
243
+ "memory_threshold_mb": 50,
244
+ "performance_degradation_threshold": 0.15,
245
+ "collect_plugin_metrics": True,
246
+ "metrics_retention_hours": 24,
247
+ "detailed_performance_tracking": True,
248
+ "enable_health_dashboard": True,
249
+ "dashboard_update_interval": 10,
250
+ "show_plugin_interactions": True,
251
+ "show_service_usage": True
252
+ },
253
+ "query_enhancer": {
254
+ "enabled": False,
255
+ "show_status": True,
256
+ "fast_model": {
257
+ "api_url": "http://localhost:1234",
258
+ "model": "qwen3-0.6b",
259
+ "temperature": 0.3,
260
+ "timeout": 5
261
+ },
262
+ "enhancement_prompt": "You are a query enhancement specialist. Your job is to improve user queries to get better responses from AI assistants.\n\nTake this user query and enhance it by:\n1. Making it more specific and detailed\n2. Adding relevant context\n3. Clarifying any ambiguity\n4. Keeping the original intent\n\nReturn ONLY the enhanced query, nothing else.\n\nOriginal query: {query}\n\nEnhanced query:",
263
+ "max_length": 500,
264
+ "min_query_length": 10,
265
+ "skip_enhancement_keywords": [
266
+ "hi",
267
+ "hello",
268
+ "thanks",
269
+ "thank you",
270
+ "ok",
271
+ "okay",
272
+ "yes",
273
+ "no"
274
+ ],
275
+ "performance_tracking": True
276
+ },
277
+ "workflow_enforcement": {
278
+ "enabled": False
279
+ },
280
+ "fullscreen": {
281
+ "enabled": False
282
+ }
283
+ },
284
+ "workflow_enforcement": {
285
+ "enabled": False,
286
+ "require_tool_calls": True,
287
+ "confirmation_timeout": 300,
288
+ "bypass_keywords": [
289
+ "bypass",
290
+ "skip",
291
+ "blocked",
292
+ "issue",
293
+ "problem"
294
+ ],
295
+ "auto_start_workflows": True,
296
+ "show_progress_in_status": True
297
+ }
298
+ }
299
+
300
+ def get_plugin_configs(self) -> Dict[str, Any]:
301
+ """Get merged configuration from all plugins.
302
+
303
+ Returns:
304
+ Merged plugin configurations or empty dict if no plugins.
305
+ """
306
+ if not self.plugin_registry:
307
+ return {}
308
+
309
+ # Discover plugin schemas first
310
+ self.discover_plugin_schemas()
311
+
312
+ def get_configs():
313
+ return self.plugin_registry.get_merged_config()
314
+
315
+ plugin_configs = safe_execute(
316
+ get_configs,
317
+ "getting plugin configurations",
318
+ default={},
319
+ logger_instance=logger
320
+ )
321
+
322
+ return plugin_configs if isinstance(plugin_configs, dict) else {}
323
+
324
+ def discover_plugin_schemas(self) -> None:
325
+ """Discover and register plugin configuration schemas."""
326
+ if not self.plugin_config_manager:
327
+ return
328
+
329
+ def discover():
330
+ self.plugin_config_manager.discover_plugin_schemas()
331
+
332
+ safe_execute(
333
+ discover,
334
+ "discovering plugin schemas",
335
+ default=None,
336
+ logger_instance=logger
337
+ )
338
+
339
+ def get_plugin_config_sections(self) -> List[Dict[str, Any]]:
340
+ """Get UI sections for plugin configuration.
341
+
342
+ Returns:
343
+ List of section definitions for the configuration UI.
344
+ """
345
+ if not self.plugin_config_manager:
346
+ return []
347
+
348
+ def get_sections():
349
+ return self.plugin_config_manager.get_plugin_config_sections()
350
+
351
+ sections = safe_execute(
352
+ get_sections,
353
+ "getting plugin config sections",
354
+ default=[],
355
+ logger_instance=logger
356
+ )
357
+
358
+ return sections if isinstance(sections, list) else []
359
+
360
+ def get_plugin_widget_definitions(self) -> List[Dict[str, Any]]:
361
+ """Get widget definitions for all plugin configurations.
362
+
363
+ Returns:
364
+ List of widget definition dictionaries.
365
+ """
366
+ if not self.plugin_config_manager:
367
+ return []
368
+
369
+ def get_widgets():
370
+ return self.plugin_config_manager.get_widget_definitions()
371
+
372
+ widgets = safe_execute(
373
+ get_widgets,
374
+ "getting plugin widget definitions",
375
+ default=[],
376
+ logger_instance=logger
377
+ )
378
+
379
+ return widgets if isinstance(widgets, list) else []
380
+
381
+ def load_complete_config(self) -> Dict[str, Any]:
382
+ """Load complete configuration including plugins.
383
+
384
+ This is the main entry point for getting a fully merged configuration
385
+ that includes base defaults, plugin configs, and user overrides.
386
+
387
+ Priority order for user config:
388
+ 1. Local config (.kollabor-cli/config.json in current directory)
389
+ 2. Global config (~/.kollabor-cli/config.json in home directory)
390
+ 3. Base defaults (if neither exists)
391
+
392
+ Returns:
393
+ Complete merged configuration dictionary.
394
+ """
395
+ # Start with base application configuration
396
+ base_config = self.get_base_config()
397
+
398
+ # Add plugin configurations
399
+ plugin_configs = self.get_plugin_configs()
400
+ if plugin_configs:
401
+ base_config = deep_merge(base_config, plugin_configs)
402
+ logger.debug(f"Merged configurations from plugins")
403
+
404
+ # Load user configuration with fallback to global
405
+ user_config = self._load_user_config_with_fallback()
406
+ if user_config:
407
+ # User config takes precedence over defaults and plugins
408
+ base_config = deep_merge(base_config, user_config)
409
+ logger.debug("Merged user configuration")
410
+
411
+ return base_config
412
+
413
+ def _load_user_config_with_fallback(self) -> Dict[str, Any]:
414
+ """Load user configuration, falling back to global config if local doesn't exist.
415
+
416
+ Priority order:
417
+ 1. Local config (current directory's .kollabor-cli/config.json)
418
+ 2. Global config (~/.kollabor-cli/config.json)
419
+
420
+ Returns:
421
+ User configuration dictionary, or empty dict if none found.
422
+ """
423
+ # Check if local config exists
424
+ if self.config_manager.config_path.exists():
425
+ user_config = self.config_manager.load_config_file()
426
+ if user_config:
427
+ logger.debug(f"Loaded local config from: {self.config_manager.config_path}")
428
+ return user_config
429
+
430
+ # Fall back to global config
431
+ global_config_path = Path.home() / ".kollabor-cli" / "config.json"
432
+ if global_config_path.exists():
433
+ try:
434
+ import json
435
+ with open(global_config_path, 'r') as f:
436
+ global_config = json.load(f)
437
+ if global_config:
438
+ logger.info(f"Using global config as fallback from: {global_config_path}")
439
+ return global_config
440
+ except Exception as e:
441
+ logger.warning(f"Failed to load global config: {e}")
442
+
443
+ logger.debug("No user configuration found (local or global)")
444
+ return {}
445
+
446
+ def save_merged_config(self, config: Dict[str, Any]) -> bool:
447
+ """Save the complete merged configuration to file.
448
+
449
+ Note: base_prompt is excluded from saving because it should always
450
+ be dynamically loaded from the system_prompt/*.md files on startup.
451
+
452
+ Args:
453
+ config: Configuration dictionary to save.
454
+
455
+ Returns:
456
+ True if save successful, False otherwise.
457
+ """
458
+ import copy
459
+ config_to_save = copy.deepcopy(config)
460
+
461
+ # Remove base_prompt - it should always be loaded fresh from .md files
462
+ try:
463
+ if "core" in config_to_save and "llm" in config_to_save["core"]:
464
+ if "system_prompt" in config_to_save["core"]["llm"]:
465
+ config_to_save["core"]["llm"]["system_prompt"].pop("base_prompt", None)
466
+ except (KeyError, TypeError):
467
+ pass # Config structure doesn't match expected format
468
+
469
+ return self.config_manager.save_config_file(config_to_save)
470
+
471
+ def update_with_plugins(self) -> bool:
472
+ """Update the configuration file with newly discovered plugins.
473
+
474
+ This method reloads the complete configuration including any new
475
+ plugin configurations and saves it to the config file.
476
+
477
+ Returns:
478
+ True if update successful, False otherwise.
479
+ """
480
+ if not self.plugin_registry:
481
+ logger.warning("No plugin registry available for config update")
482
+ return False
483
+
484
+ try:
485
+ # Load complete config including plugins
486
+ updated_config = self.load_complete_config()
487
+
488
+ # Save the updated configuration
489
+ success = self.save_merged_config(updated_config)
490
+
491
+ if success:
492
+ # Update the config manager's in-memory config
493
+ self.config_manager.config = updated_config
494
+ plugin_count = len(self.plugin_registry.list_plugins())
495
+ logger.info(f"Updated config with configurations from {plugin_count} plugins")
496
+
497
+ return success
498
+
499
+ except Exception as e:
500
+ log_and_continue(logger, "updating config with plugins", e)
501
+ return False
core/config/manager.py ADDED
@@ -0,0 +1,112 @@
1
+ """Configuration management system for Kollabor CLI."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ from ..utils import deep_merge, safe_get, safe_set
9
+ from ..utils.error_utils import safe_execute, log_and_continue
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ConfigManager:
15
+ """Configuration management system.
16
+
17
+ Handles loading and saving JSON configuration files with defaults.
18
+ """
19
+
20
+ def __init__(self, config_path: Path) -> None:
21
+ """Initialize the config manager.
22
+
23
+ Args:
24
+ config_path: Path to the config JSON file.
25
+ """
26
+ self.config_path = config_path
27
+ self.config = {}
28
+ logger.info(f"Config manager initialized: {config_path}")
29
+
30
+ def load_config_file(self) -> Dict[str, Any]:
31
+ """Load configuration from file.
32
+
33
+ Returns:
34
+ Configuration dictionary from file, or empty dict if load fails.
35
+ """
36
+ if not self.config_path.exists():
37
+ logger.debug(f"Config file does not exist: {self.config_path}")
38
+ return {}
39
+
40
+ def load_json():
41
+ with open(self.config_path, 'r') as f:
42
+ return json.load(f)
43
+
44
+ config = safe_execute(
45
+ load_json,
46
+ f"loading config from {self.config_path}",
47
+ default={},
48
+ logger_instance=logger
49
+ )
50
+
51
+ if config:
52
+ logger.info("Loaded configuration from file")
53
+
54
+ return config
55
+
56
+ def save_config_file(self, config: Dict[str, Any]) -> bool:
57
+ """Save configuration to file.
58
+
59
+ Args:
60
+ config: Configuration dictionary to save.
61
+
62
+ Returns:
63
+ True if save successful, False otherwise.
64
+ """
65
+ def save_json():
66
+ with open(self.config_path, 'w') as f:
67
+ json.dump(config, f, indent=2)
68
+
69
+ success = safe_execute(
70
+ save_json,
71
+ f"saving config to {self.config_path}",
72
+ default=False,
73
+ logger_instance=logger
74
+ )
75
+
76
+ if success is not False:
77
+ logger.debug("Configuration saved to file")
78
+ return True
79
+ return False
80
+
81
+
82
+ def get(self, key_path: str, default: Any = None) -> Any:
83
+ """Get a configuration value using dot notation.
84
+
85
+ Args:
86
+ key_path: Dot-separated path to the config value (e.g., "llm.api_url").
87
+ default: Default value if key not found.
88
+
89
+ Returns:
90
+ Configuration value or default.
91
+ """
92
+ return safe_get(self.config, key_path, default)
93
+
94
+ def set(self, key_path: str, value: Any) -> bool:
95
+ """Set a configuration value using dot notation.
96
+
97
+ Args:
98
+ key_path: Dot-separated path to the config value.
99
+ value: Value to set.
100
+
101
+ Returns:
102
+ True if set and save successful, False otherwise.
103
+ """
104
+ if safe_set(self.config, key_path, value):
105
+ success = self.save_config_file(self.config)
106
+ if success:
107
+ logger.debug(f"Set config: {key_path} = {value}")
108
+ return True
109
+
110
+ logger.error(f"Failed to set config key: {key_path}")
111
+ return False
112
+