Loki CLI - Complete Project Documentation
AI-Powered Code Analysis CLI with Error Detection, RAG Chat, and Web UI
What is Loki?
Loki CLI is a zero-cost, local-only AI code analysis tool. It scans entire multi-language codebases, detects errors using language-specific linters and AST parsing, builds a FAISS vector index for RAG-powered chat, captures runtime errors from any process, and provides AI-powered explanations and auto-fixes. Everything runs on the user's machine with encrypted cache and OS keychain API key storage.
Quick Start
# Install
pip install -e D:\pro-2
# Set up AI provider (Groq - free)
loki models groq
# Paste your API key when prompted
# Scan your project
cd my-project
loki init
# See errors
loki errors
# Fix errors with AI
loki fix
# Chat with AI about your code
loki ai
# Capture runtime errors from any process
loki capture python my_script.py
# Open web UI
loki show
# Clean up when done
loki exit
System Architecture
Module Diagram
loki-cli/
├── loki/
│ ├── cli.py # Click CLI entry point
│ ├── core/
│ │ ├── types.py # Data structures (enums, dataclasses)
│ │ ├── scanner.py # File scanner for ALL languages
│ │ ├── errors.py # Multi-language error detection
│ │ ├── cache.py # Encrypted cache manager
│ │ ├── config.py # User config (~/.loki/config.json)
│ │ ├── runtime_capture.py # Runtime error capture hooks
│ │ └── global_hook.py # Global exception hook
│ ├── security/
│ │ ├── secret_manager.py # OS keychain API key storage
│ │ ├── cache_security.py # Fernet encryption for cache
│ │ ├── secure_delete.py # 3-pass secure file deletion
│ │ ├── leak_prevention.py # API key/token redaction
│ │ └── integrity.py # Package integrity verification
│ ├── ai/
│ │ ├── rag.py # FAISS + sentence-transformers RAG
│ │ ├── chat.py # Chat session with context injection
│ │ ├── guardrails.py # Prompt injection + leak prevention
│ │ └── providers/
│ │ ├── base.py # Abstract AI provider interface
│ │ ├── groq.py # Groq API (free tier)
│ │ ├── openai_provider.py # OpenAI API
│ │ ├── anthropic.py # Anthropic API
│ │ └── openrouter.py # OpenRouter API
│ ├── commands/
│ │ ├── init_cmd.py # loki init
│ │ ├── errors_cmd.py # loki errors
│ │ ├── describe_cmd.py # loki describe
│ │ ├── ai_cmd.py # loki ai
│ │ ├── exit_cmd.py # loki exit
│ │ ├── fix_cmd.py # loki fix
│ │ ├── watch_cmd.py # loki watch
│ │ ├── report_cmd.py # loki report
│ │ ├── models_cmd.py # loki models
│ │ ├── capture_cmd.py # loki capture
│ │ └── inject_cmd.py # loki inject
│ └── ui/
│ ├── server.py # FastAPI web server
│ ├── routes.py # API endpoints
│ ├── security.py # Rate limiter + input sanitizer
│ └── static/
│ ├── index.html # Web UI HTML
│ ├── style.css # Dark theme CSS
│ └── app.js # Frontend JavaScript
├── tests/ # Test suite
├── .github/workflows/ # CI/CD pipelines
├── pyproject.toml # Package config
└── res.index.html # This documentation
Data Flow
User runs `loki init`
│
▼
┌─────────────────┐
│ FileScanner │ Walks directory, identifies 18+ languages
│ (scanner.py) │ Computes SHA256 hashes per file
└────────┬────────┘
│ ScanResult (files[], structure{})
▼
┌─────────────────┐
│ ErrorDetector │ AST parsing (Python), subprocess calls
│ (errors.py) │ to node/rustc/gcc/javac/go + linters
└────────┬────────┘
│ Error[] (file, line, severity, code, message)
▼
┌─────────────────┐
│ RAGEngine │ Chunks code → sentence-transformers → FAISS
│ (rag.py) │ Saves index.faiss + chunks.json to cache
└────────┬────────┘
│ Vector index built
▼
┌─────────────────┐
│ CacheManager │ Encrypts with Fernet → ~/.loki/{hash}/
│ (cache.py) │ API keys in OS keychain via keyring
└─────────────────┘
│
▼
User runs `loki ai` or opens `loki show`
│
▼
┌─────────────────┐
│ ChatSession │ Query → FAISS search → relevant chunks
│ (chat.py) │ + error context → AI provider → response
└─────────────────┘
CLI Commands - Detailed
loki init init_cmd.py
What it does: Scans the entire codebase, detects errors across all languages, loads runtime errors, builds the FAISS RAG index, and saves everything to encrypted cache.
Functions used:
FileScanner.scan()- Walks directory tree, identifies files by extension (35 extensions across 18 languages), computes SHA256 hashes, returns ScanResultErrorDetector.detect_all()- Runs AST parsing + subprocess calls to linters (pylint, mypy, eslint, flake8) and compilers (node, rustc, gcc, javac, go vet)get_runtime_errors()- Loads previously captured runtime errors from runtime_errors.jsonRAGEngine.chunk_code()- Splits code into 50-line chunks with 10-line overlapRAGEngine.build_index()- Encodes chunks with sentence-transformers, builds FAISS IndexFlatL2, saves to diskCacheManager.save_scan()- Serializes ScanResult to JSON, encrypts with Fernet, saves to ~/.loki/{hash}/CacheManager.save_errors()- Serializes Error list to JSON, encrypts, saves
Why each step: Scanning identifies what code exists. Error detection finds bugs. RAG indexing enables AI to understand the code. Encryption protects the cache.
loki errors errors_cmd.py
What it does: Displays all cached errors in a formatted Rich table with severity, file location, line number, error code, message, and source linter.
Functions used:
CacheManager.load_errors()- Loads and decrypts errors.json, deserializes into Error objects_get_severity()- Extracts severity string from both dict and enum objects (handles cache serialization)_get_attr()- Safely gets attributes from both dict and dataclass objects
Bug fix: Rich markup error was caused by f"[{style}]" when style was empty string. Fixed by defaulting to "white" when severity is unrecognized.
loki describe describe_cmd.py
What it does: For each error, sends it to the AI provider for detailed explanation including: what the error means, why it occurs, and how to fix it. Renders output as Markdown.
Functions used:
AIProvider.chat()- Sends error details to AI with system promptRich Markdown()- Renders AI response as formatted Markdown
loki ai ai_cmd.py
What it does: Interactive chat session with the AI. AI has full context of your code (via RAG) and all detected errors.
Functions used:
RAGEngine.query()- FAISS vector search for relevant code chunksChatSession.send()- Validates input → sanitizes → retrieves RAG context → adds error context → sends to AI → validates outputAIGuardrails.validate_input()- Blocks prompt injection and forbidden topicsAIGuardrails.validate_output()- Prevents training data leaks and system prompt exposureLeakPrevention.sanitize_for_ai()- Strips env vars and sensitive data before sending to AI
loki fix fix_cmd.py
What it does: AI-powered auto-fix. Sends each fixable error to AI, extracts the suggested code fix, and optionally applies it to the file with user confirmation.
Functions used:
_extract_code_from_response()- Uses regexr'```(?:\w+)?\n(.*?)```'to extract code blocks from AI response_apply_fix_to_file()- Reads file, replaces the error line with the AI suggestion, writes back_is_fixable()- Checks if error's fixable flag is True (handles both bool and string)
Bug fix: Code extraction originally only matched Python code blocks. Fixed regex to match any language: r'```(?:\w+)?\n(.*?)```'
loki watch watch_cmd.py
What it does: Monitors filesystem for changes using watchdog. When tracked files are modified, automatically re-runs loki init.
Functions used:
watchdog.Observer()- OS-level filesystem watcherChangeHandler.on_modified()- Callback that triggers re-scan when files change
loki report report_cmd.py
What it does: Generates a Markdown report with summary stats, errors grouped by severity, errors grouped by file, and full error details.
Functions used:
collections.Counter- Groups errors by severity and file_get_severity()/_get_attr()- Handles dict/enum data from cache
Bug fix: Original code used e.severity.value which fails when severity is a string. Fixed with _get_severity() helper.
loki models models_cmd.py
What it does: Lists all available AI providers (Groq, OpenAI, Anthropic, OpenRouter) with their models and configuration status. Can set the active provider and store API key in OS keychain.
Functions used:
SecretManager.store_key()- Stores API key in OS keychain via keyringSecretManager.retrieve_key()- Retrieves API key from keychainConfigManager.set_provider()/set_model()- Saves preferences to config.json
loki capture capture_cmd.py
What it does: Runs any command and monitors its stdout/stderr for error patterns. Works with ANY programming language. Uses 65+ regex patterns to detect errors from Python, JS, Rust, C, Java, Go, etc.
Functions used:
subprocess.Popen()- Spawns the target process_monitor_stream()- Reads stdout/stderr in separate threads_is_error_line()- Tests each line against 65+ regex patterns in ERROR_PATTERNS dict_monitor_log_files()- Watches log files for error patterns using watchdog
How error patterns work: Each pattern maps a language to regex: {"python": ["Traceback", "Error:", ...], "javascript": ["TypeError:", "ReferenceError:", ...]}
loki inject inject_cmd.py
What it does: Prepends a Python exception hook into all Python files in a directory. When those files run and crash, the hook captures the error to the Loki cache.
Functions used:
HOOK_CODE- The injected Python snippet that sets upsys.excepthook- File I/O to prepend hook code to each .py file
loki exit exit_cmd.py
What it does: Securely deletes the project cache. Uses 3-pass overwriting with random data before unlinking files.
Functions used:
SecureDeleter.secure_delete_dir()- 3-pass secure deletion: overwrites with random data, then unlinksCacheManager.clear()- Deletes entire ~/.loki/{hash}/ directory
Core Module - File by File
core/types.py
Purpose: Defines ALL data structures used across the entire application. This is the foundation - every module imports from here.
Classes:
Severity(Enum)- ERROR, WARNING, INFOLanguage(Enum)- 18 languages: PYTHON, JAVASCRIPT, TYPESCRIPT, GO, RUST, C, CPP, JAVA, RUBY, PHP, SWIFT, KOTLIN, SCALA, ERLANG, HASKELL, LUA, R, OBJECTIVE_C, OBJECTIVE_CPP, UNKNOWNFileMetadata- path, hash, size, language, lines, last_modifiedScanResult- project_hash, created_at, files[], structure{}Error- file, line, column, severity, code, message, source, fixable, suggestionErrorSummary- total, errors, warnings, infoCodeChunk- file_path, start_line, end_line, content, languageChatMessage- role, content, timestamp, context[]CommandResult- success, message, data{}, error
Why: Centralized types prevent import cycles and ensure consistency. Every module uses these same structures.
core/scanner.py
Purpose: Recursively walks a project directory, identifies files by extension across 18+ languages, computes SHA256 hashes, and produces a complete scan result.
Key functions:
scan()- Main entry point. Walks files, parses metadata, returns ScanResult_walk_files()- os.walk with ignore pattern filtering. Usesfnmatch.fnmatch()to check patterns_parse_file()- Reads file, counts lines, computes SHA256 hash withhashlib.sha256()_should_ignore()- Checks against ignore patterns (node_modules, .git, __pycache__, etc.)_find_entry_points()- Identifies likely entry points (main.py, index.js, Cargo.toml, etc.)_compute_project_hash()- SHA256 hash of project path for cache directory naming
EXTENSION_MAP: Maps 35 file extensions to Language enum values. Example: {".py": Language.PYTHON, ".js": Language.JAVASCRIPT, ".rs": Language.RUST, ...}
ENTRY_POINTS: List of 27 common entry point filenames (main.py, index.js, Cargo.toml, etc.)
core/errors.py
Purpose: Multi-language error detection engine. Uses AST parsing for syntax errors and subprocess calls to linters/compilers for deeper analysis.
Detection methods:
- Python:
ast.parse()catches SyntaxError.pylint --output-format=jsonfor style/errors.mypy --output-jsonfor type errors.flake8 --format=jsonfor linting. - JavaScript/TypeScript:
node --checkfor syntax.npx eslint --format=jsonfor linting. - Go:
go vetfor static analysis - Rust:
rustc --edition 2021 --crate-type libfor syntax checking - C/C++:
gcc -fsyntax-only/g++ -fsyntax-only - Java:
javac -versionfor compilation errors - Build systems:
make -nfor dry-run build errors
Error format: Each error becomes Error(file, line, column, severity, code, message, source, fixable)
Timeouts: Pylint: 60s, Mypy: 60s, ESLint: 30s, Flake8: 30s. Limited to 5 files per linter to avoid slowdowns.
core/cache.py
Purpose: Manages encrypted cache storage in ~/.loki/{project_hash}/. All data is encrypted with Fernet before writing to disk.
Functions:
save_scan(result)- Usesdataclasses.asdict()to serialize, thenSecureCache.save_encrypted()load_scan()- Loads and decrypts, then reconstructs ScanResult from dictsave_errors(errors)- Serializes Error list with asdict()load_errors()- Deserializes back to Error objectsis_stale()- Checks if cache is older than SCAN_TTL (300 seconds)exists()- Checks if scan.json exists in cacheclear()- Delegates to SecureDeleter for 3-pass secure deletion
core/config.py
Purpose: Reads/writes ~/.loki/config.json to persist user preferences.
Stored settings: AI provider (groq/openai/anthropic/openrouter), model name, file extensions to scan, ignore patterns.
Default config: {"provider": "groq", "model": "llama-3.3-70b-versatile", "extensions": [".py", ".js", ...], "ignore": ["node_modules", ".git", ...]}
core/runtime_capture.py
Purpose: Captures runtime errors from Python processes by installing global exception hooks.
Functions:
setup_runtime_capture(cache_dir)- Replacessys.excepthookandthreading.excepthookwith custom handlerscapture_exception(type, value, tb)- Formats traceback, saves to runtime_errors.jsoncapture_console_error(msg)- Captures console.print errorsget_runtime_errors()- Loads all captured runtime errors_save_runtime_error(error_data)- Appends error to JSON file with timestamp
How it works: When Python raises an uncaught exception, sys.excepthook is called. Our hook intercepts it, formats the traceback with traceback.format_exception(), and saves it to disk.
core/global_hook.py
Purpose: Extended global exception hook with decorators and context managers for catching exceptions in any code.
Key features:
init_hook(cache_dir)- Installs the global exception hook@catch_all- Decorator that catches any exception in a function and logs it@catch_all_async- Same for async functionsExceptionLogger()- Context manager:with ExceptionLogger(): ..._patch_except()- Patches sys.excepthook at a lower level than runtime_capture
Security Layer - 5 Layers
Layer 1: secret_manager.py - API Key Storage
Purpose: Stores API keys in the OS keychain (Windows Credential Manager, macOS Keychain, Linux Secret Service). Keys are NEVER stored in files.
Functions:
store_key(provider, key)- Validates key format, stores viakeyring.set_password("loki", provider, key)retrieve_key(provider)-keyring.get_password("loki", provider)delete_key(provider)-keyring.delete_password("loki", provider)_validate_key_format(key)- Regex patterns per provider: Groq (gsk_), OpenAI (sk-), Anthropic (sk-ant-), OpenRouter (sk-or-)_update_config_hash()- Stores SHA256 hash of API key in config for tamper detection_verify_config_hash()- Verifies stored hash matches actual key
Why keyring: OS keychain is encrypted at rest, protected by user login, and never exposed as plain text files.
Layer 2: cache_security.py - Encrypted Cache
Purpose: Encrypts all cache files on disk using Fernet symmetric encryption (AES-128-CBC with HMAC-SHA256).
Functions:
save_encrypted(filename, data)- Serializes to JSON, encrypts withFernet(key).encrypt(), writes to diskload_encrypted(filename)- Reads file, decrypts withFernet(key).decrypt(), parses JSON_get_or_create_key()- Retrieves encryption key from OS keychain, or generates new one and stores it_set_restrictive_permissions()- Sets file permissions to owner-only (0o600 on Unix)
How Fernet works: Each encrypted token contains: version (1 byte) + timestamp (8 bytes) + IV (16 bytes) + ciphertext + HMAC (32 bytes). Decryption requires the same key.
Layer 3: secure_delete.py - Secure Deletion
Purpose: Prevents data recovery by overwriting files with random data before deletion.
Algorithm (3-pass):
- Pass 1: Overwrite entire file with random bytes (
os.urandom()) - Pass 2: Overwrite with different random bytes
- Pass 3: Overwrite with zeros
- Then
os.unlink()to remove the file entry
secure_delete_dir() - Recursively walks directory, secure-deletes all files, then removes empty directories.
Layer 4: leak_prevention.py - Data Sanitization
Purpose: Prevents API keys, passwords, tokens, and secrets from being sent to AI providers.
6 sensitive patterns:
- API keys:
(?:api[_-]?key|apikey)\s*[:=]\s*['"]?([a-zA-Z0-9]{20,})['"]? - Passwords:
(?:password|passwd|pwd)\s*[:=]\code> - Tokens:
(?:token|secret|bearer)\s*[:=] - AWS keys:
AKIA[0-9A-Z]{16} - Generic secrets:
(?:sk|pk|key)[_-][a-zA-Z0-9]{20,} - Connection strings:
mongodb(\+srv)?://
sanitize_for_ai() additionally removes os.environ and os.getenv calls.
Layer 5: integrity.py - Package Integrity
Purpose: Detects tampering with the installed package files.
Functions:
generate_manifest()- Walks package directory, computes SHA256 hash of every file, saves toMANIFEST_FILEverify_manifest()- Recomputes hashes and compares against stored manifest. Returns list of modified/added/deleted files.
guardrails.py - AI Safety
Purpose: Prevents prompt injection, training data leaks, and system prompt exposure.
Input validation:
- 12 injection patterns: Detects "ignore previous instructions", "you are now", "system:", "ADMIN:", etc.
- 10 forbidden topics: Blocks questions about system prompt, training data, other users, etc.
- 8 training data patterns: Blocks probing for "as an AI", "you were trained", "your training data", etc.
Output validation:
- Strips any leaked system prompt text
- Removes shell command blocks (
```bash,```sh) - Filters training data leak patterns from response
AI Engine
ai/chat.py - ChatSession
Purpose: Manages the complete chat lifecycle: input validation → sanitization → context retrieval → AI call → output filtering → history storage.
Flow of send(message):
AIGuardrails.validate_input(message)- Block injection/forbidden topicsLeakPrevention.sanitize_for_ai(message)- Strip sensitive dataget_context(sanitized)- RAG query for relevant code chunksget_error_context()- Format detected errors as contextprovider.chat(sanitized, full_context)- Send to AI with combined contextAIGuardrails.validate_output(response)- Filter leaks from AI response- Append both messages to
self.history
Context format: Each RAG chunk becomes "file_path:start_line-end_line\n{code_content}". Errors become "- file:line [severity] message".
ai/system_prompt.py
Purpose: Hardcoded system prompt with 10 strict behavioral rules for the AI.
Rules: Code-only topics, no command execution, no prompt leakage, no personal opinions, no harmful code, always explain reasoning, ask for clarification when needed, format code properly, no speculation about users, stay in character as Loki.
AI Providers
providers/base.py - AIProvider (ABC)
Purpose: Abstract base class defining the interface all AI providers must implement.
Abstract methods: chat(prompt, context) -> str, embed(text) -> list[float], validate_key(key) -> bool
providers/groq.py - GroqProvider
Purpose: Default free AI provider. Uses Groq's fast inference API.
Implementation: groq.Groq(api_key=key).chat.completions.create(model=model, messages=[{system}, {user}])
Key validation: Regex r'^gsk_[a-zA-Z0-9]{48}$'
Default model: llama-3.3-70b-versatile
System prompt: Built from system_prompt.py with additional codebase context injected.
providers/openai_provider.py - OpenAIProvider
Purpose: OpenAI API integration (optional dependency).
Implementation: openai.OpenAI(api_key=key).chat.completions.create(model=model, messages=[...])
Also supports: embeddings.create() for vector embeddings.
Key validation: Regex r'^sk-[a-zA-Z0-9]{48}$'
providers/anthropic.py - AnthropicProvider
Purpose: Anthropic Claude API integration (optional dependency).
Implementation: anthropic.Anthropic(api_key=key).messages.create(model=model, max_tokens=4096, system=system, messages=[...])
Key validation: Regex r'^sk-ant-[a-zA-Z0-9]{93}$'
providers/openrouter.py - OpenRouterProvider
Purpose: OpenRouter API integration - access to multiple models via HTTP.
Implementation: Raw HTTP POST to https://openrouter.ai/api/v1/chat/completions using httpx.
Key validation: Regex r'^sk-or-[a-zA-Z0-9]{64}$'
RAG Engine (Retrieval-Augmented Generation)
ai/rag.py - RAGEngine
Purpose: Builds a vector search index over your codebase so the AI can find and understand relevant code when answering questions.
How it works:
- Chunking:
chunk_code()splits each file into 50-line chunks with 10-line overlap. Each chunk becomes aCodeChunk(file_path, start_line, end_line, content, language) - Embedding:
SentenceTransformer("all-MiniLM-L6-v2")converts each chunk's text into a 384-dimensional vector - Indexing:
faiss.IndexFlatL2(384)stores all vectors. L2 distance for similarity search. - Saving:
faiss.write_index()saves the index.chunks.jsonstores chunk metadata. - Querying: User question → embed →
index.search(embedding, top_k=5)→ return 5 most relevant chunks
Libraries:
faiss-cpu- Facebook's vector similarity search librarysentence-transformers- Hugging Face model for text embeddingsnumpy- Array operations for FAISS
Storage: ~/.loki/{project_hash}/embeddings/index.faiss + chunks.json
Current state: 124 chunks indexed, 190KB index file, 162KB chunks.json
Web UI Backend
ui/server.py - UIServer
Purpose: FastAPI web server that serves the web UI and API endpoints.
Features:
- CORS configured for localhost only (127.0.0.1:8080)
- Serves static files from
ui/static/ - Auto-opens browser on start with
webbrowser.open() - Uses
uvicorn.run()for ASGI server
ui/routes.py - API Endpoints
Endpoints:
GET /api/files- Returns file tree with error counts per fileGET /api/errors?file=- Returns errors, optionally filtered by file pathGET /api/content?file=- Returns raw file content for code viewerPOST /api/chat- Sends message to AI, returns response with RAG contextWS /ws/chat- WebSocket for real-time chatGET /api/runtime-errors- Returns captured runtime errors
Key pattern: All functions handle both dict and dataclass objects from cache using _get_attr() helper.
ui/security.py - Rate Limiter & Sanitizer
Functions:
RateLimiter.is_allowed(ip)- In-memory per-IP rate limiting (prevents abuse)InputSanitizer.sanitize_file_path(path)- Prevents directory traversal (removes../,..\\)InputSanitizer.sanitize_message(msg)- HTML escapes, limits length to 1000 chars, removes control characters
Frontend (HTML/CSS/JS)
ui/static/index.html
Purpose: Single-page application with three-panel layout.
Layout:
- Left panel (280px): File tree sidebar + stats (files, errors, warnings)
- Center panel (1fr): Code viewer with Prism.js syntax highlighting + error list
- Right panel (360px): AI chat sidebar
External libraries (CDN):
- Three.js r128: Animated neural-graph background with 95 particles and dynamic links
- Prism.js 1.29.0: Syntax highlighting for code blocks
- Google Fonts: Space Grotesk (headings), Inter (body), JetBrains Mono (code)
Mobile support: Slide-over drawers with hamburger menu at 900px breakpoint
ui/static/style.css
Purpose: Complete dark-theme CSS with "developer HUD" aesthetic.
Key features:
- 30+ CSS custom properties for theming
- Glass-panel effects with backdrop-filter: blur
- Neon cyan (#00f0ff) and violet (#b84dff) accent colors
- Animated corner brackets (
corner-breatheanimation) - Scan-sweep gradient animation on active elements
- Holographic tilt effects on hover
- Custom scrollbar styling
- Responsive breakpoints: 1180px, 900px, 600px
ui/static/app.js
Purpose: Frontend JavaScript - fetches data from API, renders UI, handles chat.
Key functions:
loadFiles()- Fetches/api/files, renders file tree with icons and error badgesloadContent(filePath, ext)- Fetches/api/content, applies Prism.js highlightingloadAllErrors()- Fetches/api/errors, updates stats and error listsendChat()- POST to/api/chat, appends response to chat panelgetLanguageClass(ext)- Maps file extensions to Prism.js language classesescapeHtml(text)- Prevents XSS via DOM textContentPrism.highlightElement(codeElement)- Applies syntax highlighting to code blocks
CI/CD Pipelines
.github/workflows/test.yml
Purpose: Runs tests and linting on every push/PR to main.
Steps: Checkout → Setup Python (3.10/3.11/3.12 matrix) → Install deps → pytest tests/ -v --cov=loki → ruff check .
.github/workflows/publish.yml
Purpose: Auto-publishes to PyPI when a GitHub release is created.
Steps: Checkout → Setup Python → Build (python -m build) → Publish (pypa/gh-action-pypi-publish@release/v1 with trusted publishing)
Test Suite
tests/test_cache.py
Tests: test_cache_save_and_load() - Creates temp directory, saves ScanResult, loads it back, verifies data matches. test_cache_exists() - Verifies exists() returns False when no cache, True after save.
tests/test_commands.py
Tests: test_init_creates_cache() - Creates temp project with a Python file, runs execute_init(), verifies cache directory exists.
tests/test_errors.py
Tests: test_detect_syntax_errors() - Creates file with syntax error (unclosed bracket), runs ErrorDetector, verifies error is found. Creates valid file, verifies no errors.
tests/test_providers.py
Tests: test_groq_validate_key() - Verifies valid gsk_ key is accepted, invalid keys rejected.
tests/test_scanner.py
Tests: test_scanner_finds_python_files() - Creates temp dir with .py file, runs scanner, verifies file is found. test_scanner_respects_ignore_patterns() - Creates __pycache__ directory, verifies it's skipped.
tests/test_security.py
Tests: test_sanitize_api_key() - Passes text with API key, verifies it's redacted to [REDACTED]. test_sanitize_for_ai() - Verifies os.environ calls are stripped.
Bugs Found & Fixed
Bug 1: Rich Markup Error in errors_cmd.py
Error: rich.errors.MarkupError: invalid markup when displaying errors with empty severity.
Cause: Code used f"[{style}]" where style could be empty string, creating invalid Rich markup like "".
Fix: Default to "white" when severity is unrecognized: style = _get_severity(e) or "white"
Bug 2: Severity.value Error in report_cmd.py
Error: AttributeError: 'str' object has no attribute 'value' when generating reports.
Cause: Cache deserializes severity as a string (e.g., "error"), not as Severity enum. Code used e.severity.value.
Fix: Created _get_severity() helper that handles both dict and enum: if isinstance(sev, str): return sev; if hasattr(sev, 'value'): return sev.value
Bug 3: Dict vs Object Access in All Commands
Error: Commands failed with AttributeError or KeyError when accessing error/file properties.
Cause: CacheManager.load_errors() returns deserialized data. Depending on how it's loaded, objects could be dicts OR dataclass instances.
Fix: Added _get_attr(obj, attr) helper to every command: if hasattr(obj, attr): return getattr(obj, attr); if isinstance(obj, dict): return obj.get(attr, '')
Bug 4: Python-Only Code Extraction in fix_cmd.py
Error: AI responses with non-Python code blocks weren't extracted.
Cause: Regex was r'```(?:python|py)?\n(.*?)```' - only matched Python.
Fix: Changed to r'```(?:\w+)?\n(.*?)```' - matches any language code block.
Bug 5: Linter Timeouts on Large Projects
Error: loki init would hang for minutes on projects with many files.
Cause: Pylint (120s timeout, 10 files), Mypy (120s, 10 files), ESLint (120s, 10 files) were too slow.
Fix: Reduced timeouts (60s/30s), limited to 5 files per linter, added --disable=C,R to pylint to skip convention/refactor checks.
Bug 6: RAG Only Indexed Python Files
Error: AI chat had no context about JS/TS/Rust/Go files.
Cause: init_cmd.py had if file_meta.language.value == "python": filter.
Fix: Changed to if file_meta.language.value != "unknown": to index ALL languages.
Dependencies
| Package | Version | Used In | Purpose |
|---|---|---|---|
| click | >=8.0 | cli.py | CLI framework - command parsing, help text, options |
| rich | >=13.0 | All commands | Terminal formatting - tables, colors, markdown, progress bars |
| groq | >=0.4 | providers/groq.py | Groq API client (free tier) |
| openai | >=1.0 | providers/openai_provider.py | OpenAI API client (optional) |
| anthropic | >=0.20 | providers/anthropic.py | Anthropic API client (optional) |
| httpx | >=0.25 | providers/openrouter.py | HTTP client for OpenRouter API |
| faiss-cpu | >=1.7 | ai/rag.py | Vector similarity search (Facebook AI) |
| sentence-transformers | >=2.2 | ai/rag.py | Text embeddings (Hugging Face) |
| numpy | >=1.24 | ai/rag.py | Array operations for FAISS |
| cryptography | >=41.0 | security/cache_security.py | Fernet encryption (AES-128-CBC) |
| keyring | >=24.0 | security/secret_manager.py | OS keychain access |
| fastapi | >=0.104 | ui/server.py | Web API framework |
| uvicorn | >=0.24 | ui/server.py | ASGI server |
| websockets | >=12.0 | ui/routes.py | WebSocket support for real-time chat |
| watchdog | >=3.0 | commands/watch_cmd.py | Filesystem monitoring |
| hatchling | build | pyproject.toml | Package build backend |
All Files Reference
| File | Lines | Purpose | Key Functions |
|---|---|---|---|
| pyproject.toml | 45 | Package config, deps, entry point | loki entry point, hatchling build |
| loki/__init__.py | 3 | Package version | __version__, __author__ |
| loki/cli.py | 120 | CLI entry point, 11 commands | main(), init(), errors(), show(), ai(), fix(), etc. |
| loki/core/types.py | 83 | All data structures | Severity, Language, FileMetadata, ScanResult, Error, CodeChunk |
| loki/core/scanner.py | 130 | Multi-language file scanner | scan(), _walk_files(), _parse_file(), EXTENSION_MAP |
| loki/core/errors.py | 350 | Error detection for all languages | detect_all(), _check_*_syntax(), _run_*linters() |
| loki/core/cache.py | 77 | Encrypted cache manager | save_scan(), load_scan(), save_errors(), load_errors() |
| loki/core/config.py | 65 | User config persistence | get_provider(), set_provider(), get_ignore_patterns() |
| loki/core/runtime_capture.py | 90 | Runtime error capture hooks | setup_runtime_capture(), capture_exception(), get_runtime_errors() |
| loki/core/global_hook.py | 120 | Extended exception hook | init_hook(), @catch_all, ExceptionLogger |
| loki/security/secret_manager.py | 110 | OS keychain API key storage | store_key(), retrieve_key(), _validate_key_format() |
| loki/security/cache_security.py | 80 | Fernet encryption for cache | save_encrypted(), load_encrypted(), _get_or_create_key() |
| loki/security/secure_delete.py | 55 | 3-pass secure file deletion | secure_delete(), secure_delete_dir() |
| loki/security/leak_prevention.py | 70 | Secret redaction | sanitize(), sanitize_for_ai(), SENSITIVE_PATTERNS |
| loki/security/integrity.py | 50 | Package integrity verification | generate_manifest(), verify_manifest() |
| loki/ai/rag.py | 124 | FAISS vector search index | build_index(), query(), chunk_code(), _save_index() |
| loki/ai/chat.py | 87 | Chat session management | send(), get_context(), get_error_context() |
| loki/ai/guardrails.py | 150 | AI safety (injection/leak prevention) | validate_input(), validate_output(), INJECTION_PATTERNS |
| loki/ai/system_prompt.py | 30 | AI behavioral rules | SYSTEM_PROMPT constant |
| loki/ai/providers/base.py | 25 | Abstract provider interface | AIProvider(ABC), chat(), embed(), validate_key() |
| loki/ai/providers/groq.py | 60 | Groq API provider | GroqProvider, chat(), validate_key() |
| loki/ai/providers/openai_provider.py | 70 | OpenAI API provider | OpenAIProvider, chat(), embed(), validate_key() |
| loki/ai/providers/anthropic.py | 65 | Anthropic API provider | AnthropicProvider, chat(), validate_key() |
| loki/ai/providers/openrouter.py | 70 | OpenRouter API provider | OpenRouterProvider, chat(), validate_key() |
| loki/commands/init_cmd.py | 75 | loki init implementation | execute_init() |
| loki/commands/errors_cmd.py | 65 | loki errors implementation | execute_errors(), _get_severity(), _get_attr() |
| loki/commands/describe_cmd.py | 55 | loki describe implementation | execute_describe() |
| loki/commands/ai_cmd.py | 59 | loki ai implementation | execute_ai() |
| loki/commands/exit_cmd.py | 35 | loki exit implementation | execute_exit() |
| loki/commands/fix_cmd.py | 156 | loki fix implementation | execute_fix(), _extract_code_from_response(), _apply_fix_to_file() |
| loki/commands/watch_cmd.py | 50 | loki watch implementation | execute_watch(), ChangeHandler |
| loki/commands/report_cmd.py | 80 | loki report implementation | execute_report(), _get_severity(), _get_attr() |
| loki/commands/models_cmd.py | 70 | loki models implementation | execute_models(), PROVIDERS dict |
| loki/commands/capture_cmd.py | 120 | loki capture implementation | execute_capture(), _is_error_line(), ERROR_PATTERNS |
| loki/commands/inject_cmd.py | 60 | loki inject implementation | execute_inject(), HOOK_CODE |
| loki/ui/server.py | 55 | FastAPI web server | UIServer, start(), _setup_middleware() |
| loki/ui/routes.py | 132 | API endpoints | create_router(), /api/files, /api/errors, /api/chat |
| loki/ui/security.py | 60 | Rate limiter + input sanitizer | RateLimiter, InputSanitizer |
| loki/ui/static/index.html | 200 | Web UI HTML | Three.js background, three-panel layout |
| loki/ui/static/style.css | 400 | Dark theme CSS | 30+ CSS variables, animations, responsive |
| loki/ui/static/app.js | 228 | Frontend JavaScript | loadFiles(), loadContent(), sendChat() |
| .github/workflows/test.yml | 35 | CI test pipeline | pytest + ruff on Python 3.10-3.12 |
| .github/workflows/publish.yml | 30 | PyPI publish pipeline | Build + publish on release |
| tests/test_cache.py | 30 | Cache tests | test_cache_save_and_load(), test_cache_exists() |
| tests/test_commands.py | 25 | Command tests | test_init_creates_cache() |
| tests/test_errors.py | 30 | Error detection tests | test_detect_syntax_errors() |
| tests/test_providers.py | 20 | Provider tests | test_groq_validate_key() |
| tests/test_scanner.py | 30 | Scanner tests | test_scanner_finds_python_files() |
| tests/test_security.py | 25 | Security tests | test_sanitize_api_key() |