Phase 1: Permission modal dialog, session resume modal, HistoryInput with up/down arrow cycling, remove "You:" echo from chat log, LLM client cleanup on unmount. Phase 2: Smart shell auto-approve using allowed/denied command lists from ToolsConfig, animated thinking spinner with live token count in status bar. Phase 3: /models slash command (list + switch), CLI directory positional argument, JSONL debug logger with rotation. Phase 4: Skills system with SkillsManager, load_skill LLM tool, /skills listing, skill invocation via slash commands, system prompt integration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
3.5 KiB
Python
121 lines
3.5 KiB
Python
"""SneakyCode entrypoint — argument parsing, config loading, and TUI launch."""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from app.models.config import AppConfig, load_config
|
|
from app.services.llm import LLMClient, LLMConnectionError, LLMError
|
|
from app.services.session import SessionManager
|
|
from app.utils.display import print_banner, print_error, print_info, print_success
|
|
from app.utils.logging import get_logger, setup_logging
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
"""Parse command-line arguments."""
|
|
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",
|
|
)
|
|
parser.add_argument(
|
|
"directory",
|
|
nargs="?",
|
|
type=Path,
|
|
default=None,
|
|
help="Project directory to use as workspace root (default: current directory)",
|
|
)
|
|
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()
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entrypoint: load config, preflight check, launch Textual TUI."""
|
|
args = parse_args()
|
|
|
|
# Setup logging first (will be reconfigured for TUI on mount)
|
|
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)
|
|
|
|
# Override workspace root if directory argument provided
|
|
if args.directory:
|
|
target = Path(args.directory).resolve()
|
|
if not target.is_dir():
|
|
print_error(f"Not a directory: {target}")
|
|
sys.exit(1)
|
|
config.agent.workspace_root = target
|
|
|
|
logger.info("config_loaded", model=config.llm.model, endpoint=config.llm.endpoint)
|
|
|
|
# Pre-TUI startup info (printed to console before Textual takes over)
|
|
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 manager
|
|
session_mgr = SessionManager(config.session, config.agent.workspace_root, config.llm.model)
|
|
|
|
# Clean up old session files
|
|
cleaned = session_mgr.cleanup_old()
|
|
if cleaned > 0:
|
|
logger.info("old_sessions_cleaned", count=cleaned)
|
|
|
|
# Launch Textual TUI
|
|
from app.ui.app import SneakyCodeApp
|
|
|
|
app = SneakyCodeApp(config, session_mgr=session_mgr)
|
|
app.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|