CRITICAL UX FIXES for beginners: Model Display Issues Fixed: - TUI now shows ACTUAL configured model, not hardcoded model - CLI status command shows configured vs actual model with mismatch warnings - Both TUI and CLI use identical model selection logic (no more inconsistency) Config File Visibility Improved: - Config file location prominently displayed in TUI configuration menu - CLI status shows exact config file path (.mini-rag/config.yaml) - Added clear documentation in config file header about model settings - Users can now easily find and edit YAML file for direct configuration User Trust Restored: - ✅ Shows 'Using configured: qwen3:1.7b' when config matches reality - ⚠️ Shows 'Model mismatch!' when config differs from actual - Config changes now immediately visible in status displays No more 'I changed the config but nothing happened' confusion!
694 lines
28 KiB
Python
694 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
rag-mini - FSS-Mini-RAG Command Line Interface
|
|
|
|
A lightweight, portable RAG system for semantic code search.
|
|
Usage: rag-mini <command> <project_path> [options]
|
|
"""
|
|
|
|
import sys
|
|
import argparse
|
|
from pathlib import Path
|
|
import json
|
|
import logging
|
|
|
|
# Add the RAG system to the path
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
try:
|
|
from mini_rag.indexer import ProjectIndexer
|
|
from mini_rag.search import CodeSearcher
|
|
from mini_rag.ollama_embeddings import OllamaEmbedder
|
|
from mini_rag.llm_synthesizer import LLMSynthesizer
|
|
from mini_rag.explorer import CodeExplorer
|
|
# Update system (graceful import)
|
|
try:
|
|
from mini_rag.updater import check_for_updates, get_updater
|
|
UPDATER_AVAILABLE = True
|
|
except ImportError:
|
|
UPDATER_AVAILABLE = False
|
|
except ImportError as e:
|
|
print("❌ Error: Missing dependencies!")
|
|
print()
|
|
print("It looks like you haven't installed the required packages yet.")
|
|
print("This is a common mistake - here's how to fix it:")
|
|
print()
|
|
print("1. Make sure you're in the FSS-Mini-RAG directory")
|
|
print("2. Run the installer script:")
|
|
print(" ./install_mini_rag.sh")
|
|
print()
|
|
print("Or if you want to install manually:")
|
|
print(" python3 -m venv .venv")
|
|
print(" source .venv/bin/activate")
|
|
print(" pip install -r requirements.txt")
|
|
print()
|
|
print(f"Missing module: {e.name}")
|
|
sys.exit(1)
|
|
|
|
# Configure logging for user-friendly output
|
|
logging.basicConfig(
|
|
level=logging.WARNING, # Only show warnings and errors by default
|
|
format='%(levelname)s: %(message)s'
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def index_project(project_path: Path, force: bool = False):
|
|
"""Index a project directory."""
|
|
try:
|
|
# Show what's happening
|
|
action = "Re-indexing" if force else "Indexing"
|
|
print(f"🚀 {action} {project_path.name}")
|
|
|
|
# Quick pre-check
|
|
rag_dir = project_path / '.mini-rag'
|
|
if rag_dir.exists() and not force:
|
|
print(" Checking for changes...")
|
|
|
|
indexer = ProjectIndexer(project_path)
|
|
result = indexer.index_project(force_reindex=force)
|
|
|
|
# Show results with context
|
|
files_count = result.get('files_indexed', 0)
|
|
chunks_count = result.get('chunks_created', 0)
|
|
time_taken = result.get('time_taken', 0)
|
|
|
|
if files_count == 0:
|
|
print("✅ Index up to date - no changes detected")
|
|
else:
|
|
print(f"✅ Indexed {files_count} files in {time_taken:.1f}s")
|
|
print(f" Created {chunks_count} chunks")
|
|
|
|
# Show efficiency
|
|
if time_taken > 0:
|
|
speed = files_count / time_taken
|
|
print(f" Speed: {speed:.1f} files/sec")
|
|
|
|
# Show warnings if any
|
|
failed_count = result.get('files_failed', 0)
|
|
if failed_count > 0:
|
|
print(f"⚠️ {failed_count} files failed (check logs with --verbose)")
|
|
|
|
# Quick tip for first-time users
|
|
if not (project_path / '.mini-rag' / 'last_search').exists():
|
|
print(f"\n💡 Try: rag-mini search {project_path} \"your search here\"")
|
|
|
|
except FileNotFoundError:
|
|
print(f"📁 Directory Not Found: {project_path}")
|
|
print(" Make sure the path exists and you're in the right location")
|
|
print(f" Current directory: {Path.cwd()}")
|
|
print(" Check path: ls -la /path/to/your/project")
|
|
print()
|
|
sys.exit(1)
|
|
except PermissionError:
|
|
print("🔒 Permission Denied")
|
|
print(" FSS-Mini-RAG needs to read files and create index database")
|
|
print(f" Check permissions: ls -la {project_path}")
|
|
print(" Try a different location with write access")
|
|
print()
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
# Connection errors are handled in the embedding module
|
|
if "ollama" in str(e).lower() or "connection" in str(e).lower():
|
|
sys.exit(1) # Error already displayed
|
|
|
|
print(f"❌ Indexing failed: {e}")
|
|
print()
|
|
print("🔧 Common solutions:")
|
|
print(" • Check if path exists and you have read permissions")
|
|
print(" • Ensure Python dependencies are installed: pip install -r requirements.txt")
|
|
print(" • Try with smaller project first to test setup")
|
|
print(" • Check available disk space for index files")
|
|
print()
|
|
print("📚 For detailed help:")
|
|
print(f" ./rag-mini index {project_path} --verbose")
|
|
print(" Or see: docs/TROUBLESHOOTING.md")
|
|
sys.exit(1)
|
|
|
|
def search_project(project_path: Path, query: str, top_k: int = 10, synthesize: bool = False):
|
|
"""Search a project directory."""
|
|
try:
|
|
# Check if indexed first
|
|
rag_dir = project_path / '.mini-rag'
|
|
if not rag_dir.exists():
|
|
print(f"❌ Project not indexed: {project_path.name}")
|
|
print(f" Run: rag-mini index {project_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"🔍 Searching \"{query}\" in {project_path.name}")
|
|
searcher = CodeSearcher(project_path)
|
|
results = searcher.search(query, top_k=top_k)
|
|
|
|
if not results:
|
|
print("❌ No results found")
|
|
print()
|
|
print("🔧 Quick fixes to try:")
|
|
print(" • Use broader terms: \"login\" instead of \"authenticate_user_session\"")
|
|
print(" • Try concepts: \"database query\" instead of specific function names")
|
|
print(" • Check spelling and try simpler words")
|
|
print(" • Search for file types: \"python class\" or \"javascript function\"")
|
|
print()
|
|
print("⚙️ Configuration adjustments:")
|
|
print(f" • Lower threshold: ./rag-mini search \"{project_path}\" \"{query}\" --threshold 0.05")
|
|
print(f" • More results: ./rag-mini search \"{project_path}\" \"{query}\" --top-k 20")
|
|
print()
|
|
print("📚 Need help? See: docs/TROUBLESHOOTING.md")
|
|
return
|
|
|
|
print(f"✅ Found {len(results)} results:")
|
|
print()
|
|
|
|
for i, result in enumerate(results, 1):
|
|
# Clean up file path display
|
|
file_path = Path(result.file_path)
|
|
try:
|
|
rel_path = file_path.relative_to(project_path)
|
|
except ValueError:
|
|
# If relative_to fails, just show the basename
|
|
rel_path = file_path.name
|
|
|
|
print(f"{i}. {rel_path}")
|
|
print(f" Score: {result.score:.3f}")
|
|
|
|
# Show line info if available
|
|
if hasattr(result, 'start_line') and result.start_line:
|
|
print(f" Lines: {result.start_line}-{result.end_line}")
|
|
|
|
# Show content preview
|
|
if hasattr(result, 'name') and result.name:
|
|
print(f" Context: {result.name}")
|
|
|
|
# Show full content with proper formatting
|
|
print(f" Content:")
|
|
content_lines = result.content.strip().split('\n')
|
|
for line in content_lines[:10]: # Show up to 10 lines
|
|
print(f" {line}")
|
|
|
|
if len(content_lines) > 10:
|
|
print(f" ... ({len(content_lines) - 10} more lines)")
|
|
print(f" Use --verbose or rag-mini-enhanced for full context")
|
|
|
|
print()
|
|
|
|
# LLM Synthesis if requested
|
|
if synthesize:
|
|
print("🧠 Generating LLM synthesis...")
|
|
|
|
# Load config to respect user's model preferences
|
|
from mini_rag.config import ConfigManager
|
|
config_manager = ConfigManager(project_path)
|
|
config = config_manager.load_config()
|
|
|
|
synthesizer = LLMSynthesizer(
|
|
model=config.llm.synthesis_model if config.llm.synthesis_model != "auto" else None,
|
|
config=config
|
|
)
|
|
|
|
if synthesizer.is_available():
|
|
synthesis = synthesizer.synthesize_search_results(query, results, project_path)
|
|
print()
|
|
print(synthesizer.format_synthesis_output(synthesis, query))
|
|
|
|
# Add guidance for deeper analysis
|
|
if synthesis.confidence < 0.7 or any(word in query.lower() for word in ['why', 'how', 'explain', 'debug']):
|
|
print("\n💡 Want deeper analysis with reasoning?")
|
|
print(f" Try: rag-mini explore {project_path}")
|
|
print(" Exploration mode enables thinking and remembers conversation context.")
|
|
else:
|
|
print("❌ LLM synthesis unavailable")
|
|
print(" • Ensure Ollama is running: ollama serve")
|
|
print(" • Install a model: ollama pull qwen3:1.7b")
|
|
print(" • Check connection to http://localhost:11434")
|
|
|
|
# Save last search for potential enhancements
|
|
try:
|
|
(rag_dir / 'last_search').write_text(query)
|
|
except:
|
|
pass # Don't fail if we can't save
|
|
|
|
except Exception as e:
|
|
print(f"❌ Search failed: {e}")
|
|
print()
|
|
|
|
if "not indexed" in str(e).lower():
|
|
print("🔧 Solution:")
|
|
print(f" ./rag-mini index {project_path}")
|
|
print()
|
|
else:
|
|
print("🔧 Common solutions:")
|
|
print(" • Check project path exists and is readable")
|
|
print(" • Verify index isn't corrupted: delete .mini-rag/ and re-index")
|
|
print(" • Try with a different project to test setup")
|
|
print(" • Check available memory and disk space")
|
|
print()
|
|
print("📚 Get detailed error info:")
|
|
print(f" ./rag-mini search {project_path} \"{query}\" --verbose")
|
|
print(" Or see: docs/TROUBLESHOOTING.md")
|
|
print()
|
|
sys.exit(1)
|
|
|
|
def status_check(project_path: Path):
|
|
"""Show status of RAG system."""
|
|
try:
|
|
print(f"📊 Status for {project_path.name}")
|
|
print()
|
|
|
|
# Check project indexing status first
|
|
rag_dir = project_path / '.mini-rag'
|
|
if not rag_dir.exists():
|
|
print("❌ Project not indexed")
|
|
print(f" Run: rag-mini index {project_path}")
|
|
print()
|
|
else:
|
|
manifest = rag_dir / 'manifest.json'
|
|
if manifest.exists():
|
|
try:
|
|
with open(manifest) as f:
|
|
data = json.load(f)
|
|
|
|
file_count = data.get('file_count', 0)
|
|
chunk_count = data.get('chunk_count', 0)
|
|
indexed_at = data.get('indexed_at', 'Never')
|
|
|
|
print("✅ Project indexed")
|
|
print(f" Files: {file_count}")
|
|
print(f" Chunks: {chunk_count}")
|
|
print(f" Last update: {indexed_at}")
|
|
|
|
# Show average chunks per file
|
|
if file_count > 0:
|
|
avg_chunks = chunk_count / file_count
|
|
print(f" Avg chunks/file: {avg_chunks:.1f}")
|
|
|
|
print()
|
|
except Exception:
|
|
print("⚠️ Index exists but manifest unreadable")
|
|
print()
|
|
else:
|
|
print("⚠️ Index directory exists but incomplete")
|
|
print(f" Try: rag-mini index {project_path} --force")
|
|
print()
|
|
|
|
# Check embedding system status
|
|
print("🧠 Embedding System:")
|
|
try:
|
|
embedder = OllamaEmbedder()
|
|
emb_info = embedder.get_status()
|
|
method = emb_info.get('method', 'unknown')
|
|
|
|
if method == 'ollama':
|
|
print(" ✅ Ollama (high quality)")
|
|
elif method == 'ml':
|
|
print(" ✅ ML fallback (good quality)")
|
|
elif method == 'hash':
|
|
print(" ⚠️ Hash fallback (basic quality)")
|
|
else:
|
|
print(f" ❓ Unknown method: {method}")
|
|
|
|
# Show additional details if available
|
|
if 'model' in emb_info:
|
|
print(f" Model: {emb_info['model']}")
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Status check failed: {e}")
|
|
|
|
print()
|
|
|
|
# Check LLM status and show actual vs configured model
|
|
print("🤖 LLM System:")
|
|
try:
|
|
from mini_rag.config import ConfigManager
|
|
config_manager = ConfigManager(project_path)
|
|
config = config_manager.load_config()
|
|
|
|
synthesizer = LLMSynthesizer(
|
|
model=config.llm.synthesis_model if config.llm.synthesis_model != "auto" else None,
|
|
config=config
|
|
)
|
|
|
|
if synthesizer.is_available():
|
|
synthesizer._ensure_initialized()
|
|
actual_model = synthesizer.model
|
|
config_model = config.llm.synthesis_model
|
|
|
|
if config_model == "auto":
|
|
print(f" ✅ Auto-selected: {actual_model}")
|
|
elif config_model == actual_model:
|
|
print(f" ✅ Using configured: {actual_model}")
|
|
else:
|
|
print(f" ⚠️ Model mismatch!")
|
|
print(f" Configured: {config_model}")
|
|
print(f" Actually using: {actual_model}")
|
|
print(f" (Configured model may not be installed)")
|
|
|
|
print(f" Config file: {config_manager.config_path}")
|
|
else:
|
|
print(" ❌ Ollama not available")
|
|
print(" Start with: ollama serve")
|
|
|
|
except Exception as e:
|
|
print(f" ❌ LLM status check failed: {e}")
|
|
|
|
# Show last search if available
|
|
last_search_file = rag_dir / 'last_search' if rag_dir.exists() else None
|
|
if last_search_file and last_search_file.exists():
|
|
try:
|
|
last_query = last_search_file.read_text().strip()
|
|
print(f"\n🔍 Last search: \"{last_query}\"")
|
|
except:
|
|
pass
|
|
|
|
except Exception as e:
|
|
print(f"❌ Status check failed: {e}")
|
|
sys.exit(1)
|
|
|
|
def explore_interactive(project_path: Path):
|
|
"""Interactive exploration mode with thinking and context memory for any documents."""
|
|
try:
|
|
explorer = CodeExplorer(project_path)
|
|
|
|
if not explorer.start_exploration_session():
|
|
sys.exit(1)
|
|
|
|
# Show enhanced first-time guidance
|
|
print(f"\n🤔 Ask your first question about {project_path.name}:")
|
|
print()
|
|
print("💡 Enter your search query or question below:")
|
|
print(' Examples: "How does authentication work?" or "Show me error handling"')
|
|
print()
|
|
print("🔧 Quick options:")
|
|
print(" 1. Help - Show example questions")
|
|
print(" 2. Status - Project information")
|
|
print(" 3. Suggest - Get a random starter question")
|
|
print()
|
|
|
|
is_first_question = True
|
|
|
|
while True:
|
|
try:
|
|
# Get user input with clearer prompt
|
|
if is_first_question:
|
|
question = input("📝 Enter question or option (1-3): ").strip()
|
|
else:
|
|
question = input("\n> ").strip()
|
|
|
|
# Handle exit commands
|
|
if question.lower() in ['quit', 'exit', 'q']:
|
|
print("\n" + explorer.end_session())
|
|
break
|
|
|
|
# Handle empty input
|
|
if not question:
|
|
if is_first_question:
|
|
print("Please enter a question or try option 3 for a suggestion.")
|
|
else:
|
|
print("Please enter a question or 'quit' to exit.")
|
|
continue
|
|
|
|
# Handle numbered options and special commands
|
|
if question in ['1'] or question.lower() in ['help', 'h']:
|
|
print("""
|
|
🧠 EXPLORATION MODE HELP:
|
|
• Ask any question about your documents or code
|
|
• I remember our conversation for follow-up questions
|
|
• Use 'why', 'how', 'explain' for detailed reasoning
|
|
• Type 'summary' to see session overview
|
|
• Type 'quit' or 'exit' to end session
|
|
|
|
💡 Example questions:
|
|
• "How does authentication work?"
|
|
• "What are the main components?"
|
|
• "Show me error handling patterns"
|
|
• "Why is this function slow?"
|
|
• "What security measures are in place?"
|
|
• "How does data flow through this system?"
|
|
""")
|
|
continue
|
|
|
|
elif question in ['2'] or question.lower() == 'status':
|
|
print(f"""
|
|
📊 PROJECT STATUS: {project_path.name}
|
|
• Location: {project_path}
|
|
• Exploration session active
|
|
• AI model ready for questions
|
|
• Conversation memory enabled
|
|
""")
|
|
continue
|
|
|
|
elif question in ['3'] or question.lower() == 'suggest':
|
|
# Random starter questions for first-time users
|
|
if is_first_question:
|
|
import random
|
|
starters = [
|
|
"What are the main components of this project?",
|
|
"How is error handling implemented?",
|
|
"Show me the authentication and security logic",
|
|
"What are the key functions I should understand first?",
|
|
"How does data flow through this system?",
|
|
"What configuration options are available?",
|
|
"Show me the most important files to understand"
|
|
]
|
|
suggested = random.choice(starters)
|
|
print(f"\n💡 Suggested question: {suggested}")
|
|
print(" Press Enter to use this, or type your own question:")
|
|
|
|
next_input = input("📝 > ").strip()
|
|
if not next_input: # User pressed Enter to use suggestion
|
|
question = suggested
|
|
else:
|
|
question = next_input
|
|
else:
|
|
# For subsequent questions, could add AI-powered suggestions here
|
|
print("\n💡 Based on our conversation, you might want to ask:")
|
|
print(' "Can you explain that in more detail?"')
|
|
print(' "What are the security implications?"')
|
|
print(' "Show me related code examples"')
|
|
continue
|
|
|
|
if question.lower() == 'summary':
|
|
print("\n" + explorer.get_session_summary())
|
|
continue
|
|
|
|
# Process the question
|
|
print(f"\n🔍 Searching {project_path.name}...")
|
|
print("🧠 Thinking with AI model...")
|
|
response = explorer.explore_question(question)
|
|
|
|
# Mark as no longer first question after processing
|
|
is_first_question = False
|
|
|
|
if response:
|
|
print(f"\n{response}")
|
|
else:
|
|
print("❌ Sorry, I couldn't process that question. Please try again.")
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\n\n{explorer.end_session()}")
|
|
break
|
|
except EOFError:
|
|
print(f"\n\n{explorer.end_session()}")
|
|
break
|
|
except Exception as e:
|
|
print(f"❌ Error processing question: {e}")
|
|
print("Please try again or type 'quit' to exit.")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Failed to start exploration mode: {e}")
|
|
print("Make sure the project is indexed first: rag-mini index <project>")
|
|
sys.exit(1)
|
|
|
|
def show_discrete_update_notice():
|
|
"""Show a discrete, non-intrusive update notice for CLI users."""
|
|
if not UPDATER_AVAILABLE:
|
|
return
|
|
|
|
try:
|
|
update_info = check_for_updates()
|
|
if update_info:
|
|
# Very discrete notice - just one line
|
|
print(f"🔄 (Update v{update_info.version} available - run 'rag-mini check-update' to learn more)")
|
|
except Exception:
|
|
# Silently ignore any update check failures
|
|
pass
|
|
|
|
def handle_check_update():
|
|
"""Handle the check-update command."""
|
|
if not UPDATER_AVAILABLE:
|
|
print("❌ Update system not available")
|
|
print("💡 Try updating to the latest version manually from GitHub")
|
|
return
|
|
|
|
try:
|
|
print("🔍 Checking for updates...")
|
|
update_info = check_for_updates()
|
|
|
|
if update_info:
|
|
print(f"\n🎉 Update Available: v{update_info.version}")
|
|
print("=" * 50)
|
|
print("\n📋 What's New:")
|
|
notes_lines = update_info.release_notes.split('\n')[:10] # First 10 lines
|
|
for line in notes_lines:
|
|
if line.strip():
|
|
print(f" {line.strip()}")
|
|
|
|
print(f"\n🔗 Release Page: {update_info.release_url}")
|
|
print(f"\n🚀 To install: rag-mini update")
|
|
print("💡 Or update manually from GitHub releases")
|
|
else:
|
|
print("✅ You're already on the latest version!")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Failed to check for updates: {e}")
|
|
print("💡 Try updating manually from GitHub")
|
|
|
|
def handle_update():
|
|
"""Handle the update command."""
|
|
if not UPDATER_AVAILABLE:
|
|
print("❌ Update system not available")
|
|
print("💡 Try updating manually from GitHub")
|
|
return
|
|
|
|
try:
|
|
print("🔍 Checking for updates...")
|
|
update_info = check_for_updates()
|
|
|
|
if not update_info:
|
|
print("✅ You're already on the latest version!")
|
|
return
|
|
|
|
print(f"\n🎉 Update Available: v{update_info.version}")
|
|
print("=" * 50)
|
|
|
|
# Show brief release notes
|
|
notes_lines = update_info.release_notes.split('\n')[:5]
|
|
for line in notes_lines:
|
|
if line.strip():
|
|
print(f" • {line.strip()}")
|
|
|
|
# Confirm update
|
|
confirm = input(f"\n🚀 Install v{update_info.version}? [Y/n]: ").strip().lower()
|
|
if confirm in ['', 'y', 'yes']:
|
|
updater = get_updater()
|
|
|
|
print(f"\n📥 Downloading v{update_info.version}...")
|
|
|
|
# Progress callback
|
|
def show_progress(downloaded, total):
|
|
if total > 0:
|
|
percent = (downloaded / total) * 100
|
|
bar_length = 30
|
|
filled = int(bar_length * downloaded / total)
|
|
bar = "█" * filled + "░" * (bar_length - filled)
|
|
print(f"\r [{bar}] {percent:.1f}%", end="", flush=True)
|
|
|
|
# Download and install
|
|
update_package = updater.download_update(update_info, show_progress)
|
|
if not update_package:
|
|
print("\n❌ Download failed. Please try again later.")
|
|
return
|
|
|
|
print("\n💾 Creating backup...")
|
|
if not updater.create_backup():
|
|
print("⚠️ Backup failed, but continuing anyway...")
|
|
|
|
print("🔄 Installing update...")
|
|
if updater.apply_update(update_package, update_info):
|
|
print("✅ Update successful!")
|
|
print("🚀 Restarting...")
|
|
updater.restart_application()
|
|
else:
|
|
print("❌ Update failed.")
|
|
print("🔙 Attempting rollback...")
|
|
if updater.rollback_update():
|
|
print("✅ Rollback successful.")
|
|
else:
|
|
print("❌ Rollback failed. You may need to reinstall.")
|
|
else:
|
|
print("Update cancelled.")
|
|
|
|
except Exception as e:
|
|
print(f"❌ Update failed: {e}")
|
|
print("💡 Try updating manually from GitHub")
|
|
|
|
def main():
|
|
"""Main CLI interface."""
|
|
# Check virtual environment
|
|
try:
|
|
from mini_rag.venv_checker import check_and_warn_venv
|
|
check_and_warn_venv("rag-mini.py", force_exit=False)
|
|
except ImportError:
|
|
pass # If venv checker can't be imported, continue anyway
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="FSS-Mini-RAG - Lightweight semantic code search",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
rag-mini index /path/to/project # Index a project
|
|
rag-mini search /path/to/project "query" # Search indexed project
|
|
rag-mini search /path/to/project "query" -s # Search with LLM synthesis
|
|
rag-mini explore /path/to/project # Interactive exploration mode
|
|
rag-mini status /path/to/project # Show status
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('command', choices=['index', 'search', 'explore', 'status', 'update', 'check-update'],
|
|
help='Command to execute')
|
|
parser.add_argument('project_path', type=Path, nargs='?',
|
|
help='Path to project directory (REQUIRED except for update commands)')
|
|
parser.add_argument('query', nargs='?',
|
|
help='Search query (for search command)')
|
|
parser.add_argument('--force', action='store_true',
|
|
help='Force reindex all files')
|
|
parser.add_argument('--top-k', '--limit', type=int, default=10, dest='top_k',
|
|
help='Maximum number of search results (top-k)')
|
|
parser.add_argument('--verbose', '-v', action='store_true',
|
|
help='Enable verbose logging')
|
|
parser.add_argument('--synthesize', '-s', action='store_true',
|
|
help='Generate LLM synthesis of search results (requires Ollama)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Set logging level
|
|
if args.verbose:
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
|
|
# Handle update commands first (don't require project_path)
|
|
if args.command == 'check-update':
|
|
handle_check_update()
|
|
return
|
|
elif args.command == 'update':
|
|
handle_update()
|
|
return
|
|
|
|
# All other commands require project_path
|
|
if not args.project_path:
|
|
print("❌ Project path required for this command")
|
|
sys.exit(1)
|
|
|
|
# Validate project path
|
|
if not args.project_path.exists():
|
|
print(f"❌ Project path does not exist: {args.project_path}")
|
|
sys.exit(1)
|
|
|
|
if not args.project_path.is_dir():
|
|
print(f"❌ Project path is not a directory: {args.project_path}")
|
|
sys.exit(1)
|
|
|
|
# Show discrete update notification for regular commands (non-intrusive)
|
|
show_discrete_update_notice()
|
|
|
|
# Execute command
|
|
if args.command == 'index':
|
|
index_project(args.project_path, args.force)
|
|
elif args.command == 'search':
|
|
if not args.query:
|
|
print("❌ Search query required")
|
|
sys.exit(1)
|
|
search_project(args.project_path, args.query, args.top_k, args.synthesize)
|
|
elif args.command == 'explore':
|
|
explore_interactive(args.project_path)
|
|
elif args.command == 'status':
|
|
status_check(args.project_path)
|
|
|
|
if __name__ == '__main__':
|
|
main() |