codebase-brain 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/PKG-INFO +1 -1
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_cli.py +128 -35
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/graph_builder.py +1 -1
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/llm_router.py +23 -5
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/PKG-INFO +1 -1
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/setup.py +1 -1
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/README.md +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/__init__.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/ast_parser.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/bug_detector.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/codebase_walker.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/file_watcher.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/query_engine.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/root_cause.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/universal_parser.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/SOURCES.txt +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/dependency_links.txt +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/entry_points.txt +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/requires.txt +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/top_level.txt +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/setup.cfg +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_bugs.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_classes.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_parser.py +0 -0
- {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_treesitter.py +0 -0
|
@@ -65,6 +65,75 @@ def cmd_start(path):
|
|
|
65
65
|
print(f"\nBrain: {answer}\n")
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
def _explain_risk(staged_files, G, risk, brain):
|
|
69
|
+
from brain_parser.llm_router import call_llm
|
|
70
|
+
|
|
71
|
+
context = ""
|
|
72
|
+
for f in staged_files:
|
|
73
|
+
target_norm = os.path.abspath(f).replace('\\', '/')
|
|
74
|
+
matched = None
|
|
75
|
+
for node in G.nodes():
|
|
76
|
+
node_norm = os.path.abspath(node).replace('\\', '/')
|
|
77
|
+
if node_norm == target_norm:
|
|
78
|
+
matched = node
|
|
79
|
+
break
|
|
80
|
+
if not matched:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
file_risk = risk.get(matched, 'LOW')
|
|
84
|
+
if file_risk != 'HIGH':
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
direct = list(G.predecessors(matched))
|
|
88
|
+
file_data = brain.get(matched, {})
|
|
89
|
+
functions = [fn['name'] for fn in file_data.get('functions', [])]
|
|
90
|
+
|
|
91
|
+
context += f"\nFile: {f}\n"
|
|
92
|
+
context += f"Risk: {file_risk}\n"
|
|
93
|
+
context += f"Functions in this file: {functions}\n"
|
|
94
|
+
context += f"Files that depend on it: {direct}\n"
|
|
95
|
+
|
|
96
|
+
prompt = f"""A developer is about to commit changes to a HIGH risk file.
|
|
97
|
+
Given this context:
|
|
98
|
+
{context}
|
|
99
|
+
|
|
100
|
+
In 2-3 sentences, explain in plain English:
|
|
101
|
+
1. Why this file is risky to change
|
|
102
|
+
2. What could realistically break if the change is wrong
|
|
103
|
+
3. What the developer should double check before committing
|
|
104
|
+
|
|
105
|
+
Be direct and specific. No generic advice."""
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
return call_llm(prompt, max_tokens=200)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return f"Could not generate explanation: {str(e)}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _print_impact(G, risk, filepath, clean):
|
|
114
|
+
from brain_parser.graph_builder import get_impact
|
|
115
|
+
|
|
116
|
+
result = get_impact(G, filepath)
|
|
117
|
+
|
|
118
|
+
if not result:
|
|
119
|
+
print(f"File not found in brain: {filepath}")
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
file_risk = risk.get(result['target'], 'LOW')
|
|
123
|
+
|
|
124
|
+
print(f"\n Change Impact: {filepath}")
|
|
125
|
+
print(f" Risk Level: {file_risk}\n")
|
|
126
|
+
print(f"DIRECT IMPACT ({len(result['direct'])} files):")
|
|
127
|
+
for f in result['direct']:
|
|
128
|
+
print(f" → {clean(f)}")
|
|
129
|
+
print(f"\nINDIRECT IMPACT ({len(result['indirect'])} files):")
|
|
130
|
+
for f in result['indirect']:
|
|
131
|
+
print(f" → {clean(f)}")
|
|
132
|
+
print(f"\nTotal files affected: {result['total_affected']}\n")
|
|
133
|
+
|
|
134
|
+
return {'risk': file_risk, 'total': result['total_affected']}
|
|
135
|
+
|
|
136
|
+
|
|
68
137
|
def cmd_impact(filepath=None, staged=False, block=False):
|
|
69
138
|
from brain_parser.codebase_walker import load_brain
|
|
70
139
|
from brain_parser.graph_builder import build_graph, get_impact, calculate_risk
|
|
@@ -106,10 +175,13 @@ def cmd_impact(filepath=None, staged=False, block=False):
|
|
|
106
175
|
print("BRAIN WARNING: HIGH RISK commit detected.")
|
|
107
176
|
print("This change affects critical files.")
|
|
108
177
|
print("Production incidents have originated from files like these.")
|
|
178
|
+
|
|
179
|
+
print("\nAsking Brain to explain the risk...\n")
|
|
180
|
+
explanation = _explain_risk(staged_files, G, risk, brain)
|
|
181
|
+
print(explanation)
|
|
182
|
+
|
|
109
183
|
print("\nType 'yes' to commit anyway, anything else to abort: ", end='', flush=True)
|
|
110
184
|
try:
|
|
111
|
-
# Windows: CON is the console device
|
|
112
|
-
# Unix: /dev/tty is the terminal
|
|
113
185
|
import platform
|
|
114
186
|
if platform.system() == 'Windows':
|
|
115
187
|
with open('CON', 'r') as tty:
|
|
@@ -134,30 +206,6 @@ def cmd_impact(filepath=None, staged=False, block=False):
|
|
|
134
206
|
return
|
|
135
207
|
|
|
136
208
|
_print_impact(G, risk, filepath, clean)
|
|
137
|
-
|
|
138
|
-
def _print_impact(G, risk, filepath, clean):
|
|
139
|
-
from brain_parser.graph_builder import get_impact
|
|
140
|
-
|
|
141
|
-
result = get_impact(G, filepath)
|
|
142
|
-
|
|
143
|
-
if not result:
|
|
144
|
-
print(f"File not found in brain: {filepath}")
|
|
145
|
-
return None
|
|
146
|
-
|
|
147
|
-
file_risk = risk.get(result['target'], 'LOW')
|
|
148
|
-
|
|
149
|
-
print(f"\n Change Impact: {filepath}")
|
|
150
|
-
print(f" Risk Level: {file_risk}\n")
|
|
151
|
-
print(f"DIRECT IMPACT ({len(result['direct'])} files):")
|
|
152
|
-
for f in result['direct']:
|
|
153
|
-
print(f" → {clean(f)}")
|
|
154
|
-
print(f"\nINDIRECT IMPACT ({len(result['indirect'])} files):")
|
|
155
|
-
for f in result['indirect']:
|
|
156
|
-
print(f" → {clean(f)}")
|
|
157
|
-
print(f"\nTotal files affected: {result['total_affected']}\n")
|
|
158
|
-
|
|
159
|
-
return {'risk': file_risk, 'total': result['total_affected']}
|
|
160
|
-
|
|
161
209
|
def install_hook(repo_path="."):
|
|
162
210
|
hook_dir = os.path.join(repo_path, ".git", "hooks")
|
|
163
211
|
hook_path = os.path.join(hook_dir, "pre-commit")
|
|
@@ -200,7 +248,14 @@ def install_hook(repo_path="."):
|
|
|
200
248
|
|
|
201
249
|
|
|
202
250
|
def cmd_init():
|
|
251
|
+
import os
|
|
203
252
|
print("Initializing Codebase Brain...\n")
|
|
253
|
+
|
|
254
|
+
# global config — stored once per machine
|
|
255
|
+
config_dir = os.path.expanduser("~/.codebase-brain")
|
|
256
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
257
|
+
config_path = os.path.join(config_dir, "config.env")
|
|
258
|
+
|
|
204
259
|
print("Choose your LLM provider:")
|
|
205
260
|
print(" 1. Groq — Free, fast. llama-3.3-70b (recommended to start)")
|
|
206
261
|
print(" 2. OpenAI — Reliable, trusted by enterprises. gpt-4o-mini")
|
|
@@ -237,7 +292,7 @@ def cmd_init():
|
|
|
237
292
|
},
|
|
238
293
|
"5": {
|
|
239
294
|
"name": "ollama",
|
|
240
|
-
"key_prompt": None,
|
|
295
|
+
"key_prompt": None,
|
|
241
296
|
"models": ["llama3.2", "codellama", "deepseek-coder"],
|
|
242
297
|
"default_model": "llama3.2"
|
|
243
298
|
}
|
|
@@ -248,14 +303,12 @@ def cmd_init():
|
|
|
248
303
|
|
|
249
304
|
provider = providers[choice]
|
|
250
305
|
|
|
251
|
-
# get API key
|
|
252
306
|
if provider["key_prompt"]:
|
|
253
307
|
api_key = input(provider["key_prompt"]).strip()
|
|
254
308
|
else:
|
|
255
309
|
api_key = "ollama"
|
|
256
310
|
print("Ollama selected — make sure Ollama is running at http://localhost:11434")
|
|
257
311
|
|
|
258
|
-
# pick model
|
|
259
312
|
print(f"\nAvailable models for {provider['name']}:")
|
|
260
313
|
for i, m in enumerate(provider["models"], 1):
|
|
261
314
|
default_tag = " (default)" if m == provider["default_model"] else ""
|
|
@@ -271,16 +324,16 @@ def cmd_init():
|
|
|
271
324
|
else:
|
|
272
325
|
model = provider["default_model"]
|
|
273
326
|
|
|
274
|
-
|
|
327
|
+
# save globally — one time setup
|
|
328
|
+
with open(config_path, "w") as f:
|
|
275
329
|
f.write(f"LLM_PROVIDER={provider['name']}\n")
|
|
276
330
|
f.write(f"LLM_API_KEY={api_key}\n")
|
|
277
331
|
f.write(f"LLM_MODEL={model}\n")
|
|
278
|
-
# backwards compatibility
|
|
279
332
|
f.write(f"GROQ_API_KEY={api_key}\n")
|
|
280
333
|
f.write(f"GROQ_MODEL={model}\n")
|
|
281
334
|
|
|
282
|
-
print(f"\nBrain initialized
|
|
283
|
-
print("Run 'brain start' to begin.\n")
|
|
335
|
+
print(f"\nBrain initialized globally — {provider['name']} / {model}")
|
|
336
|
+
print("Run 'brain start' in any project to begin.\n")
|
|
284
337
|
|
|
285
338
|
def cmd_rootcause(error_text=None):
|
|
286
339
|
from brain_parser.root_cause import find_root_cause
|
|
@@ -297,6 +350,41 @@ def cmd_rootcause(error_text=None):
|
|
|
297
350
|
error_text = "\n".join(lines)
|
|
298
351
|
|
|
299
352
|
print(find_root_cause(error_text))
|
|
353
|
+
def cmd_uninstall():
|
|
354
|
+
import shutil
|
|
355
|
+
|
|
356
|
+
print("Uninstalling Codebase Brain...\n")
|
|
357
|
+
|
|
358
|
+
# remove global config
|
|
359
|
+
config_dir = os.path.expanduser("~/.codebase-brain")
|
|
360
|
+
if os.path.exists(config_dir):
|
|
361
|
+
shutil.rmtree(config_dir)
|
|
362
|
+
print("Removed global config.")
|
|
363
|
+
|
|
364
|
+
# remove local brain.json
|
|
365
|
+
brain_json = os.path.join(os.getcwd(), 'brain.json')
|
|
366
|
+
if os.path.exists(brain_json):
|
|
367
|
+
os.remove(brain_json)
|
|
368
|
+
print("Removed brain.json.")
|
|
369
|
+
|
|
370
|
+
# remove local brain_map.html
|
|
371
|
+
brain_map = os.path.join(os.getcwd(), 'brain_map.html')
|
|
372
|
+
if os.path.exists(brain_map):
|
|
373
|
+
os.remove(brain_map)
|
|
374
|
+
print("Removed brain_map.html.")
|
|
375
|
+
|
|
376
|
+
# remove git hook
|
|
377
|
+
hook_path = os.path.join(os.getcwd(), '.git', 'hooks', 'pre-commit')
|
|
378
|
+
if os.path.exists(hook_path):
|
|
379
|
+
# only remove if it's a Brain hook
|
|
380
|
+
with open(hook_path, 'r') as f:
|
|
381
|
+
content = f.read()
|
|
382
|
+
if 'Codebase Brain' in content:
|
|
383
|
+
os.remove(hook_path)
|
|
384
|
+
print("Removed git hook.")
|
|
385
|
+
|
|
386
|
+
print("\nBrain removed from this project.")
|
|
387
|
+
print("To fully uninstall: pip uninstall codebase-brain")
|
|
300
388
|
|
|
301
389
|
|
|
302
390
|
def main():
|
|
@@ -307,9 +395,10 @@ def main():
|
|
|
307
395
|
parser.add_argument("--path", default=".", help="Path to codebase")
|
|
308
396
|
parser.add_argument("--file", default="", help="File to analyze impact")
|
|
309
397
|
parser.add_argument("--staged", action="store_true", help="Analyze all staged files")
|
|
310
|
-
parser.add_argument("command", choices=["start", "init", "impact", "install-hook", "rootcause"])
|
|
398
|
+
parser.add_argument("command", choices=["start", "init", "impact", "install-hook", "rootcause", "uninstall"])
|
|
311
399
|
parser.add_argument("--error", default="", help="Stacktrace to analyze")
|
|
312
400
|
parser.add_argument("--block", action="store_true", help="Block HIGH risk commits")
|
|
401
|
+
|
|
313
402
|
# ← new
|
|
314
403
|
args = parser.parse_args()
|
|
315
404
|
|
|
@@ -324,8 +413,12 @@ def main():
|
|
|
324
413
|
cmd_impact(filepath=args.file or None, staged=args.staged, block=args.block)
|
|
325
414
|
elif args.command == "install-hook":
|
|
326
415
|
install_hook()
|
|
416
|
+
elif args.command == "uninstall":
|
|
417
|
+
cmd_uninstall()
|
|
418
|
+
|
|
327
419
|
|
|
328
420
|
|
|
329
421
|
|
|
330
422
|
if __name__ == "__main__":
|
|
331
|
-
main()
|
|
423
|
+
main()
|
|
424
|
+
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from dotenv import load_dotenv
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
def get_config():
|
|
4
|
+
"""Load from local .env first, then global ~/.codebase-brain/config.env"""
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
# try local .env first
|
|
8
|
+
local_env = os.path.join(os.getcwd(), '.env')
|
|
9
|
+
if os.path.exists(local_env):
|
|
10
|
+
load_dotenv(local_env)
|
|
11
|
+
else:
|
|
12
|
+
# fallback to global config
|
|
13
|
+
global_config = os.path.expanduser("~/.codebase-brain/config.env")
|
|
14
|
+
if os.path.exists(global_config):
|
|
15
|
+
load_dotenv(global_config)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
'provider': os.getenv("LLM_PROVIDER", "groq").lower(),
|
|
19
|
+
'api_key': os.getenv("LLM_API_KEY") or os.getenv("GROQ_API_KEY"),
|
|
20
|
+
'model': os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
|
|
7
24
|
def call_llm(prompt, max_tokens=500):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
25
|
+
config = get_config()
|
|
26
|
+
provider = config['provider']
|
|
27
|
+
api_key = config['api_key']
|
|
28
|
+
model = config['model']
|
|
11
29
|
|
|
12
30
|
if provider == "groq":
|
|
13
31
|
return _call_groq(api_key, model, prompt, max_tokens)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|