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.
- core/__init__.py +18 -0
- core/application.py +578 -0
- core/cli.py +193 -0
- core/commands/__init__.py +43 -0
- core/commands/executor.py +277 -0
- core/commands/menu_renderer.py +319 -0
- core/commands/parser.py +186 -0
- core/commands/registry.py +331 -0
- core/commands/system_commands.py +479 -0
- core/config/__init__.py +7 -0
- core/config/llm_task_config.py +110 -0
- core/config/loader.py +501 -0
- core/config/manager.py +112 -0
- core/config/plugin_config_manager.py +346 -0
- core/config/plugin_schema.py +424 -0
- core/config/service.py +399 -0
- core/effects/__init__.py +1 -0
- core/events/__init__.py +12 -0
- core/events/bus.py +129 -0
- core/events/executor.py +154 -0
- core/events/models.py +258 -0
- core/events/processor.py +176 -0
- core/events/registry.py +289 -0
- core/fullscreen/__init__.py +19 -0
- core/fullscreen/command_integration.py +290 -0
- core/fullscreen/components/__init__.py +12 -0
- core/fullscreen/components/animation.py +258 -0
- core/fullscreen/components/drawing.py +160 -0
- core/fullscreen/components/matrix_components.py +177 -0
- core/fullscreen/manager.py +302 -0
- core/fullscreen/plugin.py +204 -0
- core/fullscreen/renderer.py +282 -0
- core/fullscreen/session.py +324 -0
- core/io/__init__.py +52 -0
- core/io/buffer_manager.py +362 -0
- core/io/config_status_view.py +272 -0
- core/io/core_status_views.py +410 -0
- core/io/input_errors.py +313 -0
- core/io/input_handler.py +2655 -0
- core/io/input_mode_manager.py +402 -0
- core/io/key_parser.py +344 -0
- core/io/layout.py +587 -0
- core/io/message_coordinator.py +204 -0
- core/io/message_renderer.py +601 -0
- core/io/modal_interaction_handler.py +315 -0
- core/io/raw_input_processor.py +946 -0
- core/io/status_renderer.py +845 -0
- core/io/terminal_renderer.py +586 -0
- core/io/terminal_state.py +551 -0
- core/io/visual_effects.py +734 -0
- core/llm/__init__.py +26 -0
- core/llm/api_communication_service.py +863 -0
- core/llm/conversation_logger.py +473 -0
- core/llm/conversation_manager.py +414 -0
- core/llm/file_operations_executor.py +1401 -0
- core/llm/hook_system.py +402 -0
- core/llm/llm_service.py +1629 -0
- core/llm/mcp_integration.py +386 -0
- core/llm/message_display_service.py +450 -0
- core/llm/model_router.py +214 -0
- core/llm/plugin_sdk.py +396 -0
- core/llm/response_parser.py +848 -0
- core/llm/response_processor.py +364 -0
- core/llm/tool_executor.py +520 -0
- core/logging/__init__.py +19 -0
- core/logging/setup.py +208 -0
- core/models/__init__.py +5 -0
- core/models/base.py +23 -0
- core/plugins/__init__.py +13 -0
- core/plugins/collector.py +212 -0
- core/plugins/discovery.py +386 -0
- core/plugins/factory.py +263 -0
- core/plugins/registry.py +152 -0
- core/storage/__init__.py +5 -0
- core/storage/state_manager.py +84 -0
- core/ui/__init__.py +6 -0
- core/ui/config_merger.py +176 -0
- core/ui/config_widgets.py +369 -0
- core/ui/live_modal_renderer.py +276 -0
- core/ui/modal_actions.py +162 -0
- core/ui/modal_overlay_renderer.py +373 -0
- core/ui/modal_renderer.py +591 -0
- core/ui/modal_state_manager.py +443 -0
- core/ui/widget_integration.py +222 -0
- core/ui/widgets/__init__.py +27 -0
- core/ui/widgets/base_widget.py +136 -0
- core/ui/widgets/checkbox.py +85 -0
- core/ui/widgets/dropdown.py +140 -0
- core/ui/widgets/label.py +78 -0
- core/ui/widgets/slider.py +185 -0
- core/ui/widgets/text_input.py +224 -0
- core/utils/__init__.py +11 -0
- core/utils/config_utils.py +656 -0
- core/utils/dict_utils.py +212 -0
- core/utils/error_utils.py +275 -0
- core/utils/key_reader.py +171 -0
- core/utils/plugin_utils.py +267 -0
- core/utils/prompt_renderer.py +151 -0
- kollabor-0.4.9.dist-info/METADATA +298 -0
- kollabor-0.4.9.dist-info/RECORD +128 -0
- kollabor-0.4.9.dist-info/WHEEL +5 -0
- kollabor-0.4.9.dist-info/entry_points.txt +2 -0
- kollabor-0.4.9.dist-info/licenses/LICENSE +21 -0
- kollabor-0.4.9.dist-info/top_level.txt +4 -0
- kollabor_cli_main.py +20 -0
- plugins/__init__.py +1 -0
- plugins/enhanced_input/__init__.py +18 -0
- plugins/enhanced_input/box_renderer.py +103 -0
- plugins/enhanced_input/box_styles.py +142 -0
- plugins/enhanced_input/color_engine.py +165 -0
- plugins/enhanced_input/config.py +150 -0
- plugins/enhanced_input/cursor_manager.py +72 -0
- plugins/enhanced_input/geometry.py +81 -0
- plugins/enhanced_input/state.py +130 -0
- plugins/enhanced_input/text_processor.py +115 -0
- plugins/enhanced_input_plugin.py +385 -0
- plugins/fullscreen/__init__.py +9 -0
- plugins/fullscreen/example_plugin.py +327 -0
- plugins/fullscreen/matrix_plugin.py +132 -0
- plugins/hook_monitoring_plugin.py +1299 -0
- plugins/query_enhancer_plugin.py +350 -0
- plugins/save_conversation_plugin.py +502 -0
- plugins/system_commands_plugin.py +93 -0
- plugins/tmux_plugin.py +795 -0
- plugins/workflow_enforcement_plugin.py +629 -0
- system_prompt/default.md +1286 -0
- system_prompt/default_win.md +265 -0
- 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
|
+
|