-
Notifications
You must be signed in to change notification settings - Fork 259
Expand file tree
/
Copy pathconfig_manager.py
More file actions
254 lines (213 loc) · 8.4 KB
/
Copy pathconfig_manager.py
File metadata and controls
254 lines (213 loc) · 8.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
"""
Configuration manager with keyring integration for secure credential storage.
"""
import json
from pathlib import Path
from typing import Optional
import keyring
from keyring.errors import KeyringError
from codewiki.cli.models.config import Configuration
from codewiki.cli.utils.errors import ConfigurationError, FileSystemError
from codewiki.cli.utils.fs import ensure_directory, safe_write, safe_read
# Keyring configuration
KEYRING_SERVICE = "codewiki"
KEYRING_API_KEY_ACCOUNT = "api_key"
# Configuration file location
CONFIG_DIR = Path.home() / ".codewiki"
CONFIG_FILE = CONFIG_DIR / "config.json"
CONFIG_VERSION = "1.0"
class ConfigManager:
"""
Manages CodeWiki configuration with secure keyring storage for API keys.
Storage:
- API key: System keychain via keyring (macOS Keychain, Windows Credential Manager,
Linux Secret Service)
- Other settings: ~/.codewiki/config.json
"""
def __init__(self):
"""Initialize the configuration manager."""
self._api_key: Optional[str] = None
self._config: Optional[Configuration] = None
self._keyring_available = self._check_keyring_available()
def _check_keyring_available(self) -> bool:
"""Check if system keyring is available."""
try:
# Try to get/set a test value
keyring.get_password(KEYRING_SERVICE, "__test__")
return True
except KeyringError:
return False
def load(self) -> bool:
"""
Load configuration from file and keyring.
Returns:
True if configuration exists, False otherwise
"""
# Load from JSON file
if not CONFIG_FILE.exists():
return False
try:
content = safe_read(CONFIG_FILE)
data = json.loads(content)
# Validate version
if data.get('version') != CONFIG_VERSION:
# Could implement migration here
pass
self._config = Configuration.from_dict(data)
# Load API key from keyring
try:
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
except KeyringError:
# Keyring unavailable, API key will be None
pass
return True
except (json.JSONDecodeError, FileSystemError) as e:
raise ConfigurationError(f"Failed to load configuration: {e}")
def save(
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
main_model: Optional[str] = None,
cluster_model: Optional[str] = None,
fallback_model: Optional[str] = None,
default_output: Optional[str] = None,
max_tokens: Optional[int] = None,
max_token_per_module: Optional[int] = None,
max_token_per_leaf_module: Optional[int] = None,
max_depth: Optional[int] = None,
respect_gitignore: Optional[bool] = None,
):
"""
Save configuration to file and keyring.
Args:
api_key: API key (stored in keyring)
base_url: LLM API base URL
main_model: Primary model
cluster_model: Clustering model
fallback_model: Fallback model
default_output: Default output directory
max_tokens: Maximum tokens for LLM response
max_token_per_module: Maximum tokens per module for clustering
max_token_per_leaf_module: Maximum tokens per leaf module
max_depth: Maximum depth for hierarchical decomposition
respect_gitignore: Respect .gitignore patterns during analysis
"""
# Ensure config directory exists
try:
ensure_directory(CONFIG_DIR)
except FileSystemError as e:
raise ConfigurationError(f"Cannot create config directory: {e}")
# Load existing config or create new
if self._config is None:
if CONFIG_FILE.exists():
self.load()
else:
from codewiki.cli.models.config import AgentInstructions
self._config = Configuration(
base_url="",
main_model="",
cluster_model="",
fallback_model="glm-4p5",
default_output="docs",
agent_instructions=AgentInstructions()
)
# Update fields if provided
if base_url is not None:
self._config.base_url = base_url
if main_model is not None:
self._config.main_model = main_model
if cluster_model is not None:
self._config.cluster_model = cluster_model
if fallback_model is not None:
self._config.fallback_model = fallback_model
if default_output is not None:
self._config.default_output = default_output
if max_tokens is not None:
self._config.max_tokens = max_tokens
if max_token_per_module is not None:
self._config.max_token_per_module = max_token_per_module
if max_token_per_leaf_module is not None:
self._config.max_token_per_leaf_module = max_token_per_leaf_module
if max_depth is not None:
self._config.max_depth = max_depth
if respect_gitignore is not None:
self._config.respect_gitignore = respect_gitignore
# Validate configuration (only if base fields are set)
if self._config.base_url and self._config.main_model and self._config.cluster_model:
self._config.validate()
# Save API key to keyring
if api_key is not None:
self._api_key = api_key
try:
keyring.set_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT, api_key)
except KeyringError as e:
# Fallback: warn about keyring unavailability
raise ConfigurationError(
f"System keychain unavailable: {e}\n"
f"Please ensure your system keychain is properly configured."
)
# Save non-sensitive config to JSON
config_data = {
"version": CONFIG_VERSION,
**self._config.to_dict()
}
try:
safe_write(CONFIG_FILE, json.dumps(config_data, indent=2))
except FileSystemError as e:
raise ConfigurationError(f"Failed to save configuration: {e}")
def get_api_key(self) -> Optional[str]:
"""
Get API key from keyring.
Returns:
API key or None if not set
"""
if self._api_key is None:
try:
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
except KeyringError:
pass
return self._api_key
def get_config(self) -> Optional[Configuration]:
"""
Get current configuration.
Returns:
Configuration object or None if not loaded
"""
return self._config
def is_configured(self) -> bool:
"""
Check if configuration is complete and valid.
Returns:
True if configured, False otherwise
"""
if self._config is None:
return False
# Check if API key is set
if self.get_api_key() is None:
return False
# Check if config is complete
return self._config.is_complete()
def delete_api_key(self):
"""Delete API key from keyring."""
try:
keyring.delete_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
self._api_key = None
except KeyringError:
pass
def clear(self):
"""Clear all configuration (file and keyring)."""
# Delete API key from keyring
self.delete_api_key()
# Delete config file
if CONFIG_FILE.exists():
CONFIG_FILE.unlink()
self._config = None
self._api_key = None
@property
def keyring_available(self) -> bool:
"""Check if keyring is available."""
return self._keyring_available
@property
def config_file_path(self) -> Path:
"""Get configuration file path."""
return CONFIG_FILE