minion-code 0.1.0__py3-none-any.whl → 0.1.2__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 (115) hide show
  1. examples/cli_entrypoint.py +60 -0
  2. examples/{agent_with_todos.py → components/agent_with_todos.py} +58 -47
  3. examples/{message_response_children_demo.py → components/message_response_children_demo.py} +61 -55
  4. examples/components/messages_component.py +199 -0
  5. examples/file_freshness_example.py +22 -22
  6. examples/file_watching_example.py +32 -26
  7. examples/interruptible_tui.py +921 -3
  8. examples/repl_tui.py +129 -0
  9. examples/skills/example_usage.py +57 -0
  10. examples/start.py +173 -0
  11. minion_code/__init__.py +1 -1
  12. minion_code/acp_server/__init__.py +34 -0
  13. minion_code/acp_server/agent.py +539 -0
  14. minion_code/acp_server/hooks.py +354 -0
  15. minion_code/acp_server/main.py +194 -0
  16. minion_code/acp_server/permissions.py +142 -0
  17. minion_code/acp_server/test_client.py +104 -0
  18. minion_code/adapters/__init__.py +22 -0
  19. minion_code/adapters/output_adapter.py +207 -0
  20. minion_code/adapters/rich_adapter.py +169 -0
  21. minion_code/adapters/textual_adapter.py +254 -0
  22. minion_code/agents/__init__.py +2 -2
  23. minion_code/agents/code_agent.py +517 -104
  24. minion_code/agents/hooks.py +378 -0
  25. minion_code/cli.py +538 -429
  26. minion_code/cli_simple.py +665 -0
  27. minion_code/commands/__init__.py +136 -29
  28. minion_code/commands/clear_command.py +19 -46
  29. minion_code/commands/help_command.py +33 -49
  30. minion_code/commands/history_command.py +37 -55
  31. minion_code/commands/model_command.py +194 -0
  32. minion_code/commands/quit_command.py +9 -12
  33. minion_code/commands/resume_command.py +181 -0
  34. minion_code/commands/skill_command.py +89 -0
  35. minion_code/commands/status_command.py +48 -73
  36. minion_code/commands/tools_command.py +54 -52
  37. minion_code/commands/version_command.py +34 -69
  38. minion_code/components/ConfirmDialog.py +430 -0
  39. minion_code/components/Message.py +318 -97
  40. minion_code/components/MessageResponse.py +30 -29
  41. minion_code/components/Messages.py +351 -0
  42. minion_code/components/PromptInput.py +499 -245
  43. minion_code/components/__init__.py +24 -17
  44. minion_code/const.py +7 -0
  45. minion_code/screens/REPL.py +1453 -469
  46. minion_code/screens/__init__.py +1 -1
  47. minion_code/services/__init__.py +20 -20
  48. minion_code/services/event_system.py +19 -14
  49. minion_code/services/file_freshness_service.py +223 -170
  50. minion_code/skills/__init__.py +25 -0
  51. minion_code/skills/skill.py +128 -0
  52. minion_code/skills/skill_loader.py +198 -0
  53. minion_code/skills/skill_registry.py +177 -0
  54. minion_code/subagents/__init__.py +31 -0
  55. minion_code/subagents/builtin/__init__.py +30 -0
  56. minion_code/subagents/builtin/claude_code_guide.py +32 -0
  57. minion_code/subagents/builtin/explore.py +36 -0
  58. minion_code/subagents/builtin/general_purpose.py +19 -0
  59. minion_code/subagents/builtin/plan.py +61 -0
  60. minion_code/subagents/subagent.py +116 -0
  61. minion_code/subagents/subagent_loader.py +147 -0
  62. minion_code/subagents/subagent_registry.py +151 -0
  63. minion_code/tools/__init__.py +8 -2
  64. minion_code/tools/bash_tool.py +16 -3
  65. minion_code/tools/file_edit_tool.py +201 -104
  66. minion_code/tools/file_read_tool.py +183 -26
  67. minion_code/tools/file_write_tool.py +17 -3
  68. minion_code/tools/glob_tool.py +23 -2
  69. minion_code/tools/grep_tool.py +229 -21
  70. minion_code/tools/ls_tool.py +28 -3
  71. minion_code/tools/multi_edit_tool.py +89 -84
  72. minion_code/tools/python_interpreter_tool.py +9 -1
  73. minion_code/tools/skill_tool.py +210 -0
  74. minion_code/tools/task_tool.py +287 -0
  75. minion_code/tools/todo_read_tool.py +28 -24
  76. minion_code/tools/todo_write_tool.py +82 -65
  77. minion_code/{types.py → type_defs.py} +15 -2
  78. minion_code/utils/__init__.py +45 -17
  79. minion_code/utils/config.py +610 -0
  80. minion_code/utils/history.py +114 -0
  81. minion_code/utils/logs.py +53 -0
  82. minion_code/utils/mcp_loader.py +153 -55
  83. minion_code/utils/output_truncator.py +233 -0
  84. minion_code/utils/session_storage.py +369 -0
  85. minion_code/utils/todo_file_utils.py +26 -22
  86. minion_code/utils/todo_storage.py +43 -33
  87. minion_code/web/__init__.py +9 -0
  88. minion_code/web/adapters/__init__.py +5 -0
  89. minion_code/web/adapters/web_adapter.py +524 -0
  90. minion_code/web/api/__init__.py +7 -0
  91. minion_code/web/api/chat.py +277 -0
  92. minion_code/web/api/interactions.py +136 -0
  93. minion_code/web/api/sessions.py +135 -0
  94. minion_code/web/server.py +149 -0
  95. minion_code/web/services/__init__.py +5 -0
  96. minion_code/web/services/session_manager.py +420 -0
  97. minion_code-0.1.2.dist-info/METADATA +476 -0
  98. minion_code-0.1.2.dist-info/RECORD +111 -0
  99. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/WHEEL +1 -1
  100. minion_code-0.1.2.dist-info/entry_points.txt +6 -0
  101. tests/test_adapter.py +67 -0
  102. tests/test_adapter_simple.py +79 -0
  103. tests/test_file_read_tool.py +144 -0
  104. tests/test_readonly_tools.py +0 -2
  105. tests/test_skills.py +441 -0
  106. examples/advance_tui.py +0 -508
  107. examples/rich_example.py +0 -4
  108. examples/simple_file_watching.py +0 -57
  109. examples/simple_tui.py +0 -267
  110. examples/simple_usage.py +0 -69
  111. minion_code-0.1.0.dist-info/METADATA +0 -350
  112. minion_code-0.1.0.dist-info/RECORD +0 -59
  113. minion_code-0.1.0.dist-info/entry_points.txt +0 -4
  114. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/licenses/LICENSE +0 -0
  115. {minion_code-0.1.0.dist-info → minion_code-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,610 @@
1
+ """Configuration management utilities for minion-code.
2
+
3
+ This module provides configuration management functionality similar to the TypeScript
4
+ config.ts file, adapted for Python and the minion-code project structure.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import secrets
10
+ from copy import deepcopy
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
13
+ from dataclasses import dataclass, field, asdict
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Type definitions
19
+ McpStdioServerConfig = TypedDict(
20
+ "McpStdioServerConfig",
21
+ {
22
+ "type": Optional[str], # Optional for backwards compatibility
23
+ "command": str,
24
+ "args": List[str],
25
+ "env": Optional[Dict[str, str]],
26
+ },
27
+ )
28
+
29
+ McpSSEServerConfig = TypedDict(
30
+ "McpSSEServerConfig", {"type": Literal["sse"], "url": str}
31
+ )
32
+
33
+ McpServerConfig = Union[McpStdioServerConfig, McpSSEServerConfig]
34
+
35
+ AutoUpdaterStatus = Literal["disabled", "enabled", "no_permissions", "not_configured"]
36
+ NotificationChannel = Literal[
37
+ "iterm2", "terminal_bell", "iterm2_with_bell", "notifications_disabled"
38
+ ]
39
+ ProviderType = Literal[
40
+ "anthropic",
41
+ "openai",
42
+ "mistral",
43
+ "deepseek",
44
+ "kimi",
45
+ "qwen",
46
+ "glm",
47
+ "minimax",
48
+ "baidu-qianfan",
49
+ "siliconflow",
50
+ "bigdream",
51
+ "opendev",
52
+ "xai",
53
+ "groq",
54
+ "gemini",
55
+ "ollama",
56
+ "azure",
57
+ "custom",
58
+ "custom-openai",
59
+ ]
60
+
61
+ ReasoningEffort = Literal["low", "medium", "high", "minimal"]
62
+ ModelPointerType = Literal["main", "task", "reasoning", "quick"]
63
+ ValidationStatus = Literal["valid", "needs_repair", "auto_repaired"]
64
+
65
+
66
+ @dataclass
67
+ class ModelProfile:
68
+ """Model profile configuration."""
69
+
70
+ name: str # User-friendly name
71
+ provider: ProviderType # Provider type
72
+ model_name: str # Primary key - actual model identifier
73
+ api_key: str
74
+ max_tokens: int # Output token limit
75
+ context_length: int # Context window size
76
+ is_active: bool = True # Whether profile is enabled
77
+ created_at: int = field(default_factory=lambda: int(os.time.time() * 1000))
78
+ base_url: Optional[str] = None # Custom endpoint
79
+ reasoning_effort: Optional[ReasoningEffort] = None
80
+ last_used: Optional[int] = None # Last usage timestamp
81
+ is_gpt5: Optional[bool] = None # Auto-detected GPT-5 model flag
82
+ validation_status: Optional[ValidationStatus] = None # Configuration status
83
+ last_validation: Optional[int] = None # Last validation timestamp
84
+
85
+
86
+ @dataclass
87
+ class ModelPointers:
88
+ """Model pointer system."""
89
+
90
+ main: str = "" # Main dialog model ID
91
+ task: str = "" # Task tool model ID
92
+ reasoning: str = "" # Reasoning model ID
93
+ quick: str = "" # Quick model ID
94
+
95
+
96
+ @dataclass
97
+ class AccountInfo:
98
+ """Account information."""
99
+
100
+ account_uuid: str
101
+ email_address: str
102
+ organization_uuid: Optional[str] = None
103
+
104
+
105
+ @dataclass
106
+ class ProjectConfig:
107
+ """Project-specific configuration."""
108
+
109
+ allowed_tools: List[str] = field(default_factory=list)
110
+ context: Dict[str, str] = field(default_factory=dict)
111
+ context_files: Optional[List[str]] = None
112
+ history: List[str] = field(default_factory=list)
113
+ dont_crawl_directory: bool = False
114
+ enable_architect_tool: bool = False
115
+ mcp_context_uris: List[str] = field(default_factory=list)
116
+ mcp_servers: Optional[Dict[str, McpServerConfig]] = field(default_factory=dict)
117
+ approved_mcprc_servers: Optional[List[str]] = field(default_factory=list)
118
+ rejected_mcprc_servers: Optional[List[str]] = field(default_factory=list)
119
+ last_api_duration: Optional[float] = None
120
+ last_cost: Optional[float] = None
121
+ last_duration: Optional[float] = None
122
+ last_session_id: Optional[str] = None
123
+ example_files: Optional[List[str]] = None
124
+ example_files_generated_at: Optional[int] = None
125
+ has_trust_dialog_accepted: bool = False
126
+ has_completed_project_onboarding: bool = False
127
+
128
+
129
+ @dataclass
130
+ class GlobalConfig:
131
+ """Global configuration."""
132
+
133
+ num_startups: int = 0
134
+ auto_updater_status: AutoUpdaterStatus = "not_configured"
135
+ user_id: Optional[str] = None
136
+ theme: str = "dark"
137
+ has_completed_onboarding: Optional[bool] = None
138
+ last_onboarding_version: Optional[str] = None
139
+ last_release_notes_seen: Optional[str] = None
140
+ mcp_servers: Optional[Dict[str, McpServerConfig]] = field(default_factory=dict)
141
+ preferred_notif_channel: NotificationChannel = "iterm2"
142
+ verbose: bool = False
143
+ custom_api_key_responses: Optional[Dict[str, List[str]]] = field(
144
+ default_factory=lambda: {"approved": [], "rejected": []}
145
+ )
146
+ primary_provider: ProviderType = "anthropic"
147
+ max_tokens: Optional[int] = None
148
+ has_acknowledged_cost_threshold: Optional[bool] = None
149
+ oauth_account: Optional[AccountInfo] = None
150
+ iterm2_key_binding_installed: Optional[bool] = None # Legacy
151
+ shift_enter_key_binding_installed: Optional[bool] = None
152
+ proxy: Optional[str] = None
153
+ stream: bool = True
154
+ projects: Optional[Dict[str, ProjectConfig]] = field(default_factory=dict)
155
+
156
+ # New model system
157
+ model_profiles: Optional[List[ModelProfile]] = field(default_factory=list)
158
+ model_pointers: Optional[ModelPointers] = field(default_factory=ModelPointers)
159
+ default_model_name: Optional[str] = None
160
+ last_dismissed_update_version: Optional[str] = None
161
+
162
+
163
+ # Configuration keys that can be modified
164
+ GLOBAL_CONFIG_KEYS = [
165
+ "auto_updater_status",
166
+ "theme",
167
+ "has_completed_onboarding",
168
+ "last_onboarding_version",
169
+ "last_release_notes_seen",
170
+ "verbose",
171
+ "custom_api_key_responses",
172
+ "primary_provider",
173
+ "preferred_notif_channel",
174
+ "shift_enter_key_binding_installed",
175
+ "max_tokens",
176
+ ]
177
+
178
+ PROJECT_CONFIG_KEYS = [
179
+ "dont_crawl_directory",
180
+ "enable_architect_tool",
181
+ "has_trust_dialog_accepted",
182
+ "has_completed_project_onboarding",
183
+ ]
184
+
185
+
186
+ class ConfigParseError(Exception):
187
+ """Configuration parsing error."""
188
+
189
+ def __init__(self, message: str, file_path: str, default_config: Any):
190
+ super().__init__(message)
191
+ self.file_path = file_path
192
+ self.default_config = default_config
193
+
194
+
195
+ class ConfigManager:
196
+ """Configuration manager for minion-code."""
197
+
198
+ def __init__(self, config_dir: Optional[Path] = None):
199
+ """Initialize configuration manager.
200
+
201
+ Args:
202
+ config_dir: Directory for configuration files. Defaults to ~/.minion-code
203
+ """
204
+ if config_dir is None:
205
+ config_dir = Path.home() / ".minion-code"
206
+
207
+ self.config_dir = Path(config_dir)
208
+ self.config_dir.mkdir(exist_ok=True)
209
+ self.global_config_file = self.config_dir / "config.json"
210
+
211
+ # Test configurations for testing environment
212
+ self._test_global_config: Optional[GlobalConfig] = None
213
+ self._test_project_config: Optional[ProjectConfig] = None
214
+
215
+ def _is_test_env(self) -> bool:
216
+ """Check if running in test environment."""
217
+ return (
218
+ os.getenv("PYTHON_ENV") == "test"
219
+ or os.getenv("PYTEST_CURRENT_TEST") is not None
220
+ )
221
+
222
+ def _get_current_project_path(self) -> str:
223
+ """Get current project path."""
224
+ return os.getcwd()
225
+
226
+ def _default_project_config(self, project_path: str) -> ProjectConfig:
227
+ """Get default configuration for a project."""
228
+ config = ProjectConfig()
229
+ if project_path == str(Path.home()):
230
+ config.dont_crawl_directory = True
231
+ return config
232
+
233
+ def _safe_parse_json(self, content: str) -> Any:
234
+ """Safely parse JSON content."""
235
+ try:
236
+ return json.loads(content)
237
+ except json.JSONDecodeError as e:
238
+ logger.error(f"JSON parse error: {e}")
239
+ return None
240
+
241
+ def _save_config(self, file_path: Path, config: Any, default_config: Any) -> None:
242
+ """Save configuration to file."""
243
+ # Convert dataclass to dict if needed
244
+ if hasattr(config, "__dataclass_fields__"):
245
+ config_dict = asdict(config)
246
+ else:
247
+ config_dict = config
248
+
249
+ if hasattr(default_config, "__dataclass_fields__"):
250
+ default_dict = asdict(default_config)
251
+ else:
252
+ default_dict = default_config
253
+
254
+ # Filter out values that match defaults
255
+ filtered_config = {}
256
+ for key, value in config_dict.items():
257
+ if key in default_dict:
258
+ if json.dumps(value, sort_keys=True) != json.dumps(
259
+ default_dict[key], sort_keys=True
260
+ ):
261
+ filtered_config[key] = value
262
+ else:
263
+ filtered_config[key] = value
264
+
265
+ try:
266
+ with open(file_path, "w", encoding="utf-8") as f:
267
+ json.dump(filtered_config, f, indent=2, ensure_ascii=False)
268
+ except (PermissionError, OSError) as e:
269
+ logger.warning(f"Could not save config to {file_path}: {e}")
270
+
271
+ def _load_config(
272
+ self, file_path: Path, default_config: Any, throw_on_invalid: bool = False
273
+ ) -> Any:
274
+ """Load configuration from file."""
275
+ logger.debug(f"Loading config from {file_path}")
276
+
277
+ if not file_path.exists():
278
+ logger.debug(f"Config file {file_path} does not exist, using defaults")
279
+ return deepcopy(default_config)
280
+
281
+ try:
282
+ with open(file_path, "r", encoding="utf-8") as f:
283
+ content = f.read()
284
+
285
+ parsed_config = self._safe_parse_json(content)
286
+ if parsed_config is None:
287
+ if throw_on_invalid:
288
+ raise ConfigParseError(
289
+ f"Invalid JSON in {file_path}", str(file_path), default_config
290
+ )
291
+ logger.warning(f"Invalid JSON in {file_path}, using defaults")
292
+ return deepcopy(default_config)
293
+
294
+ # Merge with defaults
295
+ if hasattr(default_config, "__dataclass_fields__"):
296
+ # Handle dataclass
297
+ default_dict = asdict(default_config)
298
+ merged_dict = {**default_dict, **parsed_config}
299
+ # Convert back to dataclass
300
+ return type(default_config)(**merged_dict)
301
+ else:
302
+ # Handle regular dict
303
+ return {**deepcopy(default_config), **parsed_config}
304
+
305
+ except Exception as e:
306
+ if throw_on_invalid:
307
+ raise ConfigParseError(str(e), str(file_path), default_config)
308
+ logger.warning(
309
+ f"Error loading config from {file_path}: {e}, using defaults"
310
+ )
311
+ return deepcopy(default_config)
312
+
313
+ def get_global_config(self) -> GlobalConfig:
314
+ """Get global configuration."""
315
+ if self._is_test_env() and self._test_global_config is not None:
316
+ return self._test_global_config
317
+
318
+ config = self._load_config(self.global_config_file, GlobalConfig())
319
+ return self._migrate_model_profiles_remove_id(config)
320
+
321
+ def save_global_config(self, config: GlobalConfig) -> None:
322
+ """Save global configuration."""
323
+ if self._is_test_env():
324
+ self._test_global_config = config
325
+ return
326
+
327
+ # Preserve projects when saving global config
328
+ current_config = self._load_config(self.global_config_file, GlobalConfig())
329
+ config.projects = current_config.projects
330
+
331
+ self._save_config(self.global_config_file, config, GlobalConfig())
332
+
333
+ def get_current_project_config(self) -> ProjectConfig:
334
+ """Get current project configuration."""
335
+ if self._is_test_env() and self._test_project_config is not None:
336
+ return self._test_project_config
337
+
338
+ project_path = self._get_current_project_path()
339
+ global_config = self.get_global_config()
340
+
341
+ if not global_config.projects:
342
+ return self._default_project_config(project_path)
343
+
344
+ project_config_data = global_config.projects.get(project_path)
345
+ if project_config_data is None:
346
+ return self._default_project_config(project_path)
347
+
348
+ # Convert dict to ProjectConfig instance if needed
349
+ if isinstance(project_config_data, dict):
350
+ # Handle legacy string format for allowed_tools
351
+ if isinstance(project_config_data.get("allowed_tools"), str):
352
+ try:
353
+ project_config_data["allowed_tools"] = json.loads(
354
+ project_config_data["allowed_tools"]
355
+ )
356
+ except json.JSONDecodeError:
357
+ project_config_data["allowed_tools"] = []
358
+
359
+ # Create ProjectConfig instance from dict
360
+ project_config = ProjectConfig(**project_config_data)
361
+ else:
362
+ # Already a ProjectConfig instance
363
+ project_config = project_config_data
364
+ # Handle legacy string format for allowed_tools
365
+ if isinstance(project_config.allowed_tools, str):
366
+ try:
367
+ project_config.allowed_tools = json.loads(
368
+ project_config.allowed_tools
369
+ )
370
+ except json.JSONDecodeError:
371
+ project_config.allowed_tools = []
372
+
373
+ return project_config
374
+
375
+ def save_current_project_config(self, project_config: ProjectConfig) -> None:
376
+ """Save current project configuration."""
377
+ if self._is_test_env():
378
+ self._test_project_config = project_config
379
+ return
380
+
381
+ project_path = self._get_current_project_path()
382
+ global_config = self.get_global_config()
383
+
384
+ if global_config.projects is None:
385
+ global_config.projects = {}
386
+
387
+ global_config.projects[project_path] = project_config
388
+ self._save_config(self.global_config_file, global_config, GlobalConfig())
389
+
390
+ def get_anthropic_api_key(self) -> Optional[str]:
391
+ """Get Anthropic API key from environment."""
392
+ return os.getenv("ANTHROPIC_API_KEY")
393
+
394
+ def get_openai_api_key(self) -> Optional[str]:
395
+ """Get OpenAI API key from environment."""
396
+ return os.getenv("OPENAI_API_KEY")
397
+
398
+ def normalize_api_key_for_config(self, api_key: str) -> str:
399
+ """Normalize API key for configuration storage."""
400
+ return api_key[-20:] if api_key else ""
401
+
402
+ def get_custom_api_key_status(
403
+ self, truncated_api_key: str
404
+ ) -> Literal["approved", "rejected", "new"]:
405
+ """Get custom API key status."""
406
+ config = self.get_global_config()
407
+ responses = config.custom_api_key_responses or {"approved": [], "rejected": []}
408
+
409
+ if truncated_api_key in responses.get("approved", []):
410
+ return "approved"
411
+ if truncated_api_key in responses.get("rejected", []):
412
+ return "rejected"
413
+ return "new"
414
+
415
+ def get_or_create_user_id(self) -> str:
416
+ """Get or create user ID."""
417
+ config = self.get_global_config()
418
+ if config.user_id:
419
+ return config.user_id
420
+
421
+ user_id = secrets.token_hex(32)
422
+ config.user_id = user_id
423
+ self.save_global_config(config)
424
+ return user_id
425
+
426
+ def check_has_trust_dialog_accepted(self) -> bool:
427
+ """Check if trust dialog has been accepted for current or parent directories."""
428
+ current_path = Path(self._get_current_project_path())
429
+ config = self.get_global_config()
430
+
431
+ # Check current and parent directories
432
+ for path in [current_path] + list(current_path.parents):
433
+ path_str = str(path)
434
+ project_config = config.projects.get(path_str) if config.projects else None
435
+ if project_config and project_config.has_trust_dialog_accepted:
436
+ return True
437
+
438
+ return False
439
+
440
+ def _migrate_model_profiles_remove_id(self, config: GlobalConfig) -> GlobalConfig:
441
+ """Migrate model profiles to remove ID field and update pointers."""
442
+ if not config.model_profiles:
443
+ return config
444
+
445
+ # Build ID to model_name mapping and remove id field
446
+ id_to_model_name = {}
447
+ migrated_profiles = []
448
+
449
+ for profile in config.model_profiles:
450
+ profile_dict = (
451
+ asdict(profile) if hasattr(profile, "__dataclass_fields__") else profile
452
+ )
453
+
454
+ # Build mapping before removing id field
455
+ if "id" in profile_dict and "model_name" in profile_dict:
456
+ id_to_model_name[profile_dict["id"]] = profile_dict["model_name"]
457
+
458
+ # Remove id field
459
+ profile_dict.pop("id", None)
460
+ migrated_profiles.append(ModelProfile(**profile_dict))
461
+
462
+ # Migrate model pointers
463
+ migrated_pointers = ModelPointers()
464
+ if config.model_pointers:
465
+ pointers_dict = (
466
+ asdict(config.model_pointers)
467
+ if hasattr(config.model_pointers, "__dataclass_fields__")
468
+ else config.model_pointers
469
+ )
470
+ for pointer, value in pointers_dict.items():
471
+ if value:
472
+ model_name = id_to_model_name.get(value, value)
473
+ setattr(migrated_pointers, pointer, model_name)
474
+
475
+ # Migrate legacy fields
476
+ default_model_name = config.default_model_name
477
+ if hasattr(config, "default_model_id"):
478
+ default_model_name = id_to_model_name.get(
479
+ getattr(config, "default_model_id"), getattr(config, "default_model_id")
480
+ )
481
+
482
+ config.model_profiles = migrated_profiles
483
+ config.model_pointers = migrated_pointers
484
+ config.default_model_name = default_model_name
485
+
486
+ return config
487
+
488
+ # GPT-5 specific functions
489
+ def is_gpt5_model_name(self, model_name: str) -> bool:
490
+ """Check if a model name represents a GPT-5 model."""
491
+ if not model_name or not isinstance(model_name, str):
492
+ return False
493
+ return "gpt-5" in model_name.lower()
494
+
495
+ def validate_and_repair_gpt5_profile(self, profile: ModelProfile) -> ModelProfile:
496
+ """Validate and auto-repair GPT-5 model configuration."""
497
+ is_gpt5 = self.is_gpt5_model_name(profile.model_name)
498
+ now = int(os.time.time() * 1000)
499
+
500
+ # Create working copy
501
+ repaired_profile = deepcopy(profile)
502
+ was_repaired = False
503
+
504
+ # Set GPT-5 detection flag
505
+ if is_gpt5 != profile.is_gpt5:
506
+ repaired_profile.is_gpt5 = is_gpt5
507
+ was_repaired = True
508
+
509
+ if is_gpt5:
510
+ # GPT-5 parameter validation and repair
511
+ valid_reasoning_efforts = ["minimal", "low", "medium", "high"]
512
+ if (
513
+ not profile.reasoning_effort
514
+ or profile.reasoning_effort not in valid_reasoning_efforts
515
+ ):
516
+ repaired_profile.reasoning_effort = "medium"
517
+ was_repaired = True
518
+ logger.info(
519
+ f"🔧 GPT-5 Config: Set reasoning effort to 'medium' for {profile.model_name}"
520
+ )
521
+
522
+ # Context length validation
523
+ if profile.context_length < 128000:
524
+ repaired_profile.context_length = 128000
525
+ was_repaired = True
526
+ logger.info(
527
+ f"🔧 GPT-5 Config: Updated context length to 128k for {profile.model_name}"
528
+ )
529
+
530
+ # Output tokens validation
531
+ if profile.max_tokens < 4000:
532
+ repaired_profile.max_tokens = 8192
533
+ was_repaired = True
534
+ logger.info(
535
+ f"🔧 GPT-5 Config: Updated max tokens to 8192 for {profile.model_name}"
536
+ )
537
+
538
+ # Base URL validation
539
+ if "gpt-5" in profile.model_name and not profile.base_url:
540
+ repaired_profile.base_url = "https://api.openai.com/v1"
541
+ was_repaired = True
542
+ logger.info(
543
+ f"🔧 GPT-5 Config: Set default base URL for {profile.model_name}"
544
+ )
545
+
546
+ # Update validation metadata
547
+ repaired_profile.validation_status = (
548
+ "auto_repaired" if was_repaired else "valid"
549
+ )
550
+ repaired_profile.last_validation = now
551
+
552
+ if was_repaired:
553
+ logger.info(
554
+ f"✅ GPT-5 Config: Auto-repaired configuration for {profile.model_name}"
555
+ )
556
+
557
+ return repaired_profile
558
+
559
+ def set_model_pointer(self, pointer: ModelPointerType, model_name: str) -> None:
560
+ """Set a model pointer to a specific model."""
561
+ config = self.get_global_config()
562
+ if config.model_pointers is None:
563
+ config.model_pointers = ModelPointers()
564
+
565
+ setattr(config.model_pointers, pointer, model_name)
566
+ self.save_global_config(config)
567
+
568
+ def set_all_pointers_to_model(self, model_name: str) -> None:
569
+ """Set all model pointers to the same model."""
570
+ config = self.get_global_config()
571
+ config.model_pointers = ModelPointers(
572
+ main=model_name, task=model_name, reasoning=model_name, quick=model_name
573
+ )
574
+ config.default_model_name = model_name
575
+ self.save_global_config(config)
576
+
577
+
578
+ # Global instance
579
+ config_manager = ConfigManager()
580
+
581
+
582
+ # Convenience functions
583
+ def get_global_config() -> GlobalConfig:
584
+ """Get global configuration."""
585
+ return config_manager.get_global_config()
586
+
587
+
588
+ def save_global_config(config: GlobalConfig) -> None:
589
+ """Save global configuration."""
590
+ config_manager.save_global_config(config)
591
+
592
+
593
+ def get_current_project_config() -> ProjectConfig:
594
+ """Get current project configuration."""
595
+ return config_manager.get_current_project_config()
596
+
597
+
598
+ def save_current_project_config(config: ProjectConfig) -> None:
599
+ """Save current project configuration."""
600
+ config_manager.save_current_project_config(config)
601
+
602
+
603
+ def get_anthropic_api_key() -> Optional[str]:
604
+ """Get Anthropic API key."""
605
+ return config_manager.get_anthropic_api_key()
606
+
607
+
608
+ def get_openai_api_key() -> Optional[str]:
609
+ """Get OpenAI API key."""
610
+ return config_manager.get_openai_api_key()