omnimancer-cli 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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
"""Agent configuration and settings management system."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
11
|
+
|
|
12
|
+
import toml
|
|
13
|
+
import yaml
|
|
14
|
+
from jsonschema import ValidationError, validate
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigFormat(Enum):
|
|
18
|
+
"""Supported configuration file formats."""
|
|
19
|
+
|
|
20
|
+
JSON = "json"
|
|
21
|
+
TOML = "toml"
|
|
22
|
+
YAML = "yaml"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProviderType(Enum):
|
|
26
|
+
"""Types of Omnimancer providers."""
|
|
27
|
+
|
|
28
|
+
ANTHROPIC = "anthropic"
|
|
29
|
+
OPENAI = "openai"
|
|
30
|
+
GOOGLE = "google"
|
|
31
|
+
AZURE = "azure"
|
|
32
|
+
BEDROCK = "bedrock"
|
|
33
|
+
MISTRAL = "mistral"
|
|
34
|
+
PERPLEXITY = "perplexity"
|
|
35
|
+
XAI = "xai"
|
|
36
|
+
OPENROUTER = "openrouter"
|
|
37
|
+
CLAUDE_CODE = "claude_code"
|
|
38
|
+
VERTEX = "vertex"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class SecuritySettings:
|
|
43
|
+
"""Security-related configuration settings."""
|
|
44
|
+
|
|
45
|
+
enabled: bool = True
|
|
46
|
+
auto_approve_safe_operations: bool = False
|
|
47
|
+
backup_before_changes: bool = True
|
|
48
|
+
max_file_size_mb: int = 10
|
|
49
|
+
allowed_commands: List[str] = field(
|
|
50
|
+
default_factory=lambda: [
|
|
51
|
+
"git",
|
|
52
|
+
"npm",
|
|
53
|
+
"pip",
|
|
54
|
+
"python",
|
|
55
|
+
"node",
|
|
56
|
+
"make",
|
|
57
|
+
"cargo",
|
|
58
|
+
"go",
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
restricted_paths: List[str] = field(
|
|
62
|
+
default_factory=lambda: [
|
|
63
|
+
".ssh",
|
|
64
|
+
".env",
|
|
65
|
+
".aws",
|
|
66
|
+
".config/gcloud",
|
|
67
|
+
"/etc",
|
|
68
|
+
"/System",
|
|
69
|
+
]
|
|
70
|
+
)
|
|
71
|
+
enable_sandboxing: bool = True
|
|
72
|
+
enable_audit_logging: bool = True
|
|
73
|
+
audit_log_level: str = "INFO"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class AgentSettings:
|
|
78
|
+
"""Core agent behavior settings."""
|
|
79
|
+
|
|
80
|
+
enabled: bool = True
|
|
81
|
+
max_concurrent_operations: int = 5
|
|
82
|
+
default_timeout_seconds: int = 300
|
|
83
|
+
enable_learning: bool = True
|
|
84
|
+
remember_user_preferences: bool = True
|
|
85
|
+
auto_backup_interval_minutes: int = 60
|
|
86
|
+
verbose_logging: bool = False
|
|
87
|
+
enable_progress_reporting: bool = True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class FileSystemSettings:
|
|
92
|
+
"""File system operation settings."""
|
|
93
|
+
|
|
94
|
+
enable_atomic_operations: bool = True
|
|
95
|
+
backup_directory: str = "/tmp/omnimancer_backups"
|
|
96
|
+
max_backup_age_days: int = 30
|
|
97
|
+
enable_version_control: bool = True
|
|
98
|
+
git_auto_add: bool = False
|
|
99
|
+
large_file_threshold_mb: int = 10
|
|
100
|
+
enable_streaming: bool = True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class WebClientSettings:
|
|
105
|
+
"""Web client configuration settings."""
|
|
106
|
+
|
|
107
|
+
enabled: bool = True
|
|
108
|
+
enable_caching: bool = True
|
|
109
|
+
cache_ttl_seconds: int = 3600
|
|
110
|
+
max_cache_size_mb: int = 100
|
|
111
|
+
enable_rate_limiting: bool = True
|
|
112
|
+
requests_per_second: float = 10.0
|
|
113
|
+
requests_per_minute: float = 100.0
|
|
114
|
+
requests_per_hour: float = 1000.0
|
|
115
|
+
timeout_seconds: int = 30
|
|
116
|
+
max_redirects: int = 10
|
|
117
|
+
user_agent: str = "Omnimancer-Agent/1.0"
|
|
118
|
+
blacklisted_domains: List[str] = field(
|
|
119
|
+
default_factory=lambda: ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
|
|
120
|
+
)
|
|
121
|
+
whitelisted_domains: List[str] = field(default_factory=list)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class ProviderConfig:
|
|
126
|
+
"""Configuration for a specific provider."""
|
|
127
|
+
|
|
128
|
+
provider_type: ProviderType
|
|
129
|
+
enabled: bool = True
|
|
130
|
+
api_key: Optional[str] = None
|
|
131
|
+
base_url: Optional[str] = None
|
|
132
|
+
model: Optional[str] = None
|
|
133
|
+
max_tokens: Optional[int] = None
|
|
134
|
+
temperature: Optional[float] = None
|
|
135
|
+
timeout_seconds: int = 30
|
|
136
|
+
retry_attempts: int = 3
|
|
137
|
+
custom_headers: Dict[str, str] = field(default_factory=dict)
|
|
138
|
+
provider_specific: Dict[str, Any] = field(default_factory=dict)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class UserPreferences:
|
|
143
|
+
"""User preferences and learned behaviors."""
|
|
144
|
+
|
|
145
|
+
preferred_approval_method: str = "interactive" # interactive, auto, batch
|
|
146
|
+
preferred_diff_format: str = "unified" # unified, context, side_by_side
|
|
147
|
+
auto_approve_types: List[str] = field(
|
|
148
|
+
default_factory=lambda: ["file_read", "system_info"]
|
|
149
|
+
)
|
|
150
|
+
remember_choices: bool = True
|
|
151
|
+
notification_level: str = "normal" # quiet, normal, verbose
|
|
152
|
+
preferred_editor: str = "auto"
|
|
153
|
+
theme: str = "auto" # auto, light, dark
|
|
154
|
+
language: str = "en"
|
|
155
|
+
timezone: str = "UTC"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ConfigurationError(Exception):
|
|
159
|
+
"""Configuration-related error."""
|
|
160
|
+
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ConfigValidator:
|
|
165
|
+
"""Validates configuration using JSON schema."""
|
|
166
|
+
|
|
167
|
+
def __init__(self):
|
|
168
|
+
self.schemas = self._load_schemas()
|
|
169
|
+
|
|
170
|
+
def _load_schemas(self) -> Dict[str, Dict]:
|
|
171
|
+
"""Load JSON schemas for configuration validation."""
|
|
172
|
+
|
|
173
|
+
security_schema = {
|
|
174
|
+
"type": "object",
|
|
175
|
+
"properties": {
|
|
176
|
+
"enabled": {"type": "boolean"},
|
|
177
|
+
"auto_approve_safe_operations": {"type": "boolean"},
|
|
178
|
+
"backup_before_changes": {"type": "boolean"},
|
|
179
|
+
"max_file_size_mb": {"type": "integer", "minimum": 1},
|
|
180
|
+
"allowed_commands": {
|
|
181
|
+
"type": "array",
|
|
182
|
+
"items": {"type": "string"},
|
|
183
|
+
},
|
|
184
|
+
"restricted_paths": {
|
|
185
|
+
"type": "array",
|
|
186
|
+
"items": {"type": "string"},
|
|
187
|
+
},
|
|
188
|
+
"enable_sandboxing": {"type": "boolean"},
|
|
189
|
+
"enable_audit_logging": {"type": "boolean"},
|
|
190
|
+
"audit_log_level": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"enum": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
"additionalProperties": False,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
agent_schema = {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {
|
|
201
|
+
"enabled": {"type": "boolean"},
|
|
202
|
+
"max_concurrent_operations": {"type": "integer", "minimum": 1},
|
|
203
|
+
"default_timeout_seconds": {"type": "integer", "minimum": 1},
|
|
204
|
+
"enable_learning": {"type": "boolean"},
|
|
205
|
+
"remember_user_preferences": {"type": "boolean"},
|
|
206
|
+
"auto_backup_interval_minutes": {
|
|
207
|
+
"type": "integer",
|
|
208
|
+
"minimum": 1,
|
|
209
|
+
},
|
|
210
|
+
"verbose_logging": {"type": "boolean"},
|
|
211
|
+
"enable_progress_reporting": {"type": "boolean"},
|
|
212
|
+
},
|
|
213
|
+
"additionalProperties": False,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
provider_schema = {
|
|
217
|
+
"type": "object",
|
|
218
|
+
"properties": {
|
|
219
|
+
"provider_type": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"enum": [p.value for p in ProviderType],
|
|
222
|
+
},
|
|
223
|
+
"enabled": {"type": "boolean"},
|
|
224
|
+
"api_key": {"type": ["string", "null"]},
|
|
225
|
+
"base_url": {"type": ["string", "null"]},
|
|
226
|
+
"model": {"type": ["string", "null"]},
|
|
227
|
+
"max_tokens": {"type": ["integer", "null"], "minimum": 1},
|
|
228
|
+
"temperature": {
|
|
229
|
+
"type": ["number", "null"],
|
|
230
|
+
"minimum": 0,
|
|
231
|
+
"maximum": 2,
|
|
232
|
+
},
|
|
233
|
+
"timeout_seconds": {"type": "integer", "minimum": 1},
|
|
234
|
+
"retry_attempts": {"type": "integer", "minimum": 0},
|
|
235
|
+
"custom_headers": {"type": "object"},
|
|
236
|
+
"provider_specific": {"type": "object"},
|
|
237
|
+
},
|
|
238
|
+
"required": ["provider_type"],
|
|
239
|
+
"additionalProperties": False,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"security": security_schema,
|
|
244
|
+
"agent": agent_schema,
|
|
245
|
+
"provider": provider_schema,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def validate_security_settings(self, settings: Dict[str, Any]) -> None:
|
|
249
|
+
"""Validate security settings."""
|
|
250
|
+
try:
|
|
251
|
+
validate(instance=settings, schema=self.schemas["security"])
|
|
252
|
+
except ValidationError as e:
|
|
253
|
+
raise ConfigurationError(f"Invalid security settings: {e.message}")
|
|
254
|
+
|
|
255
|
+
def validate_agent_settings(self, settings: Dict[str, Any]) -> None:
|
|
256
|
+
"""Validate agent settings."""
|
|
257
|
+
try:
|
|
258
|
+
validate(instance=settings, schema=self.schemas["agent"])
|
|
259
|
+
except ValidationError as e:
|
|
260
|
+
raise ConfigurationError(f"Invalid agent settings: {e.message}")
|
|
261
|
+
|
|
262
|
+
def validate_provider_config(self, config: Dict[str, Any]) -> None:
|
|
263
|
+
"""Validate provider configuration."""
|
|
264
|
+
try:
|
|
265
|
+
validate(instance=config, schema=self.schemas["provider"])
|
|
266
|
+
except ValidationError as e:
|
|
267
|
+
raise ConfigurationError(f"Invalid provider config: {e.message}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class AgentConfig:
|
|
271
|
+
"""Comprehensive configuration management for Omnimancer agents."""
|
|
272
|
+
|
|
273
|
+
CONFIG_VERSION = "1.0"
|
|
274
|
+
DEFAULT_CONFIG_DIR = Path.home() / ".omnimancer"
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
config_dir: Optional[Path] = None,
|
|
279
|
+
auto_save: bool = True,
|
|
280
|
+
watch_files: bool = True,
|
|
281
|
+
):
|
|
282
|
+
|
|
283
|
+
self.config_dir = Path(config_dir) if config_dir else self.DEFAULT_CONFIG_DIR
|
|
284
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
285
|
+
|
|
286
|
+
self.auto_save = auto_save
|
|
287
|
+
self.watch_files = watch_files
|
|
288
|
+
|
|
289
|
+
# Configuration components
|
|
290
|
+
self.security = SecuritySettings()
|
|
291
|
+
self.agent = AgentSettings()
|
|
292
|
+
self.filesystem = FileSystemSettings()
|
|
293
|
+
self.web_client = WebClientSettings()
|
|
294
|
+
self.user_preferences = UserPreferences()
|
|
295
|
+
|
|
296
|
+
# Provider configurations
|
|
297
|
+
self.providers: Dict[str, ProviderConfig] = {}
|
|
298
|
+
|
|
299
|
+
# Configuration metadata
|
|
300
|
+
self.version = self.CONFIG_VERSION
|
|
301
|
+
self.created_at = datetime.now()
|
|
302
|
+
self.updated_at = datetime.now()
|
|
303
|
+
|
|
304
|
+
# Validation and change tracking
|
|
305
|
+
self.validator = ConfigValidator()
|
|
306
|
+
self.change_callbacks: List[Callable[[str, Any, Any], None]] = []
|
|
307
|
+
self.dirty_sections: Set[str] = set()
|
|
308
|
+
|
|
309
|
+
# File watchers (if enabled)
|
|
310
|
+
self._file_watchers: List[Any] = []
|
|
311
|
+
|
|
312
|
+
# Load existing configuration
|
|
313
|
+
self._load_all_configs()
|
|
314
|
+
|
|
315
|
+
# Set up file watching
|
|
316
|
+
if self.watch_files:
|
|
317
|
+
self._setup_file_watchers()
|
|
318
|
+
|
|
319
|
+
def _get_config_path(
|
|
320
|
+
self, section: str, format: ConfigFormat = ConfigFormat.JSON
|
|
321
|
+
) -> Path:
|
|
322
|
+
"""Get path for configuration file."""
|
|
323
|
+
filename = f"{section}.{format.value}"
|
|
324
|
+
return self.config_dir / filename
|
|
325
|
+
|
|
326
|
+
def _load_config_file(self, path: Path) -> Dict[str, Any]:
|
|
327
|
+
"""Load configuration from file."""
|
|
328
|
+
if not path.exists():
|
|
329
|
+
return {}
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
333
|
+
if path.suffix == ".json":
|
|
334
|
+
return json.load(f)
|
|
335
|
+
elif path.suffix == ".toml":
|
|
336
|
+
return toml.load(f)
|
|
337
|
+
elif path.suffix in [".yaml", ".yml"]:
|
|
338
|
+
return yaml.safe_load(f)
|
|
339
|
+
else:
|
|
340
|
+
raise ConfigurationError(
|
|
341
|
+
f"Unsupported config format: {path.suffix}"
|
|
342
|
+
)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
logging.warning(f"Failed to load config file {path}: {e}")
|
|
345
|
+
return {}
|
|
346
|
+
|
|
347
|
+
def _save_config_file(
|
|
348
|
+
self,
|
|
349
|
+
path: Path,
|
|
350
|
+
data: Dict[str, Any],
|
|
351
|
+
format: ConfigFormat = ConfigFormat.JSON,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Save configuration to file."""
|
|
354
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
358
|
+
if format == ConfigFormat.JSON:
|
|
359
|
+
json.dump(data, f, indent=2, default=str, ensure_ascii=False)
|
|
360
|
+
elif format == ConfigFormat.TOML:
|
|
361
|
+
toml.dump(data, f)
|
|
362
|
+
elif format == ConfigFormat.YAML:
|
|
363
|
+
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise ConfigurationError(f"Failed to save config file {path}: {e}")
|
|
366
|
+
|
|
367
|
+
def _load_all_configs(self) -> None:
|
|
368
|
+
"""Load all configuration files."""
|
|
369
|
+
|
|
370
|
+
# Load security settings
|
|
371
|
+
security_path = self._get_config_path("security")
|
|
372
|
+
security_data = self._load_config_file(security_path)
|
|
373
|
+
if security_data:
|
|
374
|
+
self.validator.validate_security_settings(security_data)
|
|
375
|
+
self.security = SecuritySettings(**security_data)
|
|
376
|
+
|
|
377
|
+
# Load agent settings
|
|
378
|
+
agent_path = self._get_config_path("agent")
|
|
379
|
+
agent_data = self._load_config_file(agent_path)
|
|
380
|
+
if agent_data:
|
|
381
|
+
self.validator.validate_agent_settings(agent_data)
|
|
382
|
+
self.agent = AgentSettings(**agent_data)
|
|
383
|
+
|
|
384
|
+
# Load filesystem settings
|
|
385
|
+
filesystem_path = self._get_config_path("filesystem")
|
|
386
|
+
filesystem_data = self._load_config_file(filesystem_path)
|
|
387
|
+
if filesystem_data:
|
|
388
|
+
self.filesystem = FileSystemSettings(**filesystem_data)
|
|
389
|
+
|
|
390
|
+
# Load web client settings
|
|
391
|
+
webclient_path = self._get_config_path("webclient")
|
|
392
|
+
webclient_data = self._load_config_file(webclient_path)
|
|
393
|
+
if webclient_data:
|
|
394
|
+
self.web_client = WebClientSettings(**webclient_data)
|
|
395
|
+
|
|
396
|
+
# Load user preferences
|
|
397
|
+
preferences_path = self._get_config_path("preferences")
|
|
398
|
+
preferences_data = self._load_config_file(preferences_path)
|
|
399
|
+
if preferences_data:
|
|
400
|
+
self.user_preferences = UserPreferences(**preferences_data)
|
|
401
|
+
|
|
402
|
+
# Load provider configurations
|
|
403
|
+
providers_path = self._get_config_path("providers")
|
|
404
|
+
providers_data = self._load_config_file(providers_path)
|
|
405
|
+
if providers_data:
|
|
406
|
+
for provider_id, provider_data in providers_data.items():
|
|
407
|
+
self.validator.validate_provider_config(provider_data)
|
|
408
|
+
provider_type = ProviderType(provider_data["provider_type"])
|
|
409
|
+
self.providers[provider_id] = ProviderConfig(
|
|
410
|
+
provider_type=provider_type,
|
|
411
|
+
**{k: v for k, v in provider_data.items() if k != "provider_type"},
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def _save_all_configs(self) -> None:
|
|
415
|
+
"""Save all configuration files."""
|
|
416
|
+
|
|
417
|
+
# Save security settings
|
|
418
|
+
if (
|
|
419
|
+
"security" in self.dirty_sections
|
|
420
|
+
or not self._get_config_path("security").exists()
|
|
421
|
+
):
|
|
422
|
+
security_data = asdict(self.security)
|
|
423
|
+
self._save_config_file(self._get_config_path("security"), security_data)
|
|
424
|
+
|
|
425
|
+
# Save agent settings
|
|
426
|
+
if (
|
|
427
|
+
"agent" in self.dirty_sections
|
|
428
|
+
or not self._get_config_path("agent").exists()
|
|
429
|
+
):
|
|
430
|
+
agent_data = asdict(self.agent)
|
|
431
|
+
self._save_config_file(self._get_config_path("agent"), agent_data)
|
|
432
|
+
|
|
433
|
+
# Save filesystem settings
|
|
434
|
+
if (
|
|
435
|
+
"filesystem" in self.dirty_sections
|
|
436
|
+
or not self._get_config_path("filesystem").exists()
|
|
437
|
+
):
|
|
438
|
+
filesystem_data = asdict(self.filesystem)
|
|
439
|
+
self._save_config_file(self._get_config_path("filesystem"), filesystem_data)
|
|
440
|
+
|
|
441
|
+
# Save web client settings
|
|
442
|
+
if (
|
|
443
|
+
"webclient" in self.dirty_sections
|
|
444
|
+
or not self._get_config_path("webclient").exists()
|
|
445
|
+
):
|
|
446
|
+
webclient_data = asdict(self.web_client)
|
|
447
|
+
self._save_config_file(self._get_config_path("webclient"), webclient_data)
|
|
448
|
+
|
|
449
|
+
# Save user preferences
|
|
450
|
+
if (
|
|
451
|
+
"preferences" in self.dirty_sections
|
|
452
|
+
or not self._get_config_path("preferences").exists()
|
|
453
|
+
):
|
|
454
|
+
preferences_data = asdict(self.user_preferences)
|
|
455
|
+
self._save_config_file(
|
|
456
|
+
self._get_config_path("preferences"), preferences_data
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Save provider configurations
|
|
460
|
+
if (
|
|
461
|
+
"providers" in self.dirty_sections
|
|
462
|
+
or not self._get_config_path("providers").exists()
|
|
463
|
+
):
|
|
464
|
+
providers_data = {}
|
|
465
|
+
for provider_id, provider_config in self.providers.items():
|
|
466
|
+
provider_dict = asdict(provider_config)
|
|
467
|
+
provider_dict["provider_type"] = provider_config.provider_type.value
|
|
468
|
+
providers_data[provider_id] = provider_dict
|
|
469
|
+
self._save_config_file(self._get_config_path("providers"), providers_data)
|
|
470
|
+
|
|
471
|
+
# Clear dirty flags and update timestamp
|
|
472
|
+
self.dirty_sections.clear()
|
|
473
|
+
self.updated_at = datetime.now()
|
|
474
|
+
|
|
475
|
+
def _setup_file_watchers(self) -> None:
|
|
476
|
+
"""Set up file system watchers for configuration files."""
|
|
477
|
+
# This would use a library like watchdog in a real implementation
|
|
478
|
+
# For now, we'll skip the actual file watching setup
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def _mark_dirty(self, section: str) -> None:
|
|
482
|
+
"""Mark a configuration section as dirty (needs saving)."""
|
|
483
|
+
self.dirty_sections.add(section)
|
|
484
|
+
|
|
485
|
+
if self.auto_save:
|
|
486
|
+
self._save_all_configs()
|
|
487
|
+
|
|
488
|
+
def _notify_change(self, section: str, old_value: Any, new_value: Any) -> None:
|
|
489
|
+
"""Notify registered callbacks of configuration changes."""
|
|
490
|
+
for callback in self.change_callbacks:
|
|
491
|
+
try:
|
|
492
|
+
callback(section, old_value, new_value)
|
|
493
|
+
except Exception as e:
|
|
494
|
+
logging.warning(f"Configuration change callback failed: {e}")
|
|
495
|
+
|
|
496
|
+
# Configuration access and modification methods
|
|
497
|
+
|
|
498
|
+
def get_security_settings(self) -> SecuritySettings:
|
|
499
|
+
"""Get security settings."""
|
|
500
|
+
return self.security
|
|
501
|
+
|
|
502
|
+
def update_security_settings(self, **kwargs) -> None:
|
|
503
|
+
"""Update security settings."""
|
|
504
|
+
old_settings = asdict(self.security)
|
|
505
|
+
|
|
506
|
+
for key, value in kwargs.items():
|
|
507
|
+
if hasattr(self.security, key):
|
|
508
|
+
setattr(self.security, key, value)
|
|
509
|
+
else:
|
|
510
|
+
raise ConfigurationError(f"Unknown security setting: {key}")
|
|
511
|
+
|
|
512
|
+
# Validate updated settings
|
|
513
|
+
new_settings = asdict(self.security)
|
|
514
|
+
self.validator.validate_security_settings(new_settings)
|
|
515
|
+
|
|
516
|
+
self._mark_dirty("security")
|
|
517
|
+
self._notify_change("security", old_settings, new_settings)
|
|
518
|
+
|
|
519
|
+
def get_agent_settings(self) -> AgentSettings:
|
|
520
|
+
"""Get agent settings."""
|
|
521
|
+
return self.agent
|
|
522
|
+
|
|
523
|
+
def update_agent_settings(self, **kwargs) -> None:
|
|
524
|
+
"""Update agent settings."""
|
|
525
|
+
old_settings = asdict(self.agent)
|
|
526
|
+
|
|
527
|
+
for key, value in kwargs.items():
|
|
528
|
+
if hasattr(self.agent, key):
|
|
529
|
+
setattr(self.agent, key, value)
|
|
530
|
+
else:
|
|
531
|
+
raise ConfigurationError(f"Unknown agent setting: {key}")
|
|
532
|
+
|
|
533
|
+
# Validate updated settings
|
|
534
|
+
new_settings = asdict(self.agent)
|
|
535
|
+
self.validator.validate_agent_settings(new_settings)
|
|
536
|
+
|
|
537
|
+
self._mark_dirty("agent")
|
|
538
|
+
self._notify_change("agent", old_settings, new_settings)
|
|
539
|
+
|
|
540
|
+
def get_provider_config(self, provider_id: str) -> Optional[ProviderConfig]:
|
|
541
|
+
"""Get provider configuration."""
|
|
542
|
+
return self.providers.get(provider_id)
|
|
543
|
+
|
|
544
|
+
def add_provider_config(self, provider_id: str, config: ProviderConfig) -> None:
|
|
545
|
+
"""Add or update provider configuration."""
|
|
546
|
+
# Validate configuration
|
|
547
|
+
config_dict = asdict(config)
|
|
548
|
+
config_dict["provider_type"] = config.provider_type.value
|
|
549
|
+
self.validator.validate_provider_config(config_dict)
|
|
550
|
+
|
|
551
|
+
old_config = self.providers.get(provider_id)
|
|
552
|
+
self.providers[provider_id] = config
|
|
553
|
+
|
|
554
|
+
self._mark_dirty("providers")
|
|
555
|
+
self._notify_change(f"providers.{provider_id}", old_config, config)
|
|
556
|
+
|
|
557
|
+
def remove_provider_config(self, provider_id: str) -> bool:
|
|
558
|
+
"""Remove provider configuration."""
|
|
559
|
+
if provider_id in self.providers:
|
|
560
|
+
old_config = self.providers.pop(provider_id)
|
|
561
|
+
self._mark_dirty("providers")
|
|
562
|
+
self._notify_change(f"providers.{provider_id}", old_config, None)
|
|
563
|
+
return True
|
|
564
|
+
return False
|
|
565
|
+
|
|
566
|
+
def get_all_providers(self) -> Dict[str, ProviderConfig]:
|
|
567
|
+
"""Get all provider configurations."""
|
|
568
|
+
return self.providers.copy()
|
|
569
|
+
|
|
570
|
+
def get_enabled_providers(self) -> Dict[str, ProviderConfig]:
|
|
571
|
+
"""Get all enabled provider configurations."""
|
|
572
|
+
return {pid: config for pid, config in self.providers.items() if config.enabled}
|
|
573
|
+
|
|
574
|
+
def update_user_preferences(self, **kwargs) -> None:
|
|
575
|
+
"""Update user preferences."""
|
|
576
|
+
old_preferences = asdict(self.user_preferences)
|
|
577
|
+
|
|
578
|
+
for key, value in kwargs.items():
|
|
579
|
+
if hasattr(self.user_preferences, key):
|
|
580
|
+
setattr(self.user_preferences, key, value)
|
|
581
|
+
else:
|
|
582
|
+
raise ConfigurationError(f"Unknown user preference: {key}")
|
|
583
|
+
|
|
584
|
+
new_preferences = asdict(self.user_preferences)
|
|
585
|
+
self._mark_dirty("preferences")
|
|
586
|
+
self._notify_change("preferences", old_preferences, new_preferences)
|
|
587
|
+
|
|
588
|
+
def learn_preference(self, preference_type: str, value: Any) -> None:
|
|
589
|
+
"""Learn and store a user preference."""
|
|
590
|
+
if not self.user_preferences.remember_choices:
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
# This could be expanded with ML-based preference learning
|
|
594
|
+
if preference_type == "approval_method":
|
|
595
|
+
self.user_preferences.preferred_approval_method = value
|
|
596
|
+
elif preference_type == "diff_format":
|
|
597
|
+
self.user_preferences.preferred_diff_format = value
|
|
598
|
+
elif preference_type == "auto_approve_type":
|
|
599
|
+
if value not in self.user_preferences.auto_approve_types:
|
|
600
|
+
self.user_preferences.auto_approve_types.append(value)
|
|
601
|
+
|
|
602
|
+
self._mark_dirty("preferences")
|
|
603
|
+
|
|
604
|
+
def add_change_callback(self, callback: Callable[[str, Any, Any], None]) -> None:
|
|
605
|
+
"""Add callback for configuration changes."""
|
|
606
|
+
self.change_callbacks.append(callback)
|
|
607
|
+
|
|
608
|
+
def remove_change_callback(self, callback: Callable[[str, Any, Any], None]) -> None:
|
|
609
|
+
"""Remove configuration change callback."""
|
|
610
|
+
if callback in self.change_callbacks:
|
|
611
|
+
self.change_callbacks.remove(callback)
|
|
612
|
+
|
|
613
|
+
def save(self) -> None:
|
|
614
|
+
"""Manually save all configurations."""
|
|
615
|
+
self._save_all_configs()
|
|
616
|
+
|
|
617
|
+
def reload(self) -> None:
|
|
618
|
+
"""Reload all configurations from files."""
|
|
619
|
+
self._load_all_configs()
|
|
620
|
+
self.dirty_sections.clear()
|
|
621
|
+
|
|
622
|
+
def reset_to_defaults(self, section: Optional[str] = None) -> None:
|
|
623
|
+
"""Reset configuration section to defaults."""
|
|
624
|
+
if section == "security" or section is None:
|
|
625
|
+
self.security = SecuritySettings()
|
|
626
|
+
self._mark_dirty("security")
|
|
627
|
+
|
|
628
|
+
if section == "agent" or section is None:
|
|
629
|
+
self.agent = AgentSettings()
|
|
630
|
+
self._mark_dirty("agent")
|
|
631
|
+
|
|
632
|
+
if section == "filesystem" or section is None:
|
|
633
|
+
self.filesystem = FileSystemSettings()
|
|
634
|
+
self._mark_dirty("filesystem")
|
|
635
|
+
|
|
636
|
+
if section == "webclient" or section is None:
|
|
637
|
+
self.web_client = WebClientSettings()
|
|
638
|
+
self._mark_dirty("webclient")
|
|
639
|
+
|
|
640
|
+
if section == "preferences" or section is None:
|
|
641
|
+
self.user_preferences = UserPreferences()
|
|
642
|
+
self._mark_dirty("preferences")
|
|
643
|
+
|
|
644
|
+
if section == "providers" or section is None:
|
|
645
|
+
self.providers.clear()
|
|
646
|
+
self._mark_dirty("providers")
|
|
647
|
+
|
|
648
|
+
def export_config(
|
|
649
|
+
self,
|
|
650
|
+
export_path: Path,
|
|
651
|
+
format: ConfigFormat = ConfigFormat.JSON,
|
|
652
|
+
include_sensitive: bool = False,
|
|
653
|
+
) -> None:
|
|
654
|
+
"""Export complete configuration to file."""
|
|
655
|
+
config_data = {
|
|
656
|
+
"version": self.version,
|
|
657
|
+
"created_at": self.created_at.isoformat(),
|
|
658
|
+
"updated_at": self.updated_at.isoformat(),
|
|
659
|
+
"security": asdict(self.security),
|
|
660
|
+
"agent": asdict(self.agent),
|
|
661
|
+
"filesystem": asdict(self.filesystem),
|
|
662
|
+
"web_client": asdict(self.web_client),
|
|
663
|
+
"user_preferences": asdict(self.user_preferences),
|
|
664
|
+
"providers": {},
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
# Handle providers
|
|
668
|
+
for provider_id, provider_config in self.providers.items():
|
|
669
|
+
provider_dict = asdict(provider_config)
|
|
670
|
+
provider_dict["provider_type"] = provider_config.provider_type.value
|
|
671
|
+
|
|
672
|
+
# Remove sensitive data if requested
|
|
673
|
+
if not include_sensitive:
|
|
674
|
+
provider_dict.pop("api_key", None)
|
|
675
|
+
provider_dict["custom_headers"] = {
|
|
676
|
+
k: (
|
|
677
|
+
"[REDACTED]"
|
|
678
|
+
if any(
|
|
679
|
+
sensitive in k.lower()
|
|
680
|
+
for sensitive in [
|
|
681
|
+
"key",
|
|
682
|
+
"token",
|
|
683
|
+
"authorization",
|
|
684
|
+
"secret",
|
|
685
|
+
]
|
|
686
|
+
)
|
|
687
|
+
else v
|
|
688
|
+
)
|
|
689
|
+
for k, v in provider_dict.get("custom_headers", {}).items()
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
config_data["providers"][provider_id] = provider_dict
|
|
693
|
+
|
|
694
|
+
self._save_config_file(export_path, config_data, format)
|
|
695
|
+
|
|
696
|
+
def import_config(self, import_path: Path, merge: bool = True) -> None:
|
|
697
|
+
"""Import configuration from file."""
|
|
698
|
+
imported_data = self._load_config_file(import_path)
|
|
699
|
+
|
|
700
|
+
if not imported_data:
|
|
701
|
+
raise ConfigurationError(f"No data found in {import_path}")
|
|
702
|
+
|
|
703
|
+
# Version compatibility check
|
|
704
|
+
imported_version = imported_data.get("version", "1.0")
|
|
705
|
+
if imported_version != self.CONFIG_VERSION:
|
|
706
|
+
logging.warning(
|
|
707
|
+
f"Configuration version mismatch: {imported_version} vs {self.CONFIG_VERSION}"
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
if not merge:
|
|
711
|
+
self.reset_to_defaults()
|
|
712
|
+
|
|
713
|
+
# Import each section
|
|
714
|
+
if "security" in imported_data:
|
|
715
|
+
self.validator.validate_security_settings(imported_data["security"])
|
|
716
|
+
self.security = SecuritySettings(**imported_data["security"])
|
|
717
|
+
self._mark_dirty("security")
|
|
718
|
+
|
|
719
|
+
if "agent" in imported_data:
|
|
720
|
+
self.validator.validate_agent_settings(imported_data["agent"])
|
|
721
|
+
self.agent = AgentSettings(**imported_data["agent"])
|
|
722
|
+
self._mark_dirty("agent")
|
|
723
|
+
|
|
724
|
+
if "filesystem" in imported_data:
|
|
725
|
+
self.filesystem = FileSystemSettings(**imported_data["filesystem"])
|
|
726
|
+
self._mark_dirty("filesystem")
|
|
727
|
+
|
|
728
|
+
if "web_client" in imported_data:
|
|
729
|
+
self.web_client = WebClientSettings(**imported_data["web_client"])
|
|
730
|
+
self._mark_dirty("webclient")
|
|
731
|
+
|
|
732
|
+
if "user_preferences" in imported_data:
|
|
733
|
+
self.user_preferences = UserPreferences(**imported_data["user_preferences"])
|
|
734
|
+
self._mark_dirty("preferences")
|
|
735
|
+
|
|
736
|
+
if "providers" in imported_data:
|
|
737
|
+
for provider_id, provider_data in imported_data["providers"].items():
|
|
738
|
+
self.validator.validate_provider_config(provider_data)
|
|
739
|
+
provider_type = ProviderType(provider_data["provider_type"])
|
|
740
|
+
self.providers[provider_id] = ProviderConfig(
|
|
741
|
+
provider_type=provider_type,
|
|
742
|
+
**{k: v for k, v in provider_data.items() if k != "provider_type"},
|
|
743
|
+
)
|
|
744
|
+
self._mark_dirty("providers")
|
|
745
|
+
|
|
746
|
+
def get_config_summary(self) -> Dict[str, Any]:
|
|
747
|
+
"""Get summary of current configuration."""
|
|
748
|
+
return {
|
|
749
|
+
"version": self.version,
|
|
750
|
+
"config_dir": str(self.config_dir),
|
|
751
|
+
"created_at": self.created_at.isoformat(),
|
|
752
|
+
"updated_at": self.updated_at.isoformat(),
|
|
753
|
+
"sections": {
|
|
754
|
+
"security": {
|
|
755
|
+
"enabled": self.security.enabled,
|
|
756
|
+
"sandboxing": self.security.enable_sandboxing,
|
|
757
|
+
"audit_logging": self.security.enable_audit_logging,
|
|
758
|
+
"allowed_commands_count": len(self.security.allowed_commands),
|
|
759
|
+
"restricted_paths_count": len(self.security.restricted_paths),
|
|
760
|
+
},
|
|
761
|
+
"agent": {
|
|
762
|
+
"enabled": self.agent.enabled,
|
|
763
|
+
"learning_enabled": self.agent.enable_learning,
|
|
764
|
+
"max_concurrent_ops": self.agent.max_concurrent_operations,
|
|
765
|
+
"timeout_seconds": self.agent.default_timeout_seconds,
|
|
766
|
+
},
|
|
767
|
+
"providers": {
|
|
768
|
+
"total_count": len(self.providers),
|
|
769
|
+
"enabled_count": len(self.get_enabled_providers()),
|
|
770
|
+
"types": list(
|
|
771
|
+
set(p.provider_type.value for p in self.providers.values())
|
|
772
|
+
),
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
@asynccontextmanager
|
|
778
|
+
async def config_transaction(self):
|
|
779
|
+
"""Context manager for atomic configuration changes."""
|
|
780
|
+
old_auto_save = self.auto_save
|
|
781
|
+
old_dirty_sections = self.dirty_sections.copy()
|
|
782
|
+
|
|
783
|
+
# Save current state for rollback
|
|
784
|
+
checkpoint_data = {
|
|
785
|
+
"security": asdict(self.security),
|
|
786
|
+
"agent": asdict(self.agent),
|
|
787
|
+
"filesystem": asdict(self.filesystem),
|
|
788
|
+
"web_client": asdict(self.web_client),
|
|
789
|
+
"user_preferences": asdict(self.user_preferences),
|
|
790
|
+
"providers": {k: asdict(v) for k, v in self.providers.items()},
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
# Disable auto-save during transaction
|
|
794
|
+
self.auto_save = False
|
|
795
|
+
|
|
796
|
+
try:
|
|
797
|
+
yield self
|
|
798
|
+
# Save all changes at once
|
|
799
|
+
self._save_all_configs()
|
|
800
|
+
except Exception:
|
|
801
|
+
# Rollback changes to checkpoint state
|
|
802
|
+
self.security = SecuritySettings(**checkpoint_data["security"])
|
|
803
|
+
self.agent = AgentSettings(**checkpoint_data["agent"])
|
|
804
|
+
self.filesystem = FileSystemSettings(**checkpoint_data["filesystem"])
|
|
805
|
+
self.web_client = WebClientSettings(**checkpoint_data["web_client"])
|
|
806
|
+
self.user_preferences = UserPreferences(
|
|
807
|
+
**checkpoint_data["user_preferences"]
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
self.providers.clear()
|
|
811
|
+
for provider_id, provider_data in checkpoint_data["providers"].items():
|
|
812
|
+
# Convert provider_type string back to enum
|
|
813
|
+
provider_data["provider_type"] = ProviderType(
|
|
814
|
+
provider_data["provider_type"]
|
|
815
|
+
)
|
|
816
|
+
self.providers[provider_id] = ProviderConfig(**provider_data)
|
|
817
|
+
|
|
818
|
+
self.dirty_sections = old_dirty_sections
|
|
819
|
+
raise
|
|
820
|
+
finally:
|
|
821
|
+
self.auto_save = old_auto_save
|
|
822
|
+
|
|
823
|
+
def cleanup(self) -> None:
|
|
824
|
+
"""Clean up resources."""
|
|
825
|
+
# Save any pending changes
|
|
826
|
+
if self.dirty_sections and self.auto_save:
|
|
827
|
+
self._save_all_configs()
|
|
828
|
+
|
|
829
|
+
# Clean up file watchers
|
|
830
|
+
for watcher in self._file_watchers:
|
|
831
|
+
try:
|
|
832
|
+
watcher.stop()
|
|
833
|
+
except:
|
|
834
|
+
pass
|
|
835
|
+
self._file_watchers.clear()
|
|
836
|
+
|
|
837
|
+
# Clear callbacks
|
|
838
|
+
self.change_callbacks.clear()
|
|
839
|
+
|
|
840
|
+
def __del__(self):
|
|
841
|
+
"""Destructor to ensure cleanup."""
|
|
842
|
+
try:
|
|
843
|
+
self.cleanup()
|
|
844
|
+
except:
|
|
845
|
+
pass
|