project-brain-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.
- project_brain/__init__.py +1 -0
- project_brain/cli.py +644 -0
- project_brain/cli_help.py +52 -0
- project_brain/cli_ui.py +69 -0
- project_brain/config/__init__.py +0 -0
- project_brain/core/__init__.py +0 -0
- project_brain/core/analyzer.py +134 -0
- project_brain/core/config_loader.py +193 -0
- project_brain/core/differ.py +160 -0
- project_brain/core/doctor.py +29 -0
- project_brain/core/doctor_checks/analysis.py +110 -0
- project_brain/core/doctor_checks/environment.py +92 -0
- project_brain/core/doctor_checks/exports.py +70 -0
- project_brain/core/doctor_checks/llm.py +107 -0
- project_brain/core/doctor_checks/models.py +10 -0
- project_brain/core/doctor_checks/repository.py +102 -0
- project_brain/core/explainer.py +334 -0
- project_brain/core/explainer_file.py +136 -0
- project_brain/core/exporter.py +340 -0
- project_brain/core/logger.py +31 -0
- project_brain/core/results.py +163 -0
- project_brain/core/summarizer.py +108 -0
- project_brain/llm/__init__.py +0 -0
- project_brain/llm/provider.py +211 -0
- project_brain/storage/__init__.py +0 -0
- project_brain/storage/storage.py +12 -0
- project_brain_cli-1.0.0.dist-info/METADATA +1185 -0
- project_brain_cli-1.0.0.dist-info/RECORD +32 -0
- project_brain_cli-1.0.0.dist-info/WHEEL +5 -0
- project_brain_cli-1.0.0.dist-info/entry_points.txt +3 -0
- project_brain_cli-1.0.0.dist-info/licenses/LICENSE +11 -0
- project_brain_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from project_brain.core.logger import log_error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_data(root: Path):
|
|
9
|
+
data_path = root / ".brain" / "data.json"
|
|
10
|
+
if not data_path.exists():
|
|
11
|
+
return None
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
with data_path.open("r", encoding="utf-8") as f:
|
|
15
|
+
return json.load(f)
|
|
16
|
+
except Exception as e:
|
|
17
|
+
log_error(f"Function failed: {str(e)}")
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compute_basic_stats(data: dict):
|
|
22
|
+
total_files = data.get("project", {}).get("total_files", 0)
|
|
23
|
+
total_functions = len(data.get("functions", []))
|
|
24
|
+
total_classes = len(data.get("classes", []))
|
|
25
|
+
|
|
26
|
+
return total_files, total_functions, total_classes
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_top_files(functions: list, limit: int = 5):
|
|
30
|
+
counter = Counter()
|
|
31
|
+
|
|
32
|
+
for fn in functions:
|
|
33
|
+
file = fn.get("file")
|
|
34
|
+
if file:
|
|
35
|
+
counter[file] += 1
|
|
36
|
+
|
|
37
|
+
return counter.most_common(limit)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_key_classes(classes: list, limit: int = 5):
|
|
41
|
+
result = []
|
|
42
|
+
for cls in classes[:limit]:
|
|
43
|
+
result.append((cls.get("name"), cls.get("file")))
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_overview(files: list):
|
|
48
|
+
names = " ".join([f.get("path", "").lower() for f in files])
|
|
49
|
+
|
|
50
|
+
parts = []
|
|
51
|
+
|
|
52
|
+
if "auth" in names:
|
|
53
|
+
parts.append("authentication")
|
|
54
|
+
if "db" in names or "database" in names:
|
|
55
|
+
parts.append("database layer")
|
|
56
|
+
if "api" in names or "routes" in names:
|
|
57
|
+
parts.append("API/backend")
|
|
58
|
+
|
|
59
|
+
if "cli" in names:
|
|
60
|
+
parts.append("CLI tool")
|
|
61
|
+
|
|
62
|
+
if not parts:
|
|
63
|
+
return "General purpose codebase with modular structure."
|
|
64
|
+
|
|
65
|
+
if len(parts) == 1:
|
|
66
|
+
return f"Project includes {parts[0]} functionality."
|
|
67
|
+
|
|
68
|
+
return f"Project includes {', '.join(parts[:-1])} and {parts[-1]}."
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_summary(root: Path, data: dict):
|
|
72
|
+
total_files, total_functions, total_classes = compute_basic_stats(data)
|
|
73
|
+
|
|
74
|
+
top_files = get_top_files(data.get("functions", []))
|
|
75
|
+
key_classes = get_key_classes(data.get("classes", []))
|
|
76
|
+
overview = generate_overview(data.get("files", []))
|
|
77
|
+
|
|
78
|
+
lines = []
|
|
79
|
+
|
|
80
|
+
lines.append(f"Project: {root}")
|
|
81
|
+
lines.append("")
|
|
82
|
+
lines.append(f"Files: {total_files}")
|
|
83
|
+
lines.append(f"Functions: {total_functions}")
|
|
84
|
+
lines.append(f"Classes: {total_classes}")
|
|
85
|
+
lines.append("")
|
|
86
|
+
lines.append("Top Files:")
|
|
87
|
+
lines.append("")
|
|
88
|
+
|
|
89
|
+
if top_files:
|
|
90
|
+
for file, count in top_files:
|
|
91
|
+
lines.append(f"* {file} ({count} functions)")
|
|
92
|
+
else:
|
|
93
|
+
lines.append("* None")
|
|
94
|
+
|
|
95
|
+
lines.append("")
|
|
96
|
+
lines.append("Key Classes:")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
if key_classes:
|
|
100
|
+
for name, file in key_classes:
|
|
101
|
+
lines.append(f"* {name} ({file})")
|
|
102
|
+
else:
|
|
103
|
+
lines.append("* None")
|
|
104
|
+
|
|
105
|
+
lines.append("")
|
|
106
|
+
lines.append(f"Overview: {overview}")
|
|
107
|
+
|
|
108
|
+
return "\n".join(lines)
|
|
File without changes
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def call_ollama(model, prompt, include_models=False, timeout=60):
|
|
7
|
+
try:
|
|
8
|
+
proc = subprocess.run(
|
|
9
|
+
["ollama", "run", model],
|
|
10
|
+
input=prompt,
|
|
11
|
+
text=True,
|
|
12
|
+
capture_output=True,
|
|
13
|
+
timeout = int(timeout) if timeout else 60
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if proc.returncode != 0:
|
|
17
|
+
return _response(error=proc.stderr, status=500)
|
|
18
|
+
|
|
19
|
+
models = []
|
|
20
|
+
if include_models:
|
|
21
|
+
m = subprocess.run(["ollama", "list"], capture_output=True, text=True)
|
|
22
|
+
if m.returncode == 0:
|
|
23
|
+
models = m.stdout.splitlines()
|
|
24
|
+
|
|
25
|
+
return _response(proc.stdout.strip(), models, 200)
|
|
26
|
+
|
|
27
|
+
except subprocess.TimeoutExpired:
|
|
28
|
+
return _response(error="Ollama timeout", status=408)
|
|
29
|
+
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return _response(error=str(e), status=500)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def call_openai(model, prompt, api_key, include_models=False, timeout=60):
|
|
35
|
+
if not api_key:
|
|
36
|
+
return _response(error="Missing API key", status=401)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
res = requests.post(
|
|
40
|
+
"https://api.openai.com/v1/responses",
|
|
41
|
+
headers={
|
|
42
|
+
"Authorization": f"Bearer {api_key}",
|
|
43
|
+
"Content-Type": "application/json"
|
|
44
|
+
},
|
|
45
|
+
json={"model": model, "input": prompt},
|
|
46
|
+
timeout = int(timeout) if timeout else 60
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if res.status_code != 200:
|
|
50
|
+
try:
|
|
51
|
+
err = res.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
err = res.text
|
|
54
|
+
return _response(
|
|
55
|
+
error=str(err),
|
|
56
|
+
status=res.status_code
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
data = res.json()
|
|
60
|
+
output = extract_openai_output(data)
|
|
61
|
+
if not output:
|
|
62
|
+
return _response(
|
|
63
|
+
error="Empty response from OpenAI",
|
|
64
|
+
status=502
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
models = []
|
|
68
|
+
if include_models:
|
|
69
|
+
m = requests.get(
|
|
70
|
+
"https://api.openai.com/v1/models",
|
|
71
|
+
headers={"Authorization": f"Bearer {api_key}"}
|
|
72
|
+
)
|
|
73
|
+
if m.status_code == 200:
|
|
74
|
+
models = [x.get("id") for x in m.json().get("data", [])]
|
|
75
|
+
|
|
76
|
+
return _response(output, models, res.status_code)
|
|
77
|
+
|
|
78
|
+
except requests.Timeout:
|
|
79
|
+
return _response(error="Request timeout", status=408)
|
|
80
|
+
|
|
81
|
+
except requests.ConnectionError:
|
|
82
|
+
return _response(error="Connection failed", status=503)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return _response(error=str(e), status=500)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def call_huggingface(model, prompt, api_key, include_models=False, timeout=60):
|
|
89
|
+
url = f"https://api-inference.huggingface.co/v1/models/{model}"
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
res = requests.post(
|
|
93
|
+
url,
|
|
94
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
95
|
+
json={"inputs": prompt},
|
|
96
|
+
timeout = int(timeout) if timeout else 60
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if res.status_code != 200:
|
|
100
|
+
return _response(error=res.text, status=res.status_code)
|
|
101
|
+
|
|
102
|
+
data = res.json()
|
|
103
|
+
output = data[0].get("generated_text", "") if isinstance(data, list) else str(data)
|
|
104
|
+
|
|
105
|
+
models = []
|
|
106
|
+
if include_models:
|
|
107
|
+
m = requests.get("https://huggingface.co/api/models", timeout=timeout)
|
|
108
|
+
if m.status_code == 200:
|
|
109
|
+
models = [x.get("id") for x in m.json()[:10]]
|
|
110
|
+
|
|
111
|
+
return _response(output, models, res.status_code)
|
|
112
|
+
|
|
113
|
+
except requests.Timeout:
|
|
114
|
+
return _response(error="Request timeout", status=408)
|
|
115
|
+
|
|
116
|
+
except requests.ConnectionError:
|
|
117
|
+
return _response(error="Connection failed", status=503)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return _response(error=str(e), status=500)
|
|
121
|
+
|
|
122
|
+
def call_gemini(model, prompt, api_key, include_models=False, timeout=60):
|
|
123
|
+
if not api_key:
|
|
124
|
+
return _response(error="Missing API key", status=401)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
|
128
|
+
|
|
129
|
+
res = requests.post(
|
|
130
|
+
url,
|
|
131
|
+
json={"contents": [{"parts": [{"text": prompt}]}]},
|
|
132
|
+
timeout = int(timeout) if timeout else 60
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if res.status_code != 200:
|
|
136
|
+
return _response(error=res.text, status=res.status_code)
|
|
137
|
+
|
|
138
|
+
data = res.json()
|
|
139
|
+
parts = data.get("candidates", [])[0].get("content", {}).get("parts", [])
|
|
140
|
+
output = "".join(p.get("text", "") for p in parts)
|
|
141
|
+
|
|
142
|
+
models = []
|
|
143
|
+
if include_models:
|
|
144
|
+
m = requests.get(
|
|
145
|
+
f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
|
|
146
|
+
)
|
|
147
|
+
if m.status_code == 200:
|
|
148
|
+
models = [x.get("name") for x in m.json().get("models", [])]
|
|
149
|
+
|
|
150
|
+
return _response(output.strip(), models, res.status_code)
|
|
151
|
+
|
|
152
|
+
except requests.Timeout:
|
|
153
|
+
return _response(error="Request timeout", status=408)
|
|
154
|
+
|
|
155
|
+
except requests.ConnectionError:
|
|
156
|
+
return _response(error="Connection failed", status=503)
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
return _response(error=str(e), status=500)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def call_llm(provider, model, prompt, api_key="", include_models=False, timeout=60):
|
|
163
|
+
if provider == "openai":
|
|
164
|
+
api_key = os.getenv("OPENAI_API_KEY", "")
|
|
165
|
+
|
|
166
|
+
elif provider == "gemini":
|
|
167
|
+
api_key = os.getenv("GEMINI_API_KEY", "")
|
|
168
|
+
|
|
169
|
+
elif provider == "huggingface":
|
|
170
|
+
api_key = os.getenv("HUGGINGFACE_API_KEY", "")
|
|
171
|
+
|
|
172
|
+
if not model and provider != "none":
|
|
173
|
+
return _response(error="Model not specified", status=400)
|
|
174
|
+
|
|
175
|
+
if provider == "openai":
|
|
176
|
+
return call_openai(model, prompt, api_key, include_models, timeout)
|
|
177
|
+
|
|
178
|
+
if provider == "huggingface":
|
|
179
|
+
return call_huggingface(model, prompt, api_key, include_models, timeout)
|
|
180
|
+
|
|
181
|
+
if provider == "gemini":
|
|
182
|
+
return call_gemini(model, prompt, api_key, include_models, timeout)
|
|
183
|
+
|
|
184
|
+
if provider == "ollama":
|
|
185
|
+
return call_ollama(model, prompt, include_models, timeout)
|
|
186
|
+
|
|
187
|
+
return _response(error="Unsupported provider", status=400)
|
|
188
|
+
|
|
189
|
+
def _response(output="", models=None, status=200, error=None):
|
|
190
|
+
return {
|
|
191
|
+
"output": output or "",
|
|
192
|
+
"models": models or [],
|
|
193
|
+
"status_code": status,
|
|
194
|
+
"error": error
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def extract_openai_output(data):
|
|
198
|
+
try:
|
|
199
|
+
# ✅ Case 1: direct shortcut (new API)
|
|
200
|
+
if "output_text" in data:
|
|
201
|
+
return data["output_text"]
|
|
202
|
+
# ✅ Case 2: structured output
|
|
203
|
+
for item in data.get("output", []):
|
|
204
|
+
for c in item.get("content", []):
|
|
205
|
+
if c.get("type") == "output_text":
|
|
206
|
+
return c.get("text", "")
|
|
207
|
+
if "text" in c:
|
|
208
|
+
return c["text"]
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
return ""
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def save_data(data: dict, root: Path):
|
|
6
|
+
brain_dir = root / ".brain"
|
|
7
|
+
brain_dir.mkdir(exist_ok=True)
|
|
8
|
+
|
|
9
|
+
data_path = brain_dir / "data.json"
|
|
10
|
+
|
|
11
|
+
with data_path.open("w", encoding="utf-8") as f:
|
|
12
|
+
json.dump(data, f, indent=2)
|