"""SneakyCode entrypoint — argument parsing, config loading, and interactive REPL.""" import argparse import asyncio import sys from pathlib import Path import structlog from app.agent.context import SessionContext from app.agent.loop import AgentLoop from app.models.config import AppConfig, load_config from app.services.llm import LLMClient, LLMConnectionError, LLMError from app.services.permissions import PermissionsService from app.services.streaming import StreamHandler from app.tools.registry import create_default_registry from app.utils.display import ( print_banner, print_error, print_history, print_info, print_success, print_user_message, print_warning, ) from app.utils.logging import console, get_logger, setup_logging def parse_args() -> argparse.Namespace: """Parse command-line arguments. Returns: Parsed arguments namespace. """ parser = argparse.ArgumentParser( prog="sneakycode", description="SneakyCode — A privacy-first local AI coding agent", ) parser.add_argument( "--config", type=Path, default=None, help="Path to config YAML file (default: config/config.yaml)", ) parser.add_argument( "-v", "--verbose", action="store_true", default=False, help="Enable verbose (DEBUG) logging", ) parser.add_argument( "--log-file", type=Path, default=None, help="Path to log file for persistent logging", ) return parser.parse_args() async def _preflight(config: AppConfig) -> None: """Check that Ollama is reachable and the configured model is available.""" async with LLMClient(config.llm) as client: await client.preflight_check() async def _run_repl( ctx: SessionContext, config: AppConfig, logger: structlog.stdlib.BoundLogger, ) -> None: """Run the interactive REPL loop with streaming LLM responses. Args: ctx: Session context for conversation state. config: Application configuration. logger: Structured logger instance. """ registry = create_default_registry(config.agent.workspace_root, config) permissions = PermissionsService(config.permissions) async with LLMClient(config.llm) as client: handler = StreamHandler(config.display) agent = AgentLoop(config, ctx, client, handler, registry, permissions) while True: try: user_input = console.input("[bold cyan]> [/bold cyan]") except (KeyboardInterrupt, EOFError): console.print("\n[dim]Goodbye![/dim]") break user_input = user_input.strip() if not user_input: continue # Handle slash commands if user_input.startswith("/"): command = user_input.lower() if command == "/quit": console.print("[dim]Goodbye![/dim]") break elif command == "/history": print_history(ctx.get_history()) elif command == "/clear": ctx.clear_history() print_success("Conversation history cleared.") else: print_warning(f"Unknown command: {user_input}") continue print_user_message(user_input) await agent.run_turn(user_input) logger.debug("turn_complete", message_count=ctx.message_count) def main() -> None: """Main entrypoint: load config, setup logging, launch interactive REPL.""" args = parse_args() # Setup logging first setup_logging( log_file=args.log_file, verbose=args.verbose, ) logger = get_logger(__name__) # Load configuration try: config = load_config(config_path=args.config) except (FileNotFoundError, ValueError) as e: print_error(f"Configuration error: {e}") sys.exit(1) logger.info("config_loaded", model=config.llm.model, endpoint=config.llm.endpoint) # Print startup info print_banner() print_info(f"Model: {config.llm.model}") print_info(f"Endpoint: {config.llm.endpoint}") print_info(f"Workspace: {config.agent.workspace_root}") if args.verbose: print_info("Verbose mode enabled") # Preflight: check Ollama is reachable and model exists try: asyncio.run(_preflight(config)) except LLMConnectionError as e: print_error(str(e)) sys.exit(1) except LLMError as e: print_error(str(e)) sys.exit(1) print_success("Ollama connected, model ready.") # Create session and start REPL ctx = SessionContext(config) logger.info("startup_complete") print_info("Commands: /quit, /history, /clear") asyncio.run(_run_repl(ctx, config, logger)) if __name__ == "__main__": main()