comit-cli 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.
- comit/__init__.py +1 -0
- comit/ai/__init__.py +39 -0
- comit/ai/base.py +42 -0
- comit/ai/factory.py +59 -0
- comit/ai/gemini_provider.py +125 -0
- comit/ai/groq_provider.py +121 -0
- comit/ai/ollama_provider.py +107 -0
- comit/ai/openai_provider.py +107 -0
- comit/cli.py +446 -0
- comit/commit/__init__.py +21 -0
- comit/commit/context.py +60 -0
- comit/commit/validator.py +270 -0
- comit/config.py +279 -0
- comit/git.py +281 -0
- comit/prompts.py +148 -0
- comit/review/__init__.py +29 -0
- comit/review/base.py +13 -0
- comit/review/databases.py +40 -0
- comit/review/engine.py +92 -0
- comit/review/generated_files.py +75 -0
- comit/review/git_files.py +54 -0
- comit/review/large_files.py +38 -0
- comit/review/models.py +59 -0
- comit/review/private_keys.py +69 -0
- comit/review/secrets.py +230 -0
- comit/review/sensitive_files.py +79 -0
- comit/ui.py +412 -0
- comit_cli-1.0.0.dist-info/METADATA +524 -0
- comit_cli-1.0.0.dist-info/RECORD +32 -0
- comit_cli-1.0.0.dist-info/WHEEL +5 -0
- comit_cli-1.0.0.dist-info/entry_points.txt +3 -0
- comit_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional, Set
|
|
8
|
+
|
|
9
|
+
from comit.commit.context import CommitContext, ChangedFile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ValidationSeverity(str, Enum):
|
|
13
|
+
INFO = "INFO"
|
|
14
|
+
WARNING = "WARNING"
|
|
15
|
+
ERROR = "ERROR"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ValidationFinding:
|
|
20
|
+
severity: ValidationSeverity
|
|
21
|
+
category: str
|
|
22
|
+
message: str
|
|
23
|
+
details: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ValidationResult:
|
|
28
|
+
message: str
|
|
29
|
+
is_valid: bool = True
|
|
30
|
+
findings: List[ValidationFinding] = field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def has_findings(self) -> bool:
|
|
34
|
+
return bool(self.findings)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def has_warnings(self) -> bool:
|
|
38
|
+
return any(f.severity in (ValidationSeverity.WARNING, ValidationSeverity.ERROR) for f in self.findings)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def has_errors(self) -> bool:
|
|
42
|
+
return any(f.severity == ValidationSeverity.ERROR for f in self.findings)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
DEFAULT_CONVENTIONAL_TYPES = {
|
|
46
|
+
"feat",
|
|
47
|
+
"fix",
|
|
48
|
+
"docs",
|
|
49
|
+
"style",
|
|
50
|
+
"refactor",
|
|
51
|
+
"perf",
|
|
52
|
+
"test",
|
|
53
|
+
"build",
|
|
54
|
+
"ci",
|
|
55
|
+
"chore",
|
|
56
|
+
"revert",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
DOC_EXTENSIONS = {
|
|
60
|
+
".md",
|
|
61
|
+
".markdown",
|
|
62
|
+
".mdown",
|
|
63
|
+
".rst",
|
|
64
|
+
".txt",
|
|
65
|
+
".adoc",
|
|
66
|
+
".asciidoc",
|
|
67
|
+
".doc",
|
|
68
|
+
".docx",
|
|
69
|
+
".pdf",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
DOC_FILENAMES = {
|
|
73
|
+
"readme",
|
|
74
|
+
"license",
|
|
75
|
+
"contributing",
|
|
76
|
+
"changelog",
|
|
77
|
+
"authors",
|
|
78
|
+
"code_of_conduct",
|
|
79
|
+
"notice",
|
|
80
|
+
"todo",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
TEST_EXTENSIONS = {
|
|
84
|
+
".spec.js",
|
|
85
|
+
".spec.ts",
|
|
86
|
+
".spec.jsx",
|
|
87
|
+
".spec.tsx",
|
|
88
|
+
".test.js",
|
|
89
|
+
".test.ts",
|
|
90
|
+
".test.jsx",
|
|
91
|
+
".test.tsx",
|
|
92
|
+
".test.py",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
CONVENTIONAL_REGEX = re.compile(
|
|
96
|
+
r"^(?P<type>[a-zA-Z0-9_-]+)(?:\((?P<scope>[^()\r\n]+)\))?(?P<breaking>!)?:\s*(?P<subject>.*)$"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
FILE_PATH_PATTERN = re.compile(
|
|
100
|
+
r"(?:\b|['\"`])(?P<path>(?:[a-zA-Z0-9_\-\.]+[/\\])+[a-zA-Z0-9_\-\.]+|[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+)(?:\b|['\"`])"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_doc_file(path_str: str) -> bool:
|
|
105
|
+
norm_path = path_str.replace("\\", "/").lower()
|
|
106
|
+
path_obj = Path(norm_path)
|
|
107
|
+
if any(part in ("docs", "doc", "documentation") for part in path_obj.parts):
|
|
108
|
+
return True
|
|
109
|
+
if path_obj.suffix.lower() in DOC_EXTENSIONS:
|
|
110
|
+
return True
|
|
111
|
+
if path_obj.stem.lower() in DOC_FILENAMES:
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _is_test_file(path_str: str) -> bool:
|
|
117
|
+
norm_path = path_str.replace("\\", "/").lower()
|
|
118
|
+
path_obj = Path(norm_path)
|
|
119
|
+
if any(part in ("tests", "test", "testing", "__tests__", "spec", "specs") for part in path_obj.parts):
|
|
120
|
+
return True
|
|
121
|
+
stem = path_obj.stem.lower()
|
|
122
|
+
name = path_obj.name.lower()
|
|
123
|
+
if stem.startswith("test_") or stem.endswith("_test") or stem.endswith("_spec"):
|
|
124
|
+
return True
|
|
125
|
+
if any(name.endswith(ext) for ext in TEST_EXTENSIONS):
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_referenced_files(text: str) -> Set[str]:
|
|
131
|
+
referenced = set()
|
|
132
|
+
for match in FILE_PATH_PATTERN.finditer(text):
|
|
133
|
+
candidate = match.group("path").strip("'\"`")
|
|
134
|
+
if candidate.endswith((".", ",")):
|
|
135
|
+
candidate = candidate[:-1]
|
|
136
|
+
if not candidate:
|
|
137
|
+
continue
|
|
138
|
+
p = Path(candidate)
|
|
139
|
+
suffix = p.suffix.lower()
|
|
140
|
+
if suffix in (
|
|
141
|
+
".py", ".js", ".ts", ".jsx", ".tsx", ".json", ".toml", ".yaml", ".yml",
|
|
142
|
+
".md", ".rst", ".txt", ".html", ".css", ".rs", ".go", ".c", ".cpp",
|
|
143
|
+
".h", ".java", ".kt", ".rb", ".php", ".sh", ".bat", ".ps1", ".sql",
|
|
144
|
+
".env", ".lock", ".cfg", ".ini",
|
|
145
|
+
) or "/" in candidate or "\\" in candidate:
|
|
146
|
+
referenced.add(candidate.replace("\\", "/").lower())
|
|
147
|
+
return referenced
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class CommitValidator:
|
|
151
|
+
def __init__(self, allowed_types: Optional[Set[str]] = None):
|
|
152
|
+
self.allowed_types = set(allowed_types) if allowed_types else DEFAULT_CONVENTIONAL_TYPES
|
|
153
|
+
|
|
154
|
+
def validate(self, message: str, context: Optional[CommitContext] = None) -> ValidationResult:
|
|
155
|
+
result = ValidationResult(message=message)
|
|
156
|
+
|
|
157
|
+
if not message or not message.strip():
|
|
158
|
+
result.is_valid = False
|
|
159
|
+
result.findings.append(
|
|
160
|
+
ValidationFinding(
|
|
161
|
+
severity=ValidationSeverity.ERROR,
|
|
162
|
+
category="syntax",
|
|
163
|
+
message="Commit message is empty.",
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
return result
|
|
167
|
+
|
|
168
|
+
lines = message.strip().splitlines()
|
|
169
|
+
first_line = lines[0].strip()
|
|
170
|
+
|
|
171
|
+
match = CONVENTIONAL_REGEX.match(first_line)
|
|
172
|
+
msg_type: Optional[str] = None
|
|
173
|
+
msg_scope: Optional[str] = None
|
|
174
|
+
msg_subject: Optional[str] = None
|
|
175
|
+
|
|
176
|
+
if match:
|
|
177
|
+
msg_type = match.group("type").lower()
|
|
178
|
+
msg_scope = match.group("scope")
|
|
179
|
+
msg_subject = match.group("subject").strip()
|
|
180
|
+
|
|
181
|
+
if not msg_subject:
|
|
182
|
+
result.is_valid = False
|
|
183
|
+
result.findings.append(
|
|
184
|
+
ValidationFinding(
|
|
185
|
+
severity=ValidationSeverity.ERROR,
|
|
186
|
+
category="syntax",
|
|
187
|
+
message=f"Commit message type '{msg_type}' is missing a subject description.",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
result.findings.append(
|
|
192
|
+
ValidationFinding(
|
|
193
|
+
severity=ValidationSeverity.INFO,
|
|
194
|
+
category="syntax",
|
|
195
|
+
message="Commit message does not follow Conventional Commit format (type: description).",
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if context and context.changed_files:
|
|
200
|
+
staged_paths = [f.path.replace("\\", "/").lower() for f in context.changed_files]
|
|
201
|
+
staged_basenames = [Path(p).name.lower() for p in staged_paths]
|
|
202
|
+
|
|
203
|
+
is_all_docs = all(_is_doc_file(p) for p in staged_paths)
|
|
204
|
+
is_all_tests = all(_is_test_file(p) for p in staged_paths)
|
|
205
|
+
has_any_docs = any(_is_doc_file(p) for p in staged_paths)
|
|
206
|
+
has_any_tests = any(_is_test_file(p) for p in staged_paths)
|
|
207
|
+
|
|
208
|
+
if msg_type:
|
|
209
|
+
if is_all_docs and msg_type in ("feat", "fix", "perf", "refactor"):
|
|
210
|
+
result.findings.append(
|
|
211
|
+
ValidationFinding(
|
|
212
|
+
severity=ValidationSeverity.WARNING,
|
|
213
|
+
category="context_mismatch",
|
|
214
|
+
message=f"Commit message type '{msg_type}' does not match documentation-only staged changes.",
|
|
215
|
+
details=f"All {len(staged_paths)} staged file(s) are documentation files.",
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
elif msg_type == "docs" and not has_any_docs:
|
|
219
|
+
result.findings.append(
|
|
220
|
+
ValidationFinding(
|
|
221
|
+
severity=ValidationSeverity.WARNING,
|
|
222
|
+
category="context_mismatch",
|
|
223
|
+
message="Commit message type is 'docs' but no documentation files are staged.",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if is_all_tests and msg_type in ("feat", "fix", "perf"):
|
|
228
|
+
result.findings.append(
|
|
229
|
+
ValidationFinding(
|
|
230
|
+
severity=ValidationSeverity.WARNING,
|
|
231
|
+
category="context_mismatch",
|
|
232
|
+
message=f"Commit message type '{msg_type}' does not match test-only staged changes.",
|
|
233
|
+
details=f"All {len(staged_paths)} staged file(s) are test files.",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
elif msg_type == "test" and not has_any_tests:
|
|
237
|
+
result.findings.append(
|
|
238
|
+
ValidationFinding(
|
|
239
|
+
severity=ValidationSeverity.WARNING,
|
|
240
|
+
category="context_mismatch",
|
|
241
|
+
message="Commit message type is 'test' but no test files are staged.",
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
referenced_files = _extract_referenced_files(message)
|
|
246
|
+
for ref in referenced_files:
|
|
247
|
+
ref_basename = Path(ref).name.lower()
|
|
248
|
+
matches_staged = any(
|
|
249
|
+
ref == staged_p or staged_p.endswith(f"/{ref}") or ref_basename == base
|
|
250
|
+
for staged_p, base in zip(staged_paths, staged_basenames)
|
|
251
|
+
)
|
|
252
|
+
if not matches_staged:
|
|
253
|
+
result.findings.append(
|
|
254
|
+
ValidationFinding(
|
|
255
|
+
severity=ValidationSeverity.WARNING,
|
|
256
|
+
category="file_mismatch",
|
|
257
|
+
message=f"Commit message references '{ref}' which is not in staged changes.",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return result
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def validate_commit_message(
|
|
265
|
+
message: str,
|
|
266
|
+
context: Optional[CommitContext] = None,
|
|
267
|
+
allowed_types: Optional[Set[str]] = None,
|
|
268
|
+
) -> ValidationResult:
|
|
269
|
+
validator = CommitValidator(allowed_types=allowed_types)
|
|
270
|
+
return validator.validate(message=message, context=context)
|
comit/config.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
from dotenv import load_dotenv, find_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv(find_dotenv(usecwd=True))
|
|
11
|
+
|
|
12
|
+
DEFAULT_PROVIDER = "groq"
|
|
13
|
+
DEFAULT_GROQ_MODEL = "qwen/qwen3.8-27b"
|
|
14
|
+
DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"
|
|
15
|
+
DEFAULT_OPENAI_MODEL = "gpt-4o-mini"
|
|
16
|
+
DEFAULT_OLLAMA_MODEL = "llama3.2"
|
|
17
|
+
DEFAULT_OLLAMA_HOST = "http://localhost:11434"
|
|
18
|
+
|
|
19
|
+
DEFAULT_MODELS = {
|
|
20
|
+
"groq": DEFAULT_GROQ_MODEL,
|
|
21
|
+
"gemini": DEFAULT_GEMINI_MODEL,
|
|
22
|
+
"openai": DEFAULT_OPENAI_MODEL,
|
|
23
|
+
"ollama": DEFAULT_OLLAMA_MODEL,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
SUPPORTED_PROVIDERS = ("groq", "gemini", "openai", "ollama")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_config_dir() -> Path:
|
|
30
|
+
if sys.platform == "win32":
|
|
31
|
+
appdata = os.getenv("APPDATA")
|
|
32
|
+
if appdata:
|
|
33
|
+
return Path(appdata) / "comit"
|
|
34
|
+
xdg = os.getenv("XDG_CONFIG_HOME")
|
|
35
|
+
if xdg:
|
|
36
|
+
return Path(xdg) / "comit"
|
|
37
|
+
return Path.home() / ".config" / "comit"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_config_path() -> Path:
|
|
41
|
+
return get_config_dir() / "config.json"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_user_config() -> Dict[str, Any]:
|
|
45
|
+
config_file = get_config_path()
|
|
46
|
+
if not config_file.is_file():
|
|
47
|
+
return {}
|
|
48
|
+
try:
|
|
49
|
+
with open(config_file, "r", encoding="utf-8") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
except Exception:
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_user_config(data: Dict[str, Any]) -> None:
|
|
56
|
+
config_dir = get_config_dir()
|
|
57
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
config_file = get_config_path()
|
|
59
|
+
with open(config_file, "w", encoding="utf-8") as f:
|
|
60
|
+
json.dump(data, f, indent=2)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def set_user_config_value(key: str, value: Any) -> None:
|
|
64
|
+
data = load_user_config()
|
|
65
|
+
data[key] = value
|
|
66
|
+
save_user_config(data)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def reset_user_config() -> None:
|
|
70
|
+
config_file = get_config_path()
|
|
71
|
+
if config_file.is_file():
|
|
72
|
+
config_file.unlink()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def normalize_api_key(key: Optional[str]) -> Optional[str]:
|
|
76
|
+
if not key:
|
|
77
|
+
return None
|
|
78
|
+
val = str(key).strip()
|
|
79
|
+
if not val:
|
|
80
|
+
return None
|
|
81
|
+
while (len(val) >= 2) and (
|
|
82
|
+
(val.startswith('"') and val.endswith('"')) or
|
|
83
|
+
(val.startswith("'") and val.endswith("'"))
|
|
84
|
+
):
|
|
85
|
+
val = val[1:-1].strip()
|
|
86
|
+
return val if val else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_provider(override_provider: Optional[str] = None) -> str:
|
|
90
|
+
if override_provider and override_provider.strip():
|
|
91
|
+
return override_provider.strip().lower()
|
|
92
|
+
|
|
93
|
+
for var in ("COMIT_PROVIDER", "comit_provider"):
|
|
94
|
+
val = os.getenv(var)
|
|
95
|
+
if val and val.strip():
|
|
96
|
+
return val.strip().lower()
|
|
97
|
+
|
|
98
|
+
user_cfg = load_user_config()
|
|
99
|
+
cfg_val = user_cfg.get("provider")
|
|
100
|
+
if cfg_val and str(cfg_val).strip():
|
|
101
|
+
return str(cfg_val).strip().lower()
|
|
102
|
+
|
|
103
|
+
return DEFAULT_PROVIDER
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_api_key(
|
|
107
|
+
override_key: Optional[str] = None,
|
|
108
|
+
provider: Optional[str] = None,
|
|
109
|
+
) -> Optional[str]:
|
|
110
|
+
p = (provider or get_provider()).strip().lower()
|
|
111
|
+
|
|
112
|
+
if override_key:
|
|
113
|
+
normalized = normalize_api_key(override_key)
|
|
114
|
+
if normalized:
|
|
115
|
+
return normalized
|
|
116
|
+
|
|
117
|
+
if p == "ollama":
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
env_vars_map = {
|
|
121
|
+
"groq": ("GROQ_API_KEY", "groq_api_key", "Groq_Api_Key", "GROQ_KEY"),
|
|
122
|
+
"gemini": ("GEMINI_API_KEY", "gemini_api_key", "Gemini_Api_Key"),
|
|
123
|
+
"openai": ("OPENAI_API_KEY", "openai_api_key", "Openai_Api_Key"),
|
|
124
|
+
}
|
|
125
|
+
vars_to_check = env_vars_map.get(p, (f"{p.upper()}_API_KEY",))
|
|
126
|
+
|
|
127
|
+
for var in vars_to_check:
|
|
128
|
+
val = os.getenv(var)
|
|
129
|
+
if val:
|
|
130
|
+
normalized = normalize_api_key(val)
|
|
131
|
+
if normalized:
|
|
132
|
+
return normalized
|
|
133
|
+
|
|
134
|
+
for k, v in os.environ.items():
|
|
135
|
+
if k.strip().lower() in [var.lower() for var in vars_to_check] and v:
|
|
136
|
+
normalized = normalize_api_key(v)
|
|
137
|
+
if normalized:
|
|
138
|
+
return normalized
|
|
139
|
+
|
|
140
|
+
user_cfg = load_user_config()
|
|
141
|
+
cfg_keys = [f"{p}_api_key", f"{p}_key"]
|
|
142
|
+
if p == "groq":
|
|
143
|
+
cfg_keys.extend(["groq_api_key", "api_key"])
|
|
144
|
+
for key in cfg_keys:
|
|
145
|
+
cfg_val = user_cfg.get(key)
|
|
146
|
+
if cfg_val:
|
|
147
|
+
normalized = normalize_api_key(cfg_val)
|
|
148
|
+
if normalized:
|
|
149
|
+
return normalized
|
|
150
|
+
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_model(
|
|
155
|
+
override_model: Optional[str] = None,
|
|
156
|
+
provider: Optional[str] = None,
|
|
157
|
+
) -> str:
|
|
158
|
+
p = (provider or get_provider()).strip().lower()
|
|
159
|
+
|
|
160
|
+
if override_model and override_model.strip():
|
|
161
|
+
return override_model.strip()
|
|
162
|
+
|
|
163
|
+
for var in ("COMIT_MODEL", "comit_model"):
|
|
164
|
+
val = os.getenv(var)
|
|
165
|
+
if val and val.strip():
|
|
166
|
+
return val.strip()
|
|
167
|
+
|
|
168
|
+
env_vars_map = {
|
|
169
|
+
"groq": ("GROQ_MODEL", "groq_model", "Groq_Model"),
|
|
170
|
+
"gemini": ("GEMINI_MODEL", "gemini_model", "Gemini_Model"),
|
|
171
|
+
"openai": ("OPENAI_MODEL", "openai_model", "Openai_Model"),
|
|
172
|
+
"ollama": ("OLLAMA_MODEL", "ollama_model", "Ollama_Model"),
|
|
173
|
+
}
|
|
174
|
+
vars_to_check = env_vars_map.get(p, (f"{p.upper()}_MODEL",))
|
|
175
|
+
|
|
176
|
+
for var in vars_to_check:
|
|
177
|
+
val = os.getenv(var)
|
|
178
|
+
if val and val.strip():
|
|
179
|
+
return val.strip()
|
|
180
|
+
|
|
181
|
+
user_cfg = load_user_config()
|
|
182
|
+
cfg_val = (
|
|
183
|
+
user_cfg.get(f"{p}_model")
|
|
184
|
+
or (user_cfg.get("model") if user_cfg.get("provider") == p or not user_cfg.get("provider") else None)
|
|
185
|
+
or (user_cfg.get("groq_model") if p == "groq" else None)
|
|
186
|
+
)
|
|
187
|
+
if cfg_val and str(cfg_val).strip():
|
|
188
|
+
return str(cfg_val).strip()
|
|
189
|
+
|
|
190
|
+
return DEFAULT_MODELS.get(p, DEFAULT_GROQ_MODEL)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_ollama_host(override_host: Optional[str] = None) -> str:
|
|
194
|
+
if override_host and override_host.strip():
|
|
195
|
+
return override_host.strip()
|
|
196
|
+
|
|
197
|
+
for var in ("OLLAMA_HOST", "ollama_host"):
|
|
198
|
+
val = os.getenv(var)
|
|
199
|
+
if val and val.strip():
|
|
200
|
+
return val.strip()
|
|
201
|
+
|
|
202
|
+
user_cfg = load_user_config()
|
|
203
|
+
cfg_val = user_cfg.get("ollama_host")
|
|
204
|
+
if cfg_val and str(cfg_val).strip():
|
|
205
|
+
return str(cfg_val).strip()
|
|
206
|
+
|
|
207
|
+
return DEFAULT_OLLAMA_HOST
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def mask_api_key(key: Optional[str]) -> str:
|
|
211
|
+
clean = normalize_api_key(key)
|
|
212
|
+
if not clean:
|
|
213
|
+
return "Not configured"
|
|
214
|
+
if len(clean) <= 8:
|
|
215
|
+
return "*" * len(clean)
|
|
216
|
+
return f"{clean[:4]}{'*' * 12}{clean[-4:]}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def get_config_summary(provider: Optional[str] = None) -> Dict[str, Any]:
|
|
220
|
+
p = get_provider(provider)
|
|
221
|
+
model = get_model(provider=p)
|
|
222
|
+
api_key = get_api_key(provider=p)
|
|
223
|
+
ollama_host = get_ollama_host()
|
|
224
|
+
config_path = str(get_config_path())
|
|
225
|
+
user_cfg = load_user_config()
|
|
226
|
+
|
|
227
|
+
key_source = "not set"
|
|
228
|
+
if p != "ollama":
|
|
229
|
+
env_vars = {
|
|
230
|
+
"groq": ("GROQ_API_KEY", "groq_api_key", "Groq_Api_Key", "GROQ_KEY"),
|
|
231
|
+
"gemini": ("GEMINI_API_KEY", "gemini_api_key", "Gemini_Api_Key"),
|
|
232
|
+
"openai": ("OPENAI_API_KEY", "openai_api_key", "Openai_Api_Key"),
|
|
233
|
+
}.get(p, (f"{p.upper()}_API_KEY",))
|
|
234
|
+
for var in env_vars:
|
|
235
|
+
if os.getenv(var):
|
|
236
|
+
key_source = f"environment ({var})"
|
|
237
|
+
break
|
|
238
|
+
if key_source == "not set" and (user_cfg.get(f"{p}_api_key") or (p == "groq" and user_cfg.get("groq_api_key"))):
|
|
239
|
+
key_source = "user configuration"
|
|
240
|
+
|
|
241
|
+
model_source = "default"
|
|
242
|
+
for var in ("COMIT_MODEL", "comit_model"):
|
|
243
|
+
if os.getenv(var):
|
|
244
|
+
model_source = f"environment ({var})"
|
|
245
|
+
break
|
|
246
|
+
if model_source == "default":
|
|
247
|
+
provider_model_vars = {
|
|
248
|
+
"groq": ("GROQ_MODEL", "groq_model"),
|
|
249
|
+
"gemini": ("GEMINI_MODEL", "gemini_model"),
|
|
250
|
+
"openai": ("OPENAI_MODEL", "openai_model"),
|
|
251
|
+
"ollama": ("OLLAMA_MODEL", "ollama_model"),
|
|
252
|
+
}.get(p, ())
|
|
253
|
+
for var in provider_model_vars:
|
|
254
|
+
if os.getenv(var):
|
|
255
|
+
model_source = f"environment ({var})"
|
|
256
|
+
break
|
|
257
|
+
if model_source == "default" and (user_cfg.get(f"{p}_model") or user_cfg.get("model") or (p == "groq" and user_cfg.get("groq_model"))):
|
|
258
|
+
model_source = "user configuration"
|
|
259
|
+
|
|
260
|
+
provider_source = "default"
|
|
261
|
+
for var in ("COMIT_PROVIDER", "comit_provider"):
|
|
262
|
+
if os.getenv(var):
|
|
263
|
+
provider_source = f"environment ({var})"
|
|
264
|
+
break
|
|
265
|
+
if provider_source == "default" and user_cfg.get("provider"):
|
|
266
|
+
provider_source = "user configuration"
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
"provider": p,
|
|
270
|
+
"provider_source": provider_source,
|
|
271
|
+
"model": model,
|
|
272
|
+
"model_source": model_source,
|
|
273
|
+
"api_key_masked": mask_api_key(api_key) if p != "ollama" else "N/A (local)",
|
|
274
|
+
"api_key_source": key_source if p != "ollama" else "N/A",
|
|
275
|
+
"api_key_set": bool(api_key) if p != "ollama" else True,
|
|
276
|
+
"ollama_host": ollama_host,
|
|
277
|
+
"config_file": config_path,
|
|
278
|
+
"config_exists": get_config_path().is_file(),
|
|
279
|
+
}
|