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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. 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