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.
Files changed (25) hide show
  1. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/PKG-INFO +1 -1
  2. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_cli.py +128 -35
  3. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/graph_builder.py +1 -1
  4. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/llm_router.py +23 -5
  5. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/PKG-INFO +1 -1
  6. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/setup.py +1 -1
  7. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/README.md +0 -0
  8. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/__init__.py +0 -0
  9. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/ast_parser.py +0 -0
  10. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/bug_detector.py +0 -0
  11. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/codebase_walker.py +0 -0
  12. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/file_watcher.py +0 -0
  13. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/query_engine.py +0 -0
  14. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/root_cause.py +0 -0
  15. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/brain_parser/universal_parser.py +0 -0
  16. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/SOURCES.txt +0 -0
  17. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/dependency_links.txt +0 -0
  18. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/entry_points.txt +0 -0
  19. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/requires.txt +0 -0
  20. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/codebase_brain.egg-info/top_level.txt +0 -0
  21. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/setup.cfg +0 -0
  22. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_bugs.py +0 -0
  23. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_classes.py +0 -0
  24. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_parser.py +0 -0
  25. {codebase_brain-0.2.0 → codebase_brain-0.3.0}/tests/test_treesitter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brain
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: An AI brain that lives inside your codebase
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: watchdog
@@ -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, # no key needed
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
- with open(".env", "w") as f:
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 with {provider['name']} {model}")
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
+
@@ -106,7 +106,7 @@ def visualize_interactive(G):
106
106
  node_map = {}
107
107
  for node in G.nodes():
108
108
  node_map[node] = str(node)
109
-
109
+ # demo test
110
110
  for node, node_id in node_map.items():
111
111
  risk_level = risk.get(node, 'LOW')
112
112
  color = color_map[risk_level]
@@ -1,13 +1,31 @@
1
1
  import os
2
- from dotenv import load_dotenv
3
2
 
4
- load_dotenv()
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
- provider = os.getenv("LLM_PROVIDER", "groq").lower()
9
- api_key = os.getenv("LLM_API_KEY") or os.getenv("GROQ_API_KEY")
10
- model = os.getenv("LLM_MODEL") or os.getenv("GROQ_MODEL", "llama-3.3-70b-versatile")
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-brain
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: An AI brain that lives inside your codebase
5
5
  Requires-Python: >=3.8
6
6
  Requires-Dist: watchdog
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="codebase-brain",
5
- version="0.2.0",
5
+ version="0.3.0",
6
6
  description="An AI brain that lives inside your codebase",
7
7
  package_dir={"": "."},
8
8
  packages=find_packages(where="."),
File without changes
File without changes