cloudshellgpt 1.0.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.
- cloudshellgpt/__init__.py +11 -0
- cloudshellgpt/audit.py +175 -0
- cloudshellgpt/bedrock_translator.py +525 -0
- cloudshellgpt/cli.py +612 -0
- cloudshellgpt/config.py +296 -0
- cloudshellgpt/cost.py +443 -0
- cloudshellgpt/executor.py +382 -0
- cloudshellgpt/formatter.py +328 -0
- cloudshellgpt/i18n.py +203 -0
- cloudshellgpt/intent.py +1080 -0
- cloudshellgpt/learning.py +969 -0
- cloudshellgpt/mcp_server.py +264 -0
- cloudshellgpt/safety.py +952 -0
- cloudshellgpt-1.0.0.dist-info/METADATA +426 -0
- cloudshellgpt-1.0.0.dist-info/RECORD +18 -0
- cloudshellgpt-1.0.0.dist-info/WHEEL +4 -0
- cloudshellgpt-1.0.0.dist-info/entry_points.txt +2 -0
- cloudshellgpt-1.0.0.dist-info/licenses/LICENSE +198 -0
cloudshellgpt/config.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Configuration manager — handles user settings and defaults."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
from pydantic import Field, field_validator
|
|
11
|
+
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Constants
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
CONFIG_DIR = Path.home() / ".csgpt"
|
|
18
|
+
CONFIG_FILE = CONFIG_DIR / "config.yaml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Custom exceptions
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConfigError(Exception):
|
|
27
|
+
"""Raised when configuration loading, saving, or validation fails."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Enums
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OutputFormat(StrEnum):
|
|
36
|
+
"""Supported output formats."""
|
|
37
|
+
|
|
38
|
+
TABLE = "table"
|
|
39
|
+
JSON = "json"
|
|
40
|
+
YAML = "yaml"
|
|
41
|
+
CSV = "csv"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# YAML settings source
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class YamlSettingsSource(PydanticBaseSettingsSource):
|
|
50
|
+
"""Load settings from a YAML file on disk."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, settings_cls: type[BaseSettings], yaml_path: Path) -> None:
|
|
53
|
+
super().__init__(settings_cls)
|
|
54
|
+
self._yaml_path = yaml_path
|
|
55
|
+
|
|
56
|
+
def get_field_value(self, field: Any, field_name: str) -> tuple[Any, str, bool]:
|
|
57
|
+
"""Return the value for a given field from the YAML data."""
|
|
58
|
+
data = self._load_yaml()
|
|
59
|
+
value = data.get(field_name)
|
|
60
|
+
return value, field_name, False
|
|
61
|
+
|
|
62
|
+
def __call__(self) -> dict[str, Any]:
|
|
63
|
+
"""Return all values from the YAML file."""
|
|
64
|
+
return self._load_yaml()
|
|
65
|
+
|
|
66
|
+
def _load_yaml(self) -> dict[str, Any]:
|
|
67
|
+
"""Read and parse the YAML config file."""
|
|
68
|
+
if not self._yaml_path.exists():
|
|
69
|
+
return {}
|
|
70
|
+
try:
|
|
71
|
+
with self._yaml_path.open() as f:
|
|
72
|
+
data = yaml.safe_load(f)
|
|
73
|
+
return data if isinstance(data, dict) else {}
|
|
74
|
+
except (yaml.YAMLError, OSError) as exc:
|
|
75
|
+
raise ConfigError(f"Failed to read config file {self._yaml_path}: {exc}") from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Pydantic Settings model
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Config(BaseSettings):
|
|
84
|
+
"""User configuration for CloudShellGPT.
|
|
85
|
+
|
|
86
|
+
Settings are resolved in order of priority (highest first):
|
|
87
|
+
1. Environment variables (prefix CSGPT_)
|
|
88
|
+
2. YAML config file (~/.csgpt/config.yaml)
|
|
89
|
+
3. Field defaults
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
region: str = Field(default="us-east-1", description="AWS region for Bedrock calls")
|
|
93
|
+
language: str = Field(default="auto", description="UI language (auto, en, es, pt, etc.)")
|
|
94
|
+
default_output: str = Field(
|
|
95
|
+
default="table", description="Output format: table, json, yaml, csv"
|
|
96
|
+
)
|
|
97
|
+
bedrock_model: str = Field(
|
|
98
|
+
default="us.anthropic.claude-sonnet-4-6",
|
|
99
|
+
description="Bedrock model ID for translations",
|
|
100
|
+
)
|
|
101
|
+
require_confirmation_for: list[str] = Field(
|
|
102
|
+
default_factory=lambda: ["high", "critical"],
|
|
103
|
+
description="Risk levels that require user confirmation",
|
|
104
|
+
)
|
|
105
|
+
enable_pii_detection: bool = Field(
|
|
106
|
+
default=False, description="Enable PII detection via Comprehend"
|
|
107
|
+
)
|
|
108
|
+
enable_cost_preview: bool = Field(
|
|
109
|
+
default=True, description="Show cost estimate before execution"
|
|
110
|
+
)
|
|
111
|
+
enable_learning_mode: bool = Field(
|
|
112
|
+
default=True, description="Show educational tips after execution"
|
|
113
|
+
)
|
|
114
|
+
max_cost_alert: int = Field(default=100, description="USD threshold for cost alert warnings")
|
|
115
|
+
timeout: int = Field(default=30, description="Command execution timeout in seconds")
|
|
116
|
+
|
|
117
|
+
model_config = {
|
|
118
|
+
"env_prefix": "CSGPT_",
|
|
119
|
+
"env_file": ".env",
|
|
120
|
+
"env_file_encoding": "utf-8",
|
|
121
|
+
"extra": "ignore",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@field_validator("default_output")
|
|
125
|
+
@classmethod
|
|
126
|
+
def validate_output_format(cls, v: str) -> str:
|
|
127
|
+
"""Ensure default_output is one of the supported formats."""
|
|
128
|
+
allowed = {fmt.value for fmt in OutputFormat}
|
|
129
|
+
if v not in allowed:
|
|
130
|
+
msg = f"default_output must be one of {sorted(allowed)}, got '{v}'"
|
|
131
|
+
raise ValueError(msg)
|
|
132
|
+
return v
|
|
133
|
+
|
|
134
|
+
@field_validator("max_cost_alert")
|
|
135
|
+
@classmethod
|
|
136
|
+
def validate_max_cost_alert(cls, v: int) -> int:
|
|
137
|
+
"""Ensure max_cost_alert is a positive value."""
|
|
138
|
+
if v < 0:
|
|
139
|
+
msg = f"max_cost_alert must be >= 0, got {v}"
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
return v
|
|
142
|
+
|
|
143
|
+
@field_validator("timeout")
|
|
144
|
+
@classmethod
|
|
145
|
+
def validate_timeout(cls, v: int) -> int:
|
|
146
|
+
"""Ensure timeout is a positive value (> 0)."""
|
|
147
|
+
if v <= 0:
|
|
148
|
+
msg = f"timeout must be > 0, got {v}"
|
|
149
|
+
raise ValueError(msg)
|
|
150
|
+
return v
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def settings_customise_sources(
|
|
154
|
+
cls,
|
|
155
|
+
settings_cls: type[BaseSettings],
|
|
156
|
+
init_settings: PydanticBaseSettingsSource,
|
|
157
|
+
env_settings: PydanticBaseSettingsSource,
|
|
158
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
|
159
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
|
160
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
161
|
+
"""Customise settings sources to include YAML file.
|
|
162
|
+
|
|
163
|
+
Priority order (highest first):
|
|
164
|
+
1. init_settings (programmatic overrides)
|
|
165
|
+
2. env_settings (CSGPT_* env vars)
|
|
166
|
+
3. dotenv_settings (.env file)
|
|
167
|
+
4. yaml_settings (~/.csgpt/config.yaml)
|
|
168
|
+
5. file_secret_settings
|
|
169
|
+
"""
|
|
170
|
+
return (
|
|
171
|
+
init_settings,
|
|
172
|
+
env_settings,
|
|
173
|
+
dotenv_settings,
|
|
174
|
+
YamlSettingsSource(settings_cls, CONFIG_FILE),
|
|
175
|
+
file_secret_settings,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# ConfigManager
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ConfigManager:
|
|
185
|
+
"""Manages the user's CloudShellGPT configuration.
|
|
186
|
+
|
|
187
|
+
Provides load, save, get, set, and reload operations over the
|
|
188
|
+
Config model backed by ~/.csgpt/config.yaml.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
def __init__(self, config_path: Path | None = None) -> None:
|
|
192
|
+
"""Initialize the ConfigManager.
|
|
193
|
+
|
|
194
|
+
Creates the config file with defaults if it doesn't exist.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
config_path: Path to the YAML config file. Defaults to ~/.csgpt/config.yaml.
|
|
198
|
+
"""
|
|
199
|
+
self.config_path = config_path or CONFIG_FILE
|
|
200
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
file_existed = self.config_path.exists()
|
|
202
|
+
self._config = self._load()
|
|
203
|
+
if not file_existed:
|
|
204
|
+
self.save()
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def config(self) -> Config:
|
|
208
|
+
"""Return the current Config instance."""
|
|
209
|
+
return self._config
|
|
210
|
+
|
|
211
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
212
|
+
"""Get a config value by key.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
key: The configuration field name.
|
|
216
|
+
default: Fallback value if the key doesn't exist.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The configuration value or the default.
|
|
220
|
+
"""
|
|
221
|
+
return getattr(self._config, key, default)
|
|
222
|
+
|
|
223
|
+
def set(self, key: str, value: Any) -> None:
|
|
224
|
+
"""Set a config value.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
key: The configuration field name.
|
|
228
|
+
value: The new value to assign.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ConfigError: If the key is not a valid config field or validation fails.
|
|
232
|
+
"""
|
|
233
|
+
if key not in self._config.model_fields:
|
|
234
|
+
raise ConfigError(f"Unknown config key: '{key}'")
|
|
235
|
+
try:
|
|
236
|
+
data = self._config.model_dump()
|
|
237
|
+
data[key] = value
|
|
238
|
+
self._config = Config(**data)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
raise ConfigError(f"Invalid value for '{key}': {exc}") from exc
|
|
241
|
+
|
|
242
|
+
def save(self) -> None:
|
|
243
|
+
"""Save the current config to disk.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ConfigError: If writing to disk fails.
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
250
|
+
with self.config_path.open("w") as f:
|
|
251
|
+
yaml.dump(self._config.model_dump(), f, default_flow_style=False)
|
|
252
|
+
except OSError as exc:
|
|
253
|
+
raise ConfigError(f"Failed to save config to {self.config_path}: {exc}") from exc
|
|
254
|
+
|
|
255
|
+
def reset_defaults(self) -> None:
|
|
256
|
+
"""Reset configuration to defaults and save to disk.
|
|
257
|
+
|
|
258
|
+
Overwrites the current config file with all default values.
|
|
259
|
+
"""
|
|
260
|
+
self._config = Config()
|
|
261
|
+
self.save()
|
|
262
|
+
|
|
263
|
+
def reload(self) -> None:
|
|
264
|
+
"""Reload config from disk, picking up any external changes."""
|
|
265
|
+
self._config = self._load()
|
|
266
|
+
|
|
267
|
+
def to_yaml(self) -> str:
|
|
268
|
+
"""Return the config as a YAML string.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
YAML-formatted string of the current configuration.
|
|
272
|
+
"""
|
|
273
|
+
result: str = yaml.dump(self._config.model_dump(), default_flow_style=False)
|
|
274
|
+
return result
|
|
275
|
+
|
|
276
|
+
def _load(self) -> Config:
|
|
277
|
+
"""Load config from YAML file merged with env vars and defaults.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
A validated Config instance.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
ConfigError: If the config file is malformed.
|
|
284
|
+
"""
|
|
285
|
+
if not self.config_path.exists():
|
|
286
|
+
return Config()
|
|
287
|
+
try:
|
|
288
|
+
with self.config_path.open() as f:
|
|
289
|
+
data = yaml.safe_load(f) or {}
|
|
290
|
+
if not isinstance(data, dict):
|
|
291
|
+
raise ConfigError(f"Config file {self.config_path} must contain a YAML mapping")
|
|
292
|
+
return Config(**data)
|
|
293
|
+
except ConfigError:
|
|
294
|
+
raise
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
raise ConfigError(f"Failed to load config from {self.config_path}: {exc}") from exc
|