codebase-brain 0.1.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.
brain_cli.py ADDED
@@ -0,0 +1,331 @@
1
+ import threading
2
+ import subprocess
3
+ import stat
4
+ import argparse
5
+ import os
6
+ import sys
7
+ from brain_parser.codebase_walker import walk_codebase, save_brain
8
+ from brain_parser.file_watcher import start_watching
9
+ from brain_parser.graph_builder import build_graph, visualize_interactive
10
+ from brain_parser.query_engine import ask_brain
11
+ import io
12
+
13
+ # Force UTF-8 output regardless of parent process encoding
14
+ if sys.stdout.encoding != 'utf-8':
15
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
16
+ if sys.stderr.encoding != 'utf-8':
17
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
18
+
19
+ import argparse
20
+ import os
21
+ # ... rest of your existing imports
22
+
23
+ def cmd_start(path):
24
+ print(" Codebase Brain starting...")
25
+ print(f" Reading codebase at: {path}")
26
+ brain = walk_codebase(path)
27
+ save_brain(brain)
28
+ print(f"Brain built: {len(brain)} files analyzed")
29
+
30
+ print(" Building dependency graph...")
31
+ G = build_graph(brain)
32
+ visualize_interactive(G)
33
+ print("Graph ready: brain_map.html")
34
+
35
+ print("\n Scanning for bugs...")
36
+ from brain_parser.bug_detector import run_all_detectors
37
+ bugs = run_all_detectors(brain, G)
38
+ if bugs:
39
+ print(f"\n Found {len(bugs)} potential bugs:\n")
40
+ for bug in bugs:
41
+ print(f" [{bug['severity']}] {bug['type'].replace('_', ' ').upper()}")
42
+ print(f" {bug['message']}")
43
+ print(f" Fix: {bug['fix']}\n")
44
+ else:
45
+ print(" No bugs detected. Codebase looks clean.\n")
46
+
47
+ print("Starting live file watcher...")
48
+ watcher_thread = threading.Thread(
49
+ target=start_watching,
50
+ args=(path,),
51
+ daemon=True
52
+ )
53
+ watcher_thread.start()
54
+ print("Watching for changes...")
55
+
56
+ print("\n Brain is live. Ask anything.")
57
+ print("Type 'exit' to stop query mode.")
58
+ print("Press CTRL+C anytime to shut down completely.\n")
59
+ while True:
60
+ question = input("Ask your brain: ")
61
+ if question == "exit":
62
+ print("Brain shutting down.")
63
+ break
64
+ answer = ask_brain(question)
65
+ print(f"\nBrain: {answer}\n")
66
+
67
+
68
+ def cmd_impact(filepath=None, staged=False, block=False):
69
+ from brain_parser.codebase_walker import load_brain
70
+ from brain_parser.graph_builder import build_graph, get_impact, calculate_risk
71
+
72
+ brain = load_brain()
73
+ if not brain:
74
+ print("No brain found. Run 'brain start' first.")
75
+ return
76
+
77
+ G = build_graph(brain)
78
+ risk = calculate_risk(G)
79
+
80
+ root = os.path.abspath('.').replace('\\', '/')
81
+ def clean(path):
82
+ return path.replace('\\', '/').replace(root + '/', '')
83
+
84
+ if staged:
85
+ result = subprocess.run(
86
+ ["git", "diff", "--name-only", "--cached"],
87
+ capture_output=True, text=True
88
+ )
89
+ staged_files = [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
90
+
91
+ if not staged_files:
92
+ print("No staged files found.")
93
+ return
94
+
95
+ print(f"\n Staged files: {', '.join(staged_files)}\n")
96
+
97
+ high_risk_found = False
98
+ for f in staged_files:
99
+ print(f"{'='*50}")
100
+ result_data = _print_impact(G, risk, f, clean)
101
+ if result_data and result_data.get('risk') == 'HIGH':
102
+ high_risk_found = True
103
+
104
+ if block and high_risk_found:
105
+ print("\n" + "=" * 50)
106
+ print("BRAIN WARNING: HIGH RISK commit detected.")
107
+ print("This change affects critical files.")
108
+ print("Production incidents have originated from files like these.")
109
+ print("\nType 'yes' to commit anyway, anything else to abort: ", end='', flush=True)
110
+ try:
111
+ # Windows: CON is the console device
112
+ # Unix: /dev/tty is the terminal
113
+ import platform
114
+ if platform.system() == 'Windows':
115
+ with open('CON', 'r') as tty:
116
+ confirm = tty.readline().strip().lower()
117
+ else:
118
+ with open('/dev/tty', 'r') as tty:
119
+ confirm = tty.readline().strip().lower()
120
+ except:
121
+ try:
122
+ confirm = input().strip().lower()
123
+ except EOFError:
124
+ confirm = 'yes'
125
+ if confirm != 'yes':
126
+ print("\nCommit blocked by Codebase Brain.")
127
+ sys.exit(1)
128
+ else:
129
+ print("Override confirmed. Committing anyway.")
130
+ return
131
+
132
+ if not filepath:
133
+ print("Provide --file or --staged.")
134
+ return
135
+
136
+ _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
+ def install_hook(repo_path="."):
162
+ hook_dir = os.path.join(repo_path, ".git", "hooks")
163
+ hook_path = os.path.join(hook_dir, "pre-commit")
164
+
165
+ if not os.path.exists(hook_dir):
166
+ print("Not a git repository. Run this inside your project folder.")
167
+ return
168
+
169
+ hook_script = """#!/bin/sh
170
+ # Codebase Brain — Pre-commit Impact Check
171
+ # https://github.com/guruvarpatel-ai/codebase-brain
172
+
173
+ STAGED=$(git diff --name-only --cached)
174
+
175
+ if [ -z "$STAGED" ]; then
176
+ exit 0
177
+ fi
178
+
179
+ echo ""
180
+ echo "Codebase Brain — Checking blast radius..."
181
+ echo ""
182
+
183
+ brain impact --staged --block
184
+ EXIT_CODE=$?
185
+
186
+ echo ""
187
+ echo "Powered by Codebase Brain"
188
+ echo ""
189
+
190
+ exit $EXIT_CODE
191
+ """
192
+
193
+ with open(hook_path, "w", newline='\n') as f: # newline='\n' fixes Windows CRLF
194
+ f.write(hook_script)
195
+
196
+ os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
197
+
198
+ print(f"Brain hook installed at {hook_path}")
199
+ print("Every commit now shows blast radius automatically.")
200
+
201
+
202
+ def cmd_init():
203
+ print("Initializing Codebase Brain...\n")
204
+ print("Choose your LLM provider:")
205
+ print(" 1. Groq — Free, fast. llama-3.3-70b (recommended to start)")
206
+ print(" 2. OpenAI — Reliable, trusted by enterprises. gpt-4o-mini")
207
+ print(" 3. Anthropic — Best reasoning. claude-3-5-haiku (your code stays private)")
208
+ print(" 4. Google — Free tier available. gemini-1.5-flash")
209
+ print(" 5. Ollama — 100% local, no API key, no data leaves your machine")
210
+ print("")
211
+ choice = input("Enter 1-5 (default 1): ").strip() or "1"
212
+
213
+ providers = {
214
+ "1": {
215
+ "name": "groq",
216
+ "key_prompt": "Groq API key (free at console.groq.com): ",
217
+ "models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
218
+ "default_model": "llama-3.3-70b-versatile"
219
+ },
220
+ "2": {
221
+ "name": "openai",
222
+ "key_prompt": "OpenAI API key (platform.openai.com): ",
223
+ "models": ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"],
224
+ "default_model": "gpt-4o-mini"
225
+ },
226
+ "3": {
227
+ "name": "anthropic",
228
+ "key_prompt": "Anthropic API key (console.anthropic.com): ",
229
+ "models": ["claude-3-5-haiku-20241022", "claude-3-5-sonnet-20241022"],
230
+ "default_model": "claude-3-5-haiku-20241022"
231
+ },
232
+ "4": {
233
+ "name": "google",
234
+ "key_prompt": "Google API key (aistudio.google.com): ",
235
+ "models": ["gemini-1.5-flash", "gemini-1.5-pro", "gemini-2.0-flash"],
236
+ "default_model": "gemini-1.5-flash"
237
+ },
238
+ "5": {
239
+ "name": "ollama",
240
+ "key_prompt": None, # no key needed
241
+ "models": ["llama3.2", "codellama", "deepseek-coder"],
242
+ "default_model": "llama3.2"
243
+ }
244
+ }
245
+
246
+ if choice not in providers:
247
+ choice = "1"
248
+
249
+ provider = providers[choice]
250
+
251
+ # get API key
252
+ if provider["key_prompt"]:
253
+ api_key = input(provider["key_prompt"]).strip()
254
+ else:
255
+ api_key = "ollama"
256
+ print("Ollama selected — make sure Ollama is running at http://localhost:11434")
257
+
258
+ # pick model
259
+ print(f"\nAvailable models for {provider['name']}:")
260
+ for i, m in enumerate(provider["models"], 1):
261
+ default_tag = " (default)" if m == provider["default_model"] else ""
262
+ print(f" {i}. {m}{default_tag}")
263
+
264
+ model_choice = input("\nEnter model number (press Enter for default): ").strip()
265
+ if model_choice and model_choice.isdigit():
266
+ idx = int(model_choice) - 1
267
+ if 0 <= idx < len(provider["models"]):
268
+ model = provider["models"][idx]
269
+ else:
270
+ model = provider["default_model"]
271
+ else:
272
+ model = provider["default_model"]
273
+
274
+ with open(".env", "w") as f:
275
+ f.write(f"LLM_PROVIDER={provider['name']}\n")
276
+ f.write(f"LLM_API_KEY={api_key}\n")
277
+ f.write(f"LLM_MODEL={model}\n")
278
+ # backwards compatibility
279
+ f.write(f"GROQ_API_KEY={api_key}\n")
280
+ f.write(f"GROQ_MODEL={model}\n")
281
+
282
+ print(f"\nBrain initialized with {provider['name']} — {model}")
283
+ print("Run 'brain start' to begin.\n")
284
+
285
+ def cmd_rootcause(error_text=None):
286
+ from brain_parser.root_cause import find_root_cause
287
+
288
+ if not error_text:
289
+ print("Paste your error/stacktrace below.")
290
+ print("Press Enter twice when done.\n")
291
+ lines = []
292
+ while True:
293
+ line = input()
294
+ if line == "" and lines and lines[-1] == "":
295
+ break
296
+ lines.append(line)
297
+ error_text = "\n".join(lines)
298
+
299
+ print(find_root_cause(error_text))
300
+
301
+
302
+ def main():
303
+ parser = argparse.ArgumentParser(
304
+ description="Codebase Brain - AI layer over your codebase"
305
+ )
306
+ # ← "install-hook" added here
307
+ parser.add_argument("--path", default=".", help="Path to codebase")
308
+ parser.add_argument("--file", default="", help="File to analyze impact")
309
+ parser.add_argument("--staged", action="store_true", help="Analyze all staged files")
310
+ parser.add_argument("command", choices=["start", "init", "impact", "install-hook", "rootcause"])
311
+ parser.add_argument("--error", default="", help="Stacktrace to analyze")
312
+ parser.add_argument("--block", action="store_true", help="Block HIGH risk commits")
313
+ # ← new
314
+ args = parser.parse_args()
315
+
316
+
317
+ if args.command == "init":
318
+ cmd_init()
319
+ elif args.command == "rootcause":
320
+ cmd_rootcause(args.error or None)
321
+ elif args.command == "start":
322
+ cmd_start(args.path)
323
+ elif args.command == "impact":
324
+ cmd_impact(filepath=args.file or None, staged=args.staged, block=args.block)
325
+ elif args.command == "install-hook":
326
+ install_hook()
327
+
328
+
329
+
330
+ if __name__ == "__main__":
331
+ main()
File without changes
@@ -0,0 +1,62 @@
1
+ import ast
2
+ import os
3
+
4
+ def parse_file(filepath):
5
+ #hello world
6
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
7
+ source=f.read()
8
+ try:
9
+ tree=ast.parse(source)
10
+ return tree
11
+ except SyntaxError as e:
12
+ print(f"Syntax error in {filepath}: {e}")
13
+ return None
14
+
15
+ def extract_imports(tree):
16
+ imports = []
17
+ for node in ast.walk(tree):
18
+ if isinstance(node, ast.Import):
19
+ for alias in node.names:
20
+ imports.append({
21
+ "name": alias.name,
22
+ "type": "direct"
23
+ })
24
+ if isinstance(node, ast.ImportFrom):
25
+ imports.append({
26
+ "module": node.module or "unknown",
27
+ "type": "from"
28
+ })
29
+ return imports
30
+
31
+ def extract_classes_and_functions(tree):
32
+ classes = []
33
+ functions = []
34
+ for node in ast.walk(tree):
35
+ if isinstance(node, ast.ClassDef):
36
+ classes.append({
37
+ "name": node.name,
38
+ "line": node.lineno
39
+ })
40
+ if isinstance(node, ast.FunctionDef):
41
+ functions.append({
42
+ "name": node.name,
43
+ "args": [arg.id if hasattr(arg, 'id') else arg.arg
44
+ for arg in node.args.args],
45
+ "line": node.lineno
46
+ })
47
+ return {"classes": classes, "functions": functions}
48
+
49
+
50
+ def analyze_file(filepath):
51
+ tree = parse_file(filepath)
52
+ if tree is None:
53
+ return None
54
+
55
+ return {
56
+ "file":filepath ,
57
+ "imports":extract_imports(tree),
58
+ "classes_and_functions":extract_classes_and_functions(tree)
59
+ }
60
+ if __name__ == "__main__":
61
+ result = analyze_file("utils.py")
62
+ print(result)
@@ -0,0 +1,212 @@
1
+ import networkx as nx
2
+ import os
3
+ from brain_parser.codebase_walker import load_brain
4
+ from brain_parser.graph_builder import build_graph
5
+
6
+
7
+
8
+ def detect_circular_dependencies(brain, temp_path=None):
9
+ G = build_graph(brain)
10
+ bugs = []
11
+
12
+ def clean(path):
13
+ p = path.replace('\\', '/')
14
+ idx = p.find('/temp/')
15
+ if idx != -1:
16
+ after_temp = p[idx + 6:]
17
+ parts = after_temp.split('/', 1)
18
+ if len(parts) > 1:
19
+ return parts[1]
20
+ return parts[0]
21
+ # fallback — just filename
22
+ return p.split('/')[-1]
23
+
24
+ try:
25
+ cycles = list(nx.simple_cycles(G))
26
+ for cycle in cycles:
27
+ clean_cycle = [clean(f) for f in cycle]
28
+ # format as numbered list not one long line
29
+ chain = '\n '.join([f"{i+1}. {f}" for i, f in enumerate(clean_cycle)])
30
+ chain += f"\n → back to {clean_cycle[0]}"
31
+ bugs.append({
32
+ 'type': 'circular_dependency',
33
+ 'severity': 'HIGH',
34
+ 'files': clean_cycle,
35
+ 'message': f"Circular dependency ({len(cycle)} files):\n {chain}",
36
+ 'fix': 'Extract shared logic into a separate file that both can import from.'
37
+ })
38
+ except Exception as e:
39
+ print(f"Cycle detection error: {e}")
40
+
41
+ return bugs
42
+ def detect_unused_imports(brain):
43
+ # compare imports list against function/class names used in file
44
+ bugs = []
45
+
46
+ for filepath, data in brain.items():
47
+ # skip non-python files until we support body reading
48
+ if data.get('language') != 'python':
49
+ continue
50
+ imports = data.get('imports', [])
51
+ functions = [f['name'].lower() for f in data.get('functions', [])]
52
+ classes = [c['name'].lower() for c in data.get('classes', [])]
53
+ summary = data.get('summary', '').lower()
54
+
55
+ for imp in imports:
56
+ imp_name = imp.get('name', '').lower()
57
+ # extract just the module name
58
+ imp_name = imp.get('name', '').lower()
59
+
60
+ # handle "from x import y" → extract x
61
+ if imp_name.startswith('from '):
62
+ module = imp_name.split('from ')[1].split(' import')[0].strip()
63
+ # handle "import x" → extract x
64
+ elif imp_name.startswith('import '):
65
+ module = imp_name.replace('import ', '').split(' as ')[0].strip()
66
+ else:
67
+ continue
68
+
69
+ if not module:
70
+ continue
71
+
72
+ # check if module appears anywhere in file context
73
+ used = (
74
+ any(module in f for f in functions) or
75
+ any(module in c for c in classes) or
76
+ module in summary
77
+ )
78
+
79
+ if not used:
80
+ bugs.append({
81
+ 'type': 'unused_import',
82
+ 'severity': 'LOW',
83
+ 'file': filepath,
84
+ 'import': imp.get('name', ''),
85
+ 'line': imp.get('line', 0),
86
+ 'message': f"Possibly unused import '{module}' in {filepath}",
87
+ 'fix': f"Remove 'import {module}' if not needed."
88
+ })
89
+
90
+ return bugs
91
+
92
+ def detect_bugs_with_llm(brain, G):
93
+ # calculate risk → filter HIGH risk files → send to Groq → find bugs
94
+ from brain_parser.graph_builder import calculate_risk
95
+ from groq import Groq
96
+ from dotenv import load_dotenv
97
+ load_dotenv()
98
+
99
+ risk = calculate_risk(G)
100
+ high_risk_files = [f for f, r in risk.items() if r == "HIGH"]
101
+
102
+ if not high_risk_files:
103
+ return []
104
+
105
+ client = Groq(api_key=os.getenv("GROQ_API_KEY"))
106
+ bugs = []
107
+
108
+ for filepath in high_risk_files:
109
+ data = brain.get(filepath)
110
+ if not data:
111
+ continue
112
+
113
+ summary = data.get('summary', '')
114
+ functions = [f['name'] for f in data.get('functions', [])]
115
+ imports = [i['name'] for i in data.get('imports', [])]
116
+
117
+ prompt = f"""You are a senior code reviewer. Analyze this file for bugs.
118
+
119
+ File: {filepath}
120
+ Summary: {summary}
121
+ Functions: {functions}
122
+ Imports: {imports}
123
+
124
+ Rules:
125
+ - Only report bugs you can prove from the information given
126
+ - Every bug must reference a specific function name from the functions list
127
+ - Do not invent bugs that are not evident from the summary
128
+ - If no provable bugs exist respond with exactly: NO BUGS
129
+
130
+ Format each bug exactly like this:
131
+ BUG: [function_name] description of specific problem
132
+ FIX: specific code change needed"""
133
+
134
+ try:
135
+ response = client.chat.completions.create(
136
+ model="llama-3.3-70b-versatile",
137
+ messages=[{"role": "user", "content": prompt}],
138
+ max_tokens=300,
139
+ temperature = 0
140
+ )
141
+ result = response.choices[0].message.content.strip()
142
+
143
+ if "NO BUGS" not in result:
144
+ bugs.append({
145
+ 'type': 'llm_detected_bug',
146
+ 'severity': 'HIGH',
147
+ 'file': filepath,
148
+ 'message': result,
149
+ 'fix': 'See details above'
150
+ })
151
+ except Exception as e:
152
+ print(f"LLM bug detection error for {filepath}: {e}")
153
+
154
+ return bugs
155
+
156
+
157
+ DANGEROUS_IMPORTS = {
158
+ 'pickle': 'Deserialization vulnerability - pickle can execute arbitrary code',
159
+ 'subprocess': 'Potential command injection if user input is passed to subprocess',
160
+ 'eval': 'Code injection risk - eval executes arbitrary Python code',
161
+ 'exec': 'Code injection risk - exec executes arbitrary Python code',
162
+ 'hashlib.md5': 'Weak hashing - MD5 is cryptographically broken for security use',
163
+ 'tempfile': 'Insecure temporary file handling possible',
164
+ 'yaml.load': 'Use yaml.safe_load instead - yaml.load can execute arbitrary code',
165
+ }
166
+
167
+
168
+ def detect_security_antipatterns(brain):
169
+ # check imports against known dangerous patterns
170
+ bugs = []
171
+
172
+ for filepath, data in brain.items():
173
+ if data.get('language') != 'python':
174
+ continue
175
+
176
+ for imp in data.get('imports', []):
177
+ imp_name = imp.get('name', '').lower()
178
+
179
+ for dangerous, reason in DANGEROUS_IMPORTS.items():
180
+ if dangerous in imp_name:
181
+ bugs.append({
182
+ 'type': 'security_antipattern',
183
+ 'severity': 'HIGH',
184
+ 'file': filepath,
185
+ 'import': imp.get('name', ''),
186
+ 'line': imp.get('line', 0),
187
+ 'message': f"Security risk in {filepath} line {imp.get('line', 0)}: {reason}",
188
+ 'fix': f"Review usage of '{dangerous}' and ensure it never processes untrusted input."
189
+ })
190
+
191
+ return bugs
192
+
193
+
194
+ def run_all_detectors(brain=None, G=None, temp_path=None):
195
+ if not brain:
196
+ brain = load_brain()
197
+ if not brain:
198
+ return []
199
+
200
+ bugs = []
201
+ bugs.extend(detect_circular_dependencies(brain, temp_path))
202
+ bugs.extend(detect_security_antipatterns(brain))
203
+
204
+ return bugs
205
+
206
+ if __name__ == "__main__":
207
+ bugs = run_all_detectors()
208
+ print(f"\nFound {len(bugs)} potential bugs:\n")
209
+ for bug in bugs:
210
+ print(f"[{bug['severity']}] {bug['type']}")
211
+ print(f" {bug['message']}")
212
+ print(f" Fix: {bug['fix']}\n")