Download cli.py from armaanalam/CodeBase-Agent: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/armaanalam/CodeBase-Agent/resolve/main/cli.py
- Command line
-
hf download hf://armaanalam/CodeBase-Agent/cli.py
-
curl -L -o cli.py https://huggingface.co/armaanalam/CodeBase-Agent/resolve/main/cli.py
11.5 kB
| """ | |
| Codebase Assistant β Interactive CLI | |
| Run with: .venv\Scripts\python.exe cli.py | |
| """ | |
| import os | |
| import sys | |
| import asyncio | |
| # ββ styling βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CYAN = "\033[96m" | |
| GREEN = "\033[92m" | |
| YELLOW = "\033[93m" | |
| RED = "\033[91m" | |
| BOLD = "\033[1m" | |
| DIM = "\033[2m" | |
| RESET = "\033[0m" | |
| # ββ session state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Tracks the repo path provided at startup (or via re-ingest) so that option 7 | |
| # can save generated files directly into that directory without asking again. | |
| _current_repo: str = "." | |
| def banner(): | |
| print(f""" | |
| {CYAN}{BOLD} | |
| ββββββββββββββββββββββββββββββββββββββββββββ | |
| β AI Codebase Assistant CLI β | |
| ββββββββββββββββββββββββββββββββββββββββββββ | |
| {RESET}""") | |
| def section(title: str): | |
| print(f"\n{CYAN}{BOLD}ββ {title} {'β' * (42 - len(title))}{RESET}") | |
| def ok(msg: str): | |
| print(f" {GREEN}β{RESET} {msg}") | |
| def err(msg: str): | |
| print(f" {RED}β {msg}{RESET}") | |
| def info(msg: str): | |
| print(f" {DIM}{msg}{RESET}") | |
| def ask(prompt: str) -> str: | |
| return input(f"\n{YELLOW}βΆ {prompt}{RESET} ").strip() | |
| # ββ ingest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ingest_repository(root_path: str): | |
| global _current_repo | |
| section("Ingesting Repository") | |
| from rag.repository_loader import load_repository | |
| from rag.splitter import split_code | |
| from rag.embedding import embed_document | |
| from rag.retriever import build_vector_store | |
| print(f" {DIM}Loading files from: {root_path}{RESET}") | |
| docs = load_repository(root_path) | |
| if not docs: | |
| err("No supported files found. Check allowed_extensions in config.py.") | |
| return False | |
| ok(f"Loaded {len(docs)} files") | |
| all_chunks = [] | |
| for doc in docs: | |
| all_chunks.extend(split_code(doc)) | |
| ok(f"Split into {len(all_chunks)} chunks") | |
| print(f" {DIM}Embedding chunks (this may take a moment)...{RESET}") | |
| embedded = embed_document(all_chunks) | |
| ok(f"Embedded {len(embedded)} chunks") | |
| build_vector_store(embedded) | |
| ok("Vector store ready (ChromaDB)\n") | |
| _current_repo = root_path # remember for option 7 | |
| return True | |
| # ββ feature handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def feature_query(): | |
| section("Ask a Question [RAG Query]") | |
| query = ask("Your question:") | |
| if not query: | |
| return | |
| k_str = ask("Number of source chunks to retrieve? [default: 5]") | |
| k = int(k_str) if k_str.isdigit() else 5 | |
| from rag.rag_chain import run_rag_query | |
| print(f"\n {DIM}Thinking...{RESET}") | |
| try: | |
| result = run_rag_query(query, k=k) | |
| section("Answer") | |
| print(f"\n{result['answer']}") | |
| section("Sources") | |
| for s in result["sources"]: | |
| print(f" {DIM}β’ {s.get('file_path')} lines {s.get('start_line')}β{s.get('end_line')}{RESET}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_bugs(): | |
| section("Detect Bugs [LLM Code Review]") | |
| file_path = ask("File path to analyse:") | |
| if not os.path.isfile(file_path): | |
| err(f"File not found: {file_path}") | |
| return | |
| from services.code_analysis import detect_bugs | |
| print(f"\n {DIM}Analysing...{RESET}") | |
| try: | |
| bugs = detect_bugs(file_path) | |
| if not bugs: | |
| ok("No issues found.") | |
| return | |
| section(f"Found {len(bugs)} issue(s)") | |
| for b in bugs: | |
| sev = b.get("severity", "?").upper() | |
| color = RED if sev in ("HIGH", "CRITICAL") else YELLOW | |
| print(f"\n {color}[{sev}]{RESET} Line {b.get('line', '?')}: {b.get('issue', '')}") | |
| print(f" {DIM} β {b.get('suggestion', '')}{RESET}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_complexity(): | |
| section("Cyclomatic Complexity Analysis") | |
| file_path = ask("File path to analyse:") | |
| if not os.path.isfile(file_path): | |
| err(f"File not found: {file_path}") | |
| return | |
| from services.code_analysis import analyze_complexity | |
| try: | |
| result = analyze_complexity(file_path) | |
| fns = result["functions"] | |
| if not fns: | |
| info("No functions found (or file is not Python).") | |
| return | |
| section(f"{len(fns)} function(s)") | |
| for fn in fns: | |
| rank = fn["rank"] | |
| color = GREEN if rank == "A" else (YELLOW if rank in ("B", "C") else RED) | |
| bar = "β" * fn["complexity"] | |
| print(f" {color}[{rank}]{RESET} {fn['name']:<30} complexity={fn['complexity']} {DIM}{bar}{RESET}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_explain(): | |
| section("Explain a Function") | |
| file_path = ask("File path:") | |
| if not os.path.isfile(file_path): | |
| err(f"File not found: {file_path}") | |
| return | |
| func_name = ask("Function name:") | |
| if not func_name: | |
| return | |
| from services.code_analysis import explain_function | |
| print(f"\n {DIM}Thinking...{RESET}") | |
| try: | |
| explanation = explain_function(file_path, func_name) | |
| section("Explanation") | |
| print(f"\n{explanation}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_module_docs(): | |
| section("Generate Module Documentation") | |
| file_path = ask("File path:") | |
| if not os.path.isfile(file_path): | |
| err(f"File not found: {file_path}") | |
| return | |
| from services.documentation import generate_module_docs | |
| print(f"\n {DIM}Generating docs...{RESET}") | |
| try: | |
| docs = generate_module_docs(file_path) | |
| section("Documentation") | |
| print(f"\n{docs}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_readme(): | |
| section("Generate README.md") | |
| root = ask("Repository root path:") | |
| if not os.path.isdir(root): | |
| err(f"Directory not found: {root}") | |
| return | |
| from services.documentation import generate_readme | |
| print(f"\n {DIM}Generating README...{RESET}") | |
| try: | |
| readme = generate_readme(root) | |
| section("README Preview") | |
| print(f"\n{readme[:1500]}{'...' if len(readme) > 1500 else ''}") | |
| save = ask("Save to README.md in that directory? [y/N]") | |
| if save.lower() == "y": | |
| out = os.path.join(root, "README.md") | |
| with open(out, "w", encoding="utf-8") as f: | |
| f.write(readme) | |
| ok(f"Saved to {out}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_propose_file(): | |
| section("Propose a New File [AI File Creator]") | |
| description = ask("Describe the file you want to create:") | |
| if not description: | |
| return | |
| context_query = ask("Optional context query (or press Enter to skip):") | |
| from services.file_creator import propose_new_file | |
| print(f"\n {DIM}Generating...{RESET}") | |
| try: | |
| proposal = propose_new_file(description, context_query or None) | |
| section("Proposed File") | |
| print(f"\n {BOLD}Path:{RESET} {proposal.path}") | |
| print(f"\n{DIM}{'β'*50}{RESET}") | |
| print(proposal.content[:1000] + ("..." if len(proposal.content) > 1000 else "")) | |
| print(f"{DIM}{'β'*50}{RESET}") | |
| confirm = ask("Write this file to disk? [y/N]") | |
| if confirm.lower() == "y": | |
| default_dir = _current_repo | |
| typed = ask(f"Save under which directory? [default: {default_dir}]").strip() | |
| base_dir = typed if typed else default_dir | |
| if not os.path.isdir(base_dir): | |
| err(f"Directory not found: {base_dir}") | |
| return | |
| async def _write(): | |
| from services.file_creator import apply_approved_file | |
| proposal.approved = True | |
| return await apply_approved_file(proposal, True, base_dir=base_dir) | |
| result = asyncio.run(_write()) | |
| if result.get("status") == "written": | |
| ok(f"File written: {result.get('path', proposal.path)}") | |
| else: | |
| err(f"Write failed: {result}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_mcp_tools(): | |
| section("List GitHub MCP Tools") | |
| async def _list(): | |
| from mcp_client import get_github_mcp_client | |
| async with get_github_mcp_client() as client: | |
| return await client.list_tools() | |
| try: | |
| tools = asyncio.run(_list()) | |
| ok(f"{len(tools)} tools available:") | |
| for t in tools: | |
| print(f" {DIM}β’ {t['name']:<30}{RESET} {t.get('description','')[:60]}") | |
| except Exception as e: | |
| err(str(e)) | |
| def feature_reingest(): | |
| root = ask("New repository path to ingest:") | |
| if os.path.isdir(root): | |
| ingest_repository(root) | |
| else: | |
| err(f"Directory not found: {root}") | |
| # ββ menu ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MENU = [ | |
| ("Ask a question about the codebase", feature_query), | |
| ("Detect bugs in a file", feature_bugs), | |
| ("Cyclomatic complexity analysis", feature_complexity), | |
| ("Explain a function", feature_explain), | |
| ("Generate module documentation", feature_module_docs), | |
| ("Generate README for a repository", feature_readme), | |
| ("Propose & create a new file (AI)", feature_propose_file), | |
| ("List GitHub MCP tools", feature_mcp_tools), | |
| ("Re-ingest a repository", feature_reingest), | |
| ] | |
| def show_menu(): | |
| section("What would you like to do?") | |
| for i, (label, _) in enumerate(MENU, 1): | |
| print(f" {CYAN}{i:>2}{RESET} {label}") | |
| print(f" {DIM} 0 Exit{RESET}") | |
| # ββ main ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| # Enable ANSI colours on Windows | |
| os.system("") | |
| banner() | |
| # Step 1 β ask for repo path | |
| while True: | |
| root = ask("Enter the path to the repository you want to analyse:") | |
| if os.path.isdir(root): | |
| break | |
| err(f"Directory not found: {root}") | |
| # Step 2 β ingest | |
| if not ingest_repository(root): | |
| sys.exit(1) | |
| # Step 3 β interactive menu loop | |
| while True: | |
| show_menu() | |
| choice = ask("Choose an option [0β9]:") | |
| if choice == "0" or choice.lower() in ("exit", "quit", "q"): | |
| print(f"\n{DIM}Goodbye!{RESET}\n") | |
| break | |
| if choice.isdigit() and 1 <= int(choice) <= len(MENU): | |
| MENU[int(choice) - 1][1]() | |
| else: | |
| err("Invalid choice. Enter a number from the menu.") | |
| if __name__ == "__main__": | |
| main() | |